From fceabd42e88b22d0aa38785a6b04db78086158c6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 30 Jul 2012 10:44:08 -0700 Subject: [PATCH] Allow Fact app to draw charts Summary: For any count fact, allow a chart to be drawn. INCREDIBLY POWERFUL DATA ANALYSIS PLATFORM. Test Plan: Drew a chart of object counts. Drew the Maniphest burn chart. Reviewers: vrana, btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T1562 Differential Revision: https://secure.phabricator.com/D3099 --- src/__celerity_resource_map__.php | 12 +++ src/__phutil_library_map__.php | 4 + ...AphrontDefaultApplicationConfiguration.php | 14 ++- .../base/PhabricatorApplication.php | 9 ++ .../PhabricatorApplicationFact.php | 30 ++++++ .../PhabricatorFactChartController.php | 92 +++++++++++++++++++ .../PhabricatorFactHomeController.php | 65 ++++++++++++- .../engine/PhabricatorFactCountEngine.php | 15 ++- .../controller/ManiphestReportController.php | 3 +- .../maniphest/behavior-burn-chart.js | 79 ---------------- .../maniphest/behavior-line-chart.js | 89 ++++++++++++++++++ 11 files changed, 326 insertions(+), 86 deletions(-) create mode 100644 src/applications/fact/application/PhabricatorApplicationFact.php create mode 100644 src/applications/fact/controller/PhabricatorFactChartController.php delete mode 100644 webroot/rsrc/js/application/maniphest/behavior-burn-chart.js create mode 100644 webroot/rsrc/js/application/maniphest/behavior-line-chart.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index d81b865a09..ee146d88a5 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1219,6 +1219,18 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/herald/herald-rule-editor.js', ), + 'javelin-behavior-line-chart' => + array( + 'uri' => '/res/653743c8/rsrc/js/application/maniphest/behavior-line-chart.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + ), + 'disk' => '/rsrc/js/application/maniphest/behavior-line-chart.js', + ), 'javelin-behavior-maniphest-batch-editor' => array( 'uri' => '/res/d22661be/rsrc/js/application/maniphest/behavior-batch-editor.js', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4fe9c3383a..3b8cc94eb4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -532,6 +532,7 @@ phutil_register_library_map(array( 'PhabricatorAccessLog' => 'infrastructure/PhabricatorAccessLog.php', 'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php', 'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php', + 'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php', 'PhabricatorApplicationManiphest' => 'applications/maniphest/application/PhabricatorApplicationManiphest.php', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditAddCommentController' => 'applications/audit/controller/PhabricatorAuditAddCommentController.php', @@ -628,6 +629,7 @@ phutil_register_library_map(array( 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php', + 'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php', 'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php', 'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php', 'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php', @@ -1585,6 +1587,7 @@ phutil_register_library_map(array( 'PackageModifyMail' => 'PackageMail', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorApplicationDifferential' => 'PhabricatorApplication', + 'PhabricatorApplicationFact' => 'PhabricatorApplication', 'PhabricatorApplicationManiphest' => 'PhabricatorApplication', 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', 'PhabricatorAuditComment' => 'PhabricatorAuditDAO', @@ -1674,6 +1677,7 @@ phutil_register_library_map(array( 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhutilEventListener', 'PhabricatorFactAggregate' => 'PhabricatorFactDAO', + 'PhabricatorFactChartController' => 'PhabricatorFactController', 'PhabricatorFactController' => 'PhabricatorController', 'PhabricatorFactCountEngine' => 'PhabricatorFactEngine', 'PhabricatorFactCursor' => 'PhabricatorFactDAO', diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 9b9c4a151d..9be1048237 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -464,10 +464,7 @@ class AphrontDefaultApplicationConfiguration '/emailverify/(?P[^/]+)/' => 'PhabricatorEmailVerificationController', - '/fact/' => array( - '' => 'PhabricatorFactHomeController', - ), - ); + ) + $this->getApplicationRoutes(); } protected function getResourceURIMapRules() { @@ -481,6 +478,15 @@ class AphrontDefaultApplicationConfiguration ); } + private function getApplicationRoutes() { + $applications = PhabricatorApplication::getAllInstalledApplications(); + $routes = array(); + foreach ($applications as $application) { + $routes += $application->getRoutes(); + } + return $routes; + } + public function buildRequest() { $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($_GET + $_POST); diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 88ad71c947..cd917c3fa8 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -18,6 +18,7 @@ /** * @task info Application Information + * @task uri URI Routing * @task fact Fact Integration * @task meta Application Management * @group apps @@ -37,6 +38,14 @@ abstract class PhabricatorApplication { } +/* -( Application Information )-------------------------------------------- */ + + + public function getRoutes() { + return array(); + } + + /* -( Fact Integration )--------------------------------------------------- */ diff --git a/src/applications/fact/application/PhabricatorApplicationFact.php b/src/applications/fact/application/PhabricatorApplicationFact.php new file mode 100644 index 0000000000..aa856b4eb8 --- /dev/null +++ b/src/applications/fact/application/PhabricatorApplicationFact.php @@ -0,0 +1,30 @@ + array( + '' => 'PhabricatorFactHomeController', + 'chart/' => 'PhabricatorFactChartController', + ), + ); + } + +} diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php new file mode 100644 index 0000000000..1872e065b3 --- /dev/null +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -0,0 +1,92 @@ +getRequest(); + $user = $request->getUser(); + + $table = new PhabricatorFactRaw(); + $conn_r = $table->establishConnection('r'); + $table_name = $table->getTableName(); + + $series = $request->getStr('y1'); + + $specs = PhabricatorFactSpec::newSpecsForFactTypes( + PhabricatorFactEngine::loadAllEngines(), + array($series)); + $spec = idx($specs, $series); + + $data = queryfx_all( + $conn_r, + 'SELECT valueX, epoch FROM %T WHERE factType = %s ORDER BY epoch ASC', + $table_name, + $series); + + $points = array(); + $sum = 0; + foreach ($data as $key => $row) { + $sum += (int)$row['valueX']; + $points[(int)$row['epoch']] = $sum; + } + + if (!$points) { + // NOTE: Raphael crashes Safari if you hand it series with no points. + throw new Exception("No data to show!"); + } + + $x = array_keys($points); + $y = array_values($points); + + $id = celerity_generate_unique_node_id(); + $chart = phutil_render_tag( + 'div', + array( + 'id' => $id, + 'style' => 'border: 1px solid #6f6f6f; '. + 'margin: 1em 2em; '. + 'background: #ffffff; '. + 'height: 400px; ', + ), + ''); + + require_celerity_resource('raphael-core'); + require_celerity_resource('raphael-g'); + require_celerity_resource('raphael-g-line'); + + Javelin::initBehavior('line-chart', array( + 'hardpoint' => $id, + 'x' => array($x), + 'y' => array($y), + 'xformat' => 'epoch', + 'colors' => array('#0000ff'), + )); + + $panel = new AphrontPanelView(); + $panel->setHeader('Count of '.$spec->getName()); + $panel->appendChild($chart); + + return $this->buildStandardPageResponse( + $panel, + array( + 'title' => 'Chart', + )); + } + +} diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php index e632396b25..8060977a6b 100644 --- a/src/applications/fact/controller/PhabricatorFactHomeController.php +++ b/src/applications/fact/controller/PhabricatorFactHomeController.php @@ -22,6 +22,12 @@ final class PhabricatorFactHomeController extends PhabricatorFactController { $request = $this->getRequest(); $user = $request->getUser(); + if ($request->isFormPost()) { + $uri = new PhutilURI('/fact/chart/'); + $uri->setQueryParam('y1', $request->getStr('y1')); + return id(new AphrontRedirectResponse())->setURI($uri); + } + $types = array( '+N:*', '+N:DREV', @@ -64,11 +70,68 @@ final class PhabricatorFactHomeController extends PhabricatorFactController { $panel->setHeader('Facts!'); $panel->appendChild($table); + $chart_form = $this->buildChartForm(); + return $this->buildStandardPageResponse( - $panel, + array( + $chart_form, + $panel, + ), array( 'title' => 'Facts!', )); } + private function buildChartForm() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $table = new PhabricatorFactRaw(); + $conn_r = $table->establishConnection('r'); + $table_name = $table->getTableName(); + + $facts = queryfx_all( + $conn_r, + 'SELECT DISTINCT factType from %T', + $table_name); + + $specs = PhabricatorFactSpec::newSpecsForFactTypes( + PhabricatorFactEngine::loadAllEngines(), + ipull($facts, 'factType')); + + $options = array(); + foreach ($specs as $spec) { + if ($spec->getUnit() == PhabricatorFactSpec::UNIT_COUNT) { + $options[$spec->getType()] = $spec->getName(); + } + } + + if (!$options) { + return id(new AphrontErrorView()) + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) + ->setTitle(pht('No Chartable Facts')) + ->appendChild( + '

'.pht( + 'There are no facts that can be plotted yet.').'

'); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel('Y-Axis') + ->setName('y1') + ->setOptions($options)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Plot Chart')); + + $panel = new AphrontPanelView(); + $panel->appendChild($form); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->setHeader('Plot Chart'); + + return $panel; + } + } diff --git a/src/applications/fact/engine/PhabricatorFactCountEngine.php b/src/applications/fact/engine/PhabricatorFactCountEngine.php index 5e2c35a894..147f27f4fe 100644 --- a/src/applications/fact/engine/PhabricatorFactCountEngine.php +++ b/src/applications/fact/engine/PhabricatorFactCountEngine.php @@ -35,6 +35,18 @@ final class PhabricatorFactCountEngine extends PhabricatorFactEngine { ->setName($name) ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT); } + + if (!strncmp($type, 'N:', 2)) { + if ($type == 'N:*') { + $name = 'Objects'; + } else { + $name = 'Objects of type '.substr($type, 2); + } + $results[] = id(new PhabricatorFactSimpleSpec($type)) + ->setName($name) + ->setUnit(PhabricatorFactSimpleSpec::UNIT_COUNT); + } + } return $results; } @@ -53,6 +65,7 @@ final class PhabricatorFactCountEngine extends PhabricatorFactEngine { $facts[] = id(new PhabricatorFactRaw()) ->setFactType($fact_type) ->setObjectPHID($phid) + ->setValueX(1) ->setEpoch($object->getDateCreated()); } @@ -70,7 +83,7 @@ final class PhabricatorFactCountEngine extends PhabricatorFactEngine { $counts = queryfx_all( $conn, - 'SELECT factType, count(*) N FROM %T WHERE factType LIKE %> + 'SELECT factType, SUM(valueX) N FROM %T WHERE factType LIKE %> GROUP BY factType', $table_name, 'N:'); diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index fc8818124b..cfcb17d218 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -304,7 +304,7 @@ final class ManiphestReportController extends ManiphestController { require_celerity_resource('raphael-g'); require_celerity_resource('raphael-g-line'); - Javelin::initBehavior('burn-chart', array( + Javelin::initBehavior('line-chart', array( 'hardpoint' => $id, 'x' => array( $burn_x, @@ -312,6 +312,7 @@ final class ManiphestReportController extends ManiphestController { 'y' => array( $burn_y, ), + 'xformat' => 'epoch', )); return array($filter, $chart, $panel); diff --git a/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js b/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js deleted file mode 100644 index 2163423882..0000000000 --- a/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @provides javelin-behavior-burn-chart - * @requires javelin-behavior - * javelin-dom - * javelin-vector - */ - -JX.behavior('burn-chart', function(config) { - - - var h = JX.$(config.hardpoint); - var p = JX.$V(h); - var d = JX.Vector.getDim(h); - var mx = 60; - var my = 30; - - var r = Raphael(p.x, p.y, d.x, d.y); - - var l = r.linechart( - mx, my, - d.x - (2 * mx), d.y - (2 * my), - config.x, - config.y, - { - nostroke: false, - axis: "0 0 1 1", - shade: true, - gutter: 1, - colors: ['#d06'] - }); - - - // Convert the epoch timestamps on the X axis into readable dates. - - var n = 2; - var ii = 0; - var text = l.axis[0].text.items; - for (var k in text) { - if (ii++ % n) { - text[k].attr({text: ''}); - } else { - var cur = text[k].attr('text'); - var date = new Date(parseInt(cur, 10) * 1000); - var str = date.toLocaleDateString(); - text[k].attr({text: str}); - } - } - - l.hoverColumn(function() { - - var open = 0; - for (var ii = 0; ii < config.x[0].length; ii++) { - if (config.x[0][ii] > this.axis) { - break; - } - open = config.y[0][ii]; - } - - var date = new Date(parseInt(this.axis, 10) * 1000).toLocaleDateString(); - var total = open + " Open Tasks"; - - var tag = r.tag( - this.x, - this.y[0], - [date, total].join("\n"), - 180, - 24); - tag - .insertBefore(this) - .attr([{fill : '#fff'}, {fill: '#000'}]); - - this.tags = r.set(); - this.tags.push(tag); - }, function() { - this.tags && this.tags.remove(); - }); - -}); - diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js new file mode 100644 index 0000000000..9199828397 --- /dev/null +++ b/webroot/rsrc/js/application/maniphest/behavior-line-chart.js @@ -0,0 +1,89 @@ +/** + * @provides javelin-behavior-line-chart + * @requires javelin-behavior + * javelin-dom + * javelin-vector + */ + +JX.behavior('line-chart', function(config) { + + var h = JX.$(config.hardpoint); + var p = JX.$V(h); + var d = JX.Vector.getDim(h); + var mx = 60; + var my = 30; + + var r = Raphael(p.x, p.y, d.x, d.y); + + var l = r.linechart( + mx, my, + d.x - (2 * mx), d.y - (2 * my), + config.x, + config.y, + { + nostroke: false, + axis: "0 0 1 1", + shade: true, + gutter: 1, + colors: config.colors || ['#d06'] + }); + + function format(value, type) { + switch (type) { + case 'epoch': + return new Date(parseInt(value, 10) * 1000).toLocaleDateString(); + case 'int': + return parseInt(value, 10); + default: + return value; + } + } + + // Format the X axis. + + var n = 2; + var ii = 0; + var text = l.axis[0].text.items; + for (var k in text) { + if (ii++ % n) { + text[k].attr({text: ''}); + } else { + var cur = text[k].attr('text'); + str = format(cur, config.xformat); + text[k].attr({text: str}); + } + } + + // Show values on hover. + + l.hoverColumn(function() { + this.tags = r.set(); + for (var yy = 0; yy < config.y.length; yy++) { + var yvalue = 0; + for (var ii = 0; ii < config.x[0].length; ii++) { + if (config.x[0][ii] > this.axis) { + break; + } + yvalue = format(config.y[yy][ii], (config.yformat || [])[yy]); + } + + var xvalue = format(this.axis, config.xformat); + + var tag = r.tag( + this.x, + this.y[yy], + [xvalue, yvalue].join("\n"), + 180, + 24); + tag + .insertBefore(this) + .attr([{fill : '#fff'}, {fill: '#000'}]); + + this.tags.push(tag); + } + }, function() { + this.tags && this.tags.remove(); + }); + +}); +