|
|
@@ -28,7 +28,6 @@ frappe.ui.Graph = class Graph { |
|
|
|
specific_values = [], |
|
|
|
summary = [], |
|
|
|
|
|
|
|
color = 'blue', |
|
|
|
mode = '', |
|
|
|
}) { |
|
|
|
|
|
|
@@ -43,27 +42,28 @@ frappe.ui.Graph = class Graph { |
|
|
|
} |
|
|
|
|
|
|
|
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.subtitle = subtitle; |
|
|
|
|
|
|
|
// Begin axis graph-related args |
|
|
|
|
|
|
|
this.y = y; |
|
|
|
this.x = x; |
|
|
|
|
|
|
|
this.specific_values = specific_values; |
|
|
|
this.summary = summary; |
|
|
|
|
|
|
|
this.color = color; |
|
|
|
this.mode = mode; |
|
|
|
|
|
|
|
// this.current_hover_index = 0; |
|
|
|
// this.current_selected_index = 0; |
|
|
|
|
|
|
|
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)); |
|
|
|
} |
|
|
@@ -81,18 +81,16 @@ frappe.ui.Graph = class Graph { |
|
|
|
|
|
|
|
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_components(); |
|
|
|
this.setup_values(); |
|
|
|
|
|
|
|
this.setup_utils(); |
|
|
|
|
|
|
|
this.setup_components(); |
|
|
|
this.make_graph_components(); |
|
|
|
|
|
|
|
this.make_tooltip(); |
|
|
|
|
|
|
|
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() { |
|
|
|
// Graph needs a dedicated parent element |
|
|
|
this.parent.empty(); |
|
|
@@ -110,11 +122,11 @@ frappe.ui.Graph = class Graph { |
|
|
|
.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="graphics"></div>`)) |
|
|
|
.append($(`<div class="graph-stats-container"></div>`)) |
|
|
|
.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.$graph = $('<div>') |
|
|
@@ -130,6 +142,13 @@ frappe.ui.Graph = class Graph { |
|
|
|
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() { |
|
|
|
// Multiplier |
|
|
|
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() { |
|
|
|
this.make_y_axis(); |
|
|
|
this.make_x_axis(); |
|
|
|
this.y_colors = ['lightblue', 'purple', 'blue', 'green', 'lightgreen', |
|
|
|
'yellow', 'orange', 'red'] |
|
|
|
|
|
|
|
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) { |
|
|
@@ -246,6 +260,11 @@ frappe.ui.Graph = class Graph { |
|
|
|
transform: `translate(0,${start_at})` |
|
|
|
}); |
|
|
|
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.snap.line(0, 0, 0, height), |
|
|
|
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) { |
|
|
|
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); |
|
|
|
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); |
|
|
|
}); |
|
|
@@ -272,75 +296,58 @@ frappe.ui.Graph = class Graph { |
|
|
|
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'); |
|
|
|
|
|
|
|
// should be w.r.t. this.parent |
|
|
|
this.tip = new frappe.ui.SvgTip({ |
|
|
|
parent: this.$graphics, |
|
|
|
}); |
|
|
|
this.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) => { |
|
|
|
let offset = $(this.$graphics).offset(); |
|
|
|
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; |
|
|
|
} |
|
|
|
} |
|
|
|
if(relY < this.height + this.translate_y * 2) { |
|
|
|
this.map_tooltip_x_position_and_show(relX); |
|
|
|
} 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() { |
|
|
|
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": "" |
|
|
|
class: d.line_type === "dashed" ? "dashed": "" |
|
|
|
}), |
|
|
|
this.snap.text(this.width + 5, 0, d.name.toUpperCase()).attr({ |
|
|
|
dy: ".32em", |
|
|
@@ -434,6 +441,9 @@ frappe.ui.Graph = class Graph { |
|
|
|
|
|
|
|
let width = total_width / args.no_of_datasets; |
|
|
|
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({ |
|
|
|
class: `bar mini fill ${color}` |
|
|
|
}); |
|
|
@@ -442,6 +452,11 @@ frappe.ui.Graph = class Graph { |
|
|
|
return this.snap.circle(x, y, args.radius).attr({ |
|
|
|
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 = { |
|
|
|
type: 'bar', |
|
|
|
args: { |
|
|
|
// More intelligent width setting |
|
|
|
space_width: this.y.length > 1 ? |
|
|
|
me.avg_unit_width/2 : me.avg_unit_width/8, |
|
|
|
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 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); |
|
|
|
} |
|
|
|
}; |
|
|
@@ -512,7 +528,9 @@ frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { |
|
|
|
} |
|
|
|
|
|
|
|
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({ |
|
|
|
style: `padding-top: 0px; margin-bottom: 30px;` |
|
|
|
}); |
|
|
@@ -533,37 +551,523 @@ frappe.ui.PercentageGraph = class PercentageGraph extends frappe.ui.Graph { |
|
|
|
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_components() { |
|
|
|
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() { |
|
|
|
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.totals.map((total, 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.$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() { |
|
|
|
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.$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(); |
|
|
|
// } |
|
|
|
// } |