From c6052b41a604c07b437f8036ee194faa566dc953 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 06:50:23 -0700 Subject: [PATCH] Label important data on charts Summary: Ref T13279. Adds client-side support for rendering function labels on charts, then labels every function as important data. Works okay on mobile, although I'm not planning to target mobile terribly heavily for v0. Test Plan: {F6438860} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20500 --- resources/celerity/map.php | 19 +++-- .../PhabricatorChartRenderingEngine.php | 6 +- webroot/rsrc/css/phui/phui-chart.css | 41 +++++++++ webroot/rsrc/js/application/fact/Chart.js | 79 +++++++++++++---- .../js/application/fact/ChartCurtainView.js | 85 +++++++++++++++++++ 5 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 webroot/rsrc/js/application/fact/ChartCurtainView.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index a2bc989ea6..c344701d4a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -141,7 +141,7 @@ return array( 'rsrc/css/phui/phui-big-info-view.css' => '362ad37b', 'rsrc/css/phui/phui-box.css' => '5ed3b8cb', 'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30', - 'rsrc/css/phui/phui-chart.css' => '7853a69b', + 'rsrc/css/phui/phui-chart.css' => '10135a9d', 'rsrc/css/phui/phui-cms.css' => '8c05c41e', 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', @@ -389,7 +389,8 @@ return array( 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b', - 'rsrc/js/application/fact/Chart.js' => 'a3516cea', + 'rsrc/js/application/fact/Chart.js' => 'b88a227d', + 'rsrc/js/application/fact/ChartCurtainView.js' => 'd10a3c25', 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', @@ -696,7 +697,8 @@ return array( 'javelin-behavior-user-menu' => '60cd9241', 'javelin-behavior-view-placeholder' => 'a9942052', 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => 'a3516cea', + 'javelin-chart' => 'b88a227d', + 'javelin-chart-curtain-view' => 'd10a3c25', 'javelin-color' => '78f811c9', 'javelin-cookie' => '05d290ef', 'javelin-diffusion-locate-file-source' => '94243d89', @@ -823,7 +825,7 @@ return array( 'phui-calendar-day-css' => '9597d706', 'phui-calendar-list-css' => 'ccd7e4e2', 'phui-calendar-month-css' => 'cb758c42', - 'phui-chart-css' => '7853a69b', + 'phui-chart-css' => '10135a9d', 'phui-cms-css' => '8c05c41e', 'phui-comment-form-css' => '68a2d99a', 'phui-comment-panel-css' => 'ec4e31c0', @@ -1767,10 +1769,6 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', ), - 'a3516cea' => array( - 'phui-chart-css', - 'd3', - ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1935,6 +1933,11 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), + 'b88a227d' => array( + 'phui-chart-css', + 'd3', + 'javelin-chart-curtain-view', + ), 'b9109f8f' => array( 'javelin-behavior', 'javelin-uri', diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index 1b77d2403f..b89e8da861 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -94,10 +94,8 @@ final class PhabricatorChartRenderingEngine 'div', array( 'id' => $chart_node_id, - 'style' => 'background: #ffffff; '. - 'height: 480px; ', - ), - ''); + 'class' => 'chart-hardpoint', + )); $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); diff --git a/webroot/rsrc/css/phui/phui-chart.css b/webroot/rsrc/css/phui/phui-chart.css index be401b1fe3..350d86014a 100644 --- a/webroot/rsrc/css/phui/phui-chart.css +++ b/webroot/rsrc/css/phui/phui-chart.css @@ -10,6 +10,7 @@ } .chart .axis text { + font: {$basefont}; fill: {$darkgreytext}; } @@ -52,3 +53,43 @@ border-radius: 8px; pointer-events: none; } + +.chart-hardpoint { + min-height: 480px; + overflow: hidden; + position: relative; +} + +.device-desktop .chart-container { + position: absolute; + bottom: 0; + top: 0; + left: 0; + right: 300px; +} + +.device .chart-container { + min-height: 480px; +} + +.device-desktop .chart-curtain { + width: 300px; + position: absolute; + bottom: 0; + top: 0; + right: 0; +} + +.chart-function-label-list { + background: {$lightbluebackground}; + border: 1px solid {$lightblueborder}; + padding: 8px 12px; +} + +.device-desktop .chart-function-label-list { + margin-top: 23px; +} + +.chart-function-label-list-item .phui-icon-view { + margin-right: 8px; +} diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index 66a9b98d70..25703e7521 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -2,6 +2,7 @@ * @provides javelin-chart * @requires phui-chart-css * d3 + * javelin-chart-curtain-view */ JX.install('Chart', { @@ -14,6 +15,8 @@ JX.install('Chart', { members: { _rootNode: null, _data: null, + _chartContainerNode: null, + _curtain: null, setData: function(blob) { this._data = blob; @@ -26,23 +29,42 @@ JX.install('Chart', { } var hardpoint = this._rootNode; + var curtain = this._getCurtain(); + var container_node = this._getChartContainerNode(); + + var content = [ + container_node, + curtain.getNode(), + ]; + + JX.DOM.setContent(hardpoint, content); // Remove the old chart (if one exists) before drawing the new chart. - JX.DOM.setContent(hardpoint, []); + JX.DOM.setContent(container_node, []); - var viewport = JX.Vector.getDim(hardpoint); + var viewport = JX.Vector.getDim(container_node); var config = this._data; function css_function(n) { return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')'; } - var padding = { - top: 24, - left: 48, - bottom: 48, - right: 32 - }; + 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 + }; + } var size = { frameWidth: viewport.x, @@ -61,20 +83,20 @@ JX.install('Chart', { var xAxis = d3.axisBottom(x); var yAxis = d3.axisLeft(y); - var svg = d3.select('#' + hardpoint.id).append('svg') + var svg = d3.select(container_node).append('svg') .attr('width', size.frameWidth) .attr('height', size.frameHeight) .attr('class', 'chart'); var g = svg.append('g') - .attr( - 'transform', - css_function('translate', padding.left, padding.top)); + .attr( + 'transform', + css_function('translate', padding.left, padding.top)); g.append('rect') - .attr('class', 'inner') - .attr('width', size.width) - .attr('height', size.height); + .attr('class', 'inner') + .attr('width', size.width) + .attr('height', size.height); x.domain([this._newDate(config.xMin), this._newDate(config.xMax)]); y.domain([config.yMin, config.yMax]); @@ -84,16 +106,20 @@ JX.install('Chart', { .attr('class', 'chart-tooltip') .style('opacity', 0); + curtain.reset(); + for (var idx = 0; idx < config.datasets.length; idx++) { var dataset = config.datasets[idx]; switch (dataset.type) { case 'stacked-area': - this._newStackedArea(g, dataset, x, y, div); + this._newStackedArea(g, dataset, x, y, div, curtain); break; } } + curtain.redraw(); + g.append('g') .attr('class', 'x axis') .attr('transform', css_function('translate', 0, size.height)) @@ -105,7 +131,7 @@ JX.install('Chart', { .call(yAxis); }, - _newStackedArea: function(g, dataset, x, y, div) { + _newStackedArea: function(g, dataset, x, y, div, curtain) { var to_date = JX.bind(this, this._newDate); var area = d3.area() @@ -155,11 +181,30 @@ JX.install('Chart', { div.style('opacity', 0); }); + curtain.addFunctionLabel('Important Data'); } }, _newDate: function(epoch) { return new Date(epoch * 1000); + }, + + _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; } } diff --git a/webroot/rsrc/js/application/fact/ChartCurtainView.js b/webroot/rsrc/js/application/fact/ChartCurtainView.js new file mode 100644 index 0000000000..07e5af930e --- /dev/null +++ b/webroot/rsrc/js/application/fact/ChartCurtainView.js @@ -0,0 +1,85 @@ +/** + * @provides javelin-chart-curtain-view + */ +JX.install('ChartCurtainView', { + + construct: function() { + this._labels = []; + }, + + members: { + _node: null, + _labels: null, + _labelsNode: null, + + getNode: function() { + if (!this._node) { + var attr = { + className: 'chart-curtain' + }; + + this._node = JX.$N('div', attr); + } + return this._node; + }, + + reset: function() { + this._labels = []; + }, + + addFunctionLabel: function(label) { + this._labels.push(label); + return this; + }, + + redraw: function() { + var content = [this._getFunctionLabelsNode()]; + + JX.DOM.setContent(this.getNode(), content); + return this; + }, + + _getFunctionLabelsNode: function() { + if (!this._labels.length) { + return null; + } + + if (!this._labelsNode) { + var list_attrs = { + className: 'chart-function-label-list' + }; + + var labels = JX.$N('ul', list_attrs); + + var items = []; + for (var ii = 0; ii < this._labels.length; ii++) { + items.push(this._newFunctionLabelItem(this._labels[ii])); + } + + JX.DOM.setContent(labels, items); + + this._labelsNode = labels; + } + + return this._labelsNode; + }, + + _newFunctionLabelItem: function(item) { + var item_attrs = { + className: 'chart-function-label-list-item' + }; + + var icon = new JX.PHUIXIconView() + .setIcon('fa-circle'); + + var content = [ + icon.getNode(), + item + ]; + + return JX.$N('li', item_attrs, content); + } + + } + +});