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

(stable) Promote 2019 Week 38

This commit is contained in:
epriestley 2019-09-23 14:07:49 -07:00
commit f0f352ffd0
32 changed files with 924 additions and 371 deletions

View file

@ -141,7 +141,7 @@ return array(
'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' => '10135a9d',
'rsrc/css/phui/phui-chart.css' => '14df9ae3',
'rsrc/css/phui/phui-cms.css' => '8c05c41e',
'rsrc/css/phui/phui-comment-form.css' => '68a2d99a',
'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0',
@ -390,7 +390,7 @@ return array(
'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' => 'eec96de0',
'rsrc/js/application/fact/Chart.js' => '52e3ff03',
'rsrc/js/application/fact/ChartCurtainView.js' => '86954222',
'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab',
'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22',
@ -699,7 +699,7 @@ return array(
'javelin-behavior-user-menu' => '60cd9241',
'javelin-behavior-view-placeholder' => 'a9942052',
'javelin-behavior-workflow' => '9623adc1',
'javelin-chart' => 'eec96de0',
'javelin-chart' => '52e3ff03',
'javelin-chart-curtain-view' => '86954222',
'javelin-chart-function-label' => '81de1dab',
'javelin-color' => '78f811c9',
@ -828,7 +828,7 @@ return array(
'phui-calendar-day-css' => '9597d706',
'phui-calendar-list-css' => 'ccd7e4e2',
'phui-calendar-month-css' => 'cb758c42',
'phui-chart-css' => '10135a9d',
'phui-chart-css' => '14df9ae3',
'phui-cms-css' => '8c05c41e',
'phui-comment-form-css' => '68a2d99a',
'phui-comment-panel-css' => 'ec4e31c0',
@ -1369,6 +1369,12 @@ return array(
'javelin-dom',
'javelin-fx',
),
'52e3ff03' => array(
'phui-chart-css',
'd3',
'javelin-chart-curtain-view',
'javelin-chart-function-label',
),
'541f81c3' => array(
'javelin-install',
),
@ -2127,12 +2133,6 @@ return array(
'phabricator-keyboard-shortcut',
'javelin-stratcom',
),
'eec96de0' => array(
'phui-chart-css',
'd3',
'javelin-chart-curtain-view',
'javelin-chart-function-label',
),
'ef836bf2' => array(
'javelin-behavior',
'javelin-dom',

View file

@ -3104,6 +3104,7 @@ phutil_register_library_map(array(
'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php',
'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php',
'PhabricatorDemoChartEngine' => 'applications/fact/engine/PhabricatorDemoChartEngine.php',
'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php',
'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php',
'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
@ -4223,6 +4224,7 @@ phutil_register_library_map(array(
'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php',
'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php',
'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php',
'PhabricatorProjectActivityChartEngine' => 'applications/project/chart/PhabricatorProjectActivityChartEngine.php',
'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php',
'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php',
'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php',
@ -4422,6 +4424,7 @@ phutil_register_library_map(array(
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php',
'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php',
'PhabricatorProtocolLog' => 'infrastructure/log/PhabricatorProtocolLog.php',
'PhabricatorPureChartFunction' => 'applications/fact/chart/PhabricatorPureChartFunction.php',
'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php',
'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php',
'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php',
@ -8300,7 +8303,7 @@ phutil_register_library_map(array(
'PhabricatorAccessLog' => 'Phobject',
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction',
'PhabricatorAccumulateChartFunction' => 'PhabricatorHigherOrderChartFunction',
'PhabricatorActionListView' => 'AphrontTagView',
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
@ -9155,7 +9158,7 @@ phutil_register_library_map(array(
'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting',
'PhabricatorConsoleApplication' => 'PhabricatorApplication',
'PhabricatorConsoleContentSource' => 'PhabricatorContentSource',
'PhabricatorConstantChartFunction' => 'PhabricatorChartFunction',
'PhabricatorConstantChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorContentSource' => 'Phobject',
'PhabricatorContentSourceModule' => 'PhabricatorConfigModule',
@ -9167,7 +9170,7 @@ phutil_register_library_map(array(
'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType',
'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType',
'PhabricatorCosChartFunction' => 'PhabricatorChartFunction',
'PhabricatorCosChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorCountFact' => 'PhabricatorFact',
'PhabricatorCountdown' => array(
'PhabricatorCountdownDAO',
@ -9433,6 +9436,7 @@ phutil_register_library_map(array(
'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle',
'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine',
'PhabricatorDemoChartEngine' => 'PhabricatorChartEngine',
'PhabricatorDestructibleCodex' => 'Phobject',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDestructionEngineExtension' => 'Phobject',
@ -10068,7 +10072,7 @@ phutil_register_library_map(array(
'PhabricatorMarkupInterface',
),
'PhabricatorMarkupPreviewController' => 'PhabricatorController',
'PhabricatorMaxChartFunction' => 'PhabricatorChartFunction',
'PhabricatorMaxChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorMemeEngine' => 'Phobject',
'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule',
'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule',
@ -10135,7 +10139,7 @@ phutil_register_library_map(array(
'PhabricatorMetronome' => 'Phobject',
'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase',
'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock',
'PhabricatorMinChartFunction' => 'PhabricatorChartFunction',
'PhabricatorMinChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorModularTransactionType' => 'Phobject',
'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension',
@ -10730,6 +10734,7 @@ phutil_register_library_map(array(
'PhabricatorSpacesInterface',
'PhabricatorEditEngineSubtypeInterface',
),
'PhabricatorProjectActivityChartEngine' => 'PhabricatorChartEngine',
'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction',
'PhabricatorProjectApplication' => 'PhabricatorApplication',
'PhabricatorProjectArchiveController' => 'PhabricatorProjectController',
@ -10950,6 +10955,7 @@ phutil_register_library_map(array(
'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment',
'PhabricatorPronounSetting' => 'PhabricatorSelectSetting',
'PhabricatorProtocolLog' => 'Phobject',
'PhabricatorPureChartFunction' => 'PhabricatorChartFunction',
'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorQuery' => 'Phobject',
'PhabricatorQueryConstraint' => 'Phobject',
@ -11208,7 +11214,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface',
),
'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorScaleChartFunction' => 'PhabricatorChartFunction',
'PhabricatorScaleChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorScopedEnv' => 'Phobject',
'PhabricatorSearchAbstractDocument' => 'Phobject',
@ -11299,12 +11305,12 @@ phutil_register_library_map(array(
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction',
'PhabricatorShiftChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorShortSite' => 'PhabricatorSite',
'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting',
'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType',
'PhabricatorSimpleEditType' => 'PhabricatorEditType',
'PhabricatorSinChartFunction' => 'PhabricatorChartFunction',
'PhabricatorSinChartFunction' => 'PhabricatorPureChartFunction',
'PhabricatorSite' => 'AphrontSite',
'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorSlowvoteApplication' => 'PhabricatorApplication',

View file

@ -22,15 +22,10 @@ final class PhabricatorFactApplication extends PhabricatorApplication {
return self::GROUP_UTILITIES;
}
public function isPrototype() {
return true;
}
public function getRoutes() {
return array(
'/fact/' => array(
'' => 'PhabricatorFactHomeController',
'chart/' => 'PhabricatorFactChartController',
'chart/(?P<chartKey>[^/]+)/(?:(?P<mode>draw)/)?' =>
'PhabricatorFactChartController',
'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController',

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorAccumulateChartFunction
extends PhabricatorChartFunction {
extends PhabricatorHigherOrderChartFunction {
const FUNCTIONKEY = 'accumulate';
@ -13,14 +13,6 @@ final class PhabricatorAccumulateChartFunction
);
}
public function getDomain() {
return $this->getArgument('x')->getDomain();
}
public function newInputValues(PhabricatorChartDataQuery $query) {
return $this->getArgument('x')->newInputValues($query);
}
public function evaluateFunction(array $xv) {
// First, we're going to accumulate the underlying function. Then
// we'll map the inputs through the accumulation.

View file

@ -59,13 +59,6 @@ abstract class PhabricatorChartDataset
return $dataset;
}
final public function toDictionary() {
return array(
'type' => $this->getDatasetTypeKey(),
'functions' => mpull($this->getFunctions(), 'toDictionary'),
);
}
final public function getChartDisplayData(
PhabricatorChartDataQuery $data_query) {
return $this->newChartDisplayData($data_query);
@ -75,4 +68,35 @@ abstract class PhabricatorChartDataset
PhabricatorChartDataQuery $data_query);
final public function getTabularDisplayData(
PhabricatorChartDataQuery $data_query) {
$results = array();
$functions = $this->getFunctions();
foreach ($functions as $function) {
$datapoints = $function->newDatapoints($data_query);
$refs = $function->getDataRefs(ipull($datapoints, 'x'));
foreach ($datapoints as $key => $point) {
$x = $point['x'];
if (isset($refs[$x])) {
$xrefs = $refs[$x];
} else {
$xrefs = array();
}
$datapoints[$key]['refs'] = $xrefs;
}
$results[] = array(
'data' => $datapoints,
);
}
return id(new PhabricatorChartDisplayData())
->setWireData($results);
}
}

View file

@ -60,6 +60,10 @@ abstract class PhabricatorChartFunction
return $this->functionLabel;
}
final public function getKey() {
return $this->getFunctionLabel()->getKey();
}
final public static function newFromDictionary(array $map) {
PhutilTypeSpec::checkMap(
$map,
@ -86,13 +90,6 @@ abstract class PhabricatorChartFunction
return $function;
}
public function toDictionary() {
return array(
'function' => $this->getFunctionKey(),
'arguments' => $this->getArgumentParser()->getRawArguments(),
);
}
public function getSubfunctions() {
$result = array();
$result[] = $this;
@ -180,6 +177,8 @@ abstract class PhabricatorChartFunction
}
abstract public function evaluateFunction(array $xv);
abstract public function getDataRefs(array $xv);
abstract public function loadRefs(array $refs);
public function getDomain() {
return null;

View file

@ -3,11 +3,21 @@
final class PhabricatorChartFunctionLabel
extends Phobject {
private $key;
private $name;
private $color;
private $icon;
private $fillColor;
public function setKey($key) {
$this->key = $key;
return $this;
}
public function getKey() {
return $this->key;
}
public function setName($name) {
$this->name = $name;
return $this;
@ -46,6 +56,7 @@ final class PhabricatorChartFunctionLabel
public function toWireFormat() {
return array(
'key' => $this->getKey(),
'name' => $this->getName(),
'color' => $this->getColor(),
'icon' => $this->getIcon(),

View file

@ -5,24 +5,187 @@ final class PhabricatorChartStackedAreaDataset
const DATASETKEY = 'stacked-area';
private $stacks;
public function setStacks(array $stacks) {
$this->stacks = $stacks;
return $this;
}
public function getStacks() {
return $this->stacks;
}
protected function newChartDisplayData(
PhabricatorChartDataQuery $data_query) {
$functions = $this->getFunctions();
$functions = mpull($functions, null, 'getKey');
$reversed_functions = array_reverse($functions, true);
$stacks = $this->getStacks();
$function_points = array();
foreach ($reversed_functions as $function_idx => $function) {
$function_points[$function_idx] = array();
if (!$stacks) {
$stacks = array(
array_reverse(array_keys($functions), true),
);
}
$datapoints = $function->newDatapoints($data_query);
foreach ($datapoints as $point) {
$x = $point['x'];
$function_points[$function_idx][$x] = $point;
$series = array();
$raw_points = array();
foreach ($stacks as $stack) {
$stack_functions = array_select_keys($functions, $stack);
$function_points = $this->getFunctionDatapoints(
$data_query,
$stack_functions);
$stack_points = $function_points;
$function_points = $this->getGeometry(
$data_query,
$function_points);
$baseline = array();
foreach ($function_points as $function_idx => $points) {
$bounds = array();
foreach ($points as $x => $point) {
if (!isset($baseline[$x])) {
$baseline[$x] = 0;
}
$y0 = $baseline[$x];
$baseline[$x] += $point['y'];
$y1 = $baseline[$x];
$bounds[] = array(
'x' => $x,
'y0' => $y0,
'y1' => $y1,
);
if (isset($stack_points[$function_idx][$x])) {
$stack_points[$function_idx][$x]['y1'] = $y1;
}
}
$series[$function_idx] = $bounds;
}
$raw_points += $stack_points;
}
$series = array_select_keys($series, array_keys($functions));
$series = array_values($series);
$raw_points = array_select_keys($raw_points, array_keys($functions));
$raw_points = array_values($raw_points);
$range_min = null;
$range_max = null;
foreach ($series as $geometry_list) {
foreach ($geometry_list as $geometry_item) {
$y0 = $geometry_item['y0'];
$y1 = $geometry_item['y1'];
if ($range_min === null) {
$range_min = $y0;
}
$range_min = min($range_min, $y0, $y1);
if ($range_max === null) {
$range_max = $y1;
}
$range_max = max($range_max, $y0, $y1);
}
}
$raw_points = $function_points;
// We're going to group multiple events into a single point if they have
// X values that are very close to one another.
//
// If the Y values are also close to one another (these points are near
// one another in a horizontal line), it can be hard to select any
// individual point with the mouse.
//
// Even if the Y values are not close together (the points are on a
// fairly steep slope up or down), it's usually better to be able to
// mouse over a single point at the top or bottom of the slope and get
// a summary of what's going on.
$domain_max = $data_query->getMaximumValue();
$domain_min = $data_query->getMinimumValue();
$resolution = ($domain_max - $domain_min) / 100;
$events = array();
foreach ($raw_points as $function_idx => $points) {
$event_list = array();
$event_group = array();
$head_event = null;
foreach ($points as $point) {
$x = $point['x'];
if ($head_event === null) {
// We don't have any points yet, so start a new group.
$head_event = $x;
$event_group[] = $point;
} else if (($x - $head_event) <= $resolution) {
// This point is close to the first point in this group, so
// add it to the existing group.
$event_group[] = $point;
} else {
// This point is not close to the first point in the group,
// so create a new group.
$event_list[] = $event_group;
$head_event = $x;
$event_group = array($point);
}
}
if ($event_group) {
$event_list[] = $event_group;
}
$event_spec = array();
foreach ($event_list as $key => $event_points) {
// NOTE: We're using the last point as the representative point so
// that you can learn about a section of a chart by hovering over
// the point to right of the section, which is more intuitive than
// other points.
$event = last($event_points);
$event = $event + array(
'n' => count($event_points),
);
$event_list[$key] = $event;
}
$events[] = $event_list;
}
$wire_labels = array();
foreach ($functions as $function_key => $function) {
$label = $function->getFunctionLabel();
$wire_labels[] = $label->toWireFormat();
}
$result = array(
'type' => $this->getDatasetTypeKey(),
'data' => $series,
'events' => $events,
'labels' => $wire_labels,
);
return id(new PhabricatorChartDisplayData())
->setWireData($result)
->setRange(new PhabricatorChartInterval($range_min, $range_max));
}
private function getAllXValuesAsMap(
PhabricatorChartDataQuery $data_query,
array $point_lists) {
// We need to define every function we're drawing at every point where
// any of the functions we're drawing are defined. If we don't, we'll
@ -31,17 +194,54 @@ final class PhabricatorChartStackedAreaDataset
// stacking the functions on top of one another.
$must_define = array();
foreach ($function_points as $function_idx => $points) {
foreach ($points as $x => $point) {
$min = $data_query->getMinimumValue();
$max = $data_query->getMaximumValue();
$must_define[$max] = $max;
$must_define[$min] = $min;
foreach ($point_lists as $point_list) {
foreach ($point_list as $x => $point) {
$must_define[$x] = $x;
}
}
ksort($must_define);
foreach ($reversed_functions as $function_idx => $function) {
return $must_define;
}
private function getFunctionDatapoints(
PhabricatorChartDataQuery $data_query,
array $functions) {
assert_instances_of($functions, 'PhabricatorChartFunction');
$points = array();
foreach ($functions as $idx => $function) {
$points[$idx] = array();
$datapoints = $function->newDatapoints($data_query);
foreach ($datapoints as $point) {
$x_value = $point['x'];
$points[$idx][$x_value] = $point;
}
}
return $points;
}
private function getGeometry(
PhabricatorChartDataQuery $data_query,
array $point_lists) {
$must_define = $this->getAllXValuesAsMap($data_query, $point_lists);
foreach ($point_lists as $idx => $points) {
$missing = array();
foreach ($must_define as $x) {
if (!isset($function_points[$function_idx][$x])) {
if (!isset($points[$x])) {
$missing[$x] = true;
}
}
@ -50,8 +250,6 @@ final class PhabricatorChartStackedAreaDataset
continue;
}
$points = $function_points[$function_idx];
$values = array_keys($points);
$cursor = -1;
$length = count($values);
@ -84,88 +282,19 @@ final class PhabricatorChartStackedAreaDataset
$y = $ymin + (($ymax - $ymin) * $distance);
} else {
$xmin = $values[$cursor];
$y = $function_points[$function_idx][$xmin]['y'];
$y = $points[$xmin]['y'];
}
$function_points[$function_idx][$x] = array(
$point_lists[$idx][$x] = array(
'x' => $x,
'y' => $y,
);
}
ksort($function_points[$function_idx]);
ksort($point_lists[$idx]);
}
$range_min = null;
$range_max = null;
$series = array();
$baseline = array();
foreach ($function_points as $function_idx => $points) {
$below = idx($function_points, $function_idx - 1);
$bounds = array();
foreach ($points as $x => $point) {
if (!isset($baseline[$x])) {
$baseline[$x] = 0;
}
$y0 = $baseline[$x];
$baseline[$x] += $point['y'];
$y1 = $baseline[$x];
$bounds[] = array(
'x' => $x,
'y0' => $y0,
'y1' => $y1,
);
if (isset($raw_points[$function_idx][$x])) {
$raw_points[$function_idx][$x]['y1'] = $y1;
}
if ($range_min === null) {
$range_min = $y0;
}
$range_min = min($range_min, $y0, $y1);
if ($range_max === null) {
$range_max = $y1;
}
$range_max = max($range_max, $y0, $y1);
}
$series[] = $bounds;
}
$series = array_reverse($series);
$events = array();
foreach ($raw_points as $function_idx => $points) {
$event_list = array();
foreach ($points as $point) {
$event_list[] = $point;
}
$events[] = $event_list;
}
$wire_labels = array();
foreach ($functions as $function_key => $function) {
$label = $function->getFunctionLabel();
$wire_labels[] = $label->toWireFormat();
}
$result = array(
'type' => $this->getDatasetTypeKey(),
'data' => $series,
'events' => $events,
'labels' => $wire_labels,
);
return id(new PhabricatorChartDisplayData())
->setWireData($result)
->setRange(new PhabricatorChartInterval($range_min, $range_max));
return $point_lists;
}
}

View file

@ -70,4 +70,22 @@ final class PhabricatorComposeChartFunction
return $yv;
}
public function getDataRefs(array $xv) {
// TODO: This is not entirely correct. The correct implementation would
// map "x -> y" at each stage of composition and pull and aggregate all
// the datapoint refs. In practice, we currently never compose functions
// with a data function somewhere in the middle, so just grabbing the first
// result is close enough.
// In the future, we may: notably, "x -> shift(-1 month) -> ..." to
// generate a month-over-month overlay is a sensible operation which will
// source data from the middle of a function composition.
foreach ($this->getFunctionArguments() as $function) {
return $function->getDataRefs($xv);
}
return array();
}
}

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorConstantChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'constant';

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorCosChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'cos';

View file

@ -7,6 +7,7 @@ final class PhabricatorFactChartFunction
private $fact;
private $map;
private $refs;
protected function newArguments() {
$key_argument = $this->newArgument()
@ -51,13 +52,15 @@ final class PhabricatorFactChartFunction
$data = queryfx_all(
$conn,
'SELECT value, epoch FROM %T WHERE %LA ORDER BY epoch ASC',
'SELECT id, value, epoch FROM %T WHERE %LA ORDER BY epoch ASC',
$table_name,
$where);
$map = array();
$refs = array();
if ($data) {
foreach ($data as $row) {
$ref = (string)$row['id'];
$value = (int)$row['value'];
$epoch = (int)$row['epoch'];
@ -66,10 +69,17 @@ final class PhabricatorFactChartFunction
}
$map[$epoch] += $value;
if (!isset($refs[$epoch])) {
$refs[$epoch] = array();
}
$refs[$epoch][] = $ref;
}
}
$this->map = $map;
$this->refs = $refs;
}
public function getDomain() {
@ -99,4 +109,60 @@ final class PhabricatorFactChartFunction
return $yv;
}
public function getDataRefs(array $xv) {
return array_select_keys($this->refs, $xv);
}
public function loadRefs(array $refs) {
$fact = $this->fact;
$datapoint_table = $fact->newDatapoint();
$conn = $datapoint_table->establishConnection('r');
$dimension_table = new PhabricatorFactObjectDimension();
$where = array();
$where[] = qsprintf(
$conn,
'p.id IN (%Ld)',
$refs);
$rows = queryfx_all(
$conn,
'SELECT
p.id id,
p.value,
od.objectPHID objectPHID,
dd.objectPHID dimensionPHID
FROM %R p
LEFT JOIN %R od ON od.id = p.objectID
LEFT JOIN %R dd ON dd.id = p.dimensionID
WHERE %LA',
$datapoint_table,
$dimension_table,
$dimension_table,
$where);
$rows = ipull($rows, null, 'id');
$results = array();
foreach ($refs as $ref) {
if (!isset($rows[$ref])) {
continue;
}
$row = $rows[$ref];
$results[$ref] = array(
'objectPHID' => $row['objectPHID'],
'dimensionPHID' => $row['dimensionPHID'],
'value' => (float)$row['value'],
);
}
return $results;
}
}

View file

@ -32,4 +32,38 @@ abstract class PhabricatorHigherOrderChartFunction
return array_keys($map);
}
public function getDataRefs(array $xv) {
$refs = array();
foreach ($this->getFunctionArguments() as $function) {
$function_refs = $function->getDataRefs($xv);
$function_refs = array_select_keys($function_refs, $xv);
if (!$function_refs) {
continue;
}
foreach ($function_refs as $x => $ref_list) {
if (!isset($refs[$x])) {
$refs[$x] = array();
}
foreach ($ref_list as $ref) {
$refs[$x][] = $ref;
}
}
}
return $refs;
}
public function loadRefs(array $refs) {
$results = array();
foreach ($this->getFunctionArguments() as $function) {
$results += $function->loadRefs($refs);
}
return $results;
}
}

View file

@ -1,36 +1,27 @@
<?php
final class PhabricatorMaxChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'max';
protected function newArguments() {
return array(
$this->newArgument()
->setName('x')
->setType('function'),
$this->newArgument()
->setName('max')
->setType('number'),
);
}
public function getDomain() {
return $this->getArgument('x')->getDomain();
}
public function newInputValues(PhabricatorChartDataQuery $query) {
return $this->getArgument('x')->newInputValues($query);
}
public function evaluateFunction(array $xv) {
$yv = $this->getArgument('x')->evaluateFunction($xv);
$max = $this->getArgument('max');
foreach ($yv as $k => $y) {
if ($y > $max) {
$yv[$k] = null;
$yv = array();
foreach ($xv as $x) {
if ($x > $max) {
$yv[] = null;
} else {
$yv[] = $x;
}
}

View file

@ -1,36 +1,27 @@
<?php
final class PhabricatorMinChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'min';
protected function newArguments() {
return array(
$this->newArgument()
->setName('x')
->setType('function'),
$this->newArgument()
->setName('min')
->setType('number'),
);
}
public function getDomain() {
return $this->getArgument('x')->getDomain();
}
public function newInputValues(PhabricatorChartDataQuery $query) {
return $this->getArgument('x')->newInputValues($query);
}
public function evaluateFunction(array $xv) {
$yv = $this->getArgument('x')->evaluateFunction($xv);
$min = $this->getArgument('min');
foreach ($yv as $k => $y) {
if ($y < $min) {
$yv[$k] = null;
$yv = array();
foreach ($xv as $x) {
if ($x < $min) {
$yv[] = null;
} else {
$yv[] = $x;
}
}

View file

@ -0,0 +1,14 @@
<?php
abstract class PhabricatorPureChartFunction
extends PhabricatorChartFunction {
public function getDataRefs(array $xv) {
return array();
}
public function loadRefs(array $refs) {
return array();
}
}

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorScaleChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'scale';

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorShiftChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'shift';

View file

@ -1,7 +1,7 @@
<?php
final class PhabricatorSinChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'sin';

View file

@ -1,13 +1,14 @@
<?php
final class PhabricatorFactChartController extends PhabricatorFactController {
final class PhabricatorFactChartController
extends PhabricatorFactController {
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$chart_key = $request->getURIData('chartKey');
if ($chart_key === null) {
return $this->newDemoChart();
if (!$chart_key) {
return new Aphront404Response();
}
$engine = id(new PhabricatorChartRenderingEngine())
@ -24,15 +25,24 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
$mode = $request->getURIData('mode');
$is_draw_mode = ($mode === 'draw');
// TODO: For now, always pull the data. We'll throw it away if we're just
// drawing the frame, but this makes errors easier to debug.
$chart_data = $engine->newChartData();
$want_data = $is_draw_mode;
if ($is_draw_mode) {
return id(new AphrontAjaxResponse())->setContent($chart_data);
// In developer mode, always pull the data in the main request. We'll
// throw it away if we're just drawing the chart frame, but this currently
// makes errors quite a bit easier to debug.
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
$want_data = true;
}
if ($want_data) {
$chart_data = $engine->newChartData();
if ($is_draw_mode) {
return id(new AphrontAjaxResponse())->setContent($chart_data);
}
}
$chart_view = $engine->newChartView();
return $this->newChartResponse($chart_view);
}
@ -50,58 +60,10 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($box);
}
private function newDemoChart() {
$viewer = $this->getViewer();
$argvs = array();
$argvs[] = array('fact', 'tasks.count.create');
$argvs[] = array('constant', 360);
$argvs[] = array('fact', 'tasks.open-count.create');
$argvs[] = array(
'sum',
array(
'accumulate',
array('fact', 'tasks.count.create'),
),
array(
'accumulate',
array('fact', 'tasks.open-count.create'),
),
);
$argvs[] = array(
'compose',
array('scale', 0.001),
array('cos'),
array('scale', 100),
array('shift', 800),
);
$datasets = array();
foreach ($argvs as $argv) {
$datasets[] = PhabricatorChartDataset::newFromDictionary(
->appendChild(
array(
'function' => $argv,
$box,
));
}
$chart = id(new PhabricatorFactChart())
->setDatasets($datasets);
$engine = id(new PhabricatorChartRenderingEngine())
->setViewer($viewer)
->setChart($chart);
$chart = $engine->getStoredChart();
return id(new AphrontRedirectResponse())->setURI($chart->getURI());
}
}

View file

@ -1,59 +1,20 @@
<?php
final class PhabricatorFactHomeController extends PhabricatorFactController {
final class PhabricatorFactHomeController
extends PhabricatorFactController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$viewer = $this->getViewer();
if ($request->isFormPost()) {
$uri = new PhutilURI('/fact/chart/');
$uri->replaceQueryParam('y1', $request->getStr('y1'));
return id(new AphrontRedirectResponse())->setURI($uri);
}
$chart = id(new PhabricatorDemoChartEngine())
->setViewer($viewer)
->newStoredChart();
$chart_form = $this->buildChartForm();
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Home'));
$title = pht('Facts');
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild(
array(
$chart_form,
));
}
private function buildChartForm() {
$request = $this->getRequest();
$viewer = $request->getUser();
$specs = PhabricatorFact::getAllFacts();
$options = mpull($specs, 'getName', 'getKey');
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Y-Axis'))
->setName('y1')
->setOptions($options))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Plot Chart')));
$panel = new PHUIObjectBoxView();
$panel->setForm($form);
$panel->setHeaderText(pht('Plot Chart'));
return $panel;
return id(new AphrontRedirectResponse())->setURI($chart->getURI());
}
}

View file

@ -63,7 +63,7 @@ abstract class PhabricatorChartEngine
abstract protected function newChart(PhabricatorFactChart $chart, array $map);
final public function buildChartPanel() {
final public function newStoredChart() {
$viewer = $this->getViewer();
$parameters = $this->getEngineParameters();
@ -76,7 +76,11 @@ abstract class PhabricatorChartEngine
->setViewer($viewer)
->setChart($chart);
$chart = $rendering_engine->getStoredChart();
return $rendering_engine->getStoredChart();
}
final public function buildChartPanel() {
$chart = $this->newStoredChart();
$panel_type = id(new PhabricatorDashboardChartPanelType())
->getPanelTypeKey();
@ -91,7 +95,7 @@ abstract class PhabricatorChartEngine
final protected function newFunction($name /* , ... */) {
$argv = func_get_args();
return id(new PhabricatorComposeChartFunction())
->setArguments(array($argv));
->setArguments($argv);
}
}

View file

@ -109,7 +109,146 @@ final class PhabricatorChartRenderingEngine
return $chart_view;
}
public function newTabularView() {
$viewer = $this->getViewer();
$tabular_data = $this->newTabularData();
$ref_keys = array();
foreach ($tabular_data['datasets'] as $tabular_dataset) {
foreach ($tabular_dataset as $function) {
foreach ($function['data'] as $point) {
foreach ($point['refs'] as $ref) {
$ref_keys[$ref] = $ref;
}
}
}
}
$chart = $this->getStoredChart();
$ref_map = array();
foreach ($chart->getDatasets() as $dataset) {
foreach ($dataset->getFunctions() as $function) {
// If we aren't looking for anything else, bail out.
if (!$ref_keys) {
break 2;
}
$function_refs = $function->loadRefs($ref_keys);
$ref_map += $function_refs;
// Remove the ref keys that we found data for from the list of keys
// we are looking for. If any function gives us data for a given ref,
// that's satisfactory.
foreach ($function_refs as $ref_key => $ref_data) {
unset($ref_keys[$ref_key]);
}
}
}
$phids = array();
foreach ($ref_map as $ref => $ref_data) {
if (isset($ref_data['objectPHID'])) {
$phids[] = $ref_data['objectPHID'];
}
}
$handles = $viewer->loadHandles($phids);
$tabular_view = array();
foreach ($tabular_data['datasets'] as $tabular_data) {
foreach ($tabular_data as $function) {
$rows = array();
foreach ($function['data'] as $point) {
$ref_views = array();
$xv = date('Y-m-d h:i:s', $point['x']);
$yv = $point['y'];
$point_refs = array();
foreach ($point['refs'] as $ref) {
if (!isset($ref_map[$ref])) {
continue;
}
$point_refs[$ref] = $ref_map[$ref];
}
if (!$point_refs) {
$rows[] = array(
$xv,
$yv,
null,
null,
null,
);
} else {
foreach ($point_refs as $ref => $ref_data) {
$ref_value = $ref_data['value'];
$ref_link = $handles[$ref_data['objectPHID']]
->renderLink();
$view_uri = urisprintf(
'/fact/object/%s/',
$ref_data['objectPHID']);
$ref_button = id(new PHUIButtonView())
->setIcon('fa-table')
->setTag('a')
->setColor('grey')
->setHref($view_uri)
->setText(pht('View Data'));
$rows[] = array(
$xv,
$yv,
$ref_value,
$ref_link,
$ref_button,
);
$xv = null;
$yv = null;
}
}
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('X'),
pht('Y'),
pht('Raw'),
pht('Refs'),
null,
))
->setColumnClasses(
array(
'n',
'n',
'n',
'wide',
null,
));
$tabular_view[] = id(new PHUIObjectBoxView())
->setHeaderText(pht('Function'))
->setTable($table);
}
}
return $tabular_view;
}
public function newChartData() {
return $this->newWireData(false);
}
public function newTabularData() {
return $this->newWireData(true);
}
private function newWireData($is_tabular) {
$chart = $this->getStoredChart();
$chart_key = $chart->getChartKey();
@ -151,7 +290,11 @@ final class PhabricatorChartRenderingEngine
$wire_datasets = array();
$ranges = array();
foreach ($datasets as $dataset) {
$display_data = $dataset->getChartDisplayData($data_query);
if ($is_tabular) {
$display_data = $dataset->getTabularDisplayData($data_query);
} else {
$display_data = $dataset->getChartDisplayData($data_query);
}
$ranges[] = $display_data->getRange();
$wire_datasets[] = $display_data->getWireData();

View file

@ -0,0 +1,46 @@
<?php
final class PhabricatorDemoChartEngine
extends PhabricatorChartEngine {
const CHARTENGINEKEY = 'facts.demo';
protected function newChart(PhabricatorFactChart $chart, array $map) {
$viewer = $this->getViewer();
$functions = array();
$function = $this->newFunction(
array('scale', 0.0001),
array('cos'),
array('scale', 128),
array('shift', 256));
$function->getFunctionLabel()
->setKey('cos-x')
->setName(pht('cos(x)'))
->setColor('rgba(0, 200, 0, 1)')
->setFillColor('rgba(0, 200, 0, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
array('constant', 345));
$function->getFunctionLabel()
->setKey('constant-345')
->setName(pht('constant(345)'))
->setColor('rgba(0, 0, 200, 1)')
->setFillColor('rgba(0, 0, 200, 0.15)');
$functions[] = $function;
$datasets = array();
$datasets[] = id(new PhabricatorChartStackedAreaDataset())
->setFunctions($functions);
$chart->attachDatasets($datasets);
}
}

View file

@ -35,8 +35,12 @@ final class ManiphestReportController extends ManiphestController {
$nav->addLabel(pht('Open Tasks'));
$nav->addFilter('user', pht('By User'));
$nav->addFilter('project', pht('By Project'));
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
$class = 'PhabricatorFactApplication';
if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
$nav->addLabel(pht('Burnup'));
$nav->addFilter('burn', pht('Burnup Rate'));
}
$this->view = $nav->selectFilter($this->view, 'user');

View file

@ -0,0 +1,135 @@
<?php
final class PhabricatorProjectActivityChartEngine
extends PhabricatorChartEngine {
const CHARTENGINEKEY = 'project.activity';
public function setProjects(array $projects) {
assert_instances_of($projects, 'PhabricatorProject');
$project_phids = mpull($projects, 'getPHID');
return $this->setEngineParameter('projectPHIDs', $project_phids);
}
protected function newChart(PhabricatorFactChart $chart, array $map) {
$viewer = $this->getViewer();
$map = $map + array(
'projectPHIDs' => array(),
);
if ($map['projectPHIDs']) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs($map['projectPHIDs'])
->execute();
$project_phids = mpull($projects, 'getPHID');
} else {
$project_phids = array();
}
$project_phid = head($project_phids);
$functions = array();
$stacks = array();
$function = $this->newFunction(
array(
'accumulate',
array(
'compose',
array('fact', 'tasks.open-count.assign.project', $project_phid),
array('min', 0),
),
));
$function->getFunctionLabel()
->setKey('moved-in')
->setName(pht('Tasks Moved Into Project'))
->setColor('rgba(128, 128, 200, 1)')
->setFillColor('rgba(128, 128, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
array(
'accumulate',
array(
'compose',
array('fact', 'tasks.open-count.status.project', $project_phid),
array('min', 0),
),
));
$function->getFunctionLabel()
->setKey('reopened')
->setName(pht('Tasks Reopened'))
->setColor('rgba(128, 128, 200, 1)')
->setFillColor('rgba(128, 128, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
array(
'accumulate',
array('fact', 'tasks.open-count.create.project', $project_phid),
));
$function->getFunctionLabel()
->setKey('created')
->setName(pht('Tasks Created'))
->setColor('rgba(0, 0, 200, 1)')
->setFillColor('rgba(0, 0, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
array(
'accumulate',
array(
'compose',
array('fact', 'tasks.open-count.status.project', $project_phid),
array('max', 0),
),
));
$function->getFunctionLabel()
->setKey('closed')
->setName(pht('Tasks Closed'))
->setColor('rgba(0, 200, 0, 1)')
->setFillColor('rgba(0, 200, 0, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
array(
'accumulate',
array(
'compose',
array('fact', 'tasks.open-count.assign.project', $project_phid),
array('max', 0),
),
));
$function->getFunctionLabel()
->setKey('moved-out')
->setName(pht('Tasks Moved Out of Project'))
->setColor('rgba(128, 200, 128, 1)')
->setFillColor('rgba(128, 200, 128, 0.15)');
$functions[] = $function;
$stacks[] = array('created', 'reopened', 'moved-in');
$stacks[] = array('closed', 'moved-out');
$datasets = array();
$dataset = id(new PhabricatorChartStackedAreaDataset())
->setFunctions($functions)
->setStacks($stacks);
$datasets[] = $dataset;
$chart->attachDatasets($datasets);
}
}

View file

@ -30,97 +30,78 @@ final class PhabricatorProjectBurndownChartEngine
$functions = array();
if ($project_phids) {
foreach ($project_phids as $project_phid) {
$function = $this->newFunction(
'min',
$open_function = $this->newFunction(
array(
'accumulate',
array(
'accumulate',
array('fact', 'tasks.open-count.assign.project', $project_phid),
'sum',
$this->newFactSum(
'tasks.open-count.create.project', $project_phids),
$this->newFactSum(
'tasks.open-count.status.project', $project_phids),
$this->newFactSum(
'tasks.open-count.assign.project', $project_phids),
),
0);
));
$function->getFunctionLabel()
->setName(pht('Tasks Moved Into Project'))
->setColor('rgba(0, 200, 200, 1)')
->setFillColor('rgba(0, 200, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
'min',
array(
'accumulate',
array('fact', 'tasks.open-count.status.project', $project_phid),
),
0);
$function->getFunctionLabel()
->setName(pht('Tasks Reopened'))
->setColor('rgba(200, 0, 200, 1)')
->setFillColor('rgba(200, 0, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
'sum',
array(
'accumulate',
array('fact', 'tasks.open-count.create.project', $project_phid),
),
array(
'max',
array(
'accumulate',
array('fact', 'tasks.open-count.status.project', $project_phid),
),
0,
),
array(
'max',
array(
'accumulate',
array('fact', 'tasks.open-count.assign.project', $project_phid),
),
0,
));
$function->getFunctionLabel()
->setName(pht('Tasks Created'))
->setColor('rgba(0, 0, 200, 1)')
->setFillColor('rgba(0, 0, 200, 0.15)');
$functions[] = $function;
}
$closed_function = $this->newFunction(
array(
'accumulate',
$this->newFactSum('tasks.open-count.status.project', $project_phids),
));
} else {
$function = $this->newFunction(
'accumulate',
array('fact', 'tasks.open-count.create'));
$open_function = $this->newFunction(
array(
'accumulate',
array('fact', 'tasks.open-count.create'),
));
$function->getFunctionLabel()
->setName(pht('Tasks Created'))
->setColor('rgba(0, 200, 200, 1)')
->setFillColor('rgba(0, 200, 200, 0.15)');
$functions[] = $function;
$function = $this->newFunction(
'accumulate',
array('fact', 'tasks.open-count.status'));
$function->getFunctionLabel()
->setName(pht('Tasks Closed / Reopened'))
->setColor('rgba(200, 0, 200, 1)')
->setFillColor('rgba(200, 0, 200, 0.15)');
$functions[] = $function;
$closed_function = $this->newFunction(
array(
'accumulate',
array('fact', 'tasks.open-count.status'),
));
}
$open_function->getFunctionLabel()
->setKey('open')
->setName(pht('Open Tasks'))
->setColor('rgba(0, 0, 200, 1)')
->setFillColor('rgba(0, 0, 200, 0.15)');
$closed_function->getFunctionLabel()
->setKey('closed')
->setName(pht('Closed Tasks'))
->setColor('rgba(0, 200, 0, 1)')
->setFillColor('rgba(0, 200, 0, 0.15)');
$datasets = array();
$datasets[] = id(new PhabricatorChartStackedAreaDataset())
->setFunctions($functions);
$dataset = id(new PhabricatorChartStackedAreaDataset())
->setFunctions(
array(
$open_function,
$closed_function,
))
->setStacks(
array(
array('open'),
array('closed'),
));
$datasets[] = $dataset;
$chart->attachDatasets($datasets);
}
private function newFactSum($fact_key, array $phids) {
$result = array();
$result[] = 'sum';
foreach ($phids as $phid) {
$result[] = array('fact', $fact_key, $phid);
}
return $result;
}
}

View file

@ -44,10 +44,24 @@ final class PhabricatorProjectReportsController
->setParentPanelPHIDs(array())
->renderPanel();
$activity_panel = id(new PhabricatorProjectActivityChartEngine())
->setViewer($viewer)
->setProjects(array($project))
->buildChartPanel();
$activity_panel->setName(pht('%s: Activity', $project->getName()));
$activity_view = id(new PhabricatorDashboardPanelRenderingEngine())
->setViewer($viewer)
->setPanel($activity_panel)
->setParentPanelPHIDs(array())
->renderPanel();
$view = id(new PHUITwoColumnView())
->setFooter(
array(
$chart_view,
$activity_view,
));
return $this->newPage()

View file

@ -34,6 +34,11 @@ final class PhabricatorProjectReportsProfileMenuItem
return false;
}
$class = 'PhabricatorFactApplication';
if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) {
return false;
}
return true;
}

View file

@ -82,7 +82,7 @@ if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$real_address = your_custom_parsing_function($raw_header);
$_SERVER['REMOTE_ADDR'] = $raw_header;
$_SERVER['REMOTE_ADDR'] = $real_address;
}
```

View file

@ -36,16 +36,17 @@
}
.chart .point {
fill: {$lightblue};
fill: #ffffff;
stroke: {$blue};
stroke-width: 1px;
stroke-width: 2px;
position: relative;
cursor: pointer;
}
.chart-tooltip {
position: absolute;
text-align: center;
width: 120px;
height: 16px;
overflow: hidden;
padding: 2px;
background: {$lightbluebackground};

View file

@ -133,18 +133,33 @@ JX.install('Chart', {
},
_newStackedArea: function(g, dataset, x, y, div, curtain) {
var ii;
var to_date = JX.bind(this, this._newDate);
var area = d3.area()
.x(function(d) { return x(to_date(d.x)); })
.y0(function(d) { return y(d.y0); })
.y0(function(d) {
// When the area is positive, draw it above the X axis. When the area
// is negative, draw it below the X axis. We currently avoid having
// functions which cross the X axis by clever construction.
if (d.y0 >= 0 && d.y1 >= 0) {
return y(d.y0);
}
if (d.y0 <= 0 && d.y1 <= 0) {
return y(d.y0);
}
return y(0);
})
.y1(function(d) { return y(d.y1); });
var line = d3.line()
.x(function(d) { return x(to_date(d.x)); })
.y(function(d) { return y(d.y1); });
for (var ii = 0; ii < dataset.data.length; ii++) {
for (ii = 0; ii < dataset.data.length; ii++) {
var label = new JX.ChartFunctionLabel(dataset.labels[ii]);
var fill_color = label.getFillColor() || label.getColor();
@ -160,6 +175,11 @@ JX.install('Chart', {
.style('stroke', stroke_color)
.attr('d', line(dataset.data[ii]));
curtain.addFunctionLabel(label);
}
// Now that we've drawn all the areas and lines, draw the dots.
for (ii = 0; ii < dataset.data.length; ii++) {
g.selectAll('dot')
.data(dataset.events[ii])
.enter()
@ -178,8 +198,16 @@ JX.install('Chart', {
var d_d = dd.getDate();
var y = parseInt(d.y1);
var label = d.n + ' Points';
var view =
d_y + '-' + d_m + '-' + d_d + ': ' + y + '<br />' +
label;
div
.html(d_y + '-' + d_m + '-' + d_d + ': ' + d.y1)
.html(view)
.style('opacity', 0.9)
.style('left', (d3.event.pageX - 60) + 'px')
.style('top', (d3.event.pageY - 38) + 'px');
@ -187,9 +215,8 @@ JX.install('Chart', {
.on('mouseout', function() {
div.style('opacity', 0);
});
curtain.addFunctionLabel(label);
}
},
_newDate: function(epoch) {