[charts] Heatmapversion-14
@@ -49,7 +49,6 @@ | |||||
"moment": true, | "moment": true, | ||||
"hljs": true, | "hljs": true, | ||||
"Awesomplete": true, | "Awesomplete": true, | ||||
"CalHeatMap": true, | |||||
"Sortable": true, | "Sortable": true, | ||||
"Showdown": true, | "Showdown": true, | ||||
"Taggle": true, | "Taggle": true, | ||||
@@ -180,30 +180,12 @@ frappe.activity.render_heatmap = function(page) { | |||||
method: "frappe.desk.page.activity.activity.get_heatmap_data", | method: "frappe.desk.page.activity.activity.get_heatmap_data", | ||||
callback: function(r) { | callback: function(r) { | ||||
if(r.message) { | if(r.message) { | ||||
var legend = []; | |||||
var max = Math.max.apply(this, $.map(r.message, function(v) { return v })); | |||||
var legend = [cint(max/5), cint(max*2/5), cint(max*3/5), cint(max*4/5)]; | |||||
var heatmap = new CalHeatMap(); | |||||
heatmap.init({ | |||||
itemSelector: ".heatmap", | |||||
domain: "month", | |||||
subDomain: "day", | |||||
start: moment().subtract(1, 'year').add(1, 'month').toDate(), | |||||
cellSize: 9, | |||||
cellPadding: 2, | |||||
domainGutter: 2, | |||||
range: 12, | |||||
domainLabelFormat: function(date) { | |||||
return moment(date).format("MMM").toUpperCase(); | |||||
}, | |||||
displayLegend: false, | |||||
legend: legend, | |||||
tooltip: true, | |||||
subDomainTitleFormat: { | |||||
empty: "{date}", | |||||
filled: "{count} actions on {date}" | |||||
}, | |||||
subDomainDateFormat: "%d-%b" | |||||
var heatmap = new frappe.ui.HeatMap({ | |||||
parent: $(".heatmap"), | |||||
height: 100, | |||||
start: new Date(moment().subtract(1, 'year').toDate()), | |||||
count_label: "actions", | |||||
discrete_domains: 0 | |||||
}); | }); | ||||
heatmap.update(r.message); | heatmap.update(r.message); | ||||
@@ -97,7 +97,6 @@ | |||||
"public/css/bootstrap.css", | "public/css/bootstrap.css", | ||||
"public/css/font-awesome.css", | "public/css/font-awesome.css", | ||||
"public/css/octicons/octicons.css", | "public/css/octicons/octicons.css", | ||||
"public/css/cal-heatmap.css", | |||||
"public/css/c3.min.css", | "public/css/c3.min.css", | ||||
"public/css/desk.css", | "public/css/desk.css", | ||||
"public/css/indicator.css", | "public/css/indicator.css", | ||||
@@ -231,7 +230,6 @@ | |||||
], | ], | ||||
"js/d3.min.js": [ | "js/d3.min.js": [ | ||||
"public/js/lib/d3.min.js", | "public/js/lib/d3.min.js", | ||||
"public/js/lib/cal-heatmap.js", | |||||
"public/js/lib/c3.min.js" | "public/js/lib/c3.min.js" | ||||
], | ], | ||||
"css/module.min.css": [ | "css/module.min.css": [ | ||||
@@ -1,140 +0,0 @@ | |||||
/* Cal-HeatMap CSS */ | |||||
.cal-heatmap-container { | |||||
display: block; | |||||
} | |||||
.cal-heatmap-container .graph-label | |||||
{ | |||||
fill: #999; | |||||
font-size: 10px | |||||
} | |||||
.cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect { | |||||
shape-rendering: crispedges | |||||
} | |||||
.cal-heatmap-container .graph-rect | |||||
{ | |||||
fill: #ededed | |||||
} | |||||
.cal-heatmap-container .graph-subdomain-group rect:hover | |||||
{ | |||||
stroke: #000; | |||||
stroke-width: 1px | |||||
} | |||||
.cal-heatmap-container .subdomain-text { | |||||
font-size: 8px; | |||||
fill: #999; | |||||
pointer-events: none | |||||
} | |||||
.cal-heatmap-container .hover_cursor:hover { | |||||
cursor: pointer | |||||
} | |||||
.cal-heatmap-container .qi { | |||||
background-color: #999; | |||||
fill: #999 | |||||
} | |||||
/* | |||||
Remove comment to apply this style to date with value equal to 0 | |||||
.q0 | |||||
{ | |||||
background-color: #fff; | |||||
fill: #fff; | |||||
stroke: #ededed | |||||
} | |||||
*/ | |||||
.cal-heatmap-container .q1 | |||||
{ | |||||
background-color: #dae289; | |||||
fill: #dae289 | |||||
} | |||||
.cal-heatmap-container .q2 | |||||
{ | |||||
background-color: #cedb9c; | |||||
fill: #9cc069 | |||||
} | |||||
.cal-heatmap-container .q3 | |||||
{ | |||||
background-color: #b5cf6b; | |||||
fill: #669d45 | |||||
} | |||||
.cal-heatmap-container .q4 | |||||
{ | |||||
background-color: #637939; | |||||
fill: #637939 | |||||
} | |||||
.cal-heatmap-container .q5 | |||||
{ | |||||
background-color: #3b6427; | |||||
fill: #3b6427 | |||||
} | |||||
.cal-heatmap-container rect.highlight | |||||
{ | |||||
stroke:#444; | |||||
stroke-width:1 | |||||
} | |||||
.cal-heatmap-container text.highlight | |||||
{ | |||||
fill: #444 | |||||
} | |||||
.cal-heatmap-container rect.now | |||||
{ | |||||
stroke: red | |||||
} | |||||
.cal-heatmap-container text.now | |||||
{ | |||||
fill: red; | |||||
font-weight: 800 | |||||
} | |||||
.cal-heatmap-container .domain-background { | |||||
fill: none; | |||||
shape-rendering: crispedges | |||||
} | |||||
.ch-tooltip { | |||||
padding: 10px; | |||||
background: #222; | |||||
color: #bbb; | |||||
font-size: 12px; | |||||
line-height: 1.4; | |||||
width: 140px; | |||||
position: absolute; | |||||
z-index: 99999; | |||||
text-align: center; | |||||
border-radius: 2px; | |||||
box-shadow: 2px 2px 2px rgba(0,0,0,0.2); | |||||
display: none; | |||||
box-sizing: border-box; | |||||
} | |||||
.ch-tooltip::after{ | |||||
position: absolute; | |||||
width: 0; | |||||
height: 0; | |||||
border-color: transparent; | |||||
border-style: solid; | |||||
content: ""; | |||||
padding: 0; | |||||
display: block; | |||||
bottom: -6px; | |||||
left: 50%; | |||||
margin-left: -6px; | |||||
border-width: 6px 6px 0; | |||||
border-top-color: #222; | |||||
} |
@@ -98,6 +98,10 @@ | |||||
.form-dashboard-section:last-child { | .form-dashboard-section:last-child { | ||||
border-bottom: none; | border-bottom: none; | ||||
} | } | ||||
.form-heatmap .heatmap { | |||||
display: flex; | |||||
justify-content: center; | |||||
} | |||||
.form-heatmap .heatmap-message { | .form-heatmap .heatmap-message { | ||||
margin-top: 10px; | margin-top: 10px; | ||||
} | } | ||||
@@ -2,9 +2,10 @@ | |||||
.graph-container .graph-focus-margin { | .graph-container .graph-focus-margin { | ||||
margin: 0px 5%; | margin: 0px 5%; | ||||
} | } | ||||
.graph-container .graph-graphics { | |||||
.graph-container .graphics { | |||||
margin-top: 10px; | margin-top: 10px; | ||||
padding: 10px 0px; | |||||
padding-top: 10px; | |||||
padding-bottom: 10px; | |||||
position: relative; | position: relative; | ||||
} | } | ||||
.graph-container .graph-stats-group { | .graph-container .graph-stats-group { | ||||
@@ -34,31 +35,28 @@ | |||||
.graph-container .graph-stats-container .graph-data .stats-value { | .graph-container .graph-stats-container .graph-data .stats-value { | ||||
color: #98d85b; | color: #98d85b; | ||||
} | } | ||||
.graph-container .bar-graph .axis, | |||||
.graph-container .line-graph .axis { | |||||
.graph-container .axis, | |||||
.graph-container .chart-label { | |||||
font-size: 10px; | font-size: 10px; | ||||
fill: #6a737d; | |||||
fill: #959ba1; | |||||
} | } | ||||
.graph-container .bar-graph .axis line, | |||||
.graph-container .line-graph .axis line { | |||||
.graph-container .axis line, | |||||
.graph-container .chart-label line { | |||||
stroke: rgba(27, 31, 35, 0.1); | stroke: rgba(27, 31, 35, 0.1); | ||||
} | } | ||||
.graph-container .percentage-graph { | |||||
margin-top: 35px; | |||||
} | |||||
.graph-container .percentage-graph .progress { | .graph-container .percentage-graph .progress { | ||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
.graph-container .graph-data-points circle { | |||||
.graph-container .data-points circle { | |||||
stroke: #fff; | stroke: #fff; | ||||
stroke-width: 2; | stroke-width: 2; | ||||
} | } | ||||
.graph-container .graph-data-points path { | |||||
.graph-container .data-points path { | |||||
fill: none; | fill: none; | ||||
stroke-opacity: 1; | stroke-opacity: 1; | ||||
stroke-width: 2px; | stroke-width: 2px; | ||||
} | } | ||||
.graph-container line.graph-dashed { | |||||
.graph-container line.dashed { | |||||
stroke-dasharray: 5,3; | stroke-dasharray: 5,3; | ||||
} | } | ||||
.graph-container .tick.x-axis-label { | .graph-container .tick.x-axis-label { | ||||
@@ -73,7 +71,7 @@ | |||||
.graph-container .tick .x-value-text { | .graph-container .tick .x-value-text { | ||||
text-anchor: middle; | text-anchor: middle; | ||||
} | } | ||||
.graph-container .graph-svg-tip { | |||||
.graph-svg-tip { | |||||
position: absolute; | position: absolute; | ||||
z-index: 99999; | z-index: 99999; | ||||
padding: 10px; | padding: 10px; | ||||
@@ -83,12 +81,12 @@ | |||||
background: rgba(0, 0, 0, 0.8); | background: rgba(0, 0, 0, 0.8); | ||||
border-radius: 3px; | border-radius: 3px; | ||||
} | } | ||||
.graph-container .graph-svg-tip.comparison { | |||||
.graph-svg-tip.comparison { | |||||
padding: 0; | padding: 0; | ||||
text-align: left; | text-align: left; | ||||
pointer-events: none; | pointer-events: none; | ||||
} | } | ||||
.graph-container .graph-svg-tip.comparison .title { | |||||
.graph-svg-tip.comparison .title { | |||||
display: block; | display: block; | ||||
padding: 10px; | padding: 10px; | ||||
margin: 0; | margin: 0; | ||||
@@ -96,28 +94,28 @@ | |||||
line-height: 1; | line-height: 1; | ||||
pointer-events: none; | pointer-events: none; | ||||
} | } | ||||
.graph-container .graph-svg-tip.comparison ul { | |||||
.graph-svg-tip.comparison ul { | |||||
margin: 0; | margin: 0; | ||||
white-space: nowrap; | white-space: nowrap; | ||||
list-style: none; | list-style: none; | ||||
} | } | ||||
.graph-container .graph-svg-tip.comparison li { | |||||
.graph-svg-tip.comparison li { | |||||
display: inline-block; | display: inline-block; | ||||
padding: 5px 10px; | padding: 5px 10px; | ||||
} | } | ||||
.graph-container .graph-svg-tip ul, | |||||
.graph-container .graph-svg-tip ol { | |||||
.graph-svg-tip ul, | |||||
.graph-svg-tip ol { | |||||
padding-left: 0; | padding-left: 0; | ||||
display: flex; | display: flex; | ||||
} | } | ||||
.graph-container .graph-svg-tip ul.data-point-list li { | |||||
.graph-svg-tip ul.data-point-list li { | |||||
min-width: 90px; | min-width: 90px; | ||||
flex: 1; | flex: 1; | ||||
} | } | ||||
.graph-container .graph-svg-tip strong { | |||||
.graph-svg-tip strong { | |||||
color: #dfe2e5; | color: #dfe2e5; | ||||
} | } | ||||
.graph-container .graph-svg-tip::after { | |||||
.graph-svg-tip .svg-pointer { | |||||
position: absolute; | position: absolute; | ||||
bottom: -10px; | bottom: -10px; | ||||
left: 50%; | left: 50%; | ||||
@@ -128,147 +126,147 @@ | |||||
border: 5px solid transparent; | border: 5px solid transparent; | ||||
border-top-color: rgba(0, 0, 0, 0.8); | border-top-color: rgba(0, 0, 0, 0.8); | ||||
} | } | ||||
.graph-container .stroke.grey { | |||||
.stroke.grey { | |||||
stroke: #F0F4F7; | stroke: #F0F4F7; | ||||
} | } | ||||
.graph-container .stroke.blue { | |||||
.stroke.blue { | |||||
stroke: #5e64ff; | stroke: #5e64ff; | ||||
} | } | ||||
.graph-container .stroke.red { | |||||
.stroke.red { | |||||
stroke: #ff5858; | stroke: #ff5858; | ||||
} | } | ||||
.graph-container .stroke.light-green { | |||||
.stroke.light-green { | |||||
stroke: #98d85b; | stroke: #98d85b; | ||||
} | } | ||||
.graph-container .stroke.green { | |||||
.stroke.green { | |||||
stroke: #28a745; | stroke: #28a745; | ||||
} | } | ||||
.graph-container .stroke.orange { | |||||
.stroke.orange { | |||||
stroke: #ffa00a; | stroke: #ffa00a; | ||||
} | } | ||||
.graph-container .stroke.purple { | |||||
.stroke.purple { | |||||
stroke: #743ee2; | stroke: #743ee2; | ||||
} | } | ||||
.graph-container .stroke.darkgrey { | |||||
.stroke.darkgrey { | |||||
stroke: #b8c2cc; | stroke: #b8c2cc; | ||||
} | } | ||||
.graph-container .stroke.black { | |||||
.stroke.black { | |||||
stroke: #36414C; | stroke: #36414C; | ||||
} | } | ||||
.graph-container .stroke.yellow { | |||||
.stroke.yellow { | |||||
stroke: #FEEF72; | stroke: #FEEF72; | ||||
} | } | ||||
.graph-container .stroke.light-blue { | |||||
.stroke.light-blue { | |||||
stroke: #7CD6FD; | stroke: #7CD6FD; | ||||
} | } | ||||
.graph-container .stroke.lightblue { | |||||
.stroke.lightblue { | |||||
stroke: #7CD6FD; | stroke: #7CD6FD; | ||||
} | } | ||||
.graph-container .fill.grey { | |||||
.fill.grey { | |||||
fill: #F0F4F7; | fill: #F0F4F7; | ||||
} | } | ||||
.graph-container .fill.blue { | |||||
.fill.blue { | |||||
fill: #5e64ff; | fill: #5e64ff; | ||||
} | } | ||||
.graph-container .fill.red { | |||||
.fill.red { | |||||
fill: #ff5858; | fill: #ff5858; | ||||
} | } | ||||
.graph-container .fill.light-green { | |||||
.fill.light-green { | |||||
fill: #98d85b; | fill: #98d85b; | ||||
} | } | ||||
.graph-container .fill.green { | |||||
.fill.green { | |||||
fill: #28a745; | fill: #28a745; | ||||
} | } | ||||
.graph-container .fill.orange { | |||||
.fill.orange { | |||||
fill: #ffa00a; | fill: #ffa00a; | ||||
} | } | ||||
.graph-container .fill.purple { | |||||
.fill.purple { | |||||
fill: #743ee2; | fill: #743ee2; | ||||
} | } | ||||
.graph-container .fill.darkgrey { | |||||
.fill.darkgrey { | |||||
fill: #b8c2cc; | fill: #b8c2cc; | ||||
} | } | ||||
.graph-container .fill.black { | |||||
.fill.black { | |||||
fill: #36414C; | fill: #36414C; | ||||
} | } | ||||
.graph-container .fill.yellow { | |||||
.fill.yellow { | |||||
fill: #FEEF72; | fill: #FEEF72; | ||||
} | } | ||||
.graph-container .fill.light-blue { | |||||
.fill.light-blue { | |||||
fill: #7CD6FD; | fill: #7CD6FD; | ||||
} | } | ||||
.graph-container .fill.lightblue { | |||||
.fill.lightblue { | |||||
fill: #7CD6FD; | fill: #7CD6FD; | ||||
} | } | ||||
.graph-container .background.grey { | |||||
.background.grey { | |||||
background: #F0F4F7; | background: #F0F4F7; | ||||
} | } | ||||
.graph-container .background.blue { | |||||
.background.blue { | |||||
background: #5e64ff; | background: #5e64ff; | ||||
} | } | ||||
.graph-container .background.red { | |||||
.background.red { | |||||
background: #ff5858; | background: #ff5858; | ||||
} | } | ||||
.graph-container .background.light-green { | |||||
.background.light-green { | |||||
background: #98d85b; | background: #98d85b; | ||||
} | } | ||||
.graph-container .background.green { | |||||
.background.green { | |||||
background: #28a745; | background: #28a745; | ||||
} | } | ||||
.graph-container .background.orange { | |||||
.background.orange { | |||||
background: #ffa00a; | background: #ffa00a; | ||||
} | } | ||||
.graph-container .background.purple { | |||||
.background.purple { | |||||
background: #743ee2; | background: #743ee2; | ||||
} | } | ||||
.graph-container .background.darkgrey { | |||||
.background.darkgrey { | |||||
background: #b8c2cc; | background: #b8c2cc; | ||||
} | } | ||||
.graph-container .background.black { | |||||
.background.black { | |||||
background: #36414C; | background: #36414C; | ||||
} | } | ||||
.graph-container .background.yellow { | |||||
.background.yellow { | |||||
background: #FEEF72; | background: #FEEF72; | ||||
} | } | ||||
.graph-container .background.light-blue { | |||||
.background.light-blue { | |||||
background: #7CD6FD; | background: #7CD6FD; | ||||
} | } | ||||
.graph-container .background.lightblue { | |||||
.background.lightblue { | |||||
background: #7CD6FD; | background: #7CD6FD; | ||||
} | } | ||||
.graph-container .border-top.grey { | |||||
.border-top.grey { | |||||
border-top: 3px solid #F0F4F7; | border-top: 3px solid #F0F4F7; | ||||
} | } | ||||
.graph-container .border-top.blue { | |||||
.border-top.blue { | |||||
border-top: 3px solid #5e64ff; | border-top: 3px solid #5e64ff; | ||||
} | } | ||||
.graph-container .border-top.red { | |||||
.border-top.red { | |||||
border-top: 3px solid #ff5858; | border-top: 3px solid #ff5858; | ||||
} | } | ||||
.graph-container .border-top.light-green { | |||||
.border-top.light-green { | |||||
border-top: 3px solid #98d85b; | border-top: 3px solid #98d85b; | ||||
} | } | ||||
.graph-container .border-top.green { | |||||
.border-top.green { | |||||
border-top: 3px solid #28a745; | border-top: 3px solid #28a745; | ||||
} | } | ||||
.graph-container .border-top.orange { | |||||
.border-top.orange { | |||||
border-top: 3px solid #ffa00a; | border-top: 3px solid #ffa00a; | ||||
} | } | ||||
.graph-container .border-top.purple { | |||||
.border-top.purple { | |||||
border-top: 3px solid #743ee2; | border-top: 3px solid #743ee2; | ||||
} | } | ||||
.graph-container .border-top.darkgrey { | |||||
.border-top.darkgrey { | |||||
border-top: 3px solid #b8c2cc; | border-top: 3px solid #b8c2cc; | ||||
} | } | ||||
.graph-container .border-top.black { | |||||
.border-top.black { | |||||
border-top: 3px solid #36414C; | border-top: 3px solid #36414C; | ||||
} | } | ||||
.graph-container .border-top.yellow { | |||||
.border-top.yellow { | |||||
border-top: 3px solid #FEEF72; | border-top: 3px solid #FEEF72; | ||||
} | } | ||||
.graph-container .border-top.light-blue { | |||||
.border-top.light-blue { | |||||
border-top: 3px solid #7CD6FD; | border-top: 3px solid #7CD6FD; | ||||
} | } | ||||
.graph-container .border-top.lightblue { | |||||
.border-top.lightblue { | |||||
border-top: 3px solid #7CD6FD; | border-top: 3px solid #7CD6FD; | ||||
} | } |
@@ -334,22 +334,12 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
// heatmap | // heatmap | ||||
render_heatmap: function() { | render_heatmap: function() { | ||||
if(!this.heatmap) { | if(!this.heatmap) { | ||||
this.heatmap = new CalHeatMap(); | |||||
this.heatmap.init({ | |||||
itemSelector: "#heatmap-" + frappe.model.scrub(this.frm.doctype), | |||||
domain: "month", | |||||
subDomain: "day", | |||||
start: moment().subtract(1, 'year').add(1, 'month').toDate(), | |||||
cellSize: 9, | |||||
cellPadding: 2, | |||||
domainGutter: 2, | |||||
range: 12, | |||||
domainLabelFormat: function(date) { | |||||
return moment(date).format("MMM").toUpperCase(); | |||||
}, | |||||
displayLegend: false, | |||||
legend: [5, 10, 15, 20] | |||||
// subDomainTextFormat: "%d", | |||||
this.heatmap = new frappe.ui.HeatMap({ | |||||
parent: this.heatmap_area.find("#heatmap-" + frappe.model.scrub(this.frm.doctype)), | |||||
height: 100, | |||||
start: new Date(moment().subtract(1, 'year').toDate()), | |||||
count_label: "items", | |||||
discrete_domains: 0 | |||||
}); | }); | ||||
// center the heatmap | // center the heatmap | ||||
@@ -388,16 +378,14 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
return indicator; | return indicator; | ||||
}, | }, | ||||
//graphs | |||||
// graphs | |||||
setup_graph: function() { | setup_graph: function() { | ||||
var me = this; | var me = this; | ||||
var method = this.data.graph_method; | var method = this.data.graph_method; | ||||
var args = { | var args = { | ||||
doctype: this.frm.doctype, | doctype: this.frm.doctype, | ||||
docname: this.frm.doc.name, | docname: this.frm.doc.name, | ||||
}; | }; | ||||
$.extend(args, this.data.graph_method_args); | $.extend(args, this.data.graph_method_args); | ||||
frappe.call({ | frappe.call({ | ||||
@@ -421,29 +409,9 @@ frappe.ui.form.Dashboard = Class.extend({ | |||||
mode: 'line', | mode: 'line', | ||||
height: 140 | height: 140 | ||||
}); | }); | ||||
new frappe.ui.Graph(args); | new frappe.ui.Graph(args); | ||||
}, | }, | ||||
setup_chart: function(opts) { | |||||
var me = this; | |||||
this.graph_area.removeClass('hidden'); | |||||
$.extend(opts, { | |||||
wrapper: me.graph_area, | |||||
padding: { | |||||
right: 30, | |||||
bottom: 30 | |||||
} | |||||
}); | |||||
this.chart = new frappe.ui.Chart(opts); | |||||
if(this.chart) { | |||||
this.show(); | |||||
this.chart.set_chart_size(me.wrapper.width() - 60); | |||||
} | |||||
}, | |||||
show: function() { | show: function() { | ||||
this.section.removeClass('hidden'); | this.section.removeClass('hidden'); | ||||
} | } | ||||
@@ -2,7 +2,7 @@ | |||||
<div class="progress-area hidden form-dashboard-section"> | <div class="progress-area hidden form-dashboard-section"> | ||||
</div> | </div> | ||||
<div class="form-heatmap hidden form-dashboard-section"> | <div class="form-heatmap hidden form-dashboard-section"> | ||||
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}"></div> | |||||
<div id="heatmap-{{ frappe.model.scrub(frm.doctype) }}" class="heatmap"></div> | |||||
<div class="text-muted small heatmap-message hidden"></div> | <div class="text-muted small heatmap-message hidden"></div> | ||||
</div> | </div> | ||||
<div class="form-graph form-dashboard-section hidden"></div> | <div class="form-graph form-dashboard-section hidden"></div> | ||||
@@ -28,7 +28,6 @@ frappe.ui.Graph = class Graph { | |||||
specific_values = [], | specific_values = [], | ||||
summary = [], | summary = [], | ||||
color = 'blue', | |||||
mode = '', | mode = '', | ||||
}) { | }) { | ||||
@@ -43,27 +42,28 @@ frappe.ui.Graph = class Graph { | |||||
} | } | ||||
this.parent = parent; | this.parent = parent; | ||||
this.base_height = height; | |||||
this.height = height - 40; | |||||
this.translate_x = 60; | |||||
this.translate_y = 10; | |||||
this.set_margins(height); | |||||
this.title = title; | this.title = title; | ||||
this.subtitle = subtitle; | this.subtitle = subtitle; | ||||
// Begin axis graph-related args | |||||
this.y = y; | this.y = y; | ||||
this.x = x; | this.x = x; | ||||
this.specific_values = specific_values; | this.specific_values = specific_values; | ||||
this.summary = summary; | this.summary = summary; | ||||
this.color = color; | |||||
this.mode = mode; | this.mode = mode; | ||||
// this.current_hover_index = 0; | |||||
// this.current_selected_index = 0; | |||||
this.$graph = null; | this.$graph = null; | ||||
// Validate all arguments | |||||
// Validate all arguments, check passed data format, set defaults | |||||
frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); | frappe.require("assets/frappe/js/lib/snap.svg-min.js", this.setup.bind(this)); | ||||
} | } | ||||
@@ -81,18 +81,16 @@ frappe.ui.Graph = class Graph { | |||||
refresh() { | refresh() { | ||||
this.base_width = this.parent.width() - 20; | |||||
this.width = this.base_width - 100; | |||||
this.setup_base_values(); | |||||
this.set_width(); | |||||
this.width = this.base_width - this.translate_x * 2; | |||||
this.setup_container(); | this.setup_container(); | ||||
this.setup_components(); | |||||
this.setup_values(); | this.setup_values(); | ||||
this.setup_utils(); | this.setup_utils(); | ||||
this.setup_components(); | |||||
this.make_graph_components(); | this.make_graph_components(); | ||||
this.make_tooltip(); | this.make_tooltip(); | ||||
if(this.summary.length > 0) { | if(this.summary.length > 0) { | ||||
@@ -102,6 +100,20 @@ frappe.ui.Graph = class Graph { | |||||
} | } | ||||
} | } | ||||
set_margins(height) { | |||||
this.base_height = height; | |||||
this.height = height - 40; | |||||
this.translate_x = 60; | |||||
this.translate_y = 10; | |||||
} | |||||
set_width() { | |||||
this.base_width = this.parent.width(); | |||||
} | |||||
setup_base_values() {} | |||||
setup_container() { | setup_container() { | ||||
// Graph needs a dedicated parent element | // Graph needs a dedicated parent element | ||||
this.parent.empty(); | this.parent.empty(); | ||||
@@ -110,11 +122,11 @@ frappe.ui.Graph = class Graph { | |||||
.addClass('graph-container') | .addClass('graph-container') | ||||
.append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`)) | .append($(`<h6 class="title" style="margin-top: 15px;">${this.title}</h6>`)) | ||||
.append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`)) | .append($(`<h6 class="sub-title uppercase">${this.subtitle}</h6>`)) | ||||
.append($(`<div class="graph-graphics"></div>`)) | |||||
.append($(`<div class="graphics"></div>`)) | |||||
.append($(`<div class="graph-stats-container"></div>`)) | .append($(`<div class="graph-stats-container"></div>`)) | ||||
.appendTo(this.parent); | .appendTo(this.parent); | ||||
this.$graphics = this.container.find('.graph-graphics'); | |||||
this.$graphics = this.container.find('.graphics'); | |||||
this.$stats_container = this.container.find('.graph-stats-container'); | this.$stats_container = this.container.find('.graph-stats-container'); | ||||
this.$graph = $('<div>') | this.$graph = $('<div>') | ||||
@@ -130,6 +142,13 @@ frappe.ui.Graph = class Graph { | |||||
return this.$svg; | return this.$svg; | ||||
} | } | ||||
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: "data-points" }); | |||||
this.specific_y_lines = this.snap.g().attr({ class: "specific axis" }); | |||||
} | |||||
setup_values() { | setup_values() { | ||||
// Multiplier | // Multiplier | ||||
let all_values = this.specific_values.map(d => d.value); | let all_values = this.specific_values.map(d => d.value); | ||||
@@ -170,20 +189,15 @@ frappe.ui.Graph = class Graph { | |||||
}); | }); | ||||
} | } | ||||
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() { | make_graph_components() { | ||||
this.make_y_axis(); | this.make_y_axis(); | ||||
this.make_x_axis(); | this.make_x_axis(); | ||||
this.y_colors = ['lightblue', 'purple', 'blue', 'green', 'lightgreen', | |||||
'yellow', 'orange', 'red'] | |||||
this.y.map((d, i) => { | this.y.map((d, i) => { | ||||
this.make_units(d.y_tops, d.color, i); | |||||
this.make_path(d); | |||||
this.make_units(d.y_tops, d.color || this.y_colors[i], i); | |||||
this.make_path(d, d.color || this.y_colors[i]); | |||||
}); | }); | ||||
if(this.specific_values.length > 0) { | if(this.specific_values.length > 0) { | ||||
@@ -246,6 +260,11 @@ frappe.ui.Graph = class Graph { | |||||
transform: `translate(0,${start_at})` | transform: `translate(0,${start_at})` | ||||
}); | }); | ||||
this.x.values.map((point, i) => { | this.x.values.map((point, i) => { | ||||
let allowed_space = this.avg_unit_width * 1.5; | |||||
if(this.get_strwidth(point) > allowed_space) { | |||||
let allowed_letters = allowed_space / 8; | |||||
point = point.slice(0, allowed_letters-3) + " ..."; | |||||
} | |||||
this.x_axis_group.add(this.snap.g( | this.x_axis_group.add(this.snap.g( | ||||
this.snap.line(0, 0, 0, height), | this.snap.line(0, 0, 0, height), | ||||
this.snap.text(0, text_start_at, point).attr({ | this.snap.text(0, text_start_at, point).attr({ | ||||
@@ -262,8 +281,13 @@ frappe.ui.Graph = class Graph { | |||||
make_units(y_values, color, dataset_index) { | make_units(y_values, color, dataset_index) { | ||||
let d = this.unit_args; | let d = this.unit_args; | ||||
y_values.map((y, i) => { | y_values.map((y, i) => { | ||||
let data_unit = this.draw[d.type](this.x_axis_values[i], | |||||
y, d.args, color, dataset_index); | |||||
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.data_units.add(data_unit); | ||||
this.y[dataset_index].data_units.push(data_unit); | this.y[dataset_index].data_units.push(data_unit); | ||||
}); | }); | ||||
@@ -272,75 +296,58 @@ frappe.ui.Graph = class Graph { | |||||
make_path() { } | make_path() { } | ||||
make_tooltip() { | 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'); | |||||
// should be w.r.t. this.parent | |||||
this.tip = new frappe.ui.SvgTip({ | |||||
parent: this.$graphics, | |||||
}); | |||||
this.bind_tooltip(); | this.bind_tooltip(); | ||||
} | } | ||||
bind_tooltip() { | bind_tooltip() { | ||||
// should be w.r.t. this.parent, but will have to take care of | |||||
// all the elements and padding, margins on top | |||||
this.$graphics.on('mousemove', (e) => { | this.$graphics.on('mousemove', (e) => { | ||||
let offset = $(this.$graphics).offset(); | |||||
let offset = this.$graphics.offset(); | |||||
var relX = e.pageX - offset.left - this.translate_x; | var relX = e.pageX - offset.left - this.translate_x; | ||||
var relY = e.pageY - offset.top - this.translate_y; | 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; | |||||
} | |||||
} | |||||
if(relY < this.height + this.translate_y * 2) { | |||||
this.map_tooltip_x_position_and_show(relX); | |||||
} else { | } else { | ||||
this.tip.attr({ | |||||
style: `top: 0px; left: 0px; opacity: 0; pointer-events: none;` | |||||
}); | |||||
this.tip.hide_tip() | |||||
} | } | ||||
}); | }); | ||||
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); | |||||
}); | |||||
map_tooltip_x_position_and_show(relX) { | |||||
for(var i=this.x_axis_values.length - 1; i >= 0 ; i--) { | |||||
let x_val = this.x_axis_values[i]; | |||||
// let delta = i === 0 ? this.avg_unit_width : x_val - this.x_axis_values[i-1]; | |||||
if(relX > x_val - this.avg_unit_width/2) { | |||||
let x = x_val + this.translate_x - 0.5; | |||||
let y = this.y_min_tops[i] + this.translate_y; | |||||
let title = this.x.formatted && this.x.formatted.length>0 | |||||
? this.x.formatted[i] : this.x.values[i]; | |||||
let values = this.y.map((set, j) => { | |||||
return { | |||||
title: set.title, | |||||
value: set.formatted ? set.formatted[i] : set.values[i], | |||||
color: set.color || this.y_colors[j], | |||||
} | |||||
}); | |||||
this.tip.set_values(x, y, title, '', values); | |||||
this.tip.show_tip(); | |||||
break; | |||||
} | |||||
} | |||||
} | } | ||||
show_specific_values() { | show_specific_values() { | ||||
this.specific_values.map(d => { | this.specific_values.map(d => { | ||||
this.specific_y_lines.add(this.snap.g( | this.specific_y_lines.add(this.snap.g( | ||||
this.snap.line(0, 0, this.width, 0).attr({ | this.snap.line(0, 0, this.width, 0).attr({ | ||||
class: d.line_type === "dashed" ? "graph-dashed": "" | |||||
class: d.line_type === "dashed" ? "dashed": "" | |||||
}), | }), | ||||
this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ | this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ | ||||
dy: ".32em", | dy: ".32em", | ||||
@@ -434,6 +441,9 @@ frappe.ui.Graph = class Graph { | |||||
let width = total_width / args.no_of_datasets; | let width = total_width / args.no_of_datasets; | ||||
let current_x = start_x + width * index; | let current_x = start_x + width * index; | ||||
if(y == this.height) { | |||||
y = this.height * 0.98; | |||||
} | |||||
return this.snap.rect(current_x, y, width, this.height - y).attr({ | return this.snap.rect(current_x, y, width, this.height - y).attr({ | ||||
class: `bar mini fill ${color}` | class: `bar mini fill ${color}` | ||||
}); | }); | ||||
@@ -442,6 +452,11 @@ frappe.ui.Graph = class Graph { | |||||
return this.snap.circle(x, y, args.radius).attr({ | return this.snap.circle(x, y, args.radius).attr({ | ||||
class: `fill ${color}` | class: `fill ${color}` | ||||
}); | }); | ||||
}, | |||||
'rect': (x, y, args, color) => { | |||||
return this.snap.rect(x, y, args.width, args.height).attr({ | |||||
class: `fill ${color}` | |||||
}); | |||||
} | } | ||||
}; | }; | ||||
@@ -470,6 +485,7 @@ frappe.ui.BarGraph = class BarGraph extends frappe.ui.Graph { | |||||
this.unit_args = { | this.unit_args = { | ||||
type: 'bar', | type: 'bar', | ||||
args: { | args: { | ||||
// More intelligent width setting | |||||
space_width: this.y.length > 1 ? | space_width: this.y.length > 1 ? | ||||
me.avg_unit_width/2 : me.avg_unit_width/8, | me.avg_unit_width/2 : me.avg_unit_width/8, | ||||
no_of_datasets: this.y.length | no_of_datasets: this.y.length | ||||
@@ -498,10 +514,10 @@ frappe.ui.LineGraph = class LineGraph extends frappe.ui.Graph { | |||||
}; | }; | ||||
} | } | ||||
make_path(d) { | |||||
make_path(d, color) { | |||||
let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); | let points_list = d.y_tops.map((y, i) => (this.x_axis_values[i] + ',' + y)); | ||||
let path_str = "M"+points_list.join("L"); | let path_str = "M"+points_list.join("L"); | ||||
d.path = this.snap.path(path_str).attr({class: `stroke ${d.color}`}); | |||||
d.path = this.snap.path(path_str).attr({class: `stroke ${color}`}); | |||||
this.data_units.prepend(d.path); | this.data_units.prepend(d.path); | ||||
} | } | ||||
}; | }; | ||||
@@ -512,7 +528,9 @@ frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { | |||||
} | } | ||||
make_graph_area() { | make_graph_area() { | ||||
this.$graphics.addClass('graph-focus-margin'); | |||||
this.$graphics.addClass('graph-focus-margin').attr({ | |||||
style: `margin-top: 45px;` | |||||
}); | |||||
this.$stats_container.addClass('graph-focus-margin').attr({ | this.$stats_container.addClass('graph-focus-margin').attr({ | ||||
style: `padding-top: 0px; margin-bottom: 30px;` | style: `padding-top: 0px; margin-bottom: 30px;` | ||||
}); | }); | ||||
@@ -533,37 +551,523 @@ frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { | |||||
return total; | return total; | ||||
}); | }); | ||||
// Calculate x unit distances for tooltips | |||||
if(!this.x.colors) { | |||||
this.x.colors = ['green', 'blue', 'purple', 'red', 'orange', | |||||
'yellow', 'lightblue', 'lightgreen']; | |||||
} | |||||
} | } | ||||
setup_utils() { } | setup_utils() { } | ||||
setup_components() { | setup_components() { | ||||
this.$percentage_bar = $(`<div class="progress"> | this.$percentage_bar = $(`<div class="progress"> | ||||
</div>`).appendTo(this.$chart); | |||||
</div>`).appendTo(this.$chart); // get this.height, width and avg from this if needed | |||||
} | } | ||||
make_graph_components() { | make_graph_components() { | ||||
let grand_total = this.x.totals.reduce((a, b) => a + b, 0); | |||||
this.grand_total = this.x.totals.reduce((a, b) => a + b, 0); | |||||
this.x.units = []; | this.x.units = []; | ||||
this.x.totals.map((total, i) => { | this.x.totals.map((total, i) => { | ||||
let $part = $(`<div class="progress-bar background ${this.x.colors[i]}" | let $part = $(`<div class="progress-bar background ${this.x.colors[i]}" | ||||
style="width: ${total*100/grand_total}%"></div>`); | |||||
style="width: ${total*100/this.grand_total}%"></div>`); | |||||
this.x.units.push($part); | this.x.units.push($part); | ||||
this.$percentage_bar.append($part); | this.$percentage_bar.append($part); | ||||
}); | }); | ||||
} | } | ||||
make_tooltip() { } | |||||
bind_tooltip() { | |||||
this.x.units.map(($part, i) => { | |||||
$part.on('mouseenter', () => { | |||||
let g_off = this.$graphics.offset(), p_off = $part.offset(); | |||||
let x = p_off.left - g_off.left + $part.width()/2; | |||||
let y = p_off.top - g_off.top - 6; | |||||
let title = (this.x.formatted && this.x.formatted.length>0 | |||||
? this.x.formatted[i] : this.x.values[i]) + ': '; | |||||
let percent = (this.x.totals[i]*100/this.grand_total).toFixed(1); | |||||
this.tip.set_values(x, y, title, percent); | |||||
this.tip.show_tip(); | |||||
}); | |||||
}); | |||||
} | |||||
show_summary() { | show_summary() { | ||||
let values = this.x.formatted.length > 0 ? this.x.formatted : this.x.values; | |||||
let x_values = this.x.formatted && this.x.formatted.length > 0 | |||||
? this.x.formatted : this.x.values; | |||||
this.x.totals.map((d, i) => { | 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>`)); | |||||
if(d) { | |||||
this.$stats_container.append($(`<div class="stats"> | |||||
<span class="indicator ${this.x.colors[i]}"> | |||||
<span class="text-muted">${x_values[i]}:</span> | |||||
${d} | |||||
</span> | |||||
</div>`)); | |||||
} | |||||
}); | |||||
} | |||||
}; | |||||
frappe.ui.HeatMap = class HeatMap extends frappe.ui.Graph { | |||||
constructor({ | |||||
parent = null, | |||||
height = 240, | |||||
title = '', subtitle = '', | |||||
start = '', | |||||
domain = '', | |||||
subdomain = '', | |||||
data = {}, | |||||
discrete_domains = 0, | |||||
count_label = '', | |||||
// remove these graph related args | |||||
y = [], | |||||
x = [], | |||||
specific_values = [], | |||||
summary = [], | |||||
mode = 'heatmap', | |||||
} = {}) { | |||||
super(arguments[0]); | |||||
this.start = start; | |||||
this.data = data; | |||||
this.discrete_domains = discrete_domains; | |||||
this.count_label = count_label; | |||||
this.legend_colors = ['#ebedf0', '#c6e48b', '#7bc96f', '#239a3b', '#196127']; | |||||
} | |||||
setup_base_values() { | |||||
this.today = new Date(); | |||||
this.first_week_start = new Date(this.start.toDateString()); | |||||
this.last_week_start = new Date(this.today.toDateString()); | |||||
if(this.first_week_start.getDay() !== 7) { | |||||
this.add_days(this.first_week_start, (-1) * this.first_week_start.getDay()); | |||||
} | |||||
if(this.last_week_start.getDay() !== 7) { | |||||
this.add_days(this.last_week_start, (-1) * this.last_week_start.getDay()); | |||||
} | |||||
this.no_of_cols = this.get_weeks_between(this.first_week_start + '', this.last_week_start + '') + 1; | |||||
} | |||||
set_width() { | |||||
this.base_width = (this.no_of_cols) * 12; | |||||
} | |||||
setup_components() { | |||||
this.domain_label_group = this.snap.g().attr({ class: "domain-label-group chart-label" }); | |||||
this.data_groups = this.snap.g().attr({ class: "data-groups", transform: `translate(0, 20)` }); | |||||
} | |||||
setup_values() { | |||||
this.distribution = this.get_distribution(this.data, this.legend_colors); | |||||
this.month_names = ["January", "February", "March", "April", "May", "June", | |||||
"July", "August", "September", "October", "November", "December" | |||||
]; | |||||
this.render_all_weeks_and_store_x_values(this.no_of_cols); | |||||
} | |||||
render_all_weeks_and_store_x_values(no_of_weeks) { | |||||
let current_week_sunday = new Date(this.first_week_start); | |||||
this.week_col = 0; | |||||
this.current_month = current_week_sunday.getMonth(); | |||||
this.months = [this.current_month + '']; | |||||
this.month_weeks = {}, this.month_start_points = []; | |||||
this.month_weeks[this.current_month] = 0; | |||||
this.month_start_points.push(13); | |||||
for(var i = 0; i < no_of_weeks; i++) { | |||||
let data_group, month_change = 0; | |||||
let day = new Date(current_week_sunday); | |||||
[data_group, month_change] = this.get_week_squares_group(day, this.week_col); | |||||
this.data_groups.add(data_group); | |||||
this.week_col += 1 + parseInt(this.discrete_domains && month_change); | |||||
this.month_weeks[this.current_month]++; | |||||
if(month_change) { | |||||
this.current_month = (this.current_month + 1) % 12; | |||||
this.months.push(this.current_month + ''); | |||||
this.month_weeks[this.current_month] = 1; | |||||
} | |||||
this.add_days(current_week_sunday, 7); | |||||
} | |||||
this.render_month_labels(); | |||||
} | |||||
get_week_squares_group(current_date, index) { | |||||
const no_of_weekdays = 7; | |||||
const square_side = 10; | |||||
const cell_padding = 2; | |||||
const step = 1; | |||||
let month_change = 0; | |||||
let week_col_change = 0; | |||||
let data_group = this.snap.g().attr({ class: "data-group" }); | |||||
for(var y = 0, i = 0; i < no_of_weekdays; i += step, y += (square_side + cell_padding)) { | |||||
let data_value = 0; | |||||
let color_index = 0; | |||||
// TODO: More foolproof for any data | |||||
let timestamp = Math.floor(current_date.getTime()/1000).toFixed(1); | |||||
if(this.data[timestamp]) { | |||||
data_value = this.data[timestamp]; | |||||
color_index = this.get_max_checkpoint(data_value, this.distribution); | |||||
} | |||||
if(this.data[Math.round(timestamp)]) { | |||||
data_value = this.data[Math.round(timestamp)]; | |||||
color_index = this.get_max_checkpoint(data_value, this.distribution); | |||||
} | |||||
let x = 13 + (index + week_col_change) * 12; | |||||
data_group.add(this.snap.rect(x, y, square_side, square_side).attr({ | |||||
'class': `day`, | |||||
'fill': this.legend_colors[color_index], | |||||
'data-date': this.get_dd_mm_yyyy(current_date), | |||||
'data-value': data_value, | |||||
'data-day': current_date.getDay() | |||||
})); | |||||
let next_date = new Date(current_date); | |||||
this.add_days(next_date, 1); | |||||
if(next_date.getMonth() - current_date.getMonth()) { | |||||
month_change = 1; | |||||
if(this.discrete_domains) { | |||||
week_col_change = 1; | |||||
} | |||||
this.month_start_points.push(13 + (index + week_col_change) * 12); | |||||
} | |||||
current_date = next_date; | |||||
} | |||||
return [data_group, month_change]; | |||||
} | |||||
render_month_labels() { | |||||
this.first_month_label = 1; | |||||
// if (this.first_week_start.getDate() > 8) { | |||||
// this.first_month_label = 0; | |||||
// } | |||||
this.last_month_label = 1; | |||||
let first_month = this.months.shift(); | |||||
let first_month_start = this.month_start_points.shift(); | |||||
// render first month if | |||||
let last_month = this.months.pop(); | |||||
let last_month_start = this.month_start_points.pop(); | |||||
// render last month if | |||||
this.month_start_points.map((start, i) => { | |||||
let month_name = this.month_names[this.months[i]].substring(0, 3); | |||||
this.domain_label_group.add(this.snap.text(start + 12, 10, month_name).attr({ | |||||
dy: ".32em", | |||||
class: "y-value-text" | |||||
})); | |||||
}); | |||||
} | |||||
make_graph_components() { | |||||
this.container.find('.graph-stats-container, .sub-title, .title').hide(); | |||||
this.container.find('.graphics').css({'margin-top': '0px', 'padding-top': '0px'}); | |||||
} | |||||
bind_tooltip() { | |||||
this.container.on('mouseenter', '.day', (e) => { | |||||
let subdomain = $(e.target); | |||||
let count = subdomain.attr('data-value'); | |||||
let date_parts = subdomain.attr('data-date').split('-'); | |||||
let month = this.month_names[parseInt(date_parts[1])-1].substring(0, 3); | |||||
let g_off = this.$graphics.offset(), p_off = subdomain.offset(); | |||||
let width = parseInt(subdomain.attr('width')); | |||||
let x = p_off.left - g_off.left + (width+2)/2; | |||||
let y = p_off.top - g_off.top - (width+2)/2; | |||||
let value = count + ' ' + this.count_label; | |||||
let name = ' on ' + month + ' ' + date_parts[0] + ', ' + date_parts[2]; | |||||
this.tip.set_values(x, y, name, value, [], 1); | |||||
this.tip.show_tip(); | |||||
}); | |||||
} | |||||
update(data) { | |||||
this.data = data; | |||||
this.setup_values(); | |||||
} | |||||
get_distribution(data, mapper_array) { | |||||
let data_values = Object.keys(data).map(key => data[key]); | |||||
let data_max_value = Math.max(...data_values); | |||||
let distribution_step = 1 / (mapper_array.length - 1); | |||||
let distribution = []; | |||||
mapper_array.map((color, i) => { | |||||
let checkpoint = data_max_value * (distribution_step * i); | |||||
distribution.push(checkpoint); | |||||
}); | |||||
return distribution; | |||||
} | |||||
get_max_checkpoint(value, distribution) { | |||||
return distribution.filter(d => { | |||||
return d <= value; | |||||
}).length - 1; | |||||
} | |||||
// TODO: date utils, move these out | |||||
// https://stackoverflow.com/a/11252167/6495043 | |||||
treat_as_utc(date_str) { | |||||
let result = new Date(date_str); | |||||
result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); | |||||
return result; | |||||
} | |||||
get_dd_mm_yyyy(date) { | |||||
let dd = date.getDate(); | |||||
let mm = date.getMonth() + 1; // getMonth() is zero-based | |||||
return [ | |||||
(dd>9 ? '' : '0') + dd, | |||||
(mm>9 ? '' : '0') + mm, | |||||
date.getFullYear() | |||||
].join('-'); | |||||
} | |||||
get_weeks_between(start_date_str, end_date_str) { | |||||
return Math.ceil(this.get_days_between(start_date_str, end_date_str) / 7); | |||||
} | |||||
get_days_between(start_date_str, end_date_str) { | |||||
let milliseconds_per_day = 24 * 60 * 60 * 1000; | |||||
return (this.treat_as_utc(end_date_str) - this.treat_as_utc(start_date_str)) / milliseconds_per_day; | |||||
} | |||||
// mutates | |||||
add_days(date, number_of_days) { | |||||
date.setDate(date.getDate() + number_of_days); | |||||
} | |||||
get_month_name() {} | |||||
} | |||||
frappe.ui.SvgTip = class { | |||||
constructor({ | |||||
parent = null | |||||
}) { | |||||
this.parent = parent; | |||||
this.title_name = ''; | |||||
this.title_value = ''; | |||||
this.list_values = []; | |||||
this.title_value_first = 0; | |||||
this.x = 0; | |||||
this.y = 0; | |||||
this.top = 0; | |||||
this.left = 0; | |||||
this.setup(); | |||||
} | |||||
setup() { | |||||
this.make_tooltip(); | |||||
} | |||||
refresh() { | |||||
this.fill(); | |||||
this.calc_position(); | |||||
// this.show_tip(); | |||||
} | |||||
make_tooltip() { | |||||
this.container = $(`<div class="graph-svg-tip comparison"> | |||||
<span class="title"></span> | |||||
<ul class="data-point-list"></ul> | |||||
<div class="svg-pointer"></div> | |||||
</div>`).appendTo(this.parent); | |||||
this.hide_tip(); | |||||
this.title = this.container.find('.title'); | |||||
this.data_point_list = this.container.find('.data-point-list'); | |||||
this.parent.on('mouseleave', () => { | |||||
this.hide_tip(); | |||||
}); | |||||
} | |||||
fill() { | |||||
let title; | |||||
if(this.title_value_first) { | |||||
title = `<strong>${this.title_value}</strong>${this.title_name}`; | |||||
} else { | |||||
title = `${this.title_name}<strong>${this.title_value}</strong>`; | |||||
} | |||||
this.title.html(title); | |||||
this.data_point_list.empty(); | |||||
this.list_values.map((set, i) => { | |||||
let $li = $(`<li> | |||||
<strong style="display: block;">${set.value ? set.value : '' }</strong> | |||||
${set.title ? set.title : '' } | |||||
</li>`).addClass(`border-top ${set.color || 'black'}`); | |||||
this.data_point_list.append($li); | |||||
}); | |||||
} | |||||
calc_position() { | |||||
this.top = this.y - this.container.height(); | |||||
this.left = this.x - this.container.width()/2; | |||||
let max_left = this.parent.width() - this.container.width(); | |||||
let $pointer = this.container.find('.svg-pointer'); | |||||
if(this.left < 0) { | |||||
$pointer.css({ 'left': `calc(50% - ${-1 * this.left}px)` }); | |||||
this.left = 0; | |||||
} else if(this.left > max_left) { | |||||
let delta = this.left - max_left; | |||||
$pointer.css({ 'left': `calc(50% + ${delta}px)` }); | |||||
this.left = max_left; | |||||
} else { | |||||
$pointer.css({ 'left': `50%` }); | |||||
} | |||||
} | |||||
set_values(x, y, title_name = '', title_value = '', list_values = [], title_value_first = 0) { | |||||
this.title_name = title_name; | |||||
this.title_value = title_value; | |||||
this.list_values = list_values; | |||||
this.x = x; | |||||
this.y = y; | |||||
this.title_value_first = title_value_first; | |||||
this.refresh(); | |||||
} | |||||
hide_tip() { | |||||
this.container.css({ | |||||
'top': '0px', | |||||
'left': '0px', | |||||
'opacity': '0' | |||||
}); | |||||
} | |||||
show_tip() { | |||||
this.container.css({ | |||||
'top': this.top + 'px', | |||||
'left': this.left + 'px', | |||||
'opacity': '1' | |||||
}); | }); | ||||
} | } | ||||
}; | }; | ||||
frappe.provide("frappe.ui.graphs"); | |||||
frappe.ui.graphs.get_timeseries = function(start, frequency, length) { | |||||
} | |||||
frappe.ui.graphs.map_c3 = function(chart) { | |||||
if (chart.data) { | |||||
let data = chart.data; | |||||
let mode = chart.chart_type || 'line'; | |||||
if(mode === 'pie') { | |||||
mode = 'percentage'; | |||||
} | |||||
let x = {}, y = []; | |||||
if(data.columns) { | |||||
let columns = data.columns; | |||||
x.values = columns.filter(col => { | |||||
return col[0] === data.x; | |||||
})[0]; | |||||
if(x.values && x.values.length) { | |||||
let dataset_length = x.values.length; | |||||
let dirty = false; | |||||
columns.map(col => { | |||||
if(col[0] !== data.x) { | |||||
if(col.length === dataset_length) { | |||||
let title = col[0]; | |||||
col.splice(0, 1); | |||||
y.push({ | |||||
title: title, | |||||
values: col, | |||||
}); | |||||
} else { | |||||
dirty = true; | |||||
} | |||||
} | |||||
}) | |||||
if(dirty) { | |||||
return; | |||||
} | |||||
x.values.splice(0, 1); | |||||
return { | |||||
mode: mode, | |||||
y: y, | |||||
x: x | |||||
} | |||||
} | |||||
} else if(data.rows) { | |||||
let rows = data.rows; | |||||
x.values = rows[0]; | |||||
rows.map((row, i) => { | |||||
if(i === 0) { | |||||
x.values = row; | |||||
} else { | |||||
y.push({ | |||||
title: 'data' + i, | |||||
values: row, | |||||
}) | |||||
} | |||||
}); | |||||
return { | |||||
mode: mode, | |||||
y: y, | |||||
x: x | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// frappe.ui.CompositeGraph = class { | |||||
// constructor({ | |||||
// parent = null | |||||
// }) { | |||||
// this.parent = parent; | |||||
// this.title_name = ''; | |||||
// this.title_value = ''; | |||||
// this.list_values = []; | |||||
// this.x = 0; | |||||
// this.y = 0; | |||||
// this.top = 0; | |||||
// this.left = 0; | |||||
// this.setup(); | |||||
// } | |||||
// } |
@@ -3,6 +3,7 @@ | |||||
frappe.provide("frappe.views"); | frappe.provide("frappe.views"); | ||||
frappe.provide("frappe.query_reports"); | frappe.provide("frappe.query_reports"); | ||||
frappe.provide("frappe.ui.graphs"); | |||||
frappe.standard_pages["query-report"] = function() { | frappe.standard_pages["query-report"] = function() { | ||||
var wrapper = frappe.container.add_page('query-report'); | var wrapper = frappe.container.add_page('query-report'); | ||||
@@ -136,6 +136,11 @@ | |||||
.form-heatmap { | .form-heatmap { | ||||
.heatmap { | |||||
display: flex; | |||||
justify-content: center; | |||||
} | |||||
.heatmap-message { | .heatmap-message { | ||||
margin-top: 10px; | margin-top: 10px; | ||||
} | } | ||||
@@ -5,9 +5,10 @@ | |||||
margin: 0px 5%; | margin: 0px 5%; | ||||
} | } | ||||
.graph-graphics { | |||||
.graphics { | |||||
margin-top: 10px; | margin-top: 10px; | ||||
padding: 10px 0px; | |||||
padding-top: 10px; | |||||
padding-bottom: 10px; | |||||
position: relative; | position: relative; | ||||
} | } | ||||
@@ -43,28 +44,22 @@ | |||||
} | } | ||||
} | } | ||||
.bar-graph, .line-graph { | |||||
// baselines | |||||
.axis { | |||||
font-size: 10px; | |||||
fill: #6a737d; | |||||
.axis, .chart-label { | |||||
font-size: 10px; | |||||
fill: #959ba1; | |||||
line { | |||||
stroke: rgba(27,31,35,0.1); | |||||
} | |||||
line { | |||||
stroke: rgba(27,31,35,0.1); | |||||
} | } | ||||
} | } | ||||
.percentage-graph { | .percentage-graph { | ||||
margin-top: 35px; | |||||
.progress { | .progress { | ||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
} | } | ||||
.graph-data-points { | |||||
.data-points { | |||||
circle { | circle { | ||||
// fill: #28a745; | // fill: #28a745; | ||||
stroke: #fff; | stroke: #fff; | ||||
@@ -83,7 +78,7 @@ | |||||
} | } | ||||
} | } | ||||
line.graph-dashed { | |||||
line.dashed { | |||||
stroke-dasharray: 5,3; | stroke-dasharray: 5,3; | ||||
} | } | ||||
@@ -104,216 +99,215 @@ | |||||
text-anchor: middle; | 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; | |||||
.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; | 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; | |||||
ul { | |||||
margin: 0; | |||||
white-space: nowrap; | |||||
list-style: none; | |||||
} | } | ||||
&::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); | |||||
li { | |||||
display: inline-block; | |||||
padding: 5px 10px; | |||||
} | } | ||||
} | } | ||||
.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; | |||||
ul, ol { | |||||
padding-left: 0; | |||||
display: flex; | |||||
} | } | ||||
.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; | |||||
ul.data-point-list li { | |||||
min-width: 90px; | |||||
flex: 1; | |||||
} | } | ||||
.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; | |||||
strong { | |||||
color: #dfe2e5; | |||||
} | } | ||||
.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; | |||||
.svg-pointer { | |||||
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; | |||||
} | } |