From 498b80d3ab86c62a729089cbceb5f9c312c7df7a Mon Sep 17 00:00:00 2001 From: pratu16x7 Date: Sat, 21 Oct 2017 13:42:49 +0530 Subject: [PATCH] [add] region_fill property for line chart --- docs/assets/js/index.js | 17 ++-- docs/index.html | 1 + src/charts.css | 24 +++++ src/charts.js | 199 ++++++++++++++++++---------------------- 4 files changed, 125 insertions(+), 116 deletions(-) diff --git a/docs/assets/js/index.js b/docs/assets/js/index.js index 073b15c..c83c391 100755 --- a/docs/assets/js/index.js +++ b/docs/assets/js/index.js @@ -20,8 +20,8 @@ let line_data = { "labels": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], "datasets": [{ "color": "green", - // "values": [25, 40, 30, 35, 48, 52, 17], - "values": [25, -90, -30, 35, 48, 52, -17] + "values": [25, 40, 30, 35, 48, 52, 17], + // "values": [25, -90, -30, 35, 48, 52, -17] } ] @@ -70,16 +70,18 @@ let more_line_data = { let line_chart = new frappe.chart.FrappeChart ({ parent: "#charts-2", data: line_data, - type: 'bar', - height: 140 + type: 'line', + height: 340, + region_fill: 1 }) let bar_chart = new frappe.chart.FrappeChart ({ parent: "#charts-1", data: bar_data, - type: 'bar', + type: 'line', height: 140, - is_navigable: 1 + is_navigable: 1, + region_fill: 1 }) bar_chart.parent.addEventListener('data-select', (e) => { @@ -88,8 +90,9 @@ bar_chart.parent.addEventListener('data-select', (e) => { // console.log("chart", bar_chart); +let percentage_data = {}; -let heatmap_data = {} +let heatmap_data = {}; // update_test() { // setTimeout(() => { diff --git a/docs/index.html b/docs/index.html index dfd845b..02eedc1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -28,6 +28,7 @@

Frappé Charts

GitHub-inspired simple and modern charts for the web

with zero dependencies.

+

Because dumb charts are hard to come by.

diff --git a/src/charts.css b/src/charts.css index 73e686f..e278aa2 100644 --- a/src/charts.css +++ b/src/charts.css @@ -74,6 +74,30 @@ .chart-container .tick .x-value-text { text-anchor: middle; } +.chart-container .progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1) +} +.chart-container .progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #36414c; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} .graph-svg-tip { position: absolute; z-index: 99999; diff --git a/src/charts.js b/src/charts.js index 1b59de5..b81aee4 100644 --- a/src/charts.js +++ b/src/charts.js @@ -44,6 +44,8 @@ frappe.chart.FrappeChart = class { return new frappe.chart.PercentageChart(arguments[0]); } else if(type === 'heatmap') { return new frappe.chart.HeatMap(arguments[0]); + } else { + return new frappe.chart.LineChart(arguments[0]); } } @@ -133,12 +135,10 @@ frappe.chart.FrappeChart = class { this.parent.appendChild(this.container); this.chart_wrapper = this.container.querySelector('.frappe-chart'); - // this.chart_wrapper.appendChild(); + this.stats_wrapper = this.container.querySelector('.graph-stats-container'); this.make_chart_area(); this.make_draw_area(); - - this.stats_wrapper = this.container.querySelector('.graph-stats-container'); } make_chart_area() { @@ -149,6 +149,10 @@ frappe.chart.FrappeChart = class { height: this.base_height }); + this.svg_defs = $$.createSVG('defs', { + inside: this.svg, + }); + return this.svg; } @@ -417,8 +421,8 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { data.push({values: d.values}); d.svg_units = []; - this.make_new_units_for_dataset(d.y_tops, d.color || this.colors[i], i); this.make_path && this.make_path(d, d.color || this.colors[i]); + this.make_new_units_for_dataset(d.y_tops, d.color || this.colors[i], i); }); setTimeout(() => { @@ -481,13 +485,15 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { } bind_tooltip() { - // should be w.r.t. this.parent, but will have to take care of - // all the elements and padding, margins on top + // TODO: could be in tooltip itself, as it is a given functionality for its parent this.chart_wrapper.addEventListener('mousemove', (e) => { let rect = this.chart_wrapper.getBoundingClientRect(); let offset = { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft + // https://stackoverflow.com/a/7436602/6495043 + // rect.top varies with scroll, so we add whatever has been + // scrolled to it to get absolute distance from actual page top + top: rect.top + (document.documentElement.scrollTop || document.body.scrollTop), + left: rect.left + (document.documentElement.scrollLeft || document.body.scrollLeft) } let relX = e.pageX - offset.left - this.translate_x; let relY = e.pageY - offset.top - this.translate_y; @@ -518,7 +524,7 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { } }); - // TODO: upside-down tooltips + // TODO: upside-down tooltips for negative values? this.tip.set_values(x, y, title, '', values); this.tip.show_tip(); break; @@ -539,14 +545,8 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { let elements_to_animate = []; elements_to_animate = this.animate_for_equilength_data(elements_to_animate); - // create new x,y pair string and animate path if(this.y[0].path) { - this.y.map((e, i) => { - let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); - let new_path_str = "M"+new_points_list.join("L"); - let args = [{unit:this.y[i].path, object: this.y[i], key:'path'}, {d:new_path_str}, 300, "easein"]; - elements_to_animate.push(args); - }); + elements_to_animate = this.animate_path(elements_to_animate); } // elements_to_animate = elements_to_animate.concat(this.update_y_axis()); @@ -554,14 +554,16 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { if(this.svg.parentNode == this.chart_wrapper) { this.chart_wrapper.removeChild(this.svg); + this.chart_wrapper.appendChild(anim_svg); } - this.chart_wrapper.appendChild(anim_svg); // Replace the new svg (data has long been replaced) setTimeout(() => { - this.chart_wrapper.removeChild(anim_svg); - this.chart_wrapper.appendChild(this.svg); - }, 250); + if(anim_svg.parentNode == this.chart_wrapper) { + this.chart_wrapper.removeChild(anim_svg); + this.chart_wrapper.appendChild(this.svg); + } + }, 200); } update_y_axis() { @@ -581,13 +583,38 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { let type = this.unit_args.type; d.svg_units.map((unit, j) => { elements_to_animate.push(this.animate[type]( - {unit:unit, array:d.svg_units, index: j}, // unit, with info to replace from data + {unit:unit, array:d.svg_units, index: j}, // unit, with info to replace where it came from in the data d.y_tops[j], this.zero_line )); }); }); + + // Change in data, so calculate dependencies this.calc_min_tops(); + + return elements_to_animate; + } + + animate_path(elements_to_animate) { + // create new x,y pair string and animate path + + this.y.map((e, i) => { + let new_points_list = e.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let new_path_str = new_points_list.join("L"); + let path_args = [{unit: this.y[i].path, object: this.y[i], key: 'path'}, {d:"M"+new_path_str}, 250, "easein"]; + elements_to_animate.push(path_args); + + if(this.y[i].region_path) { + let region_args = [ + {unit: this.y[i].region_path, object: this.y[i], key: 'region_path'}, + {d:"M" + `0,${this.zero_line}L` + new_path_str + `L${this.width},${this.zero_line}`}, + 250, + "easein" + ]; + elements_to_animate.push(region_args); + } + }); return elements_to_animate; } @@ -672,7 +699,6 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { intervals.push(start); start += interval_size; } - console.log("intervals", intervals, count); return intervals; } @@ -782,12 +808,12 @@ frappe.chart.AxisChart = class AxisChart extends frappe.chart.FrappeChart { y = zero_line; } - return [bar_obj, {height: height, y: y}, 300, "easein"]; - // bar.animate({height: args.new_height, y: y_top}, 300, mina.easein); + return [bar_obj, {height: height, y: y}, 250, "easein"]; + // bar.animate({height: args.new_height, y: y_top}, 250, mina.easein); }, 'dot': (dot_obj, y_top) => { - return [dot_obj, {cy: y_top}, 300, "easein"]; - // dot.animate({cy: y_top}, 300, mina.easein); + return [dot_obj, {cy: y_top}, 250, "easein"]; + // dot.animate({cy: y_top}, 250, mina.easein); } }; } @@ -865,6 +891,7 @@ frappe.chart.LineChart = class LineChart extends frappe.chart.AxisChart { } this.type = 'line-graph'; + this.region_fill = args.region_fill; this.setup(); } @@ -881,24 +908,48 @@ frappe.chart.LineChart = class LineChart extends frappe.chart.AxisChart { make_path(d, color) { let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); - let path_str = "M"+points_list.join("L"); + let points_str = points_list.join("L"); d.path = $$.createSVG('path', { + inside: this.svg_units_group, className: `stroke ${color}`, - d: path_str + d: "M"+points_str }); - this.svg_units_group.prepend(d.path); - } -} + if(this.region_fill) { + let gradient_id ='path-fill-gradient' + '-' + color; -frappe.chart.RegionChart = class RegionChart extends frappe.chart.LineChart { - constructor(args) { - super(args); + this.gradient_def = $$.createSVG('linearGradient', { + inside: this.svg_defs, + id: gradient_id, + x1: 0, + x2: 0, + y1: 0, + y2: 1 + }); - this.type = 'region-graph'; - this.region_fill = 1; - this.setup(); + function set_gradient_stop(grad_elem, offset, color, opacity) { + $$.createSVG('stop', { + 'inside': grad_elem, + 'offset': offset, + 'stop-color': color, + '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.svg_units_group, + 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})`; + } } } @@ -924,7 +975,9 @@ frappe.chart.PercentageChart = class PercentageChart extends frappe.chart.Frappe 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, @@ -1414,78 +1467,6 @@ frappe.chart.SvgTip = class { } } -frappe.chart.map_c3 = (chart) => { - if (chart.data) { - let data = chart.data; - let type = chart.chart_type || 'line'; - if(type === 'pie') { - type = 'percentage'; - } - - let x = {}, y = []; - - if(data.columns) { - let columns = data.columns; - - x = columns.filter(col => { - return col[0] === data.x; - })[0]; - - if(x && x.length) { - let dataset_length = x.length; - let dirty = false; - columns.map(col => { - if(col[0] !== data.x) { - if(col.length === dataset_length) { - let title = col[0]; - col.splice(0, 1); - y.push({ - title: title, - values: col, - }); - } else { - dirty = true; - } - } - }); - - if(dirty) { - return; - } - - x.splice(0, 1); - - return { - type: type, - y: y, - x: x - } - - } - } else if(data.rows) { - let rows = data.rows; - x = rows[0]; - - rows.map((row, i) => { - if(i === 0) { - x = row; - } else { - y.push({ - title: 'data' + i, - values: row, - }); - } - }); - - return { - type: type, - y: y, - x: x - } - } - } -} - // Helpers frappe.chart.utils = {}; frappe.chart.utils.float_2 = d => parseFloat(d.toFixed(2));