mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2025-04-03 00:07:05 +00:00
246 lines
8.7 KiB
JavaScript
246 lines
8.7 KiB
JavaScript
/////
|
|
///// This file is part of Mail-in-a-Box-LDAP which is released under the
|
|
///// terms of the GNU Affero General Public License as published by the
|
|
///// Free Software Foundation, either version 3 of the License, or (at
|
|
///// your option) any later version. See file LICENSE or go to
|
|
///// https://github.com/downtownallday/mailinabox-ldap for full license
|
|
///// details.
|
|
/////
|
|
|
|
/*
|
|
stacked bar chart
|
|
*/
|
|
|
|
import { ChartPrefs, NumberFormatter, ChartVue } from "./charting.js";
|
|
|
|
|
|
export default Vue.component('chart-stacked-bar-timeseries', {
|
|
props: {
|
|
chart_data: { type:Object, required:false }, /* TimeseriesData */
|
|
width: { type:Number, default: ChartPrefs.default_width },
|
|
height: { type:Number, default: ChartPrefs.default_height },
|
|
},
|
|
|
|
render: function(ce) {
|
|
return ChartVue.create_svg(ce, [0, 0, this.width, this.height]);
|
|
},
|
|
|
|
data: function() {
|
|
return {
|
|
tsdata: null,
|
|
stacked: null,
|
|
margin: {
|
|
top: ChartPrefs.axis_font_size,
|
|
bottom: ChartPrefs.axis_font_size*2,
|
|
left: ChartPrefs.axis_font_size*3,
|
|
right: ChartPrefs.axis_font_size
|
|
},
|
|
xscale: null,
|
|
yscale: null,
|
|
colors: ChartPrefs.colors, /* array of colors */
|
|
};
|
|
},
|
|
|
|
mounted: function() {
|
|
if (this.chart_data) {
|
|
this.stack(this.chart_data);
|
|
this.draw();
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
'chart_data': function(newv, oldv) {
|
|
this.stack(newv);
|
|
this.draw();
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
stack: function(data) {
|
|
/* "stack" the data using d3.stack() */
|
|
// 1. reorganize into the format stack() wants -- an
|
|
// array of objects, with each object having a key of
|
|
// 'date', plus one for each series
|
|
var stacker_input = data.dates.map((d, i) => {
|
|
var array_el = { date: d }
|
|
data.series.forEach(s => {
|
|
array_el[s.name] = s.values[i];
|
|
})
|
|
return array_el;
|
|
});
|
|
|
|
// 2. call d3.stack() to get the stacking function, which
|
|
// creates yet another version of the series data,
|
|
// reformatted for more easily creating stacked bars.
|
|
//
|
|
// It returns a new array (see the d3 docs):
|
|
// [
|
|
// [ /* series 1 */
|
|
// [ Number, Number, data: { date: Date } ],
|
|
// [ ... ], ...
|
|
// ],
|
|
// [ /* series 2 */
|
|
// [ Number, Number, data: { date: Date } ],
|
|
// [ ... ], ...
|
|
// ],
|
|
// ...
|
|
// ]
|
|
//
|
|
var stacker = d3.stack()
|
|
.keys(data.series.map(s => s.name))
|
|
.order(d3.stackOrderNone)
|
|
.offset(d3.stackOffsetNone);
|
|
|
|
// 3. store the data
|
|
this.tsdata = data;
|
|
this.stacked = stacker(stacker_input);
|
|
},
|
|
|
|
|
|
draw: function() {
|
|
const svg = d3.select(this.$el);
|
|
svg.selectAll("g").remove();
|
|
|
|
if (this.tsdata.dates.length == 0) {
|
|
// no data ...
|
|
svg.append("g")
|
|
.append("text")
|
|
.attr("font-family", ChartPrefs.default_font_family)
|
|
.attr("font-size", ChartPrefs.label_font_size)
|
|
.attr("text-anchor", "middle")
|
|
.attr("x", this.width/2)
|
|
.attr("y", this.height/2)
|
|
.text("no data");
|
|
}
|
|
|
|
this.xscale = d3.scaleTime()
|
|
.domain(d3.extent(this.tsdata.dates))
|
|
.nice()
|
|
.range([this.margin.left, this.width - this.margin.right])
|
|
|
|
var barwidth = this.tsdata.barwidth(this.xscale);
|
|
var padding_x = barwidth / 2;
|
|
var padding_y = ChartVue.get_yAxisLegendBounds(this.tsdata).height + 2;
|
|
|
|
this.yscale = d3.scaleLinear()
|
|
.domain([
|
|
0,
|
|
d3.sum(this.tsdata.series, s => d3.max(s.values))
|
|
])
|
|
.range([
|
|
this.height - this.margin.bottom - padding_y,
|
|
this.margin.top,
|
|
]);
|
|
|
|
var g = svg.append("g")
|
|
.attr("transform", `translate(0, ${padding_y})`);
|
|
|
|
g.append("g")
|
|
.call(this.xAxis.bind(this, padding_x))
|
|
.attr("font-size", ChartPrefs.axis_font_size);
|
|
|
|
g.append("g")
|
|
.call(this.yAxis.bind(this, padding_y))
|
|
.attr("font-size", ChartPrefs.axis_font_size);
|
|
|
|
for (var s_idx=0; s_idx<this.tsdata.series.length; s_idx++) {
|
|
g.append("g")
|
|
.datum(s_idx)
|
|
.attr("fill", this.colors[s_idx])
|
|
.selectAll("rect")
|
|
.data(this.stacked[s_idx])
|
|
.join("rect")
|
|
.attr("x", d => this.xscale(d.data.date) - barwidth/2 + padding_x)
|
|
.attr("y", d => this.yscale(d[1]) + padding_y)
|
|
.attr("height", d => this.yscale(d[0]) - this.yscale(d[1]))
|
|
.attr("width", barwidth)
|
|
.call( hover.bind(this) )
|
|
|
|
// .append("title")
|
|
// .text(d => `${this.tsdata.series[s_idx].name}: ${NumberFormatter.format(d.data[this.tsdata.series[s_idx].name])}`)
|
|
;
|
|
}
|
|
|
|
g.append("g")
|
|
.attr("transform", `translate(${this.margin.left}, 0)`)
|
|
.call(
|
|
g => ChartVue.add_yAxisLegend(g, this.tsdata, this.colors)
|
|
);
|
|
|
|
var hovinfo = g.append("g");
|
|
|
|
function hover(rect) {
|
|
if ("ontouchstart" in document) rect
|
|
.style("-webkit-tap-highlight-color", "transparent")
|
|
.on("touchstart", entered.bind(this))
|
|
.on("touchend", left)
|
|
else rect
|
|
.on("mouseenter", entered.bind(this))
|
|
.on("mouseleave", left);
|
|
|
|
function entered(event, d) {
|
|
var rect = d3.select(event.target)
|
|
.attr("fill", "#ccc");
|
|
var d = rect.datum();
|
|
var s_idx = d3.select(rect.node().parentNode).datum();
|
|
var s_name = this.tsdata.series[s_idx].name;
|
|
var v = d.data[s_name];
|
|
var x = Number(rect.attr('x')) + barwidth/2;
|
|
//var y = Number(rect.attr('y')) + Number(rect.attr('height'))/2;
|
|
var y = Number(rect.attr('y'));
|
|
hovinfo.attr(
|
|
"transform",
|
|
`translate( ${x}, ${y} )`)
|
|
.append('text')
|
|
.attr("font-family", ChartPrefs.default_font_family)
|
|
.attr("font-size", ChartPrefs.default_font_size)
|
|
.attr("text-anchor", "middle")
|
|
.attr("y", -3)
|
|
.text(`${this.tsdata.formatDateTimeShort(d.data.date)}`);
|
|
hovinfo.append("text")
|
|
.attr("font-family", ChartPrefs.default_font_family)
|
|
.attr("font-size", ChartPrefs.default_font_size)
|
|
.attr("text-anchor", "middle")
|
|
.attr("y", -3 - ChartPrefs.default_font_size)
|
|
.text(`${s_name} (${NumberFormatter.format(v)})`);
|
|
|
|
}
|
|
|
|
function left(event) {
|
|
d3.select(event.target).attr("fill", null);
|
|
hovinfo.selectAll("text").remove();
|
|
}
|
|
}
|
|
},
|
|
|
|
xAxis: function(padding, g) {
|
|
var x = g.attr(
|
|
'transform',
|
|
`translate(${padding}, ${this.height - this.margin.bottom})`
|
|
).call(
|
|
d3.axisBottom(this.xscale)
|
|
.ticks(this.width / 80)
|
|
.tickSizeOuter(0)
|
|
);
|
|
return x;
|
|
},
|
|
|
|
yAxis: function(padding, g) {
|
|
var y = g.attr(
|
|
"transform",
|
|
`translate(${this.margin.left},${padding})`
|
|
).call(
|
|
d3.axisLeft(this.yscale)
|
|
.ticks(this.height/50)
|
|
).call(
|
|
g => g.select(".domain").remove()
|
|
);
|
|
|
|
return y;
|
|
},
|
|
|
|
}
|
|
});
|
|
|
|
|