diff --git a/.eslintrc b/.eslintrc index 44af7b458f..ade1623262 100644 --- a/.eslintrc +++ b/.eslintrc @@ -118,6 +118,8 @@ "getCookie": true, "getCookies": true, "get_url_arg": true, - "QUnit": true + "QUnit": true, + "Snap": true, + "mina": true } } diff --git a/frappe/docs/assets/img/desk/animated_line_graph.gif b/frappe/docs/assets/img/desk/animated_line_graph.gif new file mode 100644 index 0000000000..0b0e7b212c Binary files /dev/null and b/frappe/docs/assets/img/desk/animated_line_graph.gif differ diff --git a/frappe/docs/assets/img/desk/bar_graph.png b/frappe/docs/assets/img/desk/bar_graph.png index d25254af6d..b3bd89cc88 100644 Binary files a/frappe/docs/assets/img/desk/bar_graph.png and b/frappe/docs/assets/img/desk/bar_graph.png differ diff --git a/frappe/docs/assets/img/desk/line_graph.png b/frappe/docs/assets/img/desk/line_graph.png deleted file mode 100644 index 02c60c7c18..0000000000 Binary files a/frappe/docs/assets/img/desk/line_graph.png and /dev/null differ diff --git a/frappe/docs/assets/img/desk/line_graph_sales.png b/frappe/docs/assets/img/desk/line_graph_sales.png new file mode 100644 index 0000000000..0e70ae0031 Binary files /dev/null and b/frappe/docs/assets/img/desk/line_graph_sales.png differ diff --git a/frappe/docs/assets/img/desk/percentage_graph.png b/frappe/docs/assets/img/desk/percentage_graph.png new file mode 100644 index 0000000000..3a25d59479 Binary files /dev/null and b/frappe/docs/assets/img/desk/percentage_graph.png differ diff --git a/frappe/docs/user/en/guides/desk/making_graphs.md b/frappe/docs/user/en/guides/desk/making_graphs.md index 9234fa58b4..720c9217bf 100644 --- a/frappe/docs/user/en/guides/desk/making_graphs.md +++ b/frappe/docs/user/en/guides/desk/making_graphs.md @@ -1,61 +1,100 @@ # Making Graphs -The Frappe UI **Graph** object enables you to render simple line and bar graphs for a discreet set of data points. You can also set special checkpoint values and summary stats. +The Frappe UI **Graph** object enables you to render simple line, bar or percentage graphs for single or multiple discreet sets of data points. You can also set special checkpoint values and summary stats. ### Example: Line graph -Here's is an example of a simple sales graph: - - render_graph: function() { - $('.form-graph').empty(); - - var months = ['Aug', 'Sep', 'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']; - var values = [2410, 3100, 1700, 1200, 2700, 1600, 2740, 1000, 850, 1500, 400, 2013]; - - var goal = 2500; - var current_val = 2013; - - new frappe.ui.Graph({ - parent: $('.form-graph'), - width: 700, - height: 140, - mode: 'line-graph', - - title: 'Sales', - subtitle: 'Monthly', - y_values: values, - x_points: months, - - specific_values: [ - { - name: "Goal", - line_type: "dashed", // "dashed" or "solid" - value: goal - }, - ], - summary_values: [ - { - name: "This month", - color: 'green', // Indicator colors: 'grey', 'blue', 'red', - // 'green', 'orange', 'purple', 'darkgrey', - // 'black', 'yellow', 'lightblue' - value: '₹ ' + current_val - }, - { - name: "Goal", - color: 'blue', - value: '₹ ' + goal - }, - { - name: "Completed", - color: 'green', - value: (current_val/goal*100).toFixed(1) + "%" - } - ] - }); - }, - - - -Setting the mode to 'bar-graph': +Here's an example of a simple sales graph: + + // Data + let months = ['August, 2016', 'September, 2016', 'October, 2016', 'November, 2016', + 'December, 2016', 'January, 2017', 'February, 2017', 'March, 2017', 'April, 2017', + 'May, 2017', 'June, 2017', 'July, 2017']; + + let values1 = [24100, 31000, 17000, 12000, 27000, 16000, 27400, 11000, 8500, 15000, 4000, 20130]; + let values2 = [17890, 10400, 12350, 20400, 17050, 23000, 7100, 13800, 16000, 20400, 11000, 13000]; + let goal = 25000; + let current_val = 20130; + + let g = new frappe.ui.Graph({ + parent: $('.form-graph').empty(), + height: 200, // optional + mode: 'line', // 'line', 'bar' or 'percentage' + + title: 'Sales', + subtitle: 'Monthly', + + y: [ + { + title: 'Data 1', + values: values1, + formatted: values1.map(d => '$ ' + d), + color: 'green' // Indicator colors: 'grey', 'blue', 'red', + // 'green', 'light-green', 'orange', 'purple', 'darkgrey', + // 'black', 'yellow', 'lightblue' + }, + { + title: 'Data 2', + values: values2, + formatted: values2.map(d => '$ ' + d), + color: 'light-green' + } + ], + + x: { + values: months.map(d => d.substring(0, 3)), + formatted: months + }, + + specific_values: [ + { + name: 'Goal', + line_type: 'dashed', // 'dashed' or 'solid' + value: goal + }, + ], + + summary: [ + { + name: 'This month', + color: 'orange', + value: '$ ' + current_val + }, + { + name: 'Goal', + color: 'blue', + value: '$ ' + goal + }, + { + name: 'Completed', + color: 'green', + value: (current_val/goal*100).toFixed(1) + "%" + } + ] + }); + + + +`bar` mode yeilds: + +You can set the `colors` property of `x` to an array of color values for `percentage` mode: + + + +You can also change the values of an existing graph with a new set of `y` values: + + setTimeout(() => { + g.change_values([ + { + values: data[2], + formatted: data[2].map(d => d + 'L') + }, + { + values: data[3], + formatted: data[3].map(d => d + 'L') + } + ]); + }, 1000); + + diff --git a/frappe/public/build.json b/frappe/public/build.json index b350c8151a..054421286e 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -52,7 +52,8 @@ "public/css/desktop.css", "public/css/form.css", "public/css/mobile.css", - "public/css/kanban.css" + "public/css/kanban.css", + "public/css/graphs.css" ], "css/frappe-rtl.css": [ "public/css/bootstrap-rtl.css", @@ -164,7 +165,7 @@ "public/js/frappe/query_string.js", "public/js/frappe/ui/charts.js", - "public/js/frappe/ui/graph.js", + "public/js/frappe/ui/graphs.js", "public/js/frappe/ui/comment.js", "public/js/frappe/misc/rating_icons.html", diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 0d21271862..c56811e892 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -678,80 +678,6 @@ select.form-control { padding: 10px; margin: 10px; } -.graph-container .graphics { - margin-top: 10px; - padding: 10px 0px; -} -.graph-container .stats-group { - display: flex; - justify-content: space-around; - flex: 1; -} -.graph-container .stats-container { - display: flex; - justify-content: space-around; -} -.graph-container .stats-container .stats { - padding-bottom: 15px; -} -.graph-container .stats-container .stats-title { - color: #8D99A6; -} -.graph-container .stats-container .stats-value { - font-size: 20px; - font-weight: 300; -} -.graph-container .stats-container .stats-description { - font-size: 12px; - color: #8D99A6; -} -.graph-container .stats-container .graph-data .stats-value { - color: #98d85b; -} -.bar-graph .axis, -.line-graph .axis { - font-size: 10px; - fill: #6a737d; -} -.bar-graph .axis line, -.line-graph .axis line { - stroke: rgba(27, 31, 35, 0.1); -} -.data-points circle { - fill: #28a745; - stroke: #fff; - stroke-width: 2; -} -.data-points g.mini { - fill: #98d85b; -} -.data-points path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -.line-graph .path { - fill: none; - stroke: #28a745; - stroke-opacity: 1; - stroke-width: 2px; -} -line.dashed { - stroke-dasharray: 5,3; -} -.tick.x-axis-label { - display: block; -} -.tick .specific-value { - text-anchor: start; -} -.tick .y-value-text { - text-anchor: end; -} -.tick .x-value-text { - text-anchor: middle; -} body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { height: 80px !important; } diff --git a/frappe/public/css/graphs.css b/frappe/public/css/graphs.css new file mode 100644 index 0000000000..a9fdf62dc9 --- /dev/null +++ b/frappe/public/css/graphs.css @@ -0,0 +1,274 @@ +/* graphs */ +.graph-container .graph-focus-margin { + margin: 0px 5%; +} +.graph-container .graph-graphics { + margin-top: 10px; + padding: 10px 0px; + position: relative; +} +.graph-container .graph-stats-group { + display: flex; + justify-content: space-around; + flex: 1; +} +.graph-container .graph-stats-container { + display: flex; + justify-content: space-around; + padding-top: 10px; +} +.graph-container .graph-stats-container .stats { + padding-bottom: 15px; +} +.graph-container .graph-stats-container .stats-title { + color: #8D99A6; +} +.graph-container .graph-stats-container .stats-value { + font-size: 20px; + font-weight: 300; +} +.graph-container .graph-stats-container .stats-description { + font-size: 12px; + color: #8D99A6; +} +.graph-container .graph-stats-container .graph-data .stats-value { + color: #98d85b; +} +.graph-container .bar-graph .axis, +.graph-container .line-graph .axis { + font-size: 10px; + fill: #6a737d; +} +.graph-container .bar-graph .axis line, +.graph-container .line-graph .axis line { + stroke: rgba(27, 31, 35, 0.1); +} +.graph-container .percentage-graph { + margin-top: 35px; +} +.graph-container .percentage-graph .progress { + margin-bottom: 0px; +} +.graph-container .graph-data-points circle { + stroke: #fff; + stroke-width: 2; +} +.graph-container .graph-data-points path { + fill: none; + stroke-opacity: 1; + stroke-width: 2px; +} +.graph-container line.graph-dashed { + stroke-dasharray: 5,3; +} +.graph-container .tick.x-axis-label { + display: block; +} +.graph-container .tick .specific-value { + text-anchor: start; +} +.graph-container .tick .y-value-text { + text-anchor: end; +} +.graph-container .tick .x-value-text { + text-anchor: middle; +} +.graph-container .graph-svg-tip { + position: absolute; + z-index: 99999; + padding: 10px; + font-size: 12px; + color: #959da5; + text-align: center; + background: rgba(0, 0, 0, 0.8); + border-radius: 3px; +} +.graph-container .graph-svg-tip.comparison { + padding: 0; + text-align: left; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison .title { + display: block; + padding: 10px; + margin: 0; + font-weight: 600; + line-height: 1; + pointer-events: none; +} +.graph-container .graph-svg-tip.comparison ul { + margin: 0; + white-space: nowrap; + list-style: none; +} +.graph-container .graph-svg-tip.comparison li { + display: inline-block; + padding: 5px 10px; +} +.graph-container .graph-svg-tip ul, +.graph-container .graph-svg-tip ol { + padding-left: 0; + display: flex; +} +.graph-container .graph-svg-tip ul.data-point-list li { + min-width: 90px; + flex: 1; +} +.graph-container .graph-svg-tip strong { + color: #dfe2e5; +} +.graph-container .graph-svg-tip::after { + position: absolute; + bottom: -10px; + left: 50%; + width: 5px; + height: 5px; + margin: 0 0 0 -5px; + content: " "; + border: 5px solid transparent; + border-top-color: rgba(0, 0, 0, 0.8); +} +.graph-container .stroke.grey { + stroke: #F0F4F7; +} +.graph-container .stroke.blue { + stroke: #5e64ff; +} +.graph-container .stroke.red { + stroke: #ff5858; +} +.graph-container .stroke.light-green { + stroke: #98d85b; +} +.graph-container .stroke.green { + stroke: #28a745; +} +.graph-container .stroke.orange { + stroke: #ffa00a; +} +.graph-container .stroke.purple { + stroke: #743ee2; +} +.graph-container .stroke.darkgrey { + stroke: #b8c2cc; +} +.graph-container .stroke.black { + stroke: #36414C; +} +.graph-container .stroke.yellow { + stroke: #FEEF72; +} +.graph-container .stroke.light-blue { + stroke: #7CD6FD; +} +.graph-container .stroke.lightblue { + stroke: #7CD6FD; +} +.graph-container .fill.grey { + fill: #F0F4F7; +} +.graph-container .fill.blue { + fill: #5e64ff; +} +.graph-container .fill.red { + fill: #ff5858; +} +.graph-container .fill.light-green { + fill: #98d85b; +} +.graph-container .fill.green { + fill: #28a745; +} +.graph-container .fill.orange { + fill: #ffa00a; +} +.graph-container .fill.purple { + fill: #743ee2; +} +.graph-container .fill.darkgrey { + fill: #b8c2cc; +} +.graph-container .fill.black { + fill: #36414C; +} +.graph-container .fill.yellow { + fill: #FEEF72; +} +.graph-container .fill.light-blue { + fill: #7CD6FD; +} +.graph-container .fill.lightblue { + fill: #7CD6FD; +} +.graph-container .background.grey { + background: #F0F4F7; +} +.graph-container .background.blue { + background: #5e64ff; +} +.graph-container .background.red { + background: #ff5858; +} +.graph-container .background.light-green { + background: #98d85b; +} +.graph-container .background.green { + background: #28a745; +} +.graph-container .background.orange { + background: #ffa00a; +} +.graph-container .background.purple { + background: #743ee2; +} +.graph-container .background.darkgrey { + background: #b8c2cc; +} +.graph-container .background.black { + background: #36414C; +} +.graph-container .background.yellow { + background: #FEEF72; +} +.graph-container .background.light-blue { + background: #7CD6FD; +} +.graph-container .background.lightblue { + background: #7CD6FD; +} +.graph-container .border-top.grey { + border-top: 3px solid #F0F4F7; +} +.graph-container .border-top.blue { + border-top: 3px solid #5e64ff; +} +.graph-container .border-top.red { + border-top: 3px solid #ff5858; +} +.graph-container .border-top.light-green { + border-top: 3px solid #98d85b; +} +.graph-container .border-top.green { + border-top: 3px solid #28a745; +} +.graph-container .border-top.orange { + border-top: 3px solid #ffa00a; +} +.graph-container .border-top.purple { + border-top: 3px solid #743ee2; +} +.graph-container .border-top.darkgrey { + border-top: 3px solid #b8c2cc; +} +.graph-container .border-top.black { + border-top: 3px solid #36414C; +} +.graph-container .border-top.yellow { + border-top: 3px solid #FEEF72; +} +.graph-container .border-top.light-blue { + border-top: 3px solid #7CD6FD; +} +.graph-container .border-top.lightblue { + border-top: 3px solid #7CD6FD; +} diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index d27aa619e0..ca2b4ab9bd 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -418,9 +418,8 @@ frappe.ui.form.Dashboard = Class.extend({ this.graph_area.empty().removeClass('hidden'); $.extend(args, { parent: me.graph_area, - width: 710, - height: 140, - mode: 'line-graph' + mode: 'line', + height: 140 }); new frappe.ui.Graph(args); diff --git a/frappe/public/js/frappe/ui/graph.js b/frappe/public/js/frappe/ui/graph.js deleted file mode 100644 index 2347f13b0d..0000000000 --- a/frappe/public/js/frappe/ui/graph.js +++ /dev/null @@ -1,308 +0,0 @@ -// specific_values = [ -// { -// name: "Average", -// line_type: "dashed", // "dashed" or "solid" -// value: 10 -// }, - -// summary_values = [ -// { -// name: "Total", -// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', -// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' -// value: 80 -// } -// ] - -frappe.ui.Graph = class Graph { - constructor({ - parent = null, - - width = 0, height = 0, - title = '', subtitle = '', - - y_values = [], - x_points = [], - - specific_values = [], - summary_values = [], - - color = '', - mode = '', - } = {}) { - - if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { - if(mode === 'line-graph') { - return new frappe.ui.LineGraph(arguments[0]); - } else if(mode === 'bar-graph') { - return new frappe.ui.BarGraph(arguments[0]); - } - } - - this.parent = parent; - - this.width = width; - this.height = height; - - this.title = title; - this.subtitle = subtitle; - - this.y_values = y_values; - this.x_points = x_points; - - this.specific_values = specific_values; - this.summary_values = summary_values; - - this.color = color; - this.mode = mode; - - this.$graph = null; - - frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); - } - - setup() { - this.setup_container(); - this.refresh(); - } - - refresh() { - this.setup_values(); - this.setup_components(); - this.make_y_axis(); - this.make_x_axis(); - this.make_units(); - if(this.specific_values.length > 0) { - this.show_specific_values(); - } - this.setup_group(); - - if(this.summary_values.length > 0) { - this.show_summary(); - } - } - - setup_container() { - this.container = $('
') - .addClass('graph-container') - .append($(`
${this.title}
`)) - .append($(`
${this.subtitle}
`)) - .append($(`
`)) - .append($(`
`)) - .appendTo(this.parent); - - let $graphics = this.container.find('.graphics'); - this.$stats_container = this.container.find('.stats-container'); - - this.$graph = $('
') - .addClass(this.mode) - .appendTo($graphics); - - this.$svg = $(``); - this.$graph.append(this.$svg); - - this.snap = new Snap(this.$svg[0]); - } - - setup_values() { - this.upper_graph_bound = this.get_upper_limit_and_parts(this.y_values)[0]; - this.y_axis = this.get_y_axis(this.y_values); - this.avg_unit_width = (this.width-100)/(this.x_points.length - 1); - } - - setup_components() { - this.y_axis_group = this.snap.g().attr({ - class: "y axis" - }); - - this.x_axis_group = this.snap.g().attr({ - class: "x axis" - }); - - this.graph_list = this.snap.g().attr({ - class: "data-points", - }); - - this.specific_y_lines = this.snap.g().attr({ - class: "specific axis", - }); - } - - setup_group() { - this.snap.g( - this.y_axis_group, - this.x_axis_group, - this.graph_list, - this.specific_y_lines - ).attr({ - transform: "translate(60, 10)" // default - }); - } - - show_specific_values() { - this.specific_values.map(d => { - this.specific_y_lines.add(this.snap.g( - this.snap.line(0, 0, this.width - 70, 0).attr({ - class: d.line_type === "dashed" ? "dashed": "" - }), - this.snap.text(this.width - 95, 0, d.name.toUpperCase()).attr({ - dy: ".32em", - class: "specific-value", - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - 100/(this.upper_graph_bound/d.value) })` - })); - }); - } - - show_summary() { - this.summary_values.map(d => { - this.$stats_container.append($(`
- ${d.name}: ${d.value} -
`)); - }); - } - - // Helpers - get_upper_limit_and_parts(array) { - let specific_values = this.specific_values.map(d => d.value); - let max_val = parseInt(Math.max(...array, ...specific_values)); - if((max_val+"").length <= 1) { - return [10, 5]; - } else { - let multiplier = Math.pow(10, ((max_val+"").length - 1)); - let significant = Math.ceil(max_val/multiplier); - if(significant % 2 !== 0) significant++; - let parts = (significant < 5) ? significant : significant/2; - return [significant * multiplier, parts]; - } - } - - get_y_axis(array) { - let upper_limit, parts; - [upper_limit, parts] = this.get_upper_limit_and_parts(array); - let y_axis = []; - for(var i = 0; i <= parts; i++){ - y_axis.push(upper_limit / parts * i); - } - return y_axis; - } -}; - -frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - setup_values() { - super.setup_values(); - this.avg_unit_width = (this.width-50)/(this.x_points.length + 2); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, this.width, 0), - this.snap.text(-3, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,100)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, 6), - this.snap.text(0, 9, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick x-axis-label", - transform: `translate(${ ((this.avg_unit_width - 5)*3/2) + i * (this.avg_unit_width + 5) }, 0)` - })); - }); - } - - make_units() { - this.y_values.map((value, i) => { - this.graph_list.add(this.snap.g( - this.snap.rect( - 0, - (100 - 100/(this.upper_graph_bound/value)), - this.avg_unit_width - 5, - 100/(this.upper_graph_bound/value) - ) - ).attr({ - class: "bar mini", - transform: `translate(${ (this.avg_unit_width - 5) + i * (this.avg_unit_width + 5) }, 0)`, - })); - }); - } -}; - -frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { - constructor(args = {}) { - super(args); - } - - make_y_axis() { - this.y_axis.map((point) => { - this.y_axis_group.add(this.snap.g( - this.snap.line(0, 0, -6, 0), - this.snap.text(-9, 0, point+"").attr({ - dy: ".32em", - class: "y-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(0, ${100 - (100/(this.y_axis.length-1) - * this.y_axis.indexOf(point)) })` - })); - }); - } - - make_x_axis() { - this.x_axis_group.attr({ - transform: "translate(0,-7)" - }); - this.x_points.map((point, i) => { - this.x_axis_group.add(this.snap.g( - this.snap.line(0, 0, 0, this.height - 25), - this.snap.text(0, this.height - 15, point).attr({ - dy: ".71em", - class: "x-value-text" - }) - ).attr({ - class: "tick", - transform: `translate(${ i * this.avg_unit_width }, 0)` - })); - }); - } - - make_units() { - let points_list = []; - this.y_values.map((value, i) => { - let x = i * this.avg_unit_width; - let y = (100 - 100/(this.upper_graph_bound/value)); - this.graph_list.add(this.snap.circle( x, y, 4)); - points_list.push(x+","+y); - }); - - this.make_path("M"+points_list.join("L")); - } - - make_path(path_str) { - this.graph_list.prepend(this.snap.path(path_str)); - } - -}; diff --git a/frappe/public/js/frappe/ui/graphs.js b/frappe/public/js/frappe/ui/graphs.js new file mode 100644 index 0000000000..f45ced98ba --- /dev/null +++ b/frappe/public/js/frappe/ui/graphs.js @@ -0,0 +1,569 @@ +// specific_values = [ +// { +// name: "Average", +// line_type: "dashed", // "dashed" or "solid" +// value: 10 +// }, + +// summary = [ +// { +// name: "Total", +// color: 'blue', // Indicator colors: 'grey', 'blue', 'red', 'green', 'orange', +// // 'purple', 'darkgrey', 'black', 'yellow', 'lightblue' +// value: 80 +// } +// ] + +// Graph: Abstract object +frappe.ui.Graph = class Graph { + constructor({ + parent = null, + height = 240, + + title = '', subtitle = '', + + y = [], + x = [], + + specific_values = [], + summary = [], + + color = 'blue', + mode = '', + }) { + + if(Object.getPrototypeOf(this) === frappe.ui.Graph.prototype) { + if(mode === 'line') { + return new frappe.ui.LineGraph(arguments[0]); + } else if(mode === 'bar') { + return new frappe.ui.BarGraph(arguments[0]); + } else if(mode === 'percentage') { + return new frappe.ui.PercentageGraph(arguments[0]); + } + } + + this.parent = parent; + this.base_height = height; + this.height = height - 40; + + this.translate_x = 60; + this.translate_y = 10; + + this.title = title; + this.subtitle = subtitle; + + this.y = y; + this.x = x; + + this.specific_values = specific_values; + this.summary = summary; + + this.color = color; + this.mode = mode; + + this.$graph = null; + + // Validate all arguments + + frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); + } + + setup() { + this.bind_window_event(); + this.refresh(); + } + + bind_window_event() { + $(window).on('resize orientationChange', () => { + this.refresh(); + }); + } + + refresh() { + + this.base_width = this.parent.width() - 20; + this.width = this.base_width - 100; + + this.setup_container(); + + this.setup_values(); + + this.setup_utils(); + + this.setup_components(); + this.make_graph_components(); + + this.make_tooltip(); + + if(this.summary.length > 0) { + this.show_custom_summary(); + } else { + this.show_summary(); + } + } + + setup_container() { + // Graph needs a dedicated parent element + this.parent.empty(); + + this.container = $('
') + .addClass('graph-container') + .append($(`
${this.title}
`)) + .append($(`
${this.subtitle}
`)) + .append($(`
`)) + .append($(`
`)) + .appendTo(this.parent); + + this.$graphics = this.container.find('.graph-graphics'); + this.$stats_container = this.container.find('.graph-stats-container'); + + this.$graph = $('
') + .addClass(this.mode + '-graph') + .appendTo(this.$graphics); + + this.$graph.append(this.make_graph_area()); + } + + make_graph_area() { + this.$svg = $(``); + this.snap = new Snap(this.$svg[0]); + return this.$svg; + } + + setup_values() { + // Multiplier + let all_values = this.specific_values.map(d => d.value); + this.y.map(d => { + all_values = all_values.concat(d.values); + }); + [this.upper_limit, this.parts] = this.get_upper_limit_and_parts(all_values); + this.multiplier = this.height / this.upper_limit; + + // Baselines + this.set_avg_unit_width_and_x_offset(); + + this.x_axis_values = this.x.values.map((d, i) => this.x_offset + i * this.avg_unit_width); + this.y_axis_values = this.get_y_axis_values(this.upper_limit, this.parts); + + // Data points + this.y.map(d => { + d.y_tops = d.values.map( val => this.height - val * this.multiplier ); + d.data_units = []; + }); + + this.calc_min_tops(); + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length - 1); + this.x_offset = 0; + } + + calc_min_tops() { + this.y_min_tops = new Array(this.x_axis_values.length).fill(9999); + this.y.map(d => { + d.y_tops.map( (y_top, i) => { + if(y_top < this.y_min_tops[i]) { + this.y_min_tops[i] = y_top; + } + }); + }); + } + + setup_components() { + this.y_axis_group = this.snap.g().attr({ class: "y axis" }); + this.x_axis_group = this.snap.g().attr({ class: "x axis" }); + this.data_units = this.snap.g().attr({ class: "graph-data-points" }); + this.specific_y_lines = this.snap.g().attr({ class: "specific axis" }); + } + + make_graph_components() { + this.make_y_axis(); + this.make_x_axis(); + + this.y.map((d, i) => { + this.make_units(d.y_tops, d.color, i); + this.make_path(d); + }); + + if(this.specific_values.length > 0) { + this.show_specific_values(); + } + this.setup_group(); + } + + setup_group() { + this.snap.g( + this.y_axis_group, + this.x_axis_group, + this.data_units, + this.specific_y_lines + ).attr({ + transform: `translate(${this.translate_x}, ${this.translate_y})` + }); + } + + // make HORIZONTAL lines for y values + make_y_axis() { + let width, text_end_at = -9, label_class = '', start_at = 0; + if(this.y_axis_mode === 'span') { // long spanning lines + width = this.width + 6; + start_at = -6; + } else if(this.y_axis_mode === 'tick'){ // short label lines + width = -6; + label_class = 'y-axis-label'; + } + + this.y_axis_values.map((point) => { + this.y_axis_group.add(this.snap.g( + this.snap.line(start_at, 0, width, 0), + this.snap.text(text_end_at, 0, point+"").attr({ + dy: ".32em", + class: "y-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(0, ${this.height - point * this.multiplier })` + })); + }); + } + + // make VERTICAL lines for x values + make_x_axis() { + let start_at, height, text_start_at, label_class = ''; + if(this.x_axis_mode === 'span') { // long spanning lines + start_at = -7; + height = this.height + 15; + text_start_at = this.height + 25; + } else if(this.x_axis_mode === 'tick'){ // short label lines + start_at = this.height; + height = 6; + text_start_at = 9; + label_class = 'x-axis-label'; + } + + this.x_axis_group.attr({ + transform: `translate(0,${start_at})` + }); + this.x.values.map((point, i) => { + this.x_axis_group.add(this.snap.g( + this.snap.line(0, 0, 0, height), + this.snap.text(0, text_start_at, point).attr({ + dy: ".71em", + class: "x-value-text" + }) + ).attr({ + class: `tick ${label_class}`, + transform: `translate(${ this.x_axis_values[i] }, 0)` + })); + }); + } + + make_units(y_values, color, dataset_index) { + let d = this.unit_args; + y_values.map((y, i) => { + let data_unit = this.draw[d.type](this.x_axis_values[i], + y, d.args, color, dataset_index); + this.data_units.add(data_unit); + this.y[dataset_index].data_units.push(data_unit); + }); + } + + make_path() { } + + make_tooltip() { + this.tip = $(`
+ +
    +
+
`).attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }).appendTo(this.$graphics); + + this.tip_title = this.tip.find('.title'); + this.tip_data_point_list = this.tip.find('.data-point-list'); + + this.bind_tooltip(); + } + + bind_tooltip() { + this.$graphics.on('mousemove', (e) => { + let offset = $(this.$graphics).offset(); + var relX = e.pageX - offset.left - this.translate_x; + var relY = e.pageY - offset.top - this.translate_y; + + if(relY < this.height) { + for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) { + let x_val = this.x_axis_values[i]; + if(relX > x_val - this.avg_unit_width/2) { + let x = x_val - this.tip.width()/2 + this.translate_x; + let y = this.y_min_tops[i] - this.tip.height() + this.translate_y; + + this.fill_tooltip(i); + + this.tip.attr({ + style: `top: ${y}px; left: ${x-0.5}px; opacity: 1; pointer-events: none;` + }); + break; + } + } + } else { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + } + }); + + this.$graphics.on('mouseleave', () => { + this.tip.attr({ + style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` + }); + }); + } + + fill_tooltip(i) { + this.tip_title.html(this.x.formatted && this.x.formatted.length>0 + ? this.x.formatted[i] : this.x.values[i]); + this.tip_data_point_list.empty(); + this.y.map(y_set => { + let $li = $(`
  • + + ${y_set.formatted ? y_set.formatted[i] : y_set.values[i]} + + ${y_set.title ? y_set.title : '' } +
  • `).addClass(`border-top ${y_set.color}`); + this.tip_data_point_list.append($li); + }); + } + + show_specific_values() { + this.specific_values.map(d => { + this.specific_y_lines.add(this.snap.g( + this.snap.line(0, 0, this.width, 0).attr({ + class: d.line_type === "dashed" ? "graph-dashed": "" + }), + this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ + dy: ".32em", + class: "specific-value", + }) + ).attr({ + class: "tick", + transform: `translate(0, ${this.height - d.value * this.multiplier })` + })); + }); + } + + show_summary() { } + + show_custom_summary() { + this.summary.map(d => { + this.$stats_container.append($(`
    + ${d.name}: ${d.value} +
    `)); + }); + } + + change_values(new_y) { + let u = this.unit_args; + this.y.map((d, i) => { + let new_d = new_y[i]; + new_d.y_tops = new_d.values.map(val => this.height - val * this.multiplier); + + // below is equal to this.y[i].data_units.. + d.data_units.map((unit, j) => { + let current_y_top = d.y_tops[j]; + let current_height = this.height - current_y_top; + + let new_y_top = new_d.y_tops[j]; + let new_height = current_height - (new_y_top - current_y_top); + + this.animate[u.type](unit, new_y_top, {new_height: new_height}); + }); + }); + + // Replace values and formatted and tops + this.y.map((d, i) => { + let new_d = new_y[i]; + [d.values, d.formatted, d.y_tops] = [new_d.values, new_d.formatted, new_d.y_tops]; + }); + + this.calc_min_tops(); + + // create new x,y pair string and animate path + if(this.y[0].path) { + new_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"); + this.y[i].path.animate({d:new_path_str}, 300, mina.easein); + }); + } + } + + // Helpers + get_strwidth(string) { + return string.length * 8; + } + + get_upper_limit_and_parts(array) { + let max_val = parseInt(Math.max(...array)); + if((max_val+"").length <= 1) { + return [10, 5]; + } else { + let multiplier = Math.pow(10, ((max_val+"").length - 1)); + let significant = Math.ceil(max_val/multiplier); + if(significant % 2 !== 0) significant++; + let parts = (significant < 5) ? significant : significant/2; + return [significant * multiplier, parts]; + } + } + + get_y_axis_values(upper_limit, parts) { + let y_axis = []; + for(var i = 0; i <= parts; i++){ + y_axis.push(upper_limit / parts * i); + } + return y_axis; + } + + // Objects + setup_utils() { + this.draw = { + 'bar': (x, y, args, color, index) => { + let total_width = this.avg_unit_width - args.space_width; + let start_x = x - total_width/2; + + let width = total_width / args.no_of_datasets; + let current_x = start_x + width * index; + return this.snap.rect(current_x, y, width, this.height - y).attr({ + class: `bar mini fill ${color}` + }); + }, + 'dot': (x, y, args, color) => { + return this.snap.circle(x, y, args.radius).attr({ + class: `fill ${color}` + }); + } + }; + + this.animate = { + 'bar': (bar, new_y, args) => { + bar.animate({height: args.new_height, y: new_y}, 300, mina.easein); + }, + 'dot': (dot, new_y) => { + dot.animate({cy: new_y}, 300, mina.easein); + } + }; + } +}; + +frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + var me = this; + super.setup_values(); + this.x_offset = this.avg_unit_width; + this.y_axis_mode = 'span'; + this.x_axis_mode = 'tick'; + this.unit_args = { + type: 'bar', + args: { + space_width: this.y.length > 1 ? + me.avg_unit_width/2 : me.avg_unit_width/8, + no_of_datasets: this.y.length + } + }; + } + + set_avg_unit_width_and_x_offset() { + this.avg_unit_width = this.width/(this.x.values.length + 1); + this.x_offset = this.avg_unit_width; + } +}; + +frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + setup_values() { + super.setup_values(); + this.y_axis_mode = 'tick'; + this.x_axis_mode = 'span'; + this.unit_args = { + type: 'dot', + args: { radius: 4 } + }; + } + + make_path(d) { + let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); + let path_str = "M"+points_list.join("L"); + d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`}); + this.data_units.prepend(d.path); + } +}; + +frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { + constructor(args = {}) { + super(args); + } + + make_graph_area() { + this.$graphics.addClass('graph-focus-margin'); + this.$stats_container.addClass('graph-focus-margin').attr({ + style: `padding-top: 0px; margin-bottom: 30px;` + }); + this.$div = $(`
    +
    +
    `); + this.$chart = this.$div.find('.progress-chart'); + return this.$div; + } + + setup_values() { + this.x.totals = this.x.values.map((d, i) => { + let total = 0; + this.y.map(e => { + total += e.values[i]; + }); + return total; + }); + + // Calculate x unit distances for tooltips + } + + setup_utils() { } + setup_components() { + this.$percentage_bar = $(`
    +
    `).appendTo(this.$chart); + } + + make_graph_components() { + let grand_total = this.x.totals.reduce((a, b) => a + b, 0); + this.x.units = []; + this.x.totals.map((total, i) => { + let $part = $(`
    `); + this.x.units.push($part); + this.$percentage_bar.append($part); + }); + } + + make_tooltip() { } + + show_summary() { + let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values; + this.x.totals.map((d, i) => { + this.$stats_container.append($(`
    + + ${values[i]}: + ${d} + +
    `)); + }); + } +}; diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 3162cd5db1..b3762fea6f 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -43,7 +43,7 @@ frappe.views.QueryReport = Class.extend({ this.wrapper = $("
    ").appendTo(this.page.main); $('\ \ -
    \ +
    \