diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -141,7 +141,7 @@ '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 @@ '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 @@ '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 @@ '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 @@ 'javelin-workflow', 'phabricator-draggable-list', ), - 'a3516cea' => array( - 'phui-chart-css', - 'd3', - ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1935,6 +1933,11 @@ '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 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -94,10 +94,8 @@ '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 --- 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 --- 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 @@ members: { _rootNode: null, _data: null, + _chartContainerNode: null, + _curtain: null, setData: function(blob) { this._data = blob; @@ -26,23 +29,42 @@ } 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 @@ 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 @@ .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 @@ .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 @@ 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 --- /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); + } + + } + +});