diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -397,7 +397,7 @@ 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', - 'rsrc/js/application/maniphest/behavior-line-chart.js' => '11167911', + 'rsrc/js/application/maniphest/behavior-line-chart.js' => '495cf14d', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', 'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a', @@ -625,7 +625,7 @@ 'javelin-behavior-icon-composer' => '38a6cedb', 'javelin-behavior-launch-icon-composer' => 'a17b84f1', 'javelin-behavior-lightbox-attachments' => 'c7e748bf', - 'javelin-behavior-line-chart' => '11167911', + 'javelin-behavior-line-chart' => '495cf14d', 'javelin-behavior-linked-container' => '74446546', 'javelin-behavior-maniphest-batch-selector' => '139ef688', 'javelin-behavior-maniphest-list-editor' => 'c687e867', @@ -1007,12 +1007,6 @@ 'javelin-workflow', 'phuix-icon-view', ), - 11167911 => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-vector', - 'phui-chart-css', - ), '111bfd2d' => array( 'javelin-install', ), @@ -1325,6 +1319,12 @@ '490e2e2e' => array( 'phui-oi-list-view-css', ), + '495cf14d' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-vector', + 'phui-chart-css', + ), '4a7fb02b' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -55,8 +55,29 @@ } } - $x = array_keys($points); - $y = array_values($points); + $datasets = array(); + + $datasets[] = array( + 'x' => array_keys($points), + 'y' => array_values($points), + 'color' => '#ff0000', + ); + + + // Add a dummy "y = x" dataset to prove we can draw multiple datasets. + $x_min = min(array_keys($points)); + $x_max = max(array_keys($points)); + $x_range = ($x_max - $x_min) / 4; + $linear = array(); + foreach ($points as $x => $y) { + $linear[$x] = count($points) * (($x - $x_min) / $x_range); + } + $datasets[] = array( + 'x' => array_keys($linear), + 'y' => array_values($linear), + 'color' => '#0000ff', + ); + $id = celerity_generate_unique_node_id(); $chart = phutil_tag( @@ -70,15 +91,38 @@ require_celerity_resource('d3'); - Javelin::initBehavior('line-chart', array( - 'hardpoint' => $id, - 'x' => array($x), - 'y' => array($y), - 'yMax' => max(0, max($y)), - 'yMin' => min(0, min($y)), - 'xformat' => 'epoch', - 'colors' => array('#0000ff'), - )); + $y_min = 0; + $y_max = 0; + $x_min = null; + $x_max = 0; + foreach ($datasets as $dataset) { + if (!$dataset['y']) { + continue; + } + + $y_min = min($y_min, min($dataset['y'])); + $y_max = max($y_max, max($dataset['y'])); + + if ($x_min === null) { + $x_min = min($dataset['x']); + } else { + $x_min = min($x_min, min($dataset['x'])); + } + + $x_max = max($x_max, max($dataset['x'])); + } + + Javelin::initBehavior( + 'line-chart', + array( + 'hardpoint' => $id, + 'datasets' => $datasets, + 'xMin' => $x_min, + 'xMax' => $x_max, + 'yMin' => $y_min, + 'yMax' => $y_max, + 'xformat' => 'epoch', + )); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Count of %s', $fact->getName())) diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js --- a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js +++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js @@ -57,28 +57,61 @@ .attr('width', size.width) .attr('height', size.height); - var line = d3.svg.line() - .x(function(d) { return x(d.date); }) - .y(function(d) { return y(d.count); }); - - var data = []; - for (var ii = 0; ii < config.x[0].length; ii++) { - data.push( - { - date: new Date(config.x[0][ii] * 1000), - count: +config.y[0][ii] - }); + function as_date(value) { + return new Date(value * 1000); } - x.domain(d3.extent(data, function(d) { return d.date; })); - - var yex = d3.extent(data, function(d) { return d.count; }); + x.domain([as_date(config.xMin), as_date(config.xMax)]); y.domain([config.yMin, config.yMax]); - g.append('path') - .datum(data) - .attr('class', 'line') - .attr('d', line); + for (var idx = 0; idx < config.datasets.length; idx++) { + var dataset = config.datasets[idx]; + + var line = d3.svg.line() + .x(function(d) { return x(d.xvalue); }) + .y(function(d) { return y(d.yvalue); }); + + var data = []; + for (var ii = 0; ii < dataset.x.length; ii++) { + data.push( + { + xvalue: as_date(dataset.x[ii]), + yvalue: dataset.y[ii] + }); + } + + g.append('path') + .datum(data) + .attr('class', 'line') + .style('stroke', dataset.color) + .attr('d', line); + + g.selectAll('dot') + .data(data) + .enter() + .append('circle') + .attr('class', 'point') + .attr('r', 3) + .attr('cx', function(d) { return x(d.xvalue); }) + .attr('cy', function(d) { return y(d.yvalue); }) + .on('mouseover', function(d) { + var d_y = d.xvalue.getFullYear(); + + // NOTE: Javascript months are zero-based. See PHI1017. + var d_m = d.xvalue.getMonth() + 1; + + var d_d = d.xvalue.getDate(); + + div + .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.yvalue) + .style('opacity', 0.9) + .style('left', (d3.event.pageX - 60) + 'px') + .style('top', (d3.event.pageY - 38) + 'px'); + }) + .on('mouseout', function() { + div.style('opacity', 0); + }); + } g.append('g') .attr('class', 'x axis') @@ -95,30 +128,4 @@ .attr('class', 'chart-tooltip') .style('opacity', 0); - g.selectAll('dot') - .data(data) - .enter() - .append('circle') - .attr('class', 'point') - .attr('r', 3) - .attr('cx', function(d) { return x(d.date); }) - .attr('cy', function(d) { return y(d.count); }) - .on('mouseover', function(d) { - var d_y = d.date.getFullYear(); - - // NOTE: Javascript months are zero-based. See PHI1017. - var d_m = d.date.getMonth() + 1; - - var d_d = d.date.getDate(); - - div - .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.count) - .style('opacity', 0.9) - .style('left', (d3.event.pageX - 60) + 'px') - .style('top', (d3.event.pageY - 38) + 'px'); - }) - .on('mouseout', function() { - div.style('opacity', 0); - }); - });