@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+220 -152
+13 -8
resources/celerity/map.php
··· 389 389 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', 390 390 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', 391 391 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b', 392 + 'rsrc/js/application/fact/Chart.js' => 'fcb0c07d', 392 393 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 393 394 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 394 395 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', ··· 397 398 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 398 399 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', 399 400 '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', 401 402 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', 402 403 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', 403 404 'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a', ··· 625 626 'javelin-behavior-icon-composer' => '38a6cedb', 626 627 'javelin-behavior-launch-icon-composer' => 'a17b84f1', 627 628 'javelin-behavior-lightbox-attachments' => 'c7e748bf', 628 - 'javelin-behavior-line-chart' => '495cf14d', 629 + 'javelin-behavior-line-chart' => 'ad258e28', 629 630 'javelin-behavior-linked-container' => '74446546', 630 631 'javelin-behavior-maniphest-batch-selector' => '139ef688', 631 632 'javelin-behavior-maniphest-list-editor' => 'c687e867', ··· 695 696 'javelin-behavior-user-menu' => '60cd9241', 696 697 'javelin-behavior-view-placeholder' => 'a9942052', 697 698 'javelin-behavior-workflow' => '9623adc1', 699 + 'javelin-chart' => 'fcb0c07d', 698 700 'javelin-color' => '78f811c9', 699 701 'javelin-cookie' => '05d290ef', 700 702 'javelin-diffusion-locate-file-source' => '94243d89', ··· 1319 1321 '490e2e2e' => array( 1320 1322 'phui-oi-list-view-css', 1321 1323 ), 1322 - '495cf14d' => array( 1323 - 'javelin-behavior', 1324 - 'javelin-dom', 1325 - 'javelin-vector', 1326 - 'phui-chart-css', 1327 - ), 1328 1324 '4a7fb02b' => array( 1329 1325 'javelin-behavior', 1330 1326 'javelin-dom', ··· 1861 1857 'javelin-request', 1862 1858 'javelin-router', 1863 1859 ), 1860 + 'ad258e28' => array( 1861 + 'javelin-behavior', 1862 + 'javelin-dom', 1863 + 'javelin-chart', 1864 + ), 1864 1865 'ad486db3' => array( 1865 1866 'javelin-install', 1866 1867 'javelin-typeahead', ··· 2178 2179 ), 2179 2180 'fa74cc35' => array( 2180 2181 'phui-oi-list-view-css', 2182 + ), 2183 + 'fcb0c07d' => array( 2184 + 'phui-chart-css', 2185 + 'd3', 2181 2186 ), 2182 2187 'fdc13e4e' => array( 2183 2188 'javelin-install',
+1 -1
src/applications/fact/application/PhabricatorFactApplication.php
··· 30 30 return array( 31 31 '/fact/' => array( 32 32 '' => 'PhabricatorFactHomeController', 33 - 'chart/' => 'PhabricatorFactChartController', 33 + '(?<mode>chart|draw)/' => 'PhabricatorFactChartController', 34 34 'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController', 35 35 ), 36 36 );
+43 -24
src/applications/fact/controller/PhabricatorFactChartController.php
··· 5 5 public function handleRequest(AphrontRequest $request) { 6 6 $viewer = $request->getViewer(); 7 7 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 + 8 15 $series = $request->getStr('y1'); 9 16 10 17 $facts = PhabricatorFact::getAllFacts(); ··· 18 25 ->newDimensionID($fact->getKey()); 19 26 if (!$key_id) { 20 27 return new Aphront404Response(); 28 + } 29 + 30 + if ($is_chart_mode) { 31 + return $this->newChartResponse(); 21 32 } 22 33 23 34 $table = $fact->newDatapoint(); ··· 63 74 'color' => '#ff0000', 64 75 ); 65 76 66 - 67 77 // Add a dummy "y = x" dataset to prove we can draw multiple datasets. 68 78 $x_min = min(array_keys($points)); 69 79 $x_max = max(array_keys($points)); 70 80 $x_range = ($x_max - $x_min) / 4; 71 81 $linear = array(); 72 82 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)); 74 84 } 75 85 $datasets[] = array( 76 86 'x' => array_keys($linear), ··· 78 88 'color' => '#0000ff', 79 89 ); 80 90 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 - 94 91 $y_min = 0; 95 92 $y_max = 0; 96 93 $x_min = null; ··· 112 109 $x_max = max($x_max, max($dataset['x'])); 113 110 } 114 111 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 + 115 139 Javelin::initBehavior( 116 140 'line-chart', 117 141 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, 125 144 )); 126 145 127 146 $box = id(new PHUIObjectBoxView()) 128 - ->setHeaderText(pht('Count of %s', $fact->getName())) 129 - ->appendChild($chart); 147 + ->setHeaderText(pht('Chart')) 148 + ->appendChild($chart_view); 130 149 131 150 $crumbs = $this->buildApplicationCrumbs() 132 151 ->addTextCrumb(pht('Chart'))
+156
webroot/rsrc/js/application/fact/Chart.js
··· 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 + });
+7 -119
webroot/rsrc/js/application/maniphest/behavior-line-chart.js
··· 2 2 * @provides javelin-behavior-line-chart 3 3 * @requires javelin-behavior 4 4 * javelin-dom 5 - * javelin-vector 6 - * phui-chart-css 5 + * javelin-chart 7 6 */ 8 7 9 8 JX.behavior('line-chart', function(config) { 10 - 11 - function css_function(n) { 12 - return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')'; 13 - } 14 - 15 - var h = JX.$(config.hardpoint); 16 - var d = JX.Vector.getDim(h); 17 - 18 - var padding = { 19 - top: 24, 20 - left: 48, 21 - bottom: 48, 22 - right: 32 23 - }; 24 - 25 - var size = { 26 - frameWidth: d.x, 27 - frameHeight: d.y, 28 - }; 29 - 30 - size.width = size.frameWidth - padding.left - padding.right; 31 - size.height = size.frameHeight - padding.top - padding.bottom; 32 - 33 - var x = d3.time.scale() 34 - .range([0, size.width]); 35 - 36 - var y = d3.scale.linear() 37 - .range([size.height, 0]); 38 - 39 - var xAxis = d3.svg.axis() 40 - .scale(x) 41 - .orient('bottom'); 42 - 43 - var yAxis = d3.svg.axis() 44 - .scale(y) 45 - .orient('left'); 46 - 47 - var svg = d3.select('#' + config.hardpoint).append('svg') 48 - .attr('width', size.frameWidth) 49 - .attr('height', size.frameHeight) 50 - .attr('class', 'chart'); 51 - 52 - var g = svg.append('g') 53 - .attr('transform', css_function('translate', padding.left, padding.top)); 54 - 55 - g.append('rect') 56 - .attr('class', 'inner') 57 - .attr('width', size.width) 58 - .attr('height', size.height); 59 - 60 - function as_date(value) { 61 - return new Date(value * 1000); 62 - } 63 - 64 - x.domain([as_date(config.xMin), as_date(config.xMax)]); 65 - y.domain([config.yMin, config.yMax]); 66 - 67 - for (var idx = 0; idx < config.datasets.length; idx++) { 68 - var dataset = config.datasets[idx]; 69 - 70 - var line = d3.svg.line() 71 - .x(function(d) { return x(d.xvalue); }) 72 - .y(function(d) { return y(d.yvalue); }); 73 - 74 - var data = []; 75 - for (var ii = 0; ii < dataset.x.length; ii++) { 76 - data.push( 77 - { 78 - xvalue: as_date(dataset.x[ii]), 79 - yvalue: dataset.y[ii] 80 - }); 81 - } 82 - 83 - g.append('path') 84 - .datum(data) 85 - .attr('class', 'line') 86 - .style('stroke', dataset.color) 87 - .attr('d', line); 9 + var chart_node = JX.$(config.chartNodeID); 88 10 89 - g.selectAll('dot') 90 - .data(data) 91 - .enter() 92 - .append('circle') 93 - .attr('class', 'point') 94 - .attr('r', 3) 95 - .attr('cx', function(d) { return x(d.xvalue); }) 96 - .attr('cy', function(d) { return y(d.yvalue); }) 97 - .on('mouseover', function(d) { 98 - var d_y = d.xvalue.getFullYear(); 11 + var chart = new JX.Chart(chart_node); 99 12 100 - // NOTE: Javascript months are zero-based. See PHI1017. 101 - var d_m = d.xvalue.getMonth() + 1; 102 - 103 - var d_d = d.xvalue.getDate(); 104 - 105 - div 106 - .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.yvalue) 107 - .style('opacity', 0.9) 108 - .style('left', (d3.event.pageX - 60) + 'px') 109 - .style('top', (d3.event.pageY - 38) + 'px'); 110 - }) 111 - .on('mouseout', function() { 112 - div.style('opacity', 0); 113 - }); 13 + function onresponse(r) { 14 + chart.setData(r); 114 15 } 115 16 116 - g.append('g') 117 - .attr('class', 'x axis') 118 - .attr('transform', css_function('translate', 0, size.height)) 119 - .call(xAxis); 120 - 121 - g.append('g') 122 - .attr('class', 'y axis') 123 - .attr('transform', css_function('translate', 0, 0)) 124 - .call(yAxis); 125 - 126 - var div = d3.select('body') 127 - .append('div') 128 - .attr('class', 'chart-tooltip') 129 - .style('opacity', 0); 130 - 17 + new JX.Request(config.dataURI, onresponse) 18 + .send(); 131 19 });