* remove hardcoded height and width * [graph] relative measures, offsets, generalise all base jobs * Multiple datasets! (individually colored) * [graph] Tooltips * [graph] animate and change values * [graph] percentage graph * [graph] custom height, window resize refresh * [graph] style changes * [graph] update docs * [graphs] make graphs.less * [graphs] prefix classes with 'graph-', nest inside parent classversion-14
@@ -118,6 +118,8 @@ | |||||
"getCookie": true, | "getCookie": true, | ||||
"getCookies": true, | "getCookies": true, | ||||
"get_url_arg": true, | "get_url_arg": true, | ||||
"QUnit": true | |||||
"QUnit": true, | |||||
"Snap": true, | |||||
"mina": true | |||||
} | } | ||||
} | } |
@@ -1,61 +1,100 @@ | |||||
# Making Graphs | # 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 | ### 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) + "%" | |||||
} | |||||
] | |||||
}); | |||||
}, | |||||
<img src="{{docs_base_url}}/assets/img/desk/line_graph.png" class="screenshot"> | |||||
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) + "%" | |||||
} | |||||
] | |||||
}); | |||||
<img src="{{docs_base_url}}/assets/img/desk/line_graph_sales.png" class="screenshot"> | |||||
`bar` mode yeilds: | |||||
<img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot"> | <img src="{{docs_base_url}}/assets/img/desk/bar_graph.png" class="screenshot"> | ||||
You can set the `colors` property of `x` to an array of color values for `percentage` mode: | |||||
<img src="{{docs_base_url}}/assets/img/desk/percentage_graph.png" class="screenshot"> | |||||
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); | |||||
<img src="{{docs_base_url}}/assets/img/desk/animated_line_graph.gif" class="screenshot"> |
@@ -52,7 +52,8 @@ | |||||
"public/css/desktop.css", | "public/css/desktop.css", | ||||
"public/css/form.css", | "public/css/form.css", | ||||
"public/css/mobile.css", | "public/css/mobile.css", | ||||
"public/css/kanban.css" | |||||
"public/css/kanban.css", | |||||
"public/css/graphs.css" | |||||
], | ], | ||||
"css/frappe-rtl.css": [ | "css/frappe-rtl.css": [ | ||||
"public/css/bootstrap-rtl.css", | "public/css/bootstrap-rtl.css", | ||||
@@ -164,7 +165,7 @@ | |||||
"public/js/frappe/query_string.js", | "public/js/frappe/query_string.js", | ||||
"public/js/frappe/ui/charts.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/ui/comment.js", | ||||
"public/js/frappe/misc/rating_icons.html", | "public/js/frappe/misc/rating_icons.html", | ||||
@@ -678,80 +678,6 @@ select.form-control { | |||||
padding: 10px; | padding: 10px; | ||||
margin: 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"] { | body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { | ||||
height: 80px !important; | height: 80px !important; | ||||
} | } |
@@ -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; | |||||
} |
@@ -418,9 +418,8 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
this.graph_area.empty().removeClass('hidden'); | this.graph_area.empty().removeClass('hidden'); | ||||
$.extend(args, { | $.extend(args, { | ||||
parent: me.graph_area, | parent: me.graph_area, | ||||
width: 710, | |||||
height: 140, | |||||
mode: 'line-graph' | |||||
mode: 'line', | |||||
height: 140 | |||||
}); | }); | ||||
new frappe.ui.Graph(args); | new frappe.ui.Graph(args); | ||||
@@ -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 = $('<div>') | |||||
.addClass('graph-container') | |||||
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`)) | |||||
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`)) | |||||
.append($(`<div class="graphics"></div>`)) | |||||
.append($(`<div class="stats-container"></div>`)) | |||||
.appendTo(this.parent); | |||||
let $graphics = this.container.find('.graphics'); | |||||
this.$stats_container = this.container.find('.stats-container'); | |||||
this.$graph = $('<div>') | |||||
.addClass(this.mode) | |||||
.appendTo($graphics); | |||||
this.$svg = $(`<svg class="svg" width="${this.width}" height="${this.height}"></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($(`<div class="stats"> | |||||
<span class="indicator ${d.color}">${d.name}: ${d.value}</span> | |||||
</div>`)); | |||||
}); | |||||
} | |||||
// 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)); | |||||
} | |||||
}; |
@@ -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 = $('<div>') | |||||
.addClass('graph-container') | |||||
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`)) | |||||
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`)) | |||||
.append($(`<div class="graph-graphics"></div>`)) | |||||
.append($(`<div class="graph-stats-container"></div>`)) | |||||
.appendTo(this.parent); | |||||
this.$graphics = this.container.find('.graph-graphics'); | |||||
this.$stats_container = this.container.find('.graph-stats-container'); | |||||
this.$graph = $('<div>') | |||||
.addClass(this.mode + '-graph') | |||||
.appendTo(this.$graphics); | |||||
this.$graph.append(this.make_graph_area()); | |||||
} | |||||
make_graph_area() { | |||||
this.$svg = $(`<svg class="svg" width="${this.base_width}" height="${this.base_height}"></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 = $(`<div class="graph-svg-tip comparison"> | |||||
<span class="title"></span> | |||||
<ul class="data-point-list"> | |||||
</ul> | |||||
</div>`).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 = $(`<li> | |||||
<strong style="display: block;"> | |||||
${y_set.formatted ? y_set.formatted[i] : y_set.values[i]} | |||||
</strong> | |||||
${y_set.title ? y_set.title : '' } | |||||
</li>`).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($(`<div class="stats"> | |||||
<span class="indicator ${d.color}">${d.name}: ${d.value}</span> | |||||
</div>`)); | |||||
}); | |||||
} | |||||
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 = $(`<div class="div" width="${this.base_width}" | |||||
height="${this.base_height}"> | |||||
<div class="progress-chart"></div> | |||||
</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 = $(`<div class="progress"> | |||||
</div>`).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 = $(`<div class="progress-bar background ${this.x.colors[i]}" | |||||
style="width: ${total*100/grand_total}%"></div>`); | |||||
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($(`<div class="stats"> | |||||
<span class="indicator ${this.x.colors[i]}"> | |||||
<span class="text-muted">${values[i]}:</span> | |||||
${d} | |||||
</span> | |||||
</div>`)); | |||||
}); | |||||
} | |||||
}; |
@@ -43,7 +43,7 @@ frappe.views.QueryReport = Class.extend({ | |||||
this.wrapper = $("<div>").appendTo(this.page.main); | this.wrapper = $("<div>").appendTo(this.page.main); | ||||
$('<div class="waiting-area" style="display: none;"></div>\ | $('<div class="waiting-area" style="display: none;"></div>\ | ||||
<div class="no-report-area msg-box no-border" style="display: none;"></div>\ | <div class="no-report-area msg-box no-border" style="display: none;"></div>\ | ||||
<div class="chart_area" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 1px"></div>\ | |||||
<div class="chart_area" style="border-bottom: 1px solid #d1d8dd; padding: 0px 5%"></div>\ | |||||
<div class="results" style="display: none;">\ | <div class="results" style="display: none;">\ | ||||
<div class="result-area" style="height:400px;"></div>\ | <div class="result-area" style="height:400px;"></div>\ | ||||
<button class="btn btn-secondary btn-default btn-xs expand-all hidden" style="margin: 10px;">'+__('Expand All')+'</button>\ | <button class="btn btn-secondary btn-default btn-xs expand-all hidden" style="margin: 10px;">'+__('Expand All')+'</button>\ | ||||
@@ -708,10 +708,10 @@ select.form-control { | |||||
} | } | ||||
.password-strength-indicator { | .password-strength-indicator { | ||||
float: right; | |||||
padding: 15px; | |||||
margin-top: -41px; | |||||
margin-right: -7px; | |||||
float: right; | |||||
padding: 15px; | |||||
margin-top: -41px; | |||||
margin-right: -7px; | |||||
} | } | ||||
.password-strength-message { | .password-strength-message { | ||||
@@ -856,7 +856,6 @@ select.form-control { | |||||
} | } | ||||
/* goals */ | /* goals */ | ||||
.goals-page-container { | .goals-page-container { | ||||
background-color: #fafbfc; | background-color: #fafbfc; | ||||
padding-top: 1px; | padding-top: 1px; | ||||
@@ -870,106 +869,6 @@ select.form-control { | |||||
} | } | ||||
} | } | ||||
.graph-container { | |||||
.graphics { | |||||
margin-top: 10px; | |||||
padding: 10px 0px; | |||||
} | |||||
.stats-group { | |||||
display: flex; | |||||
justify-content: space-around; | |||||
flex: 1; | |||||
} | |||||
.stats-container { | |||||
display: flex; | |||||
justify-content: space-around; | |||||
.stats { | |||||
padding-bottom: 15px; | |||||
} | |||||
.stats-title { | |||||
color: #8D99A6; | |||||
} | |||||
.stats-value { | |||||
font-size: 20px; | |||||
font-weight: 300; | |||||
} | |||||
.stats-description { | |||||
font-size: 12px; | |||||
color: #8D99A6; | |||||
} | |||||
.graph-data .stats-value { | |||||
color: #98d85b; | |||||
} | |||||
} | |||||
} | |||||
.bar-graph, .line-graph { | |||||
.axis { | |||||
font-size: 10px; | |||||
fill: #6a737d; | |||||
line { | |||||
stroke: rgba(27,31,35,0.1); | |||||
} | |||||
} | |||||
} | |||||
.data-points { | |||||
circle { | |||||
fill: #28a745; | |||||
stroke: #fff; | |||||
stroke-width: 2; | |||||
} | |||||
g.mini { | |||||
fill: #98d85b; | |||||
} | |||||
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; | |||||
} | |||||
.specific-value { | |||||
text-anchor: start; | |||||
} | |||||
.y-value-text { | |||||
text-anchor: end; | |||||
} | |||||
.x-value-text { | |||||
text-anchor: middle; | |||||
} | |||||
} | |||||
body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { | body[data-route^="Form/Communication"] textarea[data-fieldname="subject"] { | ||||
height: 80px !important; | height: 80px !important; | ||||
} | } | ||||
@@ -0,0 +1,319 @@ | |||||
/* graphs */ | |||||
.graph-container { | |||||
.graph-focus-margin { | |||||
margin: 0px 5%; | |||||
} | |||||
.graph-graphics { | |||||
margin-top: 10px; | |||||
padding: 10px 0px; | |||||
position: relative; | |||||
} | |||||
.graph-stats-group { | |||||
display: flex; | |||||
justify-content: space-around; | |||||
flex: 1; | |||||
} | |||||
.graph-stats-container { | |||||
display: flex; | |||||
justify-content: space-around; | |||||
padding-top: 10px; | |||||
.stats { | |||||
padding-bottom: 15px; | |||||
} | |||||
// Custom (impactified) stats style | |||||
.stats-title { | |||||
color: #8D99A6; | |||||
} | |||||
.stats-value { | |||||
font-size: 20px; | |||||
font-weight: 300; | |||||
} | |||||
.stats-description { | |||||
font-size: 12px; | |||||
color: #8D99A6; | |||||
} | |||||
.graph-data .stats-value { | |||||
color: #98d85b; | |||||
} | |||||
} | |||||
.bar-graph, .line-graph { | |||||
// baselines | |||||
.axis { | |||||
font-size: 10px; | |||||
fill: #6a737d; | |||||
line { | |||||
stroke: rgba(27,31,35,0.1); | |||||
} | |||||
} | |||||
} | |||||
.percentage-graph { | |||||
margin-top: 35px; | |||||
.progress { | |||||
margin-bottom: 0px; | |||||
} | |||||
} | |||||
.graph-data-points { | |||||
circle { | |||||
// fill: #28a745; | |||||
stroke: #fff; | |||||
stroke-width: 2; | |||||
} | |||||
g.mini { | |||||
// fill: #98d85b; | |||||
} | |||||
path { | |||||
fill: none; | |||||
// stroke: #28a745; | |||||
stroke-opacity: 1; | |||||
stroke-width: 2px; | |||||
} | |||||
} | |||||
line.graph-dashed { | |||||
stroke-dasharray: 5,3; | |||||
} | |||||
.tick { | |||||
&.x-axis-label { | |||||
display: block; | |||||
} | |||||
.specific-value { | |||||
text-anchor: start; | |||||
} | |||||
.y-value-text { | |||||
text-anchor: end; | |||||
} | |||||
.x-value-text { | |||||
text-anchor: middle; | |||||
} | |||||
} | |||||
.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; | |||||
&.comparison { | |||||
padding: 0; | |||||
text-align: left; | |||||
pointer-events: none; | |||||
.title { | |||||
display: block; | |||||
padding: 10px; | |||||
margin: 0; | |||||
font-weight: 600; | |||||
line-height: 1; | |||||
pointer-events: none; | |||||
} | |||||
ul { | |||||
margin: 0; | |||||
white-space: nowrap; | |||||
list-style: none; | |||||
} | |||||
li { | |||||
display: inline-block; | |||||
padding: 5px 10px; | |||||
} | |||||
} | |||||
ul, ol { | |||||
padding-left: 0; | |||||
display: flex; | |||||
} | |||||
ul.data-point-list li { | |||||
min-width: 90px; | |||||
flex: 1; | |||||
} | |||||
strong { | |||||
color: #dfe2e5; | |||||
} | |||||
&::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); | |||||
} | |||||
} | |||||
.stroke.grey { | |||||
stroke: #F0F4F7; | |||||
} | |||||
.stroke.blue { | |||||
stroke: #5e64ff; | |||||
} | |||||
.stroke.red { | |||||
stroke: #ff5858; | |||||
} | |||||
.stroke.light-green { | |||||
stroke: #98d85b; | |||||
} | |||||
.stroke.green { | |||||
stroke: #28a745; | |||||
} | |||||
.stroke.orange { | |||||
stroke: #ffa00a; | |||||
} | |||||
.stroke.purple { | |||||
stroke: #743ee2; | |||||
} | |||||
.stroke.darkgrey { | |||||
stroke: #b8c2cc; | |||||
} | |||||
.stroke.black { | |||||
stroke: #36414C; | |||||
} | |||||
.stroke.yellow { | |||||
stroke: #FEEF72; | |||||
} | |||||
.stroke.light-blue { | |||||
stroke: #7CD6FD; | |||||
} | |||||
.stroke.lightblue { | |||||
stroke: #7CD6FD; | |||||
} | |||||
.fill.grey { | |||||
fill: #F0F4F7; | |||||
} | |||||
.fill.blue { | |||||
fill: #5e64ff; | |||||
} | |||||
.fill.red { | |||||
fill: #ff5858; | |||||
} | |||||
.fill.light-green { | |||||
fill: #98d85b; | |||||
} | |||||
.fill.green { | |||||
fill: #28a745; | |||||
} | |||||
.fill.orange { | |||||
fill: #ffa00a; | |||||
} | |||||
.fill.purple { | |||||
fill: #743ee2; | |||||
} | |||||
.fill.darkgrey { | |||||
fill: #b8c2cc; | |||||
} | |||||
.fill.black { | |||||
fill: #36414C; | |||||
} | |||||
.fill.yellow { | |||||
fill: #FEEF72; | |||||
} | |||||
.fill.light-blue { | |||||
fill: #7CD6FD; | |||||
} | |||||
.fill.lightblue { | |||||
fill: #7CD6FD; | |||||
} | |||||
.background.grey { | |||||
background: #F0F4F7; | |||||
} | |||||
.background.blue { | |||||
background: #5e64ff; | |||||
} | |||||
.background.red { | |||||
background: #ff5858; | |||||
} | |||||
.background.light-green { | |||||
background: #98d85b; | |||||
} | |||||
.background.green { | |||||
background: #28a745; | |||||
} | |||||
.background.orange { | |||||
background: #ffa00a; | |||||
} | |||||
.background.purple { | |||||
background: #743ee2; | |||||
} | |||||
.background.darkgrey { | |||||
background: #b8c2cc; | |||||
} | |||||
.background.black { | |||||
background: #36414C; | |||||
} | |||||
.background.yellow { | |||||
background: #FEEF72; | |||||
} | |||||
.background.light-blue { | |||||
background: #7CD6FD; | |||||
} | |||||
.background.lightblue { | |||||
background: #7CD6FD; | |||||
} | |||||
.border-top.grey { | |||||
border-top: 3px solid #F0F4F7; | |||||
} | |||||
.border-top.blue { | |||||
border-top: 3px solid #5e64ff; | |||||
} | |||||
.border-top.red { | |||||
border-top: 3px solid #ff5858; | |||||
} | |||||
.border-top.light-green { | |||||
border-top: 3px solid #98d85b; | |||||
} | |||||
.border-top.green { | |||||
border-top: 3px solid #28a745; | |||||
} | |||||
.border-top.orange { | |||||
border-top: 3px solid #ffa00a; | |||||
} | |||||
.border-top.purple { | |||||
border-top: 3px solid #743ee2; | |||||
} | |||||
.border-top.darkgrey { | |||||
border-top: 3px solid #b8c2cc; | |||||
} | |||||
.border-top.black { | |||||
border-top: 3px solid #36414C; | |||||
} | |||||
.border-top.yellow { | |||||
border-top: 3px solid #FEEF72; | |||||
} | |||||
.border-top.light-blue { | |||||
border-top: 3px solid #7CD6FD; | |||||
} | |||||
.border-top.lightblue { | |||||
border-top: 3px solid #7CD6FD; | |||||
} | |||||
} |
@@ -31,4 +31,4 @@ class TestGoal(unittest.TestCase): | |||||
frappe.db.set_value('Event', docname, 'description', 1) | frappe.db.set_value('Event', docname, 'description', 1) | ||||
data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', | data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', | ||||
'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count') | 'Event', '', 'description', 'creation', 'starts_on = "2014-01-01"', 'count') | ||||
self.assertEquals(float(data['y_values'][-1]), 1) | |||||
self.assertEquals(float(data['y'][0]['values'][-1]), 1) |
@@ -76,15 +76,21 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_ | |||||
month_to_value_dict[current_month_year] = current_month_value | month_to_value_dict[current_month_year] = current_month_value | ||||
months = [] | months = [] | ||||
months_formatted = [] | |||||
values = [] | values = [] | ||||
values_formatted = [] | |||||
for i in xrange(0, 12): | for i in xrange(0, 12): | ||||
month_value = formatdate(add_months(today(), -i), "MM-yyyy") | month_value = formatdate(add_months(today(), -i), "MM-yyyy") | ||||
month_word = getdate(month_value).strftime('%b') | month_word = getdate(month_value).strftime('%b') | ||||
month_year = getdate(month_value).strftime('%B') + ', ' + getdate(month_value).strftime('%Y') | |||||
months.insert(0, month_word) | months.insert(0, month_word) | ||||
months_formatted.insert(0, month_year) | |||||
if month_value in month_to_value_dict: | if month_value in month_to_value_dict: | ||||
values.insert(0, month_to_value_dict[month_value]) | |||||
val = month_to_value_dict[month_value] | |||||
else: | else: | ||||
values.insert(0, 0) | |||||
val = 0 | |||||
values.insert(0, val) | |||||
values_formatted.insert(0, format_value(val, meta.get_field(goal_total_field), doc)) | |||||
specific_values = [] | specific_values = [] | ||||
summary_values = [ | summary_values = [ | ||||
@@ -119,10 +125,20 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_ | |||||
data = { | data = { | ||||
'title': title, | 'title': title, | ||||
# 'subtitle': | # 'subtitle': | ||||
'y_values': values, | |||||
'x_points': months, | |||||
'y': [ | |||||
{ | |||||
'color': 'green', | |||||
'values': values, | |||||
'formatted': values_formatted | |||||
} | |||||
], | |||||
'x': { | |||||
'values': months, | |||||
'formatted': months_formatted | |||||
}, | |||||
'specific_values': specific_values, | 'specific_values': specific_values, | ||||
'summary_values': summary_values | |||||
'summary': summary_values | |||||
} | } | ||||
return data | return data |