1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 06:42:42 +01:00

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
This commit is contained in:
epriestley 2012-07-30 10:44:08 -07:00
parent f0af273165
commit fceabd42e8
11 changed files with 326 additions and 86 deletions

View file

@ -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',

View file

@ -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',

View file

@ -464,10 +464,7 @@ class AphrontDefaultApplicationConfiguration
'/emailverify/(?P<code>[^/]+)/' =>
'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);

View file

@ -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 )--------------------------------------------------- */

View file

@ -0,0 +1,30 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorApplicationFact extends PhabricatorApplication {
public function getRoutes() {
return array(
'/fact/' => array(
'' => 'PhabricatorFactHomeController',
'chart/' => 'PhabricatorFactChartController',
),
);
}
}

View file

@ -0,0 +1,92 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorFactChartController extends PhabricatorFactController {
public function processRequest() {
$request = $this->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',
));
}
}

View file

@ -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(
'<p>'.pht(
'There are no facts that can be plotted yet.').'</p>');
}
$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;
}
}

View file

@ -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:');

View file

@ -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);

View file

@ -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();
});
});

View file

@ -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();
});
});