* 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, | |||
"getCookies": true, | |||
"get_url_arg": true, | |||
"QUnit": true | |||
"QUnit": true, | |||
"Snap": true, | |||
"mina": true | |||
} | |||
} |
@@ -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) + "%" | |||
} | |||
] | |||
}); | |||
}, | |||
<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"> | |||
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/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", | |||
@@ -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; | |||
} |
@@ -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'); | |||
$.extend(args, { | |||
parent: me.graph_area, | |||
width: 710, | |||
height: 140, | |||
mode: 'line-graph' | |||
mode: 'line', | |||
height: 140 | |||
}); | |||
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); | |||
$('<div class="waiting-area" 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="result-area" style="height:400px;"></div>\ | |||
<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 { | |||
float: right; | |||
padding: 15px; | |||
margin-top: -41px; | |||
margin-right: -7px; | |||
float: right; | |||
padding: 15px; | |||
margin-top: -41px; | |||
margin-right: -7px; | |||
} | |||
.password-strength-message { | |||
@@ -856,7 +856,6 @@ select.form-control { | |||
} | |||
/* goals */ | |||
.goals-page-container { | |||
background-color: #fafbfc; | |||
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"] { | |||
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) | |||
data = get_monthly_goal_graph_data('Test', 'Event', docname, 'description', 'description', 'description', | |||
'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 | |||
months = [] | |||
months_formatted = [] | |||
values = [] | |||
values_formatted = [] | |||
for i in xrange(0, 12): | |||
month_value = formatdate(add_months(today(), -i), "MM-yyyy") | |||
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_formatted.insert(0, month_year) | |||
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: | |||
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 = [] | |||
summary_values = [ | |||
@@ -119,10 +125,20 @@ def get_monthly_goal_graph_data(title, doctype, docname, goal_value_field, goal_ | |||
data = { | |||
'title': title, | |||
# '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, | |||
'summary_values': summary_values | |||
'summary': summary_values | |||
} | |||
return data |