Skip to content

Commit 45b3c23

Browse files
author
epriestley
committedApr 18, 2019
Fetch chart data via async request and redraw charts when the window is resized
Summary: Depends on D20439. Ref T13279. Some day, charts will probably need to reload themselves or do a bunch of defer/request-shaping magic when they're on a dashboard with 900 other charts. Give the controller separate "HTML placeholder" and "actual data" modes, and make the placeholder fetch the data in a separate request. Then, make the chart redraw if you resize the window instead of staying at whatever size it started as. Test Plan: - Loaded a chart, saw it load data asynchronously. - Resized the window, saw the chart resize. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20440
1 parent 044c6fb commit 45b3c23

File tree

5 files changed

+220
-152
lines changed

5 files changed

+220
-152
lines changed
 

‎resources/celerity/map.php

+13-8
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@
389389
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
390390
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
391391
'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b',
392+
'rsrc/js/application/fact/Chart.js' => 'fcb0c07d',
392393
'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
393394
'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb',
394395
'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1',
@@ -397,7 +398,7 @@
397398
'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3',
398399
'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d',
399400
'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688',
400-
'rsrc/js/application/maniphest/behavior-line-chart.js' => '495cf14d',
401+
'rsrc/js/application/maniphest/behavior-line-chart.js' => 'ad258e28',
401402
'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867',
402403
'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9',
403404
'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a',
@@ -625,7 +626,7 @@
625626
'javelin-behavior-icon-composer' => '38a6cedb',
626627
'javelin-behavior-launch-icon-composer' => 'a17b84f1',
627628
'javelin-behavior-lightbox-attachments' => 'c7e748bf',
628-
'javelin-behavior-line-chart' => '495cf14d',
629+
'javelin-behavior-line-chart' => 'ad258e28',
629630
'javelin-behavior-linked-container' => '74446546',
630631
'javelin-behavior-maniphest-batch-selector' => '139ef688',
631632
'javelin-behavior-maniphest-list-editor' => 'c687e867',
@@ -695,6 +696,7 @@
695696
'javelin-behavior-user-menu' => '60cd9241',
696697
'javelin-behavior-view-placeholder' => 'a9942052',
697698
'javelin-behavior-workflow' => '9623adc1',
699+
'javelin-chart' => 'fcb0c07d',
698700
'javelin-color' => '78f811c9',
699701
'javelin-cookie' => '05d290ef',
700702
'javelin-diffusion-locate-file-source' => '94243d89',
@@ -1319,12 +1321,6 @@
13191321
'490e2e2e' => array(
13201322
'phui-oi-list-view-css',
13211323
),
1322-
'495cf14d' => array(
1323-
'javelin-behavior',
1324-
'javelin-dom',
1325-
'javelin-vector',
1326-
'phui-chart-css',
1327-
),
13281324
'4a7fb02b' => array(
13291325
'javelin-behavior',
13301326
'javelin-dom',
@@ -1861,6 +1857,11 @@
18611857
'javelin-request',
18621858
'javelin-router',
18631859
),
1860+
'ad258e28' => array(
1861+
'javelin-behavior',
1862+
'javelin-dom',
1863+
'javelin-chart',
1864+
),
18641865
'ad486db3' => array(
18651866
'javelin-install',
18661867
'javelin-typeahead',
@@ -2179,6 +2180,10 @@
21792180
'fa74cc35' => array(
21802181
'phui-oi-list-view-css',
21812182
),
2183+
'fcb0c07d' => array(
2184+
'phui-chart-css',
2185+
'd3',
2186+
),
21822187
'fdc13e4e' => array(
21832188
'javelin-install',
21842189
),

‎src/applications/fact/application/PhabricatorFactApplication.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function getRoutes() {
3030
return array(
3131
'/fact/' => array(
3232
'' => 'PhabricatorFactHomeController',
33-
'chart/' => 'PhabricatorFactChartController',
33+
'(?<mode>chart|draw)/' => 'PhabricatorFactChartController',
3434
'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController',
3535
),
3636
);

‎src/applications/fact/controller/PhabricatorFactChartController.php

+43-24
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
55
public function handleRequest(AphrontRequest $request) {
66
$viewer = $request->getViewer();
77

8+
// When drawing a chart, we send down a placeholder piece of HTML first,
9+
// then fetch the data via async request. Determine if we're drawing
10+
// the structure or actually pulling the data.
11+
$mode = $request->getURIData('mode');
12+
$is_chart_mode = ($mode === 'chart');
13+
$is_draw_mode = ($mode === 'draw');
14+
815
$series = $request->getStr('y1');
916

1017
$facts = PhabricatorFact::getAllFacts();
@@ -20,6 +27,10 @@ public function handleRequest(AphrontRequest $request) {
2027
return new Aphront404Response();
2128
}
2229

30+
if ($is_chart_mode) {
31+
return $this->newChartResponse();
32+
}
33+
2334
$table = $fact->newDatapoint();
2435
$conn_r = $table->establishConnection('r');
2536
$table_name = $table->getTableName();
@@ -63,34 +74,20 @@ public function handleRequest(AphrontRequest $request) {
6374
'color' => '#ff0000',
6475
);
6576

66-
6777
// Add a dummy "y = x" dataset to prove we can draw multiple datasets.
6878
$x_min = min(array_keys($points));
6979
$x_max = max(array_keys($points));
7080
$x_range = ($x_max - $x_min) / 4;
7181
$linear = array();
7282
foreach ($points as $x => $y) {
73-
$linear[$x] = count($points) * (($x - $x_min) / $x_range);
83+
$linear[$x] = round(count($points) * (($x - $x_min) / $x_range));
7484
}
7585
$datasets[] = array(
7686
'x' => array_keys($linear),
7787
'y' => array_values($linear),
7888
'color' => '#0000ff',
7989
);
8090

81-
82-
$id = celerity_generate_unique_node_id();
83-
$chart = phutil_tag(
84-
'div',
85-
array(
86-
'id' => $id,
87-
'style' => 'background: #ffffff; '.
88-
'height: 480px; ',
89-
),
90-
'');
91-
92-
require_celerity_resource('d3');
93-
9491
$y_min = 0;
9592
$y_max = 0;
9693
$x_min = null;
@@ -112,21 +109,43 @@ public function handleRequest(AphrontRequest $request) {
112109
$x_max = max($x_max, max($dataset['x']));
113110
}
114111

112+
$chart_data = array(
113+
'datasets' => $datasets,
114+
'xMin' => $x_min,
115+
'xMax' => $x_max,
116+
'yMin' => $y_min,
117+
'yMax' => $y_max,
118+
);
119+
120+
return id(new AphrontAjaxResponse())->setContent($chart_data);
121+
}
122+
123+
private function newChartResponse() {
124+
$request = $this->getRequest();
125+
$chart_node_id = celerity_generate_unique_node_id();
126+
127+
$chart_view = phutil_tag(
128+
'div',
129+
array(
130+
'id' => $chart_node_id,
131+
'style' => 'background: #ffffff; '.
132+
'height: 480px; ',
133+
),
134+
'');
135+
136+
$data_uri = $request->getRequestURI();
137+
$data_uri->setPath('/fact/draw/');
138+
115139
Javelin::initBehavior(
116140
'line-chart',
117141
array(
118-
'hardpoint' => $id,
119-
'datasets' => $datasets,
120-
'xMin' => $x_min,
121-
'xMax' => $x_max,
122-
'yMin' => $y_min,
123-
'yMax' => $y_max,
124-
'xformat' => 'epoch',
142+
'chartNodeID' => $chart_node_id,
143+
'dataURI' => (string)$data_uri,
125144
));
126145

127146
$box = id(new PHUIObjectBoxView())
128-
->setHeaderText(pht('Count of %s', $fact->getName()))
129-
->appendChild($chart);
147+
->setHeaderText(pht('Chart'))
148+
->appendChild($chart_view);
130149

131150
$crumbs = $this->buildApplicationCrumbs()
132151
->addTextCrumb(pht('Chart'))
+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* @provides javelin-chart
3+
* @requires phui-chart-css
4+
* d3
5+
*/
6+
JX.install('Chart', {
7+
8+
construct: function(root_node) {
9+
this._rootNode = root_node;
10+
11+
JX.Stratcom.listen('resize', null, JX.bind(this, this._redraw));
12+
},
13+
14+
members: {
15+
_rootNode: null,
16+
_data: null,
17+
18+
setData: function(blob) {
19+
this._data = blob;
20+
this._redraw();
21+
},
22+
23+
_redraw: function() {
24+
if (!this._data) {
25+
return;
26+
}
27+
28+
var hardpoint = this._rootNode;
29+
var viewport = JX.Vector.getDim(hardpoint);
30+
var config = this._data;
31+
32+
function css_function(n) {
33+
return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')';
34+
}
35+
36+
var padding = {
37+
top: 24,
38+
left: 48,
39+
bottom: 48,
40+
right: 32
41+
};
42+
43+
var size = {
44+
frameWidth: viewport.x,
45+
frameHeight: viewport.y,
46+
};
47+
48+
size.width = size.frameWidth - padding.left - padding.right;
49+
size.height = size.frameHeight - padding.top - padding.bottom;
50+
51+
var x = d3.time.scale()
52+
.range([0, size.width]);
53+
54+
var y = d3.scale.linear()
55+
.range([size.height, 0]);
56+
57+
var xAxis = d3.svg.axis()
58+
.scale(x)
59+
.orient('bottom');
60+
61+
var yAxis = d3.svg.axis()
62+
.scale(y)
63+
.orient('left');
64+
65+
// Remove the old chart (if one exists) before drawing the new chart.
66+
JX.DOM.setContent(hardpoint, []);
67+
68+
var svg = d3.select('#' + hardpoint.id).append('svg')
69+
.attr('width', size.frameWidth)
70+
.attr('height', size.frameHeight)
71+
.attr('class', 'chart');
72+
73+
var g = svg.append('g')
74+
.attr(
75+
'transform',
76+
css_function('translate', padding.left, padding.top));
77+
78+
g.append('rect')
79+
.attr('class', 'inner')
80+
.attr('width', size.width)
81+
.attr('height', size.height);
82+
83+
function as_date(value) {
84+
return new Date(value * 1000);
85+
}
86+
87+
x.domain([as_date(config.xMin), as_date(config.xMax)]);
88+
y.domain([config.yMin, config.yMax]);
89+
90+
var div = d3.select('body')
91+
.append('div')
92+
.attr('class', 'chart-tooltip')
93+
.style('opacity', 0);
94+
95+
for (var idx = 0; idx < config.datasets.length; idx++) {
96+
var dataset = config.datasets[idx];
97+
98+
var line = d3.svg.line()
99+
.x(function(d) { return x(d.xvalue); })
100+
.y(function(d) { return y(d.yvalue); });
101+
102+
var data = [];
103+
for (var ii = 0; ii < dataset.x.length; ii++) {
104+
data.push(
105+
{
106+
xvalue: as_date(dataset.x[ii]),
107+
yvalue: dataset.y[ii]
108+
});
109+
}
110+
111+
g.append('path')
112+
.datum(data)
113+
.attr('class', 'line')
114+
.style('stroke', dataset.color)
115+
.attr('d', line);
116+
117+
g.selectAll('dot')
118+
.data(data)
119+
.enter()
120+
.append('circle')
121+
.attr('class', 'point')
122+
.attr('r', 3)
123+
.attr('cx', function(d) { return x(d.xvalue); })
124+
.attr('cy', function(d) { return y(d.yvalue); })
125+
.on('mouseover', function(d) {
126+
var d_y = d.xvalue.getFullYear();
127+
128+
// NOTE: Javascript months are zero-based. See PHI1017.
129+
var d_m = d.xvalue.getMonth() + 1;
130+
131+
var d_d = d.xvalue.getDate();
132+
133+
div
134+
.html(d_y + '-' + d_m + '-' + d_d + ': ' + d.yvalue)
135+
.style('opacity', 0.9)
136+
.style('left', (d3.event.pageX - 60) + 'px')
137+
.style('top', (d3.event.pageY - 38) + 'px');
138+
})
139+
.on('mouseout', function() {
140+
div.style('opacity', 0);
141+
});
142+
}
143+
144+
g.append('g')
145+
.attr('class', 'x axis')
146+
.attr('transform', css_function('translate', 0, size.height))
147+
.call(xAxis);
148+
149+
g.append('g')
150+
.attr('class', 'y axis')
151+
.attr('transform', css_function('translate', 0, 0))
152+
.call(yAxis);
153+
}
154+
}
155+
156+
});

0 commit comments

Comments
 (0)
Failed to load comments.