mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 00:32: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:
parent
f0af273165
commit
fceabd42e8
11 changed files with 326 additions and 86 deletions
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 )--------------------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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',
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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:');
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
|
89
webroot/rsrc/js/application/maniphest/behavior-line-chart.js
Normal file
89
webroot/rsrc/js/application/maniphest/behavior-line-chart.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
||||
|
Loading…
Reference in a new issue