@@ -1,6 +1,6 @@ | |||||
<div align="center"> | <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 align="center"> | ||||
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p> | <p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p> | ||||
<a href="https://frappe.github.io/charts"> | <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 | // 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", | labels: ["2007", "2008", "2009", "2010", "2011", "2012", | ||||
"2013", "2014", "2015", "2016", "2017"], | "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: [{ | 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 c1 = document.querySelector("#chart-composite-1"); | ||||
let c2 = document.querySelector("#chart-composite-2"); | let c2 = document.querySelector("#chart-composite-2"); | ||||
let bar_composite_chart = new Chart ({ | |||||
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', | type: 'line', | ||||
height: 180, | |||||
height: 190, | |||||
colors: ['green'], | 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) | // Demo Chart (bar, linepts, scatter(blobs), percentage) | ||||
// ================================================================================ | // ================================================================================ | ||||
let type_data = { | |||||
let typeData = { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | ||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | ||||
yMarkers: [ | |||||
{ | |||||
label: "Marker", | |||||
value: 43, | |||||
// type: 'dashed' | |||||
} | |||||
], | |||||
yRegions: [ | |||||
{ | |||||
label: "Region", | |||||
start: -10, | |||||
end: 50 | |||||
}, | |||||
], | |||||
datasets: [ | 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, | 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( | Array.prototype.slice.call( | ||||
document.querySelectorAll('.chart-plot-buttons button') | |||||
document.querySelectorAll('.aggr-type-buttons button') | |||||
).map(el => { | ).map(el => { | ||||
el.addEventListener('click', (e) => { | el.addEventListener('click', (e) => { | ||||
let btn = e.target; | let btn = e.target; | ||||
let type = btn.getAttribute('data-type'); | let type = btn.getAttribute('data-type'); | ||||
let config = []; | |||||
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( | Array.prototype.slice.call( | ||||
btn.parentNode.querySelectorAll('button')).map(el => { | btn.parentNode.querySelectorAll('button')).map(el => { | ||||
el.classList.remove('active'); | 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", | 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", | "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", | ||||
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"]; | "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 | // We're gonna be shuffling this | ||||
let update_data_all_indices = update_data_all_labels.map((d,i) => i); | let update_data_all_indices = update_data_all_labels.map((d,i) => i); | ||||
@@ -209,47 +243,135 @@ let update_data = { | |||||
datasets: [{ | datasets: [{ | ||||
"values": get_update_data(update_data_all_values) | "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, | data: update_data, | ||||
type: 'line', | type: 'line', | ||||
height: 250, | 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'); | let chart_update_buttons = document.querySelector('.chart-update-buttons'); | ||||
chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => { | ||||
shuffle(update_data_all_indices); | 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) => { | 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; | 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) => { | 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)", | title: "Jupiter's Moons: Semi-major Axis (1000 km)", | ||||
data: events_data, | data: events_data, | ||||
type: 'bar', | type: 'bar', | ||||
height: 250, | height: 250, | ||||
colors: ['grey'], | colors: ['grey'], | ||||
is_navigable: 1, | |||||
isNavigable: 1, | |||||
}); | }); | ||||
let data_div = document.querySelector('.chart-events-data'); | 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"; | 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 | // Heatmap | ||||
// ================================================================================ | // ================================================================================ | ||||
let heatmap_data = {}; | |||||
let heatmapData = {}; | |||||
let current_date = new Date(); | let current_date = new Date(); | ||||
let timestamp = current_date.getTime()/1000; | let timestamp = current_date.getTime()/1000; | ||||
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight | ||||
for (var i = 0; i< 375; i++) { | for (var i = 0; i< 375; i++) { | ||||
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||||
heatmapData[parseInt(timestamp)] = Math.floor(Math.random() * 5); | |||||
timestamp = Math.floor(timestamp - 86400).toFixed(1); | timestamp = Math.floor(timestamp - 86400).toFixed(1); | ||||
} | } | ||||
new Chart({ | |||||
parent: "#chart-heatmap", | |||||
data: heatmap_data, | |||||
new Chart("#chart-heatmap", { | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: 1 // default 0 | |||||
discreteDomains: 1, | |||||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'] | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -384,10 +462,10 @@ Array.prototype.slice.call( | |||||
el.addEventListener('click', (e) => { | el.addEventListener('click', (e) => { | ||||
let btn = e.target; | let btn = e.target; | ||||
let mode = btn.getAttribute('data-mode'); | let mode = btn.getAttribute('data-mode'); | ||||
let discrete_domains = 0; | |||||
let discreteDomains = 0; | |||||
if(mode === 'discrete') { | if(mode === 'discrete') { | ||||
discrete_domains = 1; | |||||
discreteDomains = 1; | |||||
} | } | ||||
let colors = []; | let colors = []; | ||||
@@ -398,14 +476,13 @@ Array.prototype.slice.call( | |||||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | ||||
} | } | ||||
new Chart({ | |||||
parent: "#chart-heatmap", | |||||
data: heatmap_data, | |||||
new Chart("#chart-heatmap", { | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: discrete_domains, | |||||
legend_colors: colors | |||||
discreteDomains: discreteDomains, | |||||
legendColors: colors | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -428,23 +505,22 @@ Array.prototype.slice.call( | |||||
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']; | ||||
} | } | ||||
let discrete_domains = 1; | |||||
let discreteDomains = 1; | |||||
let view_mode = document | let view_mode = document | ||||
.querySelector('.heatmap-mode-buttons .active') | .querySelector('.heatmap-mode-buttons .active') | ||||
.getAttribute('data-mode'); | .getAttribute('data-mode'); | ||||
if(view_mode === 'continuous') { | if(view_mode === 'continuous') { | ||||
discrete_domains = 0; | |||||
discreteDomains = 0; | |||||
} | } | ||||
new Chart({ | |||||
parent: "#chart-heatmap", | |||||
data: heatmap_data, | |||||
new Chart("#chart-heatmap", { | |||||
data: heatmapData, | |||||
type: 'heatmap', | type: 'heatmap', | ||||
legend_scale: [0, 1, 2, 4, 5], | |||||
legendScale: [0, 1, 2, 4, 5], | |||||
height: 115, | height: 115, | ||||
discrete_domains: discrete_domains, | |||||
legend_colors: colors | |||||
discreteDomains: discreteDomains, | |||||
legendColors: colors | |||||
}); | }); | ||||
Array.prototype.slice.call( | Array.prototype.slice.call( | ||||
@@ -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="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"> | <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> | </head> | ||||
<body> | <body> | ||||
<div class="container"> | <div class="container"> | ||||
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | <div class="row hero" style="padding-top: 30px; padding-bottom: 0px;"> | ||||
<div class="jumbotron" style="background: transparent;"> | <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">GitHub-inspired simple and modern charts for the web</p> | ||||
<p class="mt-2">with zero dependencies.</p> | <p class="mt-2">with zero dependencies.</p> | ||||
<!--<p class="mt-2">Because dumb charts are hard to come by.</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="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<h6 class="margin-vertical-rem"> | |||||
<!--Bars, Lines or <a href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts" target="_blank">Percentages</a>--> | |||||
Create a chart | |||||
</h6> | |||||
<p class="step-explain">Install</p> | |||||
<pre><code class="hljs console"> npm install frappe-charts</code></pre> | |||||
<p class="step-explain">And include it in your project</p> | |||||
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre> | |||||
<p class="step-explain">... or include it directly in your HTML</p> | |||||
<pre><code class="hljs html"> <script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"></script></code></pre> | |||||
<p class="step-explain">Make a new Chart</p> | |||||
<h6 class="margin-vertical-rem">Create a chart</h6> | |||||
<pre><code class="hljs html"> <!--HTML--> | <pre><code class="hljs html"> <!--HTML--> | ||||
<div id="chart"></div></code></pre> | <div id="chart"></div></code></pre> | ||||
<pre><code class="hljs javascript"> // Javascript | <pre><code class="hljs javascript"> // Javascript | ||||
let data = { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
let chart = new Chart( "#chart", { // or DOM element | |||||
data: { | |||||
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm", | |||||
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | "12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"], | ||||
datasets: [ | |||||
{ | |||||
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> | });</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='pie'>Pie Chart</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | <button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button> | ||||
</div> | </div> | ||||
<p class="text-muted"> | |||||
<!-- <p class="text-muted"> | |||||
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | <a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a> | ||||
</p> | |||||
</p> --> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -114,42 +112,12 @@ | |||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
Update Values | Update Values | ||||
</h6> | </h6> | ||||
<pre><code class="hljs javascript"> // Update entire datasets | |||||
chart.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 id="chart-update" class="border"></div> | ||||
<div class="chart-update-buttons mt-1 mx-auto" role="group"> | <div class="chart-update-buttons mt-1 mx-auto" role="group"> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | <button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button> | ||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> ... | |||||
// Include specific Y values in input data to be displayed as lines | |||||
// (before passing data to a new chart): | |||||
data.specific_values = [ | |||||
{ | |||||
title: "Altitude", | |||||
line_type: "dashed", // or "solid" | |||||
value: 38 | |||||
} | |||||
] | |||||
...</code></pre> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -158,25 +126,20 @@ | |||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
Plot Trends | Plot Trends | ||||
</h6> | </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 id="chart-trends" class="border"></div> | ||||
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | <div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group"> | ||||
<button type="button" class="btn btn-sm btn-secondary" data-type="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 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> | </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 | 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> | ||||
</div> | </div> | ||||
@@ -204,8 +167,7 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> ... | <pre><code class="hljs javascript margin-vertical-px"> ... | ||||
type: 'bar', // Bar Chart specific properties: | |||||
is_navigable: 1, // Navigate across bars; default 0 | |||||
isNavigable: 1, // Navigate across data points; default 0 | |||||
... | ... | ||||
chart.parent.addEventListener('data-select', (e) => { | chart.parent.addEventListener('data-select', (e) => { | ||||
@@ -214,22 +176,6 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
<div class="col-sm-10 push-sm-1"> | |||||
<div class="dashboard-section"> | |||||
<h6 class="margin-vertical-rem"> | |||||
Simple Aggregations | |||||
</h6> | |||||
<div id="chart-aggr" class="border"></div> | |||||
<div class="chart-aggr-buttons mt-1 mx-auto" role="group"> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="sums">Show Sums</button> | |||||
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="average">Show Averages</button> | |||||
</div> | |||||
<pre><code class="hljs javascript margin-vertical-px"> chart.show_sums(); // and `hide_sums()` | |||||
chart.show_averages(); // and `hide_averages()`</code></pre> | |||||
</div> | |||||
</div> | |||||
<div class="col-sm-10 push-sm-1"> | <div class="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<h6 class="margin-vertical-rem"> | <h6 class="margin-vertical-rem"> | ||||
@@ -242,23 +188,23 @@ | |||||
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | <button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button> | ||||
</div> | </div> | ||||
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group"> | <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> | </div> | ||||
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({ | <pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({ | ||||
parent: "#heatmap", | parent: "#heatmap", | ||||
type: 'heatmap', | type: 'heatmap', | ||||
height: 115, | height: 115, | ||||
data: heatmap_data, // object with date/timestamp-value pairs | |||||
data: heatmapData, // object with date/timestamp-value pairs | |||||
discrete_domains: 1, // default: 0 | |||||
discreteDomains: 1 // default: 0 | |||||
start: start_date, | |||||
start: startDate, | |||||
// A Date object; | // A Date object; | ||||
// default: today's date in past year | // default: today's date in past year | ||||
// for an annual heatmap | // for an annual heatmap | ||||
legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||||
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'], | |||||
// Set of five incremental colors, | // Set of five incremental colors, | ||||
// beginning with a low-saturation color for zero data; | // beginning with a low-saturation color for zero data; | ||||
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | // default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'] | ||||
@@ -267,13 +213,97 @@ | |||||
</div> | </div> | ||||
</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="col-sm-10 push-sm-1"> | ||||
<div class="dashboard-section"> | <div class="dashboard-section"> | ||||
<!-- Closing --> | <!-- Closing --> | ||||
<div class="text-center" style="margin-top: 70px"> | <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> | <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: 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> | <p>License: MIT</p> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -285,7 +315,7 @@ | |||||
</div> | </div> | ||||
<div class="built-with-frappe text-center" style="margin-top: -20px"> | <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"> | <p style="margin: 24px 0 0px 0; font-size: 15px"> | ||||
Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>. | Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>. | ||||
Used in <a href="https://erpnext.com" target="_blank">ERPNext</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": "^2.0.1", | ||||
"rollup-plugin-uglify-es": "0.0.1", | "rollup-plugin-uglify-es": "0.0.1", | ||||
"rollup-watch": "^4.3.1" | "rollup-watch": "^4.3.1" | ||||
}, | |||||
"dependencies": { | |||||
"eslint": "^4.18.2" | |||||
} | } | ||||
} | } |
@@ -15,17 +15,19 @@ import pkg from './package.json'; | |||||
export default [ | export default [ | ||||
{ | { | ||||
input: 'src/js/charts.js', | |||||
input: 'src/js/chart.js', | |||||
sourcemap: true, | |||||
output: [ | 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: [ | plugins: [ | ||||
postcss({ | postcss({ | ||||
preprocessor: (content, id) => new Promise((resolve, reject) => { | preprocessor: (content, id) => new Promise((resolve, reject) => { | ||||
@@ -33,7 +35,6 @@ export default [ | |||||
resolve({ code: result.css.toString() }) | resolve({ code: result.css.toString() }) | ||||
}), | }), | ||||
extensions: [ '.scss' ], | extensions: [ '.scss' ], | ||||
// extract: 'dist/frappe-charts.min.css', | |||||
plugins: [ | plugins: [ | ||||
nested(), | nested(), | ||||
cssnext({ warnForDuplicates: false }), | cssnext({ warnForDuplicates: false }), | ||||
@@ -56,10 +57,14 @@ export default [ | |||||
] | ] | ||||
}, | }, | ||||
{ | { | ||||
input: 'src/js/charts.js', | |||||
input: 'src/js/chart.js', | |||||
output: [ | output: [ | ||||
{ | { | ||||
file: pkg.src, | |||||
file: pkg.main, | |||||
format: 'cjs', | |||||
}, | |||||
{ | |||||
file: pkg.module, | |||||
format: 'es', | format: 'es', | ||||
} | } | ||||
], | ], | ||||
@@ -70,7 +75,6 @@ export default [ | |||||
resolve({ code: result.css.toString() }) | resolve({ code: result.css.toString() }) | ||||
}), | }), | ||||
extensions: [ '.scss' ], | extensions: [ '.scss' ], | ||||
extract: 'dist/frappe-charts.min.css', | |||||
plugins: [ | plugins: [ | ||||
nested(), | nested(), | ||||
cssnext({ warnForDuplicates: false }), | cssnext({ warnForDuplicates: false }), | ||||
@@ -82,25 +86,24 @@ export default [ | |||||
'src/scss/**', | 'src/scss/**', | ||||
] | ] | ||||
}), | }), | ||||
babel({ | |||||
exclude: 'node_modules/**', | |||||
}), | |||||
replace({ | replace({ | ||||
exclude: 'node_modules/**', | exclude: 'node_modules/**', | ||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | ||||
}) | |||||
}), | |||||
uglify() | |||||
], | ], | ||||
}, | }, | ||||
{ | { | ||||
input: 'src/js/charts.js', | |||||
input: 'src/js/chart.js', | |||||
output: [ | output: [ | ||||
{ | { | ||||
file: 'docs/assets/js/frappe-charts.min.js', | |||||
format: 'iife', | |||||
}, | |||||
{ | |||||
file: pkg.browser, | |||||
format: 'iife', | |||||
file: pkg.src, | |||||
format: 'es', | |||||
} | } | ||||
], | ], | ||||
name: 'Chart', | |||||
plugins: [ | plugins: [ | ||||
postcss({ | postcss({ | ||||
preprocessor: (content, id) => new Promise((resolve, reject) => { | preprocessor: (content, id) => new Promise((resolve, reject) => { | ||||
@@ -108,6 +111,7 @@ export default [ | |||||
resolve({ code: result.css.toString() }) | resolve({ code: result.css.toString() }) | ||||
}), | }), | ||||
extensions: [ '.scss' ], | extensions: [ '.scss' ], | ||||
extract: 'dist/frappe-charts.min.css', | |||||
plugins: [ | plugins: [ | ||||
nested(), | nested(), | ||||
cssnext({ warnForDuplicates: false }), | cssnext({ warnForDuplicates: false }), | ||||
@@ -119,14 +123,10 @@ export default [ | |||||
'src/scss/**', | 'src/scss/**', | ||||
] | ] | ||||
}), | }), | ||||
babel({ | |||||
exclude: 'node_modules/**', | |||||
}), | |||||
replace({ | replace({ | ||||
exclude: 'node_modules/**', | exclude: 'node_modules/**', | ||||
ENV: JSON.stringify(process.env.NODE_ENV || 'development'), | 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 { $, isElementInViewport, getElementContentWidth } from '../utils/dom'; | ||||
import { makeSVGContainer, makeSVGDefs, makeSVGGroup } from '../utils/draw'; | import { makeSVGContainer, makeSVGDefs, makeSVGGroup } from '../utils/draw'; | ||||
import { getStringWidth } from '../utils/helpers'; | 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 { 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 { | 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.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; | this.colors = DEFAULT_COLORS; | ||||
} else { | |||||
this.colors = args.colors; | |||||
} | } | ||||
this.colors = this.colors.map(color => getColor(color)); | 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; | 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', { | this.container = $.create('div', { | ||||
className: 'chart-container', | className: 'chart-container', | ||||
innerHTML: `<h6 class="title">${this.title}</h6> | innerHTML: `<h6 class="title">${this.title}</h6> | ||||
@@ -185,120 +114,175 @@ export default class BaseChart { | |||||
this.parent.innerHTML = ''; | this.parent.innerHTML = ''; | ||||
this.parent.appendChild(this.container); | 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) { | 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) => { | document.addEventListener('keydown', (e) => { | ||||
if(isElementInViewport(this.chart_wrapper)) { | |||||
if(isElementInViewport(this.chartWrapper)) { | |||||
e = e || window.event; | 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'; | import { isValidColor } from '../utils/colors'; | ||||
export default class Heatmap extends BaseChart { | 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.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(); | 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']; | : ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | ||||
// Fixed 5-color theme, | // Fixed 5-color theme, | ||||
// More colors are difficult to parse visually | // More colors are difficult to parse visually | ||||
this.distribution_size = 5; | this.distribution_size = 5; | ||||
this.translate_x = 0; | |||||
this.translateX = 0; | |||||
this.setup(); | this.setup(); | ||||
} | } | ||||
setMargins() { | |||||
super.setMargins(); | |||||
this.leftMargin = 10; | |||||
this.translateY = 10; | |||||
} | |||||
validate_colors(colors) { | validate_colors(colors) { | ||||
if(colors.length < 5) return 0; | if(colors.length < 5) return 0; | ||||
@@ -54,210 +52,212 @@ export default class Heatmap extends BaseChart { | |||||
return valid; | return valid; | ||||
} | } | ||||
setup_base_values() { | |||||
configure() { | |||||
super.configure(); | |||||
this.today = new Date(); | this.today = new Date(); | ||||
if(!this.start) { | if(!this.start) { | ||||
this.start = new Date(); | this.start = new Date(); | ||||
this.start.setFullYear( this.start.getFullYear() - 1 ); | this.start.setFullYear( this.start.getFullYear() - 1 ); | ||||
} | } | ||||
this.first_week_start = new Date(this.start.toDateString()); | |||||
this.last_week_start = new Date(this.today.toDateString()); | |||||
if(this.first_week_start.getDay() !== 7) { | |||||
addDays(this.first_week_start, (-1) * this.first_week_start.getDay()); | |||||
this.firstWeekStart = new Date(this.start.toDateString()); | |||||
this.lastWeekStart = new Date(this.today.toDateString()); | |||||
if(this.firstWeekStart.getDay() !== 7) { | |||||
addDays(this.firstWeekStart, (-1) * this.firstWeekStart.getDay()); | |||||
} | } | ||||
if(this.last_week_start.getDay() !== 7) { | |||||
addDays(this.last_week_start, (-1) * this.last_week_start.getDay()); | |||||
if(this.lastWeekStart.getDay() !== 7) { | |||||
addDays(this.lastWeekStart, (-1) * this.lastWeekStart.getDay()); | |||||
} | } | ||||
this.no_of_cols = getWeeksBetween(this.first_week_start + '', this.last_week_start + '') + 1; | |||||
this.no_of_cols = getWeeksBetween(this.firstWeekStart + '', this.lastWeekStart + '') + 1; | |||||
} | } | ||||
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'); | 'domain-label-group chart-label'); | ||||
this.data_groups = this.makeDrawAreaComponent( | |||||
this.dataGroups = makeSVGGroup(this.drawArea, | |||||
'data-groups', | 'data-groups', | ||||
`translate(0, 20)` | `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" | "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++) { | 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(); | this.render_month_labels(); | ||||
} | } | ||||
get_week_squares_group(current_date, index) { | |||||
const no_of_weekdays = 7; | |||||
const square_side = 10; | |||||
const cell_padding = 2; | |||||
get_week_squares_group(currentDate, index) { | |||||
const noOfWeekdays = 7; | |||||
const squareSide = 10; | |||||
const cellPadding = 2; | |||||
const step = 1; | const step = 1; | ||||
const today_time = this.today.getTime(); | |||||
const todayTime = this.today.getTime(); | |||||
let month_change = 0; | |||||
let week_col_change = 0; | |||||
let monthChange = 0; | |||||
let weekColChange = 0; | |||||
let data_group = makeSVGGroup(this.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]) { | if(this.data[timestamp]) { | ||||
data_value = this.data[timestamp]; | |||||
dataValue = this.data[timestamp]; | |||||
} | } | ||||
if(this.data[Math.round(timestamp)]) { | if(this.data[Math.round(timestamp)]) { | ||||
data_value = this.data[Math.round(timestamp)]; | |||||
dataValue = this.data[Math.round(timestamp)]; | |||||
} | } | ||||
if(data_value) { | |||||
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 = { | 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() { | render_month_labels() { | ||||
// this.first_month_label = 1; | // this.first_month_label = 1; | ||||
// if (this.first_week_start.getDate() > 8) { | |||||
// if (this.firstWeekStart.getDate() > 8) { | |||||
// this.first_month_label = 0; | // this.first_month_label = 0; | ||||
// } | // } | ||||
// this.last_month_label = 1; | // this.last_month_label = 1; | ||||
// let first_month = this.months.shift(); | // let first_month = this.months.shift(); | ||||
// let first_month_start = this.month_start_points.shift(); | |||||
// let first_month_start = this.monthStartPoints.shift(); | |||||
// render first month if | // render first month if | ||||
// let last_month = this.months.pop(); | // let last_month = this.months.pop(); | ||||
// let last_month_start = this.month_start_points.pop(); | |||||
// let last_month_start = this.monthStartPoints.pop(); | |||||
// render last month if | // render last month if | ||||
this.months.shift(); | this.months.shift(); | ||||
this.month_start_points.shift(); | |||||
this.monthStartPoints.shift(); | |||||
this.months.pop(); | this.months.pop(); | ||||
this.month_start_points.pop(); | |||||
this.monthStartPoints.pop(); | |||||
this.month_start_points.map((start, i) => { | |||||
let month_name = this.month_names[this.months[i]].substring(0, 3); | |||||
this.monthStartPoints.map((start, i) => { | |||||
let month_name = this.monthNames[this.months[i]].substring(0, 3); | |||||
let text = makeText('y-value-text', start+12, 10, month_name); | let text = makeText('y-value-text', start+12, 10, month_name); | ||||
this.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( | Array.prototype.slice.call( | ||||
document.querySelectorAll(".data-group .day") | document.querySelectorAll(".data-group .day") | ||||
).map(el => { | ).map(el => { | ||||
el.addEventListener('mouseenter', (e) => { | el.addEventListener('mouseenter', (e) => { | ||||
let count = e.target.getAttribute('data-value'); | let count = e.target.getAttribute('data-value'); | ||||
let date_parts = e.target.getAttribute('data-date').split('-'); | |||||
let dateParts = e.target.getAttribute('data-date').split('-'); | |||||
let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3); | |||||
let month = this.monthNames[parseInt(dateParts[1])-1].substring(0, 3); | |||||
let g_off = this.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 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) { | 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.type = 'percentage'; | ||||
this.max_slices = 10; | |||||
this.max_legend_points = 6; | |||||
this.setup(); | 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', | className: 'div', | ||||
inside: this.chart_wrapper | |||||
inside: this.chartWrapper | |||||
}); | }); | ||||
this.chart = $.create('div', { | this.chart = $.create('div', { | ||||
className: 'progress-chart', | className: 'progress-chart', | ||||
inside: this.chart_div | |||||
inside: this.svg | |||||
}); | }); | ||||
} | |||||
setup_components() { | |||||
this.percentage_bar = $.create('div', { | |||||
this.percentageBar = $.create('div', { | |||||
className: 'progress', | className: 'progress', | ||||
inside: this.chart | 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', { | let slice = $.create('div', { | ||||
className: `progress-bar`, | className: `progress-bar`, | ||||
inside: this.percentage_bar, | |||||
'data-index': i, | |||||
inside: this.percentageBar, | |||||
styles: { | styles: { | ||||
background: this.colors[i], | 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 { 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.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(); | 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 startAngle = curAngle; | ||||
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE; | |||||
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE; | |||||
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | const diffAngle = clockWise ? -originDiffAngle : originDiffAngle; | ||||
const endAngle = curAngle = curAngle + diffAngle; | 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; | 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; | curStart = startPosition; | ||||
curEnd = endPosition; | 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, | startPosition, | ||||
endPosition, | endPosition, | ||||
value: total, | value: total, | ||||
total: this.grand_total, | |||||
total: s.grandTotal, | |||||
startAngle, | startAngle, | ||||
endAngle, | 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){ | calTranslateByAngle(property){ | ||||
const{radius,hoverRadio} = this; | 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)`; | return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`; | ||||
} | } | ||||
hoverSlice(path,i,flag,e){ | hoverSlice(path,i,flag,e){ | ||||
if(!path) return; | if(!path) return; | ||||
const color = this.colors[i]; | 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 x = e.pageX - g_off.left + 10; | ||||
let y = e.pageY - g_off.top - 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)'); | transform(path,'translate3d(0,0,0)'); | ||||
this.tip.hide_tip(); | |||||
this.tip.hideTip(); | |||||
path.style.fill = color; | path.style.fill = color; | ||||
} | } | ||||
} | } | ||||
bindTooltip() { | |||||
this.chartWrapper.addEventListener('mousemove', this.mouseMove); | |||||
this.chartWrapper.addEventListener('mouseleave', this.mouseLeave); | |||||
} | |||||
mouseMove(e){ | mouseMove(e){ | ||||
const target = e.target; | const target = e.target; | ||||
let slices = this.components.get('pieSlices').store; | |||||
let prevIndex = this.curActiveSliceIndex; | let prevIndex = this.curActiveSliceIndex; | ||||
let prevAcitve = this.curActiveSlice; | 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(){ | mouseLeave(){ | ||||
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false); | 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.parent = parent; | ||||
this.colors = colors; | 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.x = 0; | ||||
this.y = 0; | this.y = 0; | ||||
@@ -22,16 +22,16 @@ export default class SvgTip { | |||||
} | } | ||||
setup() { | setup() { | ||||
this.make_tooltip(); | |||||
this.makeTooltip(); | |||||
} | } | ||||
refresh() { | refresh() { | ||||
this.fill(); | this.fill(); | ||||
this.calc_position(); | |||||
// this.show_tip(); | |||||
this.calcPosition(); | |||||
// this.showTip(); | |||||
} | } | ||||
make_tooltip() { | |||||
makeTooltip() { | |||||
this.container = $.create('div', { | this.container = $.create('div', { | ||||
inside: this.parent, | inside: this.parent, | ||||
className: 'graph-svg-tip comparison', | className: 'graph-svg-tip comparison', | ||||
@@ -39,27 +39,30 @@ export default class SvgTip { | |||||
<ul class="data-point-list"></ul> | <ul class="data-point-list"></ul> | ||||
<div class="svg-pointer"></div>` | <div class="svg-pointer"></div>` | ||||
}); | }); | ||||
this.hide_tip(); | |||||
this.hideTip(); | |||||
this.title = this.container.querySelector('.title'); | 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.parent.addEventListener('mouseleave', () => { | ||||
this.hide_tip(); | |||||
this.hideTip(); | |||||
}); | }); | ||||
} | } | ||||
fill() { | fill() { | ||||
let title; | 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 { | } else { | ||||
title = `${this.title_name}<strong>${this.title_value}</strong>`; | |||||
title = `${this.titleName}<strong>${this.titleValue}</strong>`; | |||||
} | } | ||||
this.title.innerHTML = title; | 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'; | const color = this.colors[i] || 'black'; | ||||
let li = $.create('li', { | let li = $.create('li', { | ||||
@@ -70,50 +73,51 @@ export default class SvgTip { | |||||
${set.title ? set.title : '' }` | ${set.title ? set.title : '' }` | ||||
}); | }); | ||||
this.data_point_list.appendChild(li); | |||||
this.dataPointList.appendChild(li); | |||||
}); | }); | ||||
} | } | ||||
calc_position() { | |||||
calcPosition() { | |||||
let width = this.container.offsetWidth; | let width = this.container.offsetWidth; | ||||
this.top = this.y - this.container.offsetHeight; | this.top = this.y - this.container.offsetHeight; | ||||
this.left = this.x - width/2; | 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'); | let pointer = this.container.querySelector('.svg-pointer'); | ||||
if(this.left < 0) { | if(this.left < 0) { | ||||
pointer.style.left = `calc(50% - ${-1 * this.left}px)`; | pointer.style.left = `calc(50% - ${-1 * this.left}px)`; | ||||
this.left = 0; | 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 { | } else { | ||||
pointer.style.left = `50%`; | 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.x = x; | ||||
this.y = y; | this.y = y; | ||||
this.title_value_first = title_value_first; | |||||
this.titleValueFirst = title.valueFirst || 0; | |||||
this.index = index; | |||||
this.refresh(); | this.refresh(); | ||||
} | } | ||||
hide_tip() { | |||||
hideTip() { | |||||
this.container.style.top = '0px'; | this.container.style.top = '0px'; | ||||
this.container.style.left = '0px'; | this.container.style.left = '0px'; | ||||
this.container.style.opacity = '0'; | this.container.style.opacity = '0'; | ||||
} | } | ||||
show_tip() { | |||||
showTip() { | |||||
this.container.style.top = this.top + 'px'; | this.container.style.top = this.top + 'px'; | ||||
this.container.style.left = this.left + 'px'; | this.container.style.left = this.left + 'px'; | ||||
this.container.style.opacity = '1'; | this.container.style.opacity = '1'; | ||||
@@ -1,58 +1,105 @@ | |||||
import { getBarHeightAndYAttr } from './draw-utils'; | 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 | // Leveraging SMIL Animations | ||||
import { REPLACE_ALL_NEW_DUR } from './animate'; | |||||
const EASING = { | const EASING = { | ||||
ease: "0.25 0.1 0.25 1", | ease: "0.25 0.1 0.25 1", | ||||
linear: "0 0 1 1", | linear: "0 0 1 1", | ||||
@@ -9,7 +11,7 @@ const EASING = { | |||||
easeinout: "0.42 0 0.58 1" | 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 animElement = element.cloneNode(true); | ||||
let newElement = 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; | element.style.oTransform = style; | ||||
} | } | ||||
export function runSVGAnimation(svgContainer, elements) { | |||||
function animateSVG(svgContainer, elements) { | |||||
let newElements = []; | let newElements = []; | ||||
let animElements = []; | let animElements = []; | ||||
elements.map(element => { | elements.map(element => { | ||||
let obj = element[0]; | |||||
let parent = obj.unit.parentNode; | |||||
let unit = element[0]; | |||||
let parent = unit.parentNode; | |||||
let animElement, newElement; | let animElement, newElement; | ||||
element[0] = obj.unit; | |||||
[animElement, newElement] = animateSVG(...element); | |||||
element[0] = unit; | |||||
[animElement, newElement] = animateSVGElement(...element); | |||||
newElements.push(newElement); | newElements.push(newElement); | ||||
animElements.push([animElement, parent]); | 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); | let animSvg = svgContainer.cloneNode(true); | ||||
@@ -99,3 +95,22 @@ export function runSVGAnimation(svgContainer, elements) { | |||||
return animSvg; | 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', | 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){ | function limitColor(r){ | ||||
if (r > 255) return 255; | 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); | 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; | return element; | ||||
}; | }; | ||||
export function offset(element) { | |||||
export function getOffset(element) { | |||||
let rect = element.getBoundingClientRect(); | let rect = element.getBoundingClientRect(); | ||||
return { | return { | ||||
// https://stackoverflow.com/a/7436602/6495043 | // https://stackoverflow.com/a/7436602/6495043 | ||||
@@ -74,7 +74,7 @@ export function getElementContentWidth(element) { | |||||
return element.clientWidth - padding; | return element.clientWidth - padding; | ||||
} | } | ||||
$.bind = (element, o) => { | |||||
export function bind(element, o){ | |||||
if (element) { | if (element) { | ||||
for (var event in o) { | for (var event in o) { | ||||
var callback = o[event]; | var callback = o[event]; | ||||
@@ -84,9 +84,9 @@ $.bind = (element, o) => { | |||||
}); | }); | ||||
} | } | ||||
} | } | ||||
}; | |||||
} | |||||
$.unbind = (element, o) => { | |||||
export function unbind(element, o){ | |||||
if (element) { | if (element) { | ||||
for (var event in o) { | for (var event in o) { | ||||
var callback = o[event]; | 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"); | var evt = document.createEvent("HTMLEvents"); | ||||
evt.initEvent(type, true, true ); | evt.initEvent(type, true, true ); | ||||
@@ -108,4 +108,4 @@ $.fire = (target, type, properties) => { | |||||
} | } | ||||
return target.dispatchEvent(evt); | 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; | let height, y; | ||||
if (yTop <= zeroLine) { | if (yTop <= zeroLine) { | ||||
height = zeroLine - yTop; | height = zeroLine - yTop; | ||||
y = yTop; | y = yTop; | ||||
// In case of invisible bars | |||||
if(height === 0) { | |||||
height = totalHeight * 0.01; | |||||
y -= height; | |||||
} | |||||
} else { | } else { | ||||
height = yTop - zeroLine; | height = yTop - zeroLine; | ||||
y = zeroLine; | y = zeroLine; | ||||
// In case of invisible bars | |||||
if(height === 0) { | |||||
height = totalHeight * 0.01; | |||||
} | |||||
} | } | ||||
return [height, y]; | 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 { 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) { | function $(expr, con) { | ||||
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null; | 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); | var element = document.createElementNS("http://www.w3.org/2000/svg", tag); | ||||
for (var i in o) { | 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') { | export function makePath(pathStr, className='', stroke='none', fill='none') { | ||||
return createSVG('path', { | return createSVG('path', { | ||||
className: className, | 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) { | 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 gradientDef = renderVerticalGradient(svgDefElem, gradientId); | ||||
let opacities = [1, 0.6, 0.2]; | let opacities = [1, 0.6, 0.2]; | ||||
if(lighter) { | if(lighter) { | ||||
@@ -130,109 +155,385 @@ export function makeText(className, x, y, content) { | |||||
className: className, | className: className, | ||||
x: x, | x: x, | ||||
y: y, | y: y, | ||||
dy: '.32em', | |||||
dy: (FONT_SIZE / 2) + 'px', | |||||
'font-size': FONT_SIZE + 'px', | |||||
innerHTML: content | 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, | x1: 0, | ||||
x2: 0, | x2: 0, | ||||
y1: 0, | |||||
y2: height | |||||
y1: y1, | |||||
y2: y2, | |||||
styles: { | |||||
stroke: options.stroke | |||||
} | |||||
}); | }); | ||||
let text = createSVG('text', { | let text = createSVG('text', { | ||||
className: labelClass, | |||||
x: 0, | 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, | y1: 0, | ||||
y2: 0 | |||||
y2: 0, | |||||
styles: { | |||||
stroke: options.stroke | |||||
} | |||||
}); | }); | ||||
let text = createSVG('text', { | let text = createSVG('text', { | ||||
className: labelClass, | |||||
x: textEndAt, | |||||
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN, | |||||
y: 0, | 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 | 'stroke-opacity': 1 | ||||
}); | }); | ||||
if(darker) { | |||||
if(text === 0 || text === '0') { | |||||
line.style.stroke = "rgba(27, 31, 35, 0.6)"; | 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. | * Returns the value of a number upto 2 decimal places. | ||||
* @param {Number} d Any number | * @param {Number} d Any number | ||||
@@ -37,6 +39,22 @@ export function shuffle(array) { | |||||
return 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. | * Returns pixel width of string. | ||||
* @param {String} string | * @param {String} string | ||||
@@ -45,3 +63,23 @@ export function shuffle(array) { | |||||
export function getStringWidth(string, charWidth) { | export function getStringWidth(string, charWidth) { | ||||
return (string+"").length * 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) { | function normalize(x) { | ||||
// Calculates mantissa and exponent of a number | // Calculates mantissa and exponent of a number | ||||
// Returns normalized number and exponent | // Returns normalized number and exponent | ||||
@@ -21,7 +23,7 @@ function normalize(x) { | |||||
return [sig * man, exp]; | return [sig * man, exp]; | ||||
} | } | ||||
function getRangeIntervals(max, min=0) { | |||||
function getChartRangeIntervals(max, min=0) { | |||||
let upperBound = Math.ceil(max); | let upperBound = Math.ceil(max); | ||||
let lowerBound = Math.floor(min); | let lowerBound = Math.floor(min); | ||||
let range = upperBound - lowerBound; | let range = upperBound - lowerBound; | ||||
@@ -59,19 +61,19 @@ function getRangeIntervals(max, min=0) { | |||||
return intervals; | return intervals; | ||||
} | } | ||||
function getIntervals(maxValue, minValue=0) { | |||||
function getChartIntervals(maxValue, minValue=0) { | |||||
let [normalMaxValue, exponent] = normalize(maxValue); | let [normalMaxValue, exponent] = normalize(maxValue); | ||||
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0; | let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0; | ||||
// Allow only 7 significant digits | // Allow only 7 significant digits | ||||
normalMaxValue = normalMaxValue.toFixed(6); | normalMaxValue = normalMaxValue.toFixed(6); | ||||
let intervals = getRangeIntervals(normalMaxValue, normalMinValue); | |||||
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue); | |||||
intervals = intervals.map(value => value * Math.pow(10, exponent)); | intervals = intervals.map(value => value * Math.pow(10, exponent)); | ||||
return intervals; | return intervals; | ||||
} | } | ||||
export function calcIntervals(values, withMinimum=false) { | |||||
export function calcChartIntervals(values, withMinimum=false) { | |||||
//*** Where the magic happens *** | //*** Where the magic happens *** | ||||
// Calculates best-fit y intervals from given values | // 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 | let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars | ||||
function getPositiveFirstIntervals(maxValue, absMinValue) { | function getPositiveFirstIntervals(maxValue, absMinValue) { | ||||
let intervals = getIntervals(maxValue); | |||||
let intervals = getChartIntervals(maxValue); | |||||
let intervalSize = intervals[1] - intervals[0]; | let intervalSize = intervals[1] - intervals[0]; | ||||
@@ -102,9 +104,9 @@ export function calcIntervals(values, withMinimum=false) { | |||||
if(maxValue >= 0 && minValue >= 0) { | if(maxValue >= 0 && minValue >= 0) { | ||||
exponent = normalize(maxValue)[1]; | exponent = normalize(maxValue)[1]; | ||||
if(!withMinimum) { | if(!withMinimum) { | ||||
intervals = getIntervals(maxValue); | |||||
intervals = getChartIntervals(maxValue); | |||||
} else { | } else { | ||||
intervals = getIntervals(maxValue, minValue); | |||||
intervals = getChartIntervals(maxValue, minValue); | |||||
} | } | ||||
} | } | ||||
@@ -142,9 +144,9 @@ export function calcIntervals(values, withMinimum=false) { | |||||
exponent = normalize(pseudoMaxValue)[1]; | exponent = normalize(pseudoMaxValue)[1]; | ||||
if(!withMinimum) { | if(!withMinimum) { | ||||
intervals = getIntervals(pseudoMaxValue); | |||||
intervals = getChartIntervals(pseudoMaxValue); | |||||
} else { | } else { | ||||
intervals = getIntervals(pseudoMaxValue, pseudoMinValue); | |||||
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue); | |||||
} | } | ||||
intervals = intervals.reverse().map(d => d * (-1)); | intervals = intervals.reverse().map(d => d * (-1)); | ||||
@@ -153,6 +155,51 @@ export function calcIntervals(values, withMinimum=false) { | |||||
return intervals; | 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) { | export function calcDistribution(values, distributionSize) { | ||||
// Assume non-negative values, | // Assume non-negative values, | ||||
// implying distribution minimum at zero | // implying distribution minimum at zero | ||||
@@ -28,8 +28,13 @@ | |||||
} | } | ||||
.graph-stats-container { | .graph-stats-container { | ||||
display: flex; | display: flex; | ||||
justify-content: space-around; | |||||
padding-top: 10px; | |||||
justify-content: space-between; | |||||
padding: 10px; | |||||
&:before, | |||||
&:after { | |||||
content: ''; | |||||
display: block; | |||||
} | |||||
.stats { | .stats { | ||||
padding-bottom: 15px; | padding-bottom: 15px; | ||||
} | } | ||||
@@ -51,8 +56,8 @@ | |||||
} | } | ||||
} | } | ||||
.axis, .chart-label { | .axis, .chart-label { | ||||
font-size: 11px; | |||||
fill: #555b51; | fill: #555b51; | ||||
// temp commented | |||||
line { | line { | ||||
stroke: #dadada; | stroke: #dadada; | ||||
} | } | ||||
@@ -62,11 +67,26 @@ | |||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
} | } | ||||
.data-points { | |||||
.dataset-units { | |||||
circle { | circle { | ||||
stroke: #fff; | stroke: #fff; | ||||
stroke-width: 2; | 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-group { | ||||
path { | path { | ||||
@@ -78,17 +98,18 @@ | |||||
line.dashed { | line.dashed { | ||||
stroke-dasharray: 5,3; | 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 { | .specific-value { | ||||
text-anchor: start; | text-anchor: start; | ||||
} | } | ||||
.y-value-text { | |||||
.y-line { | |||||
text-anchor: end; | text-anchor: end; | ||||
} | } | ||||
.x-value-text { | |||||
.x-line { | |||||
text-anchor: middle; | text-anchor: middle; | ||||
} | } | ||||
} | } | ||||