@@ -133,6 +133,9 @@ class SvgTip { | |||||
fill() { | fill() { | ||||
let title; | let title; | ||||
if(this.index) { | |||||
this.container.setAttribute('data-point-index', this.index); | |||||
} | |||||
if(this.titleValueFirst) { | if(this.titleValueFirst) { | ||||
title = `<strong>${this.titleValue}</strong>${this.titleName}`; | title = `<strong>${this.titleValue}</strong>${this.titleName}`; | ||||
} else { | } else { | ||||
@@ -179,13 +182,14 @@ class SvgTip { | |||||
} | } | ||||
} | } | ||||
setValues(x, y, titleName = '', titleValue = '', listValues = [], titleValueFirst = 0) { | |||||
this.titleName = titleName; | |||||
this.titleValue = titleValue; | |||||
setValues(x, y, title = {}, listValues = [], index = -1) { | |||||
this.titleName = title.name; | |||||
this.titleValue = title.value; | |||||
this.listValues = listValues; | this.listValues = listValues; | ||||
this.x = x; | this.x = x; | ||||
this.y = y; | this.y = y; | ||||
this.titleValueFirst = titleValueFirst; | |||||
this.titleValueFirst = title.valueFirst || 0; | |||||
this.index = index; | |||||
this.refresh(); | this.refresh(); | ||||
} | } | ||||
@@ -202,7 +206,7 @@ class SvgTip { | |||||
} | } | ||||
} | } | ||||
const VERT_SPACE_OUTSIDE_BASE_CHART = 40; | |||||
const VERT_SPACE_OUTSIDE_BASE_CHART = 50; | |||||
const TRANSLATE_Y_BASE_CHART = 20; | const TRANSLATE_Y_BASE_CHART = 20; | ||||
const LEFT_MARGIN_BASE_CHART = 60; | const LEFT_MARGIN_BASE_CHART = 60; | ||||
const RIGHT_MARGIN_BASE_CHART = 40; | const RIGHT_MARGIN_BASE_CHART = 40; | ||||
@@ -220,7 +224,7 @@ const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||||
const LINE_CHART_DOT_SIZE = 4; | const LINE_CHART_DOT_SIZE = 4; | ||||
const DOT_OVERLAY_SIZE_INCR = 4; | const DOT_OVERLAY_SIZE_INCR = 4; | ||||
const DEFAULT_CHAR_WIDTH = 8; | |||||
const DEFAULT_CHAR_WIDTH = 7; | |||||
// Universal constants | // Universal constants | ||||
const ANGLE_RATIO = Math.PI / 180; | const ANGLE_RATIO = Math.PI / 180; | ||||
@@ -574,7 +578,7 @@ function makeVertLine(x, label, y1, y2, options={}) { | |||||
dy: FONT_SIZE + 'px', | dy: FONT_SIZE + 'px', | ||||
'font-size': FONT_SIZE + 'px', | 'font-size': FONT_SIZE + 'px', | ||||
'text-anchor': 'middle', | 'text-anchor': 'middle', | ||||
innerHTML: label | |||||
innerHTML: label + "" | |||||
}); | }); | ||||
let line = createSVG('g', { | let line = createSVG('g', { | ||||
@@ -729,7 +733,7 @@ function yRegion(y1, y2, width, label) { | |||||
let labelSvg = createSVG('text', { | let labelSvg = createSVG('text', { | ||||
className: 'chart-label', | className: 'chart-label', | ||||
x: width - getStringWidth(label, 4.5) - LABEL_MARGIN, | |||||
x: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN, | |||||
y: 0, | y: 0, | ||||
dy: (FONT_SIZE / -2) + 'px', | dy: (FONT_SIZE / -2) + 'px', | ||||
'font-size': FONT_SIZE + 'px', | 'font-size': FONT_SIZE + 'px', | ||||
@@ -761,6 +765,8 @@ function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) | |||||
height: height || meta.minHeight // TODO: correct y for positive min height | height: height || meta.minHeight // TODO: correct y for positive min height | ||||
}); | }); | ||||
label += ""; | |||||
if(!label && !label.length) { | if(!label && !label.length) { | ||||
return rect; | return rect; | ||||
} else { | } else { | ||||
@@ -777,6 +783,7 @@ function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) | |||||
}); | }); | ||||
let group = createSVG('g', { | let group = createSVG('g', { | ||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | transform: `translate(${x}, ${y})` | ||||
}); | }); | ||||
group.appendChild(rect); | group.appendChild(rect); | ||||
@@ -795,6 +802,8 @@ function datasetDot(x, y, radius, color, label='', index=0, meta={}) { | |||||
r: radius | r: radius | ||||
}); | }); | ||||
label += ""; | |||||
if(!label && !label.length) { | if(!label && !label.length) { | ||||
return dot; | return dot; | ||||
} else { | } else { | ||||
@@ -812,6 +821,7 @@ function datasetDot(x, y, radius, color, label='', index=0, meta={}) { | |||||
}); | }); | ||||
let group = createSVG('g', { | let group = createSVG('g', { | ||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | transform: `translate(${x}, ${y})` | ||||
}); | }); | ||||
group.appendChild(dot); | group.appendChild(dot); | ||||
@@ -873,9 +883,10 @@ let makeOverlay = { | |||||
} | } | ||||
let overlay = unit.cloneNode(); | let overlay = unit.cloneNode(); | ||||
let radius = unit.getAttribute('r'); | let radius = unit.getAttribute('r'); | ||||
overlay.setAttribute('r', radius + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.style.fill = '#000000'; | |||||
overlay.style.opacity = '0.4'; | |||||
let fill = unit.getAttribute('fill'); | |||||
overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.setAttribute('fill', fill); | |||||
overlay.style.opacity = '0.6'; | |||||
if(transformValue) { | if(transformValue) { | ||||
overlay.setAttribute('transform', transformValue); | overlay.setAttribute('transform', transformValue); | ||||
@@ -1266,7 +1277,10 @@ class BaseChart { | |||||
setTimeout(() => {this.update();}, this.initTimeout); | setTimeout(() => {this.update();}, this.initTimeout); | ||||
} | } | ||||
this.renderLegend(); | |||||
if(!onlyWidthChange) { | |||||
this.renderLegend(); | |||||
} | |||||
this.setupNavigation(init); | this.setupNavigation(init); | ||||
} | } | ||||
@@ -1456,10 +1470,11 @@ class AggregationChart extends BaseChart { | |||||
renderLegend() { | renderLegend() { | ||||
let s = this.state; | let s = this.state; | ||||
this.statsWrapper.textContent = ''; | |||||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | ||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : s.labels; | |||||
let xValues = s.labels; | |||||
this.legendTotals.map((d, i) => { | this.legendTotals.map((d, i) => { | ||||
if(d) { | if(d) { | ||||
let stats = $.create('div', { | let stats = $.create('div', { | ||||
@@ -1468,7 +1483,7 @@ class AggregationChart extends BaseChart { | |||||
}); | }); | ||||
stats.innerHTML = `<span class="indicator"> | stats.innerHTML = `<span class="indicator"> | ||||
<i style="background: ${this.colors[i]}"></i> | <i style="background: ${this.colors[i]}"></i> | ||||
<span class="text-muted">${x_values[i]}:</span> | |||||
<span class="text-muted">${xValues[i]}:</span> | |||||
${d} | ${d} | ||||
</span>`; | </span>`; | ||||
} | } | ||||
@@ -1542,7 +1557,7 @@ class PercentageChart extends AggregationChart { | |||||
? this.formattedLabels[i] : this.state.labels[i]) + ': '; | ? this.formattedLabels[i] : this.state.labels[i]) + ': '; | ||||
let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | ||||
this.tip.setValues(x, y, title, percent + "%"); | |||||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
} | } | ||||
}); | }); | ||||
@@ -2008,7 +2023,7 @@ class PieChart extends AggregationChart { | |||||
return { | return { | ||||
sliceStrings: s.sliceStrings, | sliceStrings: s.sliceStrings, | ||||
colors: this.colors | colors: this.colors | ||||
} | |||||
}; | |||||
}.bind(this) | }.bind(this) | ||||
] | ] | ||||
]; | ]; | ||||
@@ -2038,7 +2053,7 @@ class PieChart extends AggregationChart { | |||||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | let title = (this.formatted_labels && this.formatted_labels.length > 0 | ||||
? this.formatted_labels[i] : this.state.labels[i]) + ': '; | ? this.formatted_labels[i] : this.state.labels[i]) + ': '; | ||||
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | ||||
this.tip.setValues(x, y, title, percent + "%"); | |||||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
} else { | } else { | ||||
transform(path,'translate3d(0,0,0)'); | transform(path,'translate3d(0,0,0)'); | ||||
@@ -2106,8 +2121,6 @@ function addDays(date, numberOfDays) { | |||||
date.setDate(date.getDate() + numberOfDays); | date.setDate(date.getDate() + numberOfDays); | ||||
} | } | ||||
// export function getMonthName() {} | |||||
function normalize(x) { | function normalize(x) { | ||||
// Calculates mantissa and exponent of a number | // Calculates mantissa and exponent of a number | ||||
// Returns normalized number and exponent | // Returns normalized number and exponent | ||||
@@ -2328,15 +2341,15 @@ class Heatmap extends BaseChart { | |||||
this.domain = options.domain || ''; | this.domain = options.domain || ''; | ||||
this.subdomain = options.subdomain || ''; | this.subdomain = options.subdomain || ''; | ||||
this.data = options.data || {}; | this.data = options.data || {}; | ||||
this.discrete_domains = options.discrete_domains || 1; | |||||
this.count_label = options.count_label || ''; | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||||
this.countLabel = options.countLabel || ''; | |||||
let today = new Date(); | let today = new Date(); | ||||
this.start = options.start || addDays(today, 365); | this.start = options.start || addDays(today, 365); | ||||
let legend_colors = (options.legend_colors || []).slice(0, 5); | |||||
this.legend_colors = this.validate_colors(legend_colors) | |||||
? legend_colors | |||||
let legendColors = (options.legendColors || []).slice(0, 5); | |||||
this.legendColors = this.validate_colors(legendColors) | |||||
? legendColors | |||||
: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | ||||
// Fixed 5-color theme, | // Fixed 5-color theme, | ||||
@@ -2347,6 +2360,12 @@ class Heatmap extends BaseChart { | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
setMargins() { | |||||
super.setMargins(); | |||||
this.leftMargin = 10; | |||||
this.translateY = 10; | |||||
} | |||||
validate_colors(colors) { | validate_colors(colors) { | ||||
if(colors.length < 5) return 0; | if(colors.length < 5) return 0; | ||||
@@ -2369,21 +2388,21 @@ class Heatmap extends BaseChart { | |||||
this.start = new Date(); | this.start = new Date(); | ||||
this.start.setFullYear( this.start.getFullYear() - 1 ); | this.start.setFullYear( this.start.getFullYear() - 1 ); | ||||
} | } | ||||
this.first_week_start = new Date(this.start.toDateString()); | |||||
this.last_week_start = new Date(this.today.toDateString()); | |||||
if(this.first_week_start.getDay() !== 7) { | |||||
addDays(this.first_week_start, (-1) * this.first_week_start.getDay()); | |||||
this.firstWeekStart = new Date(this.start.toDateString()); | |||||
this.lastWeekStart = new Date(this.today.toDateString()); | |||||
if(this.firstWeekStart.getDay() !== 7) { | |||||
addDays(this.firstWeekStart, (-1) * this.firstWeekStart.getDay()); | |||||
} | } | ||||
if(this.last_week_start.getDay() !== 7) { | |||||
addDays(this.last_week_start, (-1) * this.last_week_start.getDay()); | |||||
if(this.lastWeekStart.getDay() !== 7) { | |||||
addDays(this.lastWeekStart, (-1) * this.lastWeekStart.getDay()); | |||||
} | } | ||||
this.no_of_cols = getWeeksBetween(this.first_week_start + '', this.last_week_start + '') + 1; | |||||
this.no_of_cols = getWeeksBetween(this.firstWeekStart + '', this.lastWeekStart + '') + 1; | |||||
} | } | ||||
calcWidth() { | calcWidth() { | ||||
this.baseWidth = (this.no_of_cols + 3) * 12 ; | this.baseWidth = (this.no_of_cols + 3) * 12 ; | ||||
if(this.discrete_domains) { | |||||
if(this.discreteDomains) { | |||||
this.baseWidth += (12 * 12); | this.baseWidth += (12 * 12); | ||||
} | } | ||||
} | } | ||||
@@ -2397,21 +2416,20 @@ class Heatmap extends BaseChart { | |||||
'data-groups', | 'data-groups', | ||||
`translate(0, 20)` | `translate(0, 20)` | ||||
); | ); | ||||
// Array.prototype.slice.call( | |||||
// this.container.querySelectorAll('.graph-stats-container, .sub-title, .title') | |||||
// ).map(d => { | |||||
// d.style.display = 'None'; | |||||
// }); | |||||
// this.chartWrapper.style.marginTop = '0px'; | |||||
// this.chartWrapper.style.paddingTop = '0px'; | |||||
this.container.querySelector('.title').style.display = 'None'; | |||||
this.container.querySelector('.sub-title').style.display = 'None'; | |||||
this.container.querySelector('.graph-stats-container').style.display = 'None'; | |||||
this.chartWrapper.style.marginTop = '0px'; | |||||
this.chartWrapper.style.paddingTop = '0px'; | |||||
} | } | ||||
calc() { | calc() { | ||||
let data_values = Object.keys(this.data).map(key => this.data[key]); | |||||
this.distribution = calcDistribution(data_values, this.distribution_size); | |||||
let dataValues = Object.keys(this.data).map(key => this.data[key]); | |||||
this.distribution = calcDistribution(dataValues, this.distribution_size); | |||||
this.month_names = ["January", "February", "March", "April", "May", "June", | |||||
this.monthNames = ["January", "February", "March", "April", "May", "June", | |||||
"July", "August", "September", "October", "November", "December" | "July", "August", "September", "October", "November", "December" | ||||
]; | ]; | ||||
} | } | ||||
@@ -2425,118 +2443,118 @@ class Heatmap extends BaseChart { | |||||
this.domainLabelGroup.textContent = ''; | this.domainLabelGroup.textContent = ''; | ||||
this.dataGroups.textContent = ''; | this.dataGroups.textContent = ''; | ||||
let current_week_sunday = new Date(this.first_week_start); | |||||
this.week_col = 0; | |||||
this.current_month = current_week_sunday.getMonth(); | |||||
let currentWeekSunday = new Date(this.firstWeekStart); | |||||
this.weekCol = 0; | |||||
this.currentMonth = currentWeekSunday.getMonth(); | |||||
this.months = [this.current_month + '']; | |||||
this.month_weeks = {}, this.month_start_points = []; | |||||
this.month_weeks[this.current_month] = 0; | |||||
this.month_start_points.push(13); | |||||
this.months = [this.currentMonth + '']; | |||||
this.monthWeeks = {}, this.monthStartPoints = []; | |||||
this.monthWeeks[this.currentMonth] = 0; | |||||
this.monthStartPoints.push(13); | |||||
for(var i = 0; i < no_of_weeks; i++) { | for(var i = 0; i < no_of_weeks; i++) { | ||||
let data_group, month_change = 0; | |||||
let day = new Date(current_week_sunday); | |||||
[data_group, month_change] = this.get_week_squares_group(day, this.week_col); | |||||
this.dataGroups.appendChild(data_group); | |||||
this.week_col += 1 + parseInt(this.discrete_domains && month_change); | |||||
this.month_weeks[this.current_month]++; | |||||
if(month_change) { | |||||
this.current_month = (this.current_month + 1) % 12; | |||||
this.months.push(this.current_month + ''); | |||||
this.month_weeks[this.current_month] = 1; | |||||
let dataGroup, monthChange = 0; | |||||
let day = new Date(currentWeekSunday); | |||||
[dataGroup, monthChange] = this.get_week_squares_group(day, this.weekCol); | |||||
this.dataGroups.appendChild(dataGroup); | |||||
this.weekCol += 1 + parseInt(this.discreteDomains && monthChange); | |||||
this.monthWeeks[this.currentMonth]++; | |||||
if(monthChange) { | |||||
this.currentMonth = (this.currentMonth + 1) % 12; | |||||
this.months.push(this.currentMonth + ''); | |||||
this.monthWeeks[this.currentMonth] = 1; | |||||
} | } | ||||
addDays(current_week_sunday, 7); | |||||
addDays(currentWeekSunday, 7); | |||||
} | } | ||||
this.render_month_labels(); | this.render_month_labels(); | ||||
} | } | ||||
get_week_squares_group(current_date, index) { | |||||
const no_of_weekdays = 7; | |||||
const square_side = 10; | |||||
const cell_padding = 2; | |||||
get_week_squares_group(currentDate, index) { | |||||
const noOfWeekdays = 7; | |||||
const squareSide = 10; | |||||
const cellPadding = 2; | |||||
const step = 1; | const step = 1; | ||||
const today_time = this.today.getTime(); | |||||
const todayTime = this.today.getTime(); | |||||
let month_change = 0; | |||||
let week_col_change = 0; | |||||
let monthChange = 0; | |||||
let weekColChange = 0; | |||||
let data_group = makeSVGGroup(this.dataGroups, 'data-group'); | |||||
let dataGroup = makeSVGGroup(this.dataGroups, 'data-group'); | |||||
for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) { | |||||
let data_value = 0; | |||||
for(var y = 0, i = 0; i < noOfWeekdays; i += step, y += (squareSide + cellPadding)) { | |||||
let dataValue = 0; | |||||
let colorIndex = 0; | let colorIndex = 0; | ||||
let current_timestamp = current_date.getTime()/1000; | |||||
let timestamp = Math.floor(current_timestamp - (current_timestamp % 86400)).toFixed(1); | |||||
let currentTimestamp = currentDate.getTime()/1000; | |||||
let timestamp = Math.floor(currentTimestamp - (currentTimestamp % 86400)).toFixed(1); | |||||
if(this.data[timestamp]) { | if(this.data[timestamp]) { | ||||
data_value = this.data[timestamp]; | |||||
dataValue = this.data[timestamp]; | |||||
} | } | ||||
if(this.data[Math.round(timestamp)]) { | if(this.data[Math.round(timestamp)]) { | ||||
data_value = this.data[Math.round(timestamp)]; | |||||
dataValue = this.data[Math.round(timestamp)]; | |||||
} | } | ||||
if(data_value) { | |||||
colorIndex = getMaxCheckpoint(data_value, this.distribution); | |||||
if(dataValue) { | |||||
colorIndex = getMaxCheckpoint(dataValue, this.distribution); | |||||
} | } | ||||
let x = 13 + (index + week_col_change) * 12; | |||||
let x = 13 + (index + weekColChange) * 12; | |||||
let dataAttr = { | let dataAttr = { | ||||
'data-date': getDdMmYyyy(current_date), | |||||
'data-value': data_value, | |||||
'data-day': current_date.getDay() | |||||
'data-date': getDdMmYyyy(currentDate), | |||||
'data-value': dataValue, | |||||
'data-day': currentDate.getDay() | |||||
}; | }; | ||||
let heatSquare = makeHeatSquare('day', x, y, square_side, | |||||
this.legend_colors[colorIndex], dataAttr); | |||||
let heatSquare = makeHeatSquare('day', x, y, squareSide, | |||||
this.legendColors[colorIndex], dataAttr); | |||||
data_group.appendChild(heatSquare); | |||||
dataGroup.appendChild(heatSquare); | |||||
let next_date = new Date(current_date); | |||||
addDays(next_date, 1); | |||||
if(next_date.getTime() > today_time) break; | |||||
let nextDate = new Date(currentDate); | |||||
addDays(nextDate, 1); | |||||
if(nextDate.getTime() > todayTime) break; | |||||
if(next_date.getMonth() - current_date.getMonth()) { | |||||
month_change = 1; | |||||
if(this.discrete_domains) { | |||||
week_col_change = 1; | |||||
if(nextDate.getMonth() - currentDate.getMonth()) { | |||||
monthChange = 1; | |||||
if(this.discreteDomains) { | |||||
weekColChange = 1; | |||||
} | } | ||||
this.month_start_points.push(13 + (index + week_col_change) * 12); | |||||
this.monthStartPoints.push(13 + (index + weekColChange) * 12); | |||||
} | } | ||||
current_date = next_date; | |||||
currentDate = nextDate; | |||||
} | } | ||||
return [data_group, month_change]; | |||||
return [dataGroup, monthChange]; | |||||
} | } | ||||
render_month_labels() { | render_month_labels() { | ||||
// this.first_month_label = 1; | // this.first_month_label = 1; | ||||
// if (this.first_week_start.getDate() > 8) { | |||||
// if (this.firstWeekStart.getDate() > 8) { | |||||
// this.first_month_label = 0; | // this.first_month_label = 0; | ||||
// } | // } | ||||
// this.last_month_label = 1; | // this.last_month_label = 1; | ||||
// let first_month = this.months.shift(); | // let first_month = this.months.shift(); | ||||
// let first_month_start = this.month_start_points.shift(); | |||||
// let first_month_start = this.monthStartPoints.shift(); | |||||
// render first month if | // render first month if | ||||
// let last_month = this.months.pop(); | // let last_month = this.months.pop(); | ||||
// let last_month_start = this.month_start_points.pop(); | |||||
// let last_month_start = this.monthStartPoints.pop(); | |||||
// render last month if | // render last month if | ||||
this.months.shift(); | this.months.shift(); | ||||
this.month_start_points.shift(); | |||||
this.monthStartPoints.shift(); | |||||
this.months.pop(); | this.months.pop(); | ||||
this.month_start_points.pop(); | |||||
this.monthStartPoints.pop(); | |||||
this.month_start_points.map((start, i) => { | |||||
let month_name = this.month_names[this.months[i]].substring(0, 3); | |||||
this.monthStartPoints.map((start, i) => { | |||||
let month_name = this.monthNames[this.months[i]].substring(0, 3); | |||||
let text = makeText('y-value-text', start+12, 10, month_name); | let text = makeText('y-value-text', start+12, 10, month_name); | ||||
this.domainLabelGroup.appendChild(text); | this.domainLabelGroup.appendChild(text); | ||||
}); | }); | ||||
@@ -2548,19 +2566,19 @@ class Heatmap extends BaseChart { | |||||
).map(el => { | ).map(el => { | ||||
el.addEventListener('mouseenter', (e) => { | el.addEventListener('mouseenter', (e) => { | ||||
let count = e.target.getAttribute('data-value'); | let count = e.target.getAttribute('data-value'); | ||||
let date_parts = e.target.getAttribute('data-date').split('-'); | |||||
let dateParts = e.target.getAttribute('data-date').split('-'); | |||||
let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3); | |||||
let month = this.monthNames[parseInt(dateParts[1])-1].substring(0, 3); | |||||
let g_off = this.chartWrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect(); | |||||
let gOff = this.chartWrapper.getBoundingClientRect(), pOff = e.target.getBoundingClientRect(); | |||||
let width = parseInt(e.target.getAttribute('width')); | let width = parseInt(e.target.getAttribute('width')); | ||||
let x = p_off.left - g_off.left + (width+2)/2; | |||||
let y = p_off.top - g_off.top - (width+2)/2; | |||||
let value = count + ' ' + this.count_label; | |||||
let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2]; | |||||
let x = pOff.left - gOff.left + (width+2)/2; | |||||
let y = pOff.top - gOff.top - (width+2)/2; | |||||
let value = count + ' ' + this.countLabel; | |||||
let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2]; | |||||
this.tip.setValues(x, y, name, value, [], 1); | |||||
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
}); | }); | ||||
}); | }); | ||||
@@ -2714,7 +2732,7 @@ class AxisChart extends BaseChart { | |||||
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | ||||
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | ||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 1; | |||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 0; | |||||
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | ||||
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | ||||
@@ -2810,7 +2828,7 @@ class AxisChart extends BaseChart { | |||||
return; | return; | ||||
} | } | ||||
s.yExtremes = new Array(s.datasetLength).fill(9999); | s.yExtremes = new Array(s.datasetLength).fill(9999); | ||||
s.datasets.map((d, i) => { | |||||
s.datasets.map(d => { | |||||
d.yPositions.map((pos, j) => { | d.yPositions.map((pos, j) => { | ||||
if(pos < s.yExtremes[j]) { | if(pos < s.yExtremes[j]) { | ||||
s.yExtremes[j] = pos; | s.yExtremes[j] = pos; | ||||
@@ -2824,9 +2842,9 @@ class AxisChart extends BaseChart { | |||||
if(this.data.yMarkers) { | if(this.data.yMarkers) { | ||||
this.state.yMarkers = this.data.yMarkers.map(d => { | this.state.yMarkers = this.data.yMarkers.map(d => { | ||||
d.position = scale(d.value, s.yAxis); | d.position = scale(d.value, s.yAxis); | ||||
if(!d.label.includes(':')) { | |||||
d.label += ': ' + d.value; | |||||
} | |||||
// if(!d.label.includes(':')) { | |||||
// d.label += ': ' + d.value; | |||||
// } | |||||
return d; | return d; | ||||
}); | }); | ||||
} | } | ||||
@@ -2928,7 +2946,7 @@ class AxisChart extends BaseChart { | |||||
} | } | ||||
let labels = new Array(s.datasetLength).fill(''); | let labels = new Array(s.datasetLength).fill(''); | ||||
if(this.valuesOverPoints) { | |||||
if(this.config.valuesOverPoints) { | |||||
if(stacked && d.index === s.datasets.length - 1) { | if(stacked && d.index === s.datasets.length - 1) { | ||||
labels = d.cumulativeYs; | labels = d.cumulativeYs; | ||||
} else { | } else { | ||||
@@ -3037,12 +3055,15 @@ class AxisChart extends BaseChart { | |||||
let s = this.state; | let s = this.state; | ||||
if(!s.yExtremes) return; | if(!s.yExtremes) return; | ||||
let formatY = this.config.formatTooltipY; | |||||
let formatX = this.config.formatTooltipX; | |||||
let titles = s.xAxis.labels; | let titles = s.xAxis.labels; | ||||
if(this.formatTooltipX && this.formatTooltipX(titles[0])) { | |||||
titles = titles.map(d=>this.formatTooltipX(d)); | |||||
if(formatX && formatX(titles[0])) { | |||||
titles = titles.map(d=>formatX(d)); | |||||
} | } | ||||
let formatY = this.formatTooltipY && this.formatTooltipY(this.y[0].values[0]); | |||||
formatY = formatY && formatY(s.yAxis.labels[0]) ? formatY : 0; | |||||
for(var i=s.datasetLength - 1; i >= 0 ; i--) { | for(var i=s.datasetLength - 1; i >= 0 ; i--) { | ||||
let xVal = s.xAxis.positions[i]; | let xVal = s.xAxis.positions[i]; | ||||
@@ -3053,19 +3074,37 @@ class AxisChart extends BaseChart { | |||||
let values = this.data.datasets.map((set, j) => { | let values = this.data.datasets.map((set, j) => { | ||||
return { | return { | ||||
title: set.title, | |||||
value: formatY ? this.formatTooltipY(set.values[i]) : set.values[i], | |||||
title: set.name, | |||||
value: formatY ? formatY(set.values[i]) : set.values[i], | |||||
color: this.colors[j], | color: this.colors[j], | ||||
}; | }; | ||||
}); | }); | ||||
this.tip.setValues(x, y, titles[i], '', values); | |||||
this.tip.setValues(x, y, {name: titles[i], value: ''}, values, i); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
renderLegend() { | |||||
let s = this.data; | |||||
this.statsWrapper.textContent = ''; | |||||
if(s.datasets.length > 1) { | |||||
s.datasets.map((d, i) => { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background: ${this.colors[i]}"></i> | |||||
${d.name} | |||||
</span>`; | |||||
}); | |||||
} | |||||
} | |||||
makeOverlay() { | makeOverlay() { | ||||
if(this.overlayGuides) { | if(this.overlayGuides) { | ||||
this.overlayGuides.forEach(g => { | this.overlayGuides.forEach(g => { | ||||
@@ -3079,7 +3118,7 @@ class AxisChart extends BaseChart { | |||||
type: c.unitType, | type: c.unitType, | ||||
overlay: undefined, | overlay: undefined, | ||||
units: c.units, | units: c.units, | ||||
} | |||||
}; | |||||
}); | }); | ||||
if(this.state.currentIndex === undefined) { | if(this.state.currentIndex === undefined) { | ||||
@@ -3105,19 +3144,26 @@ class AxisChart extends BaseChart { | |||||
} | } | ||||
bindOverlay() { | bindOverlay() { | ||||
// on event, update overlay | |||||
this.parent.addEventListener('data-select', (e) => { | |||||
this.parent.addEventListener('data-select', () => { | |||||
this.updateOverlay(); | this.updateOverlay(); | ||||
}); | }); | ||||
} | } | ||||
bindUnits(units_array) { | |||||
// units_array.map(unit => { | |||||
// unit.addEventListener('click', () => { | |||||
// let index = unit.getAttribute('data-point-index'); | |||||
// this.setCurrentDataPoint(index); | |||||
// }); | |||||
// }); | |||||
bindUnits() { | |||||
this.dataUnitComponents.map(c => { | |||||
c.units.map(unit => { | |||||
unit.addEventListener('click', () => { | |||||
let index = unit.getAttribute('data-point-index'); | |||||
this.setCurrentDataPoint(index); | |||||
}); | |||||
}); | |||||
}); | |||||
// Note: Doesn't work as tooltip is absolutely positioned | |||||
this.tip.container.addEventListener('click', () => { | |||||
let index = this.tip.container.getAttribute('data-point-index'); | |||||
this.setCurrentDataPoint(index); | |||||
}); | |||||
} | } | ||||
updateOverlay() { | updateOverlay() { | ||||
@@ -3136,16 +3182,12 @@ class AxisChart extends BaseChart { | |||||
} | } | ||||
getDataPoint(index=this.state.currentIndex) { | getDataPoint(index=this.state.currentIndex) { | ||||
// check for length | |||||
let s = this.state; | |||||
let data_point = { | let data_point = { | ||||
index: index | |||||
index: index, | |||||
label: s.xAxis.labels[index], | |||||
values: s.datasets.map(d => d.values[index]) | |||||
}; | }; | ||||
// let y = this.y[0]; | |||||
// ['svg_units', 'yUnitPositions', 'values'].map(key => { | |||||
// let data_key = key.slice(0, key.length-1); | |||||
// data_point[data_key] = y[key][index]; | |||||
// }); | |||||
// data_point.label = this.xAxis.labels[index]; | |||||
return data_point; | return data_point; | ||||
} | } | ||||
@@ -3186,7 +3228,14 @@ class AxisChart extends BaseChart { | |||||
// addDataset(dataset, index) {} | // addDataset(dataset, index) {} | ||||
// removeDataset(index = 0) {} | // removeDataset(index = 0) {} | ||||
// updateDatasets(datasets) {} | |||||
updateDatasets(datasets) { | |||||
this.data.datasets.map((d, i) => { | |||||
if(datasets[i]) { | |||||
d.values = datasets[i]; | |||||
} | |||||
}); | |||||
this.update(this.data); | |||||
} | |||||
// updateDataPoint(dataPoint, index = 0) {} | // updateDataPoint(dataPoint, index = 0) {} | ||||
// addDataPoint(dataPoint, index = 0) {} | // addDataPoint(dataPoint, index = 0) {} | ||||
@@ -1 +1 @@ | |||||
.chart-container{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .graph-focus-margin{margin:0 5%}.chart-container>.title{margin-top:25px;margin-left:25px;text-align:left;font-weight:400;font-size:12px;color:#6c7680}.chart-container .graphics{margin-top:10px;padding-top:10px;padding-bottom:10px;position:relative}.chart-container .graph-stats-group{-ms-flex-pack:distribute;-webkit-box-flex:1;-ms-flex:1;flex:1}.chart-container .graph-stats-container,.chart-container .graph-stats-group{display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:space-around}.chart-container .graph-stats-container{-ms-flex-pack:distribute;padding-top:10px}.chart-container .graph-stats-container .stats{padding-bottom:15px}.chart-container .graph-stats-container .stats-title{color:#8d99a6}.chart-container .graph-stats-container .stats-value{font-size:20px;font-weight:300}.chart-container .graph-stats-container .stats-description{font-size:12px;color:#8d99a6}.chart-container .graph-stats-container .graph-data .stats-value{color:#98d85b}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .percentage-graph .progress{margin-bottom:0}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path,.chart-container .multiaxis-chart .line-horizontal,.chart-container .multiaxis-chart .y-axis-guide{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.chart-container .progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#36414c;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.chart-container .graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.chart-container .graph-svg-tip ol,.chart-container .graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.chart-container .graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.chart-container .graph-svg-tip strong{color:#dfe2e5;font-weight:600}.chart-container .graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.chart-container .graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.chart-container .graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.chart-container .graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.chart-container .graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}.chart-container .indicator,.chart-container .indicator-right{background:none;font-size:12px;vertical-align:middle;font-weight:700;color:#6c7680}.chart-container .indicator i{content:"";display:inline-block;height:8px;width:8px;border-radius:8px}.chart-container .indicator:before,.chart-container .indicator i{margin:0 4px 0 0}.chart-container .indicator-right:after{margin:0 0 0 4px} | |||||
.chart-container{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}.chart-container .graph-focus-margin{margin:0 5%}.chart-container>.title{margin-top:25px;margin-left:25px;text-align:left;font-weight:400;font-size:12px;color:#6c7680}.chart-container .graphics{margin-top:10px;padding-top:10px;padding-bottom:10px;position:relative}.chart-container .graph-stats-group{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-pack:distribute;justify-content:space-around;-webkit-box-flex:1;-ms-flex:1;flex:1}.chart-container .graph-stats-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:10px}.chart-container .graph-stats-container:after,.chart-container .graph-stats-container:before{content:"";display:block}.chart-container .graph-stats-container .stats{padding-bottom:15px}.chart-container .graph-stats-container .stats-title{color:#8d99a6}.chart-container .graph-stats-container .stats-value{font-size:20px;font-weight:300}.chart-container .graph-stats-container .stats-description{font-size:12px;color:#8d99a6}.chart-container .graph-stats-container .graph-data .stats-value{color:#98d85b}.chart-container .axis,.chart-container .chart-label{fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .percentage-graph .progress{margin-bottom:0}.chart-container .dataset-units circle{stroke:#fff;stroke-width:2}.chart-container .dataset-units path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container .dataset-path,.chart-container .multiaxis-chart .line-horizontal,.chart-container .multiaxis-chart .y-axis-guide{stroke-width:2px}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .axis-line .specific-value{text-anchor:start}.chart-container .axis-line .y-line{text-anchor:end}.chart-container .axis-line .x-line{text-anchor:middle}.chart-container .progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.chart-container .progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#36414c;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.chart-container .graph-svg-tip{position:absolute;z-index:1;padding:10px;font-size:12px;color:#959da5;text-align:center;background:rgba(0,0,0,.8);border-radius:3px}.chart-container .graph-svg-tip ol,.chart-container .graph-svg-tip ul{padding-left:0;display:-webkit-box;display:-ms-flexbox;display:flex}.chart-container .graph-svg-tip ul.data-point-list li{min-width:90px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-weight:600}.chart-container .graph-svg-tip strong{color:#dfe2e5;font-weight:600}.chart-container .graph-svg-tip .svg-pointer{position:absolute;height:5px;margin:0 0 0 -5px;content:" ";border:5px solid transparent;border-top-color:rgba(0,0,0,.8)}.chart-container .graph-svg-tip.comparison{padding:0;text-align:left;pointer-events:none}.chart-container .graph-svg-tip.comparison .title{display:block;padding:10px;margin:0;font-weight:600;line-height:1;pointer-events:none}.chart-container .graph-svg-tip.comparison ul{margin:0;white-space:nowrap;list-style:none}.chart-container .graph-svg-tip.comparison li{display:inline-block;padding:5px 10px}.chart-container .indicator,.chart-container .indicator-right{background:none;font-size:12px;vertical-align:middle;font-weight:700;color:#6c7680}.chart-container .indicator i{content:"";display:inline-block;height:8px;width:8px;border-radius:8px}.chart-container .indicator:before,.chart-container .indicator i{margin:0 4px 0 0}.chart-container .indicator-right:after{margin:0 0 0 4px} |
@@ -1,76 +1,100 @@ | |||||
// Composite Chart | // Composite Chart | ||||
// ================================================================================ | // ================================================================================ | ||||
let report_count_list = [17, 40, 33, 44, 126, 156, | |||||
324, 333, 478, 495, 527]; | |||||
let reportCountList = [152, 222, 199, 287, 534, 709, | |||||
1179, 1256, 1632, 1856, 1850]; | |||||
let bar_composite_data = { | |||||
let lineCompositeData = { | |||||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", | labels: ["2007", "2008", "2009", "2010", "2011", "2012", | ||||
"2013", "2014", "2015", "2016", "2017"], | "2013", "2014", "2015", "2016", "2017"], | ||||
yMarkers: [ | yMarkers: [ | ||||
{ | { | ||||
label: "Marker 1", | |||||
value: 420, | |||||
}, | |||||
{ | |||||
label: "Marker 2", | |||||
value: 250, | |||||
label: "Average 100 reports/month", | |||||
value: 1200, | |||||
} | } | ||||
], | ], | ||||
yRegions: [ | |||||
{ | |||||
label: "Region Y 1", | |||||
start: 100, | |||||
end: 300 | |||||
}, | |||||
], | |||||
datasets: [{ | datasets: [{ | ||||
"name": "Events", | "name": "Events", | ||||
"values": report_count_list, | |||||
// "formatted": report_count_list.map(d => d + " reports") | |||||
"values": reportCountList | |||||
}] | }] | ||||
}; | }; | ||||
let line_composite_data = { | |||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||||
datasets: [{ | |||||
"values": [37, 36, 32, 33, 12, 34, 52, 45, 58, 57, 64, 35], | |||||
// "values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40], | |||||
// "values": [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40], | |||||
}] | |||||
}; | |||||
let more_line_data = [ | |||||
[4, 0, 3, 1, 1, 2, 1, 2, 1, 0, 1, 1], | |||||
let fireball_5_25 = [ | |||||
[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1], | |||||
[2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4], | |||||
[5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1], | |||||
[0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8], | |||||
[6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11], | |||||
[7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20], | |||||
[13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29], | |||||
[24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18], | |||||
[31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24], | |||||
[32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32], | |||||
[31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50], | |||||
]; | |||||
let fireball_2_5 = [ | |||||
[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20], | |||||
[11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24], | |||||
[20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9], | |||||
[7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33], | |||||
[38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53], | |||||
[50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69], | |||||
[54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87], | |||||
[84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93], | |||||
[73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89], | |||||
[106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156], | |||||
[81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171], | |||||
]; | |||||
let fireballOver25 = [ | |||||
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | ||||
[2, 3, 3, 2, 1, 4, 0, 1, 2, 7, 11, 4], | |||||
[7, 7, 2, 4, 0, 1, 5, 3, 1, 2, 0, 1], | |||||
[0, 2, 6, 2, 2, 1, 2, 3, 6, 3, 7, 10], | |||||
[9, 10, 8, 10, 6, 5, 8, 8, 24, 15, 10, 13], | |||||
[9, 13, 16, 9, 4, 5, 7, 10, 14, 22, 23, 24], | |||||
[20, 22, 28, 19, 28, 19, 14, 19, 51, 37, 29, 38], | |||||
[29, 20, 22, 16, 16, 19, 24, 26, 57, 31, 46, 27], | |||||
[36, 24, 38, 27, 15, 22, 24, 38, 32, 57, 139, 26], | |||||
[37, 36, 32, 33, 12, 34, 52, 45, 58, 57, 64, 35], | |||||
[36, 46, 45, 32, 27, 31, 30, 36, 39, 58, 82, 62], | |||||
// [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40] | |||||
// [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40] | |||||
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], | |||||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0], | |||||
[1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0], | |||||
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2], | |||||
[3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1], | |||||
[2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4], | |||||
[7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9], | |||||
[5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9], | |||||
[5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2], | |||||
[5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3], | |||||
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12] | |||||
]; | ]; | ||||
let monthNames = ["January", "February", "March", "April", "May", "June", | |||||
"July", "August", "September", "October", "November", "December"]; | |||||
let barCompositeData = { | |||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||||
datasets: [ | |||||
{ | |||||
name: "Over 25 reports", | |||||
values: fireballOver25[9], | |||||
}, | |||||
{ | |||||
name: "5 to 25 reports", | |||||
values: fireball_5_25[9], | |||||
}, | |||||
{ | |||||
name: "2 to 5 reports", | |||||
values: fireball_2_5[9] | |||||
} | |||||
] | |||||
}; | |||||
let c1 = document.querySelector("#chart-composite-1"); | let c1 = document.querySelector("#chart-composite-1"); | ||||
let c2 = document.querySelector("#chart-composite-2"); | let c2 = document.querySelector("#chart-composite-2"); | ||||
let bar_composite_chart = new Chart (c1, { | |||||
let lineCompositeChart = new Chart (c1, { | |||||
title: "Fireball/Bolide Events - Yearly (reported)", | title: "Fireball/Bolide Events - Yearly (reported)", | ||||
data: bar_composite_data, | |||||
data: lineCompositeData, | |||||
type: 'line', | type: 'line', | ||||
height: 180, | |||||
height: 190, | |||||
colors: ['green'], | colors: ['green'], | ||||
isNavigable: 1, | isNavigable: 1, | ||||
isSeries: 1, | isSeries: 1, | ||||
// valuesOverPoints: 1, | |||||
valuesOverPoints: 1, | |||||
lineOptions: { | lineOptions: { | ||||
dotSize: 8 | dotSize: 8 | ||||
@@ -79,25 +103,33 @@ let bar_composite_chart = new Chart (c1, { | |||||
// regionFill: 1 | // regionFill: 1 | ||||
}); | }); | ||||
let line_composite_chart = new Chart (c2, { | |||||
data: line_composite_data, | |||||
let barCompositeChart = new Chart (c2, { | |||||
data: barCompositeData, | |||||
type: 'bar', | type: 'bar', | ||||
height: 180, | |||||
colors: ['#46a9f9'], | |||||
height: 190, | |||||
colors: ['violet', 'light-blue', '#46a9f9'], | |||||
isSeries: 1, | isSeries: 1, | ||||
valuesOverPoints: 1, | valuesOverPoints: 1, | ||||
xAxisMode: 'tick' | |||||
axisOptions: { | |||||
xAxisMode: 'tick' | |||||
}, | |||||
barOptions: { | |||||
stacked: 1 | |||||
}, | |||||
}); | }); | ||||
bar_composite_chart.parent.addEventListener('data-select', (e) => { | |||||
line_composite_chart.updateDataset(more_line_data[e.index]); | |||||
lineCompositeChart.parent.addEventListener('data-select', (e) => { | |||||
let i = e.index; | |||||
barCompositeChart.updateDatasets([ | |||||
fireballOver25[i], fireball_5_25[i], fireball_2_5[i] | |||||
]); | |||||
}); | }); | ||||
// Demo Chart (bar, linepts, scatter(blobs), percentage) | // Demo Chart (bar, linepts, scatter(blobs), percentage) | ||||
// ================================================================================ | // ================================================================================ | ||||
let type_data = { | |||||
let typeData = { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | ||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | ||||
@@ -166,15 +198,13 @@ let type_data = { | |||||
] | ] | ||||
}; | }; | ||||
let type_chart = new Chart("#chart-types", { | |||||
// title: "My Awesome Chart", | |||||
data: type_data, | |||||
let typeChart = new Chart("#chart-types", { | |||||
title: "My Awesome Chart", | |||||
data: typeData, | |||||
type: 'bar', | type: 'bar', | ||||
height: 250, | height: 250, | ||||
colors: ['purple', 'magenta', 'light-blue'], | colors: ['purple', 'magenta', 'light-blue'], | ||||
isSeries: 1, | isSeries: 1, | ||||
xAxisMode: 'tick', | |||||
yAxisMode: 'span', | |||||
valuesOverPoints: 1, | valuesOverPoints: 1, | ||||
// maxLegendPoints: 6, | // maxLegendPoints: 6, | ||||
// maxSlices: 3, | // maxSlices: 3, | ||||
@@ -184,86 +214,45 @@ let type_chart = new Chart("#chart-types", { | |||||
}, | }, | ||||
tooltipOptions: { | tooltipOptions: { | ||||
formatTooltipX: d => (d + '').toUpperCase(), | formatTooltipX: d => (d + '').toUpperCase(), | ||||
formatTooltipY: d => d + ' pts' | |||||
formatTooltipY: d => d + ' pts', | |||||
} | } | ||||
}); | }); | ||||
Array.prototype.slice.call( | |||||
document.querySelectorAll('.chart-type-buttons button') | |||||
).map(el => { | |||||
el.addEventListener('click', (e) => { | |||||
let btn = e.target; | |||||
let type = btn.getAttribute('data-type'); | |||||
let newChart = type_chart.getDifferentChart(type); | |||||
if(newChart){ | |||||
type_chart = newChart; | |||||
} | |||||
Array.prototype.slice.call( | |||||
btn.parentNode.querySelectorAll('button')).map(el => { | |||||
el.classList.remove('active'); | |||||
}); | |||||
btn.classList.add('active'); | |||||
}); | |||||
}); | |||||
// Trends Chart | |||||
// ================================================================================ | |||||
let trends_data = { | |||||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, | |||||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, | |||||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, | |||||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, | |||||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] , | |||||
datasets: [ | |||||
{ | |||||
"values": [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, | |||||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, | |||||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, | |||||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, | |||||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||||
} | |||||
] | |||||
}; | |||||
let plotChartArgs = { | |||||
title: "Mean Total Sunspot Count - Yearly", | |||||
data: trends_data, | |||||
type: 'line', | |||||
// Aggregation chart | |||||
// ================================================================================ | |||||
let aggrChart = new Chart("#chart-aggr", { | |||||
data: typeData, | |||||
type: 'pie', | |||||
height: 250, | height: 250, | ||||
colors: ['blue'], | |||||
colors: ['purple', 'magenta', 'light-blue'], | |||||
isSeries: 1, | isSeries: 1, | ||||
lineOptions: { | |||||
hideDots: 1, | |||||
heatline: 1, | |||||
maxLegendPoints: 6, | |||||
maxSlices: 10, | |||||
barOptions: { | |||||
stacked: 1 | |||||
}, | }, | ||||
axisOptions: { | |||||
xAxisMode: 'tick', | |||||
yAxisMode: 'span', | |||||
xIsSeries: 1 | |||||
tooltipOptions: { | |||||
formatTooltipX: d => (d + '').toUpperCase(), | |||||
formatTooltipY: d => d + ' pts', | |||||
} | } | ||||
}; | |||||
new Chart("#chart-trends", plotChartArgs); | |||||
}); | |||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
document.querySelectorAll('.chart-plot-buttons button') | |||||
document.querySelectorAll('.aggr-type-buttons button') | |||||
).map(el => { | ).map(el => { | ||||
el.addEventListener('click', (e) => { | el.addEventListener('click', (e) => { | ||||
let btn = e.target; | let btn = e.target; | ||||
let type = btn.getAttribute('data-type'); | let type = btn.getAttribute('data-type'); | ||||
let config = {}; | |||||
config[type] = 1; | |||||
if(['regionFill', 'heatline'].includes(type)) { | |||||
config.hideDots = 1; | |||||
let newChart = aggrChart.getDifferentChart(type); | |||||
if(newChart){ | |||||
aggrChart = newChart; | |||||
} | } | ||||
// plotChartArgs.init = false; | |||||
plotChartArgs.lineOptions = config; | |||||
new Chart("#chart-trends", plotChartArgs); | |||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
btn.parentNode.querySelectorAll('button')).map(el => { | btn.parentNode.querySelectorAll('button')).map(el => { | ||||
el.classList.remove('active'); | el.classList.remove('active'); | ||||
@@ -337,6 +326,71 @@ chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("c | |||||
update_chart.removeDataPoint(); | update_chart.removeDataPoint(); | ||||
}); | }); | ||||
// Trends Chart | |||||
// ================================================================================ | |||||
let trends_data = { | |||||
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, | |||||
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, | |||||
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, | |||||
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, | |||||
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] , | |||||
datasets: [ | |||||
{ | |||||
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4, | |||||
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8, | |||||
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6, | |||||
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7, | |||||
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8] | |||||
} | |||||
] | |||||
}; | |||||
let plotChartArgs = { | |||||
title: "Mean Total Sunspot Count - Yearly", | |||||
data: trends_data, | |||||
type: 'line', | |||||
height: 250, | |||||
colors: ['blue'], | |||||
isSeries: 1, | |||||
lineOptions: { | |||||
hideDots: 1, | |||||
heatline: 1, | |||||
}, | |||||
axisOptions: { | |||||
xAxisMode: 'tick', | |||||
yAxisMode: 'span', | |||||
xIsSeries: 1 | |||||
} | |||||
}; | |||||
new Chart("#chart-trends", plotChartArgs); | |||||
Array.prototype.slice.call( | |||||
document.querySelectorAll('.chart-plot-buttons button') | |||||
).map(el => { | |||||
el.addEventListener('click', (e) => { | |||||
let btn = e.target; | |||||
let type = btn.getAttribute('data-type'); | |||||
let config = {}; | |||||
config[type] = 1; | |||||
if(['regionFill', 'heatline'].includes(type)) { | |||||
config.hideDots = 1; | |||||
} | |||||
// plotChartArgs.init = false; | |||||
plotChartArgs.lineOptions = config; | |||||
new Chart("#chart-trends", plotChartArgs); | |||||
Array.prototype.slice.call( | |||||
btn.parentNode.querySelectorAll('button')).map(el => { | |||||
el.classList.remove('active'); | |||||
}); | |||||
btn.classList.add('active'); | |||||
}); | |||||
}); | |||||
// Event chart | // Event chart | ||||
// ================================================================================ | // ================================================================================ | ||||
@@ -398,28 +452,24 @@ events_chart.parent.addEventListener('data-select', (e) => { | |||||
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | ||||
}); | }); | ||||
// Aggregation chart | |||||
// ================================================================================ | |||||
// Heatmap | // Heatmap | ||||
// ================================================================================ | // ================================================================================ | ||||
let heatmap_data = {}; | |||||
let heatmapData = {}; | |||||
let current_date = new Date(); | let current_date = new Date(); | ||||
let timestamp = current_date.getTime()/1000; | let timestamp = current_date.getTime()/1000; | ||||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | ||||
for (var i = 0; i< 375; i++) { | for (var i = 0; i< 375; i++) { | ||||
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||||
heatmapData[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||||
timestamp = Math.floor(timestamp - 86400).toFixed(1); | timestamp = Math.floor(timestamp - 86400).toFixed(1); | ||||
} | } | ||||
new Chart("#chart-heatmap", { | new Chart("#chart-heatmap", { | ||||
data: heatmap_data, | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: 1 | |||||
discreteDomains: 1 | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -428,10 +478,10 @@ Array.prototype.slice.call( | |||||
el.addEventListener('click', (e) => { | el.addEventListener('click', (e) => { | ||||
let btn = e.target; | let btn = e.target; | ||||
let mode = btn.getAttribute('data-mode'); | let mode = btn.getAttribute('data-mode'); | ||||
let discrete_domains = 0; | |||||
let discreteDomains = 0; | |||||
if(mode === 'discrete') { | if(mode === 'discrete') { | ||||
discrete_domains = 1; | |||||
discreteDomains = 1; | |||||
} | } | ||||
let colors = []; | let colors = []; | ||||
@@ -443,12 +493,12 @@ Array.prototype.slice.call( | |||||
} | } | ||||
new Chart("#chart-heatmap", { | new Chart("#chart-heatmap", { | ||||
data: heatmap_data, | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: discrete_domains, | |||||
legend_colors: colors | |||||
discreteDomains: discreteDomains, | |||||
legendColors: colors | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -471,22 +521,22 @@ Array.prototype.slice.call( | |||||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | ||||
} | } | ||||
let discrete_domains = 1; | |||||
let discreteDomains = 1; | |||||
let view_mode = document | let view_mode = document | ||||
.querySelector('.heatmap-mode-buttons .active') | .querySelector('.heatmap-mode-buttons .active') | ||||
.getAttribute('data-mode'); | .getAttribute('data-mode'); | ||||
if(view_mode === 'continuous') { | if(view_mode === 'continuous') { | ||||
discrete_domains = 0; | |||||
discreteDomains = 0; | |||||
} | } | ||||
new Chart("#chart-heatmap", { | new Chart("#chart-heatmap", { | ||||
data: heatmap_data, | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: discrete_domains, | |||||
legend_colors: colors | |||||
discreteDomains: discreteDomains, | |||||
legendColors: colors | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -44,67 +44,51 @@ | |||||
<div class="col-sm-10 push-sm-1"> | <div class="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<h6 class="margin-vertical-rem"> | |||||
<!--Bars, Lines or <a href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts" target="_blank">Percentages</a>--> | |||||
Create a chart | |||||
</h6> | |||||
<p class="step-explain">Install</p> | |||||
<pre><code class="hljs console"> npm install frappe-charts</code></pre> | |||||
<p class="step-explain">And include it in your project</p> | |||||
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre> | |||||
<p class="step-explain">... or include it directly in your HTML</p> | |||||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"></script></code></pre> | |||||
<p class="step-explain">Make a new Chart</p> | |||||
<h6 class="margin-vertical-rem">Create a chart</h6> | |||||
<pre><code class="hljs html"> <!--HTML--> | <pre><code class="hljs html"> <!--HTML--> | ||||
<div id="chart"></div></code></pre> | <div id="chart"></div></code></pre> | ||||
<pre><code class="hljs javascript"> // Javascript | <pre><code class="hljs javascript"> // Javascript | ||||
let data = { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
let chart = new Chart( "#chart", { // or DOM element | |||||
data: { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | ||||
datasets: [ | |||||
{ | |||||
label: "Some Data", | |||||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||||
}, | |||||
{ | |||||
label: "Another Set", | |||||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||||
}, | |||||
{ | |||||
label: "Yet Another", | |||||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||||
} | |||||
] | |||||
}; | |||||
datasets: [ | |||||
{ | |||||
label: "Some Data", type: 'bar', | |||||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||||
}, | |||||
{ | |||||
label: "Another Set", type: 'bar', | |||||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||||
}, | |||||
{ | |||||
label: "Yet Another", type: 'line', | |||||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||||
} | |||||
] | |||||
}, | |||||
let chart = new Chart({ | |||||
parent: "#chart", // or a DOM element | |||||
title: "My Awesome Chart", | title: "My Awesome Chart", | ||||
data: data, | |||||
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage' | |||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||||
height: 250, | height: 250, | ||||
colors: ['#7cd6fd', 'violet', 'blue'], | |||||
// hex-codes or these preset colors; | |||||
// defaults (in order): | |||||
// ['light-blue', 'blue', 'violet', 'red', | |||||
// 'orange', 'yellow', 'green', 'light-green', | |||||
// 'purple', 'magenta', 'grey', 'dark-grey'] | |||||
format_tooltip_x: d => (d + '').toUpperCase(), | |||||
format_tooltip_y: d => d + ' pts' | |||||
colors: ['#7cd6fd', 'violet', 'blue'] | |||||
});</code></pre> | });</code></pre> | ||||
<div id="chart-types" class="border"></div> | <div id="chart-types" class="border"></div> | ||||
<div class="btn-group chart-type-buttons margin-vertical-px mx-auto" role="group"> | |||||
<!-- <div class="btn-group chart-type-buttons margin-vertical-px mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button> | <button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button> | <button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button> | <button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | <button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | ||||
</div> --> | |||||
<div id="chart-aggr" class="border"></div> | |||||
<div class="btn-group aggr-type-buttons margin-vertical-px mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-type='pie'>Pie Chart</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | |||||
</div> | </div> | ||||
<p class="text-muted"> | |||||
<!-- <p class="text-muted"> | |||||
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | <a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | ||||
</p> | |||||
</p> --> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -113,42 +97,12 @@ | |||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
Update Values | Update Values | ||||
</h6> | </h6> | ||||
<pre><code class="hljs javascript"> // Update entire datasets | |||||
chart.updateData( | |||||
[ | |||||
{values: new_dataset_1_values}, | |||||
{values: new_dataset_2_values} | |||||
], | |||||
new_labels | |||||
); | |||||
// Add a new data point | |||||
chart.add_data_point( | |||||
[new_value_1, new_value_2], | |||||
new_label, | |||||
index // defaults to last index | |||||
); | |||||
// Remove a data point | |||||
chart.remove_data_point(index);</code></pre> | |||||
<div id="chart-update" class="border"></div> | <div id="chart-update" class="border"></div> | ||||
<div class="chart-update-buttons mt-1 mx-auto" role="group"> | <div class="chart-update-buttons mt-1 mx-auto" role="group"> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | ||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||||
// Include specific Y values in input data to be displayed as lines | |||||
// (before passing data to a new chart): | |||||
data.specific_values = [ | |||||
{ | |||||
label: "Altitude", | |||||
line_type: "dashed", // or "solid" | |||||
value: 38 | |||||
} | |||||
] | |||||
...</code></pre> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -157,12 +111,6 @@ | |||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
Plot Trends | Plot Trends | ||||
</h6> | </h6> | ||||
<pre><code class="hljs javascript"> ... | |||||
xAxisMode: 'tick', // for short label ticks | |||||
// or 'span' for long spanning vertical axis lines | |||||
yAxisMode: 'span', // for long horizontal lines, or 'tick' | |||||
isSeries: 1, // to allow for skipping of X values | |||||
...</code></pre> | |||||
<div id="chart-trends" class="border"></div> | <div id="chart-trends" class="border"></div> | ||||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | <div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type="hideDots">Line</button> | <button type="button" class="btn btn-sm btn-secondary" data-type="hideDots">Line</button> | ||||
@@ -170,13 +118,13 @@ | |||||
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> | <button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button> | <button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button> | ||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||||
type: 'line', // Line Chart specific properties: | |||||
<!-- <pre><code class="hljs javascript margin-vertical-px"> ... | |||||
lineOptions: 'line', // Line Chart specific properties: | |||||
hideDots: 1, // Hide data points on the line; default 0 | hideDots: 1, // Hide data points on the line; default 0 | ||||
heatline: 1, // Show a value-wise line gradient; default 0 | heatline: 1, // Show a value-wise line gradient; default 0 | ||||
regionFill: 1, // Fill the area under the graph; default 0 | regionFill: 1, // Fill the area under the graph; default 0 | ||||
...</code></pre> | |||||
...</code></pre> --> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -204,8 +152,7 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> ... | <pre><code class="hljs javascript margin-vertical-px"> ... | ||||
type: 'bar', // Bar Chart specific properties: | |||||
isNavigable: 1, // Navigate across bars; default 0 | |||||
isNavigable: 1, // Navigate across data points; default 0 | |||||
... | ... | ||||
chart.parent.addEventListener('data-select', (e) => { | chart.parent.addEventListener('data-select', (e) => { | ||||
@@ -214,22 +161,6 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="col-sm-10 push-sm-1"> | |||||
<div class="dashboard-section"> | |||||
<h6 class="margin-vertical-rem"> | |||||
Simple Aggregations | |||||
</h6> | |||||
<div id="chart-aggr" class="border"></div> | |||||
<div class="chart-aggr-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="sums">Show Sums</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="average">Show Averages</button> | |||||
</div> | |||||
<pre><code class="hljs javascript margin-vertical-px"> chart.show_sums(); // and `hide_sums()` | |||||
chart.show_averages(); // and `hide_averages()`</code></pre> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-10 push-sm-1"> | <div class="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
@@ -249,16 +180,16 @@ | |||||
parent: "#heatmap", | parent: "#heatmap", | ||||
type: 'heatmap', | type: 'heatmap', | ||||
height: 115, | height: 115, | ||||
data: heatmap_data, // object with date/timestamp-value pairs | |||||
data: heatmapData, // object with date/timestamp-value pairs | |||||
discrete_domains: 1 // default: 0 | |||||
discreteDomains: 1 // default: 0 | |||||
start: start_date, | |||||
start: startDate, | |||||
// A Date object; | // A Date object; | ||||
// default: today's date in past year | // default: today's date in past year | ||||
// for an annual heatmap | // for an annual heatmap | ||||
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||||
// Set of five incremental colors, | // Set of five incremental colors, | ||||
// beginning with a low-saturation color for zero data; | // beginning with a low-saturation color for zero data; | ||||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | // default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | ||||
@@ -267,6 +198,54 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="col-sm-10 push-sm-1"> | |||||
<div class="dashboard-section"> | |||||
<h6 class="margin-vertical-rem">All available options:</h6> | |||||
<pre><code class="hljs javascript"> // Javascript | |||||
let data = { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||||
datasets: [ | |||||
{ | |||||
label: "Some Data", type: 'bar', | |||||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||||
}, | |||||
{ | |||||
label: "Another Set", type: 'bar', | |||||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||||
}, | |||||
{ | |||||
label: "Yet Another", type: 'line', | |||||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||||
} | |||||
] | |||||
}; | |||||
let chart = new Chart( "#chart", { // or DOM element | |||||
title: "My Awesome Chart", | |||||
data: data, | |||||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||||
height: 250, | |||||
colors: ['#7cd6fd', 'violet', 'blue'] | |||||
});</code></pre> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-10 push-sm-1"> | |||||
<div class="dashboard-section"> | |||||
<h6 class="margin-vertical-rem">Install</h6> | |||||
<p class="step-explain">Install via npm</p> | |||||
<pre><code class="hljs console"> npm install frappe-charts</code></pre> | |||||
<p class="step-explain">And include it in your project</p> | |||||
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre> | |||||
<p class="step-explain">... or include it directly in your HTML</p> | |||||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"></script></code></pre> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-10 push-sm-1"> | <div class="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<!-- Closing --> | <!-- Closing --> | ||||
@@ -50,10 +50,11 @@ export default class AggregationChart extends BaseChart { | |||||
renderLegend() { | renderLegend() { | ||||
let s = this.state; | let s = this.state; | ||||
this.statsWrapper.textContent = ''; | |||||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | ||||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||||
? this.formatted_labels : s.labels; | |||||
let xValues = s.labels; | |||||
this.legendTotals.map((d, i) => { | this.legendTotals.map((d, i) => { | ||||
if(d) { | if(d) { | ||||
let stats = $.create('div', { | let stats = $.create('div', { | ||||
@@ -62,7 +63,7 @@ export default class AggregationChart extends BaseChart { | |||||
}); | }); | ||||
stats.innerHTML = `<span class="indicator"> | stats.innerHTML = `<span class="indicator"> | ||||
<i style="background: ${this.colors[i]}"></i> | <i style="background: ${this.colors[i]}"></i> | ||||
<span class="text-muted">${x_values[i]}:</span> | |||||
<span class="text-muted">${xValues[i]}:</span> | |||||
${d} | ${d} | ||||
</span>`; | </span>`; | ||||
} | } | ||||
@@ -2,11 +2,11 @@ import BaseChart from './BaseChart'; | |||||
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; | import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils'; | ||||
import { Y_AXIS_MARGIN } from '../utils/constants'; | import { Y_AXIS_MARGIN } from '../utils/constants'; | ||||
import { getComponent } from '../objects/ChartComponents'; | import { getComponent } from '../objects/ChartComponents'; | ||||
import { getOffset, fire } from '../utils/dom'; | |||||
import { $, getOffset, fire } from '../utils/dom'; | |||||
import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals'; | import { calcChartIntervals, getIntervalSize, getValueRange, getZeroIndex, scale } from '../utils/intervals'; | ||||
import { floatTwo } from '../utils/helpers'; | import { floatTwo } from '../utils/helpers'; | ||||
import { makeOverlay, updateOverlay } from '../utils/draw'; | import { makeOverlay, updateOverlay } from '../utils/draw'; | ||||
import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE, BAR_CHART_SPACE_RATIO, LINE_CHART_DOT_SIZE } from '../utils/constants'; | |||||
import { MIN_BAR_PERCENT_HEIGHT, BAR_CHART_SPACE_RATIO, LINE_CHART_DOT_SIZE } from '../utils/constants'; | |||||
export default class AxisChart extends BaseChart { | export default class AxisChart extends BaseChart { | ||||
constructor(parent, args) { | constructor(parent, args) { | ||||
@@ -28,7 +28,7 @@ export default class AxisChart extends BaseChart { | |||||
this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | this.config.xAxisMode = args.axisOptions.xAxisMode || 'span'; | ||||
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | this.config.yAxisMode = args.axisOptions.yAxisMode || 'span'; | ||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 1; | |||||
this.config.xIsSeries = args.axisOptions.xIsSeries || 0; | |||||
this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | this.config.formatTooltipX = args.tooltipOptions.formatTooltipX; | ||||
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | this.config.formatTooltipY = args.tooltipOptions.formatTooltipY; | ||||
@@ -88,7 +88,7 @@ export default class AxisChart extends BaseChart { | |||||
positions: yPts.map(d => zeroLine - d * scaleMultiplier), | positions: yPts.map(d => zeroLine - d * scaleMultiplier), | ||||
scaleMultiplier: scaleMultiplier, | scaleMultiplier: scaleMultiplier, | ||||
zeroLine: zeroLine, | zeroLine: zeroLine, | ||||
} | |||||
}; | |||||
// Dependent if above changes | // Dependent if above changes | ||||
this.calcDatasetPoints(); | this.calcDatasetPoints(); | ||||
@@ -124,7 +124,7 @@ export default class AxisChart extends BaseChart { | |||||
return; | return; | ||||
} | } | ||||
s.yExtremes = new Array(s.datasetLength).fill(9999); | s.yExtremes = new Array(s.datasetLength).fill(9999); | ||||
s.datasets.map((d, i) => { | |||||
s.datasets.map(d => { | |||||
d.yPositions.map((pos, j) => { | d.yPositions.map((pos, j) => { | ||||
if(pos < s.yExtremes[j]) { | if(pos < s.yExtremes[j]) { | ||||
s.yExtremes[j] = pos; | s.yExtremes[j] = pos; | ||||
@@ -138,9 +138,9 @@ export default class AxisChart extends BaseChart { | |||||
if(this.data.yMarkers) { | if(this.data.yMarkers) { | ||||
this.state.yMarkers = this.data.yMarkers.map(d => { | this.state.yMarkers = this.data.yMarkers.map(d => { | ||||
d.position = scale(d.value, s.yAxis); | d.position = scale(d.value, s.yAxis); | ||||
if(!d.label.includes(':')) { | |||||
d.label += ': ' + d.value; | |||||
} | |||||
// if(!d.label.includes(':')) { | |||||
// d.label += ': ' + d.value; | |||||
// } | |||||
return d; | return d; | ||||
}); | }); | ||||
} | } | ||||
@@ -170,7 +170,6 @@ export default class AxisChart extends BaseChart { | |||||
} | } | ||||
setupComponents() { | setupComponents() { | ||||
let s = this.state; | |||||
let componentConfigs = [ | let componentConfigs = [ | ||||
[ | [ | ||||
'yAxis', | 'yAxis', | ||||
@@ -243,7 +242,7 @@ export default class AxisChart extends BaseChart { | |||||
} | } | ||||
let labels = new Array(s.datasetLength).fill(''); | let labels = new Array(s.datasetLength).fill(''); | ||||
if(this.valuesOverPoints) { | |||||
if(this.config.valuesOverPoints) { | |||||
if(stacked && d.index === s.datasets.length - 1) { | if(stacked && d.index === s.datasets.length - 1) { | ||||
labels = d.cumulativeYs; | labels = d.cumulativeYs; | ||||
} else { | } else { | ||||
@@ -352,12 +351,15 @@ export default class AxisChart extends BaseChart { | |||||
let s = this.state; | let s = this.state; | ||||
if(!s.yExtremes) return; | if(!s.yExtremes) return; | ||||
let formatY = this.config.formatTooltipY; | |||||
let formatX = this.config.formatTooltipX; | |||||
let titles = s.xAxis.labels; | let titles = s.xAxis.labels; | ||||
if(this.formatTooltipX && this.formatTooltipX(titles[0])) { | |||||
titles = titles.map(d=>this.formatTooltipX(d)); | |||||
if(formatX && formatX(titles[0])) { | |||||
titles = titles.map(d=>formatX(d)); | |||||
} | } | ||||
let formatY = this.formatTooltipY && this.formatTooltipY(this.y[0].values[0]); | |||||
formatY = formatY && formatY(s.yAxis.labels[0]) ? formatY : 0; | |||||
for(var i=s.datasetLength - 1; i >= 0 ; i--) { | for(var i=s.datasetLength - 1; i >= 0 ; i--) { | ||||
let xVal = s.xAxis.positions[i]; | let xVal = s.xAxis.positions[i]; | ||||
@@ -368,19 +370,37 @@ export default class AxisChart extends BaseChart { | |||||
let values = this.data.datasets.map((set, j) => { | let values = this.data.datasets.map((set, j) => { | ||||
return { | return { | ||||
title: set.title, | |||||
value: formatY ? this.formatTooltipY(set.values[i]) : set.values[i], | |||||
title: set.name, | |||||
value: formatY ? formatY(set.values[i]) : set.values[i], | |||||
color: this.colors[j], | color: this.colors[j], | ||||
}; | }; | ||||
}); | }); | ||||
this.tip.setValues(x, y, titles[i], '', values); | |||||
this.tip.setValues(x, y, {name: titles[i], value: ''}, values, i); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
break; | break; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
renderLegend() { | |||||
let s = this.data; | |||||
this.statsWrapper.textContent = ''; | |||||
if(s.datasets.length > 1) { | |||||
s.datasets.map((d, i) => { | |||||
let stats = $.create('div', { | |||||
className: 'stats', | |||||
inside: this.statsWrapper | |||||
}); | |||||
stats.innerHTML = `<span class="indicator"> | |||||
<i style="background: ${this.colors[i]}"></i> | |||||
${d.name} | |||||
</span>`; | |||||
}); | |||||
} | |||||
} | |||||
makeOverlay() { | makeOverlay() { | ||||
if(this.overlayGuides) { | if(this.overlayGuides) { | ||||
this.overlayGuides.forEach(g => { | this.overlayGuides.forEach(g => { | ||||
@@ -394,7 +414,7 @@ export default class AxisChart extends BaseChart { | |||||
type: c.unitType, | type: c.unitType, | ||||
overlay: undefined, | overlay: undefined, | ||||
units: c.units, | units: c.units, | ||||
} | |||||
}; | |||||
}); | }); | ||||
if(this.state.currentIndex === undefined) { | if(this.state.currentIndex === undefined) { | ||||
@@ -406,7 +426,7 @@ export default class AxisChart extends BaseChart { | |||||
let currentUnit = d.units[this.state.currentIndex]; | let currentUnit = d.units[this.state.currentIndex]; | ||||
d.overlay = makeOverlay[d.type](currentUnit); | d.overlay = makeOverlay[d.type](currentUnit); | ||||
this.drawArea.appendChild(d.overlay); | this.drawArea.appendChild(d.overlay); | ||||
}) | |||||
}); | |||||
} | } | ||||
@@ -420,26 +440,33 @@ export default class AxisChart extends BaseChart { | |||||
} | } | ||||
bindOverlay() { | bindOverlay() { | ||||
// on event, update overlay | |||||
this.parent.addEventListener('data-select', (e) => { | |||||
this.parent.addEventListener('data-select', () => { | |||||
this.updateOverlay(); | this.updateOverlay(); | ||||
}); | }); | ||||
} | } | ||||
bindUnits(units_array) { | |||||
// units_array.map(unit => { | |||||
// unit.addEventListener('click', () => { | |||||
// let index = unit.getAttribute('data-point-index'); | |||||
// this.setCurrentDataPoint(index); | |||||
// }); | |||||
// }); | |||||
bindUnits() { | |||||
this.dataUnitComponents.map(c => { | |||||
c.units.map(unit => { | |||||
unit.addEventListener('click', () => { | |||||
let index = unit.getAttribute('data-point-index'); | |||||
this.setCurrentDataPoint(index); | |||||
}); | |||||
}); | |||||
}); | |||||
// Note: Doesn't work as tooltip is absolutely positioned | |||||
this.tip.container.addEventListener('click', () => { | |||||
let index = this.tip.container.getAttribute('data-point-index'); | |||||
this.setCurrentDataPoint(index); | |||||
}); | |||||
} | } | ||||
updateOverlay() { | updateOverlay() { | ||||
this.overlayGuides.map(d => { | this.overlayGuides.map(d => { | ||||
let currentUnit = d.units[this.state.currentIndex]; | let currentUnit = d.units[this.state.currentIndex]; | ||||
updateOverlay[d.type](currentUnit, d.overlay); | updateOverlay[d.type](currentUnit, d.overlay); | ||||
}) | |||||
}); | |||||
} | } | ||||
onLeftArrow() { | onLeftArrow() { | ||||
@@ -451,16 +478,12 @@ export default class AxisChart extends BaseChart { | |||||
} | } | ||||
getDataPoint(index=this.state.currentIndex) { | getDataPoint(index=this.state.currentIndex) { | ||||
// check for length | |||||
let s = this.state; | |||||
let data_point = { | let data_point = { | ||||
index: index | |||||
index: index, | |||||
label: s.xAxis.labels[index], | |||||
values: s.datasets.map(d => d.values[index]) | |||||
}; | }; | ||||
// let y = this.y[0]; | |||||
// ['svg_units', 'yUnitPositions', 'values'].map(key => { | |||||
// let data_key = key.slice(0, key.length-1); | |||||
// data_point[data_key] = y[key][index]; | |||||
// }); | |||||
// data_point.label = this.xAxis.labels[index]; | |||||
return data_point; | return data_point; | ||||
} | } | ||||
@@ -501,7 +524,14 @@ export default class AxisChart extends BaseChart { | |||||
// addDataset(dataset, index) {} | // addDataset(dataset, index) {} | ||||
// removeDataset(index = 0) {} | // removeDataset(index = 0) {} | ||||
// updateDatasets(datasets) {} | |||||
updateDatasets(datasets) { | |||||
this.data.datasets.map((d, i) => { | |||||
if(datasets[i]) { | |||||
d.values = datasets[i]; | |||||
} | |||||
}); | |||||
this.update(this.data); | |||||
} | |||||
// updateDataPoint(dataPoint, index = 0) {} | // updateDataPoint(dataPoint, index = 0) {} | ||||
// addDataPoint(dataPoint, index = 0) {} | // addDataPoint(dataPoint, index = 0) {} | ||||
@@ -143,7 +143,10 @@ export default class BaseChart { | |||||
setTimeout(() => {this.update();}, this.initTimeout); | setTimeout(() => {this.update();}, this.initTimeout); | ||||
} | } | ||||
this.renderLegend(); | |||||
if(!onlyWidthChange) { | |||||
this.renderLegend(); | |||||
} | |||||
this.setupNavigation(init); | this.setupNavigation(init); | ||||
} | } | ||||
@@ -13,15 +13,15 @@ export default class Heatmap extends BaseChart { | |||||
this.domain = options.domain || ''; | this.domain = options.domain || ''; | ||||
this.subdomain = options.subdomain || ''; | this.subdomain = options.subdomain || ''; | ||||
this.data = options.data || {}; | this.data = options.data || {}; | ||||
this.discrete_domains = options.discrete_domains || 1; | |||||
this.count_label = options.count_label || ''; | |||||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||||
this.countLabel = options.countLabel || ''; | |||||
let today = new Date(); | let today = new Date(); | ||||
this.start = options.start || addDays(today, 365); | this.start = options.start || addDays(today, 365); | ||||
let legend_colors = (options.legend_colors || []).slice(0, 5); | |||||
this.legend_colors = this.validate_colors(legend_colors) | |||||
? legend_colors | |||||
let legendColors = (options.legendColors || []).slice(0, 5); | |||||
this.legendColors = this.validate_colors(legendColors) | |||||
? legendColors | |||||
: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | ||||
// Fixed 5-color theme, | // Fixed 5-color theme, | ||||
@@ -32,6 +32,12 @@ export default class Heatmap extends BaseChart { | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
setMargins() { | |||||
super.setMargins(); | |||||
this.leftMargin = 10; | |||||
this.translateY = 10; | |||||
} | |||||
validate_colors(colors) { | validate_colors(colors) { | ||||
if(colors.length < 5) return 0; | if(colors.length < 5) return 0; | ||||
@@ -54,21 +60,21 @@ export default class Heatmap extends BaseChart { | |||||
this.start = new Date(); | this.start = new Date(); | ||||
this.start.setFullYear( this.start.getFullYear() - 1 ); | this.start.setFullYear( this.start.getFullYear() - 1 ); | ||||
} | } | ||||
this.first_week_start = new Date(this.start.toDateString()); | |||||
this.last_week_start = new Date(this.today.toDateString()); | |||||
if(this.first_week_start.getDay() !== 7) { | |||||
addDays(this.first_week_start, (-1) * this.first_week_start.getDay()); | |||||
this.firstWeekStart = new Date(this.start.toDateString()); | |||||
this.lastWeekStart = new Date(this.today.toDateString()); | |||||
if(this.firstWeekStart.getDay() !== 7) { | |||||
addDays(this.firstWeekStart, (-1) * this.firstWeekStart.getDay()); | |||||
} | } | ||||
if(this.last_week_start.getDay() !== 7) { | |||||
addDays(this.last_week_start, (-1) * this.last_week_start.getDay()); | |||||
if(this.lastWeekStart.getDay() !== 7) { | |||||
addDays(this.lastWeekStart, (-1) * this.lastWeekStart.getDay()); | |||||
} | } | ||||
this.no_of_cols = getWeeksBetween(this.first_week_start + '', this.last_week_start + '') + 1; | |||||
this.no_of_cols = getWeeksBetween(this.firstWeekStart + '', this.lastWeekStart + '') + 1; | |||||
} | } | ||||
calcWidth() { | calcWidth() { | ||||
this.baseWidth = (this.no_of_cols + 3) * 12 ; | this.baseWidth = (this.no_of_cols + 3) * 12 ; | ||||
if(this.discrete_domains) { | |||||
if(this.discreteDomains) { | |||||
this.baseWidth += (12 * 12); | this.baseWidth += (12 * 12); | ||||
} | } | ||||
} | } | ||||
@@ -82,21 +88,20 @@ export default class Heatmap extends BaseChart { | |||||
'data-groups', | 'data-groups', | ||||
`translate(0, 20)` | `translate(0, 20)` | ||||
); | ); | ||||
// Array.prototype.slice.call( | |||||
// this.container.querySelectorAll('.graph-stats-container, .sub-title, .title') | |||||
// ).map(d => { | |||||
// d.style.display = 'None'; | |||||
// }); | |||||
// this.chartWrapper.style.marginTop = '0px'; | |||||
// this.chartWrapper.style.paddingTop = '0px'; | |||||
this.container.querySelector('.title').style.display = 'None'; | |||||
this.container.querySelector('.sub-title').style.display = 'None'; | |||||
this.container.querySelector('.graph-stats-container').style.display = 'None'; | |||||
this.chartWrapper.style.marginTop = '0px'; | |||||
this.chartWrapper.style.paddingTop = '0px'; | |||||
} | } | ||||
calc() { | calc() { | ||||
let data_values = Object.keys(this.data).map(key => this.data[key]); | |||||
this.distribution = calcDistribution(data_values, this.distribution_size); | |||||
let dataValues = Object.keys(this.data).map(key => this.data[key]); | |||||
this.distribution = calcDistribution(dataValues, this.distribution_size); | |||||
this.month_names = ["January", "February", "March", "April", "May", "June", | |||||
this.monthNames = ["January", "February", "March", "April", "May", "June", | |||||
"July", "August", "September", "October", "November", "December" | "July", "August", "September", "October", "November", "December" | ||||
]; | ]; | ||||
} | } | ||||
@@ -110,118 +115,118 @@ export default class Heatmap extends BaseChart { | |||||
this.domainLabelGroup.textContent = ''; | this.domainLabelGroup.textContent = ''; | ||||
this.dataGroups.textContent = ''; | this.dataGroups.textContent = ''; | ||||
let current_week_sunday = new Date(this.first_week_start); | |||||
this.week_col = 0; | |||||
this.current_month = current_week_sunday.getMonth(); | |||||
let currentWeekSunday = new Date(this.firstWeekStart); | |||||
this.weekCol = 0; | |||||
this.currentMonth = currentWeekSunday.getMonth(); | |||||
this.months = [this.current_month + '']; | |||||
this.month_weeks = {}, this.month_start_points = []; | |||||
this.month_weeks[this.current_month] = 0; | |||||
this.month_start_points.push(13); | |||||
this.months = [this.currentMonth + '']; | |||||
this.monthWeeks = {}, this.monthStartPoints = []; | |||||
this.monthWeeks[this.currentMonth] = 0; | |||||
this.monthStartPoints.push(13); | |||||
for(var i = 0; i < no_of_weeks; i++) { | for(var i = 0; i < no_of_weeks; i++) { | ||||
let data_group, month_change = 0; | |||||
let day = new Date(current_week_sunday); | |||||
[data_group, month_change] = this.get_week_squares_group(day, this.week_col); | |||||
this.dataGroups.appendChild(data_group); | |||||
this.week_col += 1 + parseInt(this.discrete_domains && month_change); | |||||
this.month_weeks[this.current_month]++; | |||||
if(month_change) { | |||||
this.current_month = (this.current_month + 1) % 12; | |||||
this.months.push(this.current_month + ''); | |||||
this.month_weeks[this.current_month] = 1; | |||||
let dataGroup, monthChange = 0; | |||||
let day = new Date(currentWeekSunday); | |||||
[dataGroup, monthChange] = this.get_week_squares_group(day, this.weekCol); | |||||
this.dataGroups.appendChild(dataGroup); | |||||
this.weekCol += 1 + parseInt(this.discreteDomains && monthChange); | |||||
this.monthWeeks[this.currentMonth]++; | |||||
if(monthChange) { | |||||
this.currentMonth = (this.currentMonth + 1) % 12; | |||||
this.months.push(this.currentMonth + ''); | |||||
this.monthWeeks[this.currentMonth] = 1; | |||||
} | } | ||||
addDays(current_week_sunday, 7); | |||||
addDays(currentWeekSunday, 7); | |||||
} | } | ||||
this.render_month_labels(); | this.render_month_labels(); | ||||
} | } | ||||
get_week_squares_group(current_date, index) { | |||||
const no_of_weekdays = 7; | |||||
const square_side = 10; | |||||
const cell_padding = 2; | |||||
get_week_squares_group(currentDate, index) { | |||||
const noOfWeekdays = 7; | |||||
const squareSide = 10; | |||||
const cellPadding = 2; | |||||
const step = 1; | const step = 1; | ||||
const today_time = this.today.getTime(); | |||||
const todayTime = this.today.getTime(); | |||||
let month_change = 0; | |||||
let week_col_change = 0; | |||||
let monthChange = 0; | |||||
let weekColChange = 0; | |||||
let data_group = makeSVGGroup(this.dataGroups, 'data-group'); | |||||
let dataGroup = makeSVGGroup(this.dataGroups, 'data-group'); | |||||
for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) { | |||||
let data_value = 0; | |||||
for(var y = 0, i = 0; i < noOfWeekdays; i += step, y += (squareSide + cellPadding)) { | |||||
let dataValue = 0; | |||||
let colorIndex = 0; | let colorIndex = 0; | ||||
let current_timestamp = current_date.getTime()/1000; | |||||
let timestamp = Math.floor(current_timestamp - (current_timestamp % 86400)).toFixed(1); | |||||
let currentTimestamp = currentDate.getTime()/1000; | |||||
let timestamp = Math.floor(currentTimestamp - (currentTimestamp % 86400)).toFixed(1); | |||||
if(this.data[timestamp]) { | if(this.data[timestamp]) { | ||||
data_value = this.data[timestamp]; | |||||
dataValue = this.data[timestamp]; | |||||
} | } | ||||
if(this.data[Math.round(timestamp)]) { | if(this.data[Math.round(timestamp)]) { | ||||
data_value = this.data[Math.round(timestamp)]; | |||||
dataValue = this.data[Math.round(timestamp)]; | |||||
} | } | ||||
if(data_value) { | |||||
colorIndex = getMaxCheckpoint(data_value, this.distribution); | |||||
if(dataValue) { | |||||
colorIndex = getMaxCheckpoint(dataValue, this.distribution); | |||||
} | } | ||||
let x = 13 + (index + week_col_change) * 12; | |||||
let x = 13 + (index + weekColChange) * 12; | |||||
let dataAttr = { | let dataAttr = { | ||||
'data-date': getDdMmYyyy(current_date), | |||||
'data-value': data_value, | |||||
'data-day': current_date.getDay() | |||||
'data-date': getDdMmYyyy(currentDate), | |||||
'data-value': dataValue, | |||||
'data-day': currentDate.getDay() | |||||
}; | }; | ||||
let heatSquare = makeHeatSquare('day', x, y, square_side, | |||||
this.legend_colors[colorIndex], dataAttr); | |||||
let heatSquare = makeHeatSquare('day', x, y, squareSide, | |||||
this.legendColors[colorIndex], dataAttr); | |||||
data_group.appendChild(heatSquare); | |||||
dataGroup.appendChild(heatSquare); | |||||
let next_date = new Date(current_date); | |||||
addDays(next_date, 1); | |||||
if(next_date.getTime() > today_time) break; | |||||
let nextDate = new Date(currentDate); | |||||
addDays(nextDate, 1); | |||||
if(nextDate.getTime() > todayTime) break; | |||||
if(next_date.getMonth() - current_date.getMonth()) { | |||||
month_change = 1; | |||||
if(this.discrete_domains) { | |||||
week_col_change = 1; | |||||
if(nextDate.getMonth() - currentDate.getMonth()) { | |||||
monthChange = 1; | |||||
if(this.discreteDomains) { | |||||
weekColChange = 1; | |||||
} | } | ||||
this.month_start_points.push(13 + (index + week_col_change) * 12); | |||||
this.monthStartPoints.push(13 + (index + weekColChange) * 12); | |||||
} | } | ||||
current_date = next_date; | |||||
currentDate = nextDate; | |||||
} | } | ||||
return [data_group, month_change]; | |||||
return [dataGroup, monthChange]; | |||||
} | } | ||||
render_month_labels() { | render_month_labels() { | ||||
// this.first_month_label = 1; | // this.first_month_label = 1; | ||||
// if (this.first_week_start.getDate() > 8) { | |||||
// if (this.firstWeekStart.getDate() > 8) { | |||||
// this.first_month_label = 0; | // this.first_month_label = 0; | ||||
// } | // } | ||||
// this.last_month_label = 1; | // this.last_month_label = 1; | ||||
// let first_month = this.months.shift(); | // let first_month = this.months.shift(); | ||||
// let first_month_start = this.month_start_points.shift(); | |||||
// let first_month_start = this.monthStartPoints.shift(); | |||||
// render first month if | // render first month if | ||||
// let last_month = this.months.pop(); | // let last_month = this.months.pop(); | ||||
// let last_month_start = this.month_start_points.pop(); | |||||
// let last_month_start = this.monthStartPoints.pop(); | |||||
// render last month if | // render last month if | ||||
this.months.shift(); | this.months.shift(); | ||||
this.month_start_points.shift(); | |||||
this.monthStartPoints.shift(); | |||||
this.months.pop(); | this.months.pop(); | ||||
this.month_start_points.pop(); | |||||
this.monthStartPoints.pop(); | |||||
this.month_start_points.map((start, i) => { | |||||
let month_name = this.month_names[this.months[i]].substring(0, 3); | |||||
this.monthStartPoints.map((start, i) => { | |||||
let month_name = this.monthNames[this.months[i]].substring(0, 3); | |||||
let text = makeText('y-value-text', start+12, 10, month_name); | let text = makeText('y-value-text', start+12, 10, month_name); | ||||
this.domainLabelGroup.appendChild(text); | this.domainLabelGroup.appendChild(text); | ||||
}); | }); | ||||
@@ -233,19 +238,19 @@ export default class Heatmap extends BaseChart { | |||||
).map(el => { | ).map(el => { | ||||
el.addEventListener('mouseenter', (e) => { | el.addEventListener('mouseenter', (e) => { | ||||
let count = e.target.getAttribute('data-value'); | let count = e.target.getAttribute('data-value'); | ||||
let date_parts = e.target.getAttribute('data-date').split('-'); | |||||
let dateParts = e.target.getAttribute('data-date').split('-'); | |||||
let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3); | |||||
let month = this.monthNames[parseInt(dateParts[1])-1].substring(0, 3); | |||||
let g_off = this.chartWrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect(); | |||||
let gOff = this.chartWrapper.getBoundingClientRect(), pOff = e.target.getBoundingClientRect(); | |||||
let width = parseInt(e.target.getAttribute('width')); | let width = parseInt(e.target.getAttribute('width')); | ||||
let x = p_off.left - g_off.left + (width+2)/2; | |||||
let y = p_off.top - g_off.top - (width+2)/2; | |||||
let value = count + ' ' + this.count_label; | |||||
let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2]; | |||||
let x = pOff.left - gOff.left + (width+2)/2; | |||||
let y = pOff.top - gOff.top - (width+2)/2; | |||||
let value = count + ' ' + this.countLabel; | |||||
let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2]; | |||||
this.tip.setValues(x, y, name, value, [], 1); | |||||
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
}); | }); | ||||
}); | }); | ||||
@@ -67,7 +67,7 @@ export default class PercentageChart extends AggregationChart { | |||||
? this.formattedLabels[i] : this.state.labels[i]) + ': '; | ? this.formattedLabels[i] : this.state.labels[i]) + ': '; | ||||
let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | ||||
this.tip.setValues(x, y, title, percent + "%"); | |||||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
} | } | ||||
}); | }); | ||||
@@ -2,7 +2,7 @@ import AggregationChart from './AggregationChart'; | |||||
import { getComponent } from '../objects/ChartComponents'; | import { getComponent } from '../objects/ChartComponents'; | ||||
import { getOffset } from '../utils/dom'; | import { getOffset } from '../utils/dom'; | ||||
import { getPositionByAngle } from '../utils/helpers'; | import { getPositionByAngle } from '../utils/helpers'; | ||||
import { makePath, makeArcPathStr } from '../utils/draw'; | |||||
import { makeArcPathStr } from '../utils/draw'; | |||||
import { lightenDarkenColor } from '../utils/colors'; | import { lightenDarkenColor } from '../utils/colors'; | ||||
import { transform } from '../utils/animation'; | import { transform } from '../utils/animation'; | ||||
import { FULL_ANGLE } from '../utils/constants'; | import { FULL_ANGLE } from '../utils/constants'; | ||||
@@ -39,7 +39,7 @@ export default class PieChart extends AggregationChart { | |||||
this.center = { | this.center = { | ||||
x: this.width / 2, | x: this.width / 2, | ||||
y: this.height / 2 | y: this.height / 2 | ||||
} | |||||
}; | |||||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | this.radius = (this.height > this.width ? this.center.x : this.center.y); | ||||
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | ||||
@@ -102,7 +102,7 @@ export default class PieChart extends AggregationChart { | |||||
return { | return { | ||||
sliceStrings: s.sliceStrings, | sliceStrings: s.sliceStrings, | ||||
colors: this.colors | colors: this.colors | ||||
} | |||||
}; | |||||
}.bind(this) | }.bind(this) | ||||
] | ] | ||||
]; | ]; | ||||
@@ -132,7 +132,7 @@ export default class PieChart extends AggregationChart { | |||||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | let title = (this.formatted_labels && this.formatted_labels.length > 0 | ||||
? this.formatted_labels[i] : this.state.labels[i]) + ': '; | ? this.formatted_labels[i] : this.state.labels[i]) + ': '; | ||||
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | ||||
this.tip.setValues(x, y, title, percent + "%"); | |||||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||||
this.tip.showTip(); | this.tip.showTip(); | ||||
} else { | } else { | ||||
transform(path,'translate3d(0,0,0)'); | transform(path,'translate3d(0,0,0)'); | ||||
@@ -51,6 +51,9 @@ export default class SvgTip { | |||||
fill() { | fill() { | ||||
let title; | let title; | ||||
if(this.index) { | |||||
this.container.setAttribute('data-point-index', this.index); | |||||
} | |||||
if(this.titleValueFirst) { | if(this.titleValueFirst) { | ||||
title = `<strong>${this.titleValue}</strong>${this.titleName}`; | title = `<strong>${this.titleValue}</strong>${this.titleName}`; | ||||
} else { | } else { | ||||
@@ -97,13 +100,14 @@ export default class SvgTip { | |||||
} | } | ||||
} | } | ||||
setValues(x, y, titleName = '', titleValue = '', listValues = [], titleValueFirst = 0) { | |||||
this.titleName = titleName; | |||||
this.titleValue = titleValue; | |||||
setValues(x, y, title = {}, listValues = [], index = -1) { | |||||
this.titleName = title.name; | |||||
this.titleValue = title.value; | |||||
this.listValues = listValues; | this.listValues = listValues; | ||||
this.x = x; | this.x = x; | ||||
this.y = y; | this.y = y; | ||||
this.titleValueFirst = titleValueFirst; | |||||
this.titleValueFirst = title.valueFirst || 0; | |||||
this.index = index; | |||||
this.refresh(); | this.refresh(); | ||||
} | } | ||||
@@ -1,4 +1,4 @@ | |||||
export const VERT_SPACE_OUTSIDE_BASE_CHART = 40; | |||||
export const VERT_SPACE_OUTSIDE_BASE_CHART = 50; | |||||
export const TRANSLATE_Y_BASE_CHART = 20; | export const TRANSLATE_Y_BASE_CHART = 20; | ||||
export const LEFT_MARGIN_BASE_CHART = 60; | export const LEFT_MARGIN_BASE_CHART = 60; | ||||
export const RIGHT_MARGIN_BASE_CHART = 40; | export const RIGHT_MARGIN_BASE_CHART = 40; | ||||
@@ -16,7 +16,7 @@ export const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||||
export const LINE_CHART_DOT_SIZE = 4; | export const LINE_CHART_DOT_SIZE = 4; | ||||
export const DOT_OVERLAY_SIZE_INCR = 4; | export const DOT_OVERLAY_SIZE_INCR = 4; | ||||
export const DEFAULT_CHAR_WIDTH = 8; | |||||
export const DEFAULT_CHAR_WIDTH = 7; | |||||
// Universal constants | // Universal constants | ||||
export const ANGLE_RATIO = Math.PI / 180; | export const ANGLE_RATIO = Math.PI / 180; |
@@ -31,4 +31,9 @@ export function addDays(date, numberOfDays) { | |||||
date.setDate(date.getDate() + numberOfDays); | date.setDate(date.getDate() + numberOfDays); | ||||
} | } | ||||
// export function getMonthName() {} | |||||
export function getMonthName(i) { | |||||
let monthNames = ["January", "February", "March", "April", "May", "June", | |||||
"July", "August", "September", "October", "November", "December" | |||||
]; | |||||
return monthNames[i]; | |||||
} |
@@ -180,7 +180,7 @@ function makeVertLine(x, label, y1, y2, options={}) { | |||||
dy: FONT_SIZE + 'px', | dy: FONT_SIZE + 'px', | ||||
'font-size': FONT_SIZE + 'px', | 'font-size': FONT_SIZE + 'px', | ||||
'text-anchor': 'middle', | 'text-anchor': 'middle', | ||||
innerHTML: label | |||||
innerHTML: label + "" | |||||
}); | }); | ||||
let line = createSVG('g', { | let line = createSVG('g', { | ||||
@@ -337,7 +337,7 @@ export function yRegion(y1, y2, width, label) { | |||||
let labelSvg = createSVG('text', { | let labelSvg = createSVG('text', { | ||||
className: 'chart-label', | className: 'chart-label', | ||||
x: width - getStringWidth(label, 4.5) - LABEL_MARGIN, | |||||
x: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN, | |||||
y: 0, | y: 0, | ||||
dy: (FONT_SIZE / -2) + 'px', | dy: (FONT_SIZE / -2) + 'px', | ||||
'font-size': FONT_SIZE + 'px', | 'font-size': FONT_SIZE + 'px', | ||||
@@ -369,6 +369,8 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m | |||||
height: height || meta.minHeight // TODO: correct y for positive min height | height: height || meta.minHeight // TODO: correct y for positive min height | ||||
}); | }); | ||||
label += ""; | |||||
if(!label && !label.length) { | if(!label && !label.length) { | ||||
return rect; | return rect; | ||||
} else { | } else { | ||||
@@ -385,6 +387,7 @@ export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, m | |||||
}); | }); | ||||
let group = createSVG('g', { | let group = createSVG('g', { | ||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | transform: `translate(${x}, ${y})` | ||||
}); | }); | ||||
group.appendChild(rect); | group.appendChild(rect); | ||||
@@ -403,6 +406,8 @@ export function datasetDot(x, y, radius, color, label='', index=0, meta={}) { | |||||
r: radius | r: radius | ||||
}); | }); | ||||
label += ""; | |||||
if(!label && !label.length) { | if(!label && !label.length) { | ||||
return dot; | return dot; | ||||
} else { | } else { | ||||
@@ -420,6 +425,7 @@ export function datasetDot(x, y, radius, color, label='', index=0, meta={}) { | |||||
}); | }); | ||||
let group = createSVG('g', { | let group = createSVG('g', { | ||||
'data-point-index': index, | |||||
transform: `translate(${x}, ${y})` | transform: `translate(${x}, ${y})` | ||||
}); | }); | ||||
group.appendChild(dot); | group.appendChild(dot); | ||||
@@ -481,9 +487,10 @@ export let makeOverlay = { | |||||
} | } | ||||
let overlay = unit.cloneNode(); | let overlay = unit.cloneNode(); | ||||
let radius = unit.getAttribute('r'); | let radius = unit.getAttribute('r'); | ||||
overlay.setAttribute('r', radius + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.style.fill = '#000000'; | |||||
overlay.style.opacity = '0.4'; | |||||
let fill = unit.getAttribute('fill'); | |||||
overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR); | |||||
overlay.setAttribute('fill', fill); | |||||
overlay.style.opacity = '0.6'; | |||||
if(transformValue) { | if(transformValue) { | ||||
overlay.setAttribute('transform', transformValue); | overlay.setAttribute('transform', transformValue); | ||||
@@ -28,8 +28,13 @@ | |||||
} | } | ||||
.graph-stats-container { | .graph-stats-container { | ||||
display: flex; | display: flex; | ||||
justify-content: space-around; | |||||
padding-top: 10px; | |||||
justify-content: space-between; | |||||
padding: 10px; | |||||
&:before, | |||||
&:after { | |||||
content: ''; | |||||
display: block; | |||||
} | |||||
.stats { | .stats { | ||||
padding-bottom: 15px; | padding-bottom: 15px; | ||||
} | } | ||||