Ver código fonte

add uglify, modularize all objects

tags/1.2.0
pratu16x7 7 anos atrás
pai
commit
8e75d8e2dd
16 arquivos alterados com 1048 adições e 3771 exclusões
  1. +2
    -2775
      dist/frappe-charts.min.js
  2. +1
    -0
      dist/frappe-charts.min.js.map
  3. +0
    -2
      docs/index.html
  4. +33
    -0
      package-lock.json
  5. +2
    -1
      package.json
  6. +15
    -15
      rollup.config.js
  7. +20
    -0
      src/scripts/charts.js
  8. +4
    -978
      src/scripts/charts/AxisChart.js
  9. +70
    -0
      src/scripts/charts/BarChart.js
  10. +253
    -0
      src/scripts/charts/BaseChart.js
  11. +303
    -0
      src/scripts/charts/Heatmap.js
  12. +95
    -0
      src/scripts/charts/LineChart.js
  13. +139
    -0
      src/scripts/charts/PercentageChart.js
  14. +0
    -0
      src/scripts/helpers/dom.js
  15. +0
    -0
      src/scripts/helpers/utils.js
  16. +111
    -0
      src/scripts/objects/SvgTip.js

+ 2
- 2775
dist/frappe-charts.min.js
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 1
- 0
dist/frappe-charts.min.js.map
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 0
- 2
docs/index.html Ver arquivo

@@ -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
- 0
package-lock.json Ver arquivo

@@ -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",


+ 2
- 1
package.json Ver arquivo

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

+ 15
- 15
rollup.config.js Ver arquivo

@@ -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
- 0
src/scripts/charts.js Ver arquivo

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

src/charts.js → src/scripts/charts/AxisChart.js Ver arquivo

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

+ 70
- 0
src/scripts/charts/BarChart.js Ver arquivo

@@ -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
- 0
src/scripts/charts/BaseChart.js Ver arquivo

@@ -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
- 0
src/scripts/charts/Heatmap.js Ver arquivo

@@ -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
- 0
src/scripts/charts/LineChart.js Ver arquivo

@@ -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
- 0
src/scripts/charts/PercentageChart.js Ver arquivo

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

src/dom.js → src/scripts/helpers/dom.js Ver arquivo


src/utils.js → src/scripts/helpers/utils.js Ver arquivo


+ 111
- 0
src/scripts/objects/SvgTip.js Ver arquivo

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

Carregando…
Cancelar
Salvar