add uglify, modularize all objects
This commit is contained in:
parent
268a412e9b
commit
8e75d8e2dd
2777
dist/frappe-charts.min.js
vendored
2777
dist/frappe-charts.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/frappe-charts.min.js.map
vendored
Normal file
1
dist/frappe-charts.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -156,8 +156,6 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<script src="../dist/frappe-charts.min.js"></script>
|
<script src="../dist/frappe-charts.min.js"></script>
|
||||||
<!--<script src="../src/scripts/charts.js"></script>-->
|
|
||||||
<!--<script src="../src/charts.js"></script>-->
|
|
||||||
<script src="assets/js/index.js"></script>
|
<script src="assets/js/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -942,6 +942,12 @@
|
|||||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"commander": {
|
||||||
|
"version": "2.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
|
||||||
|
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -2260,6 +2266,15 @@
|
|||||||
"resolve": "1.5.0"
|
"resolve": "1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"rollup-plugin-uglify": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-Z7N60e/a+9g69MNrQMGJ7khmyWk=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"uglify-js": "3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"rollup-pluginutils": {
|
"rollup-pluginutils": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz",
|
||||||
@ -2519,6 +2534,24 @@
|
|||||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"uglify-js": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-tSqlO7/GZHAVSw6mbtJt2kz0ZcUrKUH7Xg92o52aE+gL0r6cXiASZY4dpHqQ7RVGXmoQuPA2qAkG4TkP59f8XA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"commander": "2.11.0",
|
||||||
|
"source-map": "0.6.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
"rollup": "^0.50.0",
|
"rollup": "^0.50.0",
|
||||||
"rollup-plugin-babel": "^3.0.2",
|
"rollup-plugin-babel": "^3.0.2",
|
||||||
"rollup-plugin-eslint": "^4.0.0",
|
"rollup-plugin-eslint": "^4.0.0",
|
||||||
"rollup-plugin-node-resolve": "^3.0.0"
|
"rollup-plugin-node-resolve": "^3.0.0",
|
||||||
|
"rollup-plugin-uglify": "^2.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
// Rollup plugins
|
// Rollup plugins
|
||||||
import resolve from 'rollup-plugin-node-resolve';
|
|
||||||
import babel from 'rollup-plugin-babel';
|
import babel from 'rollup-plugin-babel';
|
||||||
import eslint from 'rollup-plugin-eslint';
|
import eslint from 'rollup-plugin-eslint';
|
||||||
|
import uglify from 'rollup-plugin-uglify';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: 'src/charts.js',
|
input: 'src/scripts/charts.js',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/frappe-charts.min.js',
|
file: 'dist/frappe-charts.min.js',
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
},
|
},
|
||||||
name: 'Chart',
|
name: 'Chart',
|
||||||
sourcemap: 'inline',
|
sourcemap: 'true',
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
eslint(),
|
||||||
eslint(),
|
babel({
|
||||||
babel({
|
exclude: 'node_modules/**',
|
||||||
exclude: 'node_modules/**',
|
}),
|
||||||
}),
|
uglify()
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
20
src/scripts/charts.js
Normal file
20
src/scripts/charts.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import BarChart from './charts/BarChart';
|
||||||
|
import LineChart from './charts/LineChart';
|
||||||
|
import PercentageChart from './charts/PercentageChart';
|
||||||
|
import Heatmap from './charts/Heatmap';
|
||||||
|
|
||||||
|
export default class Chart {
|
||||||
|
constructor(args) {
|
||||||
|
if(args.type === 'line') {
|
||||||
|
return new LineChart(arguments[0]);
|
||||||
|
} else if(args.type === 'bar') {
|
||||||
|
return new BarChart(arguments[0]);
|
||||||
|
} else if(args.type === 'percentage') {
|
||||||
|
return new PercentageChart(arguments[0]);
|
||||||
|
} else if(args.type === 'heatmap') {
|
||||||
|
return new Heatmap(arguments[0]);
|
||||||
|
} else {
|
||||||
|
return new LineChart(arguments[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,272 +1,8 @@
|
|||||||
import $ from './dom';
|
import $ from '../helpers/dom';
|
||||||
import { float_2, arrays_equal } from './utils';
|
import { float_2, arrays_equal } from '../helpers/utils';
|
||||||
|
import BaseChart from './BaseChart';
|
||||||
|
|
||||||
export default class Chart {
|
export default class AxisChart extends BaseChart {
|
||||||
constructor({
|
|
||||||
parent = "",
|
|
||||||
height = 240,
|
|
||||||
|
|
||||||
title = '', subtitle = '',
|
|
||||||
|
|
||||||
data = {},
|
|
||||||
format_lambdas = {},
|
|
||||||
|
|
||||||
summary = [],
|
|
||||||
|
|
||||||
is_navigable = 0,
|
|
||||||
|
|
||||||
type = ''
|
|
||||||
}) {
|
|
||||||
if(Object.getPrototypeOf(this) === Chart.prototype) {
|
|
||||||
if(type === 'line') {
|
|
||||||
return new LineChart(arguments[0]);
|
|
||||||
} else if(type === 'bar') {
|
|
||||||
return new BarChart(arguments[0]);
|
|
||||||
} else if(type === 'percentage') {
|
|
||||||
return new PercentageChart(arguments[0]);
|
|
||||||
} else if(type === 'heatmap') {
|
|
||||||
return new HeatMap(arguments[0]);
|
|
||||||
} else {
|
|
||||||
return new LineChart(arguments[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.raw_chart_args = arguments[0];
|
|
||||||
|
|
||||||
this.parent = document.querySelector(parent);
|
|
||||||
this.title = title;
|
|
||||||
this.subtitle = subtitle;
|
|
||||||
|
|
||||||
this.data = data;
|
|
||||||
this.format_lambdas = format_lambdas;
|
|
||||||
|
|
||||||
this.specific_values = data.specific_values || [];
|
|
||||||
this.summary = summary;
|
|
||||||
|
|
||||||
this.is_navigable = is_navigable;
|
|
||||||
if(this.is_navigable) {
|
|
||||||
this.current_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart_types = ['line', 'bar', 'percentage', 'heatmap'];
|
|
||||||
|
|
||||||
this.set_margins(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_different_chart(type) {
|
|
||||||
if(!this.chart_types.includes(type)) {
|
|
||||||
console.error(`'${type}' is not a valid chart type.`);
|
|
||||||
}
|
|
||||||
if(type === this.type) return;
|
|
||||||
|
|
||||||
// Only across compatible types
|
|
||||||
let compatible_types = {
|
|
||||||
bar: ['line', 'percentage'],
|
|
||||||
line: ['bar', 'percentage'],
|
|
||||||
percentage: ['bar', 'line'],
|
|
||||||
heatmap: []
|
|
||||||
};
|
|
||||||
|
|
||||||
if(!compatible_types[this.type].includes(type)) {
|
|
||||||
console.error(`'${this.type}' chart cannot be converted to a '${type}' chart.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
data: this.raw_chart_args.data,
|
|
||||||
type: type,
|
|
||||||
height: this.raw_chart_args.height
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
set_margins(height) {
|
|
||||||
this.base_height = height;
|
|
||||||
this.height = height - 40;
|
|
||||||
this.translate_x = 60;
|
|
||||||
this.translate_y = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.bind_window_events();
|
|
||||||
this.refresh(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bind_window_events() {
|
|
||||||
window.addEventListener('resize', () => this.refresh());
|
|
||||||
window.addEventListener('orientationchange', () => this.refresh());
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh(init=false) {
|
|
||||||
this.setup_base_values();
|
|
||||||
this.set_width();
|
|
||||||
|
|
||||||
this.setup_container();
|
|
||||||
this.setup_components();
|
|
||||||
|
|
||||||
this.setup_values();
|
|
||||||
this.setup_utils();
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set_width() {
|
|
||||||
let special_values_width = 0;
|
|
||||||
this.specific_values.map(val => {
|
|
||||||
if(this.get_strwidth(val.title) > special_values_width) {
|
|
||||||
special_values_width = this.get_strwidth(val.title) - 40;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.base_width = this.parent.offsetWidth - special_values_width;
|
|
||||||
this.width = this.base_width - this.translate_x * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_base_values() {}
|
|
||||||
|
|
||||||
setup_container() {
|
|
||||||
this.container = $.create('div', {
|
|
||||||
className: 'chart-container',
|
|
||||||
innerHTML: `<h6 class="title" style="margin-top: 15px;">${this.title}</h6>
|
|
||||||
<h6 class="sub-title uppercase">${this.subtitle}</h6>
|
|
||||||
<div class="frappe-chart graphics"></div>
|
|
||||||
<div class="graph-stats-container"></div>`
|
|
||||||
});
|
|
||||||
|
|
||||||
// Chart needs a dedicated parent element
|
|
||||||
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.make_chart_area();
|
|
||||||
this.make_draw_area();
|
|
||||||
}
|
|
||||||
|
|
||||||
make_chart_area() {
|
|
||||||
this.svg = $.createSVG('svg', {
|
|
||||||
className: 'chart',
|
|
||||||
inside: this.chart_wrapper,
|
|
||||||
width: this.base_width,
|
|
||||||
height: this.base_height
|
|
||||||
});
|
|
||||||
|
|
||||||
this.svg_defs = $.createSVG('defs', {
|
|
||||||
inside: this.svg,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
make_draw_area() {
|
|
||||||
this.draw_area = $.createSVG("g", {
|
|
||||||
className: this.type + '-chart',
|
|
||||||
inside: this.svg,
|
|
||||||
transform: `translate(${this.translate_x}, ${this.translate_y})`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_components() { }
|
|
||||||
|
|
||||||
make_tooltip() {
|
|
||||||
this.tip = new SvgTip({
|
|
||||||
parent: this.chart_wrapper,
|
|
||||||
});
|
|
||||||
this.bind_tooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
show_summary() {}
|
|
||||||
show_custom_summary() {
|
|
||||||
this.summary.map(d => {
|
|
||||||
let stats = $.create('div', {
|
|
||||||
className: 'stats',
|
|
||||||
innerHTML: `<span class="indicator ${d.color}">${d.title}: ${d.value}</span>`
|
|
||||||
});
|
|
||||||
this.stats_wrapper.appendChild(stats);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_navigation(init=false) {
|
|
||||||
this.make_overlay();
|
|
||||||
|
|
||||||
if(init) {
|
|
||||||
this.bind_overlay();
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if($.isElementInViewport(this.chart_wrapper)) {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
make_overlay() {}
|
|
||||||
bind_overlay() {}
|
|
||||||
|
|
||||||
on_left_arrow() {}
|
|
||||||
on_right_arrow() {}
|
|
||||||
on_up_arrow() {}
|
|
||||||
on_down_arrow() {}
|
|
||||||
on_enter_key() {}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
update_current_data_point(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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
get_strwidth(string) {
|
|
||||||
return string.length * 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Objects
|
|
||||||
setup_utils() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
class AxisChart extends Chart {
|
|
||||||
constructor(args) {
|
constructor(args) {
|
||||||
super(args);
|
super(args);
|
||||||
|
|
||||||
@ -1262,713 +998,3 @@ class AxisChart extends Chart {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: {
|
|
||||||
space_width: 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LineChart extends AxisChart {
|
|
||||||
constructor(args) {
|
|
||||||
super(args);
|
|
||||||
if(Object.getPrototypeOf(this) !== LineChart.prototype) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.type = 'line';
|
|
||||||
this.region_fill = args.region_fill;
|
|
||||||
this.x_axis_mode = args.x_axis_mode || 'span';
|
|
||||||
this.y_axis_mode = args.y_axis_mode || 'span';
|
|
||||||
|
|
||||||
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] = $.createSVG('g', {
|
|
||||||
className: 'path-group path-group-' + i,
|
|
||||||
inside: this.draw_area
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_values() {
|
|
||||||
super.setup_values();
|
|
||||||
this.unit_args = {
|
|
||||||
type: 'dot',
|
|
||||||
args: { radius: 8 }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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 = $.createSVG('path', {
|
|
||||||
inside: this.paths_groups[i],
|
|
||||||
className: `stroke ${color}`,
|
|
||||||
d: "M"+points_str
|
|
||||||
});
|
|
||||||
|
|
||||||
if(this.region_fill) {
|
|
||||||
let gradient_id ='path-fill-gradient' + '-' + color;
|
|
||||||
|
|
||||||
this.gradient_def = $.createSVG('linearGradient', {
|
|
||||||
inside: this.svg_defs,
|
|
||||||
id: gradient_id,
|
|
||||||
x1: 0,
|
|
||||||
x2: 0,
|
|
||||||
y1: 0,
|
|
||||||
y2: 1
|
|
||||||
});
|
|
||||||
|
|
||||||
let set_gradient_stop = (grad_elem, offset, color, opacity) => {
|
|
||||||
$.createSVG('stop', {
|
|
||||||
'className': 'stop-color ' + color,
|
|
||||||
'inside': grad_elem,
|
|
||||||
'offset': offset,
|
|
||||||
'stop-opacity': opacity
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
set_gradient_stop(this.gradient_def, "0%", color, 0.4);
|
|
||||||
set_gradient_stop(this.gradient_def, "50%", color, 0.2);
|
|
||||||
set_gradient_stop(this.gradient_def, "100%", color, 0);
|
|
||||||
|
|
||||||
d.region_path = $.createSVG('path', {
|
|
||||||
inside: this.paths_groups[i],
|
|
||||||
className: `region-fill`,
|
|
||||||
d: "M" + `0,${this.zero_line}L` + points_str + `L${this.width},${this.zero_line}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
d.region_path.style.stroke = "none";
|
|
||||||
d.region_path.style.fill = `url(#${gradient_id})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PercentageChart extends Chart {
|
|
||||||
constructor(args) {
|
|
||||||
super(args);
|
|
||||||
this.type = 'percentage';
|
|
||||||
|
|
||||||
this.get_y_label = this.format_lambdas.y_label;
|
|
||||||
this.get_x_tooltip = this.format_lambdas.x_tooltip;
|
|
||||||
this.get_y_tooltip = this.format_lambdas.y_tooltip;
|
|
||||||
|
|
||||||
this.max_slices = 10;
|
|
||||||
this.max_legend_points = 6;
|
|
||||||
|
|
||||||
this.colors = args.colors;
|
|
||||||
|
|
||||||
if(!this.colors || this.colors.length < this.data.labels.length) {
|
|
||||||
this.colors = ['light-blue', 'blue', 'violet', 'red', 'orange',
|
|
||||||
'yellow', 'green', 'light-green', 'purple', 'magenta'];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
make_chart_area() {
|
|
||||||
this.chart_wrapper.className += ' ' + 'graph-focus-margin';
|
|
||||||
this.chart_wrapper.style.marginTop = '45px';
|
|
||||||
|
|
||||||
this.stats_wrapper.className += ' ' + 'graph-focus-margin';
|
|
||||||
this.stats_wrapper.style.marginBottom = '30px';
|
|
||||||
this.stats_wrapper.style.paddingTop = '0px';
|
|
||||||
}
|
|
||||||
|
|
||||||
make_draw_area() {
|
|
||||||
this.chart_div = $.create('div', {
|
|
||||||
className: 'div',
|
|
||||||
inside: this.chart_wrapper,
|
|
||||||
width: this.base_width,
|
|
||||||
height: this.base_height
|
|
||||||
});
|
|
||||||
|
|
||||||
this.chart = $.create('div', {
|
|
||||||
className: 'progress-chart',
|
|
||||||
inside: this.chart_div
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_components() {
|
|
||||||
this.percentage_bar = $.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_utils() { }
|
|
||||||
|
|
||||||
make_graph_components() {
|
|
||||||
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
|
|
||||||
this.slices = [];
|
|
||||||
this.slice_totals.map((total, i) => {
|
|
||||||
let slice = $.create('div', {
|
|
||||||
className: `progress-bar background ${this.colors[i]}`,
|
|
||||||
style: `width: ${total*100/this.grand_total}%`,
|
|
||||||
inside: this.percentage_bar
|
|
||||||
});
|
|
||||||
this.slices.push(slice);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bind_tooltip() {
|
|
||||||
this.slices.map((slice, i) => {
|
|
||||||
slice.addEventListener('mouseenter', () => {
|
|
||||||
let g_off = $.offset(this.chart_wrapper), p_off = $.offset(slice);
|
|
||||||
|
|
||||||
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.tip.set_values(x, y, title, percent + "%");
|
|
||||||
this.tip.show_tip();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ${this.colors[i]}">
|
|
||||||
<span class="text-muted">${x_values[i]}:</span>
|
|
||||||
${d}
|
|
||||||
</span>`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HeatMap extends Chart {
|
|
||||||
constructor({
|
|
||||||
start = '',
|
|
||||||
domain = '',
|
|
||||||
subdomain = '',
|
|
||||||
data = {},
|
|
||||||
discrete_domains = 0,
|
|
||||||
count_label = ''
|
|
||||||
}) {
|
|
||||||
super(arguments[0]);
|
|
||||||
|
|
||||||
this.type = 'heatmap';
|
|
||||||
|
|
||||||
this.domain = domain;
|
|
||||||
this.subdomain = subdomain;
|
|
||||||
this.data = data;
|
|
||||||
this.discrete_domains = discrete_domains;
|
|
||||||
this.count_label = count_label;
|
|
||||||
|
|
||||||
let today = new Date();
|
|
||||||
this.start = start || this.add_days(today, 365);
|
|
||||||
|
|
||||||
this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
|
|
||||||
|
|
||||||
this.translate_x = 0;
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_base_values() {
|
|
||||||
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) {
|
|
||||||
this.add_days(this.first_week_start, (-1) * this.first_week_start.getDay());
|
|
||||||
}
|
|
||||||
if(this.last_week_start.getDay() !== 7) {
|
|
||||||
this.add_days(this.last_week_start, (-1) * this.last_week_start.getDay());
|
|
||||||
}
|
|
||||||
this.no_of_cols = this.get_weeks_between(this.first_week_start + '', this.last_week_start + '') + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
set_width() {
|
|
||||||
this.base_width = (this.no_of_cols) * 12;
|
|
||||||
|
|
||||||
if(this.discrete_domains) {
|
|
||||||
this.base_width += (12 * 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_components() {
|
|
||||||
this.domain_label_group = $.createSVG("g", {
|
|
||||||
className: "domain-label-group chart-label",
|
|
||||||
inside: this.draw_area
|
|
||||||
});
|
|
||||||
this.data_groups = $.createSVG("g", {
|
|
||||||
className: "data-groups",
|
|
||||||
inside: this.draw_area,
|
|
||||||
transform: `translate(0, 20)`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_values() {
|
|
||||||
this.domain_label_group.textContent = '';
|
|
||||||
this.data_groups.textContent = '';
|
|
||||||
this.distribution = this.get_distribution(this.data, this.legend_colors);
|
|
||||||
this.month_names = ["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_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();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
this.add_days(current_week_sunday, 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;
|
|
||||||
const step = 1;
|
|
||||||
|
|
||||||
let month_change = 0;
|
|
||||||
let week_col_change = 0;
|
|
||||||
|
|
||||||
let data_group = $.createSVG("g", {
|
|
||||||
className: "data-group",
|
|
||||||
inside: this.data_groups
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
let timestamp = Math.floor(current_date.getTime()/1000).toFixed(1);
|
|
||||||
|
|
||||||
if(this.data[timestamp]) {
|
|
||||||
data_value = this.data[timestamp];
|
|
||||||
color_index = this.get_max_checkpoint(data_value, this.distribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.data[Math.round(timestamp)]) {
|
|
||||||
data_value = this.data[Math.round(timestamp)];
|
|
||||||
color_index = this.get_max_checkpoint(data_value, this.distribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
let x = 13 + (index + week_col_change) * 12;
|
|
||||||
|
|
||||||
$.createSVG("rect", {
|
|
||||||
className: 'day',
|
|
||||||
inside: data_group,
|
|
||||||
x: x,
|
|
||||||
y: y,
|
|
||||||
width: square_side,
|
|
||||||
height: square_side,
|
|
||||||
fill: this.legend_colors[color_index],
|
|
||||||
'data-date': this.get_dd_mm_yyyy(current_date),
|
|
||||||
'data-value': data_value,
|
|
||||||
'data-day': current_date.getDay()
|
|
||||||
});
|
|
||||||
|
|
||||||
let next_date = new Date(current_date);
|
|
||||||
this.add_days(next_date, 1);
|
|
||||||
if(next_date.getMonth() - current_date.getMonth()) {
|
|
||||||
month_change = 1;
|
|
||||||
if(this.discrete_domains) {
|
|
||||||
week_col_change = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.month_start_points.push(13 + (index + week_col_change) * 12);
|
|
||||||
}
|
|
||||||
current_date = next_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [data_group, month_change];
|
|
||||||
}
|
|
||||||
|
|
||||||
render_month_labels() {
|
|
||||||
// this.first_month_label = 1;
|
|
||||||
// if (this.first_week_start.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();
|
|
||||||
// render first month if
|
|
||||||
|
|
||||||
// let last_month = this.months.pop();
|
|
||||||
// let last_month_start = this.month_start_points.pop();
|
|
||||||
// render last month if
|
|
||||||
|
|
||||||
this.months.shift();
|
|
||||||
this.month_start_points.shift();
|
|
||||||
this.months.pop();
|
|
||||||
this.month_start_points.pop();
|
|
||||||
|
|
||||||
this.month_start_points.map((start, i) => {
|
|
||||||
let month_name = this.month_names[this.months[i]].substring(0, 3);
|
|
||||||
|
|
||||||
$.createSVG('text', {
|
|
||||||
className: 'y-value-text',
|
|
||||||
inside: this.domain_label_group,
|
|
||||||
x: start + 12,
|
|
||||||
y: 10,
|
|
||||||
dy: '.32em',
|
|
||||||
innerHTML: month_name
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
make_graph_components() {
|
|
||||||
Array.prototype.slice.call(
|
|
||||||
this.container.querySelectorAll('.graph-stats-container, .sub-title, .title')
|
|
||||||
).map(d => {
|
|
||||||
d.style.display = 'None';
|
|
||||||
});
|
|
||||||
this.chart_wrapper.style.marginTop = '0px';
|
|
||||||
this.chart_wrapper.style.paddingTop = '0px';
|
|
||||||
}
|
|
||||||
|
|
||||||
bind_tooltip() {
|
|
||||||
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 month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3);
|
|
||||||
|
|
||||||
let g_off = this.chart_wrapper.getBoundingClientRect(), p_off = 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];
|
|
||||||
|
|
||||||
this.tip.set_values(x, y, name, value, [], 1);
|
|
||||||
this.tip.show_tip();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
update(data) {
|
|
||||||
this.data = data;
|
|
||||||
this.setup_values();
|
|
||||||
this.bind_tooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
get_distribution(data={}, mapper_array) {
|
|
||||||
let data_values = Object.keys(data).map(key => data[key]);
|
|
||||||
let data_max_value = Math.max(...data_values);
|
|
||||||
|
|
||||||
let distribution_step = 1 / (mapper_array.length - 1);
|
|
||||||
let distribution = [];
|
|
||||||
|
|
||||||
mapper_array.map((color, i) => {
|
|
||||||
let checkpoint = data_max_value * (distribution_step * i);
|
|
||||||
distribution.push(checkpoint);
|
|
||||||
});
|
|
||||||
|
|
||||||
return distribution;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_max_checkpoint(value, distribution) {
|
|
||||||
return distribution.filter((d, i) => {
|
|
||||||
if(i === 1) {
|
|
||||||
return distribution[0] < value;
|
|
||||||
}
|
|
||||||
return d <= value;
|
|
||||||
}).length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: date utils, move these out
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/11252167/6495043
|
|
||||||
treat_as_utc(date_str) {
|
|
||||||
let result = new Date(date_str);
|
|
||||||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
get_dd_mm_yyyy(date) {
|
|
||||||
let dd = date.getDate();
|
|
||||||
let mm = date.getMonth() + 1; // getMonth() is zero-based
|
|
||||||
return [
|
|
||||||
(dd>9 ? '' : '0') + dd,
|
|
||||||
(mm>9 ? '' : '0') + mm,
|
|
||||||
date.getFullYear()
|
|
||||||
].join('-');
|
|
||||||
}
|
|
||||||
|
|
||||||
get_weeks_between(start_date_str, end_date_str) {
|
|
||||||
return Math.ceil(this.get_days_between(start_date_str, end_date_str) / 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_days_between(start_date_str, end_date_str) {
|
|
||||||
let milliseconds_per_day = 24 * 60 * 60 * 1000;
|
|
||||||
return (this.treat_as_utc(end_date_str) - this.treat_as_utc(start_date_str)) / milliseconds_per_day;
|
|
||||||
}
|
|
||||||
|
|
||||||
// mutates
|
|
||||||
add_days(date, number_of_days) {
|
|
||||||
date.setDate(date.getDate() + number_of_days);
|
|
||||||
}
|
|
||||||
|
|
||||||
get_month_name() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SvgTip {
|
|
||||||
constructor({
|
|
||||||
parent = null
|
|
||||||
}) {
|
|
||||||
this.parent = parent;
|
|
||||||
this.title_name = '';
|
|
||||||
this.title_value = '';
|
|
||||||
this.list_values = [];
|
|
||||||
this.title_value_first = 0;
|
|
||||||
|
|
||||||
this.x = 0;
|
|
||||||
this.y = 0;
|
|
||||||
|
|
||||||
this.top = 0;
|
|
||||||
this.left = 0;
|
|
||||||
|
|
||||||
this.setup();
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
this.make_tooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
|
||||||
this.fill();
|
|
||||||
this.calc_position();
|
|
||||||
// this.show_tip();
|
|
||||||
}
|
|
||||||
|
|
||||||
make_tooltip() {
|
|
||||||
this.container = $.create('div', {
|
|
||||||
inside: this.parent,
|
|
||||||
className: 'graph-svg-tip comparison',
|
|
||||||
innerHTML: `<span class="title"></span>
|
|
||||||
<ul class="data-point-list"></ul>
|
|
||||||
<div class="svg-pointer"></div>`
|
|
||||||
});
|
|
||||||
this.hide_tip();
|
|
||||||
|
|
||||||
this.title = this.container.querySelector('.title');
|
|
||||||
this.data_point_list = this.container.querySelector('.data-point-list');
|
|
||||||
|
|
||||||
this.parent.addEventListener('mouseleave', () => {
|
|
||||||
this.hide_tip();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fill() {
|
|
||||||
let title;
|
|
||||||
if(this.title_value_first) {
|
|
||||||
title = `<strong>${this.title_value}</strong>${this.title_name}`;
|
|
||||||
} else {
|
|
||||||
title = `${this.title_name}<strong>${this.title_value}</strong>`;
|
|
||||||
}
|
|
||||||
this.title.innerHTML = title;
|
|
||||||
this.data_point_list.innerHTML = '';
|
|
||||||
|
|
||||||
this.list_values.map((set) => {
|
|
||||||
let li = $.create('li', {
|
|
||||||
className: `border-top ${set.color || 'black'}`,
|
|
||||||
innerHTML: `<strong style="display: block;">${set.value ? set.value : '' }</strong>
|
|
||||||
${set.title ? set.title : '' }`
|
|
||||||
});
|
|
||||||
|
|
||||||
this.data_point_list.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
calc_position() {
|
|
||||||
this.top = this.y - this.container.offsetHeight;
|
|
||||||
this.left = this.x - this.container.offsetWidth/2;
|
|
||||||
let max_left = this.parent.offsetWidth - this.container.offsetWidth;
|
|
||||||
|
|
||||||
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;
|
|
||||||
pointer.style.left = `calc(50% + ${delta}px)`;
|
|
||||||
this.left = max_left;
|
|
||||||
} 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;
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.title_value_first = title_value_first;
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
hide_tip() {
|
|
||||||
this.container.style.top = '0px';
|
|
||||||
this.container.style.left = '0px';
|
|
||||||
this.container.style.opacity = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
show_tip() {
|
|
||||||
this.container.style.top = this.top + 'px';
|
|
||||||
this.container.style.left = this.left + 'px';
|
|
||||||
this.container.style.opacity = '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
src/scripts/charts/BarChart.js
Normal file
70
src/scripts/charts/BarChart.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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: {
|
||||||
|
space_width: 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
253
src/scripts/charts/BaseChart.js
Normal file
253
src/scripts/charts/BaseChart.js
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import SvgTip from '../objects/SvgTip';
|
||||||
|
import $ from '../helpers/dom';
|
||||||
|
|
||||||
|
export default class BaseChart {
|
||||||
|
constructor({
|
||||||
|
parent = "",
|
||||||
|
height = 240,
|
||||||
|
|
||||||
|
title = '', subtitle = '',
|
||||||
|
|
||||||
|
data = {},
|
||||||
|
format_lambdas = {},
|
||||||
|
|
||||||
|
summary = [],
|
||||||
|
|
||||||
|
is_navigable = 0,
|
||||||
|
|
||||||
|
type = '' // eslint-disable-line no-unused-vars
|
||||||
|
}) {
|
||||||
|
this.raw_chart_args = arguments[0];
|
||||||
|
|
||||||
|
this.parent = document.querySelector(parent);
|
||||||
|
this.title = title;
|
||||||
|
this.subtitle = subtitle;
|
||||||
|
|
||||||
|
this.data = data;
|
||||||
|
this.format_lambdas = format_lambdas;
|
||||||
|
|
||||||
|
this.specific_values = data.specific_values || [];
|
||||||
|
this.summary = summary;
|
||||||
|
|
||||||
|
this.is_navigable = is_navigable;
|
||||||
|
if(this.is_navigable) {
|
||||||
|
this.current_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chart_types = ['line', 'bar', 'percentage', 'heatmap'];
|
||||||
|
|
||||||
|
this.set_margins(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_different_chart(type) {
|
||||||
|
if(!this.chart_types.includes(type)) {
|
||||||
|
console.error(`'${type}' is not a valid chart type.`);
|
||||||
|
}
|
||||||
|
if(type === this.type) return;
|
||||||
|
|
||||||
|
// Only across compatible types
|
||||||
|
let compatible_types = {
|
||||||
|
bar: ['line', 'percentage'],
|
||||||
|
line: ['bar', 'percentage'],
|
||||||
|
percentage: ['bar', 'line'],
|
||||||
|
heatmap: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!compatible_types[this.type].includes(type)) {
|
||||||
|
console.error(`'${this.type}' chart cannot be converted to a '${type}' chart.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 BaseChart({
|
||||||
|
parent: this.raw_chart_args.parent,
|
||||||
|
data: this.raw_chart_args.data,
|
||||||
|
type: type,
|
||||||
|
height: this.raw_chart_args.height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
set_margins(height) {
|
||||||
|
this.base_height = height;
|
||||||
|
this.height = height - 40;
|
||||||
|
this.translate_x = 60;
|
||||||
|
this.translate_y = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.bind_window_events();
|
||||||
|
this.refresh(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_window_events() {
|
||||||
|
window.addEventListener('resize', () => this.refresh());
|
||||||
|
window.addEventListener('orientationchange', () => this.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(init=false) {
|
||||||
|
this.setup_base_values();
|
||||||
|
this.set_width();
|
||||||
|
|
||||||
|
this.setup_container();
|
||||||
|
this.setup_components();
|
||||||
|
|
||||||
|
this.setup_values();
|
||||||
|
this.setup_utils();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_width() {
|
||||||
|
let special_values_width = 0;
|
||||||
|
this.specific_values.map(val => {
|
||||||
|
if(this.get_strwidth(val.title) > special_values_width) {
|
||||||
|
special_values_width = this.get_strwidth(val.title) - 40;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.base_width = this.parent.offsetWidth - special_values_width;
|
||||||
|
this.width = this.base_width - this.translate_x * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_base_values() {}
|
||||||
|
|
||||||
|
setup_container() {
|
||||||
|
this.container = $.create('div', {
|
||||||
|
className: 'chart-container',
|
||||||
|
innerHTML: `<h6 class="title" style="margin-top: 15px;">${this.title}</h6>
|
||||||
|
<h6 class="sub-title uppercase">${this.subtitle}</h6>
|
||||||
|
<div class="frappe-chart graphics"></div>
|
||||||
|
<div class="graph-stats-container"></div>`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chart needs a dedicated parent element
|
||||||
|
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.make_chart_area();
|
||||||
|
this.make_draw_area();
|
||||||
|
}
|
||||||
|
|
||||||
|
make_chart_area() {
|
||||||
|
this.svg = $.createSVG('svg', {
|
||||||
|
className: 'chart',
|
||||||
|
inside: this.chart_wrapper,
|
||||||
|
width: this.base_width,
|
||||||
|
height: this.base_height
|
||||||
|
});
|
||||||
|
|
||||||
|
this.svg_defs = $.createSVG('defs', {
|
||||||
|
inside: this.svg,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
make_draw_area() {
|
||||||
|
this.draw_area = $.createSVG("g", {
|
||||||
|
className: this.type + '-chart',
|
||||||
|
inside: this.svg,
|
||||||
|
transform: `translate(${this.translate_x}, ${this.translate_y})`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_components() { }
|
||||||
|
|
||||||
|
make_tooltip() {
|
||||||
|
this.tip = new SvgTip({
|
||||||
|
parent: this.chart_wrapper,
|
||||||
|
});
|
||||||
|
this.bind_tooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
show_summary() {}
|
||||||
|
show_custom_summary() {
|
||||||
|
this.summary.map(d => {
|
||||||
|
let stats = $.create('div', {
|
||||||
|
className: 'stats',
|
||||||
|
innerHTML: `<span class="indicator ${d.color}">${d.title}: ${d.value}</span>`
|
||||||
|
});
|
||||||
|
this.stats_wrapper.appendChild(stats);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_navigation(init=false) {
|
||||||
|
this.make_overlay();
|
||||||
|
|
||||||
|
if(init) {
|
||||||
|
this.bind_overlay();
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if($.isElementInViewport(this.chart_wrapper)) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_overlay() {}
|
||||||
|
bind_overlay() {}
|
||||||
|
|
||||||
|
on_left_arrow() {}
|
||||||
|
on_right_arrow() {}
|
||||||
|
on_up_arrow() {}
|
||||||
|
on_down_arrow() {}
|
||||||
|
on_enter_key() {}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
update_current_data_point(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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get_strwidth(string) {
|
||||||
|
return string.length * 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Objects
|
||||||
|
setup_utils() { }
|
||||||
|
}
|
||||||
303
src/scripts/charts/Heatmap.js
Normal file
303
src/scripts/charts/Heatmap.js
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import BaseChart from './BaseChart';
|
||||||
|
import $ from '../helpers/dom';
|
||||||
|
|
||||||
|
export default class Heatmap extends BaseChart {
|
||||||
|
constructor({
|
||||||
|
start = '',
|
||||||
|
domain = '',
|
||||||
|
subdomain = '',
|
||||||
|
data = {},
|
||||||
|
discrete_domains = 0,
|
||||||
|
count_label = ''
|
||||||
|
}) {
|
||||||
|
super(arguments[0]);
|
||||||
|
|
||||||
|
this.type = 'heatmap';
|
||||||
|
|
||||||
|
this.domain = domain;
|
||||||
|
this.subdomain = subdomain;
|
||||||
|
this.data = data;
|
||||||
|
this.discrete_domains = discrete_domains;
|
||||||
|
this.count_label = count_label;
|
||||||
|
|
||||||
|
let today = new Date();
|
||||||
|
this.start = start || this.add_days(today, 365);
|
||||||
|
|
||||||
|
this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127'];
|
||||||
|
|
||||||
|
this.translate_x = 0;
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_base_values() {
|
||||||
|
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) {
|
||||||
|
this.add_days(this.first_week_start, (-1) * this.first_week_start.getDay());
|
||||||
|
}
|
||||||
|
if(this.last_week_start.getDay() !== 7) {
|
||||||
|
this.add_days(this.last_week_start, (-1) * this.last_week_start.getDay());
|
||||||
|
}
|
||||||
|
this.no_of_cols = this.get_weeks_between(this.first_week_start + '', this.last_week_start + '') + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
set_width() {
|
||||||
|
this.base_width = (this.no_of_cols) * 12;
|
||||||
|
|
||||||
|
if(this.discrete_domains) {
|
||||||
|
this.base_width += (12 * 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_components() {
|
||||||
|
this.domain_label_group = $.createSVG("g", {
|
||||||
|
className: "domain-label-group chart-label",
|
||||||
|
inside: this.draw_area
|
||||||
|
});
|
||||||
|
this.data_groups = $.createSVG("g", {
|
||||||
|
className: "data-groups",
|
||||||
|
inside: this.draw_area,
|
||||||
|
transform: `translate(0, 20)`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_values() {
|
||||||
|
this.domain_label_group.textContent = '';
|
||||||
|
this.data_groups.textContent = '';
|
||||||
|
this.distribution = this.get_distribution(this.data, this.legend_colors);
|
||||||
|
this.month_names = ["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_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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
this.add_days(current_week_sunday, 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;
|
||||||
|
const step = 1;
|
||||||
|
|
||||||
|
let month_change = 0;
|
||||||
|
let week_col_change = 0;
|
||||||
|
|
||||||
|
let data_group = $.createSVG("g", {
|
||||||
|
className: "data-group",
|
||||||
|
inside: this.data_groups
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
let timestamp = Math.floor(current_date.getTime()/1000).toFixed(1);
|
||||||
|
|
||||||
|
if(this.data[timestamp]) {
|
||||||
|
data_value = this.data[timestamp];
|
||||||
|
color_index = this.get_max_checkpoint(data_value, this.distribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.data[Math.round(timestamp)]) {
|
||||||
|
data_value = this.data[Math.round(timestamp)];
|
||||||
|
color_index = this.get_max_checkpoint(data_value, this.distribution);
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = 13 + (index + week_col_change) * 12;
|
||||||
|
|
||||||
|
$.createSVG("rect", {
|
||||||
|
className: 'day',
|
||||||
|
inside: data_group,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: square_side,
|
||||||
|
height: square_side,
|
||||||
|
fill: this.legend_colors[color_index],
|
||||||
|
'data-date': this.get_dd_mm_yyyy(current_date),
|
||||||
|
'data-value': data_value,
|
||||||
|
'data-day': current_date.getDay()
|
||||||
|
});
|
||||||
|
|
||||||
|
let next_date = new Date(current_date);
|
||||||
|
this.add_days(next_date, 1);
|
||||||
|
if(next_date.getMonth() - current_date.getMonth()) {
|
||||||
|
month_change = 1;
|
||||||
|
if(this.discrete_domains) {
|
||||||
|
week_col_change = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.month_start_points.push(13 + (index + week_col_change) * 12);
|
||||||
|
}
|
||||||
|
current_date = next_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [data_group, month_change];
|
||||||
|
}
|
||||||
|
|
||||||
|
render_month_labels() {
|
||||||
|
// this.first_month_label = 1;
|
||||||
|
// if (this.first_week_start.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();
|
||||||
|
// render first month if
|
||||||
|
|
||||||
|
// let last_month = this.months.pop();
|
||||||
|
// let last_month_start = this.month_start_points.pop();
|
||||||
|
// render last month if
|
||||||
|
|
||||||
|
this.months.shift();
|
||||||
|
this.month_start_points.shift();
|
||||||
|
this.months.pop();
|
||||||
|
this.month_start_points.pop();
|
||||||
|
|
||||||
|
this.month_start_points.map((start, i) => {
|
||||||
|
let month_name = this.month_names[this.months[i]].substring(0, 3);
|
||||||
|
|
||||||
|
$.createSVG('text', {
|
||||||
|
className: 'y-value-text',
|
||||||
|
inside: this.domain_label_group,
|
||||||
|
x: start + 12,
|
||||||
|
y: 10,
|
||||||
|
dy: '.32em',
|
||||||
|
innerHTML: month_name
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
make_graph_components() {
|
||||||
|
Array.prototype.slice.call(
|
||||||
|
this.container.querySelectorAll('.graph-stats-container, .sub-title, .title')
|
||||||
|
).map(d => {
|
||||||
|
d.style.display = 'None';
|
||||||
|
});
|
||||||
|
this.chart_wrapper.style.marginTop = '0px';
|
||||||
|
this.chart_wrapper.style.paddingTop = '0px';
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_tooltip() {
|
||||||
|
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 month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3);
|
||||||
|
|
||||||
|
let g_off = this.chart_wrapper.getBoundingClientRect(), p_off = 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];
|
||||||
|
|
||||||
|
this.tip.set_values(x, y, name, value, [], 1);
|
||||||
|
this.tip.show_tip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data) {
|
||||||
|
this.data = data;
|
||||||
|
this.setup_values();
|
||||||
|
this.bind_tooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
get_distribution(data={}, mapper_array) {
|
||||||
|
let data_values = Object.keys(data).map(key => data[key]);
|
||||||
|
let data_max_value = Math.max(...data_values);
|
||||||
|
|
||||||
|
let distribution_step = 1 / (mapper_array.length - 1);
|
||||||
|
let distribution = [];
|
||||||
|
|
||||||
|
mapper_array.map((color, i) => {
|
||||||
|
let checkpoint = data_max_value * (distribution_step * i);
|
||||||
|
distribution.push(checkpoint);
|
||||||
|
});
|
||||||
|
|
||||||
|
return distribution;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_max_checkpoint(value, distribution) {
|
||||||
|
return distribution.filter((d, i) => {
|
||||||
|
if(i === 1) {
|
||||||
|
return distribution[0] < value;
|
||||||
|
}
|
||||||
|
return d <= value;
|
||||||
|
}).length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: date utils, move these out
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/11252167/6495043
|
||||||
|
treat_as_utc(date_str) {
|
||||||
|
let result = new Date(date_str);
|
||||||
|
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_dd_mm_yyyy(date) {
|
||||||
|
let dd = date.getDate();
|
||||||
|
let mm = date.getMonth() + 1; // getMonth() is zero-based
|
||||||
|
return [
|
||||||
|
(dd>9 ? '' : '0') + dd,
|
||||||
|
(mm>9 ? '' : '0') + mm,
|
||||||
|
date.getFullYear()
|
||||||
|
].join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
get_weeks_between(start_date_str, end_date_str) {
|
||||||
|
return Math.ceil(this.get_days_between(start_date_str, end_date_str) / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_days_between(start_date_str, end_date_str) {
|
||||||
|
let milliseconds_per_day = 24 * 60 * 60 * 1000;
|
||||||
|
return (this.treat_as_utc(end_date_str) - this.treat_as_utc(start_date_str)) / milliseconds_per_day;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mutates
|
||||||
|
add_days(date, number_of_days) {
|
||||||
|
date.setDate(date.getDate() + number_of_days);
|
||||||
|
}
|
||||||
|
|
||||||
|
get_month_name() {}
|
||||||
|
}
|
||||||
95
src/scripts/charts/LineChart.js
Normal file
95
src/scripts/charts/LineChart.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import AxisChart from './AxisChart';
|
||||||
|
import $ from '../helpers/dom';
|
||||||
|
|
||||||
|
export default class LineChart extends AxisChart {
|
||||||
|
constructor(args) {
|
||||||
|
super(args);
|
||||||
|
if(Object.getPrototypeOf(this) !== LineChart.prototype) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.type = 'line';
|
||||||
|
this.region_fill = args.region_fill;
|
||||||
|
this.x_axis_mode = args.x_axis_mode || 'span';
|
||||||
|
this.y_axis_mode = args.y_axis_mode || 'span';
|
||||||
|
|
||||||
|
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] = $.createSVG('g', {
|
||||||
|
className: 'path-group path-group-' + i,
|
||||||
|
inside: this.draw_area
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_values() {
|
||||||
|
super.setup_values();
|
||||||
|
this.unit_args = {
|
||||||
|
type: 'dot',
|
||||||
|
args: { radius: 8 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = $.createSVG('path', {
|
||||||
|
inside: this.paths_groups[i],
|
||||||
|
className: `stroke ${color}`,
|
||||||
|
d: "M"+points_str
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.region_fill) {
|
||||||
|
let gradient_id ='path-fill-gradient' + '-' + color;
|
||||||
|
|
||||||
|
this.gradient_def = $.createSVG('linearGradient', {
|
||||||
|
inside: this.svg_defs,
|
||||||
|
id: gradient_id,
|
||||||
|
x1: 0,
|
||||||
|
x2: 0,
|
||||||
|
y1: 0,
|
||||||
|
y2: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
let set_gradient_stop = (grad_elem, offset, color, opacity) => {
|
||||||
|
$.createSVG('stop', {
|
||||||
|
'className': 'stop-color ' + color,
|
||||||
|
'inside': grad_elem,
|
||||||
|
'offset': offset,
|
||||||
|
'stop-opacity': opacity
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
set_gradient_stop(this.gradient_def, "0%", color, 0.4);
|
||||||
|
set_gradient_stop(this.gradient_def, "50%", color, 0.2);
|
||||||
|
set_gradient_stop(this.gradient_def, "100%", color, 0);
|
||||||
|
|
||||||
|
d.region_path = $.createSVG('path', {
|
||||||
|
inside: this.paths_groups[i],
|
||||||
|
className: `region-fill`,
|
||||||
|
d: "M" + `0,${this.zero_line}L` + points_str + `L${this.width},${this.zero_line}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
d.region_path.style.stroke = "none";
|
||||||
|
d.region_path.style.fill = `url(#${gradient_id})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
src/scripts/charts/PercentageChart.js
Normal file
139
src/scripts/charts/PercentageChart.js
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import BaseChart from './BaseChart';
|
||||||
|
import $ from '../helpers/dom';
|
||||||
|
|
||||||
|
export default class PercentageChart extends BaseChart {
|
||||||
|
constructor(args) {
|
||||||
|
super(args);
|
||||||
|
this.type = 'percentage';
|
||||||
|
|
||||||
|
this.get_y_label = this.format_lambdas.y_label;
|
||||||
|
this.get_x_tooltip = this.format_lambdas.x_tooltip;
|
||||||
|
this.get_y_tooltip = this.format_lambdas.y_tooltip;
|
||||||
|
|
||||||
|
this.max_slices = 10;
|
||||||
|
this.max_legend_points = 6;
|
||||||
|
|
||||||
|
this.colors = args.colors;
|
||||||
|
|
||||||
|
if(!this.colors || this.colors.length < this.data.labels.length) {
|
||||||
|
this.colors = ['light-blue', 'blue', 'violet', 'red', 'orange',
|
||||||
|
'yellow', 'green', 'light-green', 'purple', 'magenta'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
make_chart_area() {
|
||||||
|
this.chart_wrapper.className += ' ' + 'graph-focus-margin';
|
||||||
|
this.chart_wrapper.style.marginTop = '45px';
|
||||||
|
|
||||||
|
this.stats_wrapper.className += ' ' + 'graph-focus-margin';
|
||||||
|
this.stats_wrapper.style.marginBottom = '30px';
|
||||||
|
this.stats_wrapper.style.paddingTop = '0px';
|
||||||
|
}
|
||||||
|
|
||||||
|
make_draw_area() {
|
||||||
|
this.chart_div = $.create('div', {
|
||||||
|
className: 'div',
|
||||||
|
inside: this.chart_wrapper,
|
||||||
|
width: this.base_width,
|
||||||
|
height: this.base_height
|
||||||
|
});
|
||||||
|
|
||||||
|
this.chart = $.create('div', {
|
||||||
|
className: 'progress-chart',
|
||||||
|
inside: this.chart_div
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_components() {
|
||||||
|
this.percentage_bar = $.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_utils() { }
|
||||||
|
|
||||||
|
make_graph_components() {
|
||||||
|
this.grand_total = this.slice_totals.reduce((a, b) => a + b, 0);
|
||||||
|
this.slices = [];
|
||||||
|
this.slice_totals.map((total, i) => {
|
||||||
|
let slice = $.create('div', {
|
||||||
|
className: `progress-bar background ${this.colors[i]}`,
|
||||||
|
style: `width: ${total*100/this.grand_total}%`,
|
||||||
|
inside: this.percentage_bar
|
||||||
|
});
|
||||||
|
this.slices.push(slice);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind_tooltip() {
|
||||||
|
this.slices.map((slice, i) => {
|
||||||
|
slice.addEventListener('mouseenter', () => {
|
||||||
|
let g_off = $.offset(this.chart_wrapper), p_off = $.offset(slice);
|
||||||
|
|
||||||
|
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.tip.set_values(x, y, title, percent + "%");
|
||||||
|
this.tip.show_tip();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ${this.colors[i]}">
|
||||||
|
<span class="text-muted">${x_values[i]}:</span>
|
||||||
|
${d}
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
111
src/scripts/objects/SvgTip.js
Normal file
111
src/scripts/objects/SvgTip.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import $ from '../helpers/dom';
|
||||||
|
|
||||||
|
export default class SvgTip {
|
||||||
|
constructor({
|
||||||
|
parent = null
|
||||||
|
}) {
|
||||||
|
this.parent = parent;
|
||||||
|
this.title_name = '';
|
||||||
|
this.title_value = '';
|
||||||
|
this.list_values = [];
|
||||||
|
this.title_value_first = 0;
|
||||||
|
|
||||||
|
this.x = 0;
|
||||||
|
this.y = 0;
|
||||||
|
|
||||||
|
this.top = 0;
|
||||||
|
this.left = 0;
|
||||||
|
|
||||||
|
this.setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.make_tooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh() {
|
||||||
|
this.fill();
|
||||||
|
this.calc_position();
|
||||||
|
// this.show_tip();
|
||||||
|
}
|
||||||
|
|
||||||
|
make_tooltip() {
|
||||||
|
this.container = $.create('div', {
|
||||||
|
inside: this.parent,
|
||||||
|
className: 'graph-svg-tip comparison',
|
||||||
|
innerHTML: `<span class="title"></span>
|
||||||
|
<ul class="data-point-list"></ul>
|
||||||
|
<div class="svg-pointer"></div>`
|
||||||
|
});
|
||||||
|
this.hide_tip();
|
||||||
|
|
||||||
|
this.title = this.container.querySelector('.title');
|
||||||
|
this.data_point_list = this.container.querySelector('.data-point-list');
|
||||||
|
|
||||||
|
this.parent.addEventListener('mouseleave', () => {
|
||||||
|
this.hide_tip();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fill() {
|
||||||
|
let title;
|
||||||
|
if(this.title_value_first) {
|
||||||
|
title = `<strong>${this.title_value}</strong>${this.title_name}`;
|
||||||
|
} else {
|
||||||
|
title = `${this.title_name}<strong>${this.title_value}</strong>`;
|
||||||
|
}
|
||||||
|
this.title.innerHTML = title;
|
||||||
|
this.data_point_list.innerHTML = '';
|
||||||
|
|
||||||
|
this.list_values.map((set) => {
|
||||||
|
let li = $.create('li', {
|
||||||
|
className: `border-top ${set.color || 'black'}`,
|
||||||
|
innerHTML: `<strong style="display: block;">${set.value ? set.value : '' }</strong>
|
||||||
|
${set.title ? set.title : '' }`
|
||||||
|
});
|
||||||
|
|
||||||
|
this.data_point_list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
calc_position() {
|
||||||
|
this.top = this.y - this.container.offsetHeight;
|
||||||
|
this.left = this.x - this.container.offsetWidth/2;
|
||||||
|
let max_left = this.parent.offsetWidth - this.container.offsetWidth;
|
||||||
|
|
||||||
|
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;
|
||||||
|
pointer.style.left = `calc(50% + ${delta}px)`;
|
||||||
|
this.left = max_left;
|
||||||
|
} 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;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.title_value_first = title_value_first;
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
hide_tip() {
|
||||||
|
this.container.style.top = '0px';
|
||||||
|
this.container.style.left = '0px';
|
||||||
|
this.container.style.opacity = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
show_tip() {
|
||||||
|
this.container.style.top = this.top + 'px';
|
||||||
|
this.container.style.left = this.left + 'px';
|
||||||
|
this.container.style.opacity = '1';
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user