@@ -1,6 +1,6 @@ | |||
<div align="center"> | |||
<img src="https://github.com/frappe/design/blob/master/logos/frappe-charts-symbol.svg" height="128"> | |||
<h2>Frappé Charts</h2> | |||
<img src="https://github.com/frappe/design/blob/master/logos/charts-logo.svg" height="128"> | |||
<h2>Frappe Charts</h2> | |||
<p align="center"> | |||
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p> | |||
<a href="https://frappe.github.io/charts"> | |||
@@ -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{font-size:11px;fill:#555b51}.chart-container .axis line,.chart-container .chart-label line{stroke:#dadada}.chart-container .percentage-graph .progress{margin-bottom:0}.chart-container .data-points circle{stroke:#fff;stroke-width:2}.chart-container .path-group path{fill:none;stroke-opacity:1;stroke-width:2px}.chart-container line.dashed{stroke-dasharray:5,3}.chart-container .tick.x-axis-label{display:block}.chart-container .tick .specific-value{text-anchor:start}.chart-container .tick .y-value-text{text-anchor:end}.chart-container .tick .x-value-text{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,186 +1,218 @@ | |||
// Composite Chart | |||
// ================================================================================ | |||
let report_count_list = [17, 40, 33, 44, 126, 156, | |||
324, 333, 478, 495, 373]; | |||
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", | |||
"2013", "2014", "2015", "2016", "2017"], | |||
datasets: [{ | |||
"title": "Events", | |||
"values": report_count_list, | |||
// "formatted": report_count_list.map(d => d + " reports") | |||
}] | |||
}; | |||
yMarkers: [ | |||
{ | |||
label: "Average 100 reports/month", | |||
value: 1200, | |||
} | |||
], | |||
let line_composite_data = { | |||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||
datasets: [{ | |||
"values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0], | |||
// "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], | |||
"name": "Events", | |||
"values": reportCountList | |||
}] | |||
}; | |||
let more_line_data = { | |||
// 0: {values: [4, 0, 3, 1, 1, 2, 1, 2, 1, 0, 1, 1]}, | |||
0: {values: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, | |||
1: {values: [2, 3, 3, 2, 1, 4, 0, 1, 2, 7, 11, 4]}, | |||
2: {values: [7, 7, 2, 4, 0, 1, 5, 3, 1, 2, 0, 1]}, | |||
3: {values: [0, 2, 6, 2, 2, 1, 2, 3, 6, 3, 7, 10]}, | |||
4: {values: [9, 10, 8, 10, 6, 5, 8, 8, 24, 15, 10, 13]}, | |||
5: {values: [9, 13, 16, 9, 4, 5, 7, 10, 14, 22, 23, 24]}, | |||
6: {values: [20, 22, 28, 19, 28, 19, 14, 19, 51, 37, 29, 38]}, | |||
7: {values: [29, 20, 22, 16, 16, 19, 24, 26, 57, 31, 46, 27]}, | |||
8: {values: [36, 24, 38, 27, 15, 22, 24, 38, 32, 57, 139, 26]}, | |||
9: {values: [37, 36, 32, 33, 12, 34, 52, 45, 58, 57, 64, 35]}, | |||
10: {values: [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0]} | |||
// 10: {values: [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40]} | |||
// 10: {values: [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40]} | |||
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, 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 c2 = document.querySelector("#chart-composite-2"); | |||
let bar_composite_chart = new Chart ({ | |||
parent: c1, | |||
title: "Fireball/Bolide Events - Yearly (more than 5 reports)", | |||
data: bar_composite_data, | |||
type: 'bar', | |||
height: 180, | |||
colors: ['orange'], | |||
is_navigable: 1, | |||
is_series: 1 | |||
// region_fill: 1 | |||
}); | |||
let line_composite_chart = new Chart ({ | |||
parent: c2, | |||
data: line_composite_data, | |||
let lineCompositeChart = new Chart (c1, { | |||
title: "Fireball/Bolide Events - Yearly (reported)", | |||
data: lineCompositeData, | |||
type: 'line', | |||
height: 180, | |||
height: 190, | |||
colors: ['green'], | |||
is_series: 1 | |||
isNavigable: 1, | |||
valuesOverPoints: 1, | |||
lineOptions: { | |||
dotSize: 8 | |||
}, | |||
// yAxisMode: 'tick' | |||
// regionFill: 1 | |||
}); | |||
let barCompositeChart = new Chart (c2, { | |||
data: barCompositeData, | |||
type: 'bar', | |||
height: 190, | |||
colors: ['violet', 'light-blue', '#46a9f9'], | |||
valuesOverPoints: 1, | |||
axisOptions: { | |||
xAxisMode: 'tick' | |||
}, | |||
barOptions: { | |||
stacked: 1 | |||
}, | |||
}); | |||
bar_composite_chart.parent.addEventListener('data-select', (e) => { | |||
line_composite_chart.update_values([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) | |||
// ================================================================================ | |||
let type_data = { | |||
let typeData = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker", | |||
value: 43, | |||
// type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region", | |||
start: -10, | |||
end: 50 | |||
}, | |||
], | |||
datasets: [ | |||
{ | |||
title: "Some Data", | |||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
title: "Another Set", | |||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
title: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
} | |||
] | |||
}; | |||
let type_chart = new Chart({ | |||
parent: "#chart-types", | |||
title: "My Awesome Chart", | |||
data: type_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['light-blue', 'violet', 'blue'], | |||
is_series: 1, | |||
format_tooltip_x: d => (d + '').toUpperCase(), | |||
format_tooltip_y: d => d + ' pts' | |||
}); | |||
// let typeChart = new Chart("#chart-types", { | |||
// title: "My Awesome Chart", | |||
// data: typeData, | |||
// type: 'bar', | |||
// height: 250, | |||
// colors: ['purple', 'magenta', 'red'], | |||
// tooltipOptions: { | |||
// formatTooltipX: d => (d + '').toUpperCase(), | |||
// 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.get_different_chart(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 plot_chart_args = { | |||
parent: "#chart-trends", | |||
title: "Mean Total Sunspot Count - Yearly", | |||
data: trends_data, | |||
type: 'line', | |||
// Aggregation chart | |||
// ================================================================================ | |||
let args = { | |||
data: typeData, | |||
type: 'axis-mixed', | |||
height: 250, | |||
colors: ['blue'], | |||
is_series: 1, | |||
show_dots: 0, | |||
heatline: 1, | |||
x_axis_mode: 'tick', | |||
y_axis_mode: 'span' | |||
}; | |||
colors: ['purple', 'magenta', 'light-blue'], | |||
new Chart(plot_chart_args); | |||
maxLegendPoints: 6, | |||
maxSlices: 10, | |||
tooltipOptions: { | |||
formatTooltipX: d => (d + '').toUpperCase(), | |||
formatTooltipY: d => d + ' pts', | |||
} | |||
} | |||
let aggrChart = new Chart("#chart-aggr", args); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.chart-plot-buttons button') | |||
document.querySelectorAll('.aggr-type-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let type = btn.getAttribute('data-type'); | |||
let config = []; | |||
if(type === 'line') { | |||
config = [0, 0, 0]; | |||
} else if(type === 'region') { | |||
config = [0, 0, 1]; | |||
} else { | |||
config = [0, 1, 0]; | |||
} | |||
plot_chart_args.show_dots = config[0]; | |||
plot_chart_args.heatline = config[1]; | |||
plot_chart_args.region_fill = config[2]; | |||
plot_chart_args.init = false; | |||
new Chart(plot_chart_args); | |||
args.type = type; | |||
let newChart = new Chart("#chart-aggr", args);; | |||
if(newChart){ | |||
aggrChart = newChart; | |||
} | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
@@ -194,7 +226,9 @@ Array.prototype.slice.call( | |||
let update_data_all_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", | |||
"Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", | |||
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | |||
let update_data_all_values = Array.from({length: 30}, () => Math.floor(Math.random() * 75 - 15)); | |||
let getRandom = () => Math.floor(Math.random() * 75 - 15); | |||
let update_data_all_values = Array.from({length: 30}, getRandom); | |||
// We're gonna be shuffling this | |||
let update_data_all_indices = update_data_all_labels.map((d,i) => i); | |||
@@ -209,47 +243,135 @@ let update_data = { | |||
datasets: [{ | |||
"values": get_update_data(update_data_all_values) | |||
}], | |||
"specific_values": [ | |||
yMarkers: [ | |||
{ | |||
title: "Altitude", | |||
// title: "A very long text", | |||
line_type: "dashed", | |||
value: 38 | |||
label: "Altitude", | |||
value: 25, | |||
type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Range", | |||
start: 10, | |||
end: 45 | |||
}, | |||
] | |||
], | |||
}; | |||
let update_chart = new Chart({ | |||
parent: "#chart-update", | |||
let update_chart = new Chart("#chart-update", { | |||
data: update_data, | |||
type: 'line', | |||
height: 250, | |||
colors: ['red'], | |||
is_series: 1, | |||
region_fill: 1 | |||
colors: ['#ff6c03'], | |||
lineOptions: { | |||
// hideLine: 1, | |||
regionFill: 1 | |||
}, | |||
}); | |||
let chart_update_buttons = document.querySelector('.chart-update-buttons'); | |||
chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | |||
shuffle(update_data_all_indices); | |||
update_chart.update_values( | |||
[{values: get_update_data(update_data_all_values)}], | |||
update_data_all_labels.slice(0, 10) | |||
); | |||
let value = getRandom(); | |||
let start = getRandom(); | |||
let end = getRandom(); | |||
let data = { | |||
labels: update_data_all_labels.slice(0, 10), | |||
datasets: [{values: get_update_data(update_data_all_values)}], | |||
yMarkers: [ | |||
{ | |||
label: "Altitude", | |||
value: value, | |||
type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Range", | |||
start: start, | |||
end: end | |||
}, | |||
], | |||
} | |||
update_chart.update(data); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="add"]').addEventListener("click", (e) => { | |||
// NOTE: this ought to be problem, labels stay the same after update | |||
let index = update_chart.x.length; // last index to add | |||
let index = update_chart.state.datasetLength; // last index to add | |||
if(index >= update_data_all_indices.length) return; | |||
update_chart.add_data_point( | |||
[update_data_all_values[index]], update_data_all_labels[index] | |||
update_chart.addDataPoint( | |||
update_data_all_labels[index], [update_data_all_values[index]] | |||
); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("click", (e) => { | |||
update_chart.remove_data_point(); | |||
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: ['#238e38'], | |||
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'); | |||
}); | |||
}); | |||
@@ -293,14 +415,13 @@ let events_data = { | |||
] | |||
}; | |||
let events_chart = new Chart({ | |||
parent: "#chart-events", | |||
let events_chart = new Chart("#chart-events", { | |||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | |||
data: events_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['grey'], | |||
is_navigable: 1, | |||
isNavigable: 1, | |||
}); | |||
let data_div = document.querySelector('.chart-events-data'); | |||
@@ -314,68 +435,25 @@ events_chart.parent.addEventListener('data-select', (e) => { | |||
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
}); | |||
// Aggregation chart | |||
// ================================================================================ | |||
let aggr_data = { | |||
labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], | |||
datasets: [ | |||
{ | |||
"values": [25, 40, 30, 35, 8, 52, 17] | |||
}, | |||
{ | |||
"values": [25, 50, -10, 15, 18, 32, 27] | |||
} | |||
] | |||
}; | |||
let aggr_chart = new Chart({ | |||
parent: "#chart-aggr", | |||
data: aggr_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['purple', 'orange'], | |||
}); | |||
document.querySelector('[data-aggregation="sums"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Sums") { | |||
aggr_chart.show_sums(); | |||
e.target.innerHTML = "Hide Sums"; | |||
} else { | |||
aggr_chart.hide_sums(); | |||
e.target.innerHTML = "Show Sums"; | |||
} | |||
}); | |||
document.querySelector('[data-aggregation="average"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Averages") { | |||
aggr_chart.show_averages(); | |||
e.target.innerHTML = "Hide Averages"; | |||
} else { | |||
aggr_chart.hide_averages(); | |||
e.target.innerHTML = "Show Averages"; | |||
} | |||
}); | |||
// Heatmap | |||
// ================================================================================ | |||
let heatmap_data = {}; | |||
let heatmapData = {}; | |||
let current_date = new Date(); | |||
let timestamp = current_date.getTime()/1000; | |||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | |||
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); | |||
} | |||
new Chart({ | |||
parent: "#chart-heatmap", | |||
data: heatmap_data, | |||
new Chart("#chart-heatmap", { | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: 1 // default 0 | |||
discreteDomains: 1, | |||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'] | |||
}); | |||
Array.prototype.slice.call( | |||
@@ -384,10 +462,10 @@ Array.prototype.slice.call( | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let mode = btn.getAttribute('data-mode'); | |||
let discrete_domains = 0; | |||
let discreteDomains = 0; | |||
if(mode === 'discrete') { | |||
discrete_domains = 1; | |||
discreteDomains = 1; | |||
} | |||
let colors = []; | |||
@@ -398,14 +476,13 @@ Array.prototype.slice.call( | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
new Chart({ | |||
parent: "#chart-heatmap", | |||
data: heatmap_data, | |||
new Chart("#chart-heatmap", { | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
discreteDomains: discreteDomains, | |||
legendColors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
@@ -428,23 +505,22 @@ Array.prototype.slice.call( | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
let discrete_domains = 1; | |||
let discreteDomains = 1; | |||
let view_mode = document | |||
.querySelector('.heatmap-mode-buttons .active') | |||
.getAttribute('data-mode'); | |||
if(view_mode === 'continuous') { | |||
discrete_domains = 0; | |||
discreteDomains = 0; | |||
} | |||
new Chart({ | |||
parent: "#chart-heatmap", | |||
data: heatmap_data, | |||
new Chart("#chart-heatmap", { | |||
data: heatmapData, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
legendScale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
discreteDomains: discreteDomains, | |||
legendColors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
@@ -0,0 +1,559 @@ | |||
// Composite Chart | |||
// ================================================================================ | |||
let report_count_list = [17, 40, 33, 44, 126, 156, | |||
324, 333, 478, 495, 176]; | |||
let bar_composite_data = { | |||
labels: ["2007", "2008", "2009", "2010", "2011", "2012", | |||
"2013", "2014", "2015", "2016", "2017"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker 1", | |||
value: 420, | |||
}, | |||
{ | |||
label: "Marker 2", | |||
value: 250, | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region Y 1", | |||
start: 100, | |||
end: 300 | |||
}, | |||
], | |||
datasets: [{ | |||
"name": "Events", | |||
"values": report_count_list, | |||
// "formatted": report_count_list.map(d => d + " reports") | |||
}] | |||
}; | |||
let line_composite_data = { | |||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], | |||
datasets: [{ | |||
"values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0], | |||
// "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], | |||
// [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, 49, 0, 0], | |||
// [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40] | |||
// [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40] | |||
]; | |||
let c1 = document.querySelector("#chart-composite-1"); | |||
let c2 = document.querySelector("#chart-composite-2"); | |||
let bar_composite_chart = new Chart (c1, { | |||
title: "Fireball/Bolide Events - Yearly (more than 5 reports)", | |||
data: bar_composite_data, | |||
type: 'bar', | |||
height: 180, | |||
colors: ['orange'], | |||
isNavigable: 1, | |||
isSeries: 1, | |||
valuesOverPoints: 1, | |||
yAxisMode: 'tick' | |||
// regionFill: 1 | |||
}); | |||
let line_composite_chart = new Chart (c2, { | |||
data: line_composite_data, | |||
type: 'line', | |||
lineOptions: { | |||
dotSize: 10 | |||
}, | |||
height: 180, | |||
colors: ['green'], | |||
isSeries: 1, | |||
valuesOverPoints: 1, | |||
}); | |||
bar_composite_chart.parent.addEventListener('data-select', (e) => { | |||
line_composite_chart.updateDataset(more_line_data[e.index]); | |||
}); | |||
// Demo Chart (bar, linepts, scatter(blobs), percentage) | |||
// ================================================================================ | |||
let type_data = { | |||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | |||
yMarkers: [ | |||
{ | |||
label: "Marker 1", | |||
value: 42, | |||
type: 'dashed' | |||
}, | |||
{ | |||
label: "Marker 2", | |||
value: 25, | |||
type: 'dashed' | |||
} | |||
], | |||
yRegions: [ | |||
{ | |||
label: "Region Y 1", | |||
start: -10, | |||
end: 50 | |||
}, | |||
], | |||
// will depend on series code for calculating X values | |||
// xRegions: [ | |||
// { | |||
// label: "Region X 2", | |||
// start: , | |||
// end: , | |||
// } | |||
// ], | |||
datasets: [ | |||
{ | |||
name: "Some Data", | |||
values: [18, 40, 30, 35, 8, 52, 17, -4], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Another Set", | |||
values: [30, 50, -10, 15, 18, 32, 27, 14], | |||
axisPosition: 'right', | |||
chartType: 'bar' | |||
}, | |||
{ | |||
name: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37], | |||
chartType: 'line' | |||
} | |||
// temp : Stacked | |||
// { | |||
// name: "Some Data", | |||
// values:[25, 30, 50, 45, 18, 12, 27, 14] | |||
// }, | |||
// { | |||
// name: "Another Set", | |||
// values: [18, 20, 30, 35, 8, 7, 17, 4] | |||
// }, | |||
// { | |||
// name: "Another Set", | |||
// values: [11, 8, 19, 15, 3, 4, 10, 2] | |||
// }, | |||
] | |||
}; | |||
let type_chart = new Chart("#chart-types", { | |||
// title: "My Awesome Chart", | |||
data: type_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['purple', 'magenta', 'light-blue'], | |||
isSeries: 1, | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span', | |||
valuesOverPoints: 1, | |||
barOptions: { | |||
stacked: 1 | |||
} | |||
// formatTooltipX: d => (d + '').toUpperCase(), | |||
// 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 plot_chart_args = { | |||
title: "Mean Total Sunspot Count - Yearly", | |||
data: trends_data, | |||
type: 'line', | |||
height: 250, | |||
colors: ['blue'], | |||
isSeries: 1, | |||
lineOptions: { | |||
hideDots: 1, | |||
heatline: 1, | |||
}, | |||
xAxisMode: 'tick', | |||
yAxisMode: 'span' | |||
}; | |||
new Chart("#chart-trends", plot_chart_args); | |||
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 = []; | |||
if(type === 'line') { | |||
config = [0, 0, 0]; | |||
} else if(type === 'region') { | |||
config = [0, 0, 1]; | |||
} else { | |||
config = [0, 1, 0]; | |||
} | |||
plot_chart_args.hideDots = config[0]; | |||
plot_chart_args.heatline = config[1]; | |||
plot_chart_args.regionFill = config[2]; | |||
plot_chart_args.init = false; | |||
new Chart("#chart-trends", plot_chart_args); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
// Update values chart | |||
// ================================================================================ | |||
let update_data_all_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", | |||
"Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", | |||
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | |||
let update_data_all_values = Array.from({length: 30}, () => Math.floor(Math.random() * 75 - 15)); | |||
// We're gonna be shuffling this | |||
let update_data_all_indices = update_data_all_labels.map((d,i) => i); | |||
let get_update_data = (source_array, length=10) => { | |||
let indices = update_data_all_indices.slice(0, length); | |||
return indices.map((index) => source_array[index]); | |||
}; | |||
let update_data = { | |||
labels: get_update_data(update_data_all_labels), | |||
datasets: [{ | |||
"values": get_update_data(update_data_all_values) | |||
}], | |||
"specific_values": [ | |||
{ | |||
name: "Altitude", | |||
// name: "A very long text", | |||
line_type: "dashed", | |||
value: 38 | |||
}, | |||
] | |||
}; | |||
let update_chart = new Chart("#chart-update", { | |||
data: update_data, | |||
type: 'line', | |||
height: 250, | |||
colors: ['red'], | |||
isSeries: 1, | |||
lineOptions: { | |||
regionFill: 1 | |||
}, | |||
}); | |||
let chart_update_buttons = document.querySelector('.chart-update-buttons'); | |||
chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | |||
shuffle(update_data_all_indices); | |||
let data = { | |||
labels: update_data_all_labels.slice(0, 10), | |||
datasets: [{values: get_update_data(update_data_all_values)}], | |||
} | |||
update_chart.update(data); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="add"]').addEventListener("click", (e) => { | |||
let index = update_chart.state.datasetLength; // last index to add | |||
if(index >= update_data_all_indices.length) return; | |||
update_chart.addDataPoint( | |||
update_data_all_labels[index], [update_data_all_values[index]] | |||
); | |||
}); | |||
chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("click", (e) => { | |||
update_chart.removeDataPoint(); | |||
}); | |||
// Event chart | |||
// ================================================================================ | |||
let moon_names = ["Ganymede", "Callisto", "Io", "Europa"]; | |||
let masses = [14819000, 10759000, 8931900, 4800000]; | |||
let distances = [1070.412, 1882.709, 421.700, 671.034]; | |||
let diameters = [5262.4, 4820.6, 3637.4, 3121.6]; | |||
let jupiter_moons = { | |||
'Ganymede': { | |||
mass: '14819000 x 10^16 kg', | |||
'semi-major-axis': '1070412 km', | |||
'diameter': '5262.4 km' | |||
}, | |||
'Callisto': { | |||
mass: '10759000 x 10^16 kg', | |||
'semi-major-axis': '1882709 km', | |||
'diameter': '4820.6 km' | |||
}, | |||
'Io': { | |||
mass: '8931900 x 10^16 kg', | |||
'semi-major-axis': '421700 km', | |||
'diameter': '3637.4 km' | |||
}, | |||
'Europa': { | |||
mass: '4800000 x 10^16 kg', | |||
'semi-major-axis': '671034 km', | |||
'diameter': '3121.6 km' | |||
}, | |||
}; | |||
let events_data = { | |||
labels: ["Ganymede", "Callisto", "Io", "Europa"], | |||
datasets: [ | |||
{ | |||
"values": distances, | |||
"formatted": distances.map(d => d*1000 + " km") | |||
} | |||
] | |||
}; | |||
let events_chart = new Chart("#chart-events", { | |||
title: "Jupiter's Moons: Semi-major Axis (1000 km)", | |||
data: events_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['grey'], | |||
isNavigable: 1, | |||
}); | |||
let data_div = document.querySelector('.chart-events-data'); | |||
events_chart.parent.addEventListener('data-select', (e) => { | |||
let name = moon_names[e.index]; | |||
data_div.querySelector('.moon-name').innerHTML = name; | |||
data_div.querySelector('.semi-major-axis').innerHTML = distances[e.index] * 1000; | |||
data_div.querySelector('.mass').innerHTML = masses[e.index]; | |||
data_div.querySelector('.diameter').innerHTML = diameters[e.index]; | |||
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg"; | |||
}); | |||
// Aggregation chart | |||
// ================================================================================ | |||
let aggr_data = { | |||
labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], | |||
datasets: [ | |||
{ | |||
"values": [25, 40, 30, 35, 8, 52, 17] | |||
}, | |||
{ | |||
"values": [25, 50, 10, 15, 18, 32, 27], | |||
} | |||
] | |||
}; | |||
let aggr_chart = new Chart("#chart-aggr", { | |||
data: aggr_data, | |||
type: 'bar', | |||
height: 250, | |||
colors: ['light-green', 'blue'], | |||
valuesOverPoints: 1, | |||
barOptions: { | |||
// stacked: 1 | |||
} | |||
}); | |||
document.querySelector('[data-aggregation="sums"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Sums") { | |||
aggr_chart.show_sums(); | |||
e.target.innerHTML = "Hide Sums"; | |||
} else { | |||
aggr_chart.hide_sums(); | |||
e.target.innerHTML = "Show Sums"; | |||
} | |||
}); | |||
document.querySelector('[data-aggregation="average"]').addEventListener("click", (e) => { | |||
if(e.target.innerHTML === "Show Averages") { | |||
aggr_chart.show_averages(); | |||
e.target.innerHTML = "Hide Averages"; | |||
} else { | |||
aggr_chart.hide_averages(); | |||
e.target.innerHTML = "Show Averages"; | |||
} | |||
}); | |||
// Heatmap | |||
// ================================================================================ | |||
let heatmap_data = {}; | |||
let current_date = new Date(); | |||
let timestamp = current_date.getTime()/1000; | |||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | |||
for (var i = 0; i< 375; i++) { | |||
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||
timestamp = Math.floor(timestamp - 86400).toFixed(1); | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: 1 | |||
}); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.heatmap-mode-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let mode = btn.getAttribute('data-mode'); | |||
let discrete_domains = 0; | |||
if(mode === 'discrete') { | |||
discrete_domains = 1; | |||
} | |||
let colors = []; | |||
let colors_mode = document | |||
.querySelector('.heatmap-color-buttons .active') | |||
.getAttribute('data-color'); | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
Array.prototype.slice.call( | |||
document.querySelectorAll('.heatmap-color-buttons button') | |||
).map(el => { | |||
el.addEventListener('click', (e) => { | |||
let btn = e.target; | |||
let colors_mode = btn.getAttribute('data-color'); | |||
let colors = []; | |||
if(colors_mode === 'halloween') { | |||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | |||
} | |||
let discrete_domains = 1; | |||
let view_mode = document | |||
.querySelector('.heatmap-mode-buttons .active') | |||
.getAttribute('data-mode'); | |||
if(view_mode === 'continuous') { | |||
discrete_domains = 0; | |||
} | |||
new Chart("#chart-heatmap", { | |||
data: heatmap_data, | |||
type: 'heatmap', | |||
legend_scale: [0, 1, 2, 4, 5], | |||
height: 115, | |||
discrete_domains: discrete_domains, | |||
legend_colors: colors | |||
}); | |||
Array.prototype.slice.call( | |||
btn.parentNode.querySelectorAll('button')).map(el => { | |||
el.classList.remove('active'); | |||
}); | |||
btn.classList.add('active'); | |||
}); | |||
}); | |||
// Helpers | |||
// ================================================================================ | |||
function shuffle(array) { | |||
// https://stackoverflow.com/a/2450976/6495043 | |||
// Awesomeness: https://bost.ocks.org/mike/shuffle/ | |||
var currentIndex = array.length, temporaryValue, randomIndex; | |||
// While there remain elements to shuffle... | |||
while (0 !== currentIndex) { | |||
// Pick a remaining element... | |||
randomIndex = Math.floor(Math.random() * currentIndex); | |||
currentIndex -= 1; | |||
// And swap it with the current element. | |||
temporaryValue = array[currentIndex]; | |||
array[currentIndex] = array[randomIndex]; | |||
array[randomIndex] = temporaryValue; | |||
} | |||
return array; | |||
} | |||
@@ -18,13 +18,15 @@ | |||
<link rel="shortcut icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
<link rel="icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
<script async defer src="https://buttons.github.io/buttons.js"></script> | |||
</head> | |||
<body> | |||
<div class="container"> | |||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | |||
<div class="jumbotron" style="background: transparent;"> | |||
<h1>Frappé Charts</h1> | |||
<h1>Frappe Charts</h1> | |||
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p> | |||
<p class="mt-2">with zero dependencies.</p> | |||
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>--> | |||
@@ -44,68 +46,64 @@ | |||
<div class="col-sm-10 push-sm-1"> | |||
<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--> | |||
<div id="chart"></div></code></pre> | |||
<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"], | |||
datasets: [ | |||
{ | |||
title: "Some Data", | |||
values: [25, 40, 30, 35, 8, 52, 17, -4] | |||
}, | |||
{ | |||
title: "Another Set", | |||
values: [25, 50, -10, 15, 18, 32, 27, 14] | |||
}, | |||
{ | |||
title: "Yet Another", | |||
values: [15, 20, -3, -15, 58, 12, -17, 37] | |||
} | |||
] | |||
}; | |||
let chart = new Chart({ | |||
parent: "#chart", // or a DOM element | |||
title: "My Awesome Chart", | |||
data: data, | |||
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage' | |||
height: 250, | |||
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] | |||
} | |||
], | |||
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'] | |||
yMarkers: [{ label: "Marker", value: 70 }], | |||
yRegions: [{ label: "Region", start: -10, end: 50 }] | |||
}, | |||
format_tooltip_x: d => (d + '').toUpperCase(), | |||
format_tooltip_y: d => d + ' pts' | |||
title: "My Awesome Chart", | |||
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage' | |||
height: 250, | |||
colors: ['purple', '#ffa3ef', 'red'] | |||
});</code></pre> | |||
<div id="chart-types" class="border"></div> | |||
<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" data-type='line'>Line Chart</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='scatter'>Scatter Chart</button> | |||
<!-- <div id="chart-types" class="border" style="margin-bottom: 15px"></div> --> | |||
<!-- <div > | |||
<div class="btn-group x-axis-buttons margin-vertical-px" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-type='span'>X span</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='tick'>X tick</button> | |||
</div> | |||
<div class="btn-group y-axis-buttons margin-vertical-px" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-type='span'>Y span</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type='tick'>Y tick</button> | |||
</div> | |||
<div class="input-group input-group-sm"> | |||
<span class="input-group-addon">.00</span> | |||
<input type="text" class="form-control" aria-label="Amount (rounded to the nearest dollar)"> | |||
</div> | |||
</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='axis-mixed'>Mixed</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> | |||
</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> | |||
</p> | |||
</p> --> | |||
</div> | |||
</div> | |||
@@ -114,42 +112,12 @@ | |||
<h6 class="margin-vertical-rem"> | |||
Update Values | |||
</h6> | |||
<pre><code class="hljs javascript"> // Update entire datasets | |||
chart.update_values( | |||
[ | |||
{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 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="add">Add Value</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | |||
</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 = [ | |||
{ | |||
title: "Altitude", | |||
line_type: "dashed", // or "solid" | |||
value: 38 | |||
} | |||
] | |||
...</code></pre> | |||
</div> | |||
</div> | |||
@@ -158,25 +126,20 @@ | |||
<h6 class="margin-vertical-rem"> | |||
Plot Trends | |||
</h6> | |||
<pre><code class="hljs javascript"> ... | |||
x_axis_mode: 'tick', // for short label ticks | |||
// or 'span' for long spanning vertical axis lines | |||
y_axis_mode: 'span', // for long horizontal lines, or 'tick' | |||
is_series: 1, // to allow for skipping of X values | |||
...</code></pre> | |||
<div id="chart-trends" class="border"></div> | |||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button> | |||
<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="hideLine">Dots</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="region">Region</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button> | |||
</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: | |||
show_dots: 0, // Show data points on the line; default 1 | |||
hideDots: 1, // Hide data points on the line; default 0 | |||
heatline: 1, // Show a value-wise line gradient; default 0 | |||
region_fill: 1, // Fill the area under the graph; default 0 | |||
...</code></pre> | |||
regionFill: 1, // Fill the area under the graph; default 0 | |||
...</code></pre> --> | |||
</div> | |||
</div> | |||
@@ -204,8 +167,7 @@ | |||
</div> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
type: 'bar', // Bar Chart specific properties: | |||
is_navigable: 1, // Navigate across bars; default 0 | |||
isNavigable: 1, // Navigate across data points; default 0 | |||
... | |||
chart.parent.addEventListener('data-select', (e) => { | |||
@@ -214,22 +176,6 @@ | |||
</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="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
@@ -242,23 +188,23 @@ | |||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | |||
</div> | |||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="default">Default green</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="default">Default green</button> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="halloween">GitHub's Halloween</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({ | |||
parent: "#heatmap", | |||
type: 'heatmap', | |||
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; | |||
// default: today's date in past year | |||
// for an annual heatmap | |||
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||
// Set of five incremental colors, | |||
// beginning with a low-saturation color for zero data; | |||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||
@@ -267,13 +213,97 @@ | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem">Available options:</h6> | |||
<pre><code class="hljs javascript"> | |||
... | |||
{ | |||
data: { | |||
labels: [], | |||
datasets: [], | |||
yRegions: [], | |||
yMarkers: [] | |||
} | |||
title: '', | |||
colors: [], | |||
height: 200, | |||
tooltipOptions: { | |||
formatTooltipX: d => (d + '').toUpperCase(), | |||
formatTooltipY: d => d + ' pts', | |||
} | |||
// Axis charts | |||
isNavigable: 1, // default: 0 | |||
valuesOverPoints: 1, // default: 0 | |||
barOptions: { | |||
stacked: 1 // default: 0 | |||
} | |||
lineOptions: { | |||
dotSize: 6, // default: 4 | |||
hideLine: 0, // default: 0 | |||
hideDots: 1, // default: 0 | |||
heatline: 1, // default: 0 | |||
regionFill: 1 // default: 0 | |||
} | |||
axisOptions: { | |||
yAxisMode: 'span', // Axis lines, default | |||
xAxisMode: 'tick', // No axis lines, only short ticks | |||
xIsSeries: 1 // Allow skipping x values for space | |||
// default: 0 | |||
}, | |||
// Pie/Percentage charts | |||
maxLegendPoints: 6, // default: 20 | |||
maxSlices: 10, // default: 20 | |||
// Heatmap | |||
discreteDomains: 1, // default: 1 | |||
start: startDate, // Date object | |||
legendColors: [] | |||
} | |||
... | |||
// Updating values | |||
chart.update(data); | |||
// Axis charts: | |||
chart.addDataPoint(label, valueFromEachDataset, index) | |||
chart.removeDataPoint(index) | |||
chart.updateDataset(datasetValues, index) | |||
</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="dashboard-section"> | |||
<!-- Closing --> | |||
<div class="text-center" style="margin-top: 70px"> | |||
<a href="https://github.com/frappe/charts/archive/master.zip"><button class="large blue button">Download</button></a> | |||
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p> | |||
<p style="margin-top: 1rem;"><iframe src="https://ghbtns.com/github-btn.html?user=frappe&repo=charts&type=star&count=true" frameborder="0" scrolling="0" width="94px" height="20px"></iframe></p> | |||
<p style="margin-top: 1rem;"> | |||
<a class="github-button" href="https://github.com/frappe/charts" data-icon="octicon-star" data-show-count="true" aria-label="Star frappe/charts on GitHub">Star</a> | |||
</p> | |||
<p>License: MIT</p> | |||
</div> | |||
</div> | |||
@@ -285,7 +315,7 @@ | |||
</div> | |||
<div class="built-with-frappe text-center" style="margin-top: -20px"> | |||
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="https://frappe.github.io/frappe/assets/img/frappe-bird-grey.svg"> | |||
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="./assets/img/frappe-bird.png"> | |||
<p style="margin: 24px 0 0px 0; font-size: 15px"> | |||
Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>. | |||
Used in <a href="https://erpnext.com" target="_blank">ERPNext</a>. | |||
@@ -0,0 +1,312 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Frappe Charts</title> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<meta name="keywords" content="open source javascript js charts library svg zero-dependency interactive data visualization beautiful drag resize"> | |||
<meta name="description" content="A simple, responsive, modern charts library for the web."> | |||
<link rel="stylesheet" type="text/css" href="assets/css/normalize.css" media="screen"> | |||
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700' rel='stylesheet' type='text/css'> | |||
<link rel="stylesheet" type="text/css" href="assets/css/bootstrap.min.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/frappe_theme.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/index.css" media="screen"> | |||
<link rel="stylesheet" type="text/css" href="assets/css/default.css" media="screen"> | |||
<script src="assets/js/highlight.pack.js"></script> | |||
<script>hljs.initHighlightingOnLoad();</script> | |||
<link rel="shortcut icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
<link rel="icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon"> | |||
</head> | |||
<body> | |||
<div class="container"> | |||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | |||
<div class="jumbotron" style="background: transparent;"> | |||
<h1>Frappe Charts</h1> | |||
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p> | |||
<p class="mt-2">with zero dependencies.</p> | |||
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>--> | |||
</div> | |||
<div class="col-sm-10 push-sm-1 later" style="font-size: 14px;"> | |||
<div id="chart-composite-1" class="border"><svg height=225></svg></div> | |||
<p class="mt-1">Click or use arrow keys to navigate data points</p> | |||
</div> | |||
<div class="col-sm-10 push-sm-1 later" style="font-size: 14px;"> | |||
<div id="chart-composite-2" class="border"><svg height=225></svg></div> | |||
</div> | |||
</div> | |||
<div class="group later"> | |||
<div class="row section"> | |||
<div class="col-sm-10 push-sm-1"> | |||
<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> | |||
<pre><code class="hljs html"> <!--HTML--> | |||
<div id="chart"></div></code></pre> | |||
<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", | |||
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] | |||
} | |||
] | |||
}; | |||
let chart = new Chart({ | |||
parent: "#chart", // or a DOM element | |||
title: "My Awesome Chart", | |||
data: data, | |||
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage' | |||
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' | |||
});</code></pre> | |||
<div id="chart-types" class="border"></div> | |||
<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" 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='percentage'>Percentage Chart</button> | |||
</div> | |||
<p class="text-muted"> | |||
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | |||
</p> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Update Values | |||
</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 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="add">Add Value</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | |||
</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 class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Plot Trends | |||
</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 class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-type="dots">Dots</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="region">Region</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
type: 'line', // Line Chart specific properties: | |||
hideDots: 1, // Hide data points on the line; default 0 | |||
heatline: 1, // Show a value-wise line gradient; default 0 | |||
regionFill: 1, // Fill the area under the graph; default 0 | |||
...</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
Listen to state change | |||
</h6> | |||
<div class="row"> | |||
<div class="col-sm-8"> | |||
<div id="chart-events" class="border"></div> | |||
</div> | |||
<div class="col-sm-4"> | |||
<div class="chart-events-data" class="border data-container"> | |||
<div class="image-container border"> | |||
<img class="moon-image" src="./assets/img/europa.jpg"> | |||
</div> | |||
<div class="data margin-vertical-px"> | |||
<h6 class="moon-name">Europa</h6> | |||
<p>Semi-major-axis: <span class="semi-major-axis">671034</span> km</p> | |||
<p>Mass: <span class="mass">4800000</span> x 10^16 kg</p> | |||
<p>Diameter: <span class="diameter">3121.6</span> km</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||
type: 'bar', // Bar Chart specific properties: | |||
isNavigable: 1, // Navigate across bars; default 0 | |||
... | |||
chart.parent.addEventListener('data-select', (e) => { | |||
update_moon_data(e.index); // e contains index and value of current datapoint | |||
});</code></pre> | |||
</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="dashboard-section"> | |||
<h6 class="margin-vertical-rem"> | |||
And a Month-wise Heatmap | |||
</h6> | |||
<div id="chart-heatmap" class="border" | |||
style="overflow: scroll; text-align: center; padding: 20px;"></div> | |||
<div class="heatmap-mode-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-mode="discrete">Discrete</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | |||
</div> | |||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | |||
<button type="button" class="btn btn-sm btn-secondary active" data-color="default">Default green</button> | |||
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button> | |||
</div> | |||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({ | |||
parent: "#heatmap", | |||
type: 'heatmap', | |||
height: 115, | |||
data: heatmap_data, // object with date/timestamp-value pairs | |||
discrete_domains: 1 // default: 0 | |||
start: start_date, | |||
// A Date object; | |||
// default: today's date in past year | |||
// for an annual heatmap | |||
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||
// Set of five incremental colors, | |||
// beginning with a low-saturation color for zero data; | |||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | |||
});</code></pre> | |||
</div> | |||
</div> | |||
<div class="col-sm-10 push-sm-1"> | |||
<div class="dashboard-section"> | |||
<!-- Closing --> | |||
<div class="text-center" style="margin-top: 70px"> | |||
<a href="https://github.com/frappe/charts/archive/master.zip"><button class="large blue button">Download</button></a> | |||
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p> | |||
<p style="margin-top: 1rem;"><iframe src="https://ghbtns.com/github-btn.html?user=frappe&repo=charts&type=star&count=true" frameborder="0" scrolling="0" width="94px" height="20px"></iframe></p> | |||
<p>License: MIT</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="built-with-frappe text-center" style="margin-top: -20px"> | |||
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="https://frappe.github.io/frappe/assets/img/frappe-bird-grey.svg"> | |||
<p style="margin: 24px 0 0px 0; font-size: 15px"> | |||
Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>. | |||
Used in <a href="https://erpnext.com" target="_blank">ERPNext</a>. | |||
Read the <a href="https://medium.com/@pratu16x7/so-we-decided-to-create-our-own-charts-a95cb5032c97" target="_blank">blog post</a>. | |||
</p> | |||
<p style="margin: 24px 0 80px 0; font-size: 12px"> | |||
Data from the <a href="https://www.amsmeteors.org" target="_blank">American Meteor Society</a>, | |||
<a href="http://www.sidc.be/silso" target="_blank">SILSO</a> and | |||
<a href="https://api.nasa.gov/index.html" target="_blank">NASA Open APIs</a> | |||
</p> | |||
</div> | |||
<a href="https://github.com/frappe/charts" target="_blank" class="github-corner" aria-label="View source on Github"> | |||
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#9a9a9a; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"> | |||
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path> | |||
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path> | |||
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path> | |||
</svg> | |||
</a> | |||
<script src="assets/js/frappe-charts.min.js"></script> | |||
<script src="assets/js/old_index.js"></script> | |||
</body> | |||
</html> |
@@ -52,5 +52,8 @@ | |||
"rollup-plugin-uglify": "^2.0.1", | |||
"rollup-plugin-uglify-es": "0.0.1", | |||
"rollup-watch": "^4.3.1" | |||
}, | |||
"dependencies": { | |||
"eslint": "^4.18.2" | |||
} | |||
} |
@@ -15,17 +15,19 @@ import pkg from './package.json'; | |||
export default [ | |||
{ | |||
input: 'src/js/charts.js', | |||
input: 'src/js/chart.js', | |||
sourcemap: true, | |||
output: [ | |||
{ | |||
file: pkg.main, | |||
format: 'cjs', | |||
file: 'docs/assets/js/frappe-charts.min.js', | |||
format: 'iife', | |||
}, | |||
{ | |||
file: pkg.module, | |||
format: 'es', | |||
file: pkg.browser, | |||
format: 'iife', | |||
} | |||
], | |||
name: 'Chart', | |||
plugins: [ | |||
postcss({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
@@ -33,7 +35,6 @@ export default [ | |||
resolve({ code: result.css.toString() }) | |||
}), | |||
extensions: [ '.scss' ], | |||
// extract: 'dist/frappe-charts.min.css', | |||
plugins: [ | |||
nested(), | |||
cssnext({ warnForDuplicates: false }), | |||
@@ -56,10 +57,14 @@ export default [ | |||
] | |||
}, | |||
{ | |||
input: 'src/js/charts.js', | |||
input: 'src/js/chart.js', | |||
output: [ | |||
{ | |||
file: pkg.src, | |||
file: pkg.main, | |||
format: 'cjs', | |||
}, | |||
{ | |||
file: pkg.module, | |||
format: 'es', | |||
} | |||
], | |||
@@ -70,7 +75,6 @@ export default [ | |||
resolve({ code: result.css.toString() }) | |||
}), | |||
extensions: [ '.scss' ], | |||
extract: 'dist/frappe-charts.min.css', | |||
plugins: [ | |||
nested(), | |||
cssnext({ warnForDuplicates: false }), | |||
@@ -82,25 +86,24 @@ export default [ | |||
'src/scss/**', | |||
] | |||
}), | |||
babel({ | |||
exclude: 'node_modules/**', | |||
}), | |||
replace({ | |||
exclude: 'node_modules/**', | |||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | |||
}) | |||
}), | |||
uglify() | |||
], | |||
}, | |||
{ | |||
input: 'src/js/charts.js', | |||
input: 'src/js/chart.js', | |||
output: [ | |||
{ | |||
file: 'docs/assets/js/frappe-charts.min.js', | |||
format: 'iife', | |||
}, | |||
{ | |||
file: pkg.browser, | |||
format: 'iife', | |||
file: pkg.src, | |||
format: 'es', | |||
} | |||
], | |||
name: 'Chart', | |||
plugins: [ | |||
postcss({ | |||
preprocessor: (content, id) => new Promise((resolve, reject) => { | |||
@@ -108,6 +111,7 @@ export default [ | |||
resolve({ code: result.css.toString() }) | |||
}), | |||
extensions: [ '.scss' ], | |||
extract: 'dist/frappe-charts.min.css', | |||
plugins: [ | |||
nested(), | |||
cssnext({ warnForDuplicates: false }), | |||
@@ -119,14 +123,10 @@ export default [ | |||
'src/scss/**', | |||
] | |||
}), | |||
babel({ | |||
exclude: 'node_modules/**', | |||
}), | |||
replace({ | |||
exclude: 'node_modules/**', | |||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | |||
}), | |||
uglify() | |||
}) | |||
], | |||
} | |||
]; |
@@ -0,0 +1,40 @@ | |||
import '../scss/charts.scss'; | |||
// import MultiAxisChart from './charts/MultiAxisChart'; | |||
import PercentageChart from './charts/PercentageChart'; | |||
import PieChart from './charts/PieChart'; | |||
import Heatmap from './charts/Heatmap'; | |||
import AxisChart from './charts/AxisChart'; | |||
const chartTypes = { | |||
// multiaxis: MultiAxisChart, | |||
percentage: PercentageChart, | |||
heatmap: Heatmap, | |||
pie: PieChart | |||
}; | |||
function getChartByType(chartType = 'line', parent, options) { | |||
if(chartType === 'line') { | |||
options.type = 'line'; | |||
return new AxisChart(parent, options); | |||
} else if (chartType === 'bar') { | |||
options.type = 'bar'; | |||
return new AxisChart(parent, options); | |||
} else if (chartType === 'axis-mixed') { | |||
options.type = 'line'; | |||
return new AxisChart(parent, options); | |||
} | |||
if (!chartTypes[chartType]) { | |||
console.error("Undefined chart type: " + chartType); | |||
return; | |||
} | |||
return new chartTypes[chartType](parent, options); | |||
} | |||
export default class Chart { | |||
constructor(parent, options) { | |||
return getChartByType(options.type, parent, options); | |||
} | |||
} |
@@ -1,39 +0,0 @@ | |||
import '../scss/charts.scss'; | |||
import BarChart from './charts/BarChart'; | |||
import LineChart from './charts/LineChart'; | |||
import ScatterChart from './charts/ScatterChart'; | |||
import PercentageChart from './charts/PercentageChart'; | |||
import PieChart from './charts/PieChart'; | |||
import Heatmap from './charts/Heatmap'; | |||
// if (ENV !== 'production') { | |||
// // Enable LiveReload | |||
// document.write( | |||
// '<script src="http://' + (location.host || 'localhost').split(':')[0] + | |||
// ':35729/livereload.js?snipver=1"></' + 'script>' | |||
// ); | |||
// } | |||
const chartTypes = { | |||
line: LineChart, | |||
bar: BarChart, | |||
scatter: ScatterChart, | |||
percentage: PercentageChart, | |||
heatmap: Heatmap, | |||
pie: PieChart | |||
}; | |||
function getChartByType(chartType = 'line', options) { | |||
if (!chartTypes[chartType]) { | |||
return new LineChart(options); | |||
} | |||
return new chartTypes[chartType](options); | |||
} | |||
export default class Chart { | |||
constructor(args) { | |||
return getChartByType(args.type, arguments[0]); | |||
} | |||
} |
@@ -0,0 +1,72 @@ | |||
import BaseChart from './BaseChart'; | |||
import { $, getOffset } from '../utils/dom'; | |||
export default class AggregationChart extends BaseChart { | |||
constructor(parent, args) { | |||
super(parent, args); | |||
} | |||
configure(args) { | |||
super.configure(args); | |||
this.config.maxSlices = args.maxSlices || 20; | |||
this.config.maxLegendPoints = args.maxLegendPoints || 20; | |||
} | |||
calc() { | |||
let s = this.state; | |||
let maxSlices = this.config.maxSlices; | |||
s.sliceTotals = []; | |||
let allTotals = this.data.labels.map((label, i) => { | |||
let total = 0; | |||
this.data.datasets.map(e => { | |||
total += e.values[i]; | |||
}); | |||
return [total, label]; | |||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||
let totals = allTotals; | |||
if(allTotals.length > maxSlices) { | |||
// Prune and keep a grey area for rest as per maxSlices | |||
allTotals.sort((a, b) => { return b[0] - a[0]; }); | |||
totals = allTotals.slice(0, maxSlices-1); | |||
let remaining = allTotals.slice(maxSlices-1); | |||
let sumOfRemaining = 0; | |||
remaining.map(d => {sumOfRemaining += d[0];}); | |||
totals.push([sumOfRemaining, 'Rest']); | |||
this.colors[maxSlices-1] = 'grey'; | |||
} | |||
s.labels = []; | |||
totals.map(d => { | |||
s.sliceTotals.push(d[0]); | |||
s.labels.push(d[1]); | |||
}); | |||
} | |||
renderLegend() { | |||
let s = this.state; | |||
this.statsWrapper.textContent = ''; | |||
this.legendTotals = s.sliceTotals.slice(0, this.config.maxLegendPoints); | |||
let xValues = s.labels; | |||
this.legendTotals.map((d, i) => { | |||
if(d) { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
inside: this.statsWrapper | |||
}); | |||
stats.innerHTML = `<span class="indicator"> | |||
<i style="background: ${this.colors[i]}"></i> | |||
<span class="text-muted">${xValues[i]}:</span> | |||
${d} | |||
</span>`; | |||
} | |||
}); | |||
} | |||
} |
@@ -1,81 +0,0 @@ | |||
import AxisChart from './AxisChart'; | |||
export default class BarChart extends AxisChart { | |||
constructor(args) { | |||
super(args); | |||
this.type = 'bar'; | |||
this.x_axis_mode = args.x_axis_mode || 'tick'; | |||
this.y_axis_mode = args.y_axis_mode || 'span'; | |||
this.setup(); | |||
} | |||
setup_values() { | |||
super.setup_values(); | |||
this.x_offset = this.avg_unit_width; | |||
this.unit_args = { | |||
type: 'bar', | |||
args: { | |||
spaceWidth: this.avg_unit_width/2, | |||
} | |||
}; | |||
} | |||
make_overlay() { | |||
// Just make one out of the first element | |||
let index = this.x.length - 1; | |||
let unit = this.y[0].svg_units[index]; | |||
this.update_current_data_point(index); | |||
if(this.overlay) { | |||
this.overlay.parentNode.removeChild(this.overlay); | |||
} | |||
this.overlay = unit.cloneNode(); | |||
this.overlay.style.fill = '#000000'; | |||
this.overlay.style.opacity = '0.4'; | |||
this.draw_area.appendChild(this.overlay); | |||
} | |||
bind_overlay() { | |||
// on event, update overlay | |||
this.parent.addEventListener('data-select', (e) => { | |||
this.update_overlay(e.svg_unit); | |||
}); | |||
} | |||
bind_units(units_array) { | |||
units_array.map(unit => { | |||
unit.addEventListener('click', () => { | |||
let index = unit.getAttribute('data-point-index'); | |||
this.update_current_data_point(index); | |||
}); | |||
}); | |||
} | |||
update_overlay(unit) { | |||
let attributes = []; | |||
Object.keys(unit.attributes).map(index => { | |||
attributes.push(unit.attributes[index]); | |||
}); | |||
attributes.filter(attr => attr.specified).map(attr => { | |||
this.overlay.setAttribute(attr.name, attr.nodeValue); | |||
}); | |||
this.overlay.style.fill = '#000000'; | |||
this.overlay.style.opacity = '0.4'; | |||
} | |||
on_left_arrow() { | |||
this.update_current_data_point(this.current_index - 1); | |||
} | |||
on_right_arrow() { | |||
this.update_current_data_point(this.current_index + 1); | |||
} | |||
set_avg_unit_width_and_x_offset() { | |||
this.avg_unit_width = this.width/(this.x.length + 1); | |||
this.x_offset = this.avg_unit_width; | |||
} | |||
} |
@@ -2,177 +2,106 @@ import SvgTip from '../objects/SvgTip'; | |||
import { $, isElementInViewport, getElementContentWidth } from '../utils/dom'; | |||
import { makeSVGContainer, makeSVGDefs, makeSVGGroup } from '../utils/draw'; | |||
import { getStringWidth } from '../utils/helpers'; | |||
import { VERT_SPACE_OUTSIDE_BASE_CHART, TRANSLATE_Y_BASE_CHART, LEFT_MARGIN_BASE_CHART, | |||
RIGHT_MARGIN_BASE_CHART, INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT } from '../utils/constants'; | |||
import { getColor, DEFAULT_COLORS } from '../utils/colors'; | |||
import Chart from '../charts'; | |||
const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie']; | |||
const COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter', 'percentage', 'pie'], | |||
line: ['scatter', 'bar', 'percentage', 'pie'], | |||
pie: ['line', 'scatter', 'percentage', 'bar'], | |||
scatter: ['line', 'bar', 'percentage', 'pie'], | |||
percentage: ['bar', 'line', 'scatter', 'pie'], | |||
heatmap: [] | |||
}; | |||
// TODO: Needs structure as per only labels/datasets | |||
const COLOR_COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter'], | |||
line: ['scatter', 'bar'], | |||
pie: ['percentage'], | |||
scatter: ['line', 'bar'], | |||
percentage: ['pie'], | |||
heatmap: [] | |||
}; | |||
import { getDifferentChart } from '../config'; | |||
import { runSMILAnimation } from '../utils/animation'; | |||
export default class BaseChart { | |||
constructor({ | |||
height = 240, | |||
title = '', | |||
subtitle = '', | |||
colors = [], | |||
summary = [], | |||
is_navigable = 0, | |||
has_legend = 0, | |||
type = '', | |||
parent, | |||
data | |||
}) { | |||
this.raw_chart_args = arguments[0]; | |||
constructor(parent, options) { | |||
this.rawChartArgs = options; | |||
this.parent = typeof parent === 'string' ? document.querySelector(parent) : parent; | |||
this.title = title; | |||
this.subtitle = subtitle; | |||
if (!(this.parent instanceof HTMLElement)) { | |||
throw new Error('No `parent` element to render on was provided.'); | |||
} | |||
this.data = data; | |||
this.title = options.title || ''; | |||
this.subtitle = options.subtitle || ''; | |||
this.argHeight = options.height || 240; | |||
this.type = options.type || ''; | |||
this.realData = this.prepareData(options.data); | |||
this.data = this.prepareFirstData(this.realData); | |||
this.colors = []; | |||
this.config = { | |||
showTooltip: 1, // calculate | |||
showLegend: options.showLegend || 1, | |||
isNavigable: options.isNavigable || 0, | |||
animate: 1 | |||
}; | |||
this.state = {}; | |||
this.options = {}; | |||
this.specific_values = data.specific_values || []; | |||
this.summary = summary; | |||
this.initTimeout = INIT_CHART_UPDATE_TIMEOUT; | |||
this.is_navigable = is_navigable; | |||
if(this.is_navigable) { | |||
this.current_index = 0; | |||
if(this.config.isNavigable) { | |||
this.overlays = []; | |||
} | |||
this.has_legend = has_legend; | |||
this.setColors(colors, type); | |||
this.set_margins(height); | |||
this.configure(options); | |||
} | |||
get_different_chart(type) { | |||
if(type === this.type) return; | |||
if(!ALL_CHART_TYPES.includes(type)) { | |||
console.error(`'${type}' is not a valid chart type.`); | |||
} | |||
if(!COMPATIBLE_CHARTS[this.type].includes(type)) { | |||
console.error(`'${this.type}' chart cannot be converted to a '${type}' chart.`); | |||
} | |||
configure(args) { | |||
this.setColors(); | |||
this.setMargins(); | |||
// whether the new chart can use the existing colors | |||
const use_color = COLOR_COMPATIBLE_CHARTS[this.type].includes(type); | |||
// Okay, this is anticlimactic | |||
// this function will need to actually be 'change_chart_type(type)' | |||
// that will update only the required elements, but for now ... | |||
return new Chart({ | |||
parent: this.raw_chart_args.parent, | |||
title: this.title, | |||
data: this.raw_chart_args.data, | |||
type: type, | |||
height: this.raw_chart_args.height, | |||
colors: use_color ? this.colors : undefined | |||
}); | |||
// Bind window events | |||
window.addEventListener('resize', () => this.draw(true)); | |||
window.addEventListener('orientationchange', () => this.draw(true)); | |||
} | |||
setColors(colors, type) { | |||
this.colors = colors; | |||
setColors() { | |||
let args = this.rawChartArgs; | |||
// TODO: Needs structure as per only labels/datasets | |||
const list = type === 'percentage' || type === 'pie' | |||
? this.data.labels | |||
: this.data.datasets; | |||
// Needs structure as per only labels/datasets, from config | |||
const list = args.type === 'percentage' || args.type === 'pie' | |||
? args.data.labels | |||
: args.data.datasets; | |||
if(!this.colors || (list && this.colors.length < list.length)) { | |||
if(!args.colors || (list && args.colors.length < list.length)) { | |||
this.colors = DEFAULT_COLORS; | |||
} else { | |||
this.colors = args.colors; | |||
} | |||
this.colors = this.colors.map(color => getColor(color)); | |||
} | |||
set_margins(height) { | |||
this.base_height = height; | |||
this.height = height - 40; | |||
this.translate_x = 60; | |||
this.translate_y = 10; | |||
} | |||
setMargins() { | |||
let height = this.argHeight; | |||
this.baseHeight = height; | |||
this.height = height - VERT_SPACE_OUTSIDE_BASE_CHART; | |||
this.translateY = TRANSLATE_Y_BASE_CHART; | |||
setup() { | |||
if(!this.parent) { | |||
console.error("No parent element to render on was provided."); | |||
return; | |||
} | |||
if(this.validate_and_prepare_data()) { | |||
this.bind_window_events(); | |||
this.refresh(true); | |||
} | |||
// Horizontal margins | |||
this.leftMargin = LEFT_MARGIN_BASE_CHART; | |||
this.rightMargin = RIGHT_MARGIN_BASE_CHART; | |||
} | |||
validate_and_prepare_data() { | |||
validate() { | |||
return true; | |||
} | |||
bind_window_events() { | |||
window.addEventListener('resize', () => this.refresh()); | |||
window.addEventListener('orientationchange', () => this.refresh()); | |||
setup() { | |||
if(this.validate()) { | |||
this._setup(); | |||
} | |||
} | |||
refresh(init=false) { | |||
this.setup_base_values(); | |||
this.set_width(); | |||
this.setup_container(); | |||
this.setup_components(); | |||
this.setup_values(); | |||
this.setup_utils(); | |||
_setup() { | |||
this.makeContainer(); | |||
this.makeTooltip(); | |||
this.make_graph_components(init); | |||
this.make_tooltip(); | |||
if(this.summary.length > 0) { | |||
this.show_custom_summary(); | |||
} else { | |||
this.show_summary(); | |||
} | |||
if(this.is_navigable) { | |||
this.setup_navigation(init); | |||
} | |||
this.draw(false, true); | |||
} | |||
set_width() { | |||
let special_values_width = 0; | |||
let char_width = 8; | |||
this.specific_values.map(val => { | |||
let str_width = getStringWidth((val.title + ""), char_width); | |||
if(str_width > special_values_width) { | |||
special_values_width = str_width - 40; | |||
} | |||
}); | |||
this.base_width = getElementContentWidth(this.parent) - special_values_width; | |||
this.width = this.base_width - this.translate_x * 2; | |||
setupComponents() { | |||
this.components = new Map(); | |||
} | |||
setup_base_values() {} | |||
setup_container() { | |||
makeContainer() { | |||
this.container = $.create('div', { | |||
className: 'chart-container', | |||
innerHTML: `<h6 class="title">${this.title}</h6> | |||
@@ -185,120 +114,175 @@ export default class BaseChart { | |||
this.parent.innerHTML = ''; | |||
this.parent.appendChild(this.container); | |||
this.chart_wrapper = this.container.querySelector('.frappe-chart'); | |||
this.stats_wrapper = this.container.querySelector('.graph-stats-container'); | |||
this.chartWrapper = this.container.querySelector('.frappe-chart'); | |||
this.statsWrapper = this.container.querySelector('.graph-stats-container'); | |||
} | |||
this.make_chart_area(); | |||
this.make_draw_area(); | |||
makeTooltip() { | |||
this.tip = new SvgTip({ | |||
parent: this.chartWrapper, | |||
colors: this.colors | |||
}); | |||
this.bindTooltip(); | |||
} | |||
make_chart_area() { | |||
this.svg = makeSVGContainer( | |||
this.chart_wrapper, | |||
'chart', | |||
this.base_width, | |||
this.base_height | |||
); | |||
this.svg_defs = makeSVGDefs(this.svg); | |||
return this.svg; | |||
bindTooltip() {} | |||
draw(onlyWidthChange=false, init=false) { | |||
this.calcWidth(); | |||
this.calc(onlyWidthChange); | |||
this.makeChartArea(); | |||
this.setupComponents(); | |||
this.components.forEach(c => c.setup(this.drawArea)); | |||
// this.components.forEach(c => c.make()); | |||
this.render(this.components, false); | |||
if(init) { | |||
this.data = this.realData; | |||
setTimeout(() => {this.update();}, this.initTimeout); | |||
} | |||
if(!onlyWidthChange) { | |||
this.renderLegend(); | |||
} | |||
this.setupNavigation(init); | |||
} | |||
make_draw_area() { | |||
this.draw_area = makeSVGGroup( | |||
this.svg, | |||
this.type + '-chart', | |||
`translate(${this.translate_x}, ${this.translate_y})` | |||
); | |||
calcWidth() { | |||
this.baseWidth = getElementContentWidth(this.parent); | |||
this.width = this.baseWidth - (this.leftMargin + this.rightMargin); | |||
} | |||
setup_components() { } | |||
update(data=this.data) { | |||
this.data = this.prepareData(data); | |||
this.calc(); // builds state | |||
this.render(); | |||
} | |||
make_tooltip() { | |||
this.tip = new SvgTip({ | |||
parent: this.chart_wrapper, | |||
colors: this.colors | |||
}); | |||
this.bind_tooltip(); | |||
prepareData(data=this.data) { | |||
return data; | |||
} | |||
prepareFirstData(data=this.data) { | |||
return data; | |||
} | |||
show_summary() {} | |||
show_custom_summary() { | |||
this.summary.map(d => { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
innerHTML: `<span class="indicator"> | |||
<i style="background:${d.color}"></i> | |||
${d.title}: ${d.value} | |||
</span>` | |||
}); | |||
this.stats_wrapper.appendChild(stats); | |||
calc() {} // builds state | |||
render(components=this.components, animate=true) { | |||
if(this.config.isNavigable) { | |||
// Remove all existing overlays | |||
this.overlays.map(o => o.parentNode.removeChild(o)); | |||
// ref.parentNode.insertBefore(element, ref); | |||
} | |||
let elementsToAnimate = []; | |||
// Can decouple to this.refreshComponents() first to save animation timeout | |||
components.forEach(c => { | |||
elementsToAnimate = elementsToAnimate.concat(c.update(animate)); | |||
}); | |||
if(elementsToAnimate.length > 0) { | |||
runSMILAnimation(this.chartWrapper, this.svg, elementsToAnimate); | |||
setTimeout(() => { | |||
components.forEach(c => c.make()); | |||
this.updateNav(); | |||
}, CHART_POST_ANIMATE_TIMEOUT); | |||
} else { | |||
components.forEach(c => c.make()); | |||
this.updateNav(); | |||
} | |||
} | |||
updateNav() { | |||
if(this.config.isNavigable) { | |||
// if(!this.overlayGuides){ | |||
this.makeOverlay(); | |||
this.bindUnits(); | |||
// } else { | |||
// this.updateOverlay(); | |||
// } | |||
} | |||
} | |||
setup_navigation(init=false) { | |||
this.make_overlay(); | |||
makeChartArea() { | |||
if(this.svg) { | |||
this.chartWrapper.removeChild(this.svg); | |||
} | |||
this.svg = makeSVGContainer( | |||
this.chartWrapper, | |||
'chart', | |||
this.baseWidth, | |||
this.baseHeight | |||
); | |||
this.svgDefs = makeSVGDefs(this.svg); | |||
// I WISH !!! | |||
// this.svg = makeSVGGroup( | |||
// svgContainer, | |||
// 'flipped-coord-system', | |||
// `translate(0, ${this.baseHeight}) scale(1, -1)` | |||
// ); | |||
this.drawArea = makeSVGGroup( | |||
this.svg, | |||
this.type + '-chart', | |||
`translate(${this.leftMargin}, ${this.translateY})` | |||
); | |||
} | |||
renderLegend() {} | |||
setupNavigation(init=false) { | |||
if(!this.config.isNavigable) return; | |||
if(init) { | |||
this.bind_overlay(); | |||
this.bindOverlay(); | |||
this.keyActions = { | |||
'13': this.onEnterKey.bind(this), | |||
'37': this.onLeftArrow.bind(this), | |||
'38': this.onUpArrow.bind(this), | |||
'39': this.onRightArrow.bind(this), | |||
'40': this.onDownArrow.bind(this), | |||
}; | |||
document.addEventListener('keydown', (e) => { | |||
if(isElementInViewport(this.chart_wrapper)) { | |||
if(isElementInViewport(this.chartWrapper)) { | |||
e = e || window.event; | |||
if (e.keyCode == '37') { | |||
this.on_left_arrow(); | |||
} else if (e.keyCode == '39') { | |||
this.on_right_arrow(); | |||
} else if (e.keyCode == '38') { | |||
this.on_up_arrow(); | |||
} else if (e.keyCode == '40') { | |||
this.on_down_arrow(); | |||
} else if (e.keyCode == '13') { | |||
this.on_enter_key(); | |||
if(this.keyActions[e.keyCode]) { | |||
this.keyActions[e.keyCode](); | |||
} | |||
} | |||
}); | |||
} | |||
} | |||
make_overlay() {} | |||
bind_overlay() {} | |||
bind_units() {} | |||
makeOverlay() {} | |||
updateOverlay() {} | |||
bindOverlay() {} | |||
bindUnits() {} | |||
on_left_arrow() {} | |||
on_right_arrow() {} | |||
on_up_arrow() {} | |||
on_down_arrow() {} | |||
on_enter_key() {} | |||
onLeftArrow() {} | |||
onRightArrow() {} | |||
onUpArrow() {} | |||
onDownArrow() {} | |||
onEnterKey() {} | |||
get_data_point(index=this.current_index) { | |||
// check for length | |||
let data_point = { | |||
index: index | |||
}; | |||
let y = this.y[0]; | |||
['svg_units', 'y_tops', 'values'].map(key => { | |||
let data_key = key.slice(0, key.length-1); | |||
data_point[data_key] = y[key][index]; | |||
}); | |||
data_point.label = this.x[index]; | |||
return data_point; | |||
} | |||
getDataPoint(index = 0) {} | |||
setCurrentDataPoint(point) {} | |||
update_current_data_point(index) { | |||
index = parseInt(index); | |||
if(index < 0) index = 0; | |||
if(index >= this.x.length) index = this.x.length - 1; | |||
if(index === this.current_index) return; | |||
this.current_index = index; | |||
$.fire(this.parent, "data-select", this.get_data_point()); | |||
} | |||
updateDataset(dataset, index) {} | |||
addDataset(dataset, index) {} | |||
removeDataset(index = 0) {} | |||
updateDatasets(datasets) {} | |||
// Objects | |||
setup_utils() { } | |||
updateDataPoint(dataPoint, index = 0) {} | |||
addDataPoint(dataPoint, index = 0) {} | |||
removeDataPoint(index = 0) {} | |||
makeDrawAreaComponent(className, transform='') { | |||
return makeSVGGroup(this.draw_area, className, transform); | |||
getDifferentChart(type) { | |||
return getDifferentChart(type, this.type, this.parent, this.rawChartArgs); | |||
} | |||
} |
@@ -5,41 +5,39 @@ import { calcDistribution, getMaxCheckpoint } from '../utils/intervals'; | |||
import { isValidColor } from '../utils/colors'; | |||
export default class Heatmap extends BaseChart { | |||
constructor({ | |||
start = '', | |||
domain = '', | |||
subdomain = '', | |||
data = {}, | |||
discrete_domains = 0, | |||
count_label = '', | |||
legend_colors = [] | |||
}) { | |||
super(arguments[0]); | |||
constructor(parent, options) { | |||
super(parent, options); | |||
this.type = 'heatmap'; | |||
this.domain = domain; | |||
this.subdomain = subdomain; | |||
this.data = data; | |||
this.discrete_domains = discrete_domains; | |||
this.count_label = count_label; | |||
this.domain = options.domain || ''; | |||
this.subdomain = options.subdomain || ''; | |||
this.data = options.data || {}; | |||
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1; | |||
this.countLabel = options.countLabel || ''; | |||
let today = new Date(); | |||
this.start = start || addDays(today, 365); | |||
this.start = options.start || addDays(today, 365); | |||
legend_colors = 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']; | |||
// Fixed 5-color theme, | |||
// More colors are difficult to parse visually | |||
this.distribution_size = 5; | |||
this.translate_x = 0; | |||
this.translateX = 0; | |||
this.setup(); | |||
} | |||
setMargins() { | |||
super.setMargins(); | |||
this.leftMargin = 10; | |||
this.translateY = 10; | |||
} | |||
validate_colors(colors) { | |||
if(colors.length < 5) return 0; | |||
@@ -54,210 +52,212 @@ export default class Heatmap extends BaseChart { | |||
return valid; | |||
} | |||
setup_base_values() { | |||
configure() { | |||
super.configure(); | |||
this.today = new Date(); | |||
if(!this.start) { | |||
this.start = new Date(); | |||
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; | |||
} | |||
set_width() { | |||
this.base_width = (this.no_of_cols + 3) * 12 ; | |||
calcWidth() { | |||
this.baseWidth = (this.no_of_cols + 3) * 12 ; | |||
if(this.discrete_domains) { | |||
this.base_width += (12 * 12); | |||
if(this.discreteDomains) { | |||
this.baseWidth += (12 * 12); | |||
} | |||
} | |||
setup_components() { | |||
this.domain_label_group = this.makeDrawAreaComponent( | |||
makeChartArea() { | |||
super.makeChartArea(); | |||
this.domainLabelGroup = makeSVGGroup(this.drawArea, | |||
'domain-label-group chart-label'); | |||
this.data_groups = this.makeDrawAreaComponent( | |||
this.dataGroups = makeSVGGroup(this.drawArea, | |||
'data-groups', | |||
`translate(0, 20)` | |||
); | |||
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'; | |||
} | |||
setup_values() { | |||
this.domain_label_group.textContent = ''; | |||
this.data_groups.textContent = ''; | |||
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" | |||
]; | |||
} | |||
this.render_all_weeks_and_store_x_values(this.no_of_cols); | |||
render() { | |||
this.renderAllWeeksAndStoreXValues(this.no_of_cols); | |||
} | |||
render_all_weeks_and_store_x_values(no_of_weeks) { | |||
let current_week_sunday = new Date(this.first_week_start); | |||
this.week_col = 0; | |||
this.current_month = current_week_sunday.getMonth(); | |||
renderAllWeeksAndStoreXValues(no_of_weeks) { | |||
// renderAllWeeksAndStoreXValues | |||
this.domainLabelGroup.textContent = ''; | |||
this.dataGroups.textContent = ''; | |||
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++) { | |||
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.data_groups.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(); | |||
} | |||
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 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.data_groups, '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; | |||
let color_index = 0; | |||
for(var y = 0, i = 0; i < noOfWeekdays; i += step, y += (squareSide + cellPadding)) { | |||
let dataValue = 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]) { | |||
data_value = this.data[timestamp]; | |||
dataValue = this.data[timestamp]; | |||
} | |||
if(this.data[Math.round(timestamp)]) { | |||
data_value = this.data[Math.round(timestamp)]; | |||
dataValue = this.data[Math.round(timestamp)]; | |||
} | |||
if(data_value) { | |||
color_index = 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 = { | |||
'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[color_index], dataAttr); | |||
data_group.appendChild(heatSquare); | |||
let heatSquare = makeHeatSquare('day', x, y, squareSide, | |||
this.legendColors[colorIndex], dataAttr); | |||
let next_date = new Date(current_date); | |||
addDays(next_date, 1); | |||
if(next_date.getTime() > today_time) break; | |||
dataGroup.appendChild(heatSquare); | |||
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() { | |||
// this.first_month_label = 1; | |||
// if (this.first_week_start.getDate() > 8) { | |||
// if (this.firstWeekStart.getDate() > 8) { | |||
// this.first_month_label = 0; | |||
// } | |||
// this.last_month_label = 1; | |||
// 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 | |||
// 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 | |||
this.months.shift(); | |||
this.month_start_points.shift(); | |||
this.monthStartPoints.shift(); | |||
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); | |||
this.domain_label_group.appendChild(text); | |||
}); | |||
} | |||
make_graph_components() { | |||
Array.prototype.slice.call( | |||
this.container.querySelectorAll('.graph-stats-container, .sub-title, .title') | |||
).map(d => { | |||
d.style.display = 'None'; | |||
this.domainLabelGroup.appendChild(text); | |||
}); | |||
this.chart_wrapper.style.marginTop = '0px'; | |||
this.chart_wrapper.style.paddingTop = '0px'; | |||
} | |||
bind_tooltip() { | |||
bindTooltip() { | |||
Array.prototype.slice.call( | |||
document.querySelectorAll(".data-group .day") | |||
).map(el => { | |||
el.addEventListener('mouseenter', (e) => { | |||
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.chart_wrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect(); | |||
let gOff = this.chartWrapper.getBoundingClientRect(), pOff = e.target.getBoundingClientRect(); | |||
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.set_values(x, y, name, value, [], 1); | |||
this.tip.show_tip(); | |||
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []); | |||
this.tip.showTip(); | |||
}); | |||
}); | |||
} | |||
update(data) { | |||
this.data = data; | |||
this.setup_values(); | |||
this.bind_tooltip(); | |||
super.update(data); | |||
this.bindTooltip(); | |||
} | |||
} |
@@ -1,91 +0,0 @@ | |||
import AxisChart from './AxisChart'; | |||
import { makeSVGGroup, makePath, makeGradient } from '../utils/draw'; | |||
export default class LineChart extends AxisChart { | |||
constructor(args) { | |||
super(args); | |||
this.x_axis_mode = args.x_axis_mode || 'span'; | |||
this.y_axis_mode = args.y_axis_mode || 'span'; | |||
if(args.hasOwnProperty('show_dots')) { | |||
this.show_dots = args.show_dots; | |||
} else { | |||
this.show_dots = 1; | |||
} | |||
this.region_fill = args.region_fill; | |||
if(Object.getPrototypeOf(this) !== LineChart.prototype) { | |||
return; | |||
} | |||
this.dot_radius = args.dot_radius || 4; | |||
this.heatline = args.heatline; | |||
this.type = 'line'; | |||
this.setup(); | |||
} | |||
setup_graph_components() { | |||
this.setup_path_groups(); | |||
super.setup_graph_components(); | |||
} | |||
setup_path_groups() { | |||
this.paths_groups = []; | |||
this.y.map((d, i) => { | |||
this.paths_groups[i] = makeSVGGroup( | |||
this.draw_area, | |||
'path-group path-group-' + i | |||
); | |||
}); | |||
} | |||
setup_values() { | |||
super.setup_values(); | |||
this.unit_args = { | |||
type: 'dot', | |||
args: { radius: this.dot_radius } | |||
}; | |||
} | |||
make_new_units_for_dataset(x_values, y_values, color, dataset_index, | |||
no_of_datasets, units_group, units_array, unit) { | |||
if(this.show_dots) { | |||
super.make_new_units_for_dataset(x_values, y_values, color, dataset_index, | |||
no_of_datasets, units_group, units_array, unit); | |||
} | |||
} | |||
make_paths() { | |||
this.y.map((d, i) => { | |||
this.make_path(d, i, this.x_axis_positions, d.y_tops, d.color || this.colors[i]); | |||
}); | |||
} | |||
make_path(d, i, x_positions, y_positions, color) { | |||
let points_list = y_positions.map((y, i) => (x_positions[i] + ',' + y)); | |||
let points_str = points_list.join("L"); | |||
this.paths_groups[i].textContent = ''; | |||
d.path = makePath("M"+points_str, 'line-graph-path', color); | |||
this.paths_groups[i].appendChild(d.path); | |||
if(this.heatline) { | |||
let gradient_id = makeGradient(this.svg_defs, color); | |||
d.path.style.stroke = `url(#${gradient_id})`; | |||
} | |||
if(this.region_fill) { | |||
this.fill_region_for_dataset(d, i, color, points_str); | |||
} | |||
} | |||
fill_region_for_dataset(d, i, color, points_str) { | |||
let gradient_id = makeGradient(this.svg_defs, color, true); | |||
let pathStr = "M" + `0,${this.zero_line}L` + points_str + `L${this.width},${this.zero_line}`; | |||
d.regionPath = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id})`); | |||
this.paths_groups[i].appendChild(d.regionPath); | |||
} | |||
} |
@@ -0,0 +1,173 @@ | |||
import AxisChart from './AxisChart'; | |||
import { Y_AXIS_MARGIN } from '../utils/constants'; | |||
// import { ChartComponent } from '../objects/ChartComponents'; | |||
import { floatTwo } from '../utils/helpers'; | |||
export default class MultiAxisChart extends AxisChart { | |||
constructor(args) { | |||
super(args); | |||
// this.unitType = args.unitType || 'line'; | |||
// this.setup(); | |||
} | |||
preSetup() { | |||
this.type = 'multiaxis'; | |||
} | |||
setMargins() { | |||
super.setMargins(); | |||
let noOfLeftAxes = this.data.datasets.filter(d => d.axisPosition === 'left').length; | |||
this.leftMargin = (noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
this.rightMargin = (this.data.datasets.length - noOfLeftAxes) * Y_AXIS_MARGIN || Y_AXIS_MARGIN; | |||
} | |||
prepareYAxis() { } | |||
prepareData(data) { | |||
super.prepareData(data); | |||
let sets = this.state.datasets; | |||
// let axesLeft = sets.filter(d => d.axisPosition === 'left'); | |||
// let axesRight = sets.filter(d => d.axisPosition === 'right'); | |||
// let axesNone = sets.filter(d => !d.axisPosition || | |||
// !['left', 'right'].includes(d.axisPosition)); | |||
let leftCount = 0, rightCount = 0; | |||
sets.forEach((d, i) => { | |||
d.yAxis = { | |||
position: d.axisPosition, | |||
index: d.axisPosition === 'left' ? leftCount++ : rightCount++ | |||
}; | |||
}); | |||
} | |||
configure(args) { | |||
super.configure(args); | |||
this.config.xAxisMode = args.xAxisMode || 'tick'; | |||
this.config.yAxisMode = args.yAxisMode || 'span'; | |||
} | |||
// setUnitWidthAndXOffset() { | |||
// this.state.unitWidth = this.width/(this.state.datasetLength); | |||
// this.state.xOffset = this.state.unitWidth/2; | |||
// } | |||
configUnits() { | |||
this.unitArgs = { | |||
type: 'bar', | |||
args: { | |||
spaceWidth: this.state.unitWidth/2, | |||
} | |||
}; | |||
} | |||
setYAxis() { | |||
this.state.datasets.map(d => { | |||
this.calcYAxisParameters(d.yAxis, d.values, this.unitType === 'line'); | |||
}); | |||
} | |||
calcYUnits() { | |||
this.state.datasets.map(d => { | |||
d.positions = d.values.map(val => floatTwo(d.yAxis.zeroLine - val * d.yAxis.scaleMultiplier)); | |||
}); | |||
} | |||
// TODO: function doesn't exist, handle with components | |||
renderConstants() { | |||
this.state.datasets.map(d => { | |||
let guidePos = d.yAxis.position === 'left' | |||
? -1 * d.yAxis.index * Y_AXIS_MARGIN | |||
: this.width + d.yAxis.index * Y_AXIS_MARGIN; | |||
this.renderer.xLine(guidePos, '', { | |||
pos:'top', | |||
mode: 'span', | |||
stroke: this.colors[i], | |||
className: 'y-axis-guide' | |||
}) | |||
}); | |||
} | |||
getYAxesComponents() { | |||
return this.data.datasets.map((e, i) => { | |||
return new ChartComponent({ | |||
layerClass: 'y axis y-axis-' + i, | |||
make: () => { | |||
let yAxis = this.state.datasets[i].yAxis; | |||
this.renderer.setZeroline(yAxis.zeroline); | |||
let options = { | |||
pos: yAxis.position, | |||
mode: 'tick', | |||
offset: yAxis.index * Y_AXIS_MARGIN, | |||
stroke: this.colors[i] | |||
}; | |||
return yAxis.positions.map((position, j) => | |||
this.renderer.yLine(position, yAxis.labels[j], options) | |||
); | |||
}, | |||
animate: () => {} | |||
}); | |||
}); | |||
} | |||
// TODO remove renderer zeroline from above and below | |||
getChartComponents() { | |||
return this.data.datasets.map((d, index) => { | |||
return new ChartComponent({ | |||
layerClass: 'dataset-units dataset-' + index, | |||
make: () => { | |||
let d = this.state.datasets[index]; | |||
let unitType = this.unitArgs; | |||
// the only difference, should be tied to datasets or default | |||
this.renderer.setZeroline(d.yAxis.zeroLine); | |||
return d.positions.map((y, j) => { | |||
return this.renderer[unitType.type]( | |||
this.state.xAxisPositions[j], | |||
y, | |||
unitType.args, | |||
this.colors[index], | |||
j, | |||
index, | |||
this.state.datasetLength | |||
); | |||
}); | |||
}, | |||
animate: (svgUnits) => { | |||
let d = this.state.datasets[index]; | |||
let unitType = this.unitArgs.type; | |||
// have been updated in axis render; | |||
let newX = this.state.xAxisPositions; | |||
let newY = this.state.datasets[index].positions; | |||
let lastUnit = svgUnits[svgUnits.length - 1]; | |||
let parentNode = lastUnit.parentNode; | |||
if(this.oldState.xExtra > 0) { | |||
for(var i = 0; i<this.oldState.xExtra; i++) { | |||
let unit = lastUnit.cloneNode(true); | |||
parentNode.appendChild(unit); | |||
svgUnits.push(unit); | |||
} | |||
} | |||
this.renderer.setZeroline(d.yAxis.zeroLine); | |||
svgUnits.map((unit, i) => { | |||
if(newX[i] === undefined || newY[i] === undefined) return; | |||
this.elementsToAnimate.push(this.renderer['animate' + unitType]( | |||
unit, // unit, with info to replace where it came from in the data | |||
newX[i], | |||
newY[i], | |||
index, | |||
this.state.noOfDatasets | |||
)); | |||
}); | |||
} | |||
}); | |||
}); | |||
} | |||
} |
@@ -1,127 +1,74 @@ | |||
import BaseChart from './BaseChart'; | |||
import { $, offset } from '../utils/dom'; | |||
import AggregationChart from './AggregationChart'; | |||
import { $, getOffset } from '../utils/dom'; | |||
export default class PercentageChart extends BaseChart { | |||
constructor(args) { | |||
super(args); | |||
export default class PercentageChart extends AggregationChart { | |||
constructor(parent, args) { | |||
super(parent, args); | |||
this.type = 'percentage'; | |||
this.max_slices = 10; | |||
this.max_legend_points = 6; | |||
this.setup(); | |||
} | |||
make_chart_area() { | |||
this.chart_wrapper.className += ' ' + 'graph-focus-margin'; | |||
this.chart_wrapper.style.marginTop = '45px'; | |||
makeChartArea() { | |||
this.chartWrapper.className += ' ' + 'graph-focus-margin'; | |||
this.chartWrapper.style.marginTop = '45px'; | |||
this.stats_wrapper.className += ' ' + 'graph-focus-margin'; | |||
this.stats_wrapper.style.marginBottom = '30px'; | |||
this.stats_wrapper.style.paddingTop = '0px'; | |||
} | |||
this.statsWrapper.className += ' ' + 'graph-focus-margin'; | |||
this.statsWrapper.style.marginBottom = '30px'; | |||
this.statsWrapper.style.paddingTop = '0px'; | |||
make_draw_area() { | |||
this.chart_div = $.create('div', { | |||
this.svg = $.create('div', { | |||
className: 'div', | |||
inside: this.chart_wrapper | |||
inside: this.chartWrapper | |||
}); | |||
this.chart = $.create('div', { | |||
className: 'progress-chart', | |||
inside: this.chart_div | |||
inside: this.svg | |||
}); | |||
} | |||
setup_components() { | |||
this.percentage_bar = $.create('div', { | |||
this.percentageBar = $.create('div', { | |||
className: 'progress', | |||
inside: this.chart | |||
}); | |||
} | |||
setup_values() { | |||
this.slice_totals = []; | |||
let all_totals = this.data.labels.map((d, i) => { | |||
let total = 0; | |||
this.data.datasets.map(e => { | |||
total += e.values[i]; | |||
}); | |||
return [total, d]; | |||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||
let totals = all_totals; | |||
if(all_totals.length > this.max_slices) { | |||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||
totals = all_totals.slice(0, this.max_slices-1); | |||
let others = all_totals.slice(this.max_slices-1); | |||
let sum_of_others = 0; | |||
others.map(d => {sum_of_others += d[0];}); | |||
totals.push([sum_of_others, 'Rest']); | |||
this.colors[this.max_slices-1] = 'grey'; | |||
} | |||
this.labels = []; | |||
totals.map(d => { | |||
this.slice_totals.push(d[0]); | |||
this.labels.push(d[1]); | |||
}); | |||
this.legend_totals = this.slice_totals.slice(0, this.max_legend_points); | |||
} | |||
make_graph_components() { | |||
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0); | |||
this.slices = []; | |||
this.slice_totals.map((total, i) => { | |||
render() { | |||
let s = this.state; | |||
this.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | |||
s.slices = []; | |||
s.sliceTotals.map((total, i) => { | |||
let slice = $.create('div', { | |||
className: `progress-bar`, | |||
inside: this.percentage_bar, | |||
'data-index': i, | |||
inside: this.percentageBar, | |||
styles: { | |||
background: this.colors[i], | |||
width: total*100/this.grand_total + "%" | |||
width: total*100/this.grandTotal + "%" | |||
} | |||
}); | |||
this.slices.push(slice); | |||
s.slices.push(slice); | |||
}); | |||
} | |||
bind_tooltip() { | |||
this.slices.map((slice, i) => { | |||
slice.addEventListener('mouseenter', () => { | |||
let g_off = offset(this.chart_wrapper), p_off = offset(slice); | |||
bindTooltip() { | |||
let s = this.state; | |||
let x = p_off.left - g_off.left + slice.offsetWidth/2; | |||
let y = p_off.top - g_off.top - 6; | |||
let title = (this.formatted_labels && this.formatted_labels.length>0 | |||
? this.formatted_labels[i] : this.labels[i]) + ': '; | |||
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1); | |||
this.chartWrapper.addEventListener('mousemove', (e) => { | |||
let slice = e.target; | |||
if(slice.classList.contains('progress-bar')) { | |||
this.tip.set_values(x, y, title, percent + "%"); | |||
this.tip.show_tip(); | |||
}); | |||
}); | |||
} | |||
let i = slice.getAttribute('data-index'); | |||
let gOff = getOffset(this.chartWrapper), pOff = getOffset(slice); | |||
let x = pOff.left - gOff.left + slice.offsetWidth/2; | |||
let y = pOff.top - gOff.top - 6; | |||
let title = (this.formattedLabels && this.formattedLabels.length>0 | |||
? this.formattedLabels[i] : this.state.labels[i]) + ': '; | |||
let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1); | |||
show_summary() { | |||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||
? this.formatted_labels : this.labels; | |||
this.legend_totals.map((d, i) => { | |||
if(d) { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
inside: this.stats_wrapper | |||
}); | |||
stats.innerHTML = `<span class="indicator"> | |||
<i style="background: ${this.colors[i]}"></i> | |||
<span class="text-muted">${x_values[i]}:</span> | |||
${d} | |||
</span>`; | |||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||
this.tip.showTip(); | |||
} | |||
}); | |||
} | |||
@@ -1,213 +1,168 @@ | |||
import BaseChart from './BaseChart'; | |||
import { $, offset } from '../utils/dom'; | |||
import { makePath } from '../utils/draw'; | |||
import AggregationChart from './AggregationChart'; | |||
import { getComponent } from '../objects/ChartComponents'; | |||
import { getOffset } from '../utils/dom'; | |||
import { getPositionByAngle } from '../utils/helpers'; | |||
import { makeArcPathStr } from '../utils/draw'; | |||
import { lightenDarkenColor } from '../utils/colors'; | |||
import { runSVGAnimation, transform } from '../utils/animation'; | |||
const ANGLE_RATIO = Math.PI / 180; | |||
const FULL_ANGLE = 360; | |||
import { transform } from '../utils/animation'; | |||
import { FULL_ANGLE } from '../utils/constants'; | |||
export default class PieChart extends BaseChart { | |||
constructor(args) { | |||
super(args); | |||
export default class PieChart extends AggregationChart { | |||
constructor(parent, args) { | |||
super(parent, args); | |||
this.type = 'pie'; | |||
this.elements_to_animate = null; | |||
this.hoverRadio = args.hoverRadio || 0.1; | |||
this.max_slices = 10; | |||
this.max_legend_points = 6; | |||
this.isAnimate = false; | |||
this.startAngle = args.startAngle || 0; | |||
this.clockWise = args.clockWise || false; | |||
this.mouseMove = this.mouseMove.bind(this); | |||
this.mouseLeave = this.mouseLeave.bind(this); | |||
this.initTimeout = 0; | |||
this.setup(); | |||
} | |||
setup_values() { | |||
this.centerX = this.width / 2; | |||
this.centerY = this.height / 2; | |||
this.radius = (this.height > this.width ? this.centerX : this.centerY); | |||
this.slice_totals = []; | |||
let all_totals = this.data.labels.map((d, i) => { | |||
let total = 0; | |||
this.data.datasets.map(e => { | |||
total += e.values[i]; | |||
}); | |||
return [total, d]; | |||
}).filter(d => { return d[0] > 0; }); // keep only positive results | |||
let totals = all_totals; | |||
configure(args) { | |||
super.configure(args); | |||
this.mouseMove = this.mouseMove.bind(this); | |||
this.mouseLeave = this.mouseLeave.bind(this); | |||
if(all_totals.length > this.max_slices) { | |||
all_totals.sort((a, b) => { return b[0] - a[0]; }); | |||
this.hoverRadio = args.hoverRadio || 0.1; | |||
this.config.startAngle = args.startAngle || 0; | |||
totals = all_totals.slice(0, this.max_slices-1); | |||
let others = all_totals.slice(this.max_slices-1); | |||
this.clockWise = args.clockWise || false; | |||
} | |||
let sum_of_others = 0; | |||
others.map(d => {sum_of_others += d[0];}); | |||
prepareFirstData(data=this.data) { | |||
this.init = 1; | |||
return data; | |||
} | |||
totals.push([sum_of_others, 'Rest']); | |||
calc() { | |||
super.calc(); | |||
let s = this.state; | |||
this.colors[this.max_slices-1] = 'grey'; | |||
} | |||
this.center = { | |||
x: this.width / 2, | |||
y: this.height / 2 | |||
}; | |||
this.radius = (this.height > this.width ? this.center.x : this.center.y); | |||
this.labels = []; | |||
totals.map(d => { | |||
this.slice_totals.push(d[0]); | |||
this.labels.push(d[1]); | |||
}); | |||
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0); | |||
this.legend_totals = this.slice_totals.slice(0, this.max_legend_points); | |||
this.calcSlices(); | |||
} | |||
static getPositionByAngle(angle,radius){ | |||
return { | |||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||
}; | |||
} | |||
makeArcPath(startPosition,endPosition){ | |||
const{centerX,centerY,radius,clockWise} = this; | |||
return `M${centerX} ${centerY} L${centerX+startPosition.x} ${centerY+startPosition.y} A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} ${centerX+endPosition.x} ${centerY+endPosition.y} z`; | |||
} | |||
make_graph_components(init){ | |||
const{radius,clockWise} = this; | |||
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0); | |||
const prevSlicesProperties = this.slicesProperties || []; | |||
this.slices = []; | |||
this.elements_to_animate = []; | |||
this.slicesProperties = []; | |||
let curAngle = 180 - this.startAngle; | |||
this.slice_totals.map((total, i) => { | |||
calcSlices() { | |||
let s = this.state; | |||
const { radius, clockWise } = this; | |||
const prevSlicesProperties = s.slicesProperties || []; | |||
s.sliceStrings = []; | |||
s.slicesProperties = []; | |||
let curAngle = 180 - this.config.startAngle; | |||
s.sliceTotals.map((total, i) => { | |||
const startAngle = curAngle; | |||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | |||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | |||
const endAngle = curAngle = curAngle + diffAngle; | |||
const startPosition = PieChart.getPositionByAngle(startAngle,radius); | |||
const endPosition = PieChart.getPositionByAngle(endAngle,radius); | |||
const prevProperty = init && prevSlicesProperties[i]; | |||
const startPosition = getPositionByAngle(startAngle, radius); | |||
const endPosition = getPositionByAngle(endAngle, radius); | |||
const prevProperty = this.init && prevSlicesProperties[i]; | |||
let curStart,curEnd; | |||
if(init){ | |||
curStart = prevProperty?prevProperty.startPosition : startPosition; | |||
curEnd = prevProperty? prevProperty.endPosition : startPosition; | |||
}else{ | |||
if(this.init) { | |||
curStart = prevProperty ? prevProperty.startPosition : startPosition; | |||
curEnd = prevProperty ? prevProperty.endPosition : startPosition; | |||
} else { | |||
curStart = startPosition; | |||
curEnd = endPosition; | |||
} | |||
const curPath = this.makeArcPath(curStart,curEnd); | |||
let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]); | |||
slice.style.transition = 'transform .3s;'; | |||
this.draw_area.appendChild(slice); | |||
const curPath = makeArcPathStr(curStart, curEnd, this.center, this.radius, this.clockWise); | |||
this.slices.push(slice); | |||
this.slicesProperties.push({ | |||
s.sliceStrings.push(curPath); | |||
s.slicesProperties.push({ | |||
startPosition, | |||
endPosition, | |||
value: total, | |||
total: this.grand_total, | |||
total: s.grandTotal, | |||
startAngle, | |||
endAngle, | |||
angle:diffAngle | |||
angle: diffAngle | |||
}); | |||
if(init){ | |||
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1}, | |||
{d:this.makeArcPath(startPosition,endPosition)}, | |||
650, "easein",null,{ | |||
d:curPath | |||
}]); | |||
} | |||
}); | |||
if(init){ | |||
this.run_animation(); | |||
} | |||
this.init = 0; | |||
} | |||
run_animation() { | |||
// if(this.isAnimate) return ; | |||
// this.isAnimate = true; | |||
if(!this.elements_to_animate || this.elements_to_animate.length === 0) return; | |||
let anim_svg = runSVGAnimation(this.svg, this.elements_to_animate); | |||
if(this.svg.parentNode == this.chart_wrapper) { | |||
this.chart_wrapper.removeChild(this.svg); | |||
this.chart_wrapper.appendChild(anim_svg); | |||
} | |||
// Replace the new svg (data has long been replaced) | |||
setTimeout(() => { | |||
// this.isAnimate = false; | |||
if(anim_svg.parentNode == this.chart_wrapper) { | |||
this.chart_wrapper.removeChild(anim_svg); | |||
this.chart_wrapper.appendChild(this.svg); | |||
} | |||
}, 650); | |||
setupComponents() { | |||
let s = this.state; | |||
let componentConfigs = [ | |||
[ | |||
'pieSlices', | |||
{ }, | |||
function() { | |||
return { | |||
sliceStrings: s.sliceStrings, | |||
colors: this.colors | |||
}; | |||
}.bind(this) | |||
] | |||
]; | |||
this.components = new Map(componentConfigs | |||
.map(args => { | |||
let component = getComponent(...args); | |||
return [args[0], component]; | |||
})); | |||
} | |||
calTranslateByAngle(property){ | |||
const{radius,hoverRadio} = this; | |||
const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius); | |||
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | |||
} | |||
hoverSlice(path,i,flag,e){ | |||
if(!path) return; | |||
const color = this.colors[i]; | |||
if(flag){ | |||
transform(path,this.calTranslateByAngle(this.slicesProperties[i])); | |||
path.style.fill = lightenDarkenColor(color,50); | |||
let g_off = offset(this.svg); | |||
if(flag) { | |||
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i])); | |||
path.style.fill = lightenDarkenColor(color, 50); | |||
let g_off = getOffset(this.svg); | |||
let x = e.pageX - g_off.left + 10; | |||
let y = e.pageY - g_off.top - 10; | |||
let title = (this.formatted_labels && this.formatted_labels.length>0 | |||
? this.formatted_labels[i] : this.labels[i]) + ': '; | |||
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1); | |||
this.tip.set_values(x, y, title, percent + "%"); | |||
this.tip.show_tip(); | |||
}else{ | |||
let title = (this.formatted_labels && this.formatted_labels.length > 0 | |||
? this.formatted_labels[i] : this.state.labels[i]) + ': '; | |||
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1); | |||
this.tip.setValues(x, y, {name: title, value: percent + "%"}); | |||
this.tip.showTip(); | |||
} else { | |||
transform(path,'translate3d(0,0,0)'); | |||
this.tip.hide_tip(); | |||
this.tip.hideTip(); | |||
path.style.fill = color; | |||
} | |||
} | |||
bindTooltip() { | |||
this.chartWrapper.addEventListener('mousemove', this.mouseMove); | |||
this.chartWrapper.addEventListener('mouseleave', this.mouseLeave); | |||
} | |||
mouseMove(e){ | |||
const target = e.target; | |||
let slices = this.components.get('pieSlices').store; | |||
let prevIndex = this.curActiveSliceIndex; | |||
let prevAcitve = this.curActiveSlice; | |||
for(let i = 0; i < this.slices.length; i++){ | |||
if(target === this.slices[i]){ | |||
this.hoverSlice(prevAcitve,prevIndex,false); | |||
this.curActiveSlice = target; | |||
this.curActiveSliceIndex = i; | |||
this.hoverSlice(target,i,true,e); | |||
break; | |||
} | |||
if(slices.includes(target)) { | |||
let i = slices.indexOf(target); | |||
this.hoverSlice(prevAcitve, prevIndex,false); | |||
this.curActiveSlice = target; | |||
this.curActiveSliceIndex = i; | |||
this.hoverSlice(target, i, true, e); | |||
} else { | |||
this.mouseLeave(); | |||
} | |||
} | |||
mouseLeave(){ | |||
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); | |||
} | |||
bind_tooltip() { | |||
this.draw_area.addEventListener('mousemove',this.mouseMove); | |||
this.draw_area.addEventListener('mouseleave',this.mouseLeave); | |||
} | |||
show_summary() { | |||
let x_values = this.formatted_labels && this.formatted_labels.length > 0 | |||
? this.formatted_labels : this.labels; | |||
this.legend_totals.map((d, i) => { | |||
const color = this.colors[i]; | |||
if(d) { | |||
let stats = $.create('div', { | |||
className: 'stats', | |||
inside: this.stats_wrapper | |||
}); | |||
stats.innerHTML = `<span class="indicator"> | |||
<i style="background-color:${color};"></i> | |||
<span class="text-muted">${x_values[i]}:</span> | |||
${d} | |||
</span>`; | |||
} | |||
}); | |||
} | |||
} |
@@ -1,35 +0,0 @@ | |||
import LineChart from './LineChart'; | |||
export default class ScatterChart extends LineChart { | |||
constructor(args) { | |||
super(args); | |||
this.type = 'scatter'; | |||
if(!args.dot_radius) { | |||
this.dot_radius = 8; | |||
} else { | |||
this.dot_radius = args.dot_radius; | |||
} | |||
this.setup(); | |||
} | |||
setup_graph_components() { | |||
this.setup_path_groups(); | |||
super.setup_graph_components(); | |||
} | |||
setup_path_groups() {} | |||
setup_values() { | |||
super.setup_values(); | |||
this.unit_args = { | |||
type: 'dot', | |||
args: { radius: this.dot_radius } | |||
}; | |||
} | |||
make_paths() {} | |||
make_path() {} | |||
} |
@@ -0,0 +1,46 @@ | |||
import Chart from './chart'; | |||
const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie']; | |||
const COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter', 'percentage', 'pie'], | |||
line: ['scatter', 'bar', 'percentage', 'pie'], | |||
pie: ['line', 'scatter', 'percentage', 'bar'], | |||
scatter: ['line', 'bar', 'percentage', 'pie'], | |||
percentage: ['bar', 'line', 'scatter', 'pie'], | |||
heatmap: [] | |||
}; | |||
// Needs structure as per only labels/datasets | |||
const COLOR_COMPATIBLE_CHARTS = { | |||
bar: ['line', 'scatter'], | |||
line: ['scatter', 'bar'], | |||
pie: ['percentage'], | |||
scatter: ['line', 'bar'], | |||
percentage: ['pie'], | |||
heatmap: [] | |||
}; | |||
export function getDifferentChart(type, current_type, parent, args) { | |||
if(type === current_type) return; | |||
if(!ALL_CHART_TYPES.includes(type)) { | |||
console.error(`'${type}' is not a valid chart type.`); | |||
} | |||
if(!COMPATIBLE_CHARTS[current_type].includes(type)) { | |||
console.error(`'${current_type}' chart cannot be converted to a '${type}' chart.`); | |||
} | |||
// whether the new chart can use the existing colors | |||
const useColor = COLOR_COMPATIBLE_CHARTS[current_type].includes(type); | |||
// Okay, this is anticlimactic | |||
// this function will need to actually be 'changeChartType(type)' | |||
// that will update only the required elements, but for now ... | |||
args.type = type; | |||
args.colors = useColor ? args.colors : undefined; | |||
return new Chart(parent, args); | |||
} |
@@ -0,0 +1,366 @@ | |||
import { makeSVGGroup } from '../utils/draw'; | |||
import { makePath, xLine, yLine, yMarker, yRegion, datasetBar, datasetDot, getPaths } from '../utils/draw'; | |||
import { equilizeNoOfElements } from '../utils/draw-utils'; | |||
import { translateHoriLine, translateVertLine, animateRegion, animateBar, | |||
animateDot, animatePath, animatePathStr } from '../utils/animate'; | |||
class ChartComponent { | |||
constructor({ | |||
layerClass = '', | |||
layerTransform = '', | |||
constants, | |||
getData, | |||
makeElements, | |||
animateElements | |||
}) { | |||
this.layerTransform = layerTransform; | |||
this.constants = constants; | |||
this.makeElements = makeElements; | |||
this.getData = getData; | |||
this.animateElements = animateElements; | |||
this.store = []; | |||
this.layerClass = layerClass; | |||
this.layerClass = typeof(this.layerClass) === 'function' | |||
? this.layerClass() : this.layerClass; | |||
this.refresh(); | |||
} | |||
refresh(data) { | |||
this.data = data || this.getData(); | |||
} | |||
setup(parent) { | |||
this.layer = makeSVGGroup(parent, this.layerClass, this.layerTransform); | |||
} | |||
make() { | |||
this.render(this.data); | |||
this.oldData = this.data; | |||
} | |||
render(data) { | |||
this.store = this.makeElements(data); | |||
this.layer.textContent = ''; | |||
this.store.forEach(element => { | |||
this.layer.appendChild(element); | |||
}); | |||
} | |||
update(animate = true) { | |||
this.refresh(); | |||
let animateElements = []; | |||
if(animate) { | |||
animateElements = this.animateElements(this.data); | |||
} | |||
return animateElements; | |||
} | |||
} | |||
let componentConfigs = { | |||
pieSlices: { | |||
layerClass: 'pie-slices', | |||
makeElements(data) { | |||
return data.sliceStrings.map((s, i) =>{ | |||
let slice = makePath(s, 'pie-path', 'none', data.colors[i]); | |||
slice.style.transition = 'transform .3s;'; | |||
return slice; | |||
}); | |||
}, | |||
animateElements(newData) { | |||
return this.store.map((slice, i) => | |||
animatePathStr(slice, newData.sliceStrings[i]) | |||
); | |||
} | |||
}, | |||
yAxis: { | |||
layerClass: 'y axis', | |||
makeElements(data) { | |||
return data.positions.map((position, i) => | |||
yLine(position, data.labels[i], this.constants.width, | |||
{mode: this.constants.mode, pos: this.constants.pos}) | |||
); | |||
}, | |||
animateElements(newData) { | |||
let newPos = newData.positions; | |||
let newLabels = newData.labels; | |||
let oldPos = this.oldData.positions; | |||
let oldLabels = this.oldData.labels; | |||
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); | |||
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); | |||
this.render({ | |||
positions: oldPos, | |||
labels: newLabels | |||
}); | |||
return this.store.map((line, i) => { | |||
return translateHoriLine( | |||
line, newPos[i], oldPos[i] | |||
); | |||
}); | |||
} | |||
}, | |||
xAxis: { | |||
layerClass: 'x axis', | |||
makeElements(data) { | |||
return data.positions.map((position, i) => | |||
xLine(position, data.calcLabels[i], this.constants.height, | |||
{mode: this.constants.mode, pos: this.constants.pos}) | |||
); | |||
}, | |||
animateElements(newData) { | |||
let newPos = newData.positions; | |||
let newLabels = newData.calcLabels; | |||
let oldPos = this.oldData.positions; | |||
let oldLabels = this.oldData.calcLabels; | |||
[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos); | |||
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); | |||
this.render({ | |||
positions: oldPos, | |||
calcLabels: newLabels | |||
}); | |||
return this.store.map((line, i) => { | |||
return translateVertLine( | |||
line, newPos[i], oldPos[i] | |||
); | |||
}); | |||
} | |||
}, | |||
yMarkers: { | |||
layerClass: 'y-markers', | |||
makeElements(data) { | |||
return data.map(marker => | |||
yMarker(marker.position, marker.label, this.constants.width, | |||
{pos:'right', mode: 'span', lineType: 'dashed'}) | |||
); | |||
}, | |||
animateElements(newData) { | |||
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); | |||
let newPos = newData.map(d => d.position); | |||
let newLabels = newData.map(d => d.label); | |||
let oldPos = this.oldData.map(d => d.position); | |||
this.render(oldPos.map((pos, i) => { | |||
return { | |||
position: oldPos[i], | |||
label: newLabels[i] | |||
}; | |||
})); | |||
return this.store.map((line, i) => { | |||
return translateHoriLine( | |||
line, newPos[i], oldPos[i] | |||
); | |||
}); | |||
} | |||
}, | |||
yRegions: { | |||
layerClass: 'y-regions', | |||
makeElements(data) { | |||
return data.map(region => | |||
yRegion(region.startPos, region.endPos, this.constants.width, | |||
region.label) | |||
); | |||
}, | |||
animateElements(newData) { | |||
[this.oldData, newData] = equilizeNoOfElements(this.oldData, newData); | |||
let newPos = newData.map(d => d.endPos); | |||
let newLabels = newData.map(d => d.label); | |||
let newStarts = newData.map(d => d.startPos); | |||
let oldPos = this.oldData.map(d => d.endPos); | |||
let oldStarts = this.oldData.map(d => d.startPos); | |||
this.render(oldPos.map((pos, i) => { | |||
return { | |||
startPos: oldStarts[i], | |||
endPos: oldPos[i], | |||
label: newLabels[i] | |||
}; | |||
})); | |||
let animateElements = []; | |||
this.store.map((rectGroup, i) => { | |||
animateElements = animateElements.concat(animateRegion( | |||
rectGroup, newStarts[i], newPos[i], oldPos[i] | |||
)); | |||
}); | |||
return animateElements; | |||
} | |||
}, | |||
barGraph: { | |||
layerClass: function() { return 'dataset-units dataset-bars dataset-' + this.constants.index; }, | |||
makeElements(data) { | |||
let c = this.constants; | |||
this.unitType = 'bar'; | |||
this.units = data.yPositions.map((y, j) => { | |||
return datasetBar( | |||
data.xPositions[j], | |||
y, | |||
data.barWidth, | |||
c.color, | |||
data.labels[j], | |||
j, | |||
data.offsets[j], | |||
{ | |||
zeroLine: data.zeroLine, | |||
barsWidth: data.barsWidth, | |||
minHeight: c.minHeight | |||
} | |||
); | |||
}); | |||
return this.units; | |||
}, | |||
animateElements(newData) { | |||
let c = this.constants; | |||
let newXPos = newData.xPositions; | |||
let newYPos = newData.yPositions; | |||
let newOffsets = newData.offsets; | |||
let newLabels = newData.labels; | |||
let oldXPos = this.oldData.xPositions; | |||
let oldYPos = this.oldData.yPositions; | |||
let oldOffsets = this.oldData.offsets; | |||
let oldLabels = this.oldData.labels; | |||
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); | |||
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); | |||
[oldOffsets, newOffsets] = equilizeNoOfElements(oldOffsets, newOffsets); | |||
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels); | |||
this.render({ | |||
xPositions: oldXPos, | |||
yPositions: oldYPos, | |||
offsets: oldOffsets, | |||
labels: newLabels, | |||
zeroLine: this.oldData.zeroLine, | |||
barsWidth: this.oldData.barsWidth, | |||
barWidth: this.oldData.barWidth, | |||
}); | |||
let animateElements = []; | |||
this.store.map((bar, i) => { | |||
animateElements = animateElements.concat(animateBar( | |||
bar, newXPos[i], newYPos[i], newData.barWidth, newOffsets[i], c.index, | |||
{zeroLine: newData.zeroLine} | |||
)); | |||
}); | |||
return animateElements; | |||
} | |||
}, | |||
lineGraph: { | |||
layerClass: function() { return 'dataset-units dataset-line dataset-' + this.constants.index; }, | |||
makeElements(data) { | |||
let c = this.constants; | |||
this.unitType = 'dot'; | |||
this.paths = {}; | |||
if(!c.hideLine) { | |||
this.paths = getPaths( | |||
data.xPositions, | |||
data.yPositions, | |||
c.color, | |||
{ | |||
heatline: c.heatline, | |||
regionFill: c.regionFill | |||
}, | |||
{ | |||
svgDefs: c.svgDefs, | |||
zeroLine: data.zeroLine | |||
} | |||
); | |||
} | |||
this.units = []; | |||
if(!c.hideDots) { | |||
this.units = data.yPositions.map((y, j) => { | |||
return datasetDot( | |||
data.xPositions[j], | |||
y, | |||
data.radius, | |||
c.color, | |||
(c.valuesOverPoints ? data.values[j] : ''), | |||
j | |||
); | |||
}); | |||
} | |||
return Object.values(this.paths).concat(this.units); | |||
}, | |||
animateElements(newData) { | |||
let newXPos = newData.xPositions; | |||
let newYPos = newData.yPositions; | |||
let newValues = newData.values; | |||
let oldXPos = this.oldData.xPositions; | |||
let oldYPos = this.oldData.yPositions; | |||
let oldValues = this.oldData.values; | |||
[oldXPos, newXPos] = equilizeNoOfElements(oldXPos, newXPos); | |||
[oldYPos, newYPos] = equilizeNoOfElements(oldYPos, newYPos); | |||
[oldValues, newValues] = equilizeNoOfElements(oldValues, newValues); | |||
this.render({ | |||
xPositions: oldXPos, | |||
yPositions: oldYPos, | |||
values: newValues, | |||
zeroLine: this.oldData.zeroLine, | |||
radius: this.oldData.radius, | |||
}); | |||
let animateElements = []; | |||
if(Object.keys(this.paths).length) { | |||
animateElements = animateElements.concat(animatePath( | |||
this.paths, newXPos, newYPos, newData.zeroLine)); | |||
} | |||
if(this.units.length) { | |||
this.units.map((dot, i) => { | |||
animateElements = animateElements.concat(animateDot( | |||
dot, newXPos[i], newYPos[i])); | |||
}); | |||
} | |||
return animateElements; | |||
} | |||
} | |||
}; | |||
export function getComponent(name, constants, getData) { | |||
let keys = Object.keys(componentConfigs).filter(k => name.includes(k)); | |||
let config = componentConfigs[keys[0]]; | |||
Object.assign(config, { | |||
constants: constants, | |||
getData: getData | |||
}); | |||
return new ChartComponent(config); | |||
} |
@@ -7,10 +7,10 @@ export default class SvgTip { | |||
}) { | |||
this.parent = parent; | |||
this.colors = colors; | |||
this.title_name = ''; | |||
this.title_value = ''; | |||
this.list_values = []; | |||
this.title_value_first = 0; | |||
this.titleName = ''; | |||
this.titleValue = ''; | |||
this.listValues = []; | |||
this.titleValueFirst = 0; | |||
this.x = 0; | |||
this.y = 0; | |||
@@ -22,16 +22,16 @@ export default class SvgTip { | |||
} | |||
setup() { | |||
this.make_tooltip(); | |||
this.makeTooltip(); | |||
} | |||
refresh() { | |||
this.fill(); | |||
this.calc_position(); | |||
// this.show_tip(); | |||
this.calcPosition(); | |||
// this.showTip(); | |||
} | |||
make_tooltip() { | |||
makeTooltip() { | |||
this.container = $.create('div', { | |||
inside: this.parent, | |||
className: 'graph-svg-tip comparison', | |||
@@ -39,27 +39,30 @@ export default class SvgTip { | |||
<ul class="data-point-list"></ul> | |||
<div class="svg-pointer"></div>` | |||
}); | |||
this.hide_tip(); | |||
this.hideTip(); | |||
this.title = this.container.querySelector('.title'); | |||
this.data_point_list = this.container.querySelector('.data-point-list'); | |||
this.dataPointList = this.container.querySelector('.data-point-list'); | |||
this.parent.addEventListener('mouseleave', () => { | |||
this.hide_tip(); | |||
this.hideTip(); | |||
}); | |||
} | |||
fill() { | |||
let title; | |||
if(this.title_value_first) { | |||
title = `<strong>${this.title_value}</strong>${this.title_name}`; | |||
if(this.index) { | |||
this.container.setAttribute('data-point-index', this.index); | |||
} | |||
if(this.titleValueFirst) { | |||
title = `<strong>${this.titleValue}</strong>${this.titleName}`; | |||
} else { | |||
title = `${this.title_name}<strong>${this.title_value}</strong>`; | |||
title = `${this.titleName}<strong>${this.titleValue}</strong>`; | |||
} | |||
this.title.innerHTML = title; | |||
this.data_point_list.innerHTML = ''; | |||
this.dataPointList.innerHTML = ''; | |||
this.list_values.map((set, i) => { | |||
this.listValues.map((set, i) => { | |||
const color = this.colors[i] || 'black'; | |||
let li = $.create('li', { | |||
@@ -70,50 +73,51 @@ export default class SvgTip { | |||
${set.title ? set.title : '' }` | |||
}); | |||
this.data_point_list.appendChild(li); | |||
this.dataPointList.appendChild(li); | |||
}); | |||
} | |||
calc_position() { | |||
calcPosition() { | |||
let width = this.container.offsetWidth; | |||
this.top = this.y - this.container.offsetHeight; | |||
this.left = this.x - width/2; | |||
let max_left = this.parent.offsetWidth - width; | |||
let maxLeft = this.parent.offsetWidth - width; | |||
let pointer = this.container.querySelector('.svg-pointer'); | |||
if(this.left < 0) { | |||
pointer.style.left = `calc(50% - ${-1 * this.left}px)`; | |||
this.left = 0; | |||
} else if(this.left > max_left) { | |||
let delta = this.left - max_left; | |||
let pointer_offset = `calc(50% + ${delta}px)`; | |||
pointer.style.left = pointer_offset; | |||
} else if(this.left > maxLeft) { | |||
let delta = this.left - maxLeft; | |||
let pointerOffset = `calc(50% + ${delta}px)`; | |||
pointer.style.left = pointerOffset; | |||
this.left = max_left; | |||
this.left = maxLeft; | |||
} else { | |||
pointer.style.left = `50%`; | |||
} | |||
} | |||
set_values(x, y, title_name = '', title_value = '', list_values = [], title_value_first = 0) { | |||
this.title_name = title_name; | |||
this.title_value = title_value; | |||
this.list_values = list_values; | |||
setValues(x, y, title = {}, listValues = [], index = -1) { | |||
this.titleName = title.name; | |||
this.titleValue = title.value; | |||
this.listValues = listValues; | |||
this.x = x; | |||
this.y = y; | |||
this.title_value_first = title_value_first; | |||
this.titleValueFirst = title.valueFirst || 0; | |||
this.index = index; | |||
this.refresh(); | |||
} | |||
hide_tip() { | |||
hideTip() { | |||
this.container.style.top = '0px'; | |||
this.container.style.left = '0px'; | |||
this.container.style.opacity = '0'; | |||
} | |||
show_tip() { | |||
showTip() { | |||
this.container.style.top = this.top + 'px'; | |||
this.container.style.left = this.left + 'px'; | |||
this.container.style.opacity = '1'; | |||
@@ -1,58 +1,105 @@ | |||
import { getBarHeightAndYAttr } from './draw-utils'; | |||
export function getAnimXLine() {} | |||
export function getAnimYLine() {} | |||
export var Animator = (function() { | |||
var Animator = function(totalHeight, totalWidth, zeroLine, avgUnitWidth) { | |||
// constants | |||
this.totalHeight = totalHeight; | |||
this.totalWidth = totalWidth; | |||
// changeables | |||
this.avgUnitWidth = avgUnitWidth; | |||
this.zeroLine = zeroLine; | |||
}; | |||
Animator.prototype = { | |||
bar: function(barObj, x, yTop, index, noOfDatasets) { | |||
let start = x - this.avgUnitWidth/4; | |||
let width = (this.avgUnitWidth/2)/noOfDatasets; | |||
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight); | |||
x = start + (width * index); | |||
return [barObj, {width: width, height: height, x: x, y: y}, 350, "easein"]; | |||
// bar.animate({height: args.newHeight, y: yTop}, 350, mina.easein); | |||
}, | |||
dot: function(dotObj, x, yTop) { | |||
return [dotObj, {cx: x, cy: yTop}, 350, "easein"]; | |||
// dot.animate({cy: yTop}, 350, mina.easein); | |||
}, | |||
path: function(d, pathStr) { | |||
let pathComponents = []; | |||
const animPath = [{unit: d.path, object: d, key: 'path'}, {d:"M"+pathStr}, 350, "easein"]; | |||
pathComponents.push(animPath); | |||
if(d.regionPath) { | |||
let regStartPt = `0,${this.zeroLine}L`; | |||
let regEndPt = `L${this.totalWidth}, ${this.zeroLine}`; | |||
const animRegion = [ | |||
{unit: d.regionPath, object: d, key: 'regionPath'}, | |||
{d:"M" + regStartPt + pathStr + regEndPt}, | |||
350, | |||
"easein" | |||
]; | |||
pathComponents.push(animRegion); | |||
} | |||
return pathComponents; | |||
}, | |||
}; | |||
return Animator; | |||
})(); | |||
export const UNIT_ANIM_DUR = 350; | |||
export const PATH_ANIM_DUR = 350; | |||
export const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR; | |||
export const REPLACE_ALL_NEW_DUR = 250; | |||
export const STD_EASING = 'easein'; | |||
export function translate(unit, oldCoord, newCoord, duration) { | |||
let old = typeof oldCoord === 'string' ? oldCoord : oldCoord.join(', '); | |||
return [ | |||
unit, | |||
{transform: newCoord.join(', ')}, | |||
duration, | |||
STD_EASING, | |||
"translate", | |||
{transform: old} | |||
]; | |||
} | |||
export function translateVertLine(xLine, newX, oldX) { | |||
return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR); | |||
} | |||
export function translateHoriLine(yLine, newY, oldY) { | |||
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR); | |||
} | |||
export function animateRegion(rectGroup, newY1, newY2, oldY2) { | |||
let newHeight = newY1 - newY2; | |||
let rect = rectGroup.childNodes[0]; | |||
let width = rect.getAttribute("width"); | |||
let rectAnim = [ | |||
rect, | |||
{ height: newHeight, 'stroke-dasharray': `${width}, ${newHeight}` }, | |||
MARKER_LINE_ANIM_DUR, | |||
STD_EASING | |||
] | |||
let groupAnim = translate(rectGroup, [0, oldY2], [0, newY2], MARKER_LINE_ANIM_DUR); | |||
return [rectAnim, groupAnim]; | |||
} | |||
export function animateBar(bar, x, yTop, width, offset=0, index=0, meta={}) { | |||
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); | |||
y -= offset; | |||
if(bar.nodeName !== 'rect') { | |||
let rect = bar.childNodes[0]; | |||
let rectAnim = [ | |||
rect, | |||
{width: width, height: height}, | |||
UNIT_ANIM_DUR, | |||
STD_EASING | |||
] | |||
let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1); | |||
let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); | |||
return [rectAnim, groupAnim]; | |||
} else { | |||
return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]]; | |||
} | |||
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein); | |||
} | |||
export function animateDot(dot, x, y) { | |||
if(dot.nodeName !== 'circle') { | |||
let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1); | |||
let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR); | |||
return [groupAnim]; | |||
} else { | |||
return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]]; | |||
} | |||
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein); | |||
} | |||
export function animatePath(paths, newXList, newYList, zeroLine) { | |||
let pathComponents = []; | |||
let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y)); | |||
let pathStr = pointsStr.join("L"); | |||
const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING]; | |||
pathComponents.push(animPath); | |||
if(paths.region) { | |||
let regStartPt = `${newXList[0]},${zeroLine}L`; | |||
let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`; | |||
const animRegion = [ | |||
paths.region, | |||
{d:"M" + regStartPt + pathStr + regEndPt}, | |||
PATH_ANIM_DUR, | |||
STD_EASING | |||
]; | |||
pathComponents.push(animRegion); | |||
} | |||
return pathComponents; | |||
} | |||
export function animatePathStr(oldPath, pathStr) { | |||
return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING]; | |||
} | |||
@@ -1,5 +1,7 @@ | |||
// Leveraging SMIL Animations | |||
import { REPLACE_ALL_NEW_DUR } from './animate'; | |||
const EASING = { | |||
ease: "0.25 0.1 0.25 1", | |||
linear: "0 0 1 1", | |||
@@ -9,7 +11,7 @@ const EASING = { | |||
easeinout: "0.42 0 0.58 1" | |||
}; | |||
function animateSVG(element, props, dur, easingType="linear", type=undefined, oldValues={}) { | |||
function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) { | |||
let animElement = element.cloneNode(true); | |||
let newElement = element.cloneNode(true); | |||
@@ -65,29 +67,23 @@ export function transform(element, style) { // eslint-disable-line no-unused-var | |||
element.style.oTransform = style; | |||
} | |||
export function runSVGAnimation(svgContainer, elements) { | |||
function animateSVG(svgContainer, elements) { | |||
let newElements = []; | |||
let animElements = []; | |||
elements.map(element => { | |||
let obj = element[0]; | |||
let parent = obj.unit.parentNode; | |||
let unit = element[0]; | |||
let parent = unit.parentNode; | |||
let animElement, newElement; | |||
element[0] = obj.unit; | |||
[animElement, newElement] = animateSVG(...element); | |||
element[0] = unit; | |||
[animElement, newElement] = animateSVGElement(...element); | |||
newElements.push(newElement); | |||
animElements.push([animElement, parent]); | |||
parent.replaceChild(animElement, obj.unit); | |||
if(obj.array) { | |||
obj.array[obj.index] = newElement; | |||
} else { | |||
obj.object[obj.key] = newElement; | |||
} | |||
parent.replaceChild(animElement, unit); | |||
}); | |||
let animSvg = svgContainer.cloneNode(true); | |||
@@ -99,3 +95,22 @@ export function runSVGAnimation(svgContainer, elements) { | |||
return animSvg; | |||
} | |||
export function runSMILAnimation(parent, svgElement, elementsToAnimate) { | |||
if(elementsToAnimate.length === 0) return; | |||
let animSvgElement = animateSVG(svgElement, elementsToAnimate); | |||
if(svgElement.parentNode == parent) { | |||
parent.removeChild(svgElement); | |||
parent.appendChild(animSvgElement); | |||
} | |||
// Replace the new svgElement (data has already been replaced) | |||
setTimeout(() => { | |||
if(animSvgElement.parentNode == parent) { | |||
parent.removeChild(animSvgElement); | |||
parent.appendChild(svgElement); | |||
} | |||
}, REPLACE_ALL_NEW_DUR); | |||
} |
@@ -0,0 +1,124 @@ | |||
import { floatTwo, fillArray } from '../utils/helpers'; | |||
import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES, DEFAULT_CHAR_WIDTH } from '../utils/constants'; | |||
export function dataPrep(data, type) { | |||
data.labels = data.labels || []; | |||
let datasetLength = data.labels.length; | |||
// Datasets | |||
let datasets = data.datasets; | |||
let zeroArray = new Array(datasetLength).fill(0); | |||
if(!datasets) { | |||
// default | |||
datasets = [{ | |||
values: zeroArray | |||
}]; | |||
} | |||
datasets.map((d, i)=> { | |||
// Set values | |||
if(!d.values) { | |||
d.values = zeroArray; | |||
} else { | |||
// Check for non values | |||
let vals = d.values; | |||
vals = vals.map(val => (!isNaN(val) ? val : 0)); | |||
// Trim or extend | |||
if(vals.length > datasetLength) { | |||
vals = vals.slice(0, datasetLength); | |||
} else { | |||
vals = fillArray(vals, datasetLength - vals.length, 0); | |||
} | |||
} | |||
// Set labels | |||
// | |||
// Set type | |||
if(!d.chartType ) { | |||
if(!AXIS_DATASET_CHART_TYPES.includes(type)) type === DEFAULT_AXIS_CHART_TYPE; | |||
d.chartType = type; | |||
} | |||
}); | |||
// Markers | |||
// Regions | |||
// data.yRegions = data.yRegions || []; | |||
if(data.yRegions) { | |||
data.yRegions.map(d => { | |||
if(d.end < d.start) { | |||
[d.start, d.end] = [d.end, d.start]; | |||
} | |||
}); | |||
} | |||
return data; | |||
} | |||
export function zeroDataPrep(realData) { | |||
let datasetLength = realData.labels.length; | |||
let zeroArray = new Array(datasetLength).fill(0); | |||
let zeroData = { | |||
labels: realData.labels.slice(0, -1), | |||
datasets: realData.datasets.map(d => { | |||
return { | |||
name: '', | |||
values: zeroArray.slice(0, -1), | |||
chartType: d.chartType | |||
} | |||
}), | |||
}; | |||
if(realData.yMarkers) { | |||
zeroData.yMarkers = [ | |||
{ | |||
value: 0, | |||
label: '' | |||
} | |||
]; | |||
} | |||
if(realData.yRegions) { | |||
zeroData.yRegions = [ | |||
{ | |||
start: 0, | |||
end: 0, | |||
label: '' | |||
} | |||
]; | |||
} | |||
return zeroData; | |||
} | |||
export function getShortenedLabels(chartWidth, labels=[], isSeries=true) { | |||
let allowedSpace = chartWidth / labels.length; | |||
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH; | |||
let calcLabels = labels.map((label, i) => { | |||
label += ""; | |||
if(label.length > allowedLetters) { | |||
if(!isSeries) { | |||
if(allowedLetters-3 > 0) { | |||
label = label.slice(0, allowedLetters-3) + " ..."; | |||
} else { | |||
label = label.slice(0, allowedLetters) + '..'; | |||
} | |||
} else { | |||
let multiple = Math.ceil(label.length/allowedLetters); | |||
if(i % multiple !== 0) { | |||
label = ""; | |||
} | |||
} | |||
} | |||
return label; | |||
}); | |||
return calcLabels; | |||
} |
@@ -16,7 +16,7 @@ const PRESET_COLOR_MAP = { | |||
}; | |||
export const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange', | |||
'yellow', 'green', 'light-green', 'purple', 'magenta']; | |||
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey']; | |||
function limitColor(r){ | |||
if (r > 255) return 255; | |||
@@ -0,0 +1,23 @@ | |||
export const VERT_SPACE_OUTSIDE_BASE_CHART = 50; | |||
export const TRANSLATE_Y_BASE_CHART = 20; | |||
export const LEFT_MARGIN_BASE_CHART = 60; | |||
export const RIGHT_MARGIN_BASE_CHART = 40; | |||
export const Y_AXIS_MARGIN = 60; | |||
export const INIT_CHART_UPDATE_TIMEOUT = 700; | |||
export const CHART_POST_ANIMATE_TIMEOUT = 400; | |||
export const DEFAULT_AXIS_CHART_TYPE = 'line'; | |||
export const AXIS_DATASET_CHART_TYPES = ['line', 'bar']; | |||
export const BAR_CHART_SPACE_RATIO = 0.5; | |||
export const MIN_BAR_PERCENT_HEIGHT = 0.01; | |||
export const LINE_CHART_DOT_SIZE = 4; | |||
export const DOT_OVERLAY_SIZE_INCR = 4; | |||
export const DEFAULT_CHAR_WIDTH = 7; | |||
// Universal constants | |||
export const ANGLE_RATIO = Math.PI / 180; | |||
export const FULL_ANGLE = 360; |
@@ -31,4 +31,9 @@ export function addDays(date, 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]; | |||
} |
@@ -43,7 +43,7 @@ $.create = (tag, o) => { | |||
return element; | |||
}; | |||
export function offset(element) { | |||
export function getOffset(element) { | |||
let rect = element.getBoundingClientRect(); | |||
return { | |||
// https://stackoverflow.com/a/7436602/6495043 | |||
@@ -74,7 +74,7 @@ export function getElementContentWidth(element) { | |||
return element.clientWidth - padding; | |||
} | |||
$.bind = (element, o) => { | |||
export function bind(element, o){ | |||
if (element) { | |||
for (var event in o) { | |||
var callback = o[event]; | |||
@@ -84,9 +84,9 @@ $.bind = (element, o) => { | |||
}); | |||
} | |||
} | |||
}; | |||
} | |||
$.unbind = (element, o) => { | |||
export function unbind(element, o){ | |||
if (element) { | |||
for (var event in o) { | |||
var callback = o[event]; | |||
@@ -96,9 +96,9 @@ $.unbind = (element, o) => { | |||
}); | |||
} | |||
} | |||
}; | |||
} | |||
$.fire = (target, type, properties) => { | |||
export function fire(target, type, properties) { | |||
var evt = document.createEvent("HTMLEvents"); | |||
evt.initEvent(type, true, true ); | |||
@@ -108,4 +108,4 @@ $.fire = (target, type, properties) => { | |||
} | |||
return target.dispatchEvent(evt); | |||
}; | |||
} |
@@ -1,23 +1,26 @@ | |||
export function getBarHeightAndYAttr(yTop, zeroLine, totalHeight) { | |||
import { fillArray } from './helpers'; | |||
export function getBarHeightAndYAttr(yTop, zeroLine) { | |||
let height, y; | |||
if (yTop <= zeroLine) { | |||
height = zeroLine - yTop; | |||
y = yTop; | |||
// In case of invisible bars | |||
if(height === 0) { | |||
height = totalHeight * 0.01; | |||
y -= height; | |||
} | |||
} else { | |||
height = yTop - zeroLine; | |||
y = zeroLine; | |||
// In case of invisible bars | |||
if(height === 0) { | |||
height = totalHeight * 0.01; | |||
} | |||
} | |||
return [height, y]; | |||
} | |||
export function equilizeNoOfElements(array1, array2, | |||
extraCount = array2.length - array1.length) { | |||
// Doesn't work if either has zero elements. | |||
if(extraCount > 0) { | |||
array1 = fillArray(array1, extraCount); | |||
} else { | |||
array2 = fillArray(array2, extraCount); | |||
} | |||
return [array1, array2]; | |||
} |
@@ -1,12 +1,19 @@ | |||
import { getBarHeightAndYAttr } from './draw-utils'; | |||
import { getStringWidth } from './helpers'; | |||
import { STD_EASING, UNIT_ANIM_DUR, MARKER_LINE_ANIM_DUR, PATH_ANIM_DUR } from './animate'; | |||
import { DOT_OVERLAY_SIZE_INCR } from './constants'; | |||
// Constants used | |||
const AXIS_TICK_LENGTH = 6; | |||
const LABEL_MARGIN = 4; | |||
export const FONT_SIZE = 10; | |||
const BASE_LINE_COLOR = '#dadada'; | |||
const BASE_BG_COLOR = '#F7FAFC'; | |||
function $(expr, con) { | |||
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | |||
} | |||
function createSVG(tag, o) { | |||
export function createSVG(tag, o) { | |||
var element = document.createElementNS("http://www.w3.org/2000/svg", tag); | |||
for (var i in o) { | |||
@@ -82,6 +89,14 @@ export function makeSVGGroup(parent, className, transform='') { | |||
}); | |||
} | |||
export function wrapInSVGGroup(elements, className='') { | |||
let g = createSVG('g', { | |||
className: className | |||
}); | |||
elements.forEach(e => g.appendChild(e)); | |||
return g; | |||
} | |||
export function makePath(pathStr, className='', stroke='none', fill='none') { | |||
return createSVG('path', { | |||
className: className, | |||
@@ -93,8 +108,18 @@ export function makePath(pathStr, className='', stroke='none', fill='none') { | |||
}); | |||
} | |||
export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1){ | |||
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y]; | |||
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y]; | |||
return `M${center.x} ${center.y} | |||
L${arcStartX} ${arcStartY} | |||
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} | |||
${arcEndX} ${arcEndY} z`; | |||
} | |||
export function makeGradient(svgDefElem, color, lighter = false) { | |||
let gradientId ='path-fill-gradient' + '-' + color; | |||
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default'); | |||
let gradientDef = renderVerticalGradient(svgDefElem, gradientId); | |||
let opacities = [1, 0.6, 0.2]; | |||
if(lighter) { | |||
@@ -130,109 +155,385 @@ export function makeText(className, x, y, content) { | |||
className: className, | |||
x: x, | |||
y: y, | |||
dy: '.32em', | |||
dy: (FONT_SIZE / 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
innerHTML: content | |||
}); | |||
} | |||
export function makeXLine(height, textStartAt, point, labelClass, axisLineClass, xPos) { | |||
let line = createSVG('line', { | |||
function makeVertLine(x, label, y1, y2, options={}) { | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
let l = createSVG('line', { | |||
className: 'line-vertical ' + options.className, | |||
x1: 0, | |||
x2: 0, | |||
y1: 0, | |||
y2: height | |||
y1: y1, | |||
y2: y2, | |||
styles: { | |||
stroke: options.stroke | |||
} | |||
}); | |||
let text = createSVG('text', { | |||
className: labelClass, | |||
x: 0, | |||
y: textStartAt, | |||
dy: '.71em', | |||
innerHTML: point | |||
y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE, | |||
dy: FONT_SIZE + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'middle', | |||
innerHTML: label + "" | |||
}); | |||
let xLine = createSVG('g', { | |||
className: `tick ${axisLineClass}`, | |||
transform: `translate(${ xPos }, 0)` | |||
let line = createSVG('g', { | |||
transform: `translate(${ x }, 0)` | |||
}); | |||
xLine.appendChild(line); | |||
xLine.appendChild(text); | |||
line.appendChild(l); | |||
line.appendChild(text); | |||
return xLine; | |||
return line; | |||
} | |||
export function makeYLine(startAt, width, textEndAt, point, labelClass, axisLineClass, yPos, darker=false, lineType="") { | |||
let line = createSVG('line', { | |||
className: lineType === "dashed" ? "dashed": "", | |||
x1: startAt, | |||
x2: width, | |||
function makeHoriLine(y, label, x1, x2, options={}) { | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
if(!options.lineType) options.lineType = ''; | |||
let className = 'line-horizontal ' + options.className + | |||
(options.lineType === "dashed" ? "dashed": ""); | |||
let l = createSVG('line', { | |||
className: className, | |||
x1: x1, | |||
x2: x2, | |||
y1: 0, | |||
y2: 0 | |||
y2: 0, | |||
styles: { | |||
stroke: options.stroke | |||
} | |||
}); | |||
let text = createSVG('text', { | |||
className: labelClass, | |||
x: textEndAt, | |||
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, | |||
y: 0, | |||
dy: '.32em', | |||
innerHTML: point+"" | |||
dy: (FONT_SIZE / 2 - 2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': x1 < x2 ? 'end' : 'start', | |||
innerHTML: label+"" | |||
}); | |||
let yLine = createSVG('g', { | |||
className: `tick ${axisLineClass}`, | |||
transform: `translate(0, ${yPos})`, | |||
let line = createSVG('g', { | |||
transform: `translate(0, ${y})`, | |||
'stroke-opacity': 1 | |||
}); | |||
if(darker) { | |||
if(text === 0 || text === '0') { | |||
line.style.stroke = "rgba(27, 31, 35, 0.6)"; | |||
} | |||
yLine.appendChild(line); | |||
yLine.appendChild(text); | |||
line.appendChild(l); | |||
line.appendChild(text); | |||
return yLine; | |||
return line; | |||
} | |||
export var UnitRenderer = (function() { | |||
var UnitRenderer = function(totalHeight, zeroLine, avgUnitWidth) { | |||
this.totalHeight = totalHeight; | |||
this.zeroLine = zeroLine; | |||
this.avgUnitWidth = avgUnitWidth; | |||
}; | |||
export function yLine(y, label, width, options={}) { | |||
if(!options.pos) options.pos = 'left'; | |||
if(!options.offset) options.offset = 0; | |||
if(!options.mode) options.mode = 'span'; | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
if(!options.className) options.className = ''; | |||
let x1 = -1 * AXIS_TICK_LENGTH; | |||
let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0; | |||
if(options.mode === 'tick' && options.pos === 'right') { | |||
x1 = width + AXIS_TICK_LENGTH | |||
x2 = width; | |||
} | |||
let offset = options.pos === 'left' ? -1 * options.offset : options.offset; | |||
UnitRenderer.prototype = { | |||
bar: function (x, yTop, args, color, index, datasetIndex, noOfDatasets) { | |||
let totalWidth = this.avgUnitWidth - args.spaceWidth; | |||
let startX = x - totalWidth/2; | |||
let width = totalWidth / noOfDatasets; | |||
let currentX = startX + width * datasetIndex; | |||
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight); | |||
return createSVG('rect', { | |||
className: `bar mini`, | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
x: currentX, | |||
y: y, | |||
width: width, | |||
height: height | |||
}); | |||
x1 += options.offset; | |||
x2 += options.offset; | |||
return makeHoriLine(y, label, x1, x2, { | |||
stroke: options.stroke, | |||
className: options.className, | |||
lineType: options.lineType | |||
}); | |||
} | |||
export function xLine(x, label, height, options={}) { | |||
if(!options.pos) options.pos = 'bottom'; | |||
if(!options.offset) options.offset = 0; | |||
if(!options.mode) options.mode = 'span'; | |||
if(!options.stroke) options.stroke = BASE_LINE_COLOR; | |||
if(!options.className) options.className = ''; | |||
// Draw X axis line in span/tick mode with optional label | |||
// y2(span) | |||
// | | |||
// | | |||
// x line | | |||
// | | |||
// | | |||
// ---------------------+-- y2(tick) | |||
// | | |||
// y1 | |||
let y1 = height + AXIS_TICK_LENGTH; | |||
let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height; | |||
if(options.mode === 'tick' && options.pos === 'top') { | |||
// top axis ticks | |||
y1 = -1 * AXIS_TICK_LENGTH; | |||
y2 = 0; | |||
} | |||
return makeVertLine(x, label, y1, y2, { | |||
stroke: options.stroke, | |||
className: options.className, | |||
lineType: options.lineType | |||
}); | |||
} | |||
export function yMarker(y, label, width, options={}) { | |||
let labelSvg = createSVG('text', { | |||
className: 'chart-label', | |||
x: width - getStringWidth(label, 5) - LABEL_MARGIN, | |||
y: 0, | |||
dy: (FONT_SIZE / -2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'start', | |||
innerHTML: label+"" | |||
}); | |||
let line = makeHoriLine(y, '', 0, width, { | |||
stroke: options.stroke || BASE_LINE_COLOR, | |||
className: options.className || '', | |||
lineType: options.lineType | |||
}); | |||
line.appendChild(labelSvg); | |||
return line; | |||
} | |||
export function yRegion(y1, y2, width, label) { | |||
// return a group | |||
let height = y1 - y2; | |||
let rect = createSVG('rect', { | |||
className: `bar mini`, // remove class | |||
styles: { | |||
fill: `rgba(228, 234, 239, 0.49)`, | |||
stroke: BASE_LINE_COLOR, | |||
'stroke-dasharray': `${width}, ${height}` | |||
}, | |||
// 'data-point-index': index, | |||
x: 0, | |||
y: 0, | |||
width: width, | |||
height: height | |||
}); | |||
let labelSvg = createSVG('text', { | |||
className: 'chart-label', | |||
x: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN, | |||
y: 0, | |||
dy: (FONT_SIZE / -2) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'start', | |||
innerHTML: label+"" | |||
}); | |||
dot: function(x, y, args, color, index) { | |||
return createSVG('circle', { | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
cx: x, | |||
cy: y, | |||
r: args.radius | |||
}); | |||
let region = createSVG('g', { | |||
transform: `translate(0, ${y2})` | |||
}); | |||
region.appendChild(rect); | |||
region.appendChild(labelSvg); | |||
return region; | |||
} | |||
export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) { | |||
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine); | |||
y -= offset; | |||
let rect = createSVG('rect', { | |||
className: `bar mini`, | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
x: x, | |||
y: y, | |||
width: width, | |||
height: height || meta.minHeight // TODO: correct y for positive min height | |||
}); | |||
label += ""; | |||
if(!label && !label.length) { | |||
return rect; | |||
} else { | |||
rect.setAttribute('y', 0); | |||
rect.setAttribute('x', 0); | |||
let text = createSVG('text', { | |||
className: 'data-point-value', | |||
x: width/2, | |||
y: 0, | |||
dy: (FONT_SIZE / 2 * -1) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'middle', | |||
innerHTML: label | |||
}); | |||
let group = createSVG('g', { | |||
'data-point-index': index, | |||
transform: `translate(${x}, ${y})` | |||
}); | |||
group.appendChild(rect); | |||
group.appendChild(text); | |||
return group; | |||
} | |||
} | |||
export function datasetDot(x, y, radius, color, label='', index=0, meta={}) { | |||
let dot = createSVG('circle', { | |||
style: `fill: ${color}`, | |||
'data-point-index': index, | |||
cx: x, | |||
cy: y, | |||
r: radius | |||
}); | |||
label += ""; | |||
if(!label && !label.length) { | |||
return dot; | |||
} else { | |||
dot.setAttribute('cy', 0); | |||
dot.setAttribute('cx', 0); | |||
let text = createSVG('text', { | |||
className: 'data-point-value', | |||
x: 0, | |||
y: 0, | |||
dy: (FONT_SIZE / 2 * -1 - radius) + 'px', | |||
'font-size': FONT_SIZE + 'px', | |||
'text-anchor': 'middle', | |||
innerHTML: label | |||
}); | |||
let group = createSVG('g', { | |||
'data-point-index': index, | |||
transform: `translate(${x}, ${y})` | |||
}); | |||
group.appendChild(dot); | |||
group.appendChild(text); | |||
return group; | |||
} | |||
} | |||
export function getPaths(xList, yList, color, options={}, meta={}) { | |||
let pointsList = yList.map((y, i) => (xList[i] + ',' + y)); | |||
let pointsStr = pointsList.join("L"); | |||
let path = makePath("M"+pointsStr, 'line-graph-path', color); | |||
// HeatLine | |||
if(options.heatline) { | |||
let gradient_id = makeGradient(meta.svgDefs, color); | |||
path.style.stroke = `url(#${gradient_id})`; | |||
} | |||
let paths = { | |||
path: path | |||
} | |||
// Region | |||
if(options.regionFill) { | |||
let gradient_id_region = makeGradient(meta.svgDefs, color, true); | |||
// TODO: use zeroLine OR minimum | |||
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`; | |||
paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`); | |||
} | |||
return paths; | |||
} | |||
export let makeOverlay = { | |||
'bar': (unit) => { | |||
let transformValue; | |||
if(unit.nodeName !== 'rect') { | |||
transformValue = unit.getAttribute('transform'); | |||
unit = unit.childNodes[0]; | |||
} | |||
}; | |||
let overlay = unit.cloneNode(); | |||
overlay.style.fill = '#000000'; | |||
overlay.style.opacity = '0.4'; | |||
if(transformValue) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
return overlay; | |||
}, | |||
'dot': (unit) => { | |||
let transformValue; | |||
if(unit.nodeName !== 'circle') { | |||
transformValue = unit.getAttribute('transform'); | |||
unit = unit.childNodes[0]; | |||
} | |||
let overlay = unit.cloneNode(); | |||
let radius = unit.getAttribute('r'); | |||
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) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
return overlay; | |||
} | |||
} | |||
export let updateOverlay = { | |||
'bar': (unit, overlay) => { | |||
let transformValue; | |||
if(unit.nodeName !== 'rect') { | |||
transformValue = unit.getAttribute('transform'); | |||
unit = unit.childNodes[0]; | |||
} | |||
let attributes = ['x', 'y', 'width', 'height']; | |||
Object.values(unit.attributes) | |||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||
.map(attr => { | |||
overlay.setAttribute(attr.name, attr.nodeValue); | |||
}); | |||
if(transformValue) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
}, | |||
'dot': (unit, overlay) => { | |||
let transformValue; | |||
if(unit.nodeName !== 'circle') { | |||
transformValue = unit.getAttribute('transform'); | |||
unit = unit.childNodes[0]; | |||
} | |||
let attributes = ['cx', 'cy']; | |||
Object.values(unit.attributes) | |||
.filter(attr => attributes.includes(attr.name) && attr.specified) | |||
.map(attr => { | |||
overlay.setAttribute(attr.name, attr.nodeValue); | |||
}); | |||
if(transformValue) { | |||
overlay.setAttribute('transform', transformValue); | |||
} | |||
} | |||
} | |||
return UnitRenderer; | |||
})(); |
@@ -1,3 +1,5 @@ | |||
import { ANGLE_RATIO } from './constants'; | |||
/** | |||
* Returns the value of a number upto 2 decimal places. | |||
* @param {Number} d Any number | |||
@@ -37,6 +39,22 @@ export function shuffle(array) { | |||
return array; | |||
} | |||
/** | |||
* Fill an array with extra points | |||
* @param {Array} array Array | |||
* @param {Number} count number of filler elements | |||
* @param {Object} element element to fill with | |||
* @param {Boolean} start fill at start? | |||
*/ | |||
export function fillArray(array, count, element, start=false) { | |||
if(!element) { | |||
element = start ? array[0] : array[array.length - 1]; | |||
} | |||
let fillerArray = new Array(Math.abs(count)).fill(element); | |||
array = start ? fillerArray.concat(array) : array.concat(fillerArray); | |||
return array; | |||
} | |||
/** | |||
* Returns pixel width of string. | |||
* @param {String} string | |||
@@ -45,3 +63,23 @@ export function shuffle(array) { | |||
export function getStringWidth(string, charWidth) { | |||
return (string+"").length * charWidth; | |||
} | |||
export function bindChange(obj, getFn, setFn) { | |||
return new Proxy(obj, { | |||
set: function(target, prop, value) { | |||
setFn(); | |||
return Reflect.set(target, prop, value); | |||
}, | |||
get: function(target, prop, value) { | |||
getFn(); | |||
return Reflect.get(target, prop); | |||
} | |||
}); | |||
} | |||
export function getPositionByAngle(angle, radius) { | |||
return { | |||
x:Math.sin(angle * ANGLE_RATIO) * radius, | |||
y:Math.cos(angle * ANGLE_RATIO) * radius, | |||
}; | |||
} |
@@ -1,3 +1,5 @@ | |||
import { floatTwo } from './helpers'; | |||
function normalize(x) { | |||
// Calculates mantissa and exponent of a number | |||
// Returns normalized number and exponent | |||
@@ -21,7 +23,7 @@ function normalize(x) { | |||
return [sig * man, exp]; | |||
} | |||
function getRangeIntervals(max, min=0) { | |||
function getChartRangeIntervals(max, min=0) { | |||
let upperBound = Math.ceil(max); | |||
let lowerBound = Math.floor(min); | |||
let range = upperBound - lowerBound; | |||
@@ -59,19 +61,19 @@ function getRangeIntervals(max, min=0) { | |||
return intervals; | |||
} | |||
function getIntervals(maxValue, minValue=0) { | |||
function getChartIntervals(maxValue, minValue=0) { | |||
let [normalMaxValue, exponent] = normalize(maxValue); | |||
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0; | |||
// Allow only 7 significant digits | |||
normalMaxValue = normalMaxValue.toFixed(6); | |||
let intervals = getRangeIntervals(normalMaxValue, normalMinValue); | |||
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue); | |||
intervals = intervals.map(value => value * Math.pow(10, exponent)); | |||
return intervals; | |||
} | |||
export function calcIntervals(values, withMinimum=false) { | |||
export function calcChartIntervals(values, withMinimum=false) { | |||
//*** Where the magic happens *** | |||
// Calculates best-fit y intervals from given values | |||
@@ -84,7 +86,7 @@ export function calcIntervals(values, withMinimum=false) { | |||
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars | |||
function getPositiveFirstIntervals(maxValue, absMinValue) { | |||
let intervals = getIntervals(maxValue); | |||
let intervals = getChartIntervals(maxValue); | |||
let intervalSize = intervals[1] - intervals[0]; | |||
@@ -102,9 +104,9 @@ export function calcIntervals(values, withMinimum=false) { | |||
if(maxValue >= 0 && minValue >= 0) { | |||
exponent = normalize(maxValue)[1]; | |||
if(!withMinimum) { | |||
intervals = getIntervals(maxValue); | |||
intervals = getChartIntervals(maxValue); | |||
} else { | |||
intervals = getIntervals(maxValue, minValue); | |||
intervals = getChartIntervals(maxValue, minValue); | |||
} | |||
} | |||
@@ -142,9 +144,9 @@ export function calcIntervals(values, withMinimum=false) { | |||
exponent = normalize(pseudoMaxValue)[1]; | |||
if(!withMinimum) { | |||
intervals = getIntervals(pseudoMaxValue); | |||
intervals = getChartIntervals(pseudoMaxValue); | |||
} else { | |||
intervals = getIntervals(pseudoMaxValue, pseudoMinValue); | |||
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue); | |||
} | |||
intervals = intervals.reverse().map(d => d * (-1)); | |||
@@ -153,6 +155,51 @@ export function calcIntervals(values, withMinimum=false) { | |||
return intervals; | |||
} | |||
export function getZeroIndex(yPts) { | |||
let zeroIndex; | |||
let interval = getIntervalSize(yPts); | |||
if(yPts.indexOf(0) >= 0) { | |||
// the range has a given zero | |||
// zero-line on the chart | |||
zeroIndex = yPts.indexOf(0); | |||
} else if(yPts[0] > 0) { | |||
// Minimum value is positive | |||
// zero-line is off the chart: below | |||
let min = yPts[0]; | |||
zeroIndex = (-1) * min / interval; | |||
} else { | |||
// Maximum value is negative | |||
// zero-line is off the chart: above | |||
let max = yPts[yPts.length - 1]; | |||
zeroIndex = (-1) * max / interval + (yPts.length - 1); | |||
} | |||
return zeroIndex; | |||
} | |||
export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) { | |||
let range = max - min; | |||
let part = range * 1.0 / noOfIntervals; | |||
let intervals = []; | |||
for(var i = 0; i <= noOfIntervals; i++) { | |||
intervals.push(min + part * i); | |||
} | |||
return asc ? intervals : intervals.reverse(); | |||
} | |||
export function getIntervalSize(orderedArray) { | |||
return orderedArray[1] - orderedArray[0]; | |||
} | |||
export function getValueRange(orderedArray) { | |||
return orderedArray[orderedArray.length-1] - orderedArray[0]; | |||
} | |||
export function scale(val, yAxis) { | |||
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier) | |||
} | |||
export function calcDistribution(values, distributionSize) { | |||
// Assume non-negative values, | |||
// implying distribution minimum at zero | |||
@@ -28,8 +28,13 @@ | |||
} | |||
.graph-stats-container { | |||
display: flex; | |||
justify-content: space-around; | |||
padding-top: 10px; | |||
justify-content: space-between; | |||
padding: 10px; | |||
&:before, | |||
&:after { | |||
content: ''; | |||
display: block; | |||
} | |||
.stats { | |||
padding-bottom: 15px; | |||
} | |||
@@ -51,8 +56,8 @@ | |||
} | |||
} | |||
.axis, .chart-label { | |||
font-size: 11px; | |||
fill: #555b51; | |||
// temp commented | |||
line { | |||
stroke: #dadada; | |||
} | |||
@@ -62,11 +67,26 @@ | |||
margin-bottom: 0px; | |||
} | |||
} | |||
.data-points { | |||
.dataset-units { | |||
circle { | |||
stroke: #fff; | |||
stroke-width: 2; | |||
} | |||
// temp | |||
path { | |||
fill: none; | |||
stroke-opacity: 1; | |||
stroke-width: 2px; | |||
} | |||
} | |||
.multiaxis-chart { | |||
.line-horizontal, .y-axis-guide { | |||
stroke-width: 2px; | |||
} | |||
} | |||
.dataset-path { | |||
stroke-width: 2px; | |||
} | |||
.path-group { | |||
path { | |||
@@ -78,17 +98,18 @@ | |||
line.dashed { | |||
stroke-dasharray: 5,3; | |||
} | |||
.tick { | |||
&.x-axis-label { | |||
display: block; | |||
} | |||
.axis-line { | |||
// &.x-axis-label { | |||
// display: block; | |||
// } | |||
// TODO: hack dy attr to be settable via styles | |||
.specific-value { | |||
text-anchor: start; | |||
} | |||
.y-value-text { | |||
.y-line { | |||
text-anchor: end; | |||
} | |||
.x-value-text { | |||
.x-line { | |||
text-anchor: middle; | |||
} | |||
} | |||