2019-04-17 04:24:18 +02:00
|
|
|
/**
|
|
|
|
* @provides javelin-chart
|
|
|
|
* @requires phui-chart-css
|
|
|
|
* d3
|
2019-05-08 15:50:23 +02:00
|
|
|
* javelin-chart-curtain-view
|
2019-05-08 16:06:14 +02:00
|
|
|
* javelin-chart-function-label
|
2019-04-17 04:24:18 +02:00
|
|
|
*/
|
|
|
|
JX.install('Chart', {
|
|
|
|
|
|
|
|
construct: function(root_node) {
|
|
|
|
this._rootNode = root_node;
|
|
|
|
|
|
|
|
JX.Stratcom.listen('resize', null, JX.bind(this, this._redraw));
|
|
|
|
},
|
|
|
|
|
|
|
|
members: {
|
|
|
|
_rootNode: null,
|
|
|
|
_data: null,
|
2019-05-08 15:50:23 +02:00
|
|
|
_chartContainerNode: null,
|
|
|
|
_curtain: null,
|
2019-04-17 04:24:18 +02:00
|
|
|
|
|
|
|
setData: function(blob) {
|
|
|
|
this._data = blob;
|
|
|
|
this._redraw();
|
|
|
|
},
|
|
|
|
|
|
|
|
_redraw: function() {
|
|
|
|
if (!this._data) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var hardpoint = this._rootNode;
|
2019-05-08 15:50:23 +02:00
|
|
|
var curtain = this._getCurtain();
|
|
|
|
var container_node = this._getChartContainerNode();
|
|
|
|
|
|
|
|
var content = [
|
|
|
|
container_node,
|
|
|
|
curtain.getNode(),
|
|
|
|
];
|
|
|
|
|
|
|
|
JX.DOM.setContent(hardpoint, content);
|
2019-05-03 21:26:35 +02:00
|
|
|
|
|
|
|
// Remove the old chart (if one exists) before drawing the new chart.
|
2019-05-08 15:50:23 +02:00
|
|
|
JX.DOM.setContent(container_node, []);
|
2019-05-03 21:26:35 +02:00
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
var viewport = JX.Vector.getDim(container_node);
|
2019-04-17 04:24:18 +02:00
|
|
|
var config = this._data;
|
|
|
|
|
|
|
|
function css_function(n) {
|
|
|
|
return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')';
|
|
|
|
}
|
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
var padding = {};
|
|
|
|
if (JX.Device.isDesktop()) {
|
|
|
|
padding = {
|
|
|
|
top: 24,
|
|
|
|
left: 48,
|
|
|
|
bottom: 48,
|
|
|
|
right: 12
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
padding = {
|
|
|
|
top: 12,
|
|
|
|
left: 36,
|
|
|
|
bottom: 24,
|
|
|
|
right: 4
|
|
|
|
};
|
|
|
|
}
|
2019-04-17 04:24:18 +02:00
|
|
|
|
|
|
|
var size = {
|
|
|
|
frameWidth: viewport.x,
|
|
|
|
frameHeight: viewport.y,
|
|
|
|
};
|
|
|
|
|
|
|
|
size.width = size.frameWidth - padding.left - padding.right;
|
|
|
|
size.height = size.frameHeight - padding.top - padding.bottom;
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
var x = d3.scaleTime()
|
2019-04-17 04:24:18 +02:00
|
|
|
.range([0, size.width]);
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
var y = d3.scaleLinear()
|
2019-04-17 04:24:18 +02:00
|
|
|
.range([size.height, 0]);
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
var xAxis = d3.axisBottom(x);
|
|
|
|
var yAxis = d3.axisLeft(y);
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
var svg = d3.select(container_node).append('svg')
|
2019-04-17 04:24:18 +02:00
|
|
|
.attr('width', size.frameWidth)
|
|
|
|
.attr('height', size.frameHeight)
|
|
|
|
.attr('class', 'chart');
|
|
|
|
|
|
|
|
var g = svg.append('g')
|
2019-05-08 15:50:23 +02:00
|
|
|
.attr(
|
|
|
|
'transform',
|
|
|
|
css_function('translate', padding.left, padding.top));
|
2019-04-17 04:24:18 +02:00
|
|
|
|
|
|
|
g.append('rect')
|
2019-05-08 15:50:23 +02:00
|
|
|
.attr('class', 'inner')
|
|
|
|
.attr('width', size.width)
|
|
|
|
.attr('height', size.height);
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
x.domain([this._newDate(config.xMin), this._newDate(config.xMax)]);
|
2019-04-17 04:24:18 +02:00
|
|
|
y.domain([config.yMin, config.yMax]);
|
|
|
|
|
|
|
|
var div = d3.select('body')
|
|
|
|
.append('div')
|
|
|
|
.attr('class', 'chart-tooltip')
|
|
|
|
.style('opacity', 0);
|
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
curtain.reset();
|
|
|
|
|
2019-04-17 04:24:18 +02:00
|
|
|
for (var idx = 0; idx < config.datasets.length; idx++) {
|
|
|
|
var dataset = config.datasets[idx];
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
switch (dataset.type) {
|
|
|
|
case 'stacked-area':
|
2019-05-08 15:50:23 +02:00
|
|
|
this._newStackedArea(g, dataset, x, y, div, curtain);
|
2019-05-03 21:26:35 +02:00
|
|
|
break;
|
2019-04-17 04:24:18 +02:00
|
|
|
}
|
2019-05-03 21:26:35 +02:00
|
|
|
}
|
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
curtain.redraw();
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
g.append('g')
|
|
|
|
.attr('class', 'x axis')
|
|
|
|
.attr('transform', css_function('translate', 0, size.height))
|
|
|
|
.call(xAxis);
|
|
|
|
|
|
|
|
g.append('g')
|
|
|
|
.attr('class', 'y axis')
|
|
|
|
.attr('transform', css_function('translate', 0, 0))
|
|
|
|
.call(yAxis);
|
|
|
|
},
|
|
|
|
|
2019-05-08 15:50:23 +02:00
|
|
|
_newStackedArea: function(g, dataset, x, y, div, curtain) {
|
2019-05-09 17:58:11 +02:00
|
|
|
var ii;
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
var to_date = JX.bind(this, this._newDate);
|
|
|
|
|
|
|
|
var area = d3.area()
|
|
|
|
.x(function(d) { return x(to_date(d.x)); })
|
2019-09-17 17:58:32 +02:00
|
|
|
.y0(function(d) {
|
|
|
|
// When the area is positive, draw it above the X axis. When the area
|
|
|
|
// is negative, draw it below the X axis. We currently avoid having
|
|
|
|
// functions which cross the X axis by clever construction.
|
|
|
|
if (d.y0 >= 0 && d.y1 >= 0) {
|
|
|
|
return y(d.y0);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (d.y0 <= 0 && d.y1 <= 0) {
|
|
|
|
return y(d.y0);
|
|
|
|
}
|
|
|
|
|
|
|
|
return y(0);
|
|
|
|
})
|
2019-05-03 21:26:35 +02:00
|
|
|
.y1(function(d) { return y(d.y1); });
|
|
|
|
|
|
|
|
var line = d3.line()
|
|
|
|
.x(function(d) { return x(to_date(d.x)); })
|
|
|
|
.y(function(d) { return y(d.y1); });
|
|
|
|
|
2019-05-09 17:58:11 +02:00
|
|
|
for (ii = 0; ii < dataset.data.length; ii++) {
|
2019-05-08 16:06:14 +02:00
|
|
|
var label = new JX.ChartFunctionLabel(dataset.labels[ii]);
|
|
|
|
|
|
|
|
var fill_color = label.getFillColor() || label.getColor();
|
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
g.append('path')
|
2019-05-08 16:06:14 +02:00
|
|
|
.style('fill', fill_color)
|
2019-05-03 21:26:35 +02:00
|
|
|
.attr('d', area(dataset.data[ii]));
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-08 16:06:14 +02:00
|
|
|
var stroke_color = label.getColor();
|
|
|
|
|
2019-04-17 04:24:18 +02:00
|
|
|
g.append('path')
|
|
|
|
.attr('class', 'line')
|
2019-05-08 16:06:14 +02:00
|
|
|
.style('stroke', stroke_color)
|
2019-05-03 21:26:35 +02:00
|
|
|
.attr('d', line(dataset.data[ii]));
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-09 17:58:11 +02:00
|
|
|
curtain.addFunctionLabel(label);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now that we've drawn all the areas and lines, draw the dots.
|
|
|
|
for (ii = 0; ii < dataset.data.length; ii++) {
|
2019-04-17 04:24:18 +02:00
|
|
|
g.selectAll('dot')
|
2019-05-03 21:26:35 +02:00
|
|
|
.data(dataset.events[ii])
|
2019-04-17 04:24:18 +02:00
|
|
|
.enter()
|
|
|
|
.append('circle')
|
|
|
|
.attr('class', 'point')
|
|
|
|
.attr('r', 3)
|
2019-05-03 21:26:35 +02:00
|
|
|
.attr('cx', function(d) { return x(to_date(d.x)); })
|
|
|
|
.attr('cy', function(d) { return y(d.y1); })
|
Update d3 from version 5.9.2 to 6.7.0
Summary:
Update the d3 library to its last 6.x version available on https://github.com/d3/d3/releases
This also requires updating the tooltip event handling of dots in `Chart.js` to avoid an `Uncaught TypeError: d3.event is undefined` per https://observablehq.com/@d3/d3v6-migration-guide#event-management linked from https://github.com/d3/d3/releases/tag/v6.0.0
Closes T15820
Test Plan:
* Enable the Facts application, go to the Reports of a Project with task changes over time, look at charts, hover over data points, read the tooltip - e.g. on http://phorge.localhost/project/reports/1/ or http://phorge.localhost/maniphest/report/burn/
* Check HTML source of above URIs for the `<script type="text/javascript">` loading `d3.min.js` and open the JS file to verify the d3 version number bump.
* Check Console of web browser's developer tools for no errors.
Reviewers: O1 Blessed Committers, speck
Reviewed By: O1 Blessed Committers, speck
Subscribers: speck, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno
Maniphest Tasks: T15820
Differential Revision: https://we.phorge.it/D25631
2024-05-09 17:21:43 +02:00
|
|
|
.on('mouseover', function(event, d) {
|
2019-05-03 21:26:35 +02:00
|
|
|
var dd = to_date(d.x);
|
|
|
|
|
|
|
|
var d_y = dd.getFullYear();
|
2019-04-17 04:24:18 +02:00
|
|
|
|
|
|
|
// NOTE: Javascript months are zero-based. See PHI1017.
|
2024-05-09 15:50:39 +02:00
|
|
|
var d_m = (dd.getMonth() + 1).toString();
|
|
|
|
if (d_m.length == 1) {
|
|
|
|
d_m = '0' + d_m;
|
|
|
|
}
|
|
|
|
|
|
|
|
var d_d = dd.getDate().toString();
|
|
|
|
if (d_d.length == 1) {
|
|
|
|
d_d = '0' + d_d;
|
|
|
|
}
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-09 17:58:11 +02:00
|
|
|
var y = parseInt(d.y1);
|
|
|
|
|
|
|
|
var label = d.n + ' Points';
|
|
|
|
|
|
|
|
var view =
|
|
|
|
d_y + '-' + d_m + '-' + d_d + ': ' + y + '<br />' +
|
|
|
|
label;
|
|
|
|
|
2019-04-17 04:24:18 +02:00
|
|
|
div
|
2019-05-09 17:58:11 +02:00
|
|
|
.html(view)
|
2019-04-17 04:24:18 +02:00
|
|
|
.style('opacity', 0.9)
|
Update d3 from version 5.9.2 to 6.7.0
Summary:
Update the d3 library to its last 6.x version available on https://github.com/d3/d3/releases
This also requires updating the tooltip event handling of dots in `Chart.js` to avoid an `Uncaught TypeError: d3.event is undefined` per https://observablehq.com/@d3/d3v6-migration-guide#event-management linked from https://github.com/d3/d3/releases/tag/v6.0.0
Closes T15820
Test Plan:
* Enable the Facts application, go to the Reports of a Project with task changes over time, look at charts, hover over data points, read the tooltip - e.g. on http://phorge.localhost/project/reports/1/ or http://phorge.localhost/maniphest/report/burn/
* Check HTML source of above URIs for the `<script type="text/javascript">` loading `d3.min.js` and open the JS file to verify the d3 version number bump.
* Check Console of web browser's developer tools for no errors.
Reviewers: O1 Blessed Committers, speck
Reviewed By: O1 Blessed Committers, speck
Subscribers: speck, tobiaswiese, valerio.bozzolan, Matthew, Cigaryno
Maniphest Tasks: T15820
Differential Revision: https://we.phorge.it/D25631
2024-05-09 17:21:43 +02:00
|
|
|
.style('left', (event.pageX - 60) + 'px')
|
|
|
|
.style('top', (event.pageY - 38) + 'px');
|
2019-04-17 04:24:18 +02:00
|
|
|
})
|
|
|
|
.on('mouseout', function() {
|
|
|
|
div.style('opacity', 0);
|
|
|
|
});
|
2019-05-03 21:26:35 +02:00
|
|
|
}
|
2019-05-09 17:58:11 +02:00
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
},
|
2019-04-17 04:24:18 +02:00
|
|
|
|
2019-05-03 21:26:35 +02:00
|
|
|
_newDate: function(epoch) {
|
|
|
|
return new Date(epoch * 1000);
|
2019-05-08 15:50:23 +02:00
|
|
|
},
|
|
|
|
|
|
|
|
_getCurtain: function() {
|
|
|
|
if (!this._curtain) {
|
|
|
|
this._curtain = new JX.ChartCurtainView();
|
|
|
|
}
|
|
|
|
return this._curtain;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getChartContainerNode: function() {
|
|
|
|
if (!this._chartContainerNode) {
|
|
|
|
var attrs = {
|
|
|
|
className: 'chart-container'
|
|
|
|
};
|
|
|
|
|
|
|
|
this._chartContainerNode = JX.$N('div', attrs);
|
|
|
|
}
|
|
|
|
return this._chartContainerNode;
|
2019-04-17 04:24:18 +02:00
|
|
|
}
|
2019-05-03 21:26:35 +02:00
|
|
|
|
2019-04-17 04:24:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
});
|