@@ -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> |
@@ -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() | |||
], | |||
}; |
@@ -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'; | |||
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>` | |||
}); | |||
import $ from '../helpers/dom'; | |||
import { float_2, arrays_equal } from '../helpers/utils'; | |||
import BaseChart from './BaseChart'; | |||
// 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'; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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() { } | |||
} |
@@ -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() {} | |||
} |
@@ -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})`; | |||
} | |||
} | |||
} |
@@ -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>`; | |||
} | |||
}); | |||
} | |||
} |
@@ -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'; | |||
} | |||
} |