Stack chart functions in a more physical way
Summary:
Ref T13279. See that task for some discussion.
The accumulations of some of the datasets may be negative (e.g., if more tasks are moved out of a project than into it) which can lead to negative area in the stacked chart.
Introduce `min(...)` and `max(...)` to separate a function into points above or below some line, then mangle the areas to pick the negative and positive regions apart so they at least have a plausible physical interpretation and none of the areas are negative.
This is presumably not a final version, I'm just trying to produce a chart that isn't a sequence of overlapping regions with negative areas that is "technically" correct but not really possible to interpret.
Test Plan: {F6439195}
Reviewers: amckinley
Reviewed By: amckinley
Subscribers: yelirekim
Maniphest Tasks: T13279
Differential Revision: https://secure.phabricator.com/D20506
			
			
This commit is contained in:
		| @@ -3622,6 +3622,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', |     'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', | ||||||
|     'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php', |     'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php', | ||||||
|     'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php', |     'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php', | ||||||
|  |     'PhabricatorMaxChartFunction' => 'applications/fact/chart/PhabricatorMaxChartFunction.php', | ||||||
|     'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php', |     'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php', | ||||||
|     'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php', |     'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php', | ||||||
|     'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php', |     'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php', | ||||||
| @@ -3676,6 +3677,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorMetronome' => 'infrastructure/util/PhabricatorMetronome.php', |     'PhabricatorMetronome' => 'infrastructure/util/PhabricatorMetronome.php', | ||||||
|     'PhabricatorMetronomeTestCase' => 'infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php', |     'PhabricatorMetronomeTestCase' => 'infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php', | ||||||
|     'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', |     'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', | ||||||
|  |     'PhabricatorMinChartFunction' => 'applications/fact/chart/PhabricatorMinChartFunction.php', | ||||||
|     'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', |     'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', | ||||||
|     'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', |     'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', | ||||||
|     'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php', |     'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php', | ||||||
| @@ -9748,6 +9750,7 @@ phutil_register_library_map(array( | |||||||
|       'PhabricatorMarkupInterface', |       'PhabricatorMarkupInterface', | ||||||
|     ), |     ), | ||||||
|     'PhabricatorMarkupPreviewController' => 'PhabricatorController', |     'PhabricatorMarkupPreviewController' => 'PhabricatorController', | ||||||
|  |     'PhabricatorMaxChartFunction' => 'PhabricatorChartFunction', | ||||||
|     'PhabricatorMemeEngine' => 'Phobject', |     'PhabricatorMemeEngine' => 'Phobject', | ||||||
|     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', |     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', | ||||||
|     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', |     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', | ||||||
| @@ -9814,6 +9817,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorMetronome' => 'Phobject', |     'PhabricatorMetronome' => 'Phobject', | ||||||
|     'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', |     'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', |     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', | ||||||
|  |     'PhabricatorMinChartFunction' => 'PhabricatorChartFunction', | ||||||
|     'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', |     'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', | ||||||
|     'PhabricatorModularTransactionType' => 'Phobject', |     'PhabricatorModularTransactionType' => 'Phobject', | ||||||
|     'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', |     'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', | ||||||
|   | |||||||
| @@ -9,8 +9,10 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|     PhabricatorChartDataQuery $data_query) { |     PhabricatorChartDataQuery $data_query) { | ||||||
|     $functions = $this->getFunctions(); |     $functions = $this->getFunctions(); | ||||||
|  |  | ||||||
|  |     $reversed_functions = array_reverse($functions, true); | ||||||
|  |  | ||||||
|     $function_points = array(); |     $function_points = array(); | ||||||
|     foreach ($functions as $function_idx => $function) { |     foreach ($reversed_functions as $function_idx => $function) { | ||||||
|       $function_points[$function_idx] = array(); |       $function_points[$function_idx] = array(); | ||||||
|  |  | ||||||
|       $datapoints = $function->newDatapoints($data_query); |       $datapoints = $function->newDatapoints($data_query); | ||||||
| @@ -36,7 +38,7 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|     } |     } | ||||||
|     ksort($must_define); |     ksort($must_define); | ||||||
|  |  | ||||||
|     foreach ($functions as $function_idx => $function) { |     foreach ($reversed_functions as $function_idx => $function) { | ||||||
|       $missing = array(); |       $missing = array(); | ||||||
|       foreach ($must_define as $x) { |       foreach ($must_define as $x) { | ||||||
|         if (!isset($function_points[$function_idx][$x])) { |         if (!isset($function_points[$function_idx][$x])) { | ||||||
| @@ -136,6 +138,8 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|       $series[] = $bounds; |       $series[] = $bounds; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     $series = array_reverse($series); | ||||||
|  |  | ||||||
|     $events = array(); |     $events = array(); | ||||||
|     foreach ($raw_points as $function_idx => $points) { |     foreach ($raw_points as $function_idx => $points) { | ||||||
|       $event_list = array(); |       $event_list = array(); | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								src/applications/fact/chart/PhabricatorMaxChartFunction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/applications/fact/chart/PhabricatorMaxChartFunction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorMaxChartFunction | ||||||
|  |   extends PhabricatorChartFunction { | ||||||
|  |  | ||||||
|  |   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; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return $yv; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								src/applications/fact/chart/PhabricatorMinChartFunction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/applications/fact/chart/PhabricatorMinChartFunction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorMinChartFunction | ||||||
|  |   extends PhabricatorChartFunction { | ||||||
|  |  | ||||||
|  |   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; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return $yv; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -15,7 +15,7 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       $this->log(pht('Zzz...')); |       $this->log(pht('Zzz...')); | ||||||
|       $this->sleep(60 * 5); |       $this->sleep(15); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,37 +32,62 @@ final class PhabricatorProjectBurndownChartEngine | |||||||
|     if ($project_phids) { |     if ($project_phids) { | ||||||
|       foreach ($project_phids as $project_phid) { |       foreach ($project_phids as $project_phid) { | ||||||
|         $function = $this->newFunction( |         $function = $this->newFunction( | ||||||
|           'accumulate', |           'min', | ||||||
|           array('fact', 'tasks.open-count.create.project', $project_phid)); |           array( | ||||||
|  |             'accumulate', | ||||||
|  |             array('fact', 'tasks.open-count.assign.project', $project_phid), | ||||||
|  |           ), | ||||||
|  |           0); | ||||||
|  |  | ||||||
|         $function->getFunctionLabel() |         $function->getFunctionLabel() | ||||||
|           ->setName(pht('Tasks Created')) |           ->setName(pht('Tasks Moved Into Project')) | ||||||
|           ->setColor('rgba(0, 0, 200, 1)') |           ->setColor('rgba(0, 200, 200, 1)') | ||||||
|           ->setFillColor('rgba(0, 0, 200, 0.15)'); |           ->setFillColor('rgba(0, 200, 200, 0.15)'); | ||||||
|  |  | ||||||
|         $functions[] = $function; |         $functions[] = $function; | ||||||
|  |  | ||||||
|  |  | ||||||
|         $function = $this->newFunction( |         $function = $this->newFunction( | ||||||
|           'accumulate', |           'min', | ||||||
|           array('fact', 'tasks.open-count.status.project', $project_phid)); |           array( | ||||||
|  |             'accumulate', | ||||||
|  |             array('fact', 'tasks.open-count.status.project', $project_phid), | ||||||
|  |           ), | ||||||
|  |           0); | ||||||
|  |  | ||||||
|         $function->getFunctionLabel() |         $function->getFunctionLabel() | ||||||
|           ->setName(pht('Tasks Closed / Reopened')) |           ->setName(pht('Tasks Reopened')) | ||||||
|           ->setColor('rgba(200, 0, 200, 1)') |           ->setColor('rgba(200, 0, 200, 1)') | ||||||
|           ->setFillColor('rgba(200, 0, 200, 0.15)'); |           ->setFillColor('rgba(200, 0, 200, 0.15)'); | ||||||
|  |  | ||||||
|         $functions[] = $function; |         $functions[] = $function; | ||||||
|  |  | ||||||
|  |  | ||||||
|         $function = $this->newFunction( |         $function = $this->newFunction( | ||||||
|           'accumulate', |           'sum', | ||||||
|           array('fact', 'tasks.open-count.assign.project', $project_phid)); |           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() |         $function->getFunctionLabel() | ||||||
|           ->setName(pht('Tasks Rescoped')) |           ->setName(pht('Tasks Created')) | ||||||
|           ->setColor('rgba(0, 200, 200, 1)') |           ->setColor('rgba(0, 0, 200, 1)') | ||||||
|           ->setFillColor('rgba(0, 200, 200, 0.15)'); |           ->setFillColor('rgba(0, 0, 200, 0.15)'); | ||||||
|  |  | ||||||
|         $functions[] = $function; |         $functions[] = $function; | ||||||
|       } |       } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 epriestley
					epriestley