mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-16 16:58:38 +01:00
In stacked area charts, group nearby points so they don't overlap
Summary: Ref T13279. We currently draw a point on the chart for each datapoint, but this leads to many overlapping circles. Instead, aggregate the raw points into display points ("events") at the end. Test Plan: Viewed a stacked area chart with many points, saw a more palatable number of drawn dots. Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20814
This commit is contained in:
parent
c5a4dea8cf
commit
f529abf900
4 changed files with 90 additions and 20 deletions
|
@ -141,7 +141,7 @@ return array(
|
||||||
'rsrc/css/phui/phui-big-info-view.css' => '362ad37b',
|
'rsrc/css/phui/phui-big-info-view.css' => '362ad37b',
|
||||||
'rsrc/css/phui/phui-box.css' => '5ed3b8cb',
|
'rsrc/css/phui/phui-box.css' => '5ed3b8cb',
|
||||||
'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30',
|
'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30',
|
||||||
'rsrc/css/phui/phui-chart.css' => '10135a9d',
|
'rsrc/css/phui/phui-chart.css' => '14df9ae3',
|
||||||
'rsrc/css/phui/phui-cms.css' => '8c05c41e',
|
'rsrc/css/phui/phui-cms.css' => '8c05c41e',
|
||||||
'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
|
'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
|
||||||
'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
|
'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
|
||||||
|
@ -390,7 +390,7 @@ return array(
|
||||||
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
|
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
|
||||||
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
|
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
|
||||||
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b',
|
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b',
|
||||||
'rsrc/js/application/fact/Chart.js' => 'eec96de0',
|
'rsrc/js/application/fact/Chart.js' => 'ddb9dd1f',
|
||||||
'rsrc/js/application/fact/ChartCurtainView.js' => '86954222',
|
'rsrc/js/application/fact/ChartCurtainView.js' => '86954222',
|
||||||
'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab',
|
'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab',
|
||||||
'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
|
'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
|
||||||
|
@ -699,7 +699,7 @@ return array(
|
||||||
'javelin-behavior-user-menu' => '60cd9241',
|
'javelin-behavior-user-menu' => '60cd9241',
|
||||||
'javelin-behavior-view-placeholder' => 'a9942052',
|
'javelin-behavior-view-placeholder' => 'a9942052',
|
||||||
'javelin-behavior-workflow' => '9623adc1',
|
'javelin-behavior-workflow' => '9623adc1',
|
||||||
'javelin-chart' => 'eec96de0',
|
'javelin-chart' => 'ddb9dd1f',
|
||||||
'javelin-chart-curtain-view' => '86954222',
|
'javelin-chart-curtain-view' => '86954222',
|
||||||
'javelin-chart-function-label' => '81de1dab',
|
'javelin-chart-function-label' => '81de1dab',
|
||||||
'javelin-color' => '78f811c9',
|
'javelin-color' => '78f811c9',
|
||||||
|
@ -828,7 +828,7 @@ return array(
|
||||||
'phui-calendar-day-css' => '9597d706',
|
'phui-calendar-day-css' => '9597d706',
|
||||||
'phui-calendar-list-css' => 'ccd7e4e2',
|
'phui-calendar-list-css' => 'ccd7e4e2',
|
||||||
'phui-calendar-month-css' => 'cb758c42',
|
'phui-calendar-month-css' => 'cb758c42',
|
||||||
'phui-chart-css' => '10135a9d',
|
'phui-chart-css' => '14df9ae3',
|
||||||
'phui-cms-css' => '8c05c41e',
|
'phui-cms-css' => '8c05c41e',
|
||||||
'phui-comment-form-css' => '68a2d99a',
|
'phui-comment-form-css' => '68a2d99a',
|
||||||
'phui-comment-panel-css' => 'ec4e31c0',
|
'phui-comment-panel-css' => 'ec4e31c0',
|
||||||
|
@ -2066,6 +2066,12 @@ return array(
|
||||||
'javelin-uri',
|
'javelin-uri',
|
||||||
'phabricator-notification',
|
'phabricator-notification',
|
||||||
),
|
),
|
||||||
|
'ddb9dd1f' => array(
|
||||||
|
'phui-chart-css',
|
||||||
|
'd3',
|
||||||
|
'javelin-chart-curtain-view',
|
||||||
|
'javelin-chart-function-label',
|
||||||
|
),
|
||||||
'dfa1d313' => array(
|
'dfa1d313' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
@ -2127,12 +2133,6 @@ return array(
|
||||||
'phabricator-keyboard-shortcut',
|
'phabricator-keyboard-shortcut',
|
||||||
'javelin-stratcom',
|
'javelin-stratcom',
|
||||||
),
|
),
|
||||||
'eec96de0' => array(
|
|
||||||
'phui-chart-css',
|
|
||||||
'd3',
|
|
||||||
'javelin-chart-curtain-view',
|
|
||||||
'javelin-chart-function-label',
|
|
||||||
),
|
|
||||||
'ef836bf2' => array(
|
'ef836bf2' => array(
|
||||||
'javelin-behavior',
|
'javelin-behavior',
|
||||||
'javelin-dom',
|
'javelin-dom',
|
||||||
|
|
|
@ -17,8 +17,8 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
|
|
||||||
$datapoints = $function->newDatapoints($data_query);
|
$datapoints = $function->newDatapoints($data_query);
|
||||||
foreach ($datapoints as $point) {
|
foreach ($datapoints as $point) {
|
||||||
$x = $point['x'];
|
$x_value = $point['x'];
|
||||||
$function_points[$function_idx][$x] = $point;
|
$function_points[$function_idx][$x_value] = $point;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,12 +140,67 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
|
|
||||||
$series = array_reverse($series);
|
$series = array_reverse($series);
|
||||||
|
|
||||||
|
// We're going to group multiple events into a single point if they have
|
||||||
|
// X values that are very close to one another.
|
||||||
|
//
|
||||||
|
// If the Y values are also close to one another (these points are near
|
||||||
|
// one another in a horizontal line), it can be hard to select any
|
||||||
|
// individual point with the mouse.
|
||||||
|
//
|
||||||
|
// Even if the Y values are not close together (the points are on a
|
||||||
|
// fairly steep slope up or down), it's usually better to be able to
|
||||||
|
// mouse over a single point at the top or bottom of the slope and get
|
||||||
|
// a summary of what's going on.
|
||||||
|
|
||||||
|
$domain_max = $data_query->getMaximumValue();
|
||||||
|
$domain_min = $data_query->getMinimumValue();
|
||||||
|
$resolution = ($domain_max - $domain_min) / 100;
|
||||||
|
|
||||||
$events = array();
|
$events = array();
|
||||||
foreach ($raw_points as $function_idx => $points) {
|
foreach ($raw_points as $function_idx => $points) {
|
||||||
$event_list = array();
|
$event_list = array();
|
||||||
|
|
||||||
|
$event_group = array();
|
||||||
|
$head_event = null;
|
||||||
foreach ($points as $point) {
|
foreach ($points as $point) {
|
||||||
$event_list[] = $point;
|
$x = $point['x'];
|
||||||
|
|
||||||
|
if ($head_event === null) {
|
||||||
|
// We don't have any points yet, so start a new group.
|
||||||
|
$head_event = $x;
|
||||||
|
$event_group[] = $point;
|
||||||
|
} else if (($x - $head_event) <= $resolution) {
|
||||||
|
// This point is close to the first point in this group, so
|
||||||
|
// add it to the existing group.
|
||||||
|
$event_group[] = $point;
|
||||||
|
} else {
|
||||||
|
// This point is not close to the first point in the group,
|
||||||
|
// so create a new group.
|
||||||
|
$event_list[] = $event_group;
|
||||||
|
$head_event = $x;
|
||||||
|
$event_group = array($point);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($event_group) {
|
||||||
|
$event_list[] = $event_group;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event_spec = array();
|
||||||
|
foreach ($event_list as $key => $event_points) {
|
||||||
|
// NOTE: We're using the last point as the representative point so
|
||||||
|
// that you can learn about a section of a chart by hovering over
|
||||||
|
// the point to right of the section, which is more intuitive than
|
||||||
|
// other points.
|
||||||
|
$event = last($event_points);
|
||||||
|
|
||||||
|
$event = $event + array(
|
||||||
|
'n' => count($event_points),
|
||||||
|
);
|
||||||
|
|
||||||
|
$event_list[$key] = $event;
|
||||||
|
}
|
||||||
|
|
||||||
$events[] = $event_list;
|
$events[] = $event_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,16 +36,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart .point {
|
.chart .point {
|
||||||
fill: {$lightblue};
|
fill: #ffffff;
|
||||||
stroke: {$blue};
|
stroke: {$blue};
|
||||||
stroke-width: 1px;
|
stroke-width: 2px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-tooltip {
|
.chart-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 16px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
background: {$lightbluebackground};
|
background: {$lightbluebackground};
|
||||||
|
|
22
webroot/rsrc/js/application/fact/Chart.js
vendored
22
webroot/rsrc/js/application/fact/Chart.js
vendored
|
@ -133,6 +133,8 @@ JX.install('Chart', {
|
||||||
},
|
},
|
||||||
|
|
||||||
_newStackedArea: function(g, dataset, x, y, div, curtain) {
|
_newStackedArea: function(g, dataset, x, y, div, curtain) {
|
||||||
|
var ii;
|
||||||
|
|
||||||
var to_date = JX.bind(this, this._newDate);
|
var to_date = JX.bind(this, this._newDate);
|
||||||
|
|
||||||
var area = d3.area()
|
var area = d3.area()
|
||||||
|
@ -144,7 +146,7 @@ JX.install('Chart', {
|
||||||
.x(function(d) { return x(to_date(d.x)); })
|
.x(function(d) { return x(to_date(d.x)); })
|
||||||
.y(function(d) { return y(d.y1); });
|
.y(function(d) { return y(d.y1); });
|
||||||
|
|
||||||
for (var ii = 0; ii < dataset.data.length; ii++) {
|
for (ii = 0; ii < dataset.data.length; ii++) {
|
||||||
var label = new JX.ChartFunctionLabel(dataset.labels[ii]);
|
var label = new JX.ChartFunctionLabel(dataset.labels[ii]);
|
||||||
|
|
||||||
var fill_color = label.getFillColor() || label.getColor();
|
var fill_color = label.getFillColor() || label.getColor();
|
||||||
|
@ -160,6 +162,11 @@ JX.install('Chart', {
|
||||||
.style('stroke', stroke_color)
|
.style('stroke', stroke_color)
|
||||||
.attr('d', line(dataset.data[ii]));
|
.attr('d', line(dataset.data[ii]));
|
||||||
|
|
||||||
|
curtain.addFunctionLabel(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've drawn all the areas and lines, draw the dots.
|
||||||
|
for (ii = 0; ii < dataset.data.length; ii++) {
|
||||||
g.selectAll('dot')
|
g.selectAll('dot')
|
||||||
.data(dataset.events[ii])
|
.data(dataset.events[ii])
|
||||||
.enter()
|
.enter()
|
||||||
|
@ -178,8 +185,16 @@ JX.install('Chart', {
|
||||||
|
|
||||||
var d_d = dd.getDate();
|
var d_d = dd.getDate();
|
||||||
|
|
||||||
|
var y = parseInt(d.y1);
|
||||||
|
|
||||||
|
var label = d.n + ' Points';
|
||||||
|
|
||||||
|
var view =
|
||||||
|
d_y + '-' + d_m + '-' + d_d + ': ' + y + '<br />' +
|
||||||
|
label;
|
||||||
|
|
||||||
div
|
div
|
||||||
.html(d_y + '-' + d_m + '-' + d_d + ': ' + d.y1)
|
.html(view)
|
||||||
.style('opacity', 0.9)
|
.style('opacity', 0.9)
|
||||||
.style('left', (d3.event.pageX - 60) + 'px')
|
.style('left', (d3.event.pageX - 60) + 'px')
|
||||||
.style('top', (d3.event.pageY - 38) + 'px');
|
.style('top', (d3.event.pageY - 38) + 'px');
|
||||||
|
@ -187,9 +202,8 @@ JX.install('Chart', {
|
||||||
.on('mouseout', function() {
|
.on('mouseout', function() {
|
||||||
div.style('opacity', 0);
|
div.style('opacity', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
curtain.addFunctionLabel(label);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_newDate: function(epoch) {
|
_newDate: function(epoch) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue