Graph additions (#3776)
* 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 class
This commit is contained in:
parent
d384722839
commit
7fa8148d33
18 changed files with 1283 additions and 547 deletions
|
|
@ -118,6 +118,8 @@
|
|||
"getCookie": true,
|
||||
"getCookies": true,
|
||||
"get_url_arg": true,
|
||||
"QUnit": true
|
||||
"QUnit": true,
|
||||
"Snap": true,
|
||||
"mina": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
frappe/docs/assets/img/desk/animated_line_graph.gif
Normal file
BIN
frappe/docs/assets/img/desk/animated_line_graph.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
BIN
frappe/docs/assets/img/desk/line_graph_sales.png
Normal file
BIN
frappe/docs/assets/img/desk/line_graph_sales.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
BIN
frappe/docs/assets/img/desk/percentage_graph.png
Normal file
BIN
frappe/docs/assets/img/desk/percentage_graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
|
|
@ -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:
|
||||
Here's an example of a simple sales graph:
|
||||
|
||||
render_graph: function() {
|
||||
$('.form-graph').empty();
|
||||
// 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'];
|
||||
|
||||
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];
|
||||
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;
|
||||
|
||||
var goal = 2500;
|
||||
var current_val = 2013;
|
||||
let g = new frappe.ui.Graph({
|
||||
parent: $('.form-graph').empty(),
|
||||
height: 200, // optional
|
||||
mode: 'line', // 'line', 'bar' or 'percentage'
|
||||
|
||||
new frappe.ui.Graph({
|
||||
parent: $('.form-graph'),
|
||||
width: 700,
|
||||
height: 140,
|
||||
mode: 'line-graph',
|
||||
title: 'Sales',
|
||||
subtitle: 'Monthly',
|
||||
|
||||
title: 'Sales',
|
||||
subtitle: 'Monthly',
|
||||
y_values: values,
|
||||
x_points: months,
|
||||
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'
|
||||
}
|
||||
],
|
||||
|
||||
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) + "%"
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
x: {
|
||||
values: months.map(d => d.substring(0, 3)),
|
||||
formatted: months
|
||||
},
|
||||
|
||||
<img src="{{docs_base_url}}/assets/img/desk/line_graph.png" class="screenshot">
|
||||
specific_values: [
|
||||
{
|
||||
name: 'Goal',
|
||||
line_type: 'dashed', // 'dashed' or 'solid'
|
||||
value: goal
|
||||
},
|
||||
],
|
||||
|
||||
Setting the mode to 'bar-graph':
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
274
frappe/public/css/graphs.css
Normal file
274
frappe/public/css/graphs.css
Normal file
|
|
@ -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));
|
||||
}
|
||||
|
||||
};
|
||||
569
frappe/public/js/frappe/ui/graphs.js
Normal file
569
frappe/public/js/frappe/ui/graphs.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
319
frappe/public/less/graphs.less
Normal file
319
frappe/public/less/graphs.less
Normal file
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue