瀏覽代碼

Merge pull request #129 from frappe/develop

Develop
tags/1.2.0
Prateeksha Singh 7 年之前
committed by GitHub
父節點
當前提交
6a3fab09d4
沒有發現已知的金鑰在資料庫的簽署中 GPG Key ID: 4AEE18F83AFDEB23
共有 43 個文件被更改,包括 6347 次插入4276 次删除
  1. +2
    -2
      README.md
  2. +2543
    -2142
      dist/frappe-charts.esm.js
  3. +1
    -1
      dist/frappe-charts.min.cjs.js
  4. +1
    -1
      dist/frappe-charts.min.css
  5. +1
    -1
      dist/frappe-charts.min.esm.js
  6. +2
    -1
      dist/frappe-charts.min.iife.js
  7. +1
    -0
      dist/frappe-charts.min.iife.js.map
  8. 二進制
      docs/assets/img/frappe-bird.png
  9. +2
    -1
      docs/assets/js/frappe-charts.min.js
  10. +1
    -0
      docs/assets/js/frappe-charts.min.js.map
  11. +298
    -222
      docs/assets/js/index.js
  12. +559
    -0
      docs/assets/js/old_index.js
  13. +150
    -120
      docs/index.html
  14. +312
    -0
      docs/old_index.html
  15. +3
    -0
      package.json
  16. +23
    -23
      rollup.config.js
  17. +40
    -0
      src/js/chart.js
  18. +0
    -39
      src/js/charts.js
  19. +72
    -0
      src/js/charts/AggregationChart.js
  20. +433
    -712
      src/js/charts/AxisChart.js
  21. +0
    -81
      src/js/charts/BarChart.js
  22. +204
    -220
      src/js/charts/BaseChart.js
  23. +127
    -127
      src/js/charts/Heatmap.js
  24. +0
    -91
      src/js/charts/LineChart.js
  25. +173
    -0
      src/js/charts/MultiAxisChart.js
  26. +39
    -92
      src/js/charts/PercentageChart.js
  27. +109
    -154
      src/js/charts/PieChart.js
  28. +0
    -35
      src/js/charts/ScatterChart.js
  29. +46
    -0
      src/js/config.js
  30. +366
    -0
      src/js/objects/ChartComponents.js
  31. +35
    -31
      src/js/objects/SvgTip.js
  32. +103
    -56
      src/js/utils/animate.js
  33. +28
    -13
      src/js/utils/animation.js
  34. +124
    -0
      src/js/utils/axis-chart-utils.js
  35. +1
    -1
      src/js/utils/colors.js
  36. +23
    -0
      src/js/utils/constants.js
  37. +6
    -1
      src/js/utils/date-utils.js
  38. +7
    -7
      src/js/utils/dom.js
  39. +15
    -12
      src/js/utils/draw-utils.js
  40. +372
    -71
      src/js/utils/draw.js
  41. +38
    -0
      src/js/utils/helpers.js
  42. +56
    -9
      src/js/utils/intervals.js
  43. +31
    -10
      src/scss/charts.scss

+ 2
- 2
README.md 查看文件

@@ -1,6 +1,6 @@
<div align="center">
<img src="https://github.com/frappe/design/blob/master/logos/frappe-charts-symbol.svg" height="128">
<h2>Frappé Charts</h2>
<img src="https://github.com/frappe/design/blob/master/logos/charts-logo.svg" height="128">
<h2>Frappe Charts</h2>
<p align="center">
<p>GitHub-inspired modern, intuitive and responsive charts with zero dependencies</p>
<a href="https://frappe.github.io/charts">


+ 2543
- 2142
dist/frappe-charts.esm.js
文件差異過大導致無法顯示
查看文件


+ 1
- 1
dist/frappe-charts.min.cjs.js
文件差異過大導致無法顯示
查看文件


+ 1
- 1
dist/frappe-charts.min.css 查看文件

@@ -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
- 1
dist/frappe-charts.min.esm.js
文件差異過大導致無法顯示
查看文件


+ 2
- 1
dist/frappe-charts.min.iife.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
dist/frappe-charts.min.iife.js.map
文件差異過大導致無法顯示
查看文件


二進制
docs/assets/img/frappe-bird.png 查看文件

Before After
Width: 299  |  Height: 237  |  Size: 7.5 KiB

+ 2
- 1
docs/assets/js/frappe-charts.min.js
文件差異過大導致無法顯示
查看文件


+ 1
- 0
docs/assets/js/frappe-charts.min.js.map
文件差異過大導致無法顯示
查看文件


+ 298
- 222
docs/assets/js/index.js 查看文件

@@ -1,186 +1,218 @@
// Composite Chart
// ================================================================================
let report_count_list = [17, 40, 33, 44, 126, 156,
324, 333, 478, 495, 373];
let reportCountList = [152, 222, 199, 287, 534, 709,
1179, 1256, 1632, 1856, 1850];

let bar_composite_data = {
let lineCompositeData = {
labels: ["2007", "2008", "2009", "2010", "2011", "2012",
"2013", "2014", "2015", "2016", "2017"],

datasets: [{
"title": "Events",
"values": report_count_list,
// "formatted": report_count_list.map(d => d + " reports")
}]
};
yMarkers: [
{
label: "Average 100 reports/month",
value: 1200,
}
],

let line_composite_data = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [{
"values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0],
// "values": [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40],
// "values": [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40],
"name": "Events",
"values": reportCountList
}]
};

let more_line_data = {
// 0: {values: [4, 0, 3, 1, 1, 2, 1, 2, 1, 0, 1, 1]},
0: {values: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]},
1: {values: [2, 3, 3, 2, 1, 4, 0, 1, 2, 7, 11, 4]},
2: {values: [7, 7, 2, 4, 0, 1, 5, 3, 1, 2, 0, 1]},
3: {values: [0, 2, 6, 2, 2, 1, 2, 3, 6, 3, 7, 10]},
4: {values: [9, 10, 8, 10, 6, 5, 8, 8, 24, 15, 10, 13]},
5: {values: [9, 13, 16, 9, 4, 5, 7, 10, 14, 22, 23, 24]},
6: {values: [20, 22, 28, 19, 28, 19, 14, 19, 51, 37, 29, 38]},
7: {values: [29, 20, 22, 16, 16, 19, 24, 26, 57, 31, 46, 27]},
8: {values: [36, 24, 38, 27, 15, 22, 24, 38, 32, 57, 139, 26]},
9: {values: [37, 36, 32, 33, 12, 34, 52, 45, 58, 57, 64, 35]},
10: {values: [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 0, 0]}
// 10: {values: [36, 46, 45, 32, 27, 31, 30, 36, 39, 49, 40, 40]}
// 10: {values: [-36, -46, -45, -32, -27, -31, -30, -36, -39, -49, -40, -40]}

let fireball_5_25 = [
[4, 0, 3, 1, 1, 2, 1, 1, 1, 0, 1, 1],
[2, 3, 3, 2, 1, 3, 0, 1, 2, 7, 10, 4],
[5, 6, 2, 4, 0, 1, 4, 3, 0, 2, 0, 1],
[0, 2, 6, 2, 1, 1, 2, 3, 6, 3, 7, 8],
[6, 8, 7, 7, 4, 5, 6, 5, 22, 12, 10, 11],
[7, 10, 11, 7, 3, 2, 7, 7, 11, 15, 22, 20],
[13, 16, 21, 18, 19, 17, 12, 17, 31, 28, 25, 29],
[24, 14, 21, 14, 11, 15, 19, 21, 41, 22, 32, 18],
[31, 20, 30, 22, 14, 17, 21, 35, 27, 50, 117, 24],
[32, 24, 21, 27, 11, 27, 43, 37, 44, 40, 48, 32],
[31, 38, 36, 26, 23, 23, 25, 29, 26, 47, 61, 50],
];
let fireball_2_5 = [
[22, 6, 6, 9, 7, 8, 6, 14, 19, 10, 8, 20],
[11, 13, 12, 8, 9, 11, 9, 13, 10, 22, 40, 24],
[20, 13, 13, 19, 13, 10, 14, 13, 20, 18, 5, 9],
[7, 13, 16, 19, 12, 11, 21, 27, 27, 24, 33, 33],
[38, 25, 28, 22, 31, 21, 35, 42, 37, 32, 46, 53],
[50, 33, 36, 34, 35, 28, 27, 52, 58, 59, 75, 69],
[54, 67, 67, 45, 66, 51, 38, 64, 90, 113, 116, 87],
[84, 52, 56, 51, 55, 46, 50, 87, 114, 83, 152, 93],
[73, 58, 59, 63, 56, 51, 83, 140, 103, 115, 265, 89],
[106, 95, 94, 71, 77, 75, 99, 136, 129, 154, 168, 156],
[81, 102, 95, 72, 58, 91, 89, 122, 124, 135, 183, 171],
];
let fireballOver25 = [
// [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
[1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[3, 2, 1, 3, 2, 0, 2, 2, 2, 3, 0, 1],
[2, 3, 5, 2, 1, 3, 0, 2, 3, 5, 1, 4],
[7, 4, 6, 1, 9, 2, 2, 2, 20, 9, 4, 9],
[5, 6, 1, 2, 5, 4, 5, 5, 16, 9, 14, 9],
[5, 4, 7, 5, 1, 5, 3, 3, 5, 7, 22, 2],
[5, 13, 11, 6, 1, 7, 9, 8, 14, 17, 16, 3],
[8, 9, 8, 6, 4, 8, 5, 6, 14, 11, 21, 12]
];

let monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];

let barCompositeData = {
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
datasets: [
{
name: "Over 25 reports",
values: fireballOver25[9],
},
{
name: "5 to 25 reports",
values: fireball_5_25[9],
},
{
name: "2 to 5 reports",
values: fireball_2_5[9]
}
]
};

let c1 = document.querySelector("#chart-composite-1");
let c2 = document.querySelector("#chart-composite-2");

let bar_composite_chart = new Chart ({
parent: c1,
title: "Fireball/Bolide Events - Yearly (more than 5 reports)",
data: bar_composite_data,
type: 'bar',
height: 180,
colors: ['orange'],
is_navigable: 1,
is_series: 1
// region_fill: 1
});

let line_composite_chart = new Chart ({
parent: c2,
data: line_composite_data,
let lineCompositeChart = new Chart (c1, {
title: "Fireball/Bolide Events - Yearly (reported)",
data: lineCompositeData,
type: 'line',
height: 180,
height: 190,
colors: ['green'],
is_series: 1
isNavigable: 1,
valuesOverPoints: 1,

lineOptions: {
dotSize: 8
},
// yAxisMode: 'tick'
// regionFill: 1
});

let barCompositeChart = new Chart (c2, {
data: barCompositeData,
type: 'bar',
height: 190,
colors: ['violet', 'light-blue', '#46a9f9'],
valuesOverPoints: 1,
axisOptions: {
xAxisMode: 'tick'
},
barOptions: {
stacked: 1
},

});

bar_composite_chart.parent.addEventListener('data-select', (e) => {
line_composite_chart.update_values([more_line_data[e.index]]);
lineCompositeChart.parent.addEventListener('data-select', (e) => {
let i = e.index;
barCompositeChart.updateDatasets([
fireballOver25[i], fireball_5_25[i], fireball_2_5[i]
]);
});


// Demo Chart (bar, linepts, scatter(blobs), percentage)
// ================================================================================
let type_data = {
let typeData = {
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm",
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"],

yMarkers: [
{
label: "Marker",
value: 43,
// type: 'dashed'
}
],

yRegions: [
{
label: "Region",
start: -10,
end: 50
},
],

datasets: [
{
title: "Some Data",
values: [25, 40, 30, 35, 8, 52, 17, -4]
name: "Some Data",
values: [18, 40, 30, 35, 8, 52, 17, -4],
axisPosition: 'right',
chartType: 'bar'
},
{
title: "Another Set",
values: [25, 50, -10, 15, 18, 32, 27, 14]
name: "Another Set",
values: [30, 50, -10, 15, 18, 32, 27, 14],
axisPosition: 'right',
chartType: 'bar'
},
{
title: "Yet Another",
values: [15, 20, -3, -15, 58, 12, -17, 37]
name: "Yet Another",
values: [15, 20, -3, -15, 58, 12, -17, 37],
chartType: 'line'
}
]
};

let type_chart = new Chart({
parent: "#chart-types",
title: "My Awesome Chart",
data: type_data,
type: 'bar',
height: 250,
colors: ['light-blue', 'violet', 'blue'],
is_series: 1,
format_tooltip_x: d => (d + '').toUpperCase(),
format_tooltip_y: d => d + ' pts'
});
// let typeChart = new Chart("#chart-types", {
// title: "My Awesome Chart",
// data: typeData,
// type: 'bar',
// height: 250,
// colors: ['purple', 'magenta', 'red'],
// tooltipOptions: {
// formatTooltipX: d => (d + '').toUpperCase(),
// formatTooltipY: d => d + ' pts',
// }
// });

Array.prototype.slice.call(
document.querySelectorAll('.chart-type-buttons button')
).map(el => {
el.addEventListener('click', (e) => {
let btn = e.target;
let type = btn.getAttribute('data-type');

let newChart = type_chart.get_different_chart(type);
if(newChart){
type_chart = newChart;
}
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
});
btn.classList.add('active');
});
});

// Trends Chart
// ================================================================================
let trends_data = {
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976,
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986,
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996,
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] ,
datasets: [
{
"values": [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4,
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8,
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6,
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7,
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8]
}
]
};

let plot_chart_args = {
parent: "#chart-trends",
title: "Mean Total Sunspot Count - Yearly",
data: trends_data,
type: 'line',
// Aggregation chart
// ================================================================================
let args = {
data: typeData,
type: 'axis-mixed',
height: 250,
colors: ['blue'],
is_series: 1,
show_dots: 0,
heatline: 1,
x_axis_mode: 'tick',
y_axis_mode: 'span'
};
colors: ['purple', 'magenta', 'light-blue'],

new Chart(plot_chart_args);
maxLegendPoints: 6,
maxSlices: 10,

tooltipOptions: {
formatTooltipX: d => (d + '').toUpperCase(),
formatTooltipY: d => d + ' pts',
}
}
let aggrChart = new Chart("#chart-aggr", args);

Array.prototype.slice.call(
document.querySelectorAll('.chart-plot-buttons button')
document.querySelectorAll('.aggr-type-buttons button')
).map(el => {
el.addEventListener('click', (e) => {
let btn = e.target;
let type = btn.getAttribute('data-type');
let config = [];

if(type === 'line') {
config = [0, 0, 0];
} else if(type === 'region') {
config = [0, 0, 1];
} else {
config = [0, 1, 0];
}

plot_chart_args.show_dots = config[0];
plot_chart_args.heatline = config[1];
plot_chart_args.region_fill = config[2];

plot_chart_args.init = false;

new Chart(plot_chart_args);
args.type = type;

let newChart = new Chart("#chart-aggr", args);;
if(newChart){
aggrChart = newChart;
}
Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
@@ -194,7 +226,9 @@ Array.prototype.slice.call(
let update_data_all_labels = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue",
"Wed", "Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
"Sat", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"];
let update_data_all_values = Array.from({length: 30}, () => Math.floor(Math.random() * 75 - 15));

let getRandom = () => Math.floor(Math.random() * 75 - 15);
let update_data_all_values = Array.from({length: 30}, getRandom);

// We're gonna be shuffling this
let update_data_all_indices = update_data_all_labels.map((d,i) => i);
@@ -209,47 +243,135 @@ let update_data = {
datasets: [{
"values": get_update_data(update_data_all_values)
}],
"specific_values": [
yMarkers: [
{
title: "Altitude",
// title: "A very long text",
line_type: "dashed",
value: 38
label: "Altitude",
value: 25,
type: 'dashed'
}
],
yRegions: [
{
label: "Range",
start: 10,
end: 45
},
]
],
};

let update_chart = new Chart({
parent: "#chart-update",
let update_chart = new Chart("#chart-update", {
data: update_data,
type: 'line',
height: 250,
colors: ['red'],
is_series: 1,
region_fill: 1
colors: ['#ff6c03'],
lineOptions: {
// hideLine: 1,
regionFill: 1
},
});

let chart_update_buttons = document.querySelector('.chart-update-buttons');

chart_update_buttons.querySelector('[data-update="random"]').addEventListener("click", (e) => {
shuffle(update_data_all_indices);
update_chart.update_values(
[{values: get_update_data(update_data_all_values)}],
update_data_all_labels.slice(0, 10)
);
let value = getRandom();
let start = getRandom();
let end = getRandom();
let data = {
labels: update_data_all_labels.slice(0, 10),
datasets: [{values: get_update_data(update_data_all_values)}],
yMarkers: [
{
label: "Altitude",
value: value,
type: 'dashed'
}
],
yRegions: [
{
label: "Range",
start: start,
end: end
},
],
}
update_chart.update(data);
});

chart_update_buttons.querySelector('[data-update="add"]').addEventListener("click", (e) => {
// NOTE: this ought to be problem, labels stay the same after update
let index = update_chart.x.length; // last index to add
let index = update_chart.state.datasetLength; // last index to add
if(index >= update_data_all_indices.length) return;
update_chart.add_data_point(
[update_data_all_values[index]], update_data_all_labels[index]
update_chart.addDataPoint(
update_data_all_labels[index], [update_data_all_values[index]]
);
});

chart_update_buttons.querySelector('[data-update="remove"]').addEventListener("click", (e) => {
update_chart.remove_data_point();
update_chart.removeDataPoint();
});

// Trends Chart
// ================================================================================
let trends_data = {
labels: [1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976,
1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986,
1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996,
1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,
2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016] ,
datasets: [
{
values: [132.9, 150.0, 149.4, 148.0, 94.4, 97.6, 54.1, 49.2, 22.5, 18.4,
39.3, 131.0, 220.1, 218.9, 198.9, 162.4, 91.0, 60.5, 20.6, 14.8,
33.9, 123.0, 211.1, 191.8, 203.3, 133.0, 76.1, 44.9, 25.1, 11.6,
28.9, 88.3, 136.3, 173.9, 170.4, 163.6, 99.3, 65.3, 45.8, 24.7,
12.6, 4.2, 4.8, 24.9, 80.8, 84.5, 94.0, 113.3, 69.8, 39.8]
}
]
};

let plotChartArgs = {
title: "Mean Total Sunspot Count - Yearly",
data: trends_data,
type: 'line',
height: 250,
colors: ['#238e38'],
lineOptions: {
hideDots: 1,
heatline: 1,
},
axisOptions: {
xAxisMode: 'tick',
yAxisMode: 'span',
xIsSeries: 1
}
};

new Chart("#chart-trends", plotChartArgs);

Array.prototype.slice.call(
document.querySelectorAll('.chart-plot-buttons button')
).map(el => {
el.addEventListener('click', (e) => {
let btn = e.target;
let type = btn.getAttribute('data-type');
let config = {};
config[type] = 1;

if(['regionFill', 'heatline'].includes(type)) {
config.hideDots = 1;
}

// plotChartArgs.init = false;
plotChartArgs.lineOptions = config;

new Chart("#chart-trends", plotChartArgs);

Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
el.classList.remove('active');
});
btn.classList.add('active');
});
});


@@ -293,14 +415,13 @@ let events_data = {
]
};

let events_chart = new Chart({
parent: "#chart-events",
let events_chart = new Chart("#chart-events", {
title: "Jupiter's Moons: Semi-major Axis (1000 km)",
data: events_data,
type: 'bar',
height: 250,
colors: ['grey'],
is_navigable: 1,
isNavigable: 1,
});

let data_div = document.querySelector('.chart-events-data');
@@ -314,68 +435,25 @@ events_chart.parent.addEventListener('data-select', (e) => {
data_div.querySelector('img').src = "./assets/img/" + name.toLowerCase() + ".jpg";
});

// Aggregation chart
// ================================================================================
let aggr_data = {
labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
datasets: [
{
"values": [25, 40, 30, 35, 8, 52, 17]
},
{
"values": [25, 50, -10, 15, 18, 32, 27]

}
]
};

let aggr_chart = new Chart({
parent: "#chart-aggr",
data: aggr_data,
type: 'bar',
height: 250,
colors: ['purple', 'orange'],
});

document.querySelector('[data-aggregation="sums"]').addEventListener("click", (e) => {
if(e.target.innerHTML === "Show Sums") {
aggr_chart.show_sums();
e.target.innerHTML = "Hide Sums";
} else {
aggr_chart.hide_sums();
e.target.innerHTML = "Show Sums";
}
});

document.querySelector('[data-aggregation="average"]').addEventListener("click", (e) => {
if(e.target.innerHTML === "Show Averages") {
aggr_chart.show_averages();
e.target.innerHTML = "Hide Averages";
} else {
aggr_chart.hide_averages();
e.target.innerHTML = "Show Averages";
}
});

// Heatmap
// ================================================================================

let heatmap_data = {};
let heatmapData = {};
let current_date = new Date();
let timestamp = current_date.getTime()/1000;
timestamp = Math.floor(timestamp - (timestamp % 86400)).toFixed(1); // convert to midnight
for (var i = 0; i< 375; i++) {
heatmap_data[parseInt(timestamp)] = Math.floor(Math.random() * 5);
heatmapData[parseInt(timestamp)] = Math.floor(Math.random() * 5);
timestamp = Math.floor(timestamp - 86400).toFixed(1);
}

new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
new Chart("#chart-heatmap", {
data: heatmapData,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
legendScale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: 1 // default 0
discreteDomains: 1,
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c']
});

Array.prototype.slice.call(
@@ -384,10 +462,10 @@ Array.prototype.slice.call(
el.addEventListener('click', (e) => {
let btn = e.target;
let mode = btn.getAttribute('data-mode');
let discrete_domains = 0;
let discreteDomains = 0;

if(mode === 'discrete') {
discrete_domains = 1;
discreteDomains = 1;
}

let colors = [];
@@ -398,14 +476,13 @@ Array.prototype.slice.call(
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
}

new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
new Chart("#chart-heatmap", {
data: heatmapData,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
legendScale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: discrete_domains,
legend_colors: colors
discreteDomains: discreteDomains,
legendColors: colors
});

Array.prototype.slice.call(
@@ -428,23 +505,22 @@ Array.prototype.slice.call(
colors = ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'];
}

let discrete_domains = 1;
let discreteDomains = 1;

let view_mode = document
.querySelector('.heatmap-mode-buttons .active')
.getAttribute('data-mode');
if(view_mode === 'continuous') {
discrete_domains = 0;
discreteDomains = 0;
}

new Chart({
parent: "#chart-heatmap",
data: heatmap_data,
new Chart("#chart-heatmap", {
data: heatmapData,
type: 'heatmap',
legend_scale: [0, 1, 2, 4, 5],
legendScale: [0, 1, 2, 4, 5],
height: 115,
discrete_domains: discrete_domains,
legend_colors: colors
discreteDomains: discreteDomains,
legendColors: colors
});

Array.prototype.slice.call(


+ 559
- 0
docs/assets/js/old_index.js 查看文件

@@ -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;
}



+ 150
- 120
docs/index.html 查看文件

@@ -18,13 +18,15 @@

<link rel="shortcut icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon">
<link rel="icon" href="https://frappe.github.io/frappe/assets/img/favicon.png" type="image/x-icon">

<script async defer src="https://buttons.github.io/buttons.js"></script>
</head>

<body>
<div class="container">
<div class="row hero" style="padding-top: 30px; padding-bottom: 0px;">
<div class="jumbotron" style="background: transparent;">
<h1>Frappé Charts</h1>
<h1>Frappe Charts</h1>
<p class="mt-2">GitHub-inspired simple and modern charts for the web</p>
<p class="mt-2">with zero dependencies.</p>
<!--<p class="mt-2">Because dumb charts are hard to come by.</p>-->
@@ -44,68 +46,64 @@

<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">
<!--Bars, Lines or <a href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts" target="_blank">Percentages</a>-->
Create a chart
</h6>
<p class="step-explain">Install</p>
<pre><code class="hljs console"> npm install frappe-charts</code></pre>
<p class="step-explain">And include it in your project</p>
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre>
<p class="step-explain">... or include it directly in your HTML</p>
<pre><code class="hljs html"> &lt;script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"&gt;&lt;/script&gt;</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"> &lt!--HTML--&gt;
&lt;div id="chart"&gt;&lt;/div&gt;</code></pre>
<pre><code class="hljs javascript"> // Javascript
let data = {
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm",
let chart = new Chart( "#chart", { // or DOM element
data: {
labels: ["12am-3am", "3am-6am", "6am-9am", "9am-12pm",
"12pm-3pm", "3pm-6pm", "6pm-9pm", "9pm-12am"],

datasets: [
{
title: "Some Data",
values: [25, 40, 30, 35, 8, 52, 17, -4]
},
{
title: "Another Set",
values: [25, 50, -10, 15, 18, 32, 27, 14]
},
{
title: "Yet Another",
values: [15, 20, -3, -15, 58, 12, -17, 37]
}
]
};

let chart = new Chart({
parent: "#chart", // or a DOM element
title: "My Awesome Chart",
data: data,
type: 'bar', // or 'line', 'scatter', 'pie', 'percentage'
height: 250,
datasets: [
{
label: "Some Data", type: 'bar',
values: [25, 40, 30, 35, 8, 52, 17, -4]
},
{
label: "Another Set", type: 'bar',
values: [25, 50, -10, 15, 18, 32, 27, 14]
},
{
label: "Yet Another", type: 'line',
values: [15, 20, -3, -15, 58, 12, -17, 37]
}
],

colors: ['#7cd6fd', 'violet', 'blue'],
// hex-codes or these preset colors;
// defaults (in order):
// ['light-blue', 'blue', 'violet', 'red',
// 'orange', 'yellow', 'green', 'light-green',
// 'purple', 'magenta', 'grey', 'dark-grey']
yMarkers: [{ label: "Marker", value: 70 }],
yRegions: [{ label: "Region", start: -10, end: 50 }]
},

format_tooltip_x: d => (d + '').toUpperCase(),
format_tooltip_y: d => d + ' pts'
title: "My Awesome Chart",
type: 'axis-mixed', // or 'bar', 'line', 'pie', 'percentage'
height: 250,
colors: ['purple', '#ffa3ef', 'red']
});</code></pre>
<div id="chart-types" class="border"></div>
<div class="btn-group chart-type-buttons margin-vertical-px mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-type='bar'>Bar Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='line'>Line Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='scatter'>Scatter Chart</button>
<!-- <div id="chart-types" class="border" style="margin-bottom: 15px"></div> -->
<!-- <div >
<div class="btn-group x-axis-buttons margin-vertical-px" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-type='span'>X span</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='tick'>X tick</button>
</div>
<div class="btn-group y-axis-buttons margin-vertical-px" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-type='span'>Y span</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='tick'>Y tick</button>
</div>
<div class="input-group input-group-sm">
<span class="input-group-addon">.00</span>
<input type="text" class="form-control" aria-label="Amount (rounded to the nearest dollar)">
</div>
</div> -->

<div id="chart-aggr" class="border"></div>
<div class="btn-group aggr-type-buttons margin-vertical-px mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-type='axis-mixed'>Mixed</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='pie'>Pie Chart</button>
<button type="button" class="btn btn-sm btn-secondary" data-type='percentage'>Percentage Chart</button>
</div>
<p class="text-muted">
<!-- <p class="text-muted">
<a target="_blank" href="http://www.storytellingwithdata.com/blog/2011/07/death-to-pie-charts">Why Percentage?</a>
</p>
</p> -->
</div>
</div>

@@ -114,42 +112,12 @@
<h6 class="margin-vertical-rem">
Update Values
</h6>
<pre><code class="hljs javascript"> // Update entire datasets
chart.update_values(
[
{values: new_dataset_1_values},
{values: new_dataset_2_values}
],
new_labels
);

// Add a new data point
chart.add_data_point(
[new_value_1, new_value_2],
new_label,
index // defaults to last index
);

// Remove a data point
chart.remove_data_point(index);</code></pre>
<div id="chart-update" class="border"></div>
<div class="chart-update-buttons mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary" data-update="random">Random Data</button>
<button type="button" class="btn btn-sm btn-secondary" data-update="add">Add Value</button>
<button type="button" class="btn btn-sm btn-secondary" data-update="remove">Remove Value</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> ...
// Include specific Y values in input data to be displayed as lines
// (before passing data to a new chart):

data.specific_values = [
{
title: "Altitude",
line_type: "dashed", // or "solid"
value: 38
}
]
...</code></pre>
</div>
</div>

@@ -158,25 +126,20 @@
<h6 class="margin-vertical-rem">
Plot Trends
</h6>
<pre><code class="hljs javascript"> ...
x_axis_mode: 'tick', // for short label ticks
// or 'span' for long spanning vertical axis lines
y_axis_mode: 'span', // for long horizontal lines, or 'tick'
is_series: 1, // to allow for skipping of X values
...</code></pre>
<div id="chart-trends" class="border"></div>
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="hideDots">Line</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="hideLine">Dots</button>
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="region">Region</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> ...
type: 'line', // Line Chart specific properties:
<!-- <pre><code class="hljs javascript margin-vertical-px"> ...
lineOptions: 'line', // Line Chart specific properties:

show_dots: 0, // Show data points on the line; default 1
hideDots: 1, // Hide data points on the line; default 0
heatline: 1, // Show a value-wise line gradient; default 0
region_fill: 1, // Fill the area under the graph; default 0
...</code></pre>
regionFill: 1, // Fill the area under the graph; default 0
...</code></pre> -->
</div>
</div>

@@ -204,8 +167,7 @@
</div>
</div>
<pre><code class="hljs javascript margin-vertical-px"> ...
type: 'bar', // Bar Chart specific properties:
is_navigable: 1, // Navigate across bars; default 0
isNavigable: 1, // Navigate across data points; default 0
...

chart.parent.addEventListener('data-select', (e) => {
@@ -214,22 +176,6 @@
</div>
</div>

<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">
Simple Aggregations
</h6>
<div id="chart-aggr" class="border"></div>
<div class="chart-aggr-buttons mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="sums">Show Sums</button>
<button type="button" class="btn btn-sm btn-secondary" data-aggregation="average">Show Averages</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> chart.show_sums(); // and `hide_sums()`

chart.show_averages(); // and `hide_averages()`</code></pre>
</div>
</div>

<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">
@@ -242,23 +188,23 @@
<button type="button" class="btn btn-sm btn-secondary" data-mode="continuous">Continuous</button>
</div>
<div class="heatmap-color-buttons btn-group mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary active" data-color="default">Default green</button>
<button type="button" class="btn btn-sm btn-secondary" data-color="halloween">GitHub's Halloween</button>
<button type="button" class="btn btn-sm btn-secondary" data-color="default">Default green</button>
<button type="button" class="btn btn-sm btn-secondary active" data-color="halloween">GitHub's Halloween</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> let heatmap = new Chart({
parent: "#heatmap",
type: 'heatmap',
height: 115,
data: heatmap_data, // object with date/timestamp-value pairs
data: heatmapData, // object with date/timestamp-value pairs

discrete_domains: 1, // default: 0
discreteDomains: 1 // default: 0

start: start_date,
start: startDate,
// A Date object;
// default: today's date in past year
// for an annual heatmap

legend_colors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'],
legendColors: ['#ebedf0', '#fdf436', '#ffc700', '#ff9100', '#06001c'],
// Set of five incremental colors,
// beginning with a low-saturation color for zero data;
// default: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']
@@ -267,13 +213,97 @@
</div>
</div>

<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">Available options:</h6>
<pre><code class="hljs javascript">
...
{
data: {
labels: [],
datasets: [],
yRegions: [],
yMarkers: []
}
title: '',
colors: [],
height: 200,

tooltipOptions: {
formatTooltipX: d => (d + '').toUpperCase(),
formatTooltipY: d => d + ' pts',
}

// Axis charts
isNavigable: 1, // default: 0
valuesOverPoints: 1, // default: 0
barOptions: {
stacked: 1 // default: 0
}

lineOptions: {
dotSize: 6, // default: 4
hideLine: 0, // default: 0
hideDots: 1, // default: 0
heatline: 1, // default: 0
regionFill: 1 // default: 0
}

axisOptions: {
yAxisMode: 'span', // Axis lines, default
xAxisMode: 'tick', // No axis lines, only short ticks
xIsSeries: 1 // Allow skipping x values for space
// default: 0
},

// Pie/Percentage charts

maxLegendPoints: 6, // default: 20
maxSlices: 10, // default: 20

// Heatmap

discreteDomains: 1, // default: 1
start: startDate, // Date object
legendColors: []
}
...

// Updating values
chart.update(data);

// Axis charts:
chart.addDataPoint(label, valueFromEachDataset, index)
chart.removeDataPoint(index)
chart.updateDataset(datasetValues, index)

</code></pre>
</div>
</div>

<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<h6 class="margin-vertical-rem">Install</h6>
<p class="step-explain">Install via npm</p>
<pre><code class="hljs console"> npm install frappe-charts</code></pre>
<p class="step-explain">And include it in your project</p>
<pre><code class="hljs javascript"> import Chart from "frappe-charts/dist/frappe-charts.min.esm"</code></pre>
<p class="step-explain">... or include it directly in your HTML</p>
<pre><code class="hljs html"> &lt;script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"&gt;&lt;/script&gt;</code></pre>

</div>
</div>


<div class="col-sm-10 push-sm-1">
<div class="dashboard-section">
<!-- Closing -->
<div class="text-center" style="margin-top: 70px">
<a href="https://github.com/frappe/charts/archive/master.zip"><button class="large blue button">Download</button></a>
<p style="margin-top: 3rem;margin-bottom: 1.5rem;"><a href="https://github.com/frappe/charts" target="_blank">View on GitHub</a></p>
<p style="margin-top: 1rem;"><iframe src="https://ghbtns.com/github-btn.html?user=frappe&repo=charts&type=star&count=true" frameborder="0" scrolling="0" width="94px" height="20px"></iframe></p>
<p style="margin-top: 1rem;">
<a class="github-button" href="https://github.com/frappe/charts" data-icon="octicon-star" data-show-count="true" aria-label="Star frappe/charts on GitHub">Star</a>
</p>
<p>License: MIT</p>
</div>
</div>
@@ -285,7 +315,7 @@
</div>

<div class="built-with-frappe text-center" style="margin-top: -20px">
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="https://frappe.github.io/frappe/assets/img/frappe-bird-grey.svg">
<img style="padding: 5px; width: 40px; background: #fff" class="frappe-bird" src="./assets/img/frappe-bird.png">
<p style="margin: 24px 0 0px 0; font-size: 15px">
Project maintained by <a href="https://frappe.io" target="_blank">Frappe</a>.
Used in <a href="https://erpnext.com" target="_blank">ERPNext</a>.


+ 312
- 0
docs/old_index.html 查看文件

@@ -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"> &lt;script src="https://unpkg.com/frappe-charts@0.0.8/dist/frappe-charts.min.iife.js"&gt;&lt;/script&gt;</code></pre>
<p class="step-explain">Make a new Chart</p>
<pre><code class="hljs html"> &lt!--HTML--&gt;
&lt;div id="chart"&gt;&lt;/div&gt;</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>

+ 3
- 0
package.json 查看文件

@@ -52,5 +52,8 @@
"rollup-plugin-uglify": "^2.0.1",
"rollup-plugin-uglify-es": "0.0.1",
"rollup-watch": "^4.3.1"
},
"dependencies": {
"eslint": "^4.18.2"
}
}

+ 23
- 23
rollup.config.js 查看文件

@@ -15,17 +15,19 @@ import pkg from './package.json';

export default [
{
input: 'src/js/charts.js',
input: 'src/js/chart.js',
sourcemap: true,
output: [
{
file: pkg.main,
format: 'cjs',
file: 'docs/assets/js/frappe-charts.min.js',
format: 'iife',
},
{
file: pkg.module,
format: 'es',
file: pkg.browser,
format: 'iife',
}
],
name: 'Chart',
plugins: [
postcss({
preprocessor: (content, id) => new Promise((resolve, reject) => {
@@ -33,7 +35,6 @@ export default [
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
// extract: 'dist/frappe-charts.min.css',
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -56,10 +57,14 @@ export default [
]
},
{
input: 'src/js/charts.js',
input: 'src/js/chart.js',
output: [
{
file: pkg.src,
file: pkg.main,
format: 'cjs',
},
{
file: pkg.module,
format: 'es',
}
],
@@ -70,7 +75,6 @@ export default [
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extract: 'dist/frappe-charts.min.css',
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -82,25 +86,24 @@ export default [
'src/scss/**',
]
}),
babel({
exclude: 'node_modules/**',
}),
replace({
exclude: 'node_modules/**',
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
})
}),
uglify()
],
},
{
input: 'src/js/charts.js',
input: 'src/js/chart.js',
output: [
{
file: 'docs/assets/js/frappe-charts.min.js',
format: 'iife',
},
{
file: pkg.browser,
format: 'iife',
file: pkg.src,
format: 'es',
}
],
name: 'Chart',
plugins: [
postcss({
preprocessor: (content, id) => new Promise((resolve, reject) => {
@@ -108,6 +111,7 @@ export default [
resolve({ code: result.css.toString() })
}),
extensions: [ '.scss' ],
extract: 'dist/frappe-charts.min.css',
plugins: [
nested(),
cssnext({ warnForDuplicates: false }),
@@ -119,14 +123,10 @@ export default [
'src/scss/**',
]
}),
babel({
exclude: 'node_modules/**',
}),
replace({
exclude: 'node_modules/**',
ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
}),
uglify()
})
],
}
];

+ 40
- 0
src/js/chart.js 查看文件

@@ -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);
}
}

+ 0
- 39
src/js/charts.js 查看文件

@@ -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]);
}
}

+ 72
- 0
src/js/charts/AggregationChart.js 查看文件

@@ -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>`;
}
});
}
}

+ 433
- 712
src/js/charts/AxisChart.js
文件差異過大導致無法顯示
查看文件


+ 0
- 81
src/js/charts/BarChart.js 查看文件

@@ -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;
}
}

+ 204
- 220
src/js/charts/BaseChart.js 查看文件

@@ -2,177 +2,106 @@ import SvgTip from '../objects/SvgTip';
import { $, isElementInViewport, getElementContentWidth } from '../utils/dom';
import { makeSVGContainer, makeSVGDefs, makeSVGGroup } from '../utils/draw';
import { getStringWidth } from '../utils/helpers';
import { VERT_SPACE_OUTSIDE_BASE_CHART, TRANSLATE_Y_BASE_CHART, LEFT_MARGIN_BASE_CHART,
RIGHT_MARGIN_BASE_CHART, INIT_CHART_UPDATE_TIMEOUT, CHART_POST_ANIMATE_TIMEOUT } from '../utils/constants';
import { getColor, DEFAULT_COLORS } from '../utils/colors';
import Chart from '../charts';

const ALL_CHART_TYPES = ['line', 'scatter', 'bar', 'percentage', 'heatmap', 'pie'];

const COMPATIBLE_CHARTS = {
bar: ['line', 'scatter', 'percentage', 'pie'],
line: ['scatter', 'bar', 'percentage', 'pie'],
pie: ['line', 'scatter', 'percentage', 'bar'],
scatter: ['line', 'bar', 'percentage', 'pie'],
percentage: ['bar', 'line', 'scatter', 'pie'],
heatmap: []
};

// TODO: Needs structure as per only labels/datasets
const COLOR_COMPATIBLE_CHARTS = {
bar: ['line', 'scatter'],
line: ['scatter', 'bar'],
pie: ['percentage'],
scatter: ['line', 'bar'],
percentage: ['pie'],
heatmap: []
};
import { getDifferentChart } from '../config';
import { runSMILAnimation } from '../utils/animation';

export default class BaseChart {
constructor({
height = 240,

title = '',
subtitle = '',
colors = [],
summary = [],

is_navigable = 0,
has_legend = 0,

type = '',

parent,
data
}) {
this.raw_chart_args = arguments[0];
constructor(parent, options) {
this.rawChartArgs = options;

this.parent = typeof parent === 'string' ? document.querySelector(parent) : parent;
this.title = title;
this.subtitle = subtitle;
if (!(this.parent instanceof HTMLElement)) {
throw new Error('No `parent` element to render on was provided.');
}

this.data = data;
this.title = options.title || '';
this.subtitle = options.subtitle || '';
this.argHeight = options.height || 240;
this.type = options.type || '';

this.realData = this.prepareData(options.data);
this.data = this.prepareFirstData(this.realData);
this.colors = [];
this.config = {
showTooltip: 1, // calculate
showLegend: options.showLegend || 1,
isNavigable: options.isNavigable || 0,
animate: 1
};
this.state = {};
this.options = {};

this.specific_values = data.specific_values || [];
this.summary = summary;
this.initTimeout = INIT_CHART_UPDATE_TIMEOUT;

this.is_navigable = is_navigable;
if(this.is_navigable) {
this.current_index = 0;
if(this.config.isNavigable) {
this.overlays = [];
}
this.has_legend = has_legend;

this.setColors(colors, type);
this.set_margins(height);
this.configure(options);
}

get_different_chart(type) {
if(type === this.type) return;

if(!ALL_CHART_TYPES.includes(type)) {
console.error(`'${type}' is not a valid chart type.`);
}

if(!COMPATIBLE_CHARTS[this.type].includes(type)) {
console.error(`'${this.type}' chart cannot be converted to a '${type}' chart.`);
}
configure(args) {
this.setColors();
this.setMargins();

// whether the new chart can use the existing colors
const use_color = COLOR_COMPATIBLE_CHARTS[this.type].includes(type);

// Okay, this is anticlimactic
// this function will need to actually be 'change_chart_type(type)'
// that will update only the required elements, but for now ...
return new Chart({
parent: this.raw_chart_args.parent,
title: this.title,
data: this.raw_chart_args.data,
type: type,
height: this.raw_chart_args.height,
colors: use_color ? this.colors : undefined
});
// Bind window events
window.addEventListener('resize', () => this.draw(true));
window.addEventListener('orientationchange', () => this.draw(true));
}

setColors(colors, type) {
this.colors = colors;
setColors() {
let args = this.rawChartArgs;

// TODO: Needs structure as per only labels/datasets
const list = type === 'percentage' || type === 'pie'
? this.data.labels
: this.data.datasets;
// Needs structure as per only labels/datasets, from config
const list = args.type === 'percentage' || args.type === 'pie'
? args.data.labels
: args.data.datasets;

if(!this.colors || (list && this.colors.length < list.length)) {
if(!args.colors || (list && args.colors.length < list.length)) {
this.colors = DEFAULT_COLORS;
} else {
this.colors = args.colors;
}

this.colors = this.colors.map(color => getColor(color));
}

set_margins(height) {
this.base_height = height;
this.height = height - 40;
this.translate_x = 60;
this.translate_y = 10;
}
setMargins() {
let height = this.argHeight;
this.baseHeight = height;
this.height = height - VERT_SPACE_OUTSIDE_BASE_CHART;
this.translateY = TRANSLATE_Y_BASE_CHART;

setup() {
if(!this.parent) {
console.error("No parent element to render on was provided.");
return;
}
if(this.validate_and_prepare_data()) {
this.bind_window_events();
this.refresh(true);
}
// Horizontal margins
this.leftMargin = LEFT_MARGIN_BASE_CHART;
this.rightMargin = RIGHT_MARGIN_BASE_CHART;
}

validate_and_prepare_data() {
validate() {
return true;
}

bind_window_events() {
window.addEventListener('resize', () => this.refresh());
window.addEventListener('orientationchange', () => this.refresh());
setup() {
if(this.validate()) {
this._setup();
}
}

refresh(init=false) {
this.setup_base_values();
this.set_width();

this.setup_container();
this.setup_components();

this.setup_values();
this.setup_utils();
_setup() {
this.makeContainer();
this.makeTooltip();

this.make_graph_components(init);
this.make_tooltip();

if(this.summary.length > 0) {
this.show_custom_summary();
} else {
this.show_summary();
}

if(this.is_navigable) {
this.setup_navigation(init);
}
this.draw(false, true);
}

set_width() {
let special_values_width = 0;
let char_width = 8;
this.specific_values.map(val => {
let str_width = getStringWidth((val.title + ""), char_width);
if(str_width > special_values_width) {
special_values_width = str_width - 40;
}
});
this.base_width = getElementContentWidth(this.parent) - special_values_width;
this.width = this.base_width - this.translate_x * 2;
setupComponents() {
this.components = new Map();
}

setup_base_values() {}

setup_container() {
makeContainer() {
this.container = $.create('div', {
className: 'chart-container',
innerHTML: `<h6 class="title">${this.title}</h6>
@@ -185,120 +114,175 @@ export default class BaseChart {
this.parent.innerHTML = '';
this.parent.appendChild(this.container);

this.chart_wrapper = this.container.querySelector('.frappe-chart');
this.stats_wrapper = this.container.querySelector('.graph-stats-container');
this.chartWrapper = this.container.querySelector('.frappe-chart');
this.statsWrapper = this.container.querySelector('.graph-stats-container');
}

this.make_chart_area();
this.make_draw_area();
makeTooltip() {
this.tip = new SvgTip({
parent: this.chartWrapper,
colors: this.colors
});
this.bindTooltip();
}

make_chart_area() {
this.svg = makeSVGContainer(
this.chart_wrapper,
'chart',
this.base_width,
this.base_height
);
this.svg_defs = makeSVGDefs(this.svg);
return this.svg;
bindTooltip() {}

draw(onlyWidthChange=false, init=false) {
this.calcWidth();
this.calc(onlyWidthChange);
this.makeChartArea();
this.setupComponents();

this.components.forEach(c => c.setup(this.drawArea));
// this.components.forEach(c => c.make());
this.render(this.components, false);

if(init) {
this.data = this.realData;
setTimeout(() => {this.update();}, this.initTimeout);
}

if(!onlyWidthChange) {
this.renderLegend();
}

this.setupNavigation(init);
}

make_draw_area() {
this.draw_area = makeSVGGroup(
this.svg,
this.type + '-chart',
`translate(${this.translate_x}, ${this.translate_y})`
);
calcWidth() {
this.baseWidth = getElementContentWidth(this.parent);
this.width = this.baseWidth - (this.leftMargin + this.rightMargin);
}

setup_components() { }
update(data=this.data) {
this.data = this.prepareData(data);
this.calc(); // builds state
this.render();
}

make_tooltip() {
this.tip = new SvgTip({
parent: this.chart_wrapper,
colors: this.colors
});
this.bind_tooltip();
prepareData(data=this.data) {
return data;
}

prepareFirstData(data=this.data) {
return data;
}

show_summary() {}
show_custom_summary() {
this.summary.map(d => {
let stats = $.create('div', {
className: 'stats',
innerHTML: `<span class="indicator">
<i style="background:${d.color}"></i>
${d.title}: ${d.value}
</span>`
});
this.stats_wrapper.appendChild(stats);
calc() {} // builds state

render(components=this.components, animate=true) {
if(this.config.isNavigable) {
// Remove all existing overlays
this.overlays.map(o => o.parentNode.removeChild(o));
// ref.parentNode.insertBefore(element, ref);
}
let elementsToAnimate = [];
// Can decouple to this.refreshComponents() first to save animation timeout
components.forEach(c => {
elementsToAnimate = elementsToAnimate.concat(c.update(animate));
});
if(elementsToAnimate.length > 0) {
runSMILAnimation(this.chartWrapper, this.svg, elementsToAnimate);
setTimeout(() => {
components.forEach(c => c.make());
this.updateNav();
}, CHART_POST_ANIMATE_TIMEOUT);
} else {
components.forEach(c => c.make());
this.updateNav();
}
}

updateNav() {
if(this.config.isNavigable) {
// if(!this.overlayGuides){
this.makeOverlay();
this.bindUnits();
// } else {
// this.updateOverlay();
// }
}
}

setup_navigation(init=false) {
this.make_overlay();
makeChartArea() {
if(this.svg) {
this.chartWrapper.removeChild(this.svg);
}
this.svg = makeSVGContainer(
this.chartWrapper,
'chart',
this.baseWidth,
this.baseHeight
);
this.svgDefs = makeSVGDefs(this.svg);

// I WISH !!!
// this.svg = makeSVGGroup(
// svgContainer,
// 'flipped-coord-system',
// `translate(0, ${this.baseHeight}) scale(1, -1)`
// );

this.drawArea = makeSVGGroup(
this.svg,
this.type + '-chart',
`translate(${this.leftMargin}, ${this.translateY})`
);
}

renderLegend() {}

setupNavigation(init=false) {
if(!this.config.isNavigable) return;

if(init) {
this.bind_overlay();
this.bindOverlay();

this.keyActions = {
'13': this.onEnterKey.bind(this),
'37': this.onLeftArrow.bind(this),
'38': this.onUpArrow.bind(this),
'39': this.onRightArrow.bind(this),
'40': this.onDownArrow.bind(this),
};

document.addEventListener('keydown', (e) => {
if(isElementInViewport(this.chart_wrapper)) {
if(isElementInViewport(this.chartWrapper)) {
e = e || window.event;

if (e.keyCode == '37') {
this.on_left_arrow();
} else if (e.keyCode == '39') {
this.on_right_arrow();
} else if (e.keyCode == '38') {
this.on_up_arrow();
} else if (e.keyCode == '40') {
this.on_down_arrow();
} else if (e.keyCode == '13') {
this.on_enter_key();
if(this.keyActions[e.keyCode]) {
this.keyActions[e.keyCode]();
}
}
});
}
}

make_overlay() {}
bind_overlay() {}
bind_units() {}
makeOverlay() {}
updateOverlay() {}
bindOverlay() {}
bindUnits() {}

on_left_arrow() {}
on_right_arrow() {}
on_up_arrow() {}
on_down_arrow() {}
on_enter_key() {}
onLeftArrow() {}
onRightArrow() {}
onUpArrow() {}
onDownArrow() {}
onEnterKey() {}

get_data_point(index=this.current_index) {
// check for length
let data_point = {
index: index
};
let y = this.y[0];
['svg_units', 'y_tops', 'values'].map(key => {
let data_key = key.slice(0, key.length-1);
data_point[data_key] = y[key][index];
});
data_point.label = this.x[index];
return data_point;
}
getDataPoint(index = 0) {}
setCurrentDataPoint(point) {}

update_current_data_point(index) {
index = parseInt(index);
if(index < 0) index = 0;
if(index >= this.x.length) index = this.x.length - 1;
if(index === this.current_index) return;
this.current_index = index;
$.fire(this.parent, "data-select", this.get_data_point());
}
updateDataset(dataset, index) {}
addDataset(dataset, index) {}
removeDataset(index = 0) {}

updateDatasets(datasets) {}

// Objects
setup_utils() { }
updateDataPoint(dataPoint, index = 0) {}
addDataPoint(dataPoint, index = 0) {}
removeDataPoint(index = 0) {}

makeDrawAreaComponent(className, transform='') {
return makeSVGGroup(this.draw_area, className, transform);
getDifferentChart(type) {
return getDifferentChart(type, this.type, this.parent, this.rawChartArgs);
}
}

+ 127
- 127
src/js/charts/Heatmap.js 查看文件

@@ -5,41 +5,39 @@ import { calcDistribution, getMaxCheckpoint } from '../utils/intervals';
import { isValidColor } from '../utils/colors';

export default class Heatmap extends BaseChart {
constructor({
start = '',
domain = '',
subdomain = '',
data = {},
discrete_domains = 0,
count_label = '',
legend_colors = []
}) {
super(arguments[0]);
constructor(parent, options) {
super(parent, options);

this.type = 'heatmap';

this.domain = domain;
this.subdomain = subdomain;
this.data = data;
this.discrete_domains = discrete_domains;
this.count_label = count_label;
this.domain = options.domain || '';
this.subdomain = options.subdomain || '';
this.data = options.data || {};
this.discreteDomains = options.discreteDomains === 0 ? 0 : 1;
this.countLabel = options.countLabel || '';

let today = new Date();
this.start = start || addDays(today, 365);
this.start = options.start || addDays(today, 365);

legend_colors = legend_colors.slice(0, 5);
this.legend_colors = this.validate_colors(legend_colors)
? legend_colors
let legendColors = (options.legendColors || []).slice(0, 5);
this.legendColors = this.validate_colors(legendColors)
? legendColors
: ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];

// Fixed 5-color theme,
// More colors are difficult to parse visually
this.distribution_size = 5;

this.translate_x = 0;
this.translateX = 0;
this.setup();
}

setMargins() {
super.setMargins();
this.leftMargin = 10;
this.translateY = 10;
}

validate_colors(colors) {
if(colors.length < 5) return 0;

@@ -54,210 +52,212 @@ export default class Heatmap extends BaseChart {
return valid;
}

setup_base_values() {
configure() {
super.configure();
this.today = new Date();

if(!this.start) {
this.start = new Date();
this.start.setFullYear( this.start.getFullYear() - 1 );
}
this.first_week_start = new Date(this.start.toDateString());
this.last_week_start = new Date(this.today.toDateString());
if(this.first_week_start.getDay() !== 7) {
addDays(this.first_week_start, (-1) * this.first_week_start.getDay());
this.firstWeekStart = new Date(this.start.toDateString());
this.lastWeekStart = new Date(this.today.toDateString());
if(this.firstWeekStart.getDay() !== 7) {
addDays(this.firstWeekStart, (-1) * this.firstWeekStart.getDay());
}
if(this.last_week_start.getDay() !== 7) {
addDays(this.last_week_start, (-1) * this.last_week_start.getDay());
if(this.lastWeekStart.getDay() !== 7) {
addDays(this.lastWeekStart, (-1) * this.lastWeekStart.getDay());
}
this.no_of_cols = getWeeksBetween(this.first_week_start + '', this.last_week_start + '') + 1;
this.no_of_cols = getWeeksBetween(this.firstWeekStart + '', this.lastWeekStart + '') + 1;
}

set_width() {
this.base_width = (this.no_of_cols + 3) * 12 ;
calcWidth() {
this.baseWidth = (this.no_of_cols + 3) * 12 ;

if(this.discrete_domains) {
this.base_width += (12 * 12);
if(this.discreteDomains) {
this.baseWidth += (12 * 12);
}
}

setup_components() {
this.domain_label_group = this.makeDrawAreaComponent(
makeChartArea() {
super.makeChartArea();
this.domainLabelGroup = makeSVGGroup(this.drawArea,
'domain-label-group chart-label');

this.data_groups = this.makeDrawAreaComponent(
this.dataGroups = makeSVGGroup(this.drawArea,
'data-groups',
`translate(0, 20)`
);

this.container.querySelector('.title').style.display = 'None';
this.container.querySelector('.sub-title').style.display = 'None';
this.container.querySelector('.graph-stats-container').style.display = 'None';
this.chartWrapper.style.marginTop = '0px';
this.chartWrapper.style.paddingTop = '0px';
}

setup_values() {
this.domain_label_group.textContent = '';
this.data_groups.textContent = '';
calc() {

let data_values = Object.keys(this.data).map(key => this.data[key]);
this.distribution = calcDistribution(data_values, this.distribution_size);
let dataValues = Object.keys(this.data).map(key => this.data[key]);
this.distribution = calcDistribution(dataValues, this.distribution_size);

this.month_names = ["January", "February", "March", "April", "May", "June",
this.monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
}

this.render_all_weeks_and_store_x_values(this.no_of_cols);
render() {
this.renderAllWeeksAndStoreXValues(this.no_of_cols);
}

render_all_weeks_and_store_x_values(no_of_weeks) {
let current_week_sunday = new Date(this.first_week_start);
this.week_col = 0;
this.current_month = current_week_sunday.getMonth();
renderAllWeeksAndStoreXValues(no_of_weeks) {
// renderAllWeeksAndStoreXValues
this.domainLabelGroup.textContent = '';
this.dataGroups.textContent = '';

let currentWeekSunday = new Date(this.firstWeekStart);
this.weekCol = 0;
this.currentMonth = currentWeekSunday.getMonth();

this.months = [this.current_month + ''];
this.month_weeks = {}, this.month_start_points = [];
this.month_weeks[this.current_month] = 0;
this.month_start_points.push(13);
this.months = [this.currentMonth + ''];
this.monthWeeks = {}, this.monthStartPoints = [];
this.monthWeeks[this.currentMonth] = 0;
this.monthStartPoints.push(13);

for(var i = 0; i < no_of_weeks; i++) {
let data_group, month_change = 0;
let day = new Date(current_week_sunday);
[data_group, month_change] = this.get_week_squares_group(day, this.week_col);
this.data_groups.appendChild(data_group);
this.week_col += 1 + parseInt(this.discrete_domains && month_change);
this.month_weeks[this.current_month]++;
if(month_change) {
this.current_month = (this.current_month + 1) % 12;
this.months.push(this.current_month + '');
this.month_weeks[this.current_month] = 1;
let dataGroup, monthChange = 0;
let day = new Date(currentWeekSunday);
[dataGroup, monthChange] = this.get_week_squares_group(day, this.weekCol);
this.dataGroups.appendChild(dataGroup);
this.weekCol += 1 + parseInt(this.discreteDomains && monthChange);
this.monthWeeks[this.currentMonth]++;
if(monthChange) {
this.currentMonth = (this.currentMonth + 1) % 12;
this.months.push(this.currentMonth + '');
this.monthWeeks[this.currentMonth] = 1;
}
addDays(current_week_sunday, 7);
addDays(currentWeekSunday, 7);
}
this.render_month_labels();
}

get_week_squares_group(current_date, index) {
const no_of_weekdays = 7;
const square_side = 10;
const cell_padding = 2;
get_week_squares_group(currentDate, index) {
const noOfWeekdays = 7;
const squareSide = 10;
const cellPadding = 2;
const step = 1;
const today_time = this.today.getTime();
const todayTime = this.today.getTime();

let month_change = 0;
let week_col_change = 0;
let monthChange = 0;
let weekColChange = 0;

let data_group = makeSVGGroup(this.data_groups, 'data-group');
let dataGroup = makeSVGGroup(this.dataGroups, 'data-group');

for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) {
let data_value = 0;
let color_index = 0;
for(var y = 0, i = 0; i < noOfWeekdays; i += step, y += (squareSide + cellPadding)) {
let dataValue = 0;
let colorIndex = 0;

let current_timestamp = current_date.getTime()/1000;
let timestamp = Math.floor(current_timestamp - (current_timestamp % 86400)).toFixed(1);
let currentTimestamp = currentDate.getTime()/1000;
let timestamp = Math.floor(currentTimestamp - (currentTimestamp % 86400)).toFixed(1);

if(this.data[timestamp]) {
data_value = this.data[timestamp];
dataValue = this.data[timestamp];
}

if(this.data[Math.round(timestamp)]) {
data_value = this.data[Math.round(timestamp)];
dataValue = this.data[Math.round(timestamp)];
}

if(data_value) {
color_index = getMaxCheckpoint(data_value, this.distribution);
if(dataValue) {
colorIndex = getMaxCheckpoint(dataValue, this.distribution);
}

let x = 13 + (index + week_col_change) * 12;
let x = 13 + (index + weekColChange) * 12;

let dataAttr = {
'data-date': getDdMmYyyy(current_date),
'data-value': data_value,
'data-day': current_date.getDay()
'data-date': getDdMmYyyy(currentDate),
'data-value': dataValue,
'data-day': currentDate.getDay()
};
let heatSquare = makeHeatSquare('day', x, y, square_side,
this.legend_colors[color_index], dataAttr);

data_group.appendChild(heatSquare);
let heatSquare = makeHeatSquare('day', x, y, squareSide,
this.legendColors[colorIndex], dataAttr);

let next_date = new Date(current_date);
addDays(next_date, 1);
if(next_date.getTime() > today_time) break;
dataGroup.appendChild(heatSquare);

let nextDate = new Date(currentDate);
addDays(nextDate, 1);
if(nextDate.getTime() > todayTime) break;

if(next_date.getMonth() - current_date.getMonth()) {
month_change = 1;
if(this.discrete_domains) {
week_col_change = 1;

if(nextDate.getMonth() - currentDate.getMonth()) {
monthChange = 1;
if(this.discreteDomains) {
weekColChange = 1;
}

this.month_start_points.push(13 + (index + week_col_change) * 12);
this.monthStartPoints.push(13 + (index + weekColChange) * 12);
}
current_date = next_date;
currentDate = nextDate;
}

return [data_group, month_change];
return [dataGroup, monthChange];
}

render_month_labels() {
// this.first_month_label = 1;
// if (this.first_week_start.getDate() > 8) {
// if (this.firstWeekStart.getDate() > 8) {
// this.first_month_label = 0;
// }
// this.last_month_label = 1;

// let first_month = this.months.shift();
// let first_month_start = this.month_start_points.shift();
// let first_month_start = this.monthStartPoints.shift();
// render first month if

// let last_month = this.months.pop();
// let last_month_start = this.month_start_points.pop();
// let last_month_start = this.monthStartPoints.pop();
// render last month if

this.months.shift();
this.month_start_points.shift();
this.monthStartPoints.shift();
this.months.pop();
this.month_start_points.pop();
this.monthStartPoints.pop();

this.month_start_points.map((start, i) => {
let month_name = this.month_names[this.months[i]].substring(0, 3);
this.monthStartPoints.map((start, i) => {
let month_name = this.monthNames[this.months[i]].substring(0, 3);
let text = makeText('y-value-text', start+12, 10, month_name);
this.domain_label_group.appendChild(text);
});
}

make_graph_components() {
Array.prototype.slice.call(
this.container.querySelectorAll('.graph-stats-container, .sub-title, .title')
).map(d => {
d.style.display = 'None';
this.domainLabelGroup.appendChild(text);
});
this.chart_wrapper.style.marginTop = '0px';
this.chart_wrapper.style.paddingTop = '0px';
}

bind_tooltip() {
bindTooltip() {
Array.prototype.slice.call(
document.querySelectorAll(".data-group .day")
).map(el => {
el.addEventListener('mouseenter', (e) => {
let count = e.target.getAttribute('data-value');
let date_parts = e.target.getAttribute('data-date').split('-');
let dateParts = e.target.getAttribute('data-date').split('-');

let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3);
let month = this.monthNames[parseInt(dateParts[1])-1].substring(0, 3);

let g_off = this.chart_wrapper.getBoundingClientRect(), p_off = e.target.getBoundingClientRect();
let gOff = this.chartWrapper.getBoundingClientRect(), pOff = e.target.getBoundingClientRect();

let width = parseInt(e.target.getAttribute('width'));
let x = p_off.left - g_off.left + (width+2)/2;
let y = p_off.top - g_off.top - (width+2)/2;
let value = count + ' ' + this.count_label;
let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2];
let x = pOff.left - gOff.left + (width+2)/2;
let y = pOff.top - gOff.top - (width+2)/2;
let value = count + ' ' + this.countLabel;
let name = ' on ' + month + ' ' + dateParts[0] + ', ' + dateParts[2];

this.tip.set_values(x, y, name, value, [], 1);
this.tip.show_tip();
this.tip.setValues(x, y, {name: name, value: value, valueFirst: 1}, []);
this.tip.showTip();
});
});
}

update(data) {
this.data = data;
this.setup_values();
this.bind_tooltip();
super.update(data);
this.bindTooltip();
}
}

+ 0
- 91
src/js/charts/LineChart.js 查看文件

@@ -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);
}
}

+ 173
- 0
src/js/charts/MultiAxisChart.js 查看文件

@@ -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
));
});
}
});
});
}
}

+ 39
- 92
src/js/charts/PercentageChart.js 查看文件

@@ -1,127 +1,74 @@
import BaseChart from './BaseChart';
import { $, offset } from '../utils/dom';
import AggregationChart from './AggregationChart';
import { $, getOffset } from '../utils/dom';

export default class PercentageChart extends BaseChart {
constructor(args) {
super(args);
export default class PercentageChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = 'percentage';

this.max_slices = 10;
this.max_legend_points = 6;

this.setup();
}

make_chart_area() {
this.chart_wrapper.className += ' ' + 'graph-focus-margin';
this.chart_wrapper.style.marginTop = '45px';
makeChartArea() {
this.chartWrapper.className += ' ' + 'graph-focus-margin';
this.chartWrapper.style.marginTop = '45px';

this.stats_wrapper.className += ' ' + 'graph-focus-margin';
this.stats_wrapper.style.marginBottom = '30px';
this.stats_wrapper.style.paddingTop = '0px';
}
this.statsWrapper.className += ' ' + 'graph-focus-margin';
this.statsWrapper.style.marginBottom = '30px';
this.statsWrapper.style.paddingTop = '0px';

make_draw_area() {
this.chart_div = $.create('div', {
this.svg = $.create('div', {
className: 'div',
inside: this.chart_wrapper
inside: this.chartWrapper
});

this.chart = $.create('div', {
className: 'progress-chart',
inside: this.chart_div
inside: this.svg
});
}

setup_components() {
this.percentage_bar = $.create('div', {
this.percentageBar = $.create('div', {
className: 'progress',
inside: this.chart
});
}

setup_values() {
this.slice_totals = [];
let all_totals = this.data.labels.map((d, i) => {
let total = 0;
this.data.datasets.map(e => {
total += e.values[i];
});
return [total, d];
}).filter(d => { return d[0] > 0; }); // keep only positive results

let totals = all_totals;

if(all_totals.length > this.max_slices) {
all_totals.sort((a, b) => { return b[0] - a[0]; });

totals = all_totals.slice(0, this.max_slices-1);
let others = all_totals.slice(this.max_slices-1);

let sum_of_others = 0;
others.map(d => {sum_of_others += d[0];});

totals.push([sum_of_others, 'Rest']);

this.colors[this.max_slices-1] = 'grey';
}

this.labels = [];
totals.map(d => {
this.slice_totals.push(d[0]);
this.labels.push(d[1]);
});

this.legend_totals = this.slice_totals.slice(0, this.max_legend_points);
}

make_graph_components() {
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
this.slices = [];
this.slice_totals.map((total, i) => {
render() {
let s = this.state;
this.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);
s.slices = [];
s.sliceTotals.map((total, i) => {
let slice = $.create('div', {
className: `progress-bar`,
inside: this.percentage_bar,
'data-index': i,
inside: this.percentageBar,
styles: {
background: this.colors[i],
width: total*100/this.grand_total + "%"
width: total*100/this.grandTotal + "%"
}
});
this.slices.push(slice);
s.slices.push(slice);
});
}

bind_tooltip() {
this.slices.map((slice, i) => {
slice.addEventListener('mouseenter', () => {
let g_off = offset(this.chart_wrapper), p_off = offset(slice);
bindTooltip() {
let s = this.state;

let x = p_off.left - g_off.left + slice.offsetWidth/2;
let y = p_off.top - g_off.top - 6;
let title = (this.formatted_labels && this.formatted_labels.length>0
? this.formatted_labels[i] : this.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
this.chartWrapper.addEventListener('mousemove', (e) => {
let slice = e.target;
if(slice.classList.contains('progress-bar')) {

this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
});
});
}
let i = slice.getAttribute('data-index');
let gOff = getOffset(this.chartWrapper), pOff = getOffset(slice);

let x = pOff.left - gOff.left + slice.offsetWidth/2;
let y = pOff.top - gOff.top - 6;
let title = (this.formattedLabels && this.formattedLabels.length>0
? this.formattedLabels[i] : this.state.labels[i]) + ': ';
let percent = (s.sliceTotals[i]*100/this.grandTotal).toFixed(1);

show_summary() {
let x_values = this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels : this.labels;
this.legend_totals.map((d, i) => {
if(d) {
let stats = $.create('div', {
className: 'stats',
inside: this.stats_wrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background: ${this.colors[i]}"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
this.tip.setValues(x, y, {name: title, value: percent + "%"});
this.tip.showTip();
}
});
}


+ 109
- 154
src/js/charts/PieChart.js 查看文件

@@ -1,213 +1,168 @@
import BaseChart from './BaseChart';
import { $, offset } from '../utils/dom';
import { makePath } from '../utils/draw';
import AggregationChart from './AggregationChart';
import { getComponent } from '../objects/ChartComponents';
import { getOffset } from '../utils/dom';
import { getPositionByAngle } from '../utils/helpers';
import { makeArcPathStr } from '../utils/draw';
import { lightenDarkenColor } from '../utils/colors';
import { runSVGAnimation, transform } from '../utils/animation';
const ANGLE_RATIO = Math.PI / 180;
const FULL_ANGLE = 360;
import { transform } from '../utils/animation';
import { FULL_ANGLE } from '../utils/constants';

export default class PieChart extends BaseChart {
constructor(args) {
super(args);
export default class PieChart extends AggregationChart {
constructor(parent, args) {
super(parent, args);
this.type = 'pie';
this.elements_to_animate = null;
this.hoverRadio = args.hoverRadio || 0.1;
this.max_slices = 10;
this.max_legend_points = 6;
this.isAnimate = false;
this.startAngle = args.startAngle || 0;
this.clockWise = args.clockWise || false;
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);
this.initTimeout = 0;

this.setup();
}
setup_values() {
this.centerX = this.width / 2;
this.centerY = this.height / 2;
this.radius = (this.height > this.width ? this.centerX : this.centerY);
this.slice_totals = [];
let all_totals = this.data.labels.map((d, i) => {
let total = 0;
this.data.datasets.map(e => {
total += e.values[i];
});
return [total, d];
}).filter(d => { return d[0] > 0; }); // keep only positive results

let totals = all_totals;
configure(args) {
super.configure(args);
this.mouseMove = this.mouseMove.bind(this);
this.mouseLeave = this.mouseLeave.bind(this);

if(all_totals.length > this.max_slices) {
all_totals.sort((a, b) => { return b[0] - a[0]; });
this.hoverRadio = args.hoverRadio || 0.1;
this.config.startAngle = args.startAngle || 0;

totals = all_totals.slice(0, this.max_slices-1);
let others = all_totals.slice(this.max_slices-1);
this.clockWise = args.clockWise || false;
}

let sum_of_others = 0;
others.map(d => {sum_of_others += d[0];});
prepareFirstData(data=this.data) {
this.init = 1;
return data;
}

totals.push([sum_of_others, 'Rest']);
calc() {
super.calc();
let s = this.state;

this.colors[this.max_slices-1] = 'grey';
}
this.center = {
x: this.width / 2,
y: this.height / 2
};
this.radius = (this.height > this.width ? this.center.x : this.center.y);

this.labels = [];
totals.map(d => {
this.slice_totals.push(d[0]);
this.labels.push(d[1]);
});
s.grandTotal = s.sliceTotals.reduce((a, b) => a + b, 0);

this.legend_totals = this.slice_totals.slice(0, this.max_legend_points);
this.calcSlices();
}

static getPositionByAngle(angle,radius){
return {
x:Math.sin(angle * ANGLE_RATIO) * radius,
y:Math.cos(angle * ANGLE_RATIO) * radius,
};
}
makeArcPath(startPosition,endPosition){
const{centerX,centerY,radius,clockWise} = this;
return `M${centerX} ${centerY} L${centerX+startPosition.x} ${centerY+startPosition.y} A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0} ${centerX+endPosition.x} ${centerY+endPosition.y} z`;
}
make_graph_components(init){
const{radius,clockWise} = this;
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
const prevSlicesProperties = this.slicesProperties || [];
this.slices = [];
this.elements_to_animate = [];
this.slicesProperties = [];
let curAngle = 180 - this.startAngle;
this.slice_totals.map((total, i) => {
calcSlices() {
let s = this.state;
const { radius, clockWise } = this;

const prevSlicesProperties = s.slicesProperties || [];
s.sliceStrings = [];
s.slicesProperties = [];
let curAngle = 180 - this.config.startAngle;

s.sliceTotals.map((total, i) => {
const startAngle = curAngle;
const originDiffAngle = (total / this.grand_total) * FULL_ANGLE;
const originDiffAngle = (total / s.grandTotal) * FULL_ANGLE;
const diffAngle = clockWise ? -originDiffAngle : originDiffAngle;
const endAngle = curAngle = curAngle + diffAngle;
const startPosition = PieChart.getPositionByAngle(startAngle,radius);
const endPosition = PieChart.getPositionByAngle(endAngle,radius);
const prevProperty = init && prevSlicesProperties[i];
const startPosition = getPositionByAngle(startAngle, radius);
const endPosition = getPositionByAngle(endAngle, radius);

const prevProperty = this.init && prevSlicesProperties[i];

let curStart,curEnd;
if(init){
curStart = prevProperty?prevProperty.startPosition : startPosition;
curEnd = prevProperty? prevProperty.endPosition : startPosition;
}else{
if(this.init) {
curStart = prevProperty ? prevProperty.startPosition : startPosition;
curEnd = prevProperty ? prevProperty.endPosition : startPosition;
} else {
curStart = startPosition;
curEnd = endPosition;
}
const curPath = this.makeArcPath(curStart,curEnd);
let slice = makePath(curPath, 'pie-path', 'none', this.colors[i]);
slice.style.transition = 'transform .3s;';
this.draw_area.appendChild(slice);
const curPath = makeArcPathStr(curStart, curEnd, this.center, this.radius, this.clockWise);

this.slices.push(slice);
this.slicesProperties.push({
s.sliceStrings.push(curPath);
s.slicesProperties.push({
startPosition,
endPosition,
value: total,
total: this.grand_total,
total: s.grandTotal,
startAngle,
endAngle,
angle:diffAngle
angle: diffAngle
});
if(init){
this.elements_to_animate.push([{unit: slice, array: this.slices, index: this.slices.length - 1},
{d:this.makeArcPath(startPosition,endPosition)},
650, "easein",null,{
d:curPath
}]);
}

});
if(init){
this.run_animation();
}
this.init = 0;
}
run_animation() {
// if(this.isAnimate) return ;
// this.isAnimate = true;
if(!this.elements_to_animate || this.elements_to_animate.length === 0) return;
let anim_svg = runSVGAnimation(this.svg, this.elements_to_animate);

if(this.svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(this.svg);
this.chart_wrapper.appendChild(anim_svg);

}

// Replace the new svg (data has long been replaced)
setTimeout(() => {
// this.isAnimate = false;
if(anim_svg.parentNode == this.chart_wrapper) {
this.chart_wrapper.removeChild(anim_svg);
this.chart_wrapper.appendChild(this.svg);
}
}, 650);
setupComponents() {
let s = this.state;

let componentConfigs = [
[
'pieSlices',
{ },
function() {
return {
sliceStrings: s.sliceStrings,
colors: this.colors
};
}.bind(this)
]
];

this.components = new Map(componentConfigs
.map(args => {
let component = getComponent(...args);
return [args[0], component];
}));
}

calTranslateByAngle(property){
const{radius,hoverRadio} = this;
const position = PieChart.getPositionByAngle(property.startAngle+(property.angle / 2),radius);
const position = getPositionByAngle(property.startAngle+(property.angle / 2),radius);
return `translate3d(${(position.x) * hoverRadio}px,${(position.y) * hoverRadio}px,0)`;
}

hoverSlice(path,i,flag,e){
if(!path) return;
const color = this.colors[i];
if(flag){
transform(path,this.calTranslateByAngle(this.slicesProperties[i]));
path.style.fill = lightenDarkenColor(color,50);
let g_off = offset(this.svg);
if(flag) {
transform(path, this.calTranslateByAngle(this.state.slicesProperties[i]));
path.style.fill = lightenDarkenColor(color, 50);
let g_off = getOffset(this.svg);
let x = e.pageX - g_off.left + 10;
let y = e.pageY - g_off.top - 10;
let title = (this.formatted_labels && this.formatted_labels.length>0
? this.formatted_labels[i] : this.labels[i]) + ': ';
let percent = (this.slice_totals[i]*100/this.grand_total).toFixed(1);
this.tip.set_values(x, y, title, percent + "%");
this.tip.show_tip();
}else{
let title = (this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels[i] : this.state.labels[i]) + ': ';
let percent = (this.state.sliceTotals[i] * 100 / this.state.grandTotal).toFixed(1);
this.tip.setValues(x, y, {name: title, value: percent + "%"});
this.tip.showTip();
} else {
transform(path,'translate3d(0,0,0)');
this.tip.hide_tip();
this.tip.hideTip();
path.style.fill = color;
}
}

bindTooltip() {
this.chartWrapper.addEventListener('mousemove', this.mouseMove);
this.chartWrapper.addEventListener('mouseleave', this.mouseLeave);
}

mouseMove(e){
const target = e.target;
let slices = this.components.get('pieSlices').store;
let prevIndex = this.curActiveSliceIndex;
let prevAcitve = this.curActiveSlice;
for(let i = 0; i < this.slices.length; i++){
if(target === this.slices[i]){
this.hoverSlice(prevAcitve,prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target,i,true,e);
break;
}
if(slices.includes(target)) {
let i = slices.indexOf(target);
this.hoverSlice(prevAcitve, prevIndex,false);
this.curActiveSlice = target;
this.curActiveSliceIndex = i;
this.hoverSlice(target, i, true, e);
} else {
this.mouseLeave();
}
}

mouseLeave(){
this.hoverSlice(this.curActiveSlice,this.curActiveSliceIndex,false);
}
bind_tooltip() {
this.draw_area.addEventListener('mousemove',this.mouseMove);
this.draw_area.addEventListener('mouseleave',this.mouseLeave);
}

show_summary() {
let x_values = this.formatted_labels && this.formatted_labels.length > 0
? this.formatted_labels : this.labels;
this.legend_totals.map((d, i) => {
const color = this.colors[i];

if(d) {
let stats = $.create('div', {
className: 'stats',
inside: this.stats_wrapper
});
stats.innerHTML = `<span class="indicator">
<i style="background-color:${color};"></i>
<span class="text-muted">${x_values[i]}:</span>
${d}
</span>`;
}
});
}
}

+ 0
- 35
src/js/charts/ScatterChart.js 查看文件

@@ -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() {}
}

+ 46
- 0
src/js/config.js 查看文件

@@ -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);
}

+ 366
- 0
src/js/objects/ChartComponents.js 查看文件

@@ -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);
}

+ 35
- 31
src/js/objects/SvgTip.js 查看文件

@@ -7,10 +7,10 @@ export default class SvgTip {
}) {
this.parent = parent;
this.colors = colors;
this.title_name = '';
this.title_value = '';
this.list_values = [];
this.title_value_first = 0;
this.titleName = '';
this.titleValue = '';
this.listValues = [];
this.titleValueFirst = 0;

this.x = 0;
this.y = 0;
@@ -22,16 +22,16 @@ export default class SvgTip {
}

setup() {
this.make_tooltip();
this.makeTooltip();
}

refresh() {
this.fill();
this.calc_position();
// this.show_tip();
this.calcPosition();
// this.showTip();
}

make_tooltip() {
makeTooltip() {
this.container = $.create('div', {
inside: this.parent,
className: 'graph-svg-tip comparison',
@@ -39,27 +39,30 @@ export default class SvgTip {
<ul class="data-point-list"></ul>
<div class="svg-pointer"></div>`
});
this.hide_tip();
this.hideTip();

this.title = this.container.querySelector('.title');
this.data_point_list = this.container.querySelector('.data-point-list');
this.dataPointList = this.container.querySelector('.data-point-list');

this.parent.addEventListener('mouseleave', () => {
this.hide_tip();
this.hideTip();
});
}

fill() {
let title;
if(this.title_value_first) {
title = `<strong>${this.title_value}</strong>${this.title_name}`;
if(this.index) {
this.container.setAttribute('data-point-index', this.index);
}
if(this.titleValueFirst) {
title = `<strong>${this.titleValue}</strong>${this.titleName}`;
} else {
title = `${this.title_name}<strong>${this.title_value}</strong>`;
title = `${this.titleName}<strong>${this.titleValue}</strong>`;
}
this.title.innerHTML = title;
this.data_point_list.innerHTML = '';
this.dataPointList.innerHTML = '';

this.list_values.map((set, i) => {
this.listValues.map((set, i) => {
const color = this.colors[i] || 'black';

let li = $.create('li', {
@@ -70,50 +73,51 @@ export default class SvgTip {
${set.title ? set.title : '' }`
});

this.data_point_list.appendChild(li);
this.dataPointList.appendChild(li);
});
}

calc_position() {
calcPosition() {
let width = this.container.offsetWidth;

this.top = this.y - this.container.offsetHeight;
this.left = this.x - width/2;
let max_left = this.parent.offsetWidth - width;
let maxLeft = this.parent.offsetWidth - width;

let pointer = this.container.querySelector('.svg-pointer');

if(this.left < 0) {
pointer.style.left = `calc(50% - ${-1 * this.left}px)`;
this.left = 0;
} else if(this.left > max_left) {
let delta = this.left - max_left;
let pointer_offset = `calc(50% + ${delta}px)`;
pointer.style.left = pointer_offset;
} else if(this.left > maxLeft) {
let delta = this.left - maxLeft;
let pointerOffset = `calc(50% + ${delta}px)`;
pointer.style.left = pointerOffset;

this.left = max_left;
this.left = maxLeft;
} else {
pointer.style.left = `50%`;
}
}

set_values(x, y, title_name = '', title_value = '', list_values = [], title_value_first = 0) {
this.title_name = title_name;
this.title_value = title_value;
this.list_values = list_values;
setValues(x, y, title = {}, listValues = [], index = -1) {
this.titleName = title.name;
this.titleValue = title.value;
this.listValues = listValues;
this.x = x;
this.y = y;
this.title_value_first = title_value_first;
this.titleValueFirst = title.valueFirst || 0;
this.index = index;
this.refresh();
}

hide_tip() {
hideTip() {
this.container.style.top = '0px';
this.container.style.left = '0px';
this.container.style.opacity = '0';
}

show_tip() {
showTip() {
this.container.style.top = this.top + 'px';
this.container.style.left = this.left + 'px';
this.container.style.opacity = '1';


+ 103
- 56
src/js/utils/animate.js 查看文件

@@ -1,58 +1,105 @@
import { getBarHeightAndYAttr } from './draw-utils';

export function getAnimXLine() {}

export function getAnimYLine() {}

export var Animator = (function() {
var Animator = function(totalHeight, totalWidth, zeroLine, avgUnitWidth) {
// constants
this.totalHeight = totalHeight;
this.totalWidth = totalWidth;

// changeables
this.avgUnitWidth = avgUnitWidth;
this.zeroLine = zeroLine;
};

Animator.prototype = {
bar: function(barObj, x, yTop, index, noOfDatasets) {
let start = x - this.avgUnitWidth/4;
let width = (this.avgUnitWidth/2)/noOfDatasets;
let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight);

x = start + (width * index);

return [barObj, {width: width, height: height, x: x, y: y}, 350, "easein"];
// bar.animate({height: args.newHeight, y: yTop}, 350, mina.easein);
},

dot: function(dotObj, x, yTop) {
return [dotObj, {cx: x, cy: yTop}, 350, "easein"];
// dot.animate({cy: yTop}, 350, mina.easein);
},

path: function(d, pathStr) {
let pathComponents = [];
const animPath = [{unit: d.path, object: d, key: 'path'}, {d:"M"+pathStr}, 350, "easein"];
pathComponents.push(animPath);

if(d.regionPath) {
let regStartPt = `0,${this.zeroLine}L`;
let regEndPt = `L${this.totalWidth}, ${this.zeroLine}`;

const animRegion = [
{unit: d.regionPath, object: d, key: 'regionPath'},
{d:"M" + regStartPt + pathStr + regEndPt},
350,
"easein"
];
pathComponents.push(animRegion);
}

return pathComponents;
},
};

return Animator;
})();
export const UNIT_ANIM_DUR = 350;
export const PATH_ANIM_DUR = 350;
export const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
export const REPLACE_ALL_NEW_DUR = 250;

export const STD_EASING = 'easein';

export function translate(unit, oldCoord, newCoord, duration) {
let old = typeof oldCoord === 'string' ? oldCoord : oldCoord.join(', ');
return [
unit,
{transform: newCoord.join(', ')},
duration,
STD_EASING,
"translate",
{transform: old}
];
}

export function translateVertLine(xLine, newX, oldX) {
return translate(xLine, [oldX, 0], [newX, 0], MARKER_LINE_ANIM_DUR);
}

export function translateHoriLine(yLine, newY, oldY) {
return translate(yLine, [0, oldY], [0, newY], MARKER_LINE_ANIM_DUR);
}

export function animateRegion(rectGroup, newY1, newY2, oldY2) {
let newHeight = newY1 - newY2;
let rect = rectGroup.childNodes[0];
let width = rect.getAttribute("width");
let rectAnim = [
rect,
{ height: newHeight, 'stroke-dasharray': `${width}, ${newHeight}` },
MARKER_LINE_ANIM_DUR,
STD_EASING
]

let groupAnim = translate(rectGroup, [0, oldY2], [0, newY2], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim];
}

export function animateBar(bar, x, yTop, width, offset=0, index=0, meta={}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
y -= offset;
if(bar.nodeName !== 'rect') {
let rect = bar.childNodes[0];
let rectAnim = [
rect,
{width: width, height: height},
UNIT_ANIM_DUR,
STD_EASING
]

let oldCoordStr = bar.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(bar, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [rectAnim, groupAnim];
} else {
return [[bar, {width: width, height: height, x: x, y: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// bar.animate({height: args.newHeight, y: yTop}, UNIT_ANIM_DUR, mina.easein);
}

export function animateDot(dot, x, y) {
if(dot.nodeName !== 'circle') {
let oldCoordStr = dot.getAttribute("transform").split("(")[1].slice(0, -1);
let groupAnim = translate(dot, oldCoordStr, [x, y], MARKER_LINE_ANIM_DUR);
return [groupAnim];
} else {
return [[dot, {cx: x, cy: y}, UNIT_ANIM_DUR, STD_EASING]];
}
// dot.animate({cy: yTop}, UNIT_ANIM_DUR, mina.easein);
}

export function animatePath(paths, newXList, newYList, zeroLine) {
let pathComponents = [];

let pointsStr = newYList.map((y, i) => (newXList[i] + ',' + y));
let pathStr = pointsStr.join("L");

const animPath = [paths.path, {d:"M"+pathStr}, PATH_ANIM_DUR, STD_EASING];
pathComponents.push(animPath);

if(paths.region) {
let regStartPt = `${newXList[0]},${zeroLine}L`;
let regEndPt = `L${newXList.slice(-1)[0]}, ${zeroLine}`;

const animRegion = [
paths.region,
{d:"M" + regStartPt + pathStr + regEndPt},
PATH_ANIM_DUR,
STD_EASING
];
pathComponents.push(animRegion);
}

return pathComponents;
}

export function animatePathStr(oldPath, pathStr) {
return [oldPath, {d: pathStr}, UNIT_ANIM_DUR, STD_EASING];
}


+ 28
- 13
src/js/utils/animation.js 查看文件

@@ -1,5 +1,7 @@
// Leveraging SMIL Animations

import { REPLACE_ALL_NEW_DUR } from './animate';

const EASING = {
ease: "0.25 0.1 0.25 1",
linear: "0 0 1 1",
@@ -9,7 +11,7 @@ const EASING = {
easeinout: "0.42 0 0.58 1"
};

function animateSVG(element, props, dur, easingType="linear", type=undefined, oldValues={}) {
function animateSVGElement(element, props, dur, easingType="linear", type=undefined, oldValues={}) {

let animElement = element.cloneNode(true);
let newElement = element.cloneNode(true);
@@ -65,29 +67,23 @@ export function transform(element, style) { // eslint-disable-line no-unused-var
element.style.oTransform = style;
}

export function runSVGAnimation(svgContainer, elements) {
function animateSVG(svgContainer, elements) {
let newElements = [];
let animElements = [];

elements.map(element => {
let obj = element[0];
let parent = obj.unit.parentNode;
let unit = element[0];
let parent = unit.parentNode;

let animElement, newElement;

element[0] = obj.unit;
[animElement, newElement] = animateSVG(...element);
element[0] = unit;
[animElement, newElement] = animateSVGElement(...element);

newElements.push(newElement);
animElements.push([animElement, parent]);

parent.replaceChild(animElement, obj.unit);

if(obj.array) {
obj.array[obj.index] = newElement;
} else {
obj.object[obj.key] = newElement;
}
parent.replaceChild(animElement, unit);
});

let animSvg = svgContainer.cloneNode(true);
@@ -99,3 +95,22 @@ export function runSVGAnimation(svgContainer, elements) {

return animSvg;
}

export function runSMILAnimation(parent, svgElement, elementsToAnimate) {
if(elementsToAnimate.length === 0) return;

let animSvgElement = animateSVG(svgElement, elementsToAnimate);
if(svgElement.parentNode == parent) {
parent.removeChild(svgElement);
parent.appendChild(animSvgElement);

}

// Replace the new svgElement (data has already been replaced)
setTimeout(() => {
if(animSvgElement.parentNode == parent) {
parent.removeChild(animSvgElement);
parent.appendChild(svgElement);
}
}, REPLACE_ALL_NEW_DUR);
}

+ 124
- 0
src/js/utils/axis-chart-utils.js 查看文件

@@ -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;
}

+ 1
- 1
src/js/utils/colors.js 查看文件

@@ -16,7 +16,7 @@ const PRESET_COLOR_MAP = {
};

export const DEFAULT_COLORS = ['light-blue', 'blue', 'violet', 'red', 'orange',
'yellow', 'green', 'light-green', 'purple', 'magenta'];
'yellow', 'green', 'light-green', 'purple', 'magenta', 'light-grey', 'dark-grey'];

function limitColor(r){
if (r > 255) return 255;


+ 23
- 0
src/js/utils/constants.js 查看文件

@@ -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;

+ 6
- 1
src/js/utils/date-utils.js 查看文件

@@ -31,4 +31,9 @@ export function addDays(date, numberOfDays) {
date.setDate(date.getDate() + numberOfDays);
}

// export function getMonthName() {}
export function getMonthName(i) {
let monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
return monthNames[i];
}

+ 7
- 7
src/js/utils/dom.js 查看文件

@@ -43,7 +43,7 @@ $.create = (tag, o) => {
return element;
};

export function offset(element) {
export function getOffset(element) {
let rect = element.getBoundingClientRect();
return {
// https://stackoverflow.com/a/7436602/6495043
@@ -74,7 +74,7 @@ export function getElementContentWidth(element) {
return element.clientWidth - padding;
}

$.bind = (element, o) => {
export function bind(element, o){
if (element) {
for (var event in o) {
var callback = o[event];
@@ -84,9 +84,9 @@ $.bind = (element, o) => {
});
}
}
};
}

$.unbind = (element, o) => {
export function unbind(element, o){
if (element) {
for (var event in o) {
var callback = o[event];
@@ -96,9 +96,9 @@ $.unbind = (element, o) => {
});
}
}
};
}

$.fire = (target, type, properties) => {
export function fire(target, type, properties) {
var evt = document.createEvent("HTMLEvents");

evt.initEvent(type, true, true );
@@ -108,4 +108,4 @@ $.fire = (target, type, properties) => {
}

return target.dispatchEvent(evt);
};
}

+ 15
- 12
src/js/utils/draw-utils.js 查看文件

@@ -1,23 +1,26 @@
export function getBarHeightAndYAttr(yTop, zeroLine, totalHeight) {
import { fillArray } from './helpers';

export function getBarHeightAndYAttr(yTop, zeroLine) {
let height, y;
if (yTop <= zeroLine) {
height = zeroLine - yTop;
y = yTop;

// In case of invisible bars
if(height === 0) {
height = totalHeight * 0.01;
y -= height;
}
} else {
height = yTop - zeroLine;
y = zeroLine;

// In case of invisible bars
if(height === 0) {
height = totalHeight * 0.01;
}
}

return [height, y];
}

export function equilizeNoOfElements(array1, array2,
extraCount = array2.length - array1.length) {

// Doesn't work if either has zero elements.
if(extraCount > 0) {
array1 = fillArray(array1, extraCount);
} else {
array2 = fillArray(array2, extraCount);
}
return [array1, array2];
}

+ 372
- 71
src/js/utils/draw.js 查看文件

@@ -1,12 +1,19 @@
import { getBarHeightAndYAttr } from './draw-utils';
import { getStringWidth } from './helpers';
import { STD_EASING, UNIT_ANIM_DUR, MARKER_LINE_ANIM_DUR, PATH_ANIM_DUR } from './animate';
import { DOT_OVERLAY_SIZE_INCR } from './constants';

// Constants used
const AXIS_TICK_LENGTH = 6;
const LABEL_MARGIN = 4;
export const FONT_SIZE = 10;
const BASE_LINE_COLOR = '#dadada';
const BASE_BG_COLOR = '#F7FAFC';

function $(expr, con) {
return typeof expr === "string"? (con || document).querySelector(expr) : expr || null;
}

function createSVG(tag, o) {
export function createSVG(tag, o) {
var element = document.createElementNS("http://www.w3.org/2000/svg", tag);

for (var i in o) {
@@ -82,6 +89,14 @@ export function makeSVGGroup(parent, className, transform='') {
});
}

export function wrapInSVGGroup(elements, className='') {
let g = createSVG('g', {
className: className
});
elements.forEach(e => g.appendChild(e));
return g;
}

export function makePath(pathStr, className='', stroke='none', fill='none') {
return createSVG('path', {
className: className,
@@ -93,8 +108,18 @@ export function makePath(pathStr, className='', stroke='none', fill='none') {
});
}

export function makeArcPathStr(startPosition, endPosition, center, radius, clockWise=1){
let [arcStartX, arcStartY] = [center.x + startPosition.x, center.y + startPosition.y];
let [arcEndX, arcEndY] = [center.x + endPosition.x, center.y + endPosition.y];

return `M${center.x} ${center.y}
L${arcStartX} ${arcStartY}
A ${radius} ${radius} 0 0 ${clockWise ? 1 : 0}
${arcEndX} ${arcEndY} z`;
}

export function makeGradient(svgDefElem, color, lighter = false) {
let gradientId ='path-fill-gradient' + '-' + color;
let gradientId ='path-fill-gradient' + '-' + color + '-' +(lighter ? 'lighter' : 'default');
let gradientDef = renderVerticalGradient(svgDefElem, gradientId);
let opacities = [1, 0.6, 0.2];
if(lighter) {
@@ -130,109 +155,385 @@ export function makeText(className, x, y, content) {
className: className,
x: x,
y: y,
dy: '.32em',
dy: (FONT_SIZE / 2) + 'px',
'font-size': FONT_SIZE + 'px',
innerHTML: content
});
}

export function makeXLine(height, textStartAt, point, labelClass, axisLineClass, xPos) {
let line = createSVG('line', {
function makeVertLine(x, label, y1, y2, options={}) {
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
let l = createSVG('line', {
className: 'line-vertical ' + options.className,
x1: 0,
x2: 0,
y1: 0,
y2: height
y1: y1,
y2: y2,
styles: {
stroke: options.stroke
}
});

let text = createSVG('text', {
className: labelClass,
x: 0,
y: textStartAt,
dy: '.71em',
innerHTML: point
y: y1 > y2 ? y1 + LABEL_MARGIN : y1 - LABEL_MARGIN - FONT_SIZE,
dy: FONT_SIZE + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label + ""
});

let xLine = createSVG('g', {
className: `tick ${axisLineClass}`,
transform: `translate(${ xPos }, 0)`
let line = createSVG('g', {
transform: `translate(${ x }, 0)`
});

xLine.appendChild(line);
xLine.appendChild(text);
line.appendChild(l);
line.appendChild(text);

return xLine;
return line;
}

export function makeYLine(startAt, width, textEndAt, point, labelClass, axisLineClass, yPos, darker=false, lineType="") {
let line = createSVG('line', {
className: lineType === "dashed" ? "dashed": "",
x1: startAt,
x2: width,
function makeHoriLine(y, label, x1, x2, options={}) {
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.lineType) options.lineType = '';
let className = 'line-horizontal ' + options.className +
(options.lineType === "dashed" ? "dashed": "");

let l = createSVG('line', {
className: className,
x1: x1,
x2: x2,
y1: 0,
y2: 0
y2: 0,
styles: {
stroke: options.stroke
}
});

let text = createSVG('text', {
className: labelClass,
x: textEndAt,
x: x1 < x2 ? x1 - LABEL_MARGIN : x1 + LABEL_MARGIN,
y: 0,
dy: '.32em',
innerHTML: point+""
dy: (FONT_SIZE / 2 - 2) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': x1 < x2 ? 'end' : 'start',
innerHTML: label+""
});

let yLine = createSVG('g', {
className: `tick ${axisLineClass}`,
transform: `translate(0, ${yPos})`,
let line = createSVG('g', {
transform: `translate(0, ${y})`,
'stroke-opacity': 1
});

if(darker) {
if(text === 0 || text === '0') {
line.style.stroke = "rgba(27, 31, 35, 0.6)";
}

yLine.appendChild(line);
yLine.appendChild(text);
line.appendChild(l);
line.appendChild(text);

return yLine;
return line;
}

export var UnitRenderer = (function() {
var UnitRenderer = function(totalHeight, zeroLine, avgUnitWidth) {
this.totalHeight = totalHeight;
this.zeroLine = zeroLine;
this.avgUnitWidth = avgUnitWidth;
};
export function yLine(y, label, width, options={}) {
if(!options.pos) options.pos = 'left';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

let x1 = -1 * AXIS_TICK_LENGTH;
let x2 = options.mode === 'span' ? width + AXIS_TICK_LENGTH : 0;

if(options.mode === 'tick' && options.pos === 'right') {
x1 = width + AXIS_TICK_LENGTH
x2 = width;
}

let offset = options.pos === 'left' ? -1 * options.offset : options.offset;

UnitRenderer.prototype = {
bar: function (x, yTop, args, color, index, datasetIndex, noOfDatasets) {
let totalWidth = this.avgUnitWidth - args.spaceWidth;
let startX = x - totalWidth/2;

let width = totalWidth / noOfDatasets;
let currentX = startX + width * datasetIndex;

let [height, y] = getBarHeightAndYAttr(yTop, this.zeroLine, this.totalHeight);

return createSVG('rect', {
className: `bar mini`,
style: `fill: ${color}`,
'data-point-index': index,
x: currentX,
y: y,
width: width,
height: height
});
x1 += options.offset;
x2 += options.offset;

return makeHoriLine(y, label, x1, x2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}

export function xLine(x, label, height, options={}) {
if(!options.pos) options.pos = 'bottom';
if(!options.offset) options.offset = 0;
if(!options.mode) options.mode = 'span';
if(!options.stroke) options.stroke = BASE_LINE_COLOR;
if(!options.className) options.className = '';

// Draw X axis line in span/tick mode with optional label
// y2(span)
// |
// |
// x line |
// |
// |
// ---------------------+-- y2(tick)
// |
// y1

let y1 = height + AXIS_TICK_LENGTH;
let y2 = options.mode === 'span' ? -1 * AXIS_TICK_LENGTH : height;

if(options.mode === 'tick' && options.pos === 'top') {
// top axis ticks
y1 = -1 * AXIS_TICK_LENGTH;
y2 = 0;
}

return makeVertLine(x, label, y1, y2, {
stroke: options.stroke,
className: options.className,
lineType: options.lineType
});
}

export function yMarker(y, label, width, options={}) {
let labelSvg = createSVG('text', {
className: 'chart-label',
x: width - getStringWidth(label, 5) - LABEL_MARGIN,
y: 0,
dy: (FONT_SIZE / -2) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'start',
innerHTML: label+""
});

let line = makeHoriLine(y, '', 0, width, {
stroke: options.stroke || BASE_LINE_COLOR,
className: options.className || '',
lineType: options.lineType
});

line.appendChild(labelSvg);

return line;
}

export function yRegion(y1, y2, width, label) {
// return a group
let height = y1 - y2;

let rect = createSVG('rect', {
className: `bar mini`, // remove class
styles: {
fill: `rgba(228, 234, 239, 0.49)`,
stroke: BASE_LINE_COLOR,
'stroke-dasharray': `${width}, ${height}`
},
// 'data-point-index': index,
x: 0,
y: 0,
width: width,
height: height
});

let labelSvg = createSVG('text', {
className: 'chart-label',
x: width - getStringWidth(label+"", 4.5) - LABEL_MARGIN,
y: 0,
dy: (FONT_SIZE / -2) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'start',
innerHTML: label+""
});

dot: function(x, y, args, color, index) {
return createSVG('circle', {
style: `fill: ${color}`,
'data-point-index': index,
cx: x,
cy: y,
r: args.radius
});
let region = createSVG('g', {
transform: `translate(0, ${y2})`
});

region.appendChild(rect);
region.appendChild(labelSvg);

return region;
}

export function datasetBar(x, yTop, width, color, label='', index=0, offset=0, meta={}) {
let [height, y] = getBarHeightAndYAttr(yTop, meta.zeroLine);
y -= offset;

let rect = createSVG('rect', {
className: `bar mini`,
style: `fill: ${color}`,
'data-point-index': index,
x: x,
y: y,
width: width,
height: height || meta.minHeight // TODO: correct y for positive min height
});

label += "";

if(!label && !label.length) {
return rect;
} else {
rect.setAttribute('y', 0);
rect.setAttribute('x', 0);
let text = createSVG('text', {
className: 'data-point-value',
x: width/2,
y: 0,
dy: (FONT_SIZE / 2 * -1) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});

let group = createSVG('g', {
'data-point-index': index,
transform: `translate(${x}, ${y})`
});
group.appendChild(rect);
group.appendChild(text);

return group;
}
}

export function datasetDot(x, y, radius, color, label='', index=0, meta={}) {
let dot = createSVG('circle', {
style: `fill: ${color}`,
'data-point-index': index,
cx: x,
cy: y,
r: radius
});

label += "";

if(!label && !label.length) {
return dot;
} else {
dot.setAttribute('cy', 0);
dot.setAttribute('cx', 0);

let text = createSVG('text', {
className: 'data-point-value',
x: 0,
y: 0,
dy: (FONT_SIZE / 2 * -1 - radius) + 'px',
'font-size': FONT_SIZE + 'px',
'text-anchor': 'middle',
innerHTML: label
});

let group = createSVG('g', {
'data-point-index': index,
transform: `translate(${x}, ${y})`
});
group.appendChild(dot);
group.appendChild(text);

return group;
}
}

export function getPaths(xList, yList, color, options={}, meta={}) {
let pointsList = yList.map((y, i) => (xList[i] + ',' + y));
let pointsStr = pointsList.join("L");
let path = makePath("M"+pointsStr, 'line-graph-path', color);

// HeatLine
if(options.heatline) {
let gradient_id = makeGradient(meta.svgDefs, color);
path.style.stroke = `url(#${gradient_id})`;
}

let paths = {
path: path
}

// Region
if(options.regionFill) {
let gradient_id_region = makeGradient(meta.svgDefs, color, true);

// TODO: use zeroLine OR minimum
let pathStr = "M" + `${xList[0]},${meta.zeroLine}L` + pointsStr + `L${xList.slice(-1)[0]},${meta.zeroLine}`;
paths.region = makePath(pathStr, `region-fill`, 'none', `url(#${gradient_id_region})`);
}

return paths;
}

export let makeOverlay = {
'bar': (unit) => {
let transformValue;
if(unit.nodeName !== 'rect') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
};
let overlay = unit.cloneNode();
overlay.style.fill = '#000000';
overlay.style.opacity = '0.4';

if(transformValue) {
overlay.setAttribute('transform', transformValue);
}
return overlay;
},

'dot': (unit) => {
let transformValue;
if(unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let overlay = unit.cloneNode();
let radius = unit.getAttribute('r');
let fill = unit.getAttribute('fill');
overlay.setAttribute('r', parseInt(radius) + DOT_OVERLAY_SIZE_INCR);
overlay.setAttribute('fill', fill);
overlay.style.opacity = '0.6';

if(transformValue) {
overlay.setAttribute('transform', transformValue);
}
return overlay;
}
}

export let updateOverlay = {
'bar': (unit, overlay) => {
let transformValue;
if(unit.nodeName !== 'rect') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let attributes = ['x', 'y', 'width', 'height'];
Object.values(unit.attributes)
.filter(attr => attributes.includes(attr.name) && attr.specified)
.map(attr => {
overlay.setAttribute(attr.name, attr.nodeValue);
});

if(transformValue) {
overlay.setAttribute('transform', transformValue);
}
},

'dot': (unit, overlay) => {
let transformValue;
if(unit.nodeName !== 'circle') {
transformValue = unit.getAttribute('transform');
unit = unit.childNodes[0];
}
let attributes = ['cx', 'cy'];
Object.values(unit.attributes)
.filter(attr => attributes.includes(attr.name) && attr.specified)
.map(attr => {
overlay.setAttribute(attr.name, attr.nodeValue);
});

if(transformValue) {
overlay.setAttribute('transform', transformValue);
}
}
}

return UnitRenderer;
})();

+ 38
- 0
src/js/utils/helpers.js 查看文件

@@ -1,3 +1,5 @@
import { ANGLE_RATIO } from './constants';

/**
* Returns the value of a number upto 2 decimal places.
* @param {Number} d Any number
@@ -37,6 +39,22 @@ export function shuffle(array) {
return array;
}

/**
* Fill an array with extra points
* @param {Array} array Array
* @param {Number} count number of filler elements
* @param {Object} element element to fill with
* @param {Boolean} start fill at start?
*/
export function fillArray(array, count, element, start=false) {
if(!element) {
element = start ? array[0] : array[array.length - 1];
}
let fillerArray = new Array(Math.abs(count)).fill(element);
array = start ? fillerArray.concat(array) : array.concat(fillerArray);
return array;
}

/**
* Returns pixel width of string.
* @param {String} string
@@ -45,3 +63,23 @@ export function shuffle(array) {
export function getStringWidth(string, charWidth) {
return (string+"").length * charWidth;
}

export function bindChange(obj, getFn, setFn) {
return new Proxy(obj, {
set: function(target, prop, value) {
setFn();
return Reflect.set(target, prop, value);
},
get: function(target, prop, value) {
getFn();
return Reflect.get(target, prop);
}
});
}

export function getPositionByAngle(angle, radius) {
return {
x:Math.sin(angle * ANGLE_RATIO) * radius,
y:Math.cos(angle * ANGLE_RATIO) * radius,
};
}

+ 56
- 9
src/js/utils/intervals.js 查看文件

@@ -1,3 +1,5 @@
import { floatTwo } from './helpers';

function normalize(x) {
// Calculates mantissa and exponent of a number
// Returns normalized number and exponent
@@ -21,7 +23,7 @@ function normalize(x) {
return [sig * man, exp];
}

function getRangeIntervals(max, min=0) {
function getChartRangeIntervals(max, min=0) {
let upperBound = Math.ceil(max);
let lowerBound = Math.floor(min);
let range = upperBound - lowerBound;
@@ -59,19 +61,19 @@ function getRangeIntervals(max, min=0) {
return intervals;
}

function getIntervals(maxValue, minValue=0) {
function getChartIntervals(maxValue, minValue=0) {
let [normalMaxValue, exponent] = normalize(maxValue);
let normalMinValue = minValue ? minValue/Math.pow(10, exponent): 0;

// Allow only 7 significant digits
normalMaxValue = normalMaxValue.toFixed(6);

let intervals = getRangeIntervals(normalMaxValue, normalMinValue);
let intervals = getChartRangeIntervals(normalMaxValue, normalMinValue);
intervals = intervals.map(value => value * Math.pow(10, exponent));
return intervals;
}

export function calcIntervals(values, withMinimum=false) {
export function calcChartIntervals(values, withMinimum=false) {
//*** Where the magic happens ***

// Calculates best-fit y intervals from given values
@@ -84,7 +86,7 @@ export function calcIntervals(values, withMinimum=false) {
let exponent = 0, intervals = []; // eslint-disable-line no-unused-vars

function getPositiveFirstIntervals(maxValue, absMinValue) {
let intervals = getIntervals(maxValue);
let intervals = getChartIntervals(maxValue);

let intervalSize = intervals[1] - intervals[0];

@@ -102,9 +104,9 @@ export function calcIntervals(values, withMinimum=false) {
if(maxValue >= 0 && minValue >= 0) {
exponent = normalize(maxValue)[1];
if(!withMinimum) {
intervals = getIntervals(maxValue);
intervals = getChartIntervals(maxValue);
} else {
intervals = getIntervals(maxValue, minValue);
intervals = getChartIntervals(maxValue, minValue);
}
}

@@ -142,9 +144,9 @@ export function calcIntervals(values, withMinimum=false) {

exponent = normalize(pseudoMaxValue)[1];
if(!withMinimum) {
intervals = getIntervals(pseudoMaxValue);
intervals = getChartIntervals(pseudoMaxValue);
} else {
intervals = getIntervals(pseudoMaxValue, pseudoMinValue);
intervals = getChartIntervals(pseudoMaxValue, pseudoMinValue);
}

intervals = intervals.reverse().map(d => d * (-1));
@@ -153,6 +155,51 @@ export function calcIntervals(values, withMinimum=false) {
return intervals;
}

export function getZeroIndex(yPts) {
let zeroIndex;
let interval = getIntervalSize(yPts);
if(yPts.indexOf(0) >= 0) {
// the range has a given zero
// zero-line on the chart
zeroIndex = yPts.indexOf(0);
} else if(yPts[0] > 0) {
// Minimum value is positive
// zero-line is off the chart: below
let min = yPts[0];
zeroIndex = (-1) * min / interval;
} else {
// Maximum value is negative
// zero-line is off the chart: above
let max = yPts[yPts.length - 1];
zeroIndex = (-1) * max / interval + (yPts.length - 1);
}
return zeroIndex;
}

export function getRealIntervals(max, noOfIntervals, min = 0, asc = 1) {
let range = max - min;
let part = range * 1.0 / noOfIntervals;
let intervals = [];

for(var i = 0; i <= noOfIntervals; i++) {
intervals.push(min + part * i);
}

return asc ? intervals : intervals.reverse();
}

export function getIntervalSize(orderedArray) {
return orderedArray[1] - orderedArray[0];
}

export function getValueRange(orderedArray) {
return orderedArray[orderedArray.length-1] - orderedArray[0];
}

export function scale(val, yAxis) {
return floatTwo(yAxis.zeroLine - val * yAxis.scaleMultiplier)
}

export function calcDistribution(values, distributionSize) {
// Assume non-negative values,
// implying distribution minimum at zero


+ 31
- 10
src/scss/charts.scss 查看文件

@@ -28,8 +28,13 @@
}
.graph-stats-container {
display: flex;
justify-content: space-around;
padding-top: 10px;
justify-content: space-between;
padding: 10px;
&:before,
&:after {
content: '';
display: block;
}
.stats {
padding-bottom: 15px;
}
@@ -51,8 +56,8 @@
}
}
.axis, .chart-label {
font-size: 11px;
fill: #555b51;
// temp commented
line {
stroke: #dadada;
}
@@ -62,11 +67,26 @@
margin-bottom: 0px;
}
}
.data-points {
.dataset-units {
circle {
stroke: #fff;
stroke-width: 2;
}

// temp
path {
fill: none;
stroke-opacity: 1;
stroke-width: 2px;
}
}
.multiaxis-chart {
.line-horizontal, .y-axis-guide {
stroke-width: 2px;
}
}
.dataset-path {
stroke-width: 2px;
}
.path-group {
path {
@@ -78,17 +98,18 @@
line.dashed {
stroke-dasharray: 5,3;
}
.tick {
&.x-axis-label {
display: block;
}
.axis-line {
// &.x-axis-label {
// display: block;
// }
// TODO: hack dy attr to be settable via styles
.specific-value {
text-anchor: start;
}
.y-value-text {
.y-line {
text-anchor: end;
}
.x-value-text {
.x-line {
text-anchor: middle;
}
}


Loading…
取消
儲存