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>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -942,6 +942,12 @@
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -2260,6 +2266,15 @@
|
||||
"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": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-1.5.2.tgz",
|
||||
@ -2519,6 +2534,24 @@
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"rollup": "^0.50.0",
|
||||
"rollup-plugin-babel": "^3.0.2",
|
||||
"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
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
import babel from 'rollup-plugin-babel';
|
||||
import eslint from 'rollup-plugin-eslint';
|
||||
import uglify from 'rollup-plugin-uglify';
|
||||
|
||||
export default {
|
||||
input: 'src/charts.js',
|
||||
output: {
|
||||
file: 'dist/frappe-charts.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
name: 'Chart',
|
||||
sourcemap: 'inline',
|
||||
plugins: [
|
||||
resolve(),
|
||||
eslint(),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
}),
|
||||
],
|
||||
input: 'src/scripts/charts.js',
|
||||
output: {
|
||||
file: 'dist/frappe-charts.min.js',
|
||||
format: 'iife',
|
||||
},
|
||||
name: 'Chart',
|
||||
sourcemap: 'true',
|
||||
plugins: [
|
||||
eslint(),
|
||||
babel({
|
||||
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 { float_2, arrays_equal } from './utils';
|
||||
import $ from '../helpers/dom';
|
||||
import { float_2, arrays_equal } from '../helpers/utils';
|
||||
import BaseChart from './BaseChart';
|
||||
|
||||
export default class Chart {
|
||||
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 {
|
||||
export default class AxisChart extends BaseChart {
|
||||
constructor(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