From 167f06d3eb70ed187219031e3422fdb6ce8b465f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 16 May 2019 12:56:58 -0700 Subject: [PATCH 01/42] Label transaction groups with a "group ID" so Herald can reconstruct them faithfully Summary: Ref T13283. See PHI1202. See D20519. When we apply a group of transactions, label all of them with the same "group ID". This allows other things, notably Herald, to figure out which transactions applied together in a faithful way rather than by guessing, even though the guess was probably pretty good most of the time. Also expose this to `transaction.search` in case callers want to do something similar. They get a list of transaction IDs from webhooks already anyway, but some callers use `transaction.search` outside of webhooks and this information may be useful. Test Plan: - Ran Herald Test Console, saw faithful selection of recent transactions. - Changed hard limit from 1000 to 1, saw exception. Users should be very hard-pressed to hit this normally (they'd have to add 990-ish custom fields, then edit every field at once, I think) so I'm just fataling rather than processing some subset of the transaction set. - Called `transaction.search`, saw group ID information available. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13283 Differential Revision: https://secure.phabricator.com/D20524 --- .../HeraldTestConsoleController.php | 44 ++++++++++++++----- .../TransactionSearchConduitAPIMethod.php | 8 ++++ ...habricatorApplicationTransactionEditor.php | 4 ++ .../PhabricatorApplicationTransaction.php | 8 ++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index c8dfded2eb..5962996bb2 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -260,24 +260,48 @@ final class HeraldTestConsoleController extends HeraldController { $query = PhabricatorApplicationTransactionQuery::newQueryForObject( $object); - $xactions = $query + $query ->withObjectPHIDs(array($object->getPHID())) - ->setViewer($viewer) - ->setLimit(100) - ->execute(); + ->setViewer($viewer); + + $xactions = new PhabricatorQueryIterator($query); $applied = array(); - // Pick the most recent group of transactions. This may not be exactly the - // same as what Herald acted on: for example, we may select a single group - // of transactions here which were really applied across two or more edits. - // Since this is relatively rare and we show you what we picked, it's okay - // that we just do roughly the right thing. + $recent_id = null; + $hard_limit = 1000; foreach ($xactions as $xaction) { - if (!$xaction->shouldDisplayGroupWith($applied)) { + $group_id = $xaction->getTransactionGroupID(); + + // If this is the first transaction, save the group ID: we want to + // select all transactions in the same group. + if (!$applied) { + $recent_id = $group_id; + if ($recent_id === null) { + // If the first transaction has no group ID, it is likely an older + // transaction from before the introduction of group IDs. In this + // case, select only the most recent transaction and bail out. + $applied[] = $xaction; + break; + } + } + + // If this transaction is from a different transaction group, we've + // found all the transactions applied in the most recent group. + if ($group_id !== $recent_id) { break; } + $applied[] = $xaction; + + if (count($applied) > $hard_limit) { + throw new Exception( + pht( + 'This object ("%s") has more than %s transactions in its most '. + 'recent transaction group; this is too many.', + $object->getPHID(), + new PhutilNumber($hard_limit))); + } } return $applied; diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 8892933903..6f7f713dab 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -260,6 +260,13 @@ EOREMARKUP } } + $group_id = $xaction->getTransactionGroupID(); + if (!strlen($group_id)) { + $group_id = null; + } else { + $group_id = (string)$group_id; + } + $data[] = array( 'id' => (int)$xaction->getID(), 'phid' => (string)$xaction->getPHID(), @@ -268,6 +275,7 @@ EOREMARKUP 'objectPHID' => (string)$xaction->getObjectPHID(), 'dateCreated' => (int)$xaction->getDateCreated(), 'dateModified' => (int)$xaction->getDateModified(), + 'groupID' => $group_id, 'comments' => $comment_data, 'fields' => $fields, ); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index b383d0605c..c9d8a0d998 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1162,6 +1162,8 @@ abstract class PhabricatorApplicationTransactionEditor throw $ex; } + $group_id = Filesystem::readRandomCharacters(32); + foreach ($xactions as $xaction) { if ($was_locked) { $is_override = $this->isLockOverrideTransaction($xaction); @@ -1171,6 +1173,8 @@ abstract class PhabricatorApplicationTransactionEditor } $xaction->setObjectPHID($object->getPHID()); + $xaction->setTransactionGroupID($group_id); + if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); $comment_editor->applyEdit($xaction, $xaction->getComment()); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index a55d5ce73e..1250b7cb16 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -177,6 +177,14 @@ abstract class PhabricatorApplicationTransaction return (bool)$this->getMetadataValue('core.lock-override', false); } + public function setTransactionGroupID($group_id) { + return $this->setMetadataValue('core.groupID', $group_id); + } + + public function getTransactionGroupID() { + return $this->getMetadataValue('core.groupID', null); + } + public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; From a76e91ea9ef6d17987c819b595fd5c7a638c70a5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Apr 2019 11:23:33 -0700 Subject: [PATCH 02/42] Remove obsolete Dashboard panel methods with no callsites Summary: Ref T13272. Since the move to EditEngine, these methods have no callsites. Test Plan: `grep` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272 Differential Revision: https://secure.phabricator.com/D20484 --- .../PhabricatorDashboardPanelType.php | 7 ----- .../PhabricatorDashboardQueryPanelType.php | 27 ------------------- 2 files changed, 34 deletions(-) diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php index d6311e7765..ecb1658f44 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php @@ -12,13 +12,6 @@ abstract class PhabricatorDashboardPanelType extends Phobject { PhabricatorDashboardPanel $panel, PhabricatorDashboardPanelRenderingEngine $engine); - public function initializeFieldsFromRequest( - PhabricatorDashboardPanel $panel, - PhabricatorCustomFieldList $field_list, - AphrontRequest $request) { - return; - } - /** * Should this panel pull content in over AJAX? * diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php index 9e0da25c43..2174f793a4 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php @@ -55,33 +55,6 @@ final class PhabricatorDashboardQueryPanelType ); } - public function initializeFieldsFromRequest( - PhabricatorDashboardPanel $panel, - PhabricatorCustomFieldList $field_list, - AphrontRequest $request) { - - $map = array(); - if (strlen($request->getStr('engine'))) { - $map['class'] = $request->getStr('engine'); - } - - if (strlen($request->getStr('query'))) { - $map['key'] = $request->getStr('query'); - } - - $full_map = array(); - foreach ($map as $key => $value) { - $full_map["std:dashboard:core:{$key}"] = $value; - } - - foreach ($field_list->getFields() as $field) { - $field_key = $field->getFieldKey(); - if (isset($full_map[$field_key])) { - $field->setValueFromStorage($full_map[$field_key]); - } - } - } - public function renderPanelContent( PhabricatorUser $viewer, PhabricatorDashboardPanel $panel, From 06778ea5503243826589c50425eebe3a9bd170f1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 18 Apr 2019 16:42:17 -0700 Subject: [PATCH 03/42] Separate the "configuration" and "evaluation" phases of chart functions Summary: Depends on D20446. Currently, chart functions are both configured through arguments and evaluated through arguments. This sort of conflates things and makes some logic more difficult than it should be. Instead: - Function arguments are used to configure function behavior. For example, `scale(2)` configures a function which does `f(x) => 2 * x`. - Evaluation is now separate, after configuration. We can get rid of "sourceFunction" (which was basically marking one argument as "this is the thing that gets piped in" in a weird magical way) and "canEvaluate()" and "impulse". Sequences of functions are achieved with `compose(u, v, w)`, which configures a function `f(x) => w(v(u(x)))` (note order is left-to right, like piping `x | u | v | w` to produce `y`). The new flow is: - Every chartable function is `compose(...)` at top level, and composes one or more functions. `compose(x)` is longhand for `id(x)`. This just gives us a root/anchor node. - Figure out a domain, through various means. - Ask the function for a list of good input X values in that domain. This lets function chains which include a "fact" with distinct datapoints tell us that we should evaluate those datapoints. - Pipe those X values through the function. - We get Y values out. - Draw those points. Also: - Adds `accumluate()`. - Adds `sum()`, which is now easy to implement. - Adds `compose()`. - All functions can now always evaluate everywhere, they just return `null` if they are not defined at a given X. - Adds repeatable arguments for `compose(f, g, ...)` and `sum(f, g, ...)`. Test Plan: {F6409890} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Differential Revision: https://secure.phabricator.com/D20454 --- src/__phutil_library_map__.php | 10 +- .../PhabricatorAccumulateChartFunction.php | 81 +++++++++ .../fact/chart/PhabricatorChartDataQuery.php | 44 +++++ .../fact/chart/PhabricatorChartFunction.php | 169 ++++++++---------- .../PhabricatorChartFunctionArgument.php | 20 +-- ...PhabricatorChartFunctionArgumentParser.php | 101 ++++++----- .../chart/PhabricatorComposeChartFunction.php | 73 ++++++++ .../PhabricatorConstantChartFunction.php | 14 +- .../chart/PhabricatorCosChartFunction.php | 19 +- .../chart/PhabricatorFactChartFunction.php | 93 ++++------ .../PhabricatorHigherOrderChartFunction.php | 56 ++++++ .../chart/PhabricatorScaleChartFunction.php | 18 +- .../chart/PhabricatorShiftChartFunction.php | 18 +- .../chart/PhabricatorSinChartFunction.php | 19 +- .../chart/PhabricatorSumChartFunction.php | 40 +++++ .../fact/chart/PhabricatorXChartFunction.php | 20 --- .../PhabricatorFactChartController.php | 98 +++++----- 17 files changed, 574 insertions(+), 319 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorAccumulateChartFunction.php create mode 100644 src/applications/fact/chart/PhabricatorComposeChartFunction.php create mode 100644 src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php create mode 100644 src/applications/fact/chart/PhabricatorSumChartFunction.php delete mode 100644 src/applications/fact/chart/PhabricatorXChartFunction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3e9d57d4c5..e920e92b8d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2107,6 +2107,7 @@ phutil_register_library_map(array( 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', 'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php', + 'PhabricatorAccumulateChartFunction' => 'applications/fact/chart/PhabricatorAccumulateChartFunction.php', 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', @@ -2695,6 +2696,7 @@ phutil_register_library_map(array( 'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php', 'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php', 'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php', + 'PhabricatorComposeChartFunction' => 'applications/fact/chart/PhabricatorComposeChartFunction.php', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', @@ -3425,6 +3427,7 @@ phutil_register_library_map(array( 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', 'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php', 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php', + 'PhabricatorHigherOrderChartFunction' => 'applications/fact/chart/PhabricatorHigherOrderChartFunction.php', 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php', 'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php', @@ -4725,6 +4728,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php', 'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php', + 'PhabricatorSumChartFunction' => 'applications/fact/chart/PhabricatorSumChartFunction.php', 'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php', @@ -4952,7 +4956,6 @@ phutil_register_library_map(array( 'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php', 'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php', 'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php', - 'PhabricatorXChartFunction' => 'applications/fact/chart/PhabricatorXChartFunction.php', 'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php', 'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php', 'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php', @@ -7987,6 +7990,7 @@ phutil_register_library_map(array( 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', + 'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', @@ -8691,6 +8695,7 @@ phutil_register_library_map(array( 'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField', 'PhabricatorCommonPasswords' => 'Phobject', + 'PhabricatorComposeChartFunction' => 'PhabricatorHigherOrderChartFunction', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitApplication' => 'PhabricatorApplication', 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', @@ -9520,6 +9525,7 @@ phutil_register_library_map(array( 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource', 'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorHigherOrderChartFunction' => 'PhabricatorChartFunction', 'PhabricatorHomeApplication' => 'PhabricatorApplication', 'PhabricatorHomeConstants' => 'PhabricatorHomeController', 'PhabricatorHomeController' => 'PhabricatorController', @@ -11053,6 +11059,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension', + 'PhabricatorSumChartFunction' => 'PhabricatorHigherOrderChartFunction', 'PhabricatorSupportApplication' => 'PhabricatorApplication', 'PhabricatorSyntaxHighlighter' => 'Phobject', 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -11323,7 +11330,6 @@ phutil_register_library_map(array( 'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase', 'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase', - 'PhabricatorXChartFunction' => 'PhabricatorChartFunction', 'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO', 'PhabricatorXHPASTViewController' => 'PhabricatorController', diff --git a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php new file mode 100644 index 0000000000..074219504c --- /dev/null +++ b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php @@ -0,0 +1,81 @@ +newArgument() + ->setName('x') + ->setType('function'), + ); + } + + 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. + + $datasource = $this->getArgument('x'); + + // Use an unconstrained query to pull all the data from the underlying + // source. We need to accumulate data since the beginning of time to + // figure out the right Y-intercept -- otherwise, we'll always start at + // "0" wherever our domain begins. + $empty_query = new PhabricatorChartDataQuery(); + + $datasource_xv = $datasource->newInputValues($empty_query); + if (!$datasource_xv) { + // TODO: Maybe this should just be an error? + $datasource_xv = $xv; + } + + $yv = $datasource->evaluateFunction($datasource_xv); + + $map = array_combine($datasource_xv, $yv); + + $accumulator = 0; + foreach ($map as $x => $y) { + $accumulator += $y; + $map[$x] = $accumulator; + } + + // The value of "accumulate(x)" is the largest datapoint in the map which + // is no larger than "x". + + $map_x = array_keys($map); + $idx = -1; + $max = count($map_x) - 1; + + $yv = array(); + + $value = 0; + foreach ($xv as $x) { + // While the next "x" we need to evaluate the function at lies to the + // right of the next datapoint, move the current datapoint forward until + // we're at the rightmost datapoint which is not larger than "x". + while ($idx < $max) { + if ($map_x[$idx + 1] > $x) { + break; + } + + $idx++; + $value = $map[$map_x[$idx]]; + } + + $yv[] = $value; + } + + return $yv; + } + +} diff --git a/src/applications/fact/chart/PhabricatorChartDataQuery.php b/src/applications/fact/chart/PhabricatorChartDataQuery.php index 15708341f7..7e92938e2c 100644 --- a/src/applications/fact/chart/PhabricatorChartDataQuery.php +++ b/src/applications/fact/chart/PhabricatorChartDataQuery.php @@ -34,4 +34,48 @@ final class PhabricatorChartDataQuery return $this->limit; } + public function selectInputValues(array $xv) { + $result = array(); + + $x_min = $this->getMinimumValue(); + $x_max = $this->getMaximumValue(); + $limit = $this->getLimit(); + + if ($x_min !== null) { + foreach ($xv as $key => $x) { + if ($x < $x_min) { + unset($xv[$key]); + } + } + } + + if ($x_max !== null) { + foreach ($xv as $key => $x) { + if ($x > $x_max) { + unset($xv[$key]); + } + } + } + + // If we have too many data points, throw away some of the data. + + // TODO: This doesn't work especially well right now. + + if ($limit !== null) { + $count = count($xv); + if ($count > $limit) { + $ii = 0; + $every = ceil($count / $limit); + foreach ($xv as $key => $x) { + $ii++; + if (($ii % $every) && ($ii != $count)) { + unset($xv[$key]); + } + } + } + } + + return array_values($xv); + } + } diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php index 40d13b0c38..414147da56 100644 --- a/src/applications/fact/chart/PhabricatorChartFunction.php +++ b/src/applications/fact/chart/PhabricatorChartFunction.php @@ -3,11 +3,7 @@ abstract class PhabricatorChartFunction extends Phobject { - private $xAxis; - private $yAxis; - private $argumentParser; - private $sourceFunction; final public function getFunctionKey() { return $this->getPhobjectClassConstant('FUNCTIONKEY', 32); @@ -44,13 +40,73 @@ abstract class PhabricatorChartFunction $parser->setHaveAllArguments(true); $parser->parseArguments(); - $source_argument = $parser->getSourceFunctionArgument(); - if ($source_argument) { - $source_function = $this->getArgument($source_argument->getName()); - $this->setSourceFunction($source_function); + return $this; + } + + public function getSubfunctions() { + $result = array(); + $result[] = $this; + + foreach ($this->getFunctionArguments() as $argument) { + foreach ($argument->getSubfunctions() as $subfunction) { + $result[] = $subfunction; + } } - return $this; + return $result; + } + + public function getFunctionArguments() { + $results = array(); + + $parser = $this->getArgumentParser(); + foreach ($parser->getAllArguments() as $argument) { + if ($argument->getType() !== 'function') { + continue; + } + + $name = $argument->getName(); + $value = $this->getArgument($name); + + if (!is_array($value)) { + $results[] = $value; + } else { + foreach ($value as $arg_value) { + $results[] = $arg_value; + } + } + } + + return $results; + } + + public function newDatapoints(PhabricatorChartDataQuery $query) { + $xv = $this->newInputValues($query); + + if ($xv === null) { + $xv = $this->newDefaultInputValues($query); + } + + $xv = $query->selectInputValues($xv); + + $n = count($xv); + $yv = $this->evaluateFunction($xv); + + $points = array(); + for ($ii = 0; $ii < $n; $ii++) { + $y = $yv[$ii]; + + if ($y === null) { + continue; + } + + $points[] = array( + 'x' => $xv[$ii], + 'y' => $y, + ); + } + + return $points; } abstract protected function newArguments(); @@ -73,96 +129,26 @@ abstract class PhabricatorChartFunction return $this->argumentParser; } + abstract public function evaluateFunction(array $xv); + + public function getDomain() { + return null; + } + + public function newInputValues(PhabricatorChartDataQuery $query) { + return null; + } + public function loadData() { return; } - protected function setSourceFunction(PhabricatorChartFunction $source) { - $this->sourceFunction = $source; - return $this; - } - - protected function getSourceFunction() { - return $this->sourceFunction; - } - - final public function setXAxis(PhabricatorChartAxis $x_axis) { - $this->xAxis = $x_axis; - return $this; - } - - final public function getXAxis() { - return $this->xAxis; - } - - final public function setYAxis(PhabricatorChartAxis $y_axis) { - $this->yAxis = $y_axis; - return $this; - } - - final public function getYAxis() { - return $this->yAxis; - } - - protected function canEvaluateFunction() { - return false; - } - - protected function evaluateFunction($x) { - throw new PhutilMethodNotImplementedException(); - } - - public function hasDomain() { - if ($this->canEvaluateFunction()) { - return false; - } - - throw new PhutilMethodNotImplementedException(); - } - - public function getDatapoints(PhabricatorChartDataQuery $query) { - if ($this->canEvaluateFunction()) { - $points = $this->newSourceDatapoints($query); - foreach ($points as $key => $point) { - $y = $point['y']; - $y = $this->evaluateFunction($y); - $points[$key]['y'] = $y; - } - - return $points; - } - - return $this->newDatapoints($query); - } - - protected function newDatapoints(PhabricatorChartDataQuery $query) { - throw new PhutilMethodNotImplementedException(); - } - - protected function newSourceDatapoints(PhabricatorChartDataQuery $query) { - $source = $this->getSourceFunction(); - if ($source) { - return $source->getDatapoints($query); - } - - return $this->newDefaultDatapoints($query); - } - - protected function newDefaultDatapoints(PhabricatorChartDataQuery $query) { + protected function newDefaultInputValues(PhabricatorChartDataQuery $query) { $x_min = $query->getMinimumValue(); $x_max = $query->getMaximumValue(); $limit = $query->getLimit(); - $points = array(); - $steps = $this->newLinearSteps($x_min, $x_max, $limit); - foreach ($steps as $step) { - $points[] = array( - 'x' => $step, - 'y' => $step, - ); - } - - return $points; + return $this->newLinearSteps($x_min, $x_max, $limit); } protected function newLinearSteps($src, $dst, $count) { @@ -213,5 +199,4 @@ abstract class PhabricatorChartFunction return $steps; } - } diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php index baa4ee14d0..bbcf8209ba 100644 --- a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php @@ -5,7 +5,7 @@ final class PhabricatorChartFunctionArgument private $name; private $type; - private $isSourceFunction; + private $repeatable; public function setName($name) { $this->name = $name; @@ -16,6 +16,15 @@ final class PhabricatorChartFunctionArgument return $this->name; } + public function setRepeatable($repeatable) { + $this->repeatable = $repeatable; + return $this; + } + + public function getRepeatable() { + return $this->repeatable; + } + public function setType($type) { $types = array( 'fact-key' => true, @@ -40,15 +49,6 @@ final class PhabricatorChartFunctionArgument return $this->type; } - public function setIsSourceFunction($is_source_function) { - $this->isSourceFunction = $is_source_function; - return $this; - } - - public function getIsSourceFunction() { - return $this->isSourceFunction; - } - public function newValue($value) { switch ($this->getType()) { case 'fact-key': diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php index 281cb88f4d..04342ed4cc 100644 --- a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php @@ -11,6 +11,7 @@ final class PhabricatorChartFunctionArgumentParser private $argumentMap = array(); private $argumentPosition = 0; private $argumentValues = array(); + private $repeatableArgument = null; public function setFunction(PhabricatorChartFunction $function) { $this->function = $function; @@ -55,6 +56,32 @@ final class PhabricatorChartFunctionArgumentParser $name)); } + if ($this->repeatableArgument) { + if ($spec->getRepeatable()) { + throw new Exception( + pht( + 'Chart function "%s" emitted multiple repeatable argument '. + 'specifications ("%s" and "%s"). Only one argument may be '. + 'repeatable and it must be the last argument.', + $this->getFunctionArgumentSignature(), + $name, + $this->repeatableArgument->getName())); + } else { + throw new Exception( + pht( + 'Chart function "%s" emitted a repeatable argument ("%s"), then '. + 'another argument ("%s"). No arguments are permitted after a '. + 'repeatable argument.', + $this->getFunctionArgumentSignature(), + $this->repeatableArgument->getName(), + $name)); + } + } + + if ($spec->getRepeatable()) { + $this->repeatableArgument = $spec; + } + $this->argumentMap[$name] = $spec; $this->unparsedArguments[] = $spec; @@ -72,12 +99,26 @@ final class PhabricatorChartFunctionArgumentParser return $this; } + public function getAllArguments() { + return array_values($this->argumentMap); + } + public function parseArguments() { $have_count = count($this->rawArguments); $want_count = count($this->argumentMap); if ($this->haveAllArguments) { - if ($want_count !== $have_count) { + if ($this->repeatableArgument) { + if ($want_count > $have_count) { + throw new Exception( + pht( + 'Function "%s" expects %s or more argument(s), but only %s '. + 'argument(s) were provided.', + $this->getFunctionArgumentSignature(), + $want_count, + $have_count)); + } + } else if ($want_count !== $have_count) { throw new Exception( pht( 'Function "%s" expects %s argument(s), but %s argument(s) were '. @@ -105,6 +146,14 @@ final class PhabricatorChartFunctionArgumentParser $raw_argument = array_shift($this->unconsumedArguments); $this->argumentPosition++; + $is_repeatable = $argument->getRepeatable(); + + // If this argument is repeatable and we have more arguments, add it + // back to the end of the list so we can continue parsing. + if ($is_repeatable && $this->unconsumedArguments) { + $this->unparsedArguments[] = $argument; + } + try { $value = $argument->newValue($raw_argument); } catch (Exception $ex) { @@ -118,7 +167,14 @@ final class PhabricatorChartFunctionArgumentParser $ex->getMessage())); } - $this->argumentValues[$name] = $value; + if ($is_repeatable) { + if (!isset($this->argumentValues[$name])) { + $this->argumentValues[$name] = array(); + } + $this->argumentValues[$name][] = $value; + } else { + $this->argumentValues[$name] = $value; + } } } @@ -141,7 +197,7 @@ final class PhabricatorChartFunctionArgumentParser $argument_list[] = $key; } - if (!$this->haveAllArguments) { + if (!$this->haveAllArguments || $this->repeatableArgument) { $argument_list[] = '...'; } @@ -151,43 +207,4 @@ final class PhabricatorChartFunctionArgumentParser implode(', ', $argument_list)); } - public function getSourceFunctionArgument() { - $required_type = 'function'; - - $sources = array(); - foreach ($this->argumentMap as $key => $argument) { - if (!$argument->getIsSourceFunction()) { - continue; - } - - if ($argument->getType() !== $required_type) { - throw new Exception( - pht( - 'Function "%s" defines an argument "%s" which is marked as a '. - 'source function, but the type of this argument is not "%s".', - $this->getFunctionArgumentSignature(), - $argument->getName(), - $required_type)); - } - - $sources[$key] = $argument; - } - - if (!$sources) { - return null; - } - - if (count($sources) > 1) { - throw new Exception( - pht( - 'Function "%s" defines more than one argument as a source '. - 'function (arguments: %s). Functions must have zero or one '. - 'source function.', - $this->getFunctionArgumentSignature(), - implode(', ', array_keys($sources)))); - } - - return head($sources); - } - } diff --git a/src/applications/fact/chart/PhabricatorComposeChartFunction.php b/src/applications/fact/chart/PhabricatorComposeChartFunction.php new file mode 100644 index 0000000000..f6148ceae9 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorComposeChartFunction.php @@ -0,0 +1,73 @@ +newArgument() + ->setName('f') + ->setType('function') + ->setRepeatable(true), + ); + } + + public function evaluateFunction(array $xv) { + $original_positions = array_keys($xv); + $remaining_positions = $original_positions; + foreach ($this->getFunctionArguments() as $function) { + $xv = $function->evaluateFunction($xv); + + // If a function evaluates to "null" at some positions, we want to return + // "null" at those positions and stop evaluating the function. + + // We also want to pass "evaluateFunction()" a natural list containing + // only values it should evaluate: keys should not be important and we + // should not pass "null". This simplifies implementation of functions. + + // To do this, first create a map from original input positions to + // function return values. + $xv = array_combine($remaining_positions, $xv); + + // If a function evaluated to "null" at any position where we evaluated + // it, the result will be "null". We remove the position from the + // vector so we stop evaluating it. + foreach ($xv as $x => $y) { + if ($y !== null) { + continue; + } + + unset($xv[$x]); + } + + // Store the remaining original input positions for the next round, then + // throw away the array keys so we're passing the next function a natural + // list with only non-"null" values. + $remaining_positions = array_keys($xv); + $xv = array_values($xv); + + // If we have no more inputs to evaluate, we can bail out early rather + // than passing empty vectors to functions for evaluation. + if (!$xv) { + break; + } + } + + + $yv = array(); + $xv = array_combine($remaining_positions, $xv); + foreach ($original_positions as $position) { + if (isset($xv[$position])) { + $y = $xv[$position]; + } else { + $y = null; + } + $yv[$position] = $y; + } + + return $yv; + } + +} diff --git a/src/applications/fact/chart/PhabricatorConstantChartFunction.php b/src/applications/fact/chart/PhabricatorConstantChartFunction.php index 6ce9f1942d..cdc6c9494a 100644 --- a/src/applications/fact/chart/PhabricatorConstantChartFunction.php +++ b/src/applications/fact/chart/PhabricatorConstantChartFunction.php @@ -13,12 +13,16 @@ final class PhabricatorConstantChartFunction ); } - protected function canEvaluateFunction() { - return true; - } + public function evaluateFunction(array $xv) { + $n = $this->getArgument('n'); - protected function evaluateFunction($x) { - return $this->getArgument('n'); + $yv = array(); + + foreach ($xv as $x) { + $yv[] = $n; + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorCosChartFunction.php b/src/applications/fact/chart/PhabricatorCosChartFunction.php index 213124c3d1..04b8041fdb 100644 --- a/src/applications/fact/chart/PhabricatorCosChartFunction.php +++ b/src/applications/fact/chart/PhabricatorCosChartFunction.php @@ -6,20 +6,17 @@ final class PhabricatorCosChartFunction const FUNCTIONKEY = 'cos'; protected function newArguments() { - return array( - $this->newArgument() - ->setName('x') - ->setType('function') - ->setIsSourceFunction(true), - ); + return array(); } - protected function canEvaluateFunction() { - return true; - } + public function evaluateFunction(array $xv) { + $yv = array(); - protected function evaluateFunction($x) { - return cos(deg2rad($x)); + foreach ($xv as $x) { + $yv[] = cos(deg2rad($x)); + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php index 2f28b22335..ea59d3459e 100644 --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -6,7 +6,7 @@ final class PhabricatorFactChartFunction const FUNCTIONKEY = 'fact'; private $fact; - private $datapoints; + private $map; protected function newArguments() { $key_argument = $this->newArgument() @@ -44,73 +44,46 @@ final class PhabricatorFactChartFunction return; } - $points = array(); + $map = array(); + foreach ($data as $row) { + $value = (int)$row['value']; + $epoch = (int)$row['epoch']; - $sum = 0; - foreach ($data as $key => $row) { - $sum += (int)$row['value']; - $points[] = array( - 'x' => (int)$row['epoch'], - 'y' => $sum, - ); - } - - $this->datapoints = $points; - } - - public function getDatapoints(PhabricatorChartDataQuery $query) { - $points = $this->datapoints; - if (!$points) { - return array(); - } - - $x_min = $query->getMinimumValue(); - $x_max = $query->getMaximumValue(); - $limit = $query->getLimit(); - - if ($x_min !== null) { - foreach ($points as $key => $point) { - if ($point['x'] < $x_min) { - unset($points[$key]); - } + if (!isset($map[$epoch])) { + $map[$epoch] = 0; } + + $map[$epoch] += $value; } - if ($x_max !== null) { - foreach ($points as $key => $point) { - if ($point['x'] > $x_max) { - unset($points[$key]); - } - } - } - - // If we have too many data points, throw away some of the data. - if ($limit !== null) { - $count = count($points); - if ($count > $limit) { - $ii = 0; - $every = ceil($count / $limit); - foreach ($points as $key => $point) { - $ii++; - if (($ii % $every) && ($ii != $count)) { - unset($points[$key]); - } - } - } - } - - return $points; - } - - public function hasDomain() { - return true; + $this->map = $map; } public function getDomain() { - // TODO: We can examine the data to fit a better domain. + return array( + head_key($this->map), + last_key($this->map), + ); + } - $now = PhabricatorTime::getNow(); - return array($now - phutil_units('90 days in seconds'), $now); + public function newInputValues(PhabricatorChartDataQuery $query) { + return array_keys($this->map); + } + + public function evaluateFunction(array $xv) { + $map = $this->map; + + $yv = array(); + + foreach ($xv as $x) { + if (isset($map[$x])) { + $yv[] = $map[$x]; + } else { + $yv[] = null; + } + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php new file mode 100644 index 0000000000..519e602a80 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php @@ -0,0 +1,56 @@ +getFunctionArguments() as $function) { + $domain = $function->getDomain(); + if ($domain !== null) { + list($min, $max) = $domain; + $minv[] = $min; + $maxv[] = $max; + } + } + + if (!$minv && !$maxv) { + return null; + } + + $min = null; + $max = null; + + if ($minv) { + $min = min($minv); + } + + if ($maxv) { + $max = max($maxv); + } + + return array($min, $max); + } + + public function newInputValues(PhabricatorChartDataQuery $query) { + $map = array(); + foreach ($this->getFunctionArguments() as $function) { + $xv = $function->newInputValues($query); + if ($xv !== null) { + foreach ($xv as $x) { + $map[$x] = true; + } + } + } + + if (!$map) { + return null; + } + + ksort($map); + + return array_keys($map); + } + +} diff --git a/src/applications/fact/chart/PhabricatorScaleChartFunction.php b/src/applications/fact/chart/PhabricatorScaleChartFunction.php index 78540a6844..0fdcd4d64d 100644 --- a/src/applications/fact/chart/PhabricatorScaleChartFunction.php +++ b/src/applications/fact/chart/PhabricatorScaleChartFunction.php @@ -7,22 +7,22 @@ final class PhabricatorScaleChartFunction protected function newArguments() { return array( - $this->newArgument() - ->setName('x') - ->setType('function') - ->setIsSourceFunction(true), $this->newArgument() ->setName('scale') ->setType('number'), ); } - protected function canEvaluateFunction() { - return true; - } + public function evaluateFunction(array $xv) { + $scale = $this->getArgument('scale'); - protected function evaluateFunction($x) { - return $x * $this->getArgument('scale'); + $yv = array(); + + foreach ($xv as $x) { + $yv[] = $x * $scale; + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorShiftChartFunction.php b/src/applications/fact/chart/PhabricatorShiftChartFunction.php index 52f33d26b5..8b53d34277 100644 --- a/src/applications/fact/chart/PhabricatorShiftChartFunction.php +++ b/src/applications/fact/chart/PhabricatorShiftChartFunction.php @@ -7,22 +7,22 @@ final class PhabricatorShiftChartFunction protected function newArguments() { return array( - $this->newArgument() - ->setName('x') - ->setType('function') - ->setIsSourceFunction(true), $this->newArgument() ->setName('shift') ->setType('number'), ); } - protected function canEvaluateFunction() { - return true; - } + public function evaluateFunction(array $xv) { + $shift = $this->getArgument('shift'); - protected function evaluateFunction($x) { - return $x * $this->getArgument('shift'); + $yv = array(); + + foreach ($xv as $x) { + $yv[] = $x + $shift; + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorSinChartFunction.php b/src/applications/fact/chart/PhabricatorSinChartFunction.php index 1ac557f868..26a37bfd82 100644 --- a/src/applications/fact/chart/PhabricatorSinChartFunction.php +++ b/src/applications/fact/chart/PhabricatorSinChartFunction.php @@ -6,20 +6,17 @@ final class PhabricatorSinChartFunction const FUNCTIONKEY = 'sin'; protected function newArguments() { - return array( - $this->newArgument() - ->setName('x') - ->setType('function') - ->setIsSourceFunction(true), - ); + return array(); } - protected function canEvaluateFunction() { - return true; - } + public function evaluateFunction(array $xv) { + $yv = array(); - protected function evaluateFunction($x) { - return sin(deg2rad($x)); + foreach ($xv as $x) { + $yv[] = sin(deg2rad($x)); + } + + return $yv; } } diff --git a/src/applications/fact/chart/PhabricatorSumChartFunction.php b/src/applications/fact/chart/PhabricatorSumChartFunction.php new file mode 100644 index 0000000000..88d12eba62 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorSumChartFunction.php @@ -0,0 +1,40 @@ +newArgument() + ->setName('f') + ->setType('function') + ->setRepeatable(true), + ); + } + + public function evaluateFunction(array $xv) { + $fv = array(); + foreach ($this->getFunctionArguments() as $function) { + $fv[] = $function->evaluateFunction($xv); + } + + $n = count($xv); + $yv = array_fill(0, $n, null); + + foreach ($fv as $f) { + for ($ii = 0; $ii < $n; $ii++) { + if ($f[$ii] !== null) { + if (!isset($yv[$ii])) { + $yv[$ii] = 0; + } + $yv[$ii] += $f[$ii]; + } + } + } + + return $yv; + } + +} diff --git a/src/applications/fact/chart/PhabricatorXChartFunction.php b/src/applications/fact/chart/PhabricatorXChartFunction.php deleted file mode 100644 index b2b7ab36ed..0000000000 --- a/src/applications/fact/chart/PhabricatorXChartFunction.php +++ /dev/null @@ -1,20 +0,0 @@ -setArguments(array($argv)); + } - $functions[] = id(new PhabricatorFactChartFunction()) - ->setArguments(array('tasks.count.create')); + $subfunctions = array(); + foreach ($functions as $function) { + foreach ($function->getSubfunctions() as $subfunction) { + $subfunctions[] = $subfunction; + } + } - $functions[] = id(new PhabricatorFactChartFunction()) - ->setArguments(array('tasks.open-count.create')); - - $x_function = id(new PhabricatorXChartFunction()) - ->setArguments(array()); - - $functions[] = id(new PhabricatorConstantChartFunction()) - ->setArguments(array(360)); - - $functions[] = id(new PhabricatorSinChartFunction()) - ->setArguments(array($x_function)); - - $cos_function = id(new PhabricatorCosChartFunction()) - ->setArguments(array($x_function)); - - $functions[] = id(new PhabricatorShiftChartFunction()) - ->setArguments( - array( - array( - 'scale', - array( - 'cos', - array( - 'scale', - array('x'), - 0.001, - ), - ), - 10, - ), - 200, - )); + foreach ($subfunctions as $subfunction) { + $subfunction->loadData(); + } list($domain_min, $domain_max) = $this->getDomain($functions); @@ -63,11 +70,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { $datasets = array(); foreach ($functions as $function) { - $function->setXAxis($axis); - - $function->loadData(); - - $points = $function->getDatapoints($data_query); + $points = $function->newDatapoints($data_query); $x = array(); $y = array(); @@ -157,19 +160,18 @@ final class PhabricatorFactChartController extends PhabricatorFactController { private function getDomain(array $functions) { $domain_min_list = null; $domain_max_list = null; + foreach ($functions as $function) { - if ($function->hasDomain()) { - $domain = $function->getDomain(); + $domain = $function->getDomain(); - list($domain_min, $domain_max) = $domain; + list($function_min, $function_max) = $domain; - if ($domain_min !== null) { - $domain_min_list[] = $domain_min; - } + if ($function_min !== null) { + $domain_min_list[] = $function_min; + } - if ($domain_max !== null) { - $domain_max_list[] = $domain_max; - } + if ($function_max !== null) { + $domain_max_list[] = $function_max; } } From b8f6248e0782cfda3e3522a61cf13107f5d2cb3b Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 19 May 2019 15:55:59 -0700 Subject: [PATCH 04/42] Fix an issue where handles could load with the incorrect viewer when building mail about changes to related objects Summary: See . Some time ago, all handle rendering preloaded handles: things emitted a list of PHIDs they'd need handles for, then later used only those PHIDs. Later, we introduced `HandlePool` and lazy/on-demand handle loading. Modern transactions mostly use this to render object PHIDs. When we build mail, many newer transactions use an on-demand load to fetch handles to render transactions. This on-demand load may use the original viewer (the acting user) instead of the correct viewer (the mail recipient): we fetch and reset handles using the correct viewer, but do not overwrite the active viewer for on-demand loading. This could cause mail to leak the titles of related objects to users who don't have permission to see them. Instead, just reload the transactions with the correct viewer when building mail instead of playing a bunch of `setViewer()` and `clone` games. Until we're 100% on modular transactions, several pieces of the stack cache viewer or state information. Test Plan: - Created task A (public) with subtask B (private). - Closed subtask B as a user with access to it. - Viewed mail sent to subscribers of task A who can not see subtask B. - Before change: mail discloses title of subtask B. - After change: mail properly labels subtask B as "Restricted Task". Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20525 --- ...habricatorApplicationTransactionEditor.php | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c9d8a0d998..c9c2c79237 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3048,6 +3048,8 @@ abstract class PhabricatorApplicationTransactionEditor // Set this explicitly before we start swapping out the effective actor. $this->setActingAsPHID($this->getActingAsPHID()); + $xaction_phids = mpull($xactions, 'getPHID'); + $messages = array(); foreach ($targets as $target) { $original_actor = $this->getActor(); @@ -3059,10 +3061,25 @@ abstract class PhabricatorApplicationTransactionEditor $caught = null; $mail = null; try { - // Reload handles for the new viewer. - $this->loadHandles($xactions); + // Reload the transactions for the current viewer. + if ($xaction_phids) { + $query = PhabricatorApplicationTransactionQuery::newQueryForObject( + $object); - $mail = $this->buildMailForTarget($object, $xactions, $target); + $mail_xactions = $query + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->withPHIDs($xaction_phids) + ->execute(); + } else { + $mail_xactions = array(); + } + + // Reload handles for the current viewer. This covers older code which + // emits a list of handle PHIDs upfront. + $this->loadHandles($mail_xactions); + + $mail = $this->buildMailForTarget($object, $mail_xactions, $target); if ($mail) { if ($this->mustEncrypt) { From 33688c8a41ebb2126e3fb2cb7f24700b0bf305b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 May 2019 09:57:31 -0700 Subject: [PATCH 05/42] When an object is referenced by URI, treat it like a mention Summary: Ref T13291. Currently, `T123` is a mention and adds an "alice mentioned this on Txxx." to `T123`, but `https://install.com/T123` is not a mention. Make the full URI a mention. Test Plan: Commented a full URI, saw the target object get a mention story in its timeline. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20527 --- .../PhabricatorSelfHyperlinkEngineExtension.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/applications/meta/engineextension/PhabricatorSelfHyperlinkEngineExtension.php b/src/applications/meta/engineextension/PhabricatorSelfHyperlinkEngineExtension.php index 44055df3c4..0bc9228d44 100644 --- a/src/applications/meta/engineextension/PhabricatorSelfHyperlinkEngineExtension.php +++ b/src/applications/meta/engineextension/PhabricatorSelfHyperlinkEngineExtension.php @@ -53,11 +53,13 @@ final class PhabricatorSelfHyperlinkEngineExtension } if ($object_map) { - $handles = $viewer->loadHandles(mpull($object_map, 'getPHID')); + $object_phids = mpull($object_map, 'getPHID'); } else { - $handles = array(); + $object_phids = array(); } + $handles = $viewer->loadHandles($object_phids); + foreach ($object_names as $key => $object_name) { $object = idx($object_map, $object_name); if (!$object) { @@ -83,6 +85,13 @@ final class PhabricatorSelfHyperlinkEngineExtension unset($self_links[$key]); } + + $key_mentioned = PhabricatorObjectRemarkupRule::KEY_MENTIONED_OBJECTS; + $mentioned_phids = $engine->getTextMetadata($key_mentioned, array()); + foreach ($object_phids as $object_phid) { + $mentioned_phids[$object_phid] = $object_phid; + } + $engine->setTextMetadata($key_mentioned, $mentioned_phids); } } From f4201593808dadeaffc7e634e55a67ef156e0dce Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 May 2019 10:33:55 -0700 Subject: [PATCH 06/42] Implement Asana and JIRA external links via HyperlinkEngineExtension, not separate Remarkup rules Summary: Depends on D20527. Ref T13291. Now that we have more flexible support for URI rewriting, use it for Doorkeeper URIs. These are used when you set up Asana or JIRA and include the URI to an Asana task or a JIRA issue in a comment. Test Plan: - Linked up to Asana and JIRA. - Put Asana and JIRA URIs in comments. - Saw the UI update to pull task titles from Asana / JIRA using my OAuth credentials. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20528 --- src/__phutil_library_map__.php | 21 ++-- .../provider/PhabricatorAsanaAuthProvider.php | 26 ++++- .../provider/PhabricatorJIRAAuthProvider.php | 37 ++++++- .../PhabricatorDoorkeeperApplication.php | 7 -- .../doorkeeper/engine/DoorkeeperURIRef.php | 91 ++++++++++++++++ .../DoorkeeperHyperlinkEngineExtension.php | 92 ++++++++++++++++ .../DoorkeeperRemarkupURIInterface.php | 7 ++ .../remarkup/DoorkeeperAsanaRemarkupRule.php | 31 ------ .../remarkup/DoorkeeperJIRARemarkupRule.php | 44 -------- .../remarkup/DoorkeeperRemarkupRule.php | 103 ------------------ 10 files changed, 260 insertions(+), 199 deletions(-) create mode 100644 src/applications/doorkeeper/engine/DoorkeeperURIRef.php create mode 100644 src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php create mode 100644 src/applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php delete mode 100644 src/applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php delete mode 100644 src/applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php delete mode 100644 src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e920e92b8d..6b5f07eb8c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1072,7 +1072,6 @@ phutil_register_library_map(array( 'DivinerSymbolRemarkupRule' => 'applications/diviner/markup/DivinerSymbolRemarkupRule.php', 'DivinerWorkflow' => 'applications/diviner/workflow/DivinerWorkflow.php', 'DoorkeeperAsanaFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperAsanaFeedWorker.php', - 'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php', 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', @@ -1088,15 +1087,16 @@ phutil_register_library_map(array( 'DoorkeeperExternalObjectQuery' => 'applications/doorkeeper/query/DoorkeeperExternalObjectQuery.php', 'DoorkeeperFeedStoryPublisher' => 'applications/doorkeeper/engine/DoorkeeperFeedStoryPublisher.php', 'DoorkeeperFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperFeedWorker.php', + 'DoorkeeperHyperlinkEngineExtension' => 'applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php', 'DoorkeeperImportEngine' => 'applications/doorkeeper/engine/DoorkeeperImportEngine.php', 'DoorkeeperJIRAFeedWorker' => 'applications/doorkeeper/worker/DoorkeeperJIRAFeedWorker.php', - 'DoorkeeperJIRARemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php', 'DoorkeeperMissingLinkException' => 'applications/doorkeeper/exception/DoorkeeperMissingLinkException.php', 'DoorkeeperObjectRef' => 'applications/doorkeeper/engine/DoorkeeperObjectRef.php', - 'DoorkeeperRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php', + 'DoorkeeperRemarkupURIInterface' => 'applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php', 'DoorkeeperSchemaSpec' => 'applications/doorkeeper/storage/DoorkeeperSchemaSpec.php', 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', + 'DoorkeeperURIRef' => 'applications/doorkeeper/engine/DoorkeeperURIRef.php', 'DrydockAcquiredBrokenResourceException' => 'applications/drydock/exception/DrydockAcquiredBrokenResourceException.php', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', @@ -6761,7 +6761,6 @@ phutil_register_library_map(array( 'DivinerSymbolRemarkupRule' => 'PhutilRemarkupRule', 'DivinerWorkflow' => 'PhabricatorManagementWorkflow', 'DoorkeeperAsanaFeedWorker' => 'DoorkeeperFeedWorker', - 'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperBridge' => 'Phobject', 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', @@ -6779,15 +6778,15 @@ phutil_register_library_map(array( 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DoorkeeperFeedStoryPublisher' => 'Phobject', 'DoorkeeperFeedWorker' => 'FeedPushWorker', + 'DoorkeeperHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker', - 'DoorkeeperJIRARemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperMissingLinkException' => 'Exception', 'DoorkeeperObjectRef' => 'Phobject', - 'DoorkeeperRemarkupRule' => 'PhutilRemarkupRule', 'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', + 'DoorkeeperURIRef' => 'Phobject', 'DrydockAcquiredBrokenResourceException' => 'Exception', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', @@ -8090,7 +8089,10 @@ phutil_register_library_map(array( 'PhabricatorApplicationsController' => 'PhabricatorController', 'PhabricatorApplicationsListController' => 'PhabricatorApplicationsController', 'PhabricatorApplyEditField' => 'PhabricatorEditField', - 'PhabricatorAsanaAuthProvider' => 'PhabricatorOAuth2AuthProvider', + 'PhabricatorAsanaAuthProvider' => array( + 'PhabricatorOAuth2AuthProvider', + 'DoorkeeperRemarkupURIInterface', + ), 'PhabricatorAsanaConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAsanaSubtaskHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorAsanaTaskHasObjectEdgeType' => 'PhabricatorEdgeType', @@ -9569,7 +9571,10 @@ phutil_register_library_map(array( 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', - 'PhabricatorJIRAAuthProvider' => 'PhabricatorOAuth1AuthProvider', + 'PhabricatorJIRAAuthProvider' => array( + 'PhabricatorOAuth1AuthProvider', + 'DoorkeeperRemarkupURIInterface', + ), 'PhabricatorJSONConfigType' => 'PhabricatorTextConfigType', 'PhabricatorJSONDocumentEngine' => 'PhabricatorTextDocumentEngine', 'PhabricatorJSONExportFormat' => 'PhabricatorExportFormat', diff --git a/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php b/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php index 711d3d57c2..8a0fccec28 100644 --- a/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAsanaAuthProvider.php @@ -1,6 +1,8 @@ setURI($uri) + ->setApplicationType(DoorkeeperBridgeAsana::APPTYPE_ASANA) + ->setApplicationDomain(DoorkeeperBridgeAsana::APPDOMAIN_ASANA) + ->setObjectType(DoorkeeperBridgeAsana::OBJTYPE_TASK) + ->setObjectID($task_id); + } + } diff --git a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php index 4b9d53db85..23f7e7f706 100644 --- a/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorJIRAAuthProvider.php @@ -1,10 +1,8 @@ getProviderConfig()->getProperty(self::PROPERTY_JIRA_URI); - } +final class PhabricatorJIRAAuthProvider + extends PhabricatorOAuth1AuthProvider + implements DoorkeeperRemarkupURIInterface { public function getProviderName() { return pht('JIRA'); @@ -332,4 +330,33 @@ final class PhabricatorJIRAAuthProvider extends PhabricatorOAuth1AuthProvider { return $config->getProperty(self::PROPERTY_REPORT_COMMENT, true); } +/* -( DoorkeeperRemarkupURIInterface )------------------------------------- */ + + public function getDoorkeeperURIRef(PhutilURI $uri) { + $uri_string = phutil_string_cast($uri); + + $pattern = '((https?://\S+?)/browse/([A-Z]+-[1-9]\d*))'; + $matches = null; + if (!preg_match($pattern, $uri_string, $matches)) { + return null; + } + + $domain = $matches[1]; + $issue = $matches[2]; + + $config = $this->getProviderConfig(); + $base_uri = $config->getProperty(self::PROPERTY_JIRA_URI); + + if ($domain !== rtrim($base_uri, '/')) { + return null; + } + + return id(new DoorkeeperURIRef()) + ->setURI($uri) + ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA) + ->setApplicationDomain($this->getProviderDomain()) + ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE) + ->setObjectID($issue); + } + } diff --git a/src/applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php b/src/applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php index 6342dd18b3..c0bb28048c 100644 --- a/src/applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php +++ b/src/applications/doorkeeper/application/PhabricatorDoorkeeperApplication.php @@ -22,13 +22,6 @@ final class PhabricatorDoorkeeperApplication extends PhabricatorApplication { return pht('Connect to Other Software'); } - public function getRemarkupRules() { - return array( - new DoorkeeperAsanaRemarkupRule(), - new DoorkeeperJIRARemarkupRule(), - ); - } - public function getRoutes() { return array( '/doorkeeper/' => array( diff --git a/src/applications/doorkeeper/engine/DoorkeeperURIRef.php b/src/applications/doorkeeper/engine/DoorkeeperURIRef.php new file mode 100644 index 0000000000..db9b5682c1 --- /dev/null +++ b/src/applications/doorkeeper/engine/DoorkeeperURIRef.php @@ -0,0 +1,91 @@ +uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setApplicationType($application_type) { + $this->applicationType = $application_type; + return $this; + } + + public function getApplicationType() { + return $this->applicationType; + } + + public function setApplicationDomain($application_domain) { + $this->applicationDomain = $application_domain; + return $this; + } + + public function getApplicationDomain() { + return $this->applicationDomain; + } + + public function setObjectType($object_type) { + $this->objectType = $object_type; + return $this; + } + + public function getObjectType() { + return $this->objectType; + } + + public function setObjectID($object_id) { + $this->objectID = $object_id; + return $this; + } + + public function getObjectID() { + return $this->objectID; + } + + public function setText($text) { + $this->text = $text; + return $this; + } + + public function getText() { + return $this->text; + } + + public function setDisplayMode($display_mode) { + $options = array( + self::DISPLAY_FULL => true, + self::DISPLAY_SHORT => true, + ); + + if (!isset($options[$display_mode])) { + throw new Exception( + pht( + 'DoorkeeperURIRef display mode "%s" is unknown.', + $display_mode)); + } + + $this->displayMode = $display_mode; + return $this; + } + + public function getDisplayMode() { + return $this->displayMode; + } + +} diff --git a/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php b/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php new file mode 100644 index 0000000000..fd8d02e8bb --- /dev/null +++ b/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php @@ -0,0 +1,92 @@ +getEngine(); + $viewer = $engine->getConfig('viewer'); + + if (!$viewer) { + return; + } + + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->withIsEnabled(true) + ->execute(); + + $providers = array(); + foreach ($configs as $key => $config) { + $provider = $config->getProvider(); + if (($provider instanceof DoorkeeperRemarkupURIInterface)) { + $providers[] = $provider; + } + } + + if (!$providers) { + return; + } + + $refs = array(); + foreach ($hyperlinks as $hyperlink) { + $uri = $hyperlink->getURI(); + $uri = new PhutilURI($uri); + + foreach ($providers as $provider) { + $ref = $provider->getDoorkeeperURIRef($uri); + + if (($ref !== null) && !($ref instanceof DoorkeeperURIRef)) { + throw new Exception( + pht( + 'Expected "getDoorkeeperURIRef()" to return "null" or an '. + 'object of type "DoorkeeperURIRef", but got %s from provider '. + '"%s".', + phutil_describe_type($ref), + get_class($provider))); + } + + if ($ref === null) { + continue; + } + + $tag_id = celerity_generate_unique_node_id(); + $href = phutil_string_cast($ref->getURI()); + + $refs[] = array( + 'id' => $tag_id, + 'href' => $href, + 'ref' => array( + $ref->getApplicationType(), + $ref->getApplicationDomain(), + $ref->getObjectType(), + $ref->getObjectID(), + ), + 'view' => $ref->getDisplayMode(), + ); + + $text = $ref->getText(); + if ($text === null) { + $text = $href; + } + + $view = id(new PHUITagView()) + ->setID($tag_id) + ->setName($text) + ->setHref($href) + ->setType(PHUITagView::TYPE_OBJECT) + ->setExternal(true); + + $hyperlink->setResult($view); + break; + } + } + + if ($refs) { + Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); + } + } + +} diff --git a/src/applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php b/src/applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php new file mode 100644 index 0000000000..f7ba998f6f --- /dev/null +++ b/src/applications/doorkeeper/interface/DoorkeeperRemarkupURIInterface.php @@ -0,0 +1,7 @@ +addDoorkeeperTag( - array( - 'href' => $matches[0], - 'tag' => array( - 'ref' => array( - DoorkeeperBridgeAsana::APPTYPE_ASANA, - DoorkeeperBridgeAsana::APPDOMAIN_ASANA, - DoorkeeperBridgeAsana::OBJTYPE_TASK, - $matches[2], - ), - 'extra' => array( - 'asana.context' => $matches[1], - ), - ), - )); - } - -} diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php b/src/applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php deleted file mode 100644 index ca690e789c..0000000000 --- a/src/applications/doorkeeper/remarkup/DoorkeeperJIRARemarkupRule.php +++ /dev/null @@ -1,44 +0,0 @@ -getJIRABaseURI(); - if ($match_domain != rtrim($jira_base, '/')) { - return $matches[0]; - } - - return $this->addDoorkeeperTag( - array( - 'href' => $matches[0], - 'tag' => array( - 'ref' => array( - DoorkeeperBridgeJIRA::APPTYPE_JIRA, - $provider->getProviderDomain(), - DoorkeeperBridgeJIRA::OBJTYPE_ISSUE, - $match_issue, - ), - ), - )); - } - - -} diff --git a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php b/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php deleted file mode 100644 index 9654d12bb8..0000000000 --- a/src/applications/doorkeeper/remarkup/DoorkeeperRemarkupRule.php +++ /dev/null @@ -1,103 +0,0 @@ - 'string', - 'tag' => 'map', - - 'name' => 'optional string', - 'view' => 'optional string', - )); - - $spec = $spec + array( - 'view' => self::VIEW_FULL, - ); - - $views = array( - self::VIEW_FULL, - self::VIEW_SHORT, - ); - $views = array_fuse($views); - if (!isset($views[$spec['view']])) { - throw new Exception( - pht( - 'Unsupported Doorkeeper tag view mode "%s". Supported modes are: %s.', - $spec['view'], - implode(', ', $views))); - } - - $key = self::KEY_TAGS; - $engine = $this->getEngine(); - $token = $engine->storeText(get_class($this)); - - $tags = $engine->getTextMetadata($key, array()); - - $tags[] = array( - 'token' => $token, - ) + $spec + array( - 'extra' => array(), - ); - - $engine->setTextMetadata($key, $tags); - return $token; - } - - public function didMarkupText() { - $key = self::KEY_TAGS; - $engine = $this->getEngine(); - $tags = $engine->getTextMetadata($key, array()); - - if (!$tags) { - return; - } - - $refs = array(); - foreach ($tags as $spec) { - $href = $spec['href']; - $name = idx($spec, 'name', $href); - - $this->assertFlatText($href); - $this->assertFlatText($name); - - if ($this->getEngine()->isTextMode()) { - $view = "{$name} <{$href}>"; - } else { - $tag_id = celerity_generate_unique_node_id(); - - $refs[] = array( - 'id' => $tag_id, - 'view' => $spec['view'], - ) + $spec['tag']; - - $view = id(new PHUITagView()) - ->setID($tag_id) - ->setName($name) - ->setHref($href) - ->setType(PHUITagView::TYPE_OBJECT) - ->setExternal(true); - } - - $engine->overwriteStoredText($spec['token'], $view); - } - - if ($refs) { - Javelin::initBehavior('doorkeeper-tag', array('tags' => $refs)); - } - - $engine->setTextMetadata($key, array()); - } - -} From 2f0e655a9b9e9543e02879cfcf007afa5e59b2b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 May 2019 11:40:05 -0700 Subject: [PATCH 07/42] Add a 15-second timeout to external service calls to fill Doorkeeper link tags Summary: Depends on D20528. Ref T13291. Ref T13285. Currently, we don't put a timeout on external service calls when enriching URIs for external Asana/JIRA tasks. Add a 15-second timeout so we'll do something reasonable-ish in the face of a downed service provider. Later, I plan to healthcheck Asana/JIRA providers in a generic way (see T13287) so we can stop making calls if they time out / fail too frequently. Test Plan: - Linked to JIRA and Asana tasks in comments. - Set timeout to 0.0001 seconds, saw requests time out. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291, T13285 Differential Revision: https://secure.phabricator.com/D20530 --- .../doorkeeper/bridge/DoorkeeperBridge.php | 10 +++++++++ .../bridge/DoorkeeperBridgeAsana.php | 5 +++++ .../bridge/DoorkeeperBridgeJIRA.php | 10 ++++++++- .../controller/DoorkeeperTagsController.php | 1 + .../engine/DoorkeeperImportEngine.php | 22 ++++++++++++++++--- 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridge.php b/src/applications/doorkeeper/bridge/DoorkeeperBridge.php index 4a4ee2667b..d25d48e857 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridge.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridge.php @@ -5,6 +5,16 @@ abstract class DoorkeeperBridge extends Phobject { private $viewer; private $context = array(); private $throwOnMissingLink; + private $timeout; + + public function setTimeout($timeout) { + $this->timeout = $timeout; + return $this; + } + + public function getTimeout() { + return $this->timeout; + } public function setThrowOnMissingLink($throw_on_missing_link) { $this->throwOnMissingLink = $throw_on_missing_link; diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php index ec604e158e..05ee786337 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php @@ -62,6 +62,11 @@ final class DoorkeeperBridgeAsana extends DoorkeeperBridge { $template = id(new PhutilAsanaFuture()) ->setAccessToken($token); + $timeout = $this->getTimeout(); + if ($timeout !== null) { + $template->setTimeout($timeout); + } + $futures = array(); foreach ($id_map as $key => $id) { $futures[$key] = id(clone $template) diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php index 920f2eeb91..f82bb1ba25 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php @@ -47,12 +47,20 @@ final class DoorkeeperBridgeJIRA extends DoorkeeperBridge { // (by querying all instances). For now, just query the one instance. $account = head($accounts); + $timeout = $this->getTimeout(); + $futures = array(); foreach ($id_map as $key => $id) { - $futures[$key] = $provider->newJIRAFuture( + $future = $provider->newJIRAFuture( $account, 'rest/api/2/issue/'.phutil_escape_uri($id), 'GET'); + + if ($timeout !== null) { + $future->setTimeout($timeout); + } + + $futures[$key] = $future; } $results = array(); diff --git a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php index 5a886cba3e..f4b4195f11 100644 --- a/src/applications/doorkeeper/controller/DoorkeeperTagsController.php +++ b/src/applications/doorkeeper/controller/DoorkeeperTagsController.php @@ -26,6 +26,7 @@ final class DoorkeeperTagsController extends PhabricatorController { $refs = id(new DoorkeeperImportEngine()) ->setViewer($viewer) ->setRefs($refs) + ->setTimeout(15) ->execute(); $results = array(); diff --git a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php index 8c58f5bd88..02642cb994 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php +++ b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php @@ -8,6 +8,7 @@ final class DoorkeeperImportEngine extends Phobject { private $localOnly; private $throwOnMissingLink; private $context = array(); + private $timeout; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -43,6 +44,15 @@ final class DoorkeeperImportEngine extends Phobject { return $this; } + public function setTimeout($timeout) { + $this->timeout = $timeout; + return $this; + } + + public function getTimeout() { + return $this->timeout; + } + /** * Configure behavior if remote refs can not be retrieved because an * authentication link is missing. @@ -98,10 +108,16 @@ final class DoorkeeperImportEngine extends Phobject { ->setFilterMethod('isEnabled') ->execute(); + $timeout = $this->getTimeout(); foreach ($bridges as $key => $bridge) { - $bridge->setViewer($viewer); - $bridge->setThrowOnMissingLink($this->throwOnMissingLink); - $bridge->setContext($this->context); + $bridge + ->setViewer($viewer) + ->setThrowOnMissingLink($this->throwOnMissingLink) + ->setContext($this->context); + + if ($timeout !== null) { + $bridge->setTimeout($timeout); + } } $working_set = $refs; From 5305ebddda9c7ff4d7156a8eb7aec1c1085eebcc Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 06:31:24 -0700 Subject: [PATCH 08/42] To improve wrapping behavior of rendered README files, don't use "PHUIDocumentView" in Diffusion Summary: See PHI1268. We currently do some weird width handling when rendering Diffusion readmes in a document directory view. I think this came from D12330, which used `PHUIDocumentViewPro` to change the font, but we later reverted the font and were left with the `DocumentView`. Other changes after that modified `DocumentView` to have fixed-width behavior, but it doesn't make much sense here since the content panel is clearly rendered full-width. Today, the `DocumentView` is a more structural element with methods like `setCurtain()`. Just get rid of it to simplify things, at least as a first step. Test Plan: Before: {F6463493} After: {F6463492} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20536 --- .../diffusion/view/DiffusionReadmeView.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/applications/diffusion/view/DiffusionReadmeView.php b/src/applications/diffusion/view/DiffusionReadmeView.php index 21e97f4718..2f65f5730f 100644 --- a/src/applications/diffusion/view/DiffusionReadmeView.php +++ b/src/applications/diffusion/view/DiffusionReadmeView.php @@ -75,7 +75,7 @@ final class DiffusionReadmeView extends DiffusionView { $engine = $markup_object->newMarkupEngine($markup_field); $readme_content = $content; - $class = null; + $class = 'ml'; break; case 'rainbow': $content = id(new PhutilRainbowSyntaxHighlighter()) @@ -93,10 +93,12 @@ final class DiffusionReadmeView extends DiffusionView { break; } - $readme_content = phutil_tag_div($class, $readme_content); - $document = id(new PHUIDocumentView()) - ->setFluid(true) - ->appendChild($readme_content); + $readme_content = phutil_tag( + 'div', + array( + 'class' => $class, + ), + $readme_content); $header = id(new PHUIHeaderView()) ->setHeader($readme_name) @@ -106,7 +108,7 @@ final class DiffusionReadmeView extends DiffusionView { ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addClass('diffusion-mobile-view') - ->appendChild($document) + ->appendChild($readme_content) ->addClass('diffusion-readme-view'); } From 642113708a83429f0928e04fc15875e54b148210 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 20 May 2019 14:02:38 -0700 Subject: [PATCH 09/42] Build a rough transaction-level view of Feed Summary: Ref T13294. An install is interested in a way to easily answer audit-focused questions like "what edits were made to any Herald rule in Q1 2019?". We can answer this kind of question with a more granular version of feed that focuses on being exhaustive rather than being human-readable. This starts a rough version of it and deals with the two major tricky pieces: transactions are in a lot of different tables; and paging across them is not trivial. To solve "lots of tables", we just query every table. There's a little bit of sleight-of-hand to get this working, but nothing too awful. To solve "paging is hard", we order by "". The "phid" part of this order doesn't have much meaning, but it lets us put every transaction in a single, stable, global order and identify a place in that ordering given only one transaction PHID. Test Plan: {F6463076} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13294 Differential Revision: https://secure.phabricator.com/D20531 --- src/__phutil_library_map__.php | 6 + .../PhabricatorFeedApplication.php | 4 + .../controller/PhabricatorFeedController.php | 24 +-- .../PhabricatorFeedListController.php | 21 ++- ...abricatorFeedTransactionListController.php | 16 ++ .../query/PhabricatorFeedTransactionQuery.php | 178 ++++++++++++++++++ ...PhabricatorFeedTransactionSearchEngine.php | 113 +++++++++++ ...PhabricatorApplicationTransactionQuery.php | 73 +++++++ .../PhabricatorApplicationTransaction.php | 6 + ...PhabricatorCursorPagedPolicyAwareQuery.php | 9 +- 10 files changed, 416 insertions(+), 34 deletions(-) create mode 100644 src/applications/feed/controller/PhabricatorFeedTransactionListController.php create mode 100644 src/applications/feed/query/PhabricatorFeedTransactionQuery.php create mode 100644 src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6b5f07eb8c..dc331be164 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3265,6 +3265,9 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php', 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', + 'PhabricatorFeedTransactionListController' => 'applications/feed/controller/PhabricatorFeedTransactionListController.php', + 'PhabricatorFeedTransactionQuery' => 'applications/feed/query/PhabricatorFeedTransactionQuery.php', + 'PhabricatorFeedTransactionSearchEngine' => 'applications/feed/query/PhabricatorFeedTransactionSearchEngine.php', 'PhabricatorFerretEngine' => 'applications/search/ferret/PhabricatorFerretEngine.php', 'PhabricatorFerretEngineTestCase' => 'applications/search/ferret/__tests__/PhabricatorFerretEngineTestCase.php', 'PhabricatorFerretFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorFerretFulltextEngineExtension.php', @@ -9330,6 +9333,9 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryPublisher' => 'Phobject', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', + 'PhabricatorFeedTransactionListController' => 'PhabricatorFeedController', + 'PhabricatorFeedTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorFeedTransactionSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFerretEngine' => 'Phobject', 'PhabricatorFerretEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorFerretFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', diff --git a/src/applications/feed/application/PhabricatorFeedApplication.php b/src/applications/feed/application/PhabricatorFeedApplication.php index 287cf2d387..7d267394e9 100644 --- a/src/applications/feed/application/PhabricatorFeedApplication.php +++ b/src/applications/feed/application/PhabricatorFeedApplication.php @@ -31,6 +31,10 @@ final class PhabricatorFeedApplication extends PhabricatorApplication { '/feed/' => array( '(?P\d+)/' => 'PhabricatorFeedDetailController', '(?:query/(?P[^/]+)/)?' => 'PhabricatorFeedListController', + 'transactions/' => array( + $this->getQueryRoutePattern() + => 'PhabricatorFeedTransactionListController', + ), ), ); } diff --git a/src/applications/feed/controller/PhabricatorFeedController.php b/src/applications/feed/controller/PhabricatorFeedController.php index 6e4d353518..cb7059a554 100644 --- a/src/applications/feed/controller/PhabricatorFeedController.php +++ b/src/applications/feed/controller/PhabricatorFeedController.php @@ -1,24 +1,4 @@ getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhabricatorFeedSearchEngine()) - ->setViewer($user) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; - } - - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } - -} +abstract class PhabricatorFeedController + extends PhabricatorController {} diff --git a/src/applications/feed/controller/PhabricatorFeedListController.php b/src/applications/feed/controller/PhabricatorFeedListController.php index 8451592362..2acc06689e 100644 --- a/src/applications/feed/controller/PhabricatorFeedListController.php +++ b/src/applications/feed/controller/PhabricatorFeedListController.php @@ -1,20 +1,27 @@ getURIData('queryKey'); + $navigation = array(); - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine(new PhabricatorFeedSearchEngine()) - ->setNavigation($this->buildSideNavView()); + $navigation[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Transactions')); - return $this->delegateToController($controller); + $navigation[] = id(new PHUIListItemView()) + ->setName(pht('Transaction Logs')) + ->setHref($this->getApplicationURI('transactions/')); + + return id(new PhabricatorFeedSearchEngine()) + ->setController($this) + ->setNavigationItems($navigation) + ->buildResponse(); } } diff --git a/src/applications/feed/controller/PhabricatorFeedTransactionListController.php b/src/applications/feed/controller/PhabricatorFeedTransactionListController.php new file mode 100644 index 0000000000..16e5b0673b --- /dev/null +++ b/src/applications/feed/controller/PhabricatorFeedTransactionListController.php @@ -0,0 +1,16 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php new file mode 100644 index 0000000000..00da566532 --- /dev/null +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -0,0 +1,178 @@ +phids = $phids; + return $this; + } + + public function withDateCreatedBetween($min, $max) { + $this->createdMin = $min; + $this->createdMax = $max; + return $this; + } + + protected function loadPage() { + $queries = $this->newTransactionQueries(); + + $xactions = array(); + + if ($this->shouldLimitResults()) { + $limit = $this->getRawResultLimit(); + if (!$limit) { + $limit = null; + } + } else { + $limit = null; + } + + // We're doing a bit of manual work to get paging working, because this + // query aggregates the results of a large number of subqueries. + + // Overall, we're ordering transactions by "". Ordering + // by PHID is not very meaningful, but we don't need the ordering to be + // especially meaningful, just consistent. Using PHIDs is easy and does + // everything we need it to technically. + + // To actually configure paging, if we have an external cursor, we load + // the internal cursor first. Then we pass it to each subquery and the + // subqueries pretend they just loaded a page where it was the last object. + // This configures their queries properly and we can aggregate a cohesive + // set of results by combining all the queries. + + $cursor = $this->getExternalCursorString(); + if ($cursor !== null) { + $cursor_object = $this->newInternalCursorFromExternalCursor($cursor); + } else { + $cursor_object = null; + } + + $is_reversed = $this->getIsQueryOrderReversed(); + + $created_min = $this->createdMin; + $created_max = $this->createdMax; + + $xaction_phids = $this->phids; + + foreach ($queries as $query) { + $query->withDateCreatedBetween($created_min, $created_max); + + if ($xaction_phids !== null) { + $query->withPHIDs($xaction_phids); + } + + if ($limit !== null) { + $query->setLimit($limit); + } + + if ($cursor_object !== null) { + $query + ->setAggregatePagingCursor($cursor_object) + ->setIsQueryOrderReversed($is_reversed); + } + + $query->setOrder('global'); + + $query_xactions = $query->execute(); + foreach ($query_xactions as $query_xaction) { + $xactions[] = $query_xaction; + } + + $xactions = msortv($xactions, 'newGlobalSortVector'); + if ($is_reversed) { + $xactions = array_reverse($xactions); + } + + if ($limit !== null) { + $xactions = array_slice($xactions, 0, $limit); + + // If we've found enough transactions to fill up the entire requested + // page size, we can narrow the search window: transactions after the + // last transaction we've found so far can't possibly be part of the + // result set. + + if (count($xactions) === $limit) { + $last_date = last($xactions)->getDateCreated(); + if ($is_reversed) { + if ($created_max === null) { + $created_max = $last_date; + } else { + $created_max = min($created_max, $last_date); + } + } else { + if ($created_min === null) { + $created_min = $last_date; + } else { + $created_min = max($created_min, $last_date); + } + } + } + } + } + + return $xactions; + } + + public function getQueryApplicationClass() { + return 'PhabricatorFeedApplication'; + } + + private function newTransactionQueries() { + $viewer = $this->getViewer(); + + $queries = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorApplicationTransactionQuery') + ->execute(); + + $type_map = array(); + + // If we're querying for specific transaction PHIDs, we only need to + // consider queries which may load transactions with subtypes present + // in the list. + + // For example, if we're loading Maniphest Task transaction PHIDs, we know + // we only have to look at Maniphest Task transactions, since other types + // of objects will never have the right transaction PHIDs. + + $xaction_phids = $this->phids; + if ($xaction_phids) { + foreach ($xaction_phids as $xaction_phid) { + $type_map[phid_get_subtype($xaction_phid)] = true; + } + } + + $results = array(); + foreach ($queries as $query) { + if ($type_map) { + $type = $query->getTemplateApplicationTransaction() + ->getApplicationTransactionType(); + if (!isset($type_map[$type])) { + continue; + } + } + + $results[] = id(clone $query) + ->setViewer($viewer) + ->setParentQuery($this); + } + + return $results; + } + + protected function newExternalCursorStringForResult($object) { + return (string)$object->getPHID(); + } + + protected function applyExternalCursorConstraintsToQuery( + PhabricatorCursorPagedPolicyAwareQuery $subquery, + $cursor) { + $subquery->withPHIDs(array($cursor)); + } + +} diff --git a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php new file mode 100644 index 0000000000..bc0d27c70c --- /dev/null +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -0,0 +1,113 @@ +newQuery(); + + return $query; + } + + protected function getURI($path) { + return '/feed/transactions/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Transactions'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery() + ->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $objects, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($objects, 'PhabricatorApplicationTransaction'); + + $viewer = $this->requireViewer(); + + $handle_phids = array(); + foreach ($objects as $object) { + $author_phid = $object->getAuthorPHID(); + if ($author_phid !== null) { + $handle_phids[] = $author_phid; + } + $object_phid = $object->getObjectPHID(); + if ($object_phid !== null) { + $handle_phids[] = $object_phid; + } + } + + $handles = $viewer->loadHandles($handle_phids); + + $rows = array(); + foreach ($objects as $object) { + $author_phid = $object->getAuthorPHID(); + $object_phid = $object->getObjectPHID(); + + try { + $title = $object->getTitle(); + } catch (Exception $ex) { + $title = null; + } + + $rows[] = array( + $handles[$author_phid]->renderLink(), + $handles[$object_phid]->renderLink(), + AphrontTableView::renderSingleDisplayLine($title), + phabricator_datetime($object->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('Actor'), + pht('Object'), + pht('Transaction'), + pht('Date'), + )) + ->setColumnClasses( + array( + null, + null, + 'wide', + 'right', + )); + + return id(new PhabricatorApplicationSearchResultView()) + ->setTable($table); + } + +} diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php index 1db622163d..195de601f0 100644 --- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php +++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php @@ -9,6 +9,9 @@ abstract class PhabricatorApplicationTransactionQuery private $authorPHIDs; private $transactionTypes; private $withComments; + private $createdMin; + private $createdMax; + private $aggregatePagingCursor; private $needComments = true; private $needHandles = true; @@ -66,6 +69,12 @@ abstract class PhabricatorApplicationTransactionQuery return $this; } + public function withDateCreatedBetween($min, $max) { + $this->createdMin = $min; + $this->createdMax = $max; + return $this; + } + public function needComments($need) { $this->needComments = $need; return $this; @@ -76,6 +85,22 @@ abstract class PhabricatorApplicationTransactionQuery return $this; } + public function setAggregatePagingCursor(PhabricatorQueryCursor $cursor) { + $this->aggregatePagingCursor = $cursor; + return $this; + } + + public function getAggregatePagingCursor() { + return $this->aggregatePagingCursor; + } + + protected function willExecute() { + $cursor_object = $this->getAggregatePagingCursor(); + if ($cursor_object) { + $this->nextPage(array($cursor_object->getObject())); + } + } + protected function loadPage() { $table = $this->getTemplateApplicationTransaction(); @@ -206,6 +231,20 @@ abstract class PhabricatorApplicationTransactionQuery } } + if ($this->createdMin !== null) { + $where[] = qsprintf( + $conn, + 'x.dateCreated >= %d', + $this->createdMin); + } + + if ($this->createdMax !== null) { + $where[] = qsprintf( + $conn, + 'x.dateCreated <= %d', + $this->createdMax); + } + return $where; } @@ -262,4 +301,38 @@ abstract class PhabricatorApplicationTransactionQuery return 'x'; } + protected function newPagingMapFromPartialObject($object) { + return parent::newPagingMapFromPartialObject($object) + array( + 'created' => $object->getDateCreated(), + 'phid' => $object->getPHID(), + ); + } + + public function getBuiltinOrders() { + return parent::getBuiltinOrders() + array( + 'global' => array( + 'vector' => array('created', 'phid'), + 'name' => pht('Global'), + ), + ); + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'created' => array( + 'table' => 'x', + 'column' => 'dateCreated', + 'type' => 'int', + ), + 'phid' => array( + 'table' => 'x', + 'column' => 'phid', + 'type' => 'string', + 'reverse' => true, + 'unique' => true, + ), + ); + } + + } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 1250b7cb16..6fa9446911 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1711,6 +1711,12 @@ abstract class PhabricatorApplicationTransaction return array($done, $undone); } + public function newGlobalSortVector() { + return id(new PhutilSortVector()) + ->addInt(-$this->getDateCreated()) + ->addString($this->getPHID()); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index f5586fd90f..978de0fcf8 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -104,7 +104,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } // Now that we made sure the viewer can actually see the object the - // external cursor identifies, return the internal cursor the query + // external cursor identifies, return the internal cursor the query // generated as a side effect while loading the object. return $query->getInternalCursorObject(); } @@ -134,7 +134,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery ); } - final private function getExternalCursorStringForResult($object) { $cursor = $this->newExternalCursorStringForResult($object); @@ -150,7 +149,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery return $cursor; } - final private function getExternalCursorString() { + final protected function getExternalCursorString() { return $this->externalCursorString; } @@ -159,11 +158,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery return $this; } - final private function getIsQueryOrderReversed() { + final protected function getIsQueryOrderReversed() { return $this->isQueryOrderReversed; } - final private function setIsQueryOrderReversed($is_reversed) { + final protected function setIsQueryOrderReversed($is_reversed) { $this->isQueryOrderReversed = $is_reversed; return $this; } From 2e5b1885e70147f1c46737dda156bb29749c4475 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 05:23:31 -0700 Subject: [PATCH 10/42] Add support for querying feed transactions by author and date range Summary: Depends on D20531. Ref T13294. Enable finding raw transactions in particular date ranges or with particular authors. Test Plan: {F6463471} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13294 Differential Revision: https://secure.phabricator.com/D20533 --- .../query/PhabricatorFeedTransactionQuery.php | 11 ++++++ ...PhabricatorFeedTransactionSearchEngine.php | 36 +++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php index 00da566532..e7ab4cc55d 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -4,6 +4,7 @@ final class PhabricatorFeedTransactionQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $phids; + private $authorPHIDs; private $createdMin; private $createdMax; @@ -12,6 +13,11 @@ final class PhabricatorFeedTransactionQuery return $this; } + public function withAuthorPHIDs(array $phids) { + $this->authorPHIDs = $phids; + return $this; + } + public function withDateCreatedBetween($min, $max) { $this->createdMin = $min; $this->createdMax = $max; @@ -59,6 +65,7 @@ final class PhabricatorFeedTransactionQuery $created_max = $this->createdMax; $xaction_phids = $this->phids; + $author_phids = $this->authorPHIDs; foreach ($queries as $query) { $query->withDateCreatedBetween($created_min, $created_max); @@ -67,6 +74,10 @@ final class PhabricatorFeedTransactionQuery $query->withPHIDs($xaction_phids); } + if ($author_phids !== null) { + $query->withAuthorPHIDs($author_phids); + } + if ($limit !== null) { $query->setLimit($limit); } diff --git a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php index bc0d27c70c..5c73818e4d 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -16,12 +16,44 @@ final class PhabricatorFeedTransactionSearchEngine } protected function buildCustomSearchFields() { - return array(); + return array( + id(new PhabricatorUsersSearchField()) + ->setLabel(pht('Authors')) + ->setKey('authorPHIDs') + ->setAliases(array('author', 'authors')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), + ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['authorPHIDs']) { + $query->withAuthorPHIDs($map['authorPHIDs']); + } + + $created_min = $map['createdStart']; + $created_max = $map['createdEnd']; + + if ($created_min && $created_max) { + if ($created_min > $created_max) { + throw new PhabricatorSearchConstraintException( + pht( + 'The specified "Created Before" date is earlier in time than the '. + 'specified "Created After" date, so this query can never match '. + 'any results.')); + } + } + + if ($created_min || $created_max) { + $query->withDateCreatedBetween($created_min, $created_max); + } + return $query; } @@ -93,7 +125,7 @@ final class PhabricatorFeedTransactionSearchEngine $table = id(new AphrontTableView($rows)) ->setHeaders( array( - pht('Actor'), + pht('Author'), pht('Object'), pht('Transaction'), pht('Date'), From 16537f7b322f1ce01261f953a51f5ced3d7ca5df Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 05:40:11 -0700 Subject: [PATCH 11/42] Support filtering feed transactions by object type Summary: Depends on D20533. Allow querying for transactions of a specific object type, so you can run queries like "Show all edits to Herald rules between date X and Y". Test Plan: {F6463478} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20534 --- src/__phutil_library_map__.php | 2 + .../query/PhabricatorFeedTransactionQuery.php | 24 ++++++- ...PhabricatorFeedTransactionSearchEngine.php | 9 +++ ...icatorTransactionsObjectTypeDatasource.php | 63 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 src/applications/transactions/typeahead/PhabricatorTransactionsObjectTypeDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index dc331be164..20de984e5a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4809,6 +4809,7 @@ phutil_register_library_map(array( 'PhabricatorTransactionsApplication' => 'applications/transactions/application/PhabricatorTransactionsApplication.php', 'PhabricatorTransactionsDestructionEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsDestructionEngineExtension.php', 'PhabricatorTransactionsFulltextEngineExtension' => 'applications/transactions/engineextension/PhabricatorTransactionsFulltextEngineExtension.php', + 'PhabricatorTransactionsObjectTypeDatasource' => 'applications/transactions/typeahead/PhabricatorTransactionsObjectTypeDatasource.php', 'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php', 'PhabricatorTranslationSetting' => 'applications/settings/setting/PhabricatorTranslationSetting.php', 'PhabricatorTranslationsConfigOptions' => 'applications/config/option/PhabricatorTranslationsConfigOptions.php', @@ -11158,6 +11159,7 @@ phutil_register_library_map(array( 'PhabricatorTransactionsApplication' => 'PhabricatorApplication', 'PhabricatorTransactionsDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorTransactionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', + 'PhabricatorTransactionsObjectTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTransformedFile' => 'PhabricatorFileDAO', 'PhabricatorTranslationSetting' => 'PhabricatorOptionGroupSetting', 'PhabricatorTranslationsConfigOptions' => 'PhabricatorApplicationConfigOptions', diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php index e7ab4cc55d..ba25f0842d 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -5,6 +5,7 @@ final class PhabricatorFeedTransactionQuery private $phids; private $authorPHIDs; + private $objectTypes; private $createdMin; private $createdMax; @@ -18,6 +19,11 @@ final class PhabricatorFeedTransactionQuery return $this; } + public function withObjectTypes(array $types) { + $this->objectTypes = $types; + return $this; + } + public function withDateCreatedBetween($min, $max) { $this->createdMin = $min; $this->createdMax = $max; @@ -158,12 +164,24 @@ final class PhabricatorFeedTransactionQuery } } + $object_types = $this->objectTypes; + if ($object_types) { + $object_types = array_fuse($object_types); + } + $results = array(); foreach ($queries as $query) { + $query_type = $query->getTemplateApplicationTransaction() + ->getApplicationTransactionType(); + if ($type_map) { - $type = $query->getTemplateApplicationTransaction() - ->getApplicationTransactionType(); - if (!isset($type_map[$type])) { + if (!isset($type_map[$query_type])) { + continue; + } + } + + if ($object_types) { + if (!isset($object_types[$query_type])) { continue; } } diff --git a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php index 5c73818e4d..af40b6ff6b 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -21,6 +21,11 @@ final class PhabricatorFeedTransactionSearchEngine ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setAliases(array('author', 'authors')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Object Types')) + ->setKey('objectTypes') + ->setAliases(array('objectType')) + ->setDatasource(new PhabricatorTransactionsObjectTypeDatasource()), id(new PhabricatorSearchDateField()) ->setLabel(pht('Created After')) ->setKey('createdStart'), @@ -37,6 +42,10 @@ final class PhabricatorFeedTransactionSearchEngine $query->withAuthorPHIDs($map['authorPHIDs']); } + if ($map['objectTypes']) { + $query->withObjectTypes($map['objectTypes']); + } + $created_min = $map['createdStart']; $created_max = $map['createdEnd']; diff --git a/src/applications/transactions/typeahead/PhabricatorTransactionsObjectTypeDatasource.php b/src/applications/transactions/typeahead/PhabricatorTransactionsObjectTypeDatasource.php new file mode 100644 index 0000000000..e7292fc522 --- /dev/null +++ b/src/applications/transactions/typeahead/PhabricatorTransactionsObjectTypeDatasource.php @@ -0,0 +1,63 @@ +renderTokensFromResults($this->buildResults(), $values); + } + + public function loadResults() { + $results = $this->buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + private function buildResults() { + $queries = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhabricatorApplicationTransactionQuery') + ->execute(); + + $phid_types = PhabricatorPHIDType::getAllTypes(); + + $results = array(); + foreach ($queries as $query) { + $query_type = $query->getTemplateApplicationTransaction() + ->getApplicationTransactionType(); + + $phid_type = idx($phid_types, $query_type); + + if ($phid_type) { + $name = $phid_type->getTypeName(); + $icon = $phid_type->getTypeIcon(); + } else { + $name = pht('%s ("%s")', $query_type, get_class($query)); + $icon = null; + } + + $result = id(new PhabricatorTypeaheadResult()) + ->setName($name) + ->setPHID($query_type); + + if ($icon) { + $result->setIcon($icon); + } + + $results[$query_type] = $result; + } + + return $results; + } + +} From 2f3869576871cb96bb0d8f90d3eaee0d9e4e22a1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 06:04:37 -0700 Subject: [PATCH 12/42] Support export of feed transactions to CSV/Excel/etc Summary: Depends on D20534. Ref T13294. Add export support so you can dump these out, print them on paper, notarize them, and store them in a box under a tree or whatever. Test Plan: Exported transactions to a flat file, read the file. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13294 Differential Revision: https://secure.phabricator.com/D20535 --- .../query/PhabricatorFeedTransactionQuery.php | 8 ++ ...PhabricatorFeedTransactionSearchEngine.php | 76 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php index ba25f0842d..d0a9f53e35 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -30,6 +30,14 @@ final class PhabricatorFeedTransactionQuery return $this; } + public function newResultObject() { + // Return an arbitrary valid transaction object. The actual query may + // return objects of any subclass of "ApplicationTransaction" when it is + // executed, but we need to pick something concrete here to make some + // integrations work (like automatic handling of PHIDs in data export). + return new PhabricatorUserTransaction(); + } + protected function loadPage() { $queries = $this->newTransactionQueries(); diff --git a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php index af40b6ff6b..0cbbcd23b1 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -151,4 +151,80 @@ final class PhabricatorFeedTransactionSearchEngine ->setTable($table); } + protected function newExportFields() { + $fields = array( + id(new PhabricatorPHIDExportField()) + ->setKey('authorPHID') + ->setLabel(pht('Author PHID')), + id(new PhabricatorStringExportField()) + ->setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorStringExportField()) + ->setKey('objectType') + ->setLabel(pht('Object Type')), + id(new PhabricatorPHIDExportField()) + ->setKey('objectPHID') + ->setLabel(pht('Object PHID')), + id(new PhabricatorStringExportField()) + ->setKey('objectName') + ->setLabel(pht('Object Name')), + id(new PhabricatorStringExportField()) + ->setKey('description') + ->setLabel(pht('Description')), + ); + + return $fields; + } + + protected function newExportData(array $xactions) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($xactions as $xaction) { + $phids[] = $xaction->getAuthorPHID(); + $phids[] = $xaction->getObjectPHID(); + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($xactions as $xaction) { + $xaction_phid = $xaction->getPHID(); + + $author_phid = $xaction->getAuthorPHID(); + if ($author_phid) { + $author_name = $handles[$author_phid]->getName(); + } else { + $author_name = null; + } + + $object_phid = $xaction->getObjectPHID(); + if ($object_phid) { + $object_name = $handles[$object_phid]->getName(); + } else { + $object_name = null; + } + + $old_target = $xaction->getRenderingTarget(); + try { + $description = $xaction + ->setRenderingTarget(PhabricatorApplicationTransaction::TARGET_TEXT) + ->getTitle(); + } catch (Exception $ex) { + $description = null; + } + $xaction->setRenderingTarget($old_target); + + $export[] = array( + 'authorPHID' => $author_phid, + 'author' => $author_name, + 'objectType' => phid_get_subtype($xaction_phid), + 'objectPHID' => $object_phid, + 'objectName' => $object_name, + 'description' => $description, + ); + } + + return $export; + } + } From 7c1f6519e0b2cfde0d7a5d938ead8db29534d34f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 07:22:38 -0700 Subject: [PATCH 13/42] Support "none()" in Differential to find revisions with no (un-resigned) reviewers Summary: Ref T13289. In Maniphest, you can currently search for "Owner: none()" to find tasks with no owner, but there's no way to search for "Reviewers: none()" in Differential right now. Add support for this, since it's consistent and reasonable and doesn't seem too weird or niche. Test Plan: Searched for "Reviewers: none()", found revisions with no reviewers. Searched for "Reviewers: alice, none()", "Reviewers: alice", and "Reviewers: " and got sensible results. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20537 --- src/__phutil_library_map__.php | 4 + .../query/DifferentialRevisionQuery.php | 60 +++++++++++++- .../DifferentialRevisionSearchEngine.php | 2 +- .../DifferentialNoReviewersDatasource.php | 78 +++++++++++++++++++ ...DifferentialReviewerFunctionDatasource.php | 26 +++++++ .../view/DifferentialRevisionListView.php | 10 +++ 6 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/applications/differential/typeahead/DifferentialNoReviewersDatasource.php create mode 100644 src/applications/differential/typeahead/DifferentialReviewerFunctionDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 20de984e5a..5d4d64641b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -538,6 +538,7 @@ phutil_register_library_map(array( 'DifferentialMailEngineExtension' => 'applications/differential/engineextension/DifferentialMailEngineExtension.php', 'DifferentialMailView' => 'applications/differential/mail/DifferentialMailView.php', 'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php', + 'DifferentialNoReviewersDatasource' => 'applications/differential/typeahead/DifferentialNoReviewersDatasource.php', 'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php', 'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php', 'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', @@ -561,6 +562,7 @@ phutil_register_library_map(array( 'DifferentialReviewer' => 'applications/differential/storage/DifferentialReviewer.php', 'DifferentialReviewerDatasource' => 'applications/differential/typeahead/DifferentialReviewerDatasource.php', 'DifferentialReviewerForRevisionEdgeType' => 'applications/differential/edge/DifferentialReviewerForRevisionEdgeType.php', + 'DifferentialReviewerFunctionDatasource' => 'applications/differential/typeahead/DifferentialReviewerFunctionDatasource.php', 'DifferentialReviewerStatus' => 'applications/differential/constants/DifferentialReviewerStatus.php', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingReviewersHeraldAction.php', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'applications/differential/herald/DifferentialReviewersAddBlockingSelfHeraldAction.php', @@ -6197,6 +6199,7 @@ phutil_register_library_map(array( 'DifferentialMailEngineExtension' => 'PhabricatorMailEngineExtension', 'DifferentialMailView' => 'Phobject', 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', + 'DifferentialNoReviewersDatasource' => 'PhabricatorTypeaheadDatasource', 'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector', 'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', @@ -6220,6 +6223,7 @@ phutil_register_library_map(array( 'DifferentialReviewer' => 'DifferentialDAO', 'DifferentialReviewerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialReviewerForRevisionEdgeType' => 'PhabricatorEdgeType', + 'DifferentialReviewerFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'DifferentialReviewerStatus' => 'Phobject', 'DifferentialReviewersAddBlockingReviewersHeraldAction' => 'DifferentialReviewersHeraldAction', 'DifferentialReviewersAddBlockingSelfHeraldAction' => 'DifferentialReviewersHeraldAction', diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index 738df3156a..84f9f07f61 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -26,6 +26,7 @@ final class DifferentialRevisionQuery private $isOpen; private $createdEpochMin; private $createdEpochMax; + private $noReviewers; const ORDER_MODIFIED = 'order-modified'; const ORDER_CREATED = 'order-created'; @@ -98,7 +99,31 @@ final class DifferentialRevisionQuery * @task config */ public function withReviewers(array $reviewer_phids) { - $this->reviewers = $reviewer_phids; + if ($reviewer_phids === array()) { + throw new Exception( + pht( + 'Empty "withReviewers()" constraint is invalid. Provide one or '. + 'more values, or remove the constraint.')); + } + + $with_none = false; + + foreach ($reviewer_phids as $key => $phid) { + switch ($phid) { + case DifferentialNoReviewersDatasource::FUNCTION_TOKEN: + $with_none = true; + unset($reviewer_phids[$key]); + break; + default: + break; + } + } + + $this->noReviewers = $with_none; + if ($reviewer_phids) { + $this->reviewers = array_values($reviewer_phids); + } + return $this; } @@ -572,7 +597,7 @@ final class DifferentialRevisionQuery if ($this->reviewers) { $joins[] = qsprintf( $conn, - 'JOIN %T reviewer ON reviewer.revisionPHID = r.phid + 'LEFT JOIN %T reviewer ON reviewer.revisionPHID = r.phid AND reviewer.reviewerStatus != %s AND reviewer.reviewerPHID in (%Ls)', id(new DifferentialReviewer())->getTableName(), @@ -580,6 +605,15 @@ final class DifferentialRevisionQuery $this->reviewers); } + if ($this->noReviewers) { + $joins[] = qsprintf( + $conn, + 'LEFT JOIN %T no_reviewer ON no_reviewer.revisionPHID = r.phid + AND no_reviewer.reviewerStatus != %s', + id(new DifferentialReviewer())->getTableName(), + DifferentialReviewerStatus::STATUS_RESIGNED); + } + if ($this->draftAuthors) { $joins[] = qsprintf( $conn, @@ -715,6 +749,24 @@ final class DifferentialRevisionQuery $statuses); } + $reviewer_subclauses = array(); + + if ($this->noReviewers) { + $reviewer_subclauses[] = qsprintf( + $conn, + 'no_reviewer.reviewerPHID IS NULL'); + } + + if ($this->reviewers) { + $reviewer_subclauses[] = qsprintf( + $conn, + 'reviewer.reviewerPHID IS NOT NULL'); + } + + if ($reviewer_subclauses) { + $where[] = qsprintf($conn, '%LO', $reviewer_subclauses); + } + $where[] = $this->buildWhereClauseParts($conn); return $this->formatWhereClause($conn, $where); @@ -735,6 +787,10 @@ final class DifferentialRevisionQuery return true; } + if ($this->noReviewers) { + return true; + } + return parent::shouldGroupQueryResultRows(); } diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php index be753c5e17..14c9dd0301 100644 --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -73,7 +73,7 @@ final class DifferentialRevisionSearchEngine ->setLabel(pht('Reviewers')) ->setKey('reviewerPHIDs') ->setAliases(array('reviewer', 'reviewers', 'reviewerPHID')) - ->setDatasource(new DiffusionAuditorFunctionDatasource()) + ->setDatasource(new DifferentialReviewerFunctionDatasource()) ->setDescription( pht('Find revisions with specific reviewers.')), id(new PhabricatorSearchDatasourceField()) diff --git a/src/applications/differential/typeahead/DifferentialNoReviewersDatasource.php b/src/applications/differential/typeahead/DifferentialNoReviewersDatasource.php new file mode 100644 index 0000000000..083b6c7cb5 --- /dev/null +++ b/src/applications/differential/typeahead/DifferentialNoReviewersDatasource.php @@ -0,0 +1,78 @@ + array( + 'name' => pht('No Reviewers'), + 'summary' => pht('Find results which have no reviewers.'), + 'description' => pht( + "This function includes results which have no reviewers. Use a ". + "query like this to find results with no reviewers:\n\n%s\n\n". + "If you combine this function with other functions, the query will ". + "return results which match the other selectors //or// have no ". + "reviewers. For example, this query will find results which have ". + "`alincoln` as a reviewer, and will also find results which have ". + "no reviewers:". + "\n\n%s", + '> none()', + '> alincoln, none()'), + ), + ); + } + + public function loadResults() { + $results = array( + $this->buildNoReviewersResult(), + ); + return $this->filterResultsAgainstTokens($results); + } + + protected function evaluateFunction($function, array $argv_list) { + $results = array(); + + foreach ($argv_list as $argv) { + $results[] = self::FUNCTION_TOKEN; + } + + return $results; + } + + public function renderFunctionTokens($function, array $argv_list) { + $results = array(); + foreach ($argv_list as $argv) { + $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $this->buildNoReviewersResult()); + } + return $results; + } + + private function buildNoReviewersResult() { + $name = pht('No Reviewers'); + + return $this->newFunctionResult() + ->setName($name.' none') + ->setDisplayName($name) + ->setIcon('fa-ban') + ->setPHID('none()') + ->setUnique(true) + ->addAttribute(pht('Select results with no reviewers.')); + } + +} diff --git a/src/applications/differential/typeahead/DifferentialReviewerFunctionDatasource.php b/src/applications/differential/typeahead/DifferentialReviewerFunctionDatasource.php new file mode 100644 index 0000000000..3b79016055 --- /dev/null +++ b/src/applications/differential/typeahead/DifferentialReviewerFunctionDatasource.php @@ -0,0 +1,26 @@ +revisions as $key => $revision) { $reviewers = $revision->getReviewers(); + + // Don't show reviewers who have resigned. The "Reviewers" constraint + // does not respect these reviewers and they largely don't count as + // reviewers. + foreach ($reviewers as $reviewer_key => $reviewer) { + if ($reviewer->isResigned()) { + unset($reviewers[$reviewer_key]); + } + } + if (count($reviewers) > $reviewer_limit) { $reviewers = array_slice($reviewers, 0, $reviewer_limit); $reviewer_more[$key] = true; From 56e7bde68d9bccc405d2ea98c16ff3abe820c1a8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 09:41:23 -0700 Subject: [PATCH 14/42] Recognize self-URI links to Diffusion files and give them special rendering behavior Summary: Depends on D20530. Ref T13291. When users paste links to files in Diffusion into remarkup contexts, identify them and specialize the rendering. When the URIs are embedded with `{...}`, parse them in more detail. This is a lead-up to a `{src ...}` rule which will use the same `View` but give users more options to customize presentation. Test Plan: {F6463580} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20538 --- src/__phutil_library_map__.php | 10 +- ...iffusionSourceHyperlinkEngineExtension.php | 84 +++++++ .../diffusion/request/DiffusionGitRequest.php | 4 - .../request/DiffusionMercurialRequest.php | 4 - .../diffusion/request/DiffusionRequest.php | 5 +- .../diffusion/request/DiffusionSvnRequest.php | 4 - .../view/DiffusionSourceLinkView.php | 208 ++++++++++++++++++ .../DoorkeeperHyperlinkEngineExtension.php | 2 +- ...habricatorSelfHyperlinkEngineExtension.php | 12 +- ...icatorRemarkupHyperlinkEngineExtension.php | 32 +++ .../storage/PhabricatorRepository.php | 9 + 11 files changed, 348 insertions(+), 26 deletions(-) create mode 100644 src/applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php create mode 100644 src/applications/diffusion/view/DiffusionSourceLinkView.php create mode 100644 src/applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5d4d64641b..74aa217b05 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -999,6 +999,8 @@ phutil_register_library_map(array( 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', + 'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', + 'DiffusionSourceLinkView' => 'applications/diffusion/view/DiffusionSourceLinkView.php', 'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php', 'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php', 'DiffusionSubversionServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php', @@ -4340,6 +4342,7 @@ phutil_register_library_map(array( 'PhabricatorRemarkupDocumentEngine' => 'applications/files/document/PhabricatorRemarkupDocumentEngine.php', 'PhabricatorRemarkupEditField' => 'applications/transactions/editfield/PhabricatorRemarkupEditField.php', 'PhabricatorRemarkupFigletBlockInterpreter' => 'infrastructure/markup/interpreter/PhabricatorRemarkupFigletBlockInterpreter.php', + 'PhabricatorRemarkupHyperlinkEngineExtension' => 'applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php', 'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php', 'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', @@ -6681,6 +6684,8 @@ phutil_register_library_map(array( 'DiffusionServeController' => 'DiffusionController', 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'Exception', + 'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', + 'DiffusionSourceLinkView' => 'AphrontView', 'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine', 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow', 'DiffusionSubversionServeSSHWorkflow' => 'DiffusionSubversionSSHWorkflow', @@ -6786,7 +6791,7 @@ phutil_register_library_map(array( 'DoorkeeperExternalObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DoorkeeperFeedStoryPublisher' => 'Phobject', 'DoorkeeperFeedWorker' => 'FeedPushWorker', - 'DoorkeeperHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', + 'DoorkeeperHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', 'DoorkeeperImportEngine' => 'Phobject', 'DoorkeeperJIRAFeedWorker' => 'DoorkeeperFeedWorker', 'DoorkeeperMissingLinkException' => 'Exception', @@ -10595,6 +10600,7 @@ phutil_register_library_map(array( 'PhabricatorRemarkupDocumentEngine' => 'PhabricatorDocumentEngine', 'PhabricatorRemarkupEditField' => 'PhabricatorEditField', 'PhabricatorRemarkupFigletBlockInterpreter' => 'PhutilRemarkupBlockInterpreter', + 'PhabricatorRemarkupHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', 'PhabricatorRemarkupUIExample' => 'PhabricatorUIExample', 'PhabricatorRepositoriesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorRepository' => array( @@ -10883,7 +10889,7 @@ phutil_register_library_map(array( 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorSelectEditField' => 'PhabricatorEditField', 'PhabricatorSelectSetting' => 'PhabricatorSetting', - 'PhabricatorSelfHyperlinkEngineExtension' => 'PhutilRemarkupHyperlinkEngineExtension', + 'PhabricatorSelfHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', 'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorSetConfigType' => 'PhabricatorTextConfigType', 'PhabricatorSetting' => 'Phobject', diff --git a/src/applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php b/src/applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php new file mode 100644 index 0000000000..1ebda4a998 --- /dev/null +++ b/src/applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php @@ -0,0 +1,84 @@ +getEngine(); + $viewer = $engine->getConfig('viewer'); + + if (!$viewer) { + return; + } + + $hyperlinks = $this->getSelfLinks($hyperlinks); + + $links = array(); + foreach ($hyperlinks as $link) { + $uri = $link->getURI(); + $uri = new PhutilURI($uri); + + $path = $uri->getPath(); + + $pattern = + '(^'. + '/(?:diffusion|source)'. + '/(?P[^/]+)'. + '/browse'. + '/(?P.*)'. + '\z)'; + $matches = null; + if (!preg_match($pattern, $path, $matches)) { + continue; + } + + $links[] = array( + 'ref' => $link, + 'identifier' => $matches['identifier'], + 'blob' => $matches['blob'], + ); + } + + if (!$links) { + return; + } + + $identifiers = ipull($links, 'identifier'); + + $query = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withIdentifiers($identifiers); + + $query->execute(); + + $repository_map = $query->getIdentifierMap(); + + foreach ($links as $link) { + $identifier = $link['identifier']; + + $repository = idx($repository_map, $identifier); + if (!$repository) { + continue; + } + + $ref = $link['ref']; + $uri = $ref->getURI(); + + + $tag = id(new DiffusionSourceLinkView()) + ->setViewer($viewer) + ->setRepository($repository) + ->setURI($uri) + ->setBlob($link['blob']); + + if (!$ref->isEmbed()) { + $tag->setText($uri); + } + + $ref->setResult($tag); + } + } + +} diff --git a/src/applications/diffusion/request/DiffusionGitRequest.php b/src/applications/diffusion/request/DiffusionGitRequest.php index 6e30886eec..a283fff206 100644 --- a/src/applications/diffusion/request/DiffusionGitRequest.php +++ b/src/applications/diffusion/request/DiffusionGitRequest.php @@ -2,10 +2,6 @@ final class DiffusionGitRequest extends DiffusionRequest { - public function supportsBranches() { - return true; - } - protected function isStableCommit($symbol) { return preg_match('/^[a-f0-9]{40}\z/', $symbol); } diff --git a/src/applications/diffusion/request/DiffusionMercurialRequest.php b/src/applications/diffusion/request/DiffusionMercurialRequest.php index a43cb089dd..b626d62750 100644 --- a/src/applications/diffusion/request/DiffusionMercurialRequest.php +++ b/src/applications/diffusion/request/DiffusionMercurialRequest.php @@ -2,10 +2,6 @@ final class DiffusionMercurialRequest extends DiffusionRequest { - public function supportsBranches() { - return true; - } - protected function isStableCommit($symbol) { return preg_match('/^[a-f0-9]{40}\z/', $symbol); } diff --git a/src/applications/diffusion/request/DiffusionRequest.php b/src/applications/diffusion/request/DiffusionRequest.php index 52a09d12ae..d866874f5d 100644 --- a/src/applications/diffusion/request/DiffusionRequest.php +++ b/src/applications/diffusion/request/DiffusionRequest.php @@ -28,7 +28,10 @@ abstract class DiffusionRequest extends Phobject { private $branchObject = false; private $refAlternatives; - abstract public function supportsBranches(); + final public function supportsBranches() { + return $this->getRepository()->supportsRefs(); + } + abstract protected function isStableCommit($symbol); protected function didInitialize() { diff --git a/src/applications/diffusion/request/DiffusionSvnRequest.php b/src/applications/diffusion/request/DiffusionSvnRequest.php index 9ebefd01ab..5f50366331 100644 --- a/src/applications/diffusion/request/DiffusionSvnRequest.php +++ b/src/applications/diffusion/request/DiffusionSvnRequest.php @@ -2,10 +2,6 @@ final class DiffusionSvnRequest extends DiffusionRequest { - public function supportsBranches() { - return false; - } - protected function isStableCommit($symbol) { return preg_match('/^[1-9]\d*\z/', $symbol); } diff --git a/src/applications/diffusion/view/DiffusionSourceLinkView.php b/src/applications/diffusion/view/DiffusionSourceLinkView.php new file mode 100644 index 0000000000..4610062c23 --- /dev/null +++ b/src/applications/diffusion/view/DiffusionSourceLinkView.php @@ -0,0 +1,208 @@ +repository = $repository; + $this->blobMap = null; + return $this; + } + + public function getRepository() { + return $this->repository; + } + + public function setText($text) { + $this->text = $text; + return $this; + } + + public function getText() { + return $this->text; + } + + public function setURI($uri) { + $this->uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setBlob($blob) { + $this->blob = $blob; + $this->blobMap = null; + return $this; + } + + public function getBlob() { + return $this->blob; + } + + public function setRefName($ref_name) { + $this->refName = $ref_name; + return $this; + } + + public function getRefName() { + return $this->refName; + } + + public function setPath($path) { + $this->path = $path; + return $this; + } + + public function getPath() { + return $this->path; + } + + public function setCommit($commit) { + $this->commit = $commit; + return $this; + } + + public function getCommit() { + return $this->commit; + } + + public function setLine($line) { + $this->line = $line; + return $this; + } + + public function getLine() { + return $this->line; + } + + public function getDisplayPath() { + if ($this->path !== null) { + return $this->path; + } + + return $this->getBlobPath(); + } + + public function getDisplayRefName() { + if ($this->refName !== null) { + return $this->refName; + } + + return $this->getBlobRefName(); + } + + public function getDisplayCommit() { + if ($this->commit !== null) { + return $this->commit; + } + + return $this->getBlobCommit(); + } + + public function getDisplayLine() { + if ($this->line !== null) { + return $this->line; + } + + return $this->getBlobLine(); + } + + private function getBlobPath() { + return idx($this->getBlobMap(), 'path'); + } + + private function getBlobRefName() { + return idx($this->getBlobMap(), 'branch'); + } + + private function getBlobLine() { + return idx($this->getBlobMap(), 'line'); + } + + private function getBlobCommit() { + return idx($this->getBlobMap(), 'commit'); + } + + private function getBlobMap() { + if ($this->blobMap === null) { + $repository = $this->getRepository(); + $blob = $this->blob; + + if ($repository && ($blob !== null)) { + $map = DiffusionRequest::parseRequestBlob( + $blob, + $repository->supportsRefs()); + } else { + $map = array(); + } + + $this->blobMap = $map; + } + + return $this->blobMap; + } + + public function render() { + $repository = $this->getRepository(); + $uri = $this->getURI(); + + $color = 'blue'; + $icon = 'fa-file-text-o'; + + $text = $this->getText(); + if (!strlen($text)) { + $path = $this->getDisplayPath(); + + $line = $this->getDisplayLine(); + if ($line !== null) { + $path = pht('%s:%s', $path, $line); + } + + if ($repository) { + $path = pht('%s %s', $repository->getMonogram(), $path); + } + + if ($repository && $repository->supportsRefs()) { + $default_ref = $repository->getDefaultBranch(); + } else { + $default_ref = null; + } + + $ref_name = $this->getDisplayRefName(); + if ($ref_name === $default_ref) { + $ref_name = null; + } + + $commit = $this->getDisplayCommit(); + if ($ref_name !== null && $commit !== null) { + $text = pht('%s (on %s at %s)', $path, $ref_name, $commit); + } else if ($ref_name !== null) { + $text = pht('%s (on %s)', $path, $ref_name); + } else if ($commit !== null) { + $text = pht('%s (at %s)', $path, $commit); + } else { + $text = $path; + } + } + + return id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor($color) + ->setIcon($icon) + ->setHref($uri) + ->setName($text); + } + +} diff --git a/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php b/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php index fd8d02e8bb..675c9efcbc 100644 --- a/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php +++ b/src/applications/doorkeeper/engineextension/DoorkeeperHyperlinkEngineExtension.php @@ -1,7 +1,7 @@ getURI(); - if (PhabricatorEnv::isSelfURI($uri)) { - $self_links[] = $link; - } - } + $self_links = $this->getSelfLinks($hyperlinks); // For links in the form "/X123", we can reasonably guess that they are // fairly likely to be object names. Try to look them up. diff --git a/src/applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php b/src/applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php new file mode 100644 index 0000000000..d3c7a3f624 --- /dev/null +++ b/src/applications/remarkup/engineextension/PhabricatorRemarkupHyperlinkEngineExtension.php @@ -0,0 +1,32 @@ + true, + 'https' => true, + ); + + $results = array(); + foreach ($hyperlinks as $link) { + $uri = $link->getURI(); + + if (!PhabricatorEnv::isSelfURI($uri)) { + continue; + } + + $protocol = id(new PhutilURI($uri))->getProtocol(); + if (!isset($allowed_protocols[$protocol])) { + continue; + } + + $results[] = $link; + } + + return $results; + } +} diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index ebefa19a53..30eb56cfd5 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2040,6 +2040,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return true; } + + public function supportsRefs() { + if ($this->isSVN()) { + return false; + } + + return true; + } + public function getAlmanacServiceCacheKey() { $service_phid = $this->getAlmanacServicePHID(); if (!$service_phid) { From 4180b337cf7e201cc8e1c7ae6139615aa135c700 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 10:19:49 -0700 Subject: [PATCH 15/42] Add a "{src ...}" Remarkup rule to provide a more flexible way to reference source files in Diffusion Summary: Depends on D20538. Ref T13291. We now recognize full source URIs, but encoding full URIs isn't super human-friendly and we can't do stuff like relative links with them. Add `{src ...}` as a way to get to this behavior that supports options and more flexible syntax. Test Plan: {F6463607} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291 Differential Revision: https://secure.phabricator.com/D20539 --- src/__phutil_library_map__.php | 2 + .../PhabricatorDiffusionApplication.php | 1 + .../DiffusionSourceLinkRemarkupRule.php | 221 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 src/applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 74aa217b05..6777c69bd0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1000,6 +1000,7 @@ phutil_register_library_map(array( 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', + 'DiffusionSourceLinkRemarkupRule' => 'applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php', 'DiffusionSourceLinkView' => 'applications/diffusion/view/DiffusionSourceLinkView.php', 'DiffusionSubversionCommandEngine' => 'applications/diffusion/protocol/DiffusionSubversionCommandEngine.php', 'DiffusionSubversionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSubversionSSHWorkflow.php', @@ -6685,6 +6686,7 @@ phutil_register_library_map(array( 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'Exception', 'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', + 'DiffusionSourceLinkRemarkupRule' => 'PhutilRemarkupRule', 'DiffusionSourceLinkView' => 'AphrontView', 'DiffusionSubversionCommandEngine' => 'DiffusionCommandEngine', 'DiffusionSubversionSSHWorkflow' => 'DiffusionSSHWorkflow', diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index 96dbac7c8e..513103db9a 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -40,6 +40,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { new DiffusionCommitRemarkupRule(), new DiffusionRepositoryRemarkupRule(), new DiffusionRepositoryByIDRemarkupRule(), + new DiffusionSourceLinkRemarkupRule(), ); } diff --git a/src/applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php b/src/applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php new file mode 100644 index 0000000000..7f896c1ccc --- /dev/null +++ b/src/applications/diffusion/remarkup/DiffusionSourceLinkRemarkupRule.php @@ -0,0 +1,221 @@ +getEngine(); + $text_mode = $engine->isTextMode(); + $mail_mode = $engine->isHTMLMailMode(); + + if (!$this->isFlatText($matches[0]) || $text_mode || $mail_mode) { + // We could do better than this in text mode and mail mode, but focus + // on web mode first. + return $matches[0]; + } + + $metadata_key = self::KEY_SOURCELINKS; + $metadata = $engine->getTextMetadata($metadata_key, array()); + + $token = $engine->storeText($matches[0]); + + $metadata[] = array( + 'token' => $token, + 'raw' => $matches[0], + 'input' => $matches[1], + ); + + $engine->setTextMetadata($metadata_key, $metadata); + + return $token; + } + + public function didMarkupText() { + $engine = $this->getEngine(); + $metadata_key = self::KEY_SOURCELINKS; + $metadata = $engine->getTextMetadata($metadata_key, array()); + + if (!$metadata) { + return; + } + + $viewer = $engine->getConfig('viewer'); + if (!$viewer) { + return; + } + + $defaults = array( + 'repository' => null, + 'line' => null, + 'commit' => null, + 'ref' => null, + ); + + $tags = array(); + foreach ($metadata as $ref) { + $token = $ref['token']; + $raw = $ref['raw']; + $input = $ref['input']; + + $pattern = + '(^'. + '[\s,]*'. + '(?:"(?P(?:[^\\\\"]+|\\.)+)"|(?P[^\s,]+))'. + '[\s,]*'. + '(?P.*)'. + '\z)'; + $matches = null; + if (!preg_match($pattern, $input, $matches)) { + $hint_text = pht( + 'Missing path, expected "{src path ...}" in: %s', + $raw); + $hint = $this->newSyntaxHint($hint_text); + + $engine->overwriteStoredText($token, $hint); + continue; + } + + $path = idx($matches, 'rawpath'); + if (!strlen($path)) { + $path = idx($matches, 'quotedpath'); + $path = stripcslashes($path); + } + + $parts = explode(':', $path, 2); + if (count($parts) == 2) { + $repository = nonempty($parts[0], null); + $path = $parts[1]; + } else { + $repository = null; + $path = $parts[0]; + } + + $options = $matches['options']; + + $parser = new PhutilSimpleOptions(); + $options = $parser->parse($options) + $defaults; + + foreach ($options as $key => $value) { + if (!array_key_exists($key, $defaults)) { + $hint_text = pht( + 'Unknown option "%s" in: %s', + $key, + $raw); + $hint = $this->newSyntaxHint($hint_text); + + $engine->overwriteStoredText($token, $hint); + continue 2; + } + } + + if ($options['repository'] !== null) { + $repository = $options['repository']; + } + + if ($repository === null) { + $hint_text = pht( + 'Missing repository, expected "{src repository:path ...}" '. + 'or "{src path repository=...}" in: %s', + $raw); + $hint = $this->newSyntaxHint($hint_text); + + $engine->overwriteStoredText($token, $hint); + continue; + } + + $tags[] = array( + 'token' => $token, + 'raw' => $raw, + 'identifier' => $repository, + 'path' => $path, + 'options' => $options, + ); + } + + if (!$tags) { + return; + } + + $query = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->withIdentifiers(ipull($tags, 'identifier')); + + $query->execute(); + + $repository_map = $query->getIdentifierMap(); + + foreach ($tags as $tag) { + $token = $tag['token']; + + $identifier = $tag['identifier']; + $repository = idx($repository_map, $identifier); + if (!$repository) { + // For now, just bail out here. Ideally, we should distingiush between + // restricted and invalid repositories. + continue; + } + + $drequest = DiffusionRequest::newFromDictionary( + array( + 'user' => $viewer, + 'repository' => $repository, + )); + + $options = $tag['options']; + + $line = $options['line']; + $commit = $options['commit']; + $ref_name = $options['ref']; + + $link_uri = $drequest->generateURI( + array( + 'action' => 'browse', + 'path' => $tag['path'], + 'commit' => $commit, + 'line' => $line, + 'branch' => $ref_name, + )); + + $view = id(new DiffusionSourceLinkView()) + ->setRepository($repository) + ->setPath($tag['path']) + ->setURI($link_uri); + + if ($line !== null) { + $view->setLine($line); + } + + if ($commit !== null) { + $view->setCommit($commit); + } + + if ($ref_name !== null) { + $view->setRefName($ref_name); + } + + $engine->overwriteStoredText($token, $view); + } + } + + private function newSyntaxHint($text) { + return id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setColor('red') + ->setIcon('fa-exclamation-triangle') + ->setName($text); + } + +} From c458b50b856963a0f367d37e79bdeb4adf652478 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Apr 2019 11:18:42 -0700 Subject: [PATCH 16/42] Render charts from storage instead of just one ad-hoc hard-coded chart Summary: Ref T13279. This changes the chart controller: - if we have no arguments, build a demo chart and redirect to it; - otherwise, load the specified chart from storage and render it. This mostly prepares for "Chart" panels on dashboards. Test Plan: Visited `/fact/chart/`, got redirected to a chart from storage. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20483 --- src/__phutil_library_map__.php | 4 + .../PhabricatorFactApplication.php | 4 +- .../fact/chart/PhabricatorChartDataset.php | 37 +++ .../fact/chart/PhabricatorChartFunction.php | 4 + ...PhabricatorChartFunctionArgumentParser.php | 4 + .../PhabricatorFactChartController.php | 214 +++++------------- .../fact/engine/PhabricatorChartEngine.php | 214 ++++++++++++++++++ .../fact/storage/PhabricatorFactChart.php | 48 +++- 8 files changed, 370 insertions(+), 159 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorChartDataset.php create mode 100644 src/applications/fact/engine/PhabricatorChartEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6777c69bd0..3ecee44659 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2663,6 +2663,8 @@ phutil_register_library_map(array( 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', + 'PhabricatorChartDataset' => 'applications/fact/chart/PhabricatorChartDataset.php', + 'PhabricatorChartEngine' => 'applications/fact/engine/PhabricatorChartEngine.php', 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', @@ -8669,6 +8671,8 @@ phutil_register_library_map(array( 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChartAxis' => 'Phobject', 'PhabricatorChartDataQuery' => 'Phobject', + 'PhabricatorChartDataset' => 'Phobject', + 'PhabricatorChartEngine' => 'Phobject', 'PhabricatorChartFunction' => 'Phobject', 'PhabricatorChartFunctionArgument' => 'Phobject', 'PhabricatorChartFunctionArgumentParser' => 'Phobject', diff --git a/src/applications/fact/application/PhabricatorFactApplication.php b/src/applications/fact/application/PhabricatorFactApplication.php index 2b493b7bea..b3e0417754 100644 --- a/src/applications/fact/application/PhabricatorFactApplication.php +++ b/src/applications/fact/application/PhabricatorFactApplication.php @@ -30,7 +30,9 @@ final class PhabricatorFactApplication extends PhabricatorApplication { return array( '/fact/' => array( '' => 'PhabricatorFactHomeController', - '(?chart|draw)/' => 'PhabricatorFactChartController', + 'chart/' => 'PhabricatorFactChartController', + 'chart/(?P[^/]+)/(?:(?Pdraw)/)?' => + 'PhabricatorFactChartController', 'object/(?[^/]+)/' => 'PhabricatorFactObjectController', ), ); diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php new file mode 100644 index 0000000000..3251808188 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorChartDataset.php @@ -0,0 +1,37 @@ +function; + } + + public static function newFromDictionary(array $map) { + PhutilTypeSpec::checkMap( + $map, + array( + 'function' => 'list', + )); + + $dataset = new self(); + + $dataset->function = id(new PhabricatorComposeChartFunction()) + ->setArguments(array($map['function'])); + + return $dataset; + } + + public function toDictionary() { + // Since we wrap the raw value in a "compose(...)", when deserializing, + // we need to unwrap it when serializing. + $function_raw = head($this->getFunction()->toDictionary()); + + return array( + 'function' => $function_raw, + ); + } + +} diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php index 414147da56..b4a66645ad 100644 --- a/src/applications/fact/chart/PhabricatorChartFunction.php +++ b/src/applications/fact/chart/PhabricatorChartFunction.php @@ -43,6 +43,10 @@ abstract class PhabricatorChartFunction return $this; } + public function toDictionary() { + return $this->getArgumentParser()->getRawArguments(); + } + public function getSubfunctions() { $result = array(); $result[] = $this; diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php index 04342ed4cc..01fec105a3 100644 --- a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php @@ -103,6 +103,10 @@ final class PhabricatorChartFunctionArgumentParser return array_values($this->argumentMap); } + public function getRawArguments() { + return $this->rawArguments; + } + public function parseArguments() { $have_count = count($this->rawArguments); $want_count = count($this->argumentMap); diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php index 24d54a1cf6..cfda18e759 100644 --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -5,13 +5,60 @@ 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(); + } + + $chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + if (!$chart) { + return new Aphront404Response(); + } + + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer) + ->setChart($chart); + // When drawing a chart, we send down a placeholder piece of HTML first, // then fetch the data via async request. Determine if we're drawing // the structure or actually pulling the data. $mode = $request->getURIData('mode'); - $is_chart_mode = ($mode === 'chart'); $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(); + + if ($is_draw_mode) { + return id(new AphrontAjaxResponse())->setContent($chart_data); + } + + $chart_view = $engine->newChartView(); + return $this->newChartResponse($chart_view); + } + + private function newChartResponse($chart_view) { + $box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Chart')) + ->appendChild($chart_view); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Chart')) + ->setBorder(true); + + $title = pht('Chart'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($box); + } + + private function newDemoChart() { + $viewer = $this->getViewer(); + $argvs = array(); $argvs[] = array('fact', 'tasks.count.create'); @@ -40,165 +87,24 @@ final class PhabricatorFactChartController extends PhabricatorFactController { array('shift', 800), ); - $functions = array(); - foreach ($argvs as $argv) { - $functions[] = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); - } - - $subfunctions = array(); - foreach ($functions as $function) { - foreach ($function->getSubfunctions() as $subfunction) { - $subfunctions[] = $subfunction; - } - } - - foreach ($subfunctions as $subfunction) { - $subfunction->loadData(); - } - - list($domain_min, $domain_max) = $this->getDomain($functions); - - $axis = id(new PhabricatorChartAxis()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max); - - $data_query = id(new PhabricatorChartDataQuery()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max) - ->setLimit(2000); - $datasets = array(); - foreach ($functions as $function) { - $points = $function->newDatapoints($data_query); - - $x = array(); - $y = array(); - - foreach ($points as $point) { - $x[] = $point['x']; - $y[] = $point['y']; - } - - $datasets[] = array( - 'x' => $x, - 'y' => $y, - 'color' => '#ff00ff', - ); + foreach ($argvs as $argv) { + $datasets[] = PhabricatorChartDataset::newFromDictionary( + array( + 'function' => $argv, + )); } + $chart = id(new PhabricatorFactChart()) + ->setDatasets($datasets); - $y_min = 0; - $y_max = 0; - foreach ($datasets as $dataset) { - if (!$dataset['y']) { - continue; - } + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer) + ->setChart($chart); - $y_min = min($y_min, min($dataset['y'])); - $y_max = max($y_max, max($dataset['y'])); - } + $chart = $engine->getStoredChart(); - $chart_data = array( - 'datasets' => $datasets, - 'xMin' => $domain_min, - 'xMax' => $domain_max, - 'yMin' => $y_min, - 'yMax' => $y_max, - ); - - // TODO: Move this back up, it's just down here for now to make - // debugging easier so the main page throws a more visible exception when - // something goes wrong. - if ($is_chart_mode) { - return $this->newChartResponse(); - } - - return id(new AphrontAjaxResponse())->setContent($chart_data); + return id(new AphrontRedirectResponse())->setURI($chart->getURI()); } - private function newChartResponse() { - $request = $this->getRequest(); - $chart_node_id = celerity_generate_unique_node_id(); - - $chart_view = phutil_tag( - 'div', - array( - 'id' => $chart_node_id, - 'style' => 'background: #ffffff; '. - 'height: 480px; ', - ), - ''); - - $data_uri = $request->getRequestURI(); - $data_uri->setPath('/fact/draw/'); - - Javelin::initBehavior( - 'line-chart', - array( - 'chartNodeID' => $chart_node_id, - 'dataURI' => (string)$data_uri, - )); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Chart')) - ->appendChild($chart_view); - - $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb(pht('Chart')) - ->setBorder(true); - - $title = pht('Chart'); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($box); - - } - - private function getDomain(array $functions) { - $domain_min_list = null; - $domain_max_list = null; - - foreach ($functions as $function) { - $domain = $function->getDomain(); - - list($function_min, $function_max) = $domain; - - if ($function_min !== null) { - $domain_min_list[] = $function_min; - } - - if ($function_max !== null) { - $domain_max_list[] = $function_max; - } - } - - $domain_min = null; - $domain_max = null; - - if ($domain_min_list) { - $domain_min = min($domain_min_list); - } - - if ($domain_max_list) { - $domain_max = max($domain_max_list); - } - - // If we don't have any domain data from the actual functions, pick a - // plausible domain automatically. - - if ($domain_max === null) { - $domain_max = PhabricatorTime::getNow(); - } - - if ($domain_min === null) { - $domain_min = $domain_max - phutil_units('365 days in seconds'); - } - - return array($domain_min, $domain_max); - } - - } diff --git a/src/applications/fact/engine/PhabricatorChartEngine.php b/src/applications/fact/engine/PhabricatorChartEngine.php new file mode 100644 index 0000000000..ef962e2173 --- /dev/null +++ b/src/applications/fact/engine/PhabricatorChartEngine.php @@ -0,0 +1,214 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setChart(PhabricatorFactChart $chart) { + $this->chart = $chart; + return $this; + } + + public function getChart() { + return $this->chart; + } + + public function getStoredChart() { + if (!$this->storedChart) { + $chart = $this->getChart(); + $chart_key = $chart->getChartKey(); + if (!$chart_key) { + $chart_key = $chart->newChartKey(); + + $stored_chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + if ($stored_chart) { + $chart = $stored_chart; + } else { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + try { + $chart->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + $chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + if (!$chart) { + throw new Exception( + pht( + 'Failed to load chart with key "%s" after key collision. '. + 'This should not be possible.', + $chart_key)); + } + } + + unset($unguarded); + } + $this->setChart($chart); + } + + $this->storedChart = $chart; + } + + return $this->storedChart; + } + + public function newChartView() { + $chart = $this->getStoredChart(); + $chart_key = $chart->getChartKey(); + + $chart_node_id = celerity_generate_unique_node_id(); + + $chart_view = phutil_tag( + 'div', + array( + 'id' => $chart_node_id, + 'style' => 'background: #ffffff; '. + 'height: 480px; ', + ), + ''); + + $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); + + Javelin::initBehavior( + 'line-chart', + array( + 'chartNodeID' => $chart_node_id, + 'dataURI' => (string)$data_uri, + )); + + return $chart_view; + } + + public function newChartData() { + $chart = $this->getStoredChart(); + $chart_key = $chart->getChartKey(); + + $datasets = $chart->getDatasets(); + + $functions = array(); + foreach ($datasets as $dataset) { + $functions[] = $dataset->getFunction(); + } + + $subfunctions = array(); + foreach ($functions as $function) { + foreach ($function->getSubfunctions() as $subfunction) { + $subfunctions[] = $subfunction; + } + } + + foreach ($subfunctions as $subfunction) { + $subfunction->loadData(); + } + + list($domain_min, $domain_max) = $this->getDomain($functions); + + $axis = id(new PhabricatorChartAxis()) + ->setMinimumValue($domain_min) + ->setMaximumValue($domain_max); + + $data_query = id(new PhabricatorChartDataQuery()) + ->setMinimumValue($domain_min) + ->setMaximumValue($domain_max) + ->setLimit(2000); + + $datasets = array(); + foreach ($functions as $function) { + $points = $function->newDatapoints($data_query); + + $x = array(); + $y = array(); + + foreach ($points as $point) { + $x[] = $point['x']; + $y[] = $point['y']; + } + + $datasets[] = array( + 'x' => $x, + 'y' => $y, + 'color' => '#ff00ff', + ); + } + + + $y_min = 0; + $y_max = 0; + foreach ($datasets as $dataset) { + if (!$dataset['y']) { + continue; + } + + $y_min = min($y_min, min($dataset['y'])); + $y_max = max($y_max, max($dataset['y'])); + } + + $chart_data = array( + 'datasets' => $datasets, + 'xMin' => $domain_min, + 'xMax' => $domain_max, + 'yMin' => $y_min, + 'yMax' => $y_max, + ); + + return $chart_data; + } + + private function getDomain(array $functions) { + $domain_min_list = null; + $domain_max_list = null; + + foreach ($functions as $function) { + $domain = $function->getDomain(); + + list($function_min, $function_max) = $domain; + + if ($function_min !== null) { + $domain_min_list[] = $function_min; + } + + if ($function_max !== null) { + $domain_max_list[] = $function_max; + } + } + + $domain_min = null; + $domain_max = null; + + if ($domain_min_list) { + $domain_min = min($domain_min_list); + } + + if ($domain_max_list) { + $domain_max = max($domain_max_list); + } + + // If we don't have any domain data from the actual functions, pick a + // plausible domain automatically. + + if ($domain_max === null) { + $domain_max = PhabricatorTime::getNow(); + } + + if ($domain_min === null) { + $domain_min = $domain_max - phutil_units('365 days in seconds'); + } + + return array($domain_min, $domain_max); + } + +} diff --git a/src/applications/fact/storage/PhabricatorFactChart.php b/src/applications/fact/storage/PhabricatorFactChart.php index 7829010037..515b5f0a72 100644 --- a/src/applications/fact/storage/PhabricatorFactChart.php +++ b/src/applications/fact/storage/PhabricatorFactChart.php @@ -7,6 +7,8 @@ final class PhabricatorFactChart protected $chartKey; protected $chartParameters = array(); + private $datasets; + protected function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( @@ -33,6 +35,12 @@ final class PhabricatorFactChart return idx($this->chartParameters, $key, $default); } + public function newChartKey() { + $digest = serialize($this->chartParameters); + $digest = PhabricatorHash::digestForIndex($digest); + return $digest; + } + public function save() { if ($this->getID()) { throw new Exception( @@ -41,14 +49,46 @@ final class PhabricatorFactChart 'overwrite an existing chart configuration.')); } - $digest = serialize($this->chartParameters); - $digest = PhabricatorHash::digestForIndex($digest); - - $this->chartKey = $digest; + $this->chartKey = $this->newChartKey(); return parent::save(); } + public function setDatasets(array $datasets) { + assert_instances_of($datasets, 'PhabricatorChartDataset'); + + $dataset_list = array(); + foreach ($datasets as $dataset) { + $dataset_list[] = $dataset->toDictionary(); + } + + $this->setChartParameter('datasets', $dataset_list); + $this->datasets = null; + + return $this; + } + + public function getDatasets() { + if ($this->datasets === null) { + $this->datasets = $this->newDatasets(); + } + return $this->datasets; + } + + private function newDatasets() { + $datasets = $this->getChartParameter('datasets', array()); + + foreach ($datasets as $key => $dataset) { + $datasets[$key] = PhabricatorChartDataset::newFromDictionary($dataset); + } + + return $datasets; + } + + public function getURI() { + return urisprintf('/fact/chart/%s/', $this->getChartKey()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { From ff6b13872ccfa6f2cc8861eb7960fae706652412 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Apr 2019 11:23:38 -0700 Subject: [PATCH 17/42] Add a rough "Chart" Dashboard Panel Summary: Depends on D20484. Ref T13279. Allows a chart to render as a panel. Configuring these is currently quite low-level (you have to manually copy/paste a chart key in), but works well enough. Test Plan: {F6412708} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20485 --- src/__phutil_library_map__.php | 4 + .../PhabricatorDashboardChartPanelType.php | 76 +++++++++++++++++++ ...torDashboardChartPanelChartTransaction.php | 12 +++ .../PhabricatorFactChartController.php | 11 +-- .../fact/engine/PhabricatorChartEngine.php | 18 +++++ 5 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php create mode 100644 src/applications/dashboard/xaction/panel/PhabricatorDashboardChartPanelChartTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3ecee44659..867fce8822 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2937,6 +2937,8 @@ phutil_register_library_map(array( 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 'PhabricatorDashboardApplicationInstallWorkflow' => 'applications/dashboard/install/PhabricatorDashboardApplicationInstallWorkflow.php', 'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardArchiveController.php', + 'PhabricatorDashboardChartPanelChartTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardChartPanelChartTransaction.php', + 'PhabricatorDashboardChartPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php', 'PhabricatorDashboardColumn' => 'applications/dashboard/layoutconfig/PhabricatorDashboardColumn.php', 'PhabricatorDashboardConsoleController' => 'applications/dashboard/controller/PhabricatorDashboardConsoleController.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', @@ -8983,6 +8985,8 @@ phutil_register_library_map(array( 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardApplicationInstallWorkflow' => 'PhabricatorDashboardInstallWorkflow', 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardChartPanelChartTransaction' => 'PhabricatorDashboardPanelPropertyTransaction', + 'PhabricatorDashboardChartPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardColumn' => 'Phobject', 'PhabricatorDashboardConsoleController' => 'PhabricatorDashboardController', 'PhabricatorDashboardController' => 'PhabricatorController', diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php new file mode 100644 index 0000000000..880d9c9dc2 --- /dev/null +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php @@ -0,0 +1,76 @@ +setKey('chartKey') + ->setLabel(pht('Chart')) + ->setTransactionType( + PhabricatorDashboardChartPanelChartTransaction::TRANSACTIONTYPE) + ->setValue($panel->getProperty('chartKey', '')); + + return array( + $chart_field, + ); + } + + public function renderPanelContent( + PhabricatorUser $viewer, + PhabricatorDashboardPanel $panel, + PhabricatorDashboardPanelRenderingEngine $engine) { + + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer); + + $chart = $engine->loadChart($panel->getProperty('chartKey')); + if (!$chart) { + return pht('no such chart!'); + } + + return $engine->newChartView(); + } + + public function adjustPanelHeader( + PhabricatorUser $viewer, + PhabricatorDashboardPanel $panel, + PhabricatorDashboardPanelRenderingEngine $engine, + PHUIHeaderView $header) { + + $key = $panel->getProperty('chartKey'); + $uri = PhabricatorChartEngine::getChartURI($key); + + $icon = id(new PHUIIconView()) + ->setIcon('fa-area-chart'); + + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View Chart')) + ->setIcon($icon) + ->setHref($uri) + ->setColor(PHUIButtonView::GREY); + + $header->addActionLink($button); + + return $header; + } + + +} diff --git a/src/applications/dashboard/xaction/panel/PhabricatorDashboardChartPanelChartTransaction.php b/src/applications/dashboard/xaction/panel/PhabricatorDashboardChartPanelChartTransaction.php new file mode 100644 index 0000000000..fad5a4b191 --- /dev/null +++ b/src/applications/dashboard/xaction/panel/PhabricatorDashboardChartPanelChartTransaction.php @@ -0,0 +1,12 @@ +newDemoChart(); } - $chart = id(new PhabricatorFactChart())->loadOneWhere( - 'chartKey = %s', - $chart_key); + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer); + + $chart = $engine->loadChart($chart_key); if (!$chart) { return new Aphront404Response(); } - $engine = id(new PhabricatorChartEngine()) - ->setViewer($viewer) - ->setChart($chart); - // When drawing a chart, we send down a placeholder piece of HTML first, // then fetch the data via async request. Determine if we're drawing // the structure or actually pulling the data. diff --git a/src/applications/fact/engine/PhabricatorChartEngine.php b/src/applications/fact/engine/PhabricatorChartEngine.php index ef962e2173..fb8c8e1151 100644 --- a/src/applications/fact/engine/PhabricatorChartEngine.php +++ b/src/applications/fact/engine/PhabricatorChartEngine.php @@ -25,6 +25,24 @@ final class PhabricatorChartEngine return $this->chart; } + public function loadChart($chart_key) { + $chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + + if ($chart) { + $this->setChart($chart); + } + + return $chart; + } + + public static function getChartURI($chart_key) { + return id(new PhabricatorFactChart()) + ->setChartKey($chart_key) + ->getURI(); + } + public function getStoredChart() { if (!$this->storedChart) { $chart = $this->getChart(); From f8ebc71b8f217ed156f416ddb4cd028dcaa28174 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Apr 2019 11:55:22 -0700 Subject: [PATCH 18/42] Replace the chart in Maniphest Reports with a chart driven by Facts Summary: Depends on D20485. Ref T13279. This removes the ad-hoc charting in Maniphest and replaces it with a Facts-based chart. (To do this, we build a dashboard panel inline and render it.) Test Plan: {F6412720} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20486 --- .../fact/chart/PhabricatorChartDataset.php | 5 ++ .../PhabricatorChartFunctionArgument.php | 5 ++ .../chart/PhabricatorFactChartFunction.php | 37 +++++++--- .../fact/fact/PhabricatorFact.php | 41 ++++++++++- .../controller/ManiphestReportController.php | 73 ++++++++++++++----- 5 files changed, 130 insertions(+), 31 deletions(-) diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php index 3251808188..48355c3b36 100644 --- a/src/applications/fact/chart/PhabricatorChartDataset.php +++ b/src/applications/fact/chart/PhabricatorChartDataset.php @@ -9,6 +9,11 @@ final class PhabricatorChartDataset return $this->function; } + public function setFunction(PhabricatorComposeChartFunction $function) { + $this->function = $function; + return $this; + } + public static function newFromDictionary(array $map) { PhutilTypeSpec::checkMap( $map, diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php index bbcf8209ba..91786dc9da 100644 --- a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php @@ -30,6 +30,7 @@ final class PhabricatorChartFunctionArgument 'fact-key' => true, 'function' => true, 'number' => true, + 'phid' => true, ); if (!isset($types[$type])) { @@ -51,6 +52,10 @@ final class PhabricatorChartFunctionArgument public function newValue($value) { switch ($this->getType()) { + case 'phid': + // TODO: This could be validated better, but probably should not be + // a primitive type. + return $value; case 'fact-key': if (!is_string($value)) { throw new Exception( diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php index ea59d3459e..ae2ba52472 100644 --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -35,25 +35,38 @@ final class PhabricatorFactChartFunction $conn = $table->establishConnection('r'); $table_name = $table->getTableName(); - $data = queryfx_all( + $where = array(); + + $where[] = qsprintf( $conn, - 'SELECT value, epoch FROM %T WHERE keyID = %d ORDER BY epoch ASC', - $table_name, + 'keyID = %d', $key_id); - if (!$data) { - return; + + $parser = $this->getArgumentParser(); + + $parts = $fact->buildWhereClauseParts($conn, $parser); + foreach ($parts as $part) { + $where[] = $part; } + $data = queryfx_all( + $conn, + 'SELECT value, epoch FROM %T WHERE %LA ORDER BY epoch ASC', + $table_name, + $where); + $map = array(); - foreach ($data as $row) { - $value = (int)$row['value']; - $epoch = (int)$row['epoch']; + if ($data) { + foreach ($data as $row) { + $value = (int)$row['value']; + $epoch = (int)$row['epoch']; - if (!isset($map[$epoch])) { - $map[$epoch] = 0; + if (!isset($map[$epoch])) { + $map[$epoch] = 0; + } + + $map[$epoch] += $value; } - - $map[$epoch] += $value; } $this->map = $map; diff --git a/src/applications/fact/fact/PhabricatorFact.php b/src/applications/fact/fact/PhabricatorFact.php index a52fe5435e..a30f34fa56 100644 --- a/src/applications/fact/fact/PhabricatorFact.php +++ b/src/applications/fact/fact/PhabricatorFact.php @@ -38,7 +38,46 @@ abstract class PhabricatorFact extends Phobject { abstract protected function newTemplateDatapoint(); final public function getFunctionArguments() { - return array(); + $key = $this->getKey(); + + $argv = array(); + + if (preg_match('/\.project\z/', $key)) { + $argv[] = id(new PhabricatorChartFunctionArgument()) + ->setName('phid') + ->setType('phid'); + } + + if (preg_match('/\.owner\z/', $key)) { + $argv[] = id(new PhabricatorChartFunctionArgument()) + ->setName('phid') + ->setType('phid'); + } + + return $argv; } + final public function buildWhereClauseParts( + AphrontDatabaseConnection $conn, + PhabricatorChartFunctionArgumentParser $arguments) { + $where = array(); + + $has_phid = $this->getFunctionArguments(); + + if ($has_phid) { + $phid = $arguments->getArgumentValue('phid'); + + $dimension_id = id(new PhabricatorFactObjectDimension()) + ->newDimensionID($phid); + + $where[] = qsprintf( + $conn, + 'dimensionID = %d', + $dimension_id); + } + + return $where; + } + + } diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 90210a0ee4..4e5c57c76e 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -337,7 +337,8 @@ final class ManiphestReportController extends ManiphestController { 'the project recently, it is counted on the day it was '. 'opened, not the day it was categorized. If a task was part '. 'of this project in the past but no longer is, it is not '. - 'counted at all.'); + 'counted at all. This table may not agree exactly with the chart '. + 'above.'); $header = pht('Task Burn Rate for Project %s', $handle->renderLink()); $caption = phutil_tag('p', array(), $inst); } else { @@ -379,26 +380,62 @@ final class ManiphestReportController extends ManiphestController { list($burn_x, $burn_y) = $this->buildSeries($data); - require_celerity_resource('d3'); - require_celerity_resource('phui-chart-css'); + if ($project_phid) { + $argv = array( + 'sum', + array( + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.assign.project', $project_phid), + ), + ); + } else { + $argv = array( + 'sum', + array('accumulate', array('fact', 'tasks.open-count.create')), + array('accumulate', array('fact', 'tasks.open-count.status')), + ); + } - Javelin::initBehavior('line-chart-legacy', array( - 'hardpoint' => $id, - 'x' => array( - $burn_x, - ), - 'y' => array( - $burn_y, - ), - 'xformat' => 'epoch', - 'yformat' => 'int', - )); + $function = id(new PhabricatorComposeChartFunction()) + ->setArguments(array($argv)); - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Burnup Rate')) - ->appendChild($chart); + $datasets = array( + id(new PhabricatorChartDataset()) + ->setFunction($function), + ); - return array($filter, $box, $panel); + $chart = id(new PhabricatorFactChart()) + ->setDatasets($datasets); + + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer) + ->setChart($chart); + + $chart = $engine->getStoredChart(); + + $panel_type = id(new PhabricatorDashboardChartPanelType()) + ->getPanelTypeKey(); + + $chart_panel = id(new PhabricatorDashboardPanel()) + ->setPanelType($panel_type) + ->setName(pht('Burnup Rate')) + ->setProperty('chartKey', $chart->getChartKey()); + + $chart_view = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($chart_panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); + + return array($filter, $chart_view, $panel); } private function renderReportFilters(array $tokens, $has_window) { From 146317f2c447b5a717d97932bb17e7097036b982 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 29 Apr 2019 12:00:58 -0700 Subject: [PATCH 19/42] Remove the legacy chart behavior from Maniphest Summary: Depends on D20486. Ref T13279. Now that the "Reports" UI uses a panel to draw a real chart from Facts, throw away the copy of the old code. Test Plan: `grep` Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20487 --- resources/celerity/map.php | 8 -- .../maniphest/behavior-line-chart-legacy.js | 126 ------------------ 2 files changed, 134 deletions(-) delete mode 100644 webroot/rsrc/js/application/maniphest/behavior-line-chart-legacy.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6184c2c7ae..c425693ed8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -398,7 +398,6 @@ return array( 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', - 'rsrc/js/application/maniphest/behavior-line-chart-legacy.js' => 'faf3ab6b', 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'ad258e28', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', @@ -628,7 +627,6 @@ return array( 'javelin-behavior-launch-icon-composer' => 'a17b84f1', 'javelin-behavior-lightbox-attachments' => 'c7e748bf', 'javelin-behavior-line-chart' => 'ad258e28', - 'javelin-behavior-line-chart-legacy' => 'faf3ab6b', 'javelin-behavior-linked-container' => '74446546', 'javelin-behavior-maniphest-batch-selector' => '139ef688', 'javelin-behavior-maniphest-list-editor' => 'c687e867', @@ -2182,12 +2180,6 @@ return array( 'fa74cc35' => array( 'phui-oi-list-view-css', ), - 'faf3ab6b' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-vector', - 'phui-chart-css', - ), 'fcb0c07d' => array( 'phui-chart-css', 'd3', diff --git a/webroot/rsrc/js/application/maniphest/behavior-line-chart-legacy.js b/webroot/rsrc/js/application/maniphest/behavior-line-chart-legacy.js deleted file mode 100644 index 187f39a8d4..0000000000 --- a/webroot/rsrc/js/application/maniphest/behavior-line-chart-legacy.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @provides javelin-behavior-line-chart-legacy - * @requires javelin-behavior - * javelin-dom - * javelin-vector - * phui-chart-css - */ - -JX.behavior('line-chart-legacy', function(config) { - - function fn(n) { - return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')'; - } - - var h = JX.$(config.hardpoint); - var d = JX.Vector.getDim(h); - - var padding = { - top: 24, - left: 48, - bottom: 48, - right: 32 - }; - - var size = { - frameWidth: d.x, - frameHeight: d.y, - }; - - size.width = size.frameWidth - padding.left - padding.right; - size.height = size.frameHeight - padding.top - padding.bottom; - - var x = d3.time.scale() - .range([0, size.width]); - - var y = d3.scale.linear() - .range([size.height, 0]); - - var xAxis = d3.svg.axis() - .scale(x) - .orient('bottom'); - - var yAxis = d3.svg.axis() - .scale(y) - .orient('left'); - - var svg = d3.select('#' + config.hardpoint).append('svg') - .attr('width', size.frameWidth) - .attr('height', size.frameHeight) - .attr('class', 'chart'); - - var g = svg.append('g') - .attr('transform', fn('translate', padding.left, padding.top)); - - g.append('rect') - .attr('class', 'inner') - .attr('width', size.width) - .attr('height', size.height); - - var line = d3.svg.line() - .x(function(d) { return x(d.date); }) - .y(function(d) { return y(d.count); }); - - var data = []; - for (var ii = 0; ii < config.x[0].length; ii++) { - data.push( - { - date: new Date(config.x[0][ii] * 1000), - count: +config.y[0][ii] - }); - } - - x.domain(d3.extent(data, function(d) { return d.date; })); - - var yex = d3.extent(data, function(d) { return d.count; }); - yex[0] = 0; - yex[1] = yex[1] * 1.05; - y.domain(yex); - - g.append('path') - .datum(data) - .attr('class', 'line') - .attr('d', line); - - g.append('g') - .attr('class', 'x axis') - .attr('transform', fn('translate', 0, size.height)) - .call(xAxis); - - g.append('g') - .attr('class', 'y axis') - .attr('transform', fn('translate', 0, 0)) - .call(yAxis); - - var div = d3.select('body') - .append('div') - .attr('class', 'chart-tooltip') - .style('opacity', 0); - - g.selectAll('dot') - .data(data) - .enter() - .append('circle') - .attr('class', 'point') - .attr('r', 3) - .attr('cx', function(d) { return x(d.date); }) - .attr('cy', function(d) { return y(d.count); }) - .on('mouseover', function(d) { - var d_y = d.date.getFullYear(); - - // NOTE: Javascript months are zero-based. See PHI1017. - var d_m = d.date.getMonth() + 1; - - var d_d = d.date.getDate(); - - div - .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.count) - .style('opacity', 0.9) - .style('left', (d3.event.pageX - 60) + 'px') - .style('top', (d3.event.pageY - 38) + 'px'); - }) - .on('mouseout', function() { - div.style('opacity', 0); - }); - -}); From 10afe1f2b59c08e53549c9081d0da9ffc5e69f1f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Apr 2019 06:49:28 -0700 Subject: [PATCH 20/42] Fix handling of "null" domain values in Charts Summary: Depends on D20487. If you `min(1, 2, null)`, you get `null`. We want `1`. Test Plan: Viewed a "burnup for project X" chart where one dataseries had no datapoints. Saw a sensible domain selected automatically. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Differential Revision: https://secure.phabricator.com/D20488 --- .../fact/chart/PhabricatorHigherOrderChartFunction.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php index 519e602a80..aef8f948be 100644 --- a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php +++ b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php @@ -10,8 +10,12 @@ abstract class PhabricatorHigherOrderChartFunction $domain = $function->getDomain(); if ($domain !== null) { list($min, $max) = $domain; - $minv[] = $min; - $maxv[] = $max; + if ($min !== null) { + $minv[] = $min; + } + if ($max !== null) { + $maxv[] = $max; + } } } From f87c1ac362a7c90c19a86601e8abbb7b8d07f492 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 30 Apr 2019 06:49:28 -0700 Subject: [PATCH 21/42] Start the fact daemon in "bin/phd start" Summary: Depends on D20488. Ref T13279. When installs run `bin/phd start`, start the fact daemon alongside other daemons. Since "Reports" in Maniphest now relies on Facts data, populate it. Test Plan: Ran `bin/phd start`, saw the Fact daemon start. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20489 --- .../daemon/management/PhabricatorDaemonManagementWorkflow.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 77f32c293f..d5b4ed23e5 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -368,6 +368,10 @@ abstract class PhabricatorDaemonManagementWorkflow 'class' => 'PhabricatorTriggerDaemon', 'label' => 'trigger', ), + array( + 'class' => 'PhabricatorFactDaemon', + 'label' => 'fact', + ), array( 'class' => 'PhabricatorTaskmasterDaemon', 'label' => 'task', From 0aee3da19e6a27ae6f0f05f8f2d769535923f7b9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 3 May 2019 11:44:05 -0700 Subject: [PATCH 22/42] Add a "Reports" menu item to Projects Summary: Ref T13279. Since the use cases that have made it upstream are all for relatively complex charts (e.g., requiring aggregation and composition of multiple data series in nontrivial ways) I'm currently looking at an overall approach like this: - At least for now, Charts provides a low-level internal-only API for composing charts from raw datasets. - This is exposed to users through pre-built `SearchEngine`-like interfaces that provide a small number of more manageable controls (show chart from date X to date Y, show projects A, B, C), but not the full set of composition features (`compose(scale(2), cos())` and such). - Eventually, we may put more UI on the raw chart composition stuff and let you build your own fully custom charts by gluing together datasets and functions. - Or we may add this stuff in piecemeal to the higher-level UI as tools like "add goal line" or "add trend line" or whatever. This will let the low-level API mature/evolve a bit before users get hold of it directly, if they ever do. Most requests today are likely satisfiable with a small number of chart engines plus raw API data access, so maybe UI access to flexible charting is far away. Step toward this by adding a "Reports" section to projects. For now, this just renders a basic burnup for the current project. Followups will add an "Engine" layer above this and make the chart it produces more useful. Test Plan: {F6426984} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20495 --- src/__phutil_library_map__.php | 4 + .../PhabricatorProjectApplication.php | 2 + .../PhabricatorProjectReportsController.php | 96 +++++++++++++++++++ .../PhabricatorProjectProfileMenuEngine.php | 4 + ...abricatorProjectReportsProfileMenuItem.php | 80 ++++++++++++++++ .../project/storage/PhabricatorProject.php | 5 + 6 files changed, 191 insertions(+) create mode 100644 src/applications/project/controller/PhabricatorProjectReportsController.php create mode 100644 src/applications/project/menuitem/PhabricatorProjectReportsProfileMenuItem.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 867fce8822..2be86fe4e4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4245,6 +4245,8 @@ phutil_register_library_map(array( 'PhabricatorProjectProjectPHIDType' => 'applications/project/phid/PhabricatorProjectProjectPHIDType.php', 'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php', 'PhabricatorProjectRemoveHeraldAction' => 'applications/project/herald/PhabricatorProjectRemoveHeraldAction.php', + 'PhabricatorProjectReportsController' => 'applications/project/controller/PhabricatorProjectReportsController.php', + 'PhabricatorProjectReportsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectReportsProfileMenuItem.php', 'PhabricatorProjectSchemaSpec' => 'applications/project/storage/PhabricatorProjectSchemaSpec.php', 'PhabricatorProjectSearchEngine' => 'applications/project/query/PhabricatorProjectSearchEngine.php', 'PhabricatorProjectSearchField' => 'applications/project/searchfield/PhabricatorProjectSearchField.php', @@ -10496,6 +10498,8 @@ phutil_register_library_map(array( 'PhabricatorProjectProjectPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectRemoveHeraldAction' => 'PhabricatorProjectHeraldAction', + 'PhabricatorProjectReportsController' => 'PhabricatorProjectController', + 'PhabricatorProjectReportsProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorProjectSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorProjectSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectSearchField' => 'PhabricatorSearchTokenizerField', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 46d7558f5b..d8e78c5f7d 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -71,6 +71,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', 'cover/' => 'PhabricatorProjectCoverController', + 'reports/(?P[1-9]\d*)/' => + 'PhabricatorProjectReportsController', 'board/(?P[1-9]\d*)/' => array( 'edit/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnEditController', diff --git a/src/applications/project/controller/PhabricatorProjectReportsController.php b/src/applications/project/controller/PhabricatorProjectReportsController.php new file mode 100644 index 0000000000..4a08dd6103 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectReportsController.php @@ -0,0 +1,96 @@ +getViewer(); + + $response = $this->loadProject(); + if ($response) { + return $response; + } + + $project = $this->getProject(); + $id = $project->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_REPORTS); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Reports')); + $crumbs->setBorder(true); + + $project_phid = $project->getPHID(); + + $argv = array( + 'sum', + array( + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.assign.project', $project_phid), + ), + ); + + $function = id(new PhabricatorComposeChartFunction()) + ->setArguments(array($argv)); + + $datasets = array( + id(new PhabricatorChartDataset()) + ->setFunction($function), + ); + + $chart = id(new PhabricatorFactChart()) + ->setDatasets($datasets); + + $engine = id(new PhabricatorChartEngine()) + ->setViewer($viewer) + ->setChart($chart); + + $chart = $engine->getStoredChart(); + + $panel_type = id(new PhabricatorDashboardChartPanelType()) + ->getPanelTypeKey(); + + $chart_panel = id(new PhabricatorDashboardPanel()) + ->setPanelType($panel_type) + ->setName(pht('%s: Burndown', $project->getName())) + ->setProperty('chartKey', $chart->getChartKey()); + + $chart_view = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($chart_panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); + + $view = id(new PHUITwoColumnView()) + ->setFooter( + array( + $chart_view, + )); + + return $this->newPage() + ->setNavigation($nav) + ->setCrumbs($crumbs) + ->setTitle(array($project->getName(), pht('Reports'))) + ->appendChild($view); + } + +} diff --git a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php index 813cd01781..5ce4a3001b 100644 --- a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php @@ -37,6 +37,10 @@ final class PhabricatorProjectProfileMenuEngine ->setBuiltinKey(PhabricatorProject::ITEM_WORKBOARD) ->setMenuItemKey(PhabricatorProjectWorkboardProfileMenuItem::MENUITEMKEY); + $items[] = $this->newItem() + ->setBuiltinKey(PhabricatorProject::ITEM_REPORTS) + ->setMenuItemKey(PhabricatorProjectReportsProfileMenuItem::MENUITEMKEY); + $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_MEMBERS) ->setMenuItemKey(PhabricatorProjectMembersProfileMenuItem::MENUITEMKEY); diff --git a/src/applications/project/menuitem/PhabricatorProjectReportsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectReportsProfileMenuItem.php new file mode 100644 index 0000000000..d1350238f5 --- /dev/null +++ b/src/applications/project/menuitem/PhabricatorProjectReportsProfileMenuItem.php @@ -0,0 +1,80 @@ +getViewer(); + + if (!PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { + return false; + } + + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfileMenuItemConfiguration $config) { + $name = $config->getMenuItemProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + $project = $config->getProfileObject(); + + $id = $project->getID(); + $uri = $project->getReportsURI(); + $name = $this->getDisplayName($config); + + $item = $this->newItemView() + ->setURI($uri) + ->setName($name) + ->setIcon('fa-area-chart'); + + return array( + $item, + ); + } + +} diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 67ab05f5fd..54267829d3 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -58,6 +58,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO const ITEM_PROFILE = 'project.profile'; const ITEM_POINTS = 'project.points'; const ITEM_WORKBOARD = 'project.workboard'; + const ITEM_REPORTS = 'project.reports'; const ITEM_MEMBERS = 'project.members'; const ITEM_MANAGE = 'project.manage'; const ITEM_MILESTONES = 'project.milestones'; @@ -396,6 +397,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO return urisprintf('/project/board/%d/', $this->getID()); } + public function getReportsURI() { + return urisprintf('/project/reports/%d/', $this->getID()); + } + public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); From 5c1b91ab457db9f3db10d8cc5e07831512645ebb Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 3 May 2019 12:14:10 -0700 Subject: [PATCH 23/42] Consolidate burndown logic into a "BurndownChartEngine" Summary: Ref T13279. For now, we need to render burndowns from both Maniphest (legacy) and Projects (new prototype). Consolidate this logic into a "BurndownChartEngine". I plan to expand this to work a bit like a "SearchEngine", and serve as a UI layer on top of the raw chart features. The old "ChartEngine" is now "ChartRenderingEngine". Test Plan: - Viewed burndowns ("burnups") in Maniphest. - Viewed burndowns in Projects. - Saw the same chart. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20496 --- src/__phutil_library_map__.php | 4 + .../PhabricatorDashboardChartPanelType.php | 4 +- .../PhabricatorFactChartController.php | 4 +- .../fact/engine/PhabricatorChartEngine.php | 234 ++---------------- .../PhabricatorChartRenderingEngine.php | 232 +++++++++++++++++ .../controller/ManiphestReportController.php | 51 +--- .../PhabricatorProjectBurndownChartEngine.php | 71 ++++++ .../PhabricatorProjectReportsController.php | 44 +--- 8 files changed, 349 insertions(+), 295 deletions(-) create mode 100644 src/applications/fact/engine/PhabricatorChartRenderingEngine.php create mode 100644 src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2be86fe4e4..4b32787a36 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2668,6 +2668,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', + 'PhabricatorChartRenderingEngine' => 'applications/fact/engine/PhabricatorChartRenderingEngine.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', @@ -4142,6 +4143,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', 'PhabricatorProjectBuiltinsExample' => 'applications/uiexample/examples/PhabricatorProjectBuiltinsExample.php', + 'PhabricatorProjectBurndownChartEngine' => 'applications/project/chart/PhabricatorProjectBurndownChartEngine.php', 'PhabricatorProjectCardView' => 'applications/project/view/PhabricatorProjectCardView.php', 'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php', 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', @@ -8680,6 +8682,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunction' => 'Phobject', 'PhabricatorChartFunctionArgument' => 'Phobject', 'PhabricatorChartFunctionArgumentParser' => 'Phobject', + 'PhabricatorChartRenderingEngine' => 'Phobject', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', 'PhabricatorChatLogChannel' => array( 'PhabricatorChatLogDAO', @@ -10383,6 +10386,7 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBuiltinsExample' => 'PhabricatorUIExample', + 'PhabricatorProjectBurndownChartEngine' => 'PhabricatorChartEngine', 'PhabricatorProjectCardView' => 'AphrontTagView', 'PhabricatorProjectColorTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectColorsConfigType' => 'PhabricatorJSONConfigType', diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php index 880d9c9dc2..927f2476c7 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardChartPanelType.php @@ -37,7 +37,7 @@ final class PhabricatorDashboardChartPanelType PhabricatorDashboardPanel $panel, PhabricatorDashboardPanelRenderingEngine $engine) { - $engine = id(new PhabricatorChartEngine()) + $engine = id(new PhabricatorChartRenderingEngine()) ->setViewer($viewer); $chart = $engine->loadChart($panel->getProperty('chartKey')); @@ -55,7 +55,7 @@ final class PhabricatorDashboardChartPanelType PHUIHeaderView $header) { $key = $panel->getProperty('chartKey'); - $uri = PhabricatorChartEngine::getChartURI($key); + $uri = PhabricatorChartRenderingEngine::getChartURI($key); $icon = id(new PHUIIconView()) ->setIcon('fa-area-chart'); diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php index fc628a706c..0a458a5f14 100644 --- a/src/applications/fact/controller/PhabricatorFactChartController.php +++ b/src/applications/fact/controller/PhabricatorFactChartController.php @@ -10,7 +10,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { return $this->newDemoChart(); } - $engine = id(new PhabricatorChartEngine()) + $engine = id(new PhabricatorChartRenderingEngine()) ->setViewer($viewer); $chart = $engine->loadChart($chart_key); @@ -95,7 +95,7 @@ final class PhabricatorFactChartController extends PhabricatorFactController { $chart = id(new PhabricatorFactChart()) ->setDatasets($datasets); - $engine = id(new PhabricatorChartEngine()) + $engine = id(new PhabricatorChartRenderingEngine()) ->setViewer($viewer) ->setChart($chart); diff --git a/src/applications/fact/engine/PhabricatorChartEngine.php b/src/applications/fact/engine/PhabricatorChartEngine.php index fb8c8e1151..d0ccca2034 100644 --- a/src/applications/fact/engine/PhabricatorChartEngine.php +++ b/src/applications/fact/engine/PhabricatorChartEngine.php @@ -1,232 +1,48 @@ viewer = $viewer; return $this; } - public function getViewer() { + final public function getViewer() { return $this->viewer; } - public function setChart(PhabricatorFactChart $chart) { - $this->chart = $chart; - return $this; + final public function getChartEngineKey() { + return $this->getPhobjectClassConstant('CHARTENGINEKEY', 32); } - public function getChart() { - return $this->chart; + abstract protected function newChart(); + + final public function buildChart() { + $viewer = $this->getViewer(); + + $chart = $this->newChart(); + + $rendering_engine = id(new PhabricatorChartRenderingEngine()) + ->setViewer($viewer) + ->setChart($chart); + + return $rendering_engine->getStoredChart(); } - public function loadChart($chart_key) { - $chart = id(new PhabricatorFactChart())->loadOneWhere( - 'chartKey = %s', - $chart_key); + final public function buildChartPanel() { + $chart = $this->buildChart(); - if ($chart) { - $this->setChart($chart); - } + $panel_type = id(new PhabricatorDashboardChartPanelType()) + ->getPanelTypeKey(); - return $chart; - } + $chart_panel = id(new PhabricatorDashboardPanel()) + ->setPanelType($panel_type) + ->setProperty('chartKey', $chart->getChartKey()); - public static function getChartURI($chart_key) { - return id(new PhabricatorFactChart()) - ->setChartKey($chart_key) - ->getURI(); - } - - public function getStoredChart() { - if (!$this->storedChart) { - $chart = $this->getChart(); - $chart_key = $chart->getChartKey(); - if (!$chart_key) { - $chart_key = $chart->newChartKey(); - - $stored_chart = id(new PhabricatorFactChart())->loadOneWhere( - 'chartKey = %s', - $chart_key); - if ($stored_chart) { - $chart = $stored_chart; - } else { - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - try { - $chart->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - $chart = id(new PhabricatorFactChart())->loadOneWhere( - 'chartKey = %s', - $chart_key); - if (!$chart) { - throw new Exception( - pht( - 'Failed to load chart with key "%s" after key collision. '. - 'This should not be possible.', - $chart_key)); - } - } - - unset($unguarded); - } - $this->setChart($chart); - } - - $this->storedChart = $chart; - } - - return $this->storedChart; - } - - public function newChartView() { - $chart = $this->getStoredChart(); - $chart_key = $chart->getChartKey(); - - $chart_node_id = celerity_generate_unique_node_id(); - - $chart_view = phutil_tag( - 'div', - array( - 'id' => $chart_node_id, - 'style' => 'background: #ffffff; '. - 'height: 480px; ', - ), - ''); - - $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); - - Javelin::initBehavior( - 'line-chart', - array( - 'chartNodeID' => $chart_node_id, - 'dataURI' => (string)$data_uri, - )); - - return $chart_view; - } - - public function newChartData() { - $chart = $this->getStoredChart(); - $chart_key = $chart->getChartKey(); - - $datasets = $chart->getDatasets(); - - $functions = array(); - foreach ($datasets as $dataset) { - $functions[] = $dataset->getFunction(); - } - - $subfunctions = array(); - foreach ($functions as $function) { - foreach ($function->getSubfunctions() as $subfunction) { - $subfunctions[] = $subfunction; - } - } - - foreach ($subfunctions as $subfunction) { - $subfunction->loadData(); - } - - list($domain_min, $domain_max) = $this->getDomain($functions); - - $axis = id(new PhabricatorChartAxis()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max); - - $data_query = id(new PhabricatorChartDataQuery()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max) - ->setLimit(2000); - - $datasets = array(); - foreach ($functions as $function) { - $points = $function->newDatapoints($data_query); - - $x = array(); - $y = array(); - - foreach ($points as $point) { - $x[] = $point['x']; - $y[] = $point['y']; - } - - $datasets[] = array( - 'x' => $x, - 'y' => $y, - 'color' => '#ff00ff', - ); - } - - - $y_min = 0; - $y_max = 0; - foreach ($datasets as $dataset) { - if (!$dataset['y']) { - continue; - } - - $y_min = min($y_min, min($dataset['y'])); - $y_max = max($y_max, max($dataset['y'])); - } - - $chart_data = array( - 'datasets' => $datasets, - 'xMin' => $domain_min, - 'xMax' => $domain_max, - 'yMin' => $y_min, - 'yMax' => $y_max, - ); - - return $chart_data; - } - - private function getDomain(array $functions) { - $domain_min_list = null; - $domain_max_list = null; - - foreach ($functions as $function) { - $domain = $function->getDomain(); - - list($function_min, $function_max) = $domain; - - if ($function_min !== null) { - $domain_min_list[] = $function_min; - } - - if ($function_max !== null) { - $domain_max_list[] = $function_max; - } - } - - $domain_min = null; - $domain_max = null; - - if ($domain_min_list) { - $domain_min = min($domain_min_list); - } - - if ($domain_max_list) { - $domain_max = max($domain_max_list); - } - - // If we don't have any domain data from the actual functions, pick a - // plausible domain automatically. - - if ($domain_max === null) { - $domain_max = PhabricatorTime::getNow(); - } - - if ($domain_min === null) { - $domain_min = $domain_max - phutil_units('365 days in seconds'); - } - - return array($domain_min, $domain_max); + return $chart_panel; } } diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php new file mode 100644 index 0000000000..7916e35704 --- /dev/null +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -0,0 +1,232 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setChart(PhabricatorFactChart $chart) { + $this->chart = $chart; + return $this; + } + + public function getChart() { + return $this->chart; + } + + public function loadChart($chart_key) { + $chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + + if ($chart) { + $this->setChart($chart); + } + + return $chart; + } + + public static function getChartURI($chart_key) { + return id(new PhabricatorFactChart()) + ->setChartKey($chart_key) + ->getURI(); + } + + public function getStoredChart() { + if (!$this->storedChart) { + $chart = $this->getChart(); + $chart_key = $chart->getChartKey(); + if (!$chart_key) { + $chart_key = $chart->newChartKey(); + + $stored_chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + if ($stored_chart) { + $chart = $stored_chart; + } else { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + + try { + $chart->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + $chart = id(new PhabricatorFactChart())->loadOneWhere( + 'chartKey = %s', + $chart_key); + if (!$chart) { + throw new Exception( + pht( + 'Failed to load chart with key "%s" after key collision. '. + 'This should not be possible.', + $chart_key)); + } + } + + unset($unguarded); + } + $this->setChart($chart); + } + + $this->storedChart = $chart; + } + + return $this->storedChart; + } + + public function newChartView() { + $chart = $this->getStoredChart(); + $chart_key = $chart->getChartKey(); + + $chart_node_id = celerity_generate_unique_node_id(); + + $chart_view = phutil_tag( + 'div', + array( + 'id' => $chart_node_id, + 'style' => 'background: #ffffff; '. + 'height: 480px; ', + ), + ''); + + $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); + + Javelin::initBehavior( + 'line-chart', + array( + 'chartNodeID' => $chart_node_id, + 'dataURI' => (string)$data_uri, + )); + + return $chart_view; + } + + public function newChartData() { + $chart = $this->getStoredChart(); + $chart_key = $chart->getChartKey(); + + $datasets = $chart->getDatasets(); + + $functions = array(); + foreach ($datasets as $dataset) { + $functions[] = $dataset->getFunction(); + } + + $subfunctions = array(); + foreach ($functions as $function) { + foreach ($function->getSubfunctions() as $subfunction) { + $subfunctions[] = $subfunction; + } + } + + foreach ($subfunctions as $subfunction) { + $subfunction->loadData(); + } + + list($domain_min, $domain_max) = $this->getDomain($functions); + + $axis = id(new PhabricatorChartAxis()) + ->setMinimumValue($domain_min) + ->setMaximumValue($domain_max); + + $data_query = id(new PhabricatorChartDataQuery()) + ->setMinimumValue($domain_min) + ->setMaximumValue($domain_max) + ->setLimit(2000); + + $datasets = array(); + foreach ($functions as $function) { + $points = $function->newDatapoints($data_query); + + $x = array(); + $y = array(); + + foreach ($points as $point) { + $x[] = $point['x']; + $y[] = $point['y']; + } + + $datasets[] = array( + 'x' => $x, + 'y' => $y, + 'color' => '#ff00ff', + ); + } + + + $y_min = 0; + $y_max = 0; + foreach ($datasets as $dataset) { + if (!$dataset['y']) { + continue; + } + + $y_min = min($y_min, min($dataset['y'])); + $y_max = max($y_max, max($dataset['y'])); + } + + $chart_data = array( + 'datasets' => $datasets, + 'xMin' => $domain_min, + 'xMax' => $domain_max, + 'yMin' => $y_min, + 'yMax' => $y_max, + ); + + return $chart_data; + } + + private function getDomain(array $functions) { + $domain_min_list = null; + $domain_max_list = null; + + foreach ($functions as $function) { + $domain = $function->getDomain(); + + list($function_min, $function_max) = $domain; + + if ($function_min !== null) { + $domain_min_list[] = $function_min; + } + + if ($function_max !== null) { + $domain_max_list[] = $function_max; + } + } + + $domain_min = null; + $domain_max = null; + + if ($domain_min_list) { + $domain_min = min($domain_min_list); + } + + if ($domain_max_list) { + $domain_max = max($domain_max_list); + } + + // If we don't have any domain data from the actual functions, pick a + // plausible domain automatically. + + if ($domain_max === null) { + $domain_max = PhabricatorTime::getNow(); + } + + if ($domain_min === null) { + $domain_min = $domain_max - phutil_units('365 days in seconds'); + } + + return array($domain_min, $domain_max); + } + +} diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 4e5c57c76e..a5141e6122 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -381,53 +381,20 @@ final class ManiphestReportController extends ManiphestController { list($burn_x, $burn_y) = $this->buildSeries($data); if ($project_phid) { - $argv = array( - 'sum', - array( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), - ), - ); + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($project_phid)) + ->execute(); } else { - $argv = array( - 'sum', - array('accumulate', array('fact', 'tasks.open-count.create')), - array('accumulate', array('fact', 'tasks.open-count.status')), - ); + $projects = array(); } - $function = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); - - $datasets = array( - id(new PhabricatorChartDataset()) - ->setFunction($function), - ); - - $chart = id(new PhabricatorFactChart()) - ->setDatasets($datasets); - - $engine = id(new PhabricatorChartEngine()) + $panel = id(new PhabricatorProjectBurndownChartEngine()) ->setViewer($viewer) - ->setChart($chart); + ->setProjects($projects) + ->buildChartPanel(); - $chart = $engine->getStoredChart(); - - $panel_type = id(new PhabricatorDashboardChartPanelType()) - ->getPanelTypeKey(); - - $chart_panel = id(new PhabricatorDashboardPanel()) - ->setPanelType($panel_type) - ->setName(pht('Burnup Rate')) - ->setProperty('chartKey', $chart->getChartKey()); + $chart_panel = $panel->setName(pht('Burnup Rate')); $chart_view = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php new file mode 100644 index 0000000000..4b4a99ecf4 --- /dev/null +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -0,0 +1,71 @@ +projects = $projects; + + return $this; + } + + public function getProjects() { + return $this->projects; + } + + protected function newChart() { + if ($this->projects !== null) { + $project_phids = mpull($this->projects, 'getPHID'); + } else { + $project_phids = null; + } + + $argvs = array(); + if ($project_phids) { + foreach ($project_phids as $project_phid) { + $argvs[] = array( + 'sum', + array( + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid), + ), + array( + 'accumulate', + array('fact', 'tasks.open-count.assign.project', $project_phid), + ), + ); + } + } else { + $argvs[] = array( + 'sum', + array('accumulate', array('fact', 'tasks.open-count.create')), + array('accumulate', array('fact', 'tasks.open-count.status')), + ); + } + + $datasets = array(); + foreach ($argvs as $argv) { + $function = id(new PhabricatorComposeChartFunction()) + ->setArguments(array($argv)); + + $datasets[] = id(new PhabricatorChartDataset()) + ->setFunction($function); + } + + $chart = id(new PhabricatorFactChart()) + ->setDatasets($datasets); + + return $chart; + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectReportsController.php b/src/applications/project/controller/PhabricatorProjectReportsController.php index 4a08dd6103..bee114917b 100644 --- a/src/applications/project/controller/PhabricatorProjectReportsController.php +++ b/src/applications/project/controller/PhabricatorProjectReportsController.php @@ -31,48 +31,12 @@ final class PhabricatorProjectReportsController $crumbs->addTextCrumb(pht('Reports')); $crumbs->setBorder(true); - $project_phid = $project->getPHID(); - - $argv = array( - 'sum', - array( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), - ), - ); - - $function = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); - - $datasets = array( - id(new PhabricatorChartDataset()) - ->setFunction($function), - ); - - $chart = id(new PhabricatorFactChart()) - ->setDatasets($datasets); - - $engine = id(new PhabricatorChartEngine()) + $chart_panel = id(new PhabricatorProjectBurndownChartEngine()) ->setViewer($viewer) - ->setChart($chart); + ->setProjects(array($project)) + ->buildChartPanel(); - $chart = $engine->getStoredChart(); - - $panel_type = id(new PhabricatorDashboardChartPanelType()) - ->getPanelTypeKey(); - - $chart_panel = id(new PhabricatorDashboardPanel()) - ->setPanelType($panel_type) - ->setName(pht('%s: Burndown', $project->getName())) - ->setProperty('chartKey', $chart->getChartKey()); + $chart_panel->setName(pht('%s: Burndown', $project->getName())); $chart_view = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) From 0776b5ca2c4d0e4f30522432ff9deb8b64cb1292 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 3 May 2019 14:40:21 -0700 Subject: [PATCH 24/42] Update D3 to the current version Summary: Ref T13279. Old D3 seems perfectly fine, but most of the good references seem to have been written by people who update D3 more than once every 10 years (???). This requires some minor API changes, see next diff. Test Plan: See next diff. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20497 --- resources/celerity/map.php | 4 +- webroot/rsrc/externals/d3/LICENSE | 31 +++++++-------- webroot/rsrc/externals/d3/README.md | 58 ++++++++++++++++++++++++++--- webroot/rsrc/externals/d3/d3.min.js | 7 +--- 4 files changed, 73 insertions(+), 27 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c425693ed8..b23ffca37c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -183,7 +183,7 @@ return array( 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', - 'rsrc/externals/d3/d3.min.js' => 'd67475f5', + 'rsrc/externals/d3/d3.min.js' => '9d068042', 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '23f8c698', 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '70983df0', 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'cd02f93b', @@ -550,7 +550,7 @@ return array( 'conpherence-participant-pane-css' => '69e0058a', 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', - 'd3' => 'd67475f5', + 'd3' => '9d068042', 'differential-changeset-view-css' => 'bde53589', 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', diff --git a/webroot/rsrc/externals/d3/LICENSE b/webroot/rsrc/externals/d3/LICENSE index 83013469b9..1d9d875edb 100644 --- a/webroot/rsrc/externals/d3/LICENSE +++ b/webroot/rsrc/externals/d3/LICENSE @@ -1,8 +1,8 @@ -Copyright (c) 2010-2014, Michael Bostock +Copyright 2010-2017 Mike Bostock All rights reserved. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. @@ -11,16 +11,17 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. +* Neither the name of the author nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY -OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/webroot/rsrc/externals/d3/README.md b/webroot/rsrc/externals/d3/README.md index eb334e2701..696df69ce1 100644 --- a/webroot/rsrc/externals/d3/README.md +++ b/webroot/rsrc/externals/d3/README.md @@ -1,9 +1,57 @@ -# Data-Driven Documents +# D3: Data-Driven Documents - + -**D3.js** is a JavaScript library for manipulating documents based on data. **D3** helps you bring data to life using HTML, SVG and CSS. D3’s emphasis on web standards gives you the full capabilities of modern browsers without tying yourself to a proprietary framework, combining powerful visualization components and a data-driven approach to DOM manipulation. +**D3** (or **D3.js**) is a JavaScript library for visualizing data using web standards. D3 helps you bring data to life using SVG, Canvas and HTML. D3 combines powerful visualization and interaction techniques with a data-driven approach to DOM manipulation, giving you the full capabilities of modern browsers and the freedom to design the right visual interface for your data. -Want to learn more? [See the wiki.](https://github.com/mbostock/d3/wiki) +## Resources -For examples, [see the gallery](https://github.com/mbostock/d3/wiki/Gallery) and [mbostock’s bl.ocks](http://bl.ocks.org/mbostock). +* [API Reference](https://github.com/d3/d3/blob/master/API.md) +* [Release Notes](https://github.com/d3/d3/releases) +* [Gallery](https://github.com/d3/d3/wiki/Gallery) +* [Examples](https://bl.ocks.org/mbostock) +* [Wiki](https://github.com/d3/d3/wiki) + +## Installing + +If you use npm, `npm install d3`. Otherwise, download the [latest release](https://github.com/d3/d3/releases/latest). The released bundle supports anonymous AMD, CommonJS, and vanilla environments. You can load directly from [d3js.org](https://d3js.org), [CDNJS](https://cdnjs.com/libraries/d3), or [unpkg](https://unpkg.com/d3/). For example: + +```html + +``` + +For the minified version: + +```html + +``` + +You can also use the standalone D3 microlibraries. For example, [d3-selection](https://github.com/d3/d3-selection): + +```html + +``` + +D3 is written using [ES2015 modules](http://www.2ality.com/2014/09/es6-modules-final.html). Create a [custom bundle using Rollup](https://bl.ocks.org/mbostock/bb09af4c39c79cffcde4), Webpack, or your preferred bundler. To import D3 into an ES2015 application, either import specific symbols from specific D3 modules: + +```js +import {scaleLinear} from "d3-scale"; +``` + +Or import everything into a namespace (here, `d3`): + +```js +import * as d3 from "d3"; +``` + +In Node: + +```js +var d3 = require("d3"); +``` + +You can also require individual modules and combine them into a `d3` object using [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign): + +```js +var d3 = Object.assign({}, require("d3-format"), require("d3-geo"), require("d3-geo-projection")); +``` diff --git a/webroot/rsrc/externals/d3/d3.min.js b/webroot/rsrc/externals/d3/d3.min.js index f878a89d69..dfbfa98c1a 100644 --- a/webroot/rsrc/externals/d3/d3.min.js +++ b/webroot/rsrc/externals/d3/d3.min.js @@ -2,8 +2,5 @@ * @provides d3 * @do-not-minify */ -!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function u(n){return!isNaN(n)}function i(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function a(n){return n.length}function o(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function s(n){return(n+="")===xa||n[0]===ba?ba+n:n}function f(n){return(n+="")[0]===ba?n.slice(1):n}function h(n){return s(n)in this._}function g(n){return(n=s(n))in this._&&delete this._[n]}function p(){var n=[];for(var t in this._)n.push(f(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function m(){this._=Object.create(null)}function y(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=_a.length;r>e;++e){var u=_a[e]+t;if(u in n)return u}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],a=0,o=i.length;o>a;a++)(u=i[a])&&t(u,a,e);return n}function Z(n){return Sa(n,za),n}function V(n){var t,e;return function(r,u,i){var a,o=n[i].update,l=o.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(a=o[t])&&++t0&&(n=n.slice(0,o));var c=La.get(n);return c&&(n=c,l=B),o?t?u:r:t?b:i}function $(n,t){return function(e){var r=oa.event;oa.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{oa.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Ta,u="click"+r,i=oa.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==qa&&(qa="onselectstart"in e?!1:x(e.style,"userSelect")),qa){var a=n(e).style,o=a[qa];a[qa]="none"}return function(n){if(i.on(r,null),qa&&(a[qa]=o),n){var t=function(){i.on(u,null)};i.on(u,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var u=r.createSVGPoint();if(0>Ra){var i=t(n);if(i.scrollX||i.scrollY){r=oa.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var a=r[0][0].getScreenCTM();Ra=!(a.f||a.e),r.remove()}}return Ra?(u.x=e.pageX,u.y=e.pageY):(u.x=e.clientX,u.y=e.clientY),u=u.matrixTransform(n.getScreenCTM().inverse()),[u.x,u.y]}var o=n.getBoundingClientRect();return[e.clientX-o.left-n.clientLeft,e.clientY-o.top-n.clientTop]}function G(){return oa.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?Ua:Math.acos(n)}function tn(n){return n>1?Ha:-1>n?-Ha:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function an(n){return(n=Math.sin(n/2))*n}function on(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(a-i)*n/60:180>n?a:240>n?i+(a-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,i=2*e-a,new yn(u(n+120),u(n),u(n-120))}function sn(n,t,e){return this instanceof sn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof sn?new sn(n.h,n.c,n.l):n instanceof hn?pn(n.l,n.a,n.b):pn((n=Sn((n=oa.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new sn(n,t,e)}function fn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Oa)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof sn?fn(n.h,n.c,n.l):Sn((n=yn(n)).r,n.g,n.b):new hn(n,t,e)}function gn(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=vn(u)*Ka,r=vn(r)*Qa,i=vn(i)*no,new yn(mn(3.2404542*u-1.5371385*r-.4985314*i),mn(-.969266*u+1.8760108*r+.041556*i),mn(.0556434*u-.2040259*r+1.0572252*i))}function pn(n,t,e){return n>0?new sn(Math.atan2(e,t)*Ia,Math.sqrt(t*t+e*e),n):new sn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function mn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function yn(n,t,e){return this instanceof yn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof yn?new yn(n.r,n.g,n.b):_n(""+n,yn,cn):new yn(n,t,e)}function Mn(n){return new yn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,u,i,a=0,o=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Nn(u[0]),Nn(u[1]),Nn(u[2]))}return(i=ro.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.slice(1),16))||(4===n.length?(a=(3840&i)>>4,a=a>>4|a,o=240&i,o=o>>4|o,l=15&i,l=l<<4|l):7===n.length&&(a=(16711680&i)>>16,o=(65280&i)>>8,l=255&i)),t(a,o,l))}function wn(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-i,l=(a+i)/2;return o?(u=.5>l?o/(a+i):o/(2-a-i),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=NaN,u=l>0&&1>l?0:r),new ln(r,u,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/Ka),u=dn((.2126729*n+.7151522*t+.072175*e)/Qa),i=dn((.0193339*n+.119192*t+.9503041*e)/no);return hn(116*u-16,500*(r-u),200*(u-i))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function u(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(i,l)}catch(r){return void a.error.call(i,r)}a.load.call(i,n)}else a.error.call(i,l)}var i={},a=oa.dispatch("beforesend","progress","load","error"),o={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=u:l.onreadystatechange=function(){l.readyState>3&&u()},l.onprogress=function(n){var t=oa.event;oa.event=n;try{a.progress.call(i,l)}finally{oa.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(c=n,i):c},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(ca(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),l.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),l.setRequestHeader)for(var s in o)l.setRequestHeader(s,o[s]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),a.beforesend.call(i,l),l.send(null==r?null:r),i},i.abort=function(){return l.abort(),i},oa.rebind(i,a,"on"),null==r?i:i.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,n:null};return io?io.n=i:uo=i,io=i,ao||(oo=clearTimeout(oo),ao=1,lo(Tn)),i}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(oo),oo=setTimeout(Tn,t)),ao=0):(ao=1,lo(Tn))}function Rn(){for(var n=Date.now(),t=uo;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=uo,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function jn(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r&&e?function(n,t){for(var u=n.length,i=[],a=0,o=r[0],l=0;u>0&&o>0&&(l+o+1>t&&(o=Math.max(1,t-l)),i.push(n.substring(u-=o,u+o)),!((l+=o+1)>t));)o=r[a=(a+1)%r.length];return i.reverse().join(e)}:y;return function(n){var e=so.exec(n),r=e[1]||" ",a=e[2]||">",o=e[3]||"-",l=e[4]||"",c=e[5],s=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1,y=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===a)&&(c=r="0",a="="),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+g.toLowerCase());case"c":y=!1;case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===l&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=fo.get(g)||Fn;var M=c&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===o?"":o;if(0>p){var l=oa.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=p;n=g(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=y?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&f&&(x=i(x,1/0));var S=v.length+x.length+b.length+(M?0:u.length),k=s>S?new Array(S=s-S+1).join(r):"";return M&&(x=i(k+x,k.length?s-b.length:1/0)),u+=v,n=x+b,("<"===a?u+n+k:">"===a?k+u+n:"^"===a?k.substring(0,S>>=1)+u+n+k.substring(S):u+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new go(e-1)),1),e}function i(n,e){return t(n=new go(+n),e),n}function a(n,r,i){var a=u(n),o=[];if(i>1)for(;r>a;)e(a)%i||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{go=Hn;var r=new Hn;return r._=n,a(r,t,e)}finally{go=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=a;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(u),l.offset=In(i),l.range=o,n}function In(n){return function(t,e){try{go=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{go=Date}}}function Yn(n){function t(n){function t(t){for(var e,u,i,a=[],o=-1,l=0;++oo;){if(r>=c)return-1;if(u=t.charCodeAt(o++),37===u){if(a=t.charAt(o++),i=C[a in vo?t.charAt(o++):a],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function s(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{go=Hn;var t=new go;return t._=n,r(t)}finally{go=Date}}var r=t(n);return e.parse=function(n){try{go=Hn;var t=r.parse(n);return t&&t._}finally{go=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=oa.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(m),k=Xn(m),N=Vn(y),E=Xn(y);p.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ho.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ho.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ho.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:ot,"%":function(){return"%"}},C={a:r,A:u,b:i,B:a,c:o,d:tt,e:tt,H:rt,I:rt,j:et,L:at,m:nt,M:ut,p:s,S:it,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Vn(n){return new RegExp("^(?:"+n.map(oa.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ut(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function it(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function at(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ot(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=Ma(t)/60|0,u=Ma(t)%60;return e+Zn(r,"0",2)+Zn(u,"0",2)}function lt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,o=a*e,l=Math.cos(t),c=Math.sin(t),s=i*c,f=u*l+s*Math.cos(o),h=s*a*Math.sin(o);So.add(Math.atan2(h,f)),r=n,u=l,i=c}var t,e,r,u,i;ko.point=function(a,o){ko.point=n,r=(t=a)*Oa,u=Math.cos(o=(e=o)*Oa/2+Ua/4),i=Math.sin(o)},ko.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function mt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function yt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return Ma(n[0]-t[0])o;++o)u.point((e=n[o])[0],e[1]);return void u.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,i.push(l),a.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,i.push(l),a.push(c)}}),a.sort(t),qt(i),qt(a),i.length){for(var o=0,l=e,c=a.length;c>o;++o)a[o].e=l=!l;for(var s,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;s=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var o=0,c=s.length;c>o;++o)u.point((f=s[o])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){s=g.p.z;for(var o=s.length-1;o>=0;--o)u.point((f=s[o])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,s=g.z,p=!p}while(!g.v);u.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r0){for(b||(i.polygonStart(),b=!0),i.lineStart();++a1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Dt))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:a,lineStart:l,lineEnd:c,polygonStart:function(){y.point=s,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=a,y.lineStart=l,y.lineEnd=c,g=oa.merge(g);var n=Ot(m,p);g.length?(b||(i.polygonStart(),b=!0),Lt(g,Ut,n,e,i)):n&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},M=Pt(),x=t(M),b=!1;return y}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ut(n,t){return((n=n.x)[0]<0?n[1]-Ha-Da:Ha-n[1])-((t=t.x)[0]<0?t[1]-Ha-Da:Ha-t[1])}function jt(n){var t,e=NaN,r=NaN,u=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(i,a){var o=i>0?Ua:-Ua,l=Ma(i-e);Ma(l-Ua)0?Ha:-Ha),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(i,r),t=0):u!==o&&l>=Ua&&(Ma(e-u)Da?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*a)):(t+r)/2}function Ht(n,t,e,r){var u;if(null==n)u=e*Ha,r.point(-Ua,u),r.point(0,u),r.point(Ua,u),r.point(Ua,0),r.point(Ua,-u),r.point(0,-u),r.point(-Ua,-u),r.point(-Ua,0),r.point(-Ua,u);else if(Ma(n[0]-t[0])>Da){var i=n[0]o;++o){var c=t[o],s=c.length;if(s)for(var f=c[0],h=f[0],g=f[1]/2+Ua/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===s&&(d=0),n=c[d];var m=n[0],y=n[1]/2+Ua/4,M=Math.sin(y),x=Math.cos(y),b=m-h,_=b>=0?1:-1,w=_*b,S=w>Ua,k=p*M;if(So.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),i+=S?b+_*ja:b,S^h>=e^m>=e){var N=yt(dt(f),dt(n));bt(N);var E=yt(u,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(a+=S^b>=0?1:-1)}if(!d++)break;h=m,p=M,v=x,f=n}}return(-Da>i||Da>i&&0>So)^1&a}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,l,c,s;return{lineStart:function(){c=l=!1,s=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=a?v?0:u(f,h):v?u(f+(0>f?Ua:-Ua),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(g=r(e,p),(wt(e,g)||wt(p,g))&&(p[0]+=Da,p[1]+=Da,v=t(p[0],p[1]))),v!==l)s=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(o&&e&&a^v){var m;d&i||!(m=r(p,e,!0))||(s=0,a?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&wt(e,p)||n.point(p[0],p[1]),e=p,l=v,i=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return s|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),u=dt(t),a=[1,0,0],o=yt(r,u),l=mt(o,o),c=o[0],s=l-c*c;if(!s)return!e&&n;var f=i*l/s,h=-i*c/s,g=yt(a,o),p=xt(a,f),v=xt(o,h);Mt(p,v);var d=g,m=mt(p,d),y=mt(d,d),M=m*m-y*(mt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-m-x)/y);if(Mt(b,p),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=Ma(E-Ua)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(Ma(b[0]-w)Ua^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-m+x)/y);return Mt(z,p),[b,_t(z)]}}}function u(t,e){var r=a?n:Ua-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),a=i>0,o=Ma(i)>Da,l=ve(n,6*Oa);return Rt(t,e,l,a?[0,-n]:[-Ua,n-Ua])}function Yt(n,t,e,r){return function(u){var i,a=u.a,o=u.b,l=a.x,c=a.y,s=o.x,f=o.y,h=0,g=1,p=s-l,v=f-c;if(i=n-l,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-l,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-c,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-c,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:l+h*p,y:c+h*v}),1>g&&(u.b={x:l+g*p,y:c+g*v}),u}}}}}}function Zt(n,t,e,r){function u(r,u){return Ma(r[0]-n)0?0:3:Ma(r[0]-e)0?2:1:Ma(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return a(n.x,t.x)}function a(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(o){function l(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,a=1,o=d[u],l=o.length,c=o[0];l>a;++a)i=o[a],c[1]<=r?i[1]>r&&Q(c,i,n)>0&&++t:i[1]<=r&&Q(c,i,n)<0&&--t,c=i;return 0!==t}function c(i,o,l,c){var s=0,f=0;if(null==i||(s=u(i,l))!==(f=u(o,l))||a(i,o)<0^l>0){do c.point(0===s||3===s?n:e,s>1?r:t);while((s=(s+l+4)%4)!==f)}else c.point(o[0],o[1])}function s(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){s(n,t)&&o.point(n,t)}function h(){C.point=p,d&&d.push(m=[]),S=!0,w=!1,b=_=NaN}function g(){v&&(p(y,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=f,w&&o.lineEnd()}function p(n,t){n=Math.max(-Fo,Math.min(Fo,n)),t=Math.max(-Fo,Math.min(Fo,t));var e=s(n,t);if(d&&m.push([n,t]),S)y=n,M=t,x=e,S=!1,e&&(o.lineStart(),o.point(n,t));else if(e&&w)o.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(o.lineStart(),o.point(r.a.x,r.a.y)),o.point(r.b.x,r.b.y),e||o.lineEnd(),k=!1):e&&(o.lineStart(),o.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,m,y,M,x,b,_,w,S,k,N=o,E=Pt(),A=Yt(n,t,e,r),C={point:f,lineStart:h,lineEnd:g,polygonStart:function(){o=E,v=[],d=[],k=!0},polygonEnd:function(){o=N,v=oa.merge(v);var t=l([n,r]),e=k&&t,u=v.length;(e||u)&&(o.polygonStart(),e&&(o.lineStart(),c(null,null,1,o),o.lineEnd()),u&&Lt(v,i,t,c,o),o.polygonEnd()),v=d=m=null}};return C}}function Vt(n){var t=0,e=Ua/3,r=oe(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Ua/180,e=n[1]*Ua/180):[t/Ua*180,e/Ua*180]},u}function Xt(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),a-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),a=Math.sqrt(i)/u;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/u,tn((i-(n*n+e*e)*u*u)/(2*u))]},e}function $t(){function n(n,t){Oo+=u*n-r*t,r=n,u=t}var t,e,r,u;Xo.point=function(i,a){Xo.point=n,t=r=i,e=u=a},Xo.lineEnd=function(){n(t,e)}}function Bt(n,t){Io>n&&(Io=n),n>Zo&&(Zo=n),Yo>t&&(Yo=t),t>Vo&&(Vo=t)}function Wt(){function n(n,t){a.push("M",n,",",t,i)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function u(){a.push("Z")}var i=Jt(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return i=Jt(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ao+=n,Co+=t,++zo}function Kt(){function n(n,r){var u=n-t,i=r-e,a=Math.sqrt(u*u+i*i);Lo+=a*(t+n)/2,qo+=a*(e+r)/2,To+=a,Gt(t=n,e=r)}var t,e;Bo.point=function(r,u){Bo.point=n,Gt(t=r,e=u)}}function Qt(){Bo.point=Gt}function ne(){function n(n,t){var e=n-r,i=t-u,a=Math.sqrt(e*e+i*i);Lo+=a*(r+n)/2,qo+=a*(u+t)/2,To+=a,a=u*n-r*t,Ro+=a*(r+n),Do+=a*(u+t),Po+=3*a,Gt(r=n,u=t)}var t,e,r,u;Bo.point=function(i,a){Bo.point=n,Gt(t=r=i,e=u=a)},Bo.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+a,e),n.arc(t,e,a,0,ja)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function u(){o.point=t}function i(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:u,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=u,o.point=t},pointRadius:function(n){return a=n,o},result:b};return o}function ee(n){function t(n){return(o?r:e)(n)}function e(t){return ie(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=i,t.lineStart()}function i(e,r){var i=dt([e,r]),a=n(e,r);u(M,x,y,b,_,w,M=a[0],x=a[1],y=e,b=i[0],_=i[1],w=i[2],o,t),t.point(M,x)}function a(){S.point=e,t.lineEnd()}function l(){ -r(),S.point=c,S.lineEnd=s}function c(n,t){i(f=n,h=t),g=M,p=x,v=b,d=_,m=w,S.point=i}function s(){u(M,x,y,b,_,w,g,p,f,v,d,m,o,t),S.lineEnd=a,a()}var f,h,g,p,v,d,m,y,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:a,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,o,l,c,s,f,h,g,p,v,d,m){var y=s-t,M=f-e,x=y*y+M*M;if(x>4*i&&d--){var b=o+g,_=l+p,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=Ma(Ma(w)-1)i||Ma((y*z+M*L)/x-.5)>.3||a>o*g+l*p+c*v)&&(u(t,e,r,o,l,c,A,C,N,b/=S,_/=S,w,d,m),m.point(A,C),u(A,C,N,b,_,w,s,f,h,g,p,v,d,m))}}var i=.5,a=Math.cos(30*Oa),o=16;return t.precision=function(n){return arguments.length?(o=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function re(n){var t=ee(function(t,e){return n([t*Ia,e*Ia])});return function(n){return le(t(n))}}function ue(n){this.stream=n}function ie(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ae(n){return oe(function(){return n})()}function oe(n){function t(n){return n=o(n[0]*Oa,n[1]*Oa),[n[0]*h+l,c-n[1]*h]}function e(n){return n=o.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Ia,n[1]*Ia]}function r(){o=Ct(a=fe(m,M,x),i);var n=i(v,d);return l=g-n[0]*h,c=p+n[1]*h,u()}function u(){return s&&(s.valid=!1,s=null),t}var i,a,o,l,c,s,f=ee(function(n,t){return n=i(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,M=0,x=0,b=jo,_=y,w=null,S=null;return t.stream=function(n){return s&&(s.valid=!1),s=le(b(a,f(_(n)))),s.valid=!0,s},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,jo):It((w=+n)*Oa),u()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):y,u()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Oa,d=n[1]%360*Oa,r()):[v*Ia,d*Ia]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Oa,M=n[1]%360*Oa,x=n.length>2?n[2]%360*Oa:0,r()):[m*Ia,M*Ia,x*Ia]},oa.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function le(n){return ie(n,function(t,e){n.point(t*Oa,e*Oa)})}function ce(n,t){return[n,t]}function se(n,t){return[n>Ua?n-ja:-Ua>n?n+ja:n,t]}function fe(n,t,e){return n?t||e?Ct(ge(n),pe(t,e)):ge(n):t||e?pe(t,e):se}function he(n){return function(t,e){return t+=n,[t>Ua?t-ja:-Ua>t?t+ja:t,e]}}function ge(n){var t=he(n);return t.invert=he(-n),t}function pe(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*r+o*u;return[Math.atan2(l*i-s*a,o*r-c*u),tn(s*i+l*a)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*i-l*a;return[Math.atan2(l*i+c*a,o*r+s*u),tn(s*r-o*u)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,a,o){var l=a*t;null!=u?(u=de(e,u),i=de(e,i),(a>0?i>u:u>i)&&(u+=a*ja)):(u=n+a*ja,i=n-.5*l);for(var c,s=u;a>0?s>i:i>s;s-=l)o.point((c=_t([e,-r*Math.cos(s),-r*Math.sin(s)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Da)%(2*Math.PI)}function me(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function ye(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),a=Math.cos(r),o=Math.sin(r),l=u*Math.cos(n),c=u*Math.sin(n),s=a*Math.cos(e),f=a*Math.sin(e),h=2*Math.asin(Math.sqrt(an(r-t)+u*a*an(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*l+t*s,u=e*c+t*f,a=e*i+t*o;return[Math.atan2(u,r)*Ia,Math.atan2(a,Math.sqrt(r*r+u*u))*Ia]}:function(){return[n*Ia,t*Ia]};return p.distance=h,p}function _e(){function n(n,u){var i=Math.sin(u*=Oa),a=Math.cos(u),o=Ma((n*=Oa)-t),l=Math.cos(o);Wo+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*i-e*a*l)*o),e*i+r*a*l),t=n,e=i,r=a}var t,e,r;Jo.point=function(u,i){t=u*Oa,e=Math.sin(i*=Oa),r=Math.cos(i),Jo.point=n},Jo.lineEnd=function(){Jo.point=Jo.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),a=Math.cos(u);return[Math.atan2(n*i,r*a),Math.asin(r&&e*i/r)]},e}function Se(n,t){function e(n,t){a>0?-Ha+Da>t&&(t=-Ha+Da):t>Ha-Da&&(t=Ha-Da);var e=a/Math.pow(u(t),i);return[e*Math.sin(i*n),a-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Ua/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),a=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=a-t,r=K(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(a/r,1/i))-Ha]},e):Ne}function ke(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return Ma(u)u;u++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var u=n[0],i=e[0],a=t[0]-u,o=r[0]-i,l=n[1],c=e[1],s=t[1]-l,f=r[1]-c,h=(o*(l-c)-f*(u-i))/(f*a-o*s);return[u+h*a,l+h*s]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function Ue(n){var t=ll.pop()||new Pe;return t.site=n,t}function je(n){Be(n),il.remove(n),ll.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,a=n.N,o=[n];je(n);for(var l=i;l.circle&&Ma(e-l.circle.x)s;++s)c=o[s],l=o[s-1],nr(c.edge,l.site,c.site,u);l=o[0],c=o[f-1],c.edge=Ke(l.site,c.site,null,u),$e(l),$e(c)}function He(n){for(var t,e,r,u,i=n.x,a=n.y,o=il._;o;)if(r=Oe(o,a)-i,r>Da)o=o.L;else{if(u=i-Ie(o,a),!(u>Da)){r>-Da?(t=o.P,e=o):u>-Da?(t=o,e=o.N):t=e=o;break}if(!o.R){t=o;break}o=o.R}var l=Ue(n);if(il.insert(t,l),t||e){if(t===e)return Be(t),e=Ue(t.site),il.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,s=c.x,f=c.y,h=n.x-s,g=n.y-f,p=e.site,v=p.x-s,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,M=v*v+d*d,x={x:(d*y-g*M)/m+s,y:(h*M-v*y)/m+f};nr(e.edge,c,p,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,p,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var a=n.P;if(!a)return-(1/0);e=a.site;var o=e.x,l=e.y,c=l-t;if(!c)return o;var s=o-r,f=1/i-1/c,h=s/c;return f?(-h+Math.sqrt(h*h-2*f*(s*s/(-2*c)-l+c/2+u-i/2)))/f+r:(r+o)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,u,i,a,o,l,c,s,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=ul,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(o=i.edges,l=o.length,a=0;l>a;)s=o[a].end(),r=s.x,u=s.y,c=o[++a%l].start(),t=c.x,e=c.y,(Ma(r-t)>Da||Ma(u-e)>Da)&&(o.splice(a,0,new tr(Qe(i.site,s,Ma(r-f)Da?{x:f,y:Ma(t-f)Da?{x:Ma(e-p)Da?{x:h,y:Ma(t-h)Da?{x:Ma(e-g)=-Pa)){var g=l*l+c*c,p=s*s+f*f,v=(f*g-c*p)/h,d=(l*p-s*g)/h,f=d+o,m=cl.pop()||new Xe;m.arc=n,m.site=u,m.x=v+a,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,M=ol._;M;)if(m.yd||d>=o)return;if(h>p){if(i){if(i.y>=c)return}else i={x:d,y:l};e={x:d,y:c}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=c)return}else i={x:(l-u)/r,y:l};e={x:(c-u)/r,y:c}}else{if(i){if(i.yg){if(i){if(i.x>=o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}else{if(i){if(i.xi||f>a||r>h||u>g)){if(p=n.point){var p,v=t-n.x,d=e-n.y,m=v*v+d*d;if(l>m){var y=Math.sqrt(l=m);r=t-y,u=e-y,i=t+y,a=e+y,o=p}}for(var M=n.nodes,x=.5*(s+h),b=.5*(f+g),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,s,f,x,b);break;case 1:c(n,x,f,h,b);break;case 2:c(n,s,b,x,g);break;case 3:c(n,x,b,h,g)}}}(n,r,u,i,a),o}function vr(n,t){n=oa.rgb(n),t=oa.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,a=t.g-r,o=t.b-u;return function(n){return"#"+bn(Math.round(e+i*n))+bn(Math.round(r+a*n))+bn(Math.round(u+o*n))}}function dr(n,t){var e,r={},u={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function mr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function yr(n,t){var e,r,u,i=fl.lastIndex=hl.lastIndex=0,a=-1,o=[],l=[];for(n+="",t+="";(e=fl.exec(n))&&(r=hl.exec(t));)(u=r.index)>i&&(u=t.slice(i,u),o[a]?o[a]+=u:o[++a]=u),(e=e[0])===(r=r[0])?o[a]?o[a]+=r:o[++a]=r:(o[++a]=null,l.push({i:a,x:mr(e,r)})),i=hl.lastIndex;return ir;++r)o[(e=l[r]).i]=e.x(n);return o.join("")})}function Mr(n,t){for(var e,r=oa.interpolators.length;--r>=0&&!(e=oa.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],u=[],i=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(Mr(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;a>e;++e)u[e]=t[e];return function(n){for(e=0;o>e;++e)u[e]=r[e](n);return u}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Ha)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ja*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ja/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=oa.hcl(n),t=oa.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,a=t.c-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return fn(e+i*n,r+a*n,u+o*n)+""}}function Dr(n,t){n=oa.hsl(n),t=oa.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,a=t.s-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return cn(e+i*n,r+a*n,u+o*n)+""}}function Pr(n,t){n=oa.lab(n),t=oa.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,a=t.a-r,o=t.b-u;return function(n){return gn(e+i*n,r+a*n,u+o*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function jr(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),u=Fr(t,e),i=Hr(Or(e,t,-u))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:mr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:mr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var u=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:u-4,x:mr(n[0],t[0])},{i:u-2,x:mr(n[1],t[1])})}else(1!==t[0]||1!==t[1])&&e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=oa.transform(n),t=oa.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,u=-1,i=r.length;++u=0;)e.push(u[r])}function au(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,a=-1;++ae;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function mu(n){return n.reduce(yu,0)}function yu(n,t){return n+t[1]}function Mu(n,t){return xu(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xu(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function bu(n){return[oa.min(n),oa.max(n)]}function _u(n,t){return n.value-t.value}function wu(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Su(n,t){n._pack_next=t,t._pack_prev=n}function ku(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Nu(n){function t(n){s=Math.min(n.x-n.r,s),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(c=e.length)){var e,r,u,i,a,o,l,c,s=1/0,f=-(1/0),h=1/0,g=-(1/0);if(e.forEach(Eu),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(u=e[1],u.x=u.r,u.y=0,t(u),c>2))for(i=e[2],zu(r,u,i),t(i),wu(r,i),r._pack_prev=i,wu(i,u),u=r._pack_next,a=3;c>a;a++){zu(r,u,i=e[a]);var p=0,v=1,d=1;for(o=u._pack_next;o!==u;o=o._pack_next,v++)if(ku(o,i)){p=1;break}if(1==p)for(l=r._pack_prev;l!==o._pack_prev&&!ku(l,i);l=l._pack_prev,d++);p?(d>v||v==d&&u.ra;a++)i=e[a],i.x-=m,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(Au)}}function Eu(n){n._pack_next=n._pack_prev=n}function Au(n){delete n._pack_next,delete n._pack_prev}function Cu(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,a=u.length;++i=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pu(n,t,e){return n.a.parent===t.parent?n.a:e}function Uu(n){return 1+oa.max(n,function(n){return n.y})}function ju(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fu(n){var t=n.children;return t&&t.length?Fu(t[0]):n}function Hu(n){var t,e=n.children;return e&&(t=e.length)?Hu(e[t-1]):n}function Ou(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Iu(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Yu(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zu(n){return n.rangeExtent?n.rangeExtent():Yu(n.range())}function Vu(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Xu(n,t){var e,r=0,u=n.length-1,i=n[r],a=n[u];return i>a&&(e=r,r=u,u=e,e=i,i=a,a=e),n[r]=t.floor(i),n[u]=t.ceil(a),n}function $u(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:wl}function Bu(n,t,e,r){var u=[],i=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]2?Bu:Vu,l=r?Wr:Br;return a=u(n,t,l,e),o=u(t,n,l,Mr),i}function i(n){return a(n)}var a,o;return i.invert=function(n){return o(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Ur)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Qu(n,t)},i.tickFormat=function(t,e){return ni(n,t,e)},i.nice=function(t){return Gu(n,t),u()},i.copy=function(){return Wu(n,t,e,r)},u()}function Ju(n,t){return oa.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gu(n,t){return Xu(n,$u(Ku(n,t)[2])),Xu(n,$u(Ku(n,t)[2])),n}function Ku(n,t){null==t&&(t=10);var e=Yu(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Qu(n,t){return oa.range.apply(oa,Ku(n,t))}function ni(n,t,e){var r=Ku(n,t);if(e){var u=so.exec(e);if(u.shift(),"s"===u[8]){var i=oa.formatPrefix(Math.max(Ma(r[0]),Ma(r[1])));return u[7]||(u[7]="."+ti(i.scale(r[2]))),u[8]="f",e=oa.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+ei(u[8],r)),e=u.join("")}else e=",."+ti(r[2])+"f";return oa.format(e)}function ti(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function ei(n,t){var e=ti(t[2]);return n in Sl?Math.abs(e-ti(Math.max(Ma(t[0]),Ma(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ri(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function a(t){return n(u(t))}return a.invert=function(t){return i(n.invert(t))},a.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),a):r},a.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),a):t},a.nice=function(){var t=Xu(r.map(u),e?Math:Nl);return n.domain(t),r=t.map(i),a},a.ticks=function(){var n=Yu(r),a=[],o=n[0],l=n[1],c=Math.floor(u(o)),s=Math.ceil(u(l)),f=t%1?2:t;if(isFinite(s-c)){if(e){for(;s>c;c++)for(var h=1;f>h;h++)a.push(i(c)*h);a.push(i(c))}else for(a.push(i(c));c++0;h--)a.push(i(c)*h);for(c=0;a[c]l;s--);a=a.slice(c,s)}return a},a.tickFormat=function(n,e){if(!arguments.length)return kl;arguments.length<2?e=kl:"function"!=typeof e&&(e=oa.format(e));var r=Math.max(1,t*n/a.ticks().length);return function(n){var a=n/i(Math.round(u(n)));return t-.5>a*t&&(a*=t),r>=a?e(n):""}},a.copy=function(){return ri(n.copy(),t,e,r)},Ju(a,n)}function ui(n,t,e){function r(t){return n(u(t))}var u=ii(t),i=ii(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Qu(e,n)},r.tickFormat=function(n,t){return ni(e,n,t)},r.nice=function(n){return r.domain(Gu(e,n))},r.exponent=function(a){return arguments.length?(u=ii(t=a),i=ii(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return ui(n.copy(),t,e)},Ju(r,n)}function ii(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ai(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):NaN))-1)%i.length]}function r(t,e){return oa.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new c;for(var i,a=-1,o=r.length;++ae?[NaN,NaN]:[e>0?o[e-1]:n[0],et?NaN:t/i+n,[t,t+1/i]},r.copy=function(){return li(n,t,e)},u()}function ci(n,t){function e(e){return e>=e?t[oa.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return ci(n,t)},e}function si(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qu(n,t)},t.tickFormat=function(t,e){return ni(n,t,e)},t.copy=function(){return si(n)},t}function fi(){return 0}function hi(n){return n.innerRadius}function gi(n){return n.outerRadius}function pi(n){return n.startAngle}function vi(n){return n.endAngle}function di(n){return n&&n.padAngle}function mi(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function yi(n,t,e,r,u){var i=n[0]-t[0],a=n[1]-t[1],o=(u?r:-r)/Math.sqrt(i*i+a*a),l=o*a,c=-o*i,s=n[0]+l,f=n[1]+c,h=t[0]+l,g=t[1]+c,p=(s+h)/2,v=(f+g)/2,d=h-s,m=g-f,y=d*d+m*m,M=e-r,x=s*g-h*f,b=(0>m?-1:1)*Math.sqrt(Math.max(0,M*M*y-x*x)),_=(x*m-d*b)/y,w=(-x*d-m*b)/y,S=(x*m+d*b)/y,k=(-x*d+m*b)/y,N=_-p,E=w-v,A=S-p,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mi(n){function t(t){function a(){c.push("M",i(n(s),o))}for(var l,c=[],s=[],f=-1,h=t.length,g=En(e),p=En(r);++f1?n.join("L"):n+"Z"}function bi(n){return n.join("L")+"Z"}function _i(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1&&u.push("H",r[0]),u.join("")}function wi(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){o=t[1],i=n[l],l++,r+="C"+(u[0]+a[0])+","+(u[1]+a[1])+","+(i[0]-o[0])+","+(i[1]-o[1])+","+i[0]+","+i[1];for(var c=2;c9&&(u=3*t/Math.sqrt(u),a[o]=u*e,a[o+1]=u*r));for(o=-1;++o<=l;)u=(n[Math.min(l,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),i.push([u||0,a[o]*u||0]);return i}function Fi(n){return n.length<3?xi(n):n[0]+Ai(n,ji(n))}function Hi(n){for(var t,e,r,u=-1,i=n.length;++u=t?a(n-t):void(s.c=a)}function a(e){var u=p.active,i=p[u];i&&(i.timer.c=null,i.timer.t=NaN,--p.count,delete p[u],i.event&&i.event.interrupt.call(n,n.__data__,i.index));for(var a in p)if(r>+a){var c=p[a];c.timer.c=null,c.timer.t=NaN,--p.count,delete p[a]}s.c=o,qn(function(){return s.c&&o(e||1)&&(s.c=null,s.t=NaN),1},0,l),p.active=r,v.event&&v.event.start.call(n,n.__data__,t),g=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&g.push(r)}),h=v.ease,f=v.duration}function o(u){for(var i=u/f,a=h(i),o=g.length;o>0;)g[--o].call(n,a);return i>=1?(v.event&&v.event.end.call(n,n.__data__,t),--p.count?delete p[r]:delete n[e],1):void 0}var l,s,f,h,g,p=n[e]||(n[e]={active:0,count:0}),v=p[r];v||(l=u.time,s=qn(i,0,l),v=p[r]={tween:new c,time:l,timer:s,delay:u.delay,duration:u.duration,ease:u.ease,index:t},u=null,++p.count)}function na(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function ta(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function ea(n){return n.toISOString()}function ra(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=oa.bisect(Gl,u);return i==Gl.length?[t.year,Ku(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Gl[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=ua(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=ua(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yu(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],ua(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ra(n.copy(),t,e)},Ju(r,n)}function ua(n){return new Date(n)}function ia(n){return JSON.parse(n.responseText)}function aa(n){var t=sa.createRange();return t.selectNode(sa.body),t.createContextualFragment(n.responseText)}var oa={version:"3.5.14"},la=[].slice,ca=function(n){return la.call(n)},sa=this.document;if(sa)try{ca(sa.documentElement.childNodes)[0].nodeType}catch(fa){ca=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),sa)try{sa.createElement("DIV").style.setProperty("opacity",0,"")}catch(ha){var ga=this.Element.prototype,pa=ga.setAttribute,va=ga.setAttributeNS,da=this.CSSStyleDeclaration.prototype,ma=da.setProperty;ga.setAttribute=function(n,t){pa.call(this,n,t+"")},ga.setAttributeNS=function(n,t,e){va.call(this,n,t,e+"")},da.setProperty=function(n,t,e){ma.call(this,n,t+"",e)}}oa.ascending=e,oa.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},oa.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ur&&(e=r)}else{for(;++u=r){e=r;break}for(;++ur&&(e=r)}return e},oa.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ue&&(e=r)}else{for(;++u=r){e=r;break}for(;++ue&&(e=r)}return e},oa.extent=function(n,t){var e,r,u,i=-1,a=n.length;if(1===arguments.length){for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},oa.sum=function(n,t){var e,r=0,i=n.length,a=-1;if(1===arguments.length)for(;++a1?l/(s-1):void 0},oa.deviation=function(){var n=oa.variance.apply(this,arguments);return n?Math.sqrt(n):n};var ya=i(e);oa.bisectLeft=ya.left,oa.bisect=oa.bisectRight=ya.right,oa.bisector=function(n){return i(1===n.length?function(t,r){return e(n(t),r)}:n)},oa.shuffle=function(n,t,e){(i=arguments.length)<3&&(e=n.length,2>i&&(t=0));for(var r,u,i=e-t;i;)u=Math.random()*i--|0,r=n[i+t],n[i+t]=n[u+t],n[u+t]=r;return n},oa.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},oa.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},oa.zip=function(){if(!(r=arguments.length))return[];for(var n=-1,t=oa.min(arguments,a),e=new Array(t);++n=0;)for(r=n[u],t=r.length;--t>=0;)e[--a]=r[t];return e};var Ma=Math.abs;oa.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,u=[],i=o(Ma(e)),a=-1;if(n*=i,t*=i,e*=i,0>e)for(;(r=n+e*++a)>t;)u.push(r/i);else for(;(r=n+e*++a)=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var l,s,f,h,g=-1,p=a.length,v=i[o++],d=new c;++g=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Ca.hasOwnProperty(e)?{space:Ca[e],local:n}:n}},Aa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=oa.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Aa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,u=-1;if(t=e.classList){for(;++uu){if("string"!=typeof n){2>u&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>u){var i=this.node();return t(i).getComputedStyle(i,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Aa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},Aa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Aa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Aa.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Aa.insert=function(n,t){return n=j(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Aa.remove=function(){return this.each(F)},Aa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new c,y=new Array(a);for(r=-1;++rr;++r)p[r]=H(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,o.push(p),l.push(g),s.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++ii;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return E(u)},Aa.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},Aa.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},Aa.size=function(){var n=0;return Y(this,function(){++n}),n};var za=[];oa.selection.enter=Z,oa.selection.enter.prototype=za,za.append=Aa.append,za.empty=Aa.empty,za.node=Aa.node,za.call=Aa.call,za.size=Aa.size,za.select=function(n){for(var t,e,r,u,i,a=[],o=-1,l=this.length;++or){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var La=oa.map({mouseenter:"mouseover",mouseleave:"mouseout"});sa&&La.forEach(function(n){"on"+n in sa&&La.remove(n)});var qa,Ta=0;oa.mouse=function(n){return J(n,k())};var Ra=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;oa.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return J(n,r)},oa.behavior.drag=function(){function n(){this.on("mousedown.drag",i).on("touchstart.drag",a)}function e(n,t,e,i,a){return function(){function o(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],p|=n|e,M=r,g({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(m.on(i+d,null).on(a+d,null),y(p),g({type:"dragend"}))}var c,s=this,f=oa.event.target,h=s.parentNode,g=r.of(s,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=oa.select(e(f)).on(i+d,o).on(a+d,l),y=W(f),M=t(h,v);u?(c=u.apply(s,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],g({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),u=null,i=e(b,oa.mouse,t,"mousemove","mouseup"),a=e(G,oa.touch,y,"touchmove","touchend");return n.origin=function(t){return arguments.length?(u=t,n):u},oa.rebind(n,r,"on")},oa.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?ca(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Da=1e-6,Pa=Da*Da,Ua=Math.PI,ja=2*Ua,Fa=ja-Da,Ha=Ua/2,Oa=Ua/180,Ia=180/Ua,Ya=Math.SQRT2,Za=2,Va=4;oa.interpolateZoom=function(n,t){var e,r,u=n[0],i=n[1],a=n[2],o=t[0],l=t[1],c=t[2],s=o-u,f=l-i,h=s*s+f*f;if(Pa>h)r=Math.log(c/a)/Ya,e=function(n){return[u+n*s,i+n*f,a*Math.exp(Ya*n*r)]};else{var g=Math.sqrt(h),p=(c*c-a*a+Va*h)/(2*a*Za*g),v=(c*c-a*a-Va*h)/(2*c*Za*g),d=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(v*v+1)-v);r=(m-d)/Ya,e=function(n){var t=n*r,e=rn(d),o=a/(Za*g)*(e*un(Ya*t+d)-en(d));return[u+o*s,i+o*f,a*e/rn(Ya*t+d)]}}return e.duration=1e3*r,e},oa.behavior.zoom=function(){function n(n){n.on(L,f).on($a+".zoom",g).on("dblclick.zoom",p).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function u(n){k.k=Math.max(A[0],Math.min(A[1],n))}function i(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function a(t,e,r,a){t.__chart__={x:k.x,y:k.y,k:k.k},u(Math.pow(2,a)),i(d=e,r),t=oa.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function o(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){o(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function s(n){--z||(n({type:"zoomend"}),d=null)}function f(){function n(){o=1,i(oa.mouse(u),h),c(a)}function r(){f.on(q,null).on(T,null),g(o),s(a)}var u=this,a=D.of(u,arguments),o=0,f=oa.select(t(u)).on(q,n).on(T,r),h=e(oa.mouse(u)),g=W(u);Ol.call(u),l(a)}function h(){function n(){var n=oa.touches(p);return g=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=oa.event.target;oa.select(t).on(x,r).on(b,o),_.push(t);for(var e=oa.event.changedTouches,u=0,i=e.length;i>u;++u)d[e[u].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var s=l[0];a(p,s,d[s.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var s=l[0],f=l[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function r(){var n,t,e,r,a=oa.touches(p);Ol.call(p);for(var o=0,l=a.length;l>o;++o,r=null)if(e=a[o],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var s=(s=e[0]-n[0])*s+(s=e[1]-n[1])*s,f=m&&Math.sqrt(s/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],u(f*g)}M=null,i(n,t),c(v)}function o(){if(oa.event.touches.length){for(var t=oa.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var u in d)return void n()}oa.selectAll(_).on(y,null),w.on(L,f).on(R,h),N(),s(v)}var g,p=this,v=D.of(p,arguments),d={},m=0,y=".zoom-"+oa.event.changedTouches[0].identifier,x="touchmove"+y,b="touchend"+y,_=[],w=oa.select(p),N=W(p);t(),l(v),w.on(L,null).on(R,t)}function g(){var n=D.of(this,arguments);y?clearTimeout(y):(Ol.call(this),v=e(d=m||oa.mouse(this)),l(n)),y=setTimeout(function(){y=null,s(n)},50),S(),u(Math.pow(2,.002*Xa())*k.k),i(d,v),c(n)}function p(){var n=oa.mouse(this),t=Math.log(k.k)/Math.LN2;a(this,n,e(n),oa.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,m,y,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Ba,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return $a||($a="onwheel"in sa?(Xa=function(){return-oa.event.deltaY*(oa.event.deltaMode?120:1)},"wheel"):"onmousewheel"in sa?(Xa=function(){return oa.event.wheelDelta},"mousewheel"):(Xa=function(){return-oa.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Fl?oa.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],u=d?d[0]:e/2,i=d?d[1]:r/2,a=oa.interpolateZoom([(u-k.x)/k.k,(i-k.y)/k.k,e/k.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=a(t),o=e/r[2];this.__chart__=k={x:u-r[0]*o,y:i-r[1]*o,k:o},c(n)}}).each("interrupt.zoom",function(){s(n)}).each("end.zoom",function(){s(n)}):(this.__chart__=k,l(n),c(n),s(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},o(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},u(+t),o(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Ba:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(m=t&&[+t[0],+t[1]],n):m},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},oa.rebind(n,D,"on")};var Xa,$a,Ba=[0,1/0];oa.color=on,on.prototype.toString=function(){return this.rgb()+""},oa.hsl=ln;var Wa=ln.prototype=new on;Wa.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Wa.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Wa.rgb=function(){return cn(this.h,this.s,this.l)},oa.hcl=sn;var Ja=sn.prototype=new on;Ja.brighter=function(n){return new sn(this.h,this.c,Math.min(100,this.l+Ga*(arguments.length?n:1)))},Ja.darker=function(n){return new sn(this.h,this.c,Math.max(0,this.l-Ga*(arguments.length?n:1)))},Ja.rgb=function(){return fn(this.h,this.c,this.l).rgb()},oa.lab=hn;var Ga=18,Ka=.95047,Qa=1,no=1.08883,to=hn.prototype=new on;to.brighter=function(n){return new hn(Math.min(100,this.l+Ga*(arguments.length?n:1)),this.a,this.b)},to.darker=function(n){return new hn(Math.max(0,this.l-Ga*(arguments.length?n:1)),this.a,this.b)},to.rgb=function(){return gn(this.l,this.a,this.b)},oa.rgb=yn;var eo=yn.prototype=new on;eo.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new yn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new yn(u,u,u)},eo.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new yn(n*this.r,n*this.g,n*this.b)},eo.hsl=function(){return wn(this.r,this.g,this.b)},eo.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ro=oa.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ro.forEach(function(n,t){ro.set(n,Mn(t))}),oa.functor=En,oa.xhr=An(y),oa.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var a=Cn(n,t,null==e?r:u(e),i);return a.row=function(n){return arguments.length?a.response(null==(e=n)?r:u(n)):e},a}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(a).join(n)}function a(n){return o.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var o=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(s>=c)return a;if(u)return u=!1,i;var t=s;if(34===n.charCodeAt(t)){for(var e=t;e++s;){var r=n.charCodeAt(s++),o=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(s)&&(++s,++o);else if(r!==l)continue;return n.slice(t,s-o)}return n.slice(t)}for(var r,u,i={},a={},o=[],c=n.length,s=0,f=0;(r=e())!==a;){for(var h=[];r!==i&&r!==a;)h.push(r),r=e();t&&null==(h=t(h,f++))||o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new m,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(a).join(n)].concat(t.map(function(t){return u.map(function(n){return a(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},oa.csv=oa.dsv(",","text/csv"),oa.tsv=oa.dsv(" ","text/tab-separated-values");var uo,io,ao,oo,lo=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};oa.timer=function(){qn.apply(this,arguments)},oa.timer.flush=function(){Rn(),Dn()},oa.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var co=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Un);oa.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=oa.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),co[8+e/3]};var so=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,fo=oa.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=oa.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ho=oa.time={},go=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){po.setUTCDate.apply(this._,arguments)},setDay:function(){po.setUTCDay.apply(this._,arguments)},setFullYear:function(){po.setUTCFullYear.apply(this._,arguments)},setHours:function(){po.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){po.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){po.setUTCMinutes.apply(this._,arguments)},setMonth:function(){po.setUTCMonth.apply(this._,arguments)},setSeconds:function(){po.setUTCSeconds.apply(this._,arguments)},setTime:function(){po.setTime.apply(this._,arguments)}};var po=Date.prototype;ho.year=On(function(n){return n=ho.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ho.years=ho.year.range,ho.years.utc=ho.year.utc.range,ho.day=On(function(n){var t=new go(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ho.days=ho.day.range,ho.days.utc=ho.day.utc.range,ho.dayOfYear=function(n){var t=ho.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ho[n]=On(function(n){return(n=ho.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ho[n+"s"]=e.range,ho[n+"s"].utc=e.utc.range,ho[n+"OfYear"]=function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)}}),ho.week=ho.sunday,ho.weeks=ho.sunday.range,ho.weeks.utc=ho.sunday.utc.range,ho.weekOfYear=ho.sundayOfYear;var vo={"-":"",_:" ",0:"0"},mo=/^\s*\d+/,yo=/^%/;oa.locale=function(n){return{numberFormat:jn(n),timeFormat:Yn(n)}};var Mo=oa.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], -shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});oa.format=Mo.numberFormat,oa.geo={},st.prototype={s:0,t:0,add:function(n){ft(n,this.t,xo),ft(xo.s,this.s,this),this.s?this.t+=xo.t:this.s=xo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var xo=new st;oa.geo.stream=function(n,t){n&&bo.hasOwnProperty(n.type)?bo[n.type](n,t):ht(n,t)};var bo={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*Ua+n:n,ko.lineStart=ko.lineEnd=ko.point=b}};oa.geo.bounds=function(){function n(n,t){M.push(x=[s=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=dt([t*Oa,e*Oa]);if(m){var u=yt(m,r),i=[u[1],-u[0],0],a=yt(i,u);bt(a),a=_t(a);var l=t-p,c=l>0?1:-1,v=a[0]*Ia*c,d=Ma(l)>180;if(d^(v>c*p&&c*t>v)){var y=a[1]*Ia;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>c*p&&c*t>v)){var y=-a[1]*Ia;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t):h>=s?(s>t&&(s=t),t>h&&(h=t)):t>p?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t)}else n(t,e);m=r,p=t}function e(){b.point=t}function r(){x[0]=s,x[1]=h,b.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=Ma(r)>180?r+(r>0?360:-360):r}else v=n,d=e;ko.point(n,e),t(n,e)}function i(){ko.lineStart()}function a(){u(v,d),ko.lineEnd(),Ma(y)>Da&&(s=-(h=180)),x[0]=s,x[1]=h,m=null}function o(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nSo?(s=-(h=180),f=-(g=90)):y>Da?g=90:-Da>y&&(f=-90),x[0]=s,x[1]=h}};return function(n){g=h=-(s=f=1/0),M=[],oa.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,u=M[0],i=[u];t>r;++r)e=M[r],c(e[0],u)||c(e[1],u)?(o(u[0],e[1])>o(u[0],u[1])&&(u[1]=e[1]),o(e[0],u[1])>o(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var a,e,p=-(1/0),t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(a=o(u[1],e[0]))>p&&(p=a,s=e[0],h=u[1])}return M=x=null,s===1/0||f===1/0?[[NaN,NaN],[NaN,NaN]]:[[s,f],[h,g]]}}(),oa.geo.centroid=function(n){No=Eo=Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,Uo);var t=Ro,e=Do,r=Po,u=t*t+e*e+r*r;return Pa>u&&(t=Lo,e=qo,r=To,Da>Eo&&(t=Ao,e=Co,r=zo),u=t*t+e*e+r*r,Pa>u)?[NaN,NaN]:[Math.atan2(e,t)*Ia,tn(r/Math.sqrt(u))*Ia]};var No,Eo,Ao,Co,zo,Lo,qo,To,Ro,Do,Po,Uo={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){Uo.lineStart=At},polygonEnd:function(){Uo.lineStart=Nt}},jo=Rt(zt,jt,Ht,[-Ua,-Ua/2]),Fo=1e9;oa.geo.clipExtent=function(){var n,t,e,r,u,i,a={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(o){return arguments.length?(i=Zt(n=+o[0][0],t=+o[0][1],e=+o[1][0],r=+o[1][1]),u&&(u.valid=!1,u=null),a):[[n,t],[e,r]]}};return a.extent([[0,0],[960,500]])},(oa.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,oa.geo.albers=function(){return oa.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},oa.geo.albersUsa=function(){function n(n){var i=n[0],a=n[1];return t=null,e(i,a),t||(r(i,a),t)||u(i,a),t}var t,e,r,u,i=oa.geo.albers(),a=oa.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),o=oa.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?a:u>=.166&&.234>u&&r>=-.214&&-.115>r?o:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),a.precision(t),o.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),a.scale(.35*t),o.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var c=i.scale(),s=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[s-.455*c,f-.238*c],[s+.455*c,f+.238*c]]).stream(l).point,r=a.translate([s-.307*c,f+.201*c]).clipExtent([[s-.425*c+Da,f+.12*c+Da],[s-.214*c-Da,f+.234*c-Da]]).stream(l).point,u=o.translate([s-.205*c,f+.212*c]).clipExtent([[s-.214*c+Da,f+.166*c+Da],[s-.115*c-Da,f+.234*c-Da]]).stream(l).point,n},n.scale(1070)};var Ho,Oo,Io,Yo,Zo,Vo,Xo={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Oo=0,Xo.lineStart=$t},polygonEnd:function(){Xo.lineStart=Xo.lineEnd=Xo.point=b,Ho+=Ma(Oo/2)}},$o={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Bo={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Bo.lineStart=ne},polygonEnd:function(){Bo.point=Gt,Bo.lineStart=Kt,Bo.lineEnd=Qt}};oa.geo.path=function(){function n(n){return n&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=u(i)),oa.geo.stream(n,a)),i.result()}function t(){return a=null,n}var e,r,u,i,a,o=4.5;return n.area=function(n){return Ho=0,oa.geo.stream(n,u(Xo)),Ho},n.centroid=function(n){return Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,u(Bo)),Po?[Ro/Po,Do/Po]:To?[Lo/To,qo/To]:zo?[Ao/zo,Co/zo]:[NaN,NaN]},n.bounds=function(n){return Zo=Vo=-(Io=Yo=1/0),oa.geo.stream(n,u($o)),[[Io,Yo],[Zo,Vo]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||re(n):y,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Wt:new te(n),"function"!=typeof o&&i.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(i.pointRadius(+t),+t),n):o},n.projection(oa.geo.albersUsa()).context(null)},oa.geo.transform=function(n){return{stream:function(t){var e=new ue(t);for(var r in n)e[r]=n[r];return e}}},ue.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},oa.geo.projection=ae,oa.geo.projectionMutator=oe,(oa.geo.equirectangular=function(){return ae(ce)}).raw=ce.invert=ce,oa.geo.rotation=function(n){function t(t){return t=n(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t}return n=fe(n[0]%360*Oa,n[1]*Oa,n.length>2?n[2]*Oa:0),t.invert=function(t){return t=n.invert(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t},t},se.invert=ce,oa.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=fe(-n[0]*Oa,-n[1]*Oa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ia,n[1]*=Ia}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Oa,u*Oa),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Oa,(u=+r)*Oa),n):u},n.angle(90)},oa.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Oa,u=n[1]*Oa,i=t[1]*Oa,a=Math.sin(r),o=Math.cos(r),l=Math.sin(u),c=Math.cos(u),s=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*a)*e+(e=c*s-l*f*o)*e),l*s+c*f*o)},oa.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return oa.range(Math.ceil(i/d)*d,u,d).map(h).concat(oa.range(Math.ceil(c/m)*m,l,m).map(g)).concat(oa.range(Math.ceil(r/p)*p,e,p).filter(function(n){return Ma(n%d)>Da}).map(s)).concat(oa.range(Math.ceil(o/v)*v,a,v).filter(function(n){return Ma(n%m)>Da}).map(f))}var e,r,u,i,a,o,l,c,s,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(l).slice(1),h(u).reverse().slice(1),g(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],c=+t[0][1],l=+t[1][1],i>u&&(t=i,i=u,u=t),c>l&&(t=c,c=l,l=t),n.precision(y)):[[i,c],[u,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,s=me(o,a,90),f=ye(r,e,y),h=me(c,l,90),g=ye(i,u,y),n):y},n.majorExtent([[-180,-90+Da],[180,90-Da]]).minorExtent([[-180,-80-Da],[180,80+Da]])},oa.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=Me,u=xe;return n.distance=function(){return oa.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},oa.geo.interpolate=function(n,t){return be(n[0]*Oa,n[1]*Oa,t[0]*Oa,t[1]*Oa)},oa.geo.length=function(n){return Wo=0,oa.geo.stream(n,Jo),Wo};var Wo,Jo={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Go=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(oa.geo.azimuthalEqualArea=function(){return ae(Go)}).raw=Go;var Ko=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},y);(oa.geo.azimuthalEquidistant=function(){return ae(Ko)}).raw=Ko,(oa.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(oa.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var Qo=we(function(n){return 1/n},Math.atan);(oa.geo.gnomonic=function(){return ae(Qo)}).raw=Qo,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ha]},(oa.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var nl=we(function(){return 1},Math.asin);(oa.geo.orthographic=function(){return ae(nl)}).raw=nl;var tl=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(oa.geo.stereographic=function(){return ae(tl)}).raw=tl,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ha]},(oa.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,oa.geom={},oa.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=En(e),i=En(r),a=n.length,o=[],l=[];for(t=0;a>t;t++)o.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(o.sort(qe),t=0;a>t;t++)l.push([o[t][0],-o[t][1]]);var c=Le(o),s=Le(l),f=s[0]===c[0],h=s[s.length-1]===c[c.length-1],g=[];for(t=c.length-1;t>=0;--t)g.push(n[o[c[t]][2]]);for(t=+f;t=r&&c.x<=i&&c.y>=u&&c.y<=a?[[r,a],[i,a],[i,u],[r,u]]:[];s.point=n[o]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Da)*Da,y:Math.round(a(n,t)/Da)*Da,i:t}})}var r=Ce,u=ze,i=r,a=u,o=sl;return n?t(n):(t.links=function(n){return or(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return or(e(n)).cells.forEach(function(e,r){for(var u,i,a=e.site,o=e.edges.sort(Ve),l=-1,c=o.length,s=o[c-1].edge,f=s.l===a?s.r:s.l;++l=c,h=r>=s,g=h<<1|f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),f?u=c:o=c,h?a=s:l=s,i(n,t,e,r,u,a,o,l)}var s,f,h,g,p,v,d,m,y,M=En(o),x=En(l);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,a)for(g=0;p>g;++g)s=n[g],s.xm&&(m=s.x),s.y>y&&(y=s.y),f.push(s.x),h.push(s.y);else for(g=0;p>g;++g){var b=+M(s=n[g],g),_=+x(s,g);v>b&&(v=b),d>_&&(d=_),b>m&&(m=b),_>y&&(y=_),f.push(b),h.push(_)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=hr();if(k.add=function(n){i(k,n,+M(n,++g),+x(n,g),v,d,m,y)},k.visit=function(n){gr(n,k,v,d,m,y)},k.find=function(n){return pr(k,n[0],n[1],v,d,m,y)},g=-1,null==t){for(;++g=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=pl.get(e)||gl,r=vl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=Ur,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new jr(e?e.matrix:dl)})(n)},jr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var dl={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++eo*o/m){if(v>l){var c=t.charge/l;n.px-=i*c,n.py-=a*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=i*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=oa.event.x,n.py=oa.event.y,l.resume()}var e,r,u,i,a,o,l={},c=oa.dispatch("start","tick","end"),s=[1,1],f=.9,h=ml,g=yl,p=-30,v=Ml,d=.1,m=.64,M=[],x=[];return l.tick=function(){if((u*=.99)<.005)return e=null,c.end({type:"end",alpha:u=0}),!0;var t,r,l,h,g,v,m,y,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,g=l.target,y=g.x-h.x,b=g.y-h.y,(v=y*y+b*b)&&(v=u*a[r]*((v=Math.sqrt(v))-i[r])/v,y*=v,b*=v,g.x-=y*(m=h.weight+g.weight?h.weight/(h.weight+g.weight):.5),g.y-=b*m,h.x+=y*(m=1-m),h.y+=b*m);if((m=u*d)&&(y=s[0]/2,b=s[1]/2,r=-1,m))for(;++r<_;)l=M[r],l.x+=(y-l.x)*m,l.y+=(b-l.y)*m;if(p)for(ru(t=oa.geom.quadtree(M),u,o),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*f,l.y-=(l.py-(l.py=l.y))*f);c.tick({type:"tick",alpha:u})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(s=n,l):s},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.friction=function(n){return arguments.length?(f=+n,l):f},l.charge=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(m=n*n,l):Math.sqrt(m)},l.alpha=function(n){return arguments.length?(n=+n,u?n>0?u=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:u=0})):n>0&&(c.start({type:"start",alpha:u=n}),e=qn(l.tick)),l):u},l.start=function(){function n(n,r){if(!e){for(e=new Array(u),l=0;u>l;++l)e[l]=[];for(l=0;c>l;++l){var i=x[l];e[i.source.index].push(i.target),e[i.target.index].push(i.source)}}for(var a,o=e[t],l=-1,s=o.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var ml=20,yl=1,Ml=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===xl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=xl,r=0,u=ja,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var xl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:bl.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:_l.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var bl=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),_l=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=s[0]&&o<=s[1]&&(a=l[oa.bisect(f,o,1,g)-1],a.y+=p,a.push(n[i]));return l}var t=!0,e=Number,r=bu,u=Mu;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return xu(n,t)}:En(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},oa.layout.pack=function(){function n(n,i){var a=e.call(this,n,i),o=a[0],l=u[0],c=u[1],s=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(o.x=o.y=0,au(o,function(n){n.r=+s(n.value)}),au(o,Nu),r){var f=r*(t?1:Math.max(2*o.r/l,2*o.r/c))/2;au(o,function(n){n.r+=f}),au(o,Nu),au(o,function(n){n.r-=f})}return Cu(o,l/2,c/2,t?1:1/Math.max(2*o.r/l,2*o.r/c)),a}var t,e=oa.layout.hierarchy().sort(_u),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},uu(n,e)},oa.layout.tree=function(){function n(n,u){var s=a.call(this,n,u),f=s[0],h=t(f);if(au(h,e),h.parent.m=-h.z,iu(h,r),c)iu(f,i);else{var g=f,p=f,v=f;iu(f,function(n){n.xp.x&&(p=n),n.depth>v.depth&&(v=n)});var d=o(g,p)/2-g.x,m=l[0]/(p.x+o(p,g)/2+d),y=l[1]/(v.depth||1);iu(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return s}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,a=0,o=i.length;o>a;++a)r.push((i[a]=u={_:i[a],parent:t,children:(u=i[a].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:a}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Du(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+o(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+o(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,a=t,l=u.parent.children[0],c=u.m,s=i.m,f=a.m,h=l.m;a=Tu(a),u=qu(u),a&&u;)l=qu(l),i=Tu(i),i.a=n,r=a.z+f-u.z-c+o(a._,u._),r>0&&(Ru(Pu(a,n,e),n,r),c+=r,s+=r),f+=a.m,c+=u.m,h+=l.m,s+=i.m;a&&!Tu(i)&&(i.t=a,i.m+=f-s),u&&!qu(l)&&(l.t=u,l.m+=c-h,e=n)}return e}function i(n){n.x*=l[0],n.y=n.depth*l[1]}var a=oa.layout.hierarchy().sort(null).value(null),o=Lu,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(o=t,n):o},n.size=function(t){return arguments.length?(c=null==(l=t)?i:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:i,n):c?l:null},uu(n,a)},oa.layout.cluster=function(){function n(n,i){var a,o=t.call(this,n,i),l=o[0],c=0;au(l,function(n){var t=n.children;t&&t.length?(n.x=ju(t),n.y=Uu(t)):(n.x=a?c+=e(n,a):0,n.y=0,a=n)});var s=Fu(l),f=Hu(l),h=s.x-e(s,f)/2,g=f.x+e(f,s)/2;return au(l,u?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),o}var t=oa.layout.hierarchy().sort(null).value(null),e=Lu,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},uu(n,t)},oa.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var a,o,l,c=f(e),s=[],h=i.slice(),p=1/0,v="slice"===g?c.dx:"dice"===g?c.dy:"slice-dice"===g?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),s.area=0;(l=h.length)>0;)s.push(a=h[l-1]),s.area+=a.area,"squarify"!==g||(o=r(s,v))<=p?(h.pop(),p=o):(s.area-=s.pop().area,u(s,v,c,!1),v=Math.min(c.dx,c.dy),s.length=s.area=0,p=1/0);s.length&&(u(s,v,c,!0),s.length=s.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,a=f(t),o=r.slice(),l=[];for(n(o,a.dx*a.dy/t.value),l.area=0;i=o.pop();)l.push(i),l.area+=i.area,null!=i.z&&(u(l,i.z?a.dx:a.dy,a,!o.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,a=-1,o=n.length;++ae&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,a=n.length,o=e.x,c=e.y,s=t?l(n.area/t):0; -if(t==e.dx){for((r||s>e.dy)&&(s=e.dy);++ie.dx)&&(s=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=oa.random.normal.apply(oa,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=oa.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},oa.scale={};var wl={floor:y,ceil:y};oa.scale.linear=function(){return Wu([0,1],[0,1],Mr,!1)};var Sl={s:1,g:1,p:1,r:1,e:1};oa.scale.log=function(){return ri(oa.scale.linear().domain([0,1]),10,!0,[1,10])};var kl=oa.format(".0e"),Nl={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};oa.scale.pow=function(){return ui(oa.scale.linear(),1,[0,1])},oa.scale.sqrt=function(){return oa.scale.pow().exponent(.5)},oa.scale.ordinal=function(){return ai([],{t:"range",a:[[]]})},oa.scale.category10=function(){return oa.scale.ordinal().range(El)},oa.scale.category20=function(){return oa.scale.ordinal().range(Al)},oa.scale.category20b=function(){return oa.scale.ordinal().range(Cl)},oa.scale.category20c=function(){return oa.scale.ordinal().range(zl)};var El=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Al=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),Cl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),zl=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);oa.scale.quantile=function(){return oi([],[])},oa.scale.quantize=function(){return li(0,1,[0,1])},oa.scale.threshold=function(){return ci([.5],[0,1])},oa.scale.identity=function(){return si([0,1])},oa.svg={},oa.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),s=a.apply(this,arguments)-Ha,f=o.apply(this,arguments)-Ha,h=Math.abs(f-s),g=s>f?0:1;if(n>c&&(p=c,c=n,n=p),h>=Fa)return t(c,g)+(n?t(n,1-g):"")+"Z";var p,v,d,m,y,M,x,b,_,w,S,k,N=0,E=0,A=[];if((m=(+l.apply(this,arguments)||0)/2)&&(d=i===Ll?Math.sqrt(n*n+c*c):+i.apply(this,arguments),g||(E*=-1),c&&(E=tn(d/c*Math.sin(m))),n&&(N=tn(d/n*Math.sin(m)))),c){y=c*Math.cos(s+E),M=c*Math.sin(s+E),x=c*Math.cos(f-E),b=c*Math.sin(f-E);var C=Math.abs(f-s-2*E)<=Ua?0:1;if(E&&mi(y,M,x,b)===g^C){var z=(s+f)/2;y=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else y=M=0;if(n){_=n*Math.cos(f-N),w=n*Math.sin(f-N),S=n*Math.cos(s+N),k=n*Math.sin(s+N);var L=Math.abs(s-f+2*N)<=Ua?0:1;if(N&&mi(_,w,S,k)===1-g^L){var q=(s+f)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Da&&(p=Math.min(Math.abs(c-n)/2,+u.apply(this,arguments)))>.001){v=c>n^g?0:1;var T=p,R=p;if(Ua>h){var D=null==S?[_,w]:null==x?[y,M]:Re([y,M],[S,k],[x,b],[_,w]),P=y-D[0],U=M-D[1],j=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*j+U*F)/(Math.sqrt(P*P+U*U)*Math.sqrt(j*j+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(p,(n-O)/(H-1)),T=Math.min(p,(c-O)/(H+1))}if(null!=x){var I=yi(null==S?[_,w]:[S,k],[y,M],c,T,g),Y=yi([x,b],[_,w],c,T,g);p===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-g^mi(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",g," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",y,",",M);if(null!=S){var Z=yi([y,M],[S,k],n,-R,g),V=yi([_,w],null==x?[y,M]:[x,b],n,-R,g);p===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",g^mi(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-g," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",y,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",g," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-g," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hi,r=gi,u=fi,i=Ll,a=pi,o=vi,l=di;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(u=En(t),n):u},n.padRadius=function(t){return arguments.length?(i=t==Ll?Ll:En(t),n):i},n.startAngle=function(t){return arguments.length?(a=En(t),n):a},n.endAngle=function(t){return arguments.length?(o=En(t),n):o},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+a.apply(this,arguments)+ +o.apply(this,arguments))/2-Ha;return[Math.cos(t)*n,Math.sin(t)*n]},n};var Ll="auto";oa.svg.line=function(){return Mi(y)};var ql=oa.map({linear:xi,"linear-closed":bi,step:_i,"step-before":wi,"step-after":Si,basis:zi,"basis-open":Li,"basis-closed":qi,bundle:Ti,cardinal:Ei,"cardinal-open":ki,"cardinal-closed":Ni,monotone:Fi});ql.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Tl=[0,2/3,1/3,0],Rl=[0,1/3,2/3,0],Dl=[0,1/6,2/3,1/6];oa.svg.line.radial=function(){var n=Mi(Hi);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wi.reverse=Si,Si.reverse=wi,oa.svg.area=function(){return Oi(y)},oa.svg.area.radial=function(){var n=Oi(Hi);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},oa.svg.chord=function(){function n(n,o){var l=t(this,i,n,o),c=t(this,a,n,o);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?u(l.r,l.p1,l.r,l.p0):u(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+u(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=o.call(n,u,r),a=l.call(n,u,r)-Ha,s=c.call(n,u,r)-Ha;return{r:i,a0:a,a1:s,p0:[i*Math.cos(a),i*Math.sin(a)],p1:[i*Math.cos(s),i*Math.sin(s)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Ua)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=Me,a=xe,o=Ii,l=pi,c=vi;return n.radius=function(t){return arguments.length?(o=En(t),n):o},n.source=function(t){return arguments.length?(i=En(t),n):i},n.target=function(t){return arguments.length?(a=En(t),n):a},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},oa.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),a=e.call(this,n,u),o=(i.y+a.y)/2,l=[i,{x:i.x,y:o},{x:a.x,y:o},a];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yi;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},oa.svg.diagonal.radial=function(){var n=oa.svg.diagonal(),t=Yi,e=n.projection;return n.projection=function(n){return arguments.length?e(Zi(t=n)):t},n},oa.svg.symbol=function(){function n(n,r){return(Pl.get(t.call(this,n,r))||$i)(e.call(this,n,r))}var t=Xi,e=Vi;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Pl=oa.map({circle:$i,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*jl)),e=t*jl;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/Ul),e=t*Ul/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/Ul),e=t*Ul/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});oa.svg.symbolTypes=Pl.keys();var Ul=Math.sqrt(3),jl=Math.tan(30*Oa);Aa.transition=function(n){for(var t,e,r=Fl||++Yl,u=Ki(n),i=[],a=Hl||{time:Date.now(),ease:Nr,delay:0,duration:250},o=-1,l=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return Wi(u,this.namespace,this.id)},Il.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(u){u[r][e].tween.set(n,t)})},Il.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a="transform"==n?$r:Mr,o=oa.ns.qualify(n);return Ji(this,"attr."+n,t,o.local?i:u)},Il.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=oa.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Il.style=function(n,e,r){function u(){this.style.removeProperty(n)}function i(e){return null==e?u:(e+="",function(){var u,i=t(this).getComputedStyle(this,null).getPropertyValue(n);return i!==e&&(u=Mr(i,e),function(t){this.style.setProperty(n,u(t),r)})})}var a=arguments.length;if(3>a){if("string"!=typeof n){2>a&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ji(this,"style."+n,e,i)},Il.styleTween=function(n,e,r){function u(u,i){var a=e.call(this,u,i,t(this).getComputedStyle(this,null).getPropertyValue(n));return a&&function(t){this.style.setProperty(n,a(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,u)},Il.text=function(n){return Ji(this,"text",n,Gi)},Il.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Il.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=oa.ease.apply(oa,arguments)),Y(this,function(r){r[e][t].ease=n}))},Il.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,u,i){r[e][t].delay=+n.call(r,r.__data__,u,i)}:(n=+n,function(r){r[e][t].delay=n}))},Il.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,u,i){r[e][t].duration=Math.max(1,n.call(r,r.__data__,u,i))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Il.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var u=Hl,i=Fl;try{Fl=e,Y(this,function(t,u,i){Hl=t[r][e],n.call(t,t.__data__,u,i)})}finally{Hl=u,Fl=i}}else Y(this,function(u){var i=u[r][e];(i.event||(i.event=oa.dispatch("start","end","interrupt"))).on(n,t)});return this},Il.transition=function(){for(var n,t,e,r,u=this.id,i=++Yl,a=this.namespace,o=[],l=0,c=this.length;c>l;l++){o.push(n=[]);for(var t=this[l],s=0,f=t.length;f>s;s++)(e=t[s])&&(r=e[a][u],Qi(e,s,a,i,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wi(o,a,i)},oa.svg.axis=function(){function n(n){n.each(function(){var n,c=oa.select(this),s=this.__chart__||e,f=this.__chart__=e.copy(),h=null==l?f.ticks?f.ticks.apply(f,o):f.domain():l,g=null==t?f.tickFormat?f.tickFormat.apply(f,o):y:t,p=c.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Da),d=oa.transition(p.exit()).style("opacity",Da).remove(),m=oa.transition(p.order()).style("opacity",1),M=Math.max(u,0)+a,x=Zu(f),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),oa.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=m.select("line"),C=p.select("text").text(g),z=v.select("text"),L=m.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=na,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*i+"V0H"+x[1]+"V"+q*i)):(n=ta,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*i+","+x[0]+"H0V"+x[1]+"H"+q*i)),E.attr(N,q*u),z.attr(k,q*M),A.attr(S,0).attr(N,q*u),L.attr(w,0).attr(k,q*M),f.rangeBand){var T=f,R=T.rangeBand()/2;s=f=function(n){return T(n)+R}}else s.rangeBand?s=f:d.call(n,f,s);v.call(n,s,f),m.call(n,f,f)})}var t,e=oa.scale.linear(),r=Zl,u=6,i=6,a=3,o=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Vl?t+"":Zl,n):r},n.ticks=function(){return arguments.length?(o=ca(arguments),n):o},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(a=+t,n):a},n.tickSubdivide=function(){return arguments.length&&n},n};var Zl="bottom",Vl={top:1,right:1,bottom:1,left:1};oa.svg.brush=function(){function n(t){t.each(function(){var t=oa.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",i).on("touchstart.brush",i),a=t.selectAll(".background").data([0]);a.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var o=t.selectAll(".resize").data(v,y);o.exit().remove(),o.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Xl[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),o.style("display",n.empty()?"none":null);var l,f=oa.transition(t),h=oa.transition(a);c&&(l=Zu(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(f)),s&&(l=Zu(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),u(f)),e(f)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+f[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",f[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1]-f[0])}function u(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function i(){function i(){32==oa.event.keyCode&&(C||(M=null,L[0]-=f[1],L[1]-=h[1],C=2),S())}function v(){32==oa.event.keyCode&&2==C&&(L[0]+=f[1],L[1]+=h[1],C=0,S())}function d(){var n=oa.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(oa.event.altKey?(M||(M=[(f[0]+f[1])/2,(h[0]+h[1])/2]),L[0]=f[+(n[0]s?(u=r,r=s):u=s),v[0]!=r||v[1]!=u?(e?o=null:a=null,v[0]=r,v[1]=u,!0):void 0}function y(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),oa.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=oa.select(oa.event.target),w=l.of(b,arguments),k=oa.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&s,C=_.classed("extent"),z=W(b),L=oa.mouse(b),q=oa.select(t(b)).on("keydown.brush",i).on("keyup.brush",v);if(oa.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",y):q.on("mousemove.brush",d).on("mouseup.brush",y),k.interrupt().selectAll("*").interrupt(),C)L[0]=f[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[f[1-T]-L[0],h[1-R]-L[1]],L[0]=f[T],L[1]=h[R]}else oa.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),oa.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var a,o,l=N(n,"brushstart","brush","brushend"),c=null,s=null,f=[0,0],h=[0,0],g=!0,p=!0,v=$l[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:f,y:h,i:a,j:o},e=this.__chart__||t;this.__chart__=t,Fl?oa.select(this).transition().each("start.brush",function(){a=e.i,o=e.j,f=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(f,t.x),r=xr(h,t.y);return a=o=null,function(u){f=t.x=e(u),h=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){a=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=$l[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,v=$l[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(g=!!t[0],p=!!t[1]):c?g=!!t:s&&(p=!!t),n):c&&s?[g,p]:c?g:s?p:null},n.extent=function(t){var e,r,u,i,l;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),a=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),(e!=f[0]||r!=f[1])&&(f=[e,r])),s&&(u=t[0],i=t[1],c&&(u=u[1],i=i[1]),o=[u,i],s.invert&&(u=s(u),i=s(i)),u>i&&(l=u,u=i,i=l),(u!=h[0]||i!=h[1])&&(h=[u,i])),n):(c&&(a?(e=a[0],r=a[1]):(e=f[0],r=f[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),s&&(o?(u=o[0],i=o[1]):(u=h[0],i=h[1],s.invert&&(u=s.invert(u),i=s.invert(i)),u>i&&(l=u,u=i,i=l))),c&&s?[[e,u],[r,i]]:c?[e,r]:s&&[u,i])},n.clear=function(){return n.empty()||(f=[0,0],h=[0,0],a=o=null),n},n.empty=function(){return!!c&&f[0]==f[1]||!!s&&h[0]==h[1]},oa.rebind(n,l,"on")};var Xl={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},$l=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Bl=ho.format=Mo.timeFormat,Wl=Bl.utc,Jl=Wl("%Y-%m-%dT%H:%M:%S.%LZ");Bl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?ea:Jl,ea.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},ea.toString=Jl.toString,ho.second=On(function(n){return new go(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ho.seconds=ho.second.range,ho.seconds.utc=ho.second.utc.range,ho.minute=On(function(n){return new go(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ho.minutes=ho.minute.range,ho.minutes.utc=ho.minute.utc.range,ho.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new go(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ho.hours=ho.hour.range,ho.hours.utc=ho.hour.utc.range,ho.month=On(function(n){return n=ho.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ho.months=ho.month.range,ho.months.utc=ho.month.utc.range;var Gl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Kl=[[ho.second,1],[ho.second,5],[ho.second,15],[ho.second,30],[ho.minute,1],[ho.minute,5],[ho.minute,15],[ho.minute,30],[ho.hour,1],[ho.hour,3],[ho.hour,6],[ho.hour,12],[ho.day,1],[ho.day,2],[ho.week,1],[ho.month,1],[ho.month,3],[ho.year,1]],Ql=Bl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),nc={range:function(n,t,e){return oa.range(Math.ceil(n/e)*e,+t,e).map(ua)},floor:y,ceil:y};Kl.year=ho.year,ho.scale=function(){return ra(oa.scale.linear(),Kl,Ql)};var tc=Kl.map(function(n){return[n[0].utc,n[1]]}),ec=Wl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);tc.year=ho.year.utc,ho.scale.utc=function(){return ra(oa.scale.linear(),tc,ec)},oa.text=An(function(n){return n.responseText}),oa.json=function(n,t){return Cn(n,"application/json",ia,t)},oa.html=function(n,t){return Cn(n,"text/html",aa,t)},oa.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=oa,define(oa)):"object"==typeof module&&module.exports?module.exports=oa:this.d3=oa}(); \ No newline at end of file +// https://d3js.org v5.9.2 Copyright 2019 Mike Bostock +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t,n){return tn?1:t>=n?0:NaN}function e(t){var e;return 1===t.length&&(e=t,t=function(t,r){return n(e(t),r)}),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}var r=e(n),i=r.right,o=r.left;function a(t,n){return[t,n]}function u(t){return null===t?NaN:+t}function c(t,n){var e,r,i=t.length,o=0,a=-1,c=0,f=0;if(null==n)for(;++a1)return f/(o-1)}function f(t,n){var e=c(t,n);return e?Math.sqrt(e):e}function s(t,n){var e,r,i,o=t.length,a=-1;if(null==n){for(;++a=e)for(r=i=e;++ae&&(r=e),i=e)for(r=i=e;++ae&&(r=e),i0)return[t];if((r=n0)for(t=Math.ceil(t/a),n=Math.floor(n/a),o=new Array(i=Math.ceil(n-t+1));++u=0?(o>=y?10:o>=_?5:o>=b?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(o>=y?10:o>=_?5:o>=b?2:1)}function w(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=y?i*=10:o>=_?i*=5:o>=b&&(i*=2),n=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,o=Math.floor(i),a=+e(t[o],o,t);return a+(+e(t[o+1],o+1,t)-a)*(i-o)}}function A(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++or&&(r=e)}else for(;++o=e)for(r=e;++or&&(r=e);return r}function T(t){for(var n,e,r,i=t.length,o=-1,a=0;++o=0;)for(n=(r=t[i]).length;--n>=0;)e[--a]=r[n];return e}function S(t,n){var e,r,i=t.length,o=-1;if(null==n){for(;++o=e)for(r=e;++oe&&(r=e)}else for(;++o=e)for(r=e;++oe&&(r=e);return r}function k(t){if(!(i=t.length))return[];for(var n=-1,e=S(t,E),r=new Array(e);++n=0&&(n=t.slice(e+1),t=t.slice(0,e)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:n}})),a=-1,u=o.length;if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++a0)for(var e,r,i=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),V.hasOwnProperty(n)?{space:V[n],local:t}:t}function W(t){var n=$(t);return(n.local?function(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}:function(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===G&&n.documentElement.namespaceURI===G?n.createElement(t):n.createElementNS(e,t)}})(n)}function Z(){}function Q(t){return null==t?Z:function(){return this.querySelector(t)}}function J(){return[]}function K(t){return null==t?J:function(){return this.querySelectorAll(t)}}function tt(t){return function(){return this.matches(t)}}function nt(t){return new Array(t.length)}function et(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}et.prototype={constructor:et,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var rt="$";function it(t,n,e,r,i,o){for(var a,u=0,c=n.length,f=o.length;un?1:t>=n?0:NaN}function ut(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function ct(t,n){return t.style.getPropertyValue(n)||ut(t).getComputedStyle(t,null).getPropertyValue(n)}function ft(t){return t.trim().split(/^|\s+/)}function st(t){return t.classList||new lt(t)}function lt(t){this._node=t,this._names=ft(t.getAttribute("class")||"")}function ht(t,n){for(var e=st(t),r=-1,i=n.length;++r=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var wt={};(t.event=null,"undefined"!=typeof document)&&("onmouseenter"in document.documentElement||(wt={mouseenter:"mouseover",mouseleave:"mouseout"}));function Mt(t,n,e){return t=Nt(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function Nt(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function At(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r=x&&(x=m+1);!(b=y[x])&&++x=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=at);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o1?this.each((null==n?function(t){return function(){this.style.removeProperty(t)}}:"function"==typeof n?function(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}:function(t,n,e){return function(){this.style.setProperty(t,n,e)}})(t,n,null==e?"":e)):ct(this.node(),t)},property:function(t,n){return arguments.length>1?this.each((null==n?function(t){return function(){delete this[t]}}:"function"==typeof n?function(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}:function(t,n){return function(){this[t]=n}})(t,n)):this.node()[t]},classed:function(t,n){var e=ft(t+"");if(arguments.length<2){for(var r=st(this.node()),i=-1,o=e.length;++i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}(t+""),a=o.length;if(!(arguments.length<2)){for(u=n?Tt:At,null==e&&(e=!1),r=0;r>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1):(n=rn.exec(t))?dn(parseInt(n[1],16)):(n=on.exec(t))?new yn(n[1],n[2],n[3],1):(n=an.exec(t))?new yn(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=un.exec(t))?pn(n[1],n[2],n[3],n[4]):(n=cn.exec(t))?pn(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=fn.exec(t))?bn(n[1],n[2]/100,n[3]/100,1):(n=sn.exec(t))?bn(n[1],n[2]/100,n[3]/100,n[4]):ln.hasOwnProperty(t)?dn(ln[t]):"transparent"===t?new yn(NaN,NaN,NaN,0):null}function dn(t){return new yn(t>>16&255,t>>8&255,255&t,1)}function pn(t,n,e,r){return r<=0&&(t=n=e=NaN),new yn(t,n,e,r)}function vn(t){return t instanceof Jt||(t=hn(t)),t?new yn((t=t.rgb()).r,t.g,t.b,t.opacity):new yn}function gn(t,n,e,r){return 1===arguments.length?vn(t):new yn(t,n,e,null==r?1:r)}function yn(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function _n(t){return((t=Math.max(0,Math.min(255,Math.round(t)||0)))<16?"0":"")+t.toString(16)}function bn(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new xn(t,n,e,r)}function mn(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof xn)return new xn(t.h,t.s,t.l,t.opacity);if(t instanceof Jt||(t=hn(t)),!t)return new xn;if(t instanceof xn)return t;var n=(t=t.rgb()).r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),a=NaN,u=o-i,c=(o+i)/2;return u?(a=n===o?(e-r)/u+6*(e0&&c<1?0:a,new xn(a,u,c,t.opacity)}(t):new xn(t,n,e,null==r?1:r)}function xn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function wn(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}Zt(Jt,hn,{displayable:function(){return this.rgb().displayable()},hex:function(){return this.rgb().hex()},toString:function(){return this.rgb()+""}}),Zt(yn,gn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new yn(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return 0<=this.r&&this.r<=255&&0<=this.g&&this.g<=255&&0<=this.b&&this.b<=255&&0<=this.opacity&&this.opacity<=1},hex:function(){return"#"+_n(this.r)+_n(this.g)+_n(this.b)},toString:function(){var t=this.opacity;return(1===(t=isNaN(t)?1:Math.max(0,Math.min(1,t)))?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),Zt(xn,mn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new xn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new yn(wn(t>=240?t-240:t+120,i,r),wn(t,i,r),wn(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Mn=Math.PI/180,Nn=180/Math.PI,An=.96422,Tn=1,Sn=.82521,kn=4/29,En=6/29,Cn=3*En*En,Pn=En*En*En;function zn(t){if(t instanceof Dn)return new Dn(t.l,t.a,t.b,t.opacity);if(t instanceof Fn){if(isNaN(t.h))return new Dn(t.l,0,0,t.opacity);var n=t.h*Mn;return new Dn(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof yn||(t=vn(t));var e,r,i=On(t.r),o=On(t.g),a=On(t.b),u=qn((.2225045*i+.7168786*o+.0606169*a)/Tn);return i===o&&o===a?e=r=u:(e=qn((.4360747*i+.3850649*o+.1430804*a)/An),r=qn((.0139322*i+.0971045*o+.7141733*a)/Sn)),new Dn(116*u-16,500*(e-u),200*(u-r),t.opacity)}function Rn(t,n,e,r){return 1===arguments.length?zn(t):new Dn(t,n,e,null==r?1:r)}function Dn(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function qn(t){return t>Pn?Math.pow(t,1/3):t/Cn+kn}function Ln(t){return t>En?t*t*t:Cn*(t-kn)}function Un(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function On(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Bn(t){if(t instanceof Fn)return new Fn(t.h,t.c,t.l,t.opacity);if(t instanceof Dn||(t=zn(t)),0===t.a&&0===t.b)return new Fn(NaN,0,t.l,t.opacity);var n=Math.atan2(t.b,t.a)*Nn;return new Fn(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Yn(t,n,e,r){return 1===arguments.length?Bn(t):new Fn(t,n,e,null==r?1:r)}function Fn(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}Zt(Dn,Rn,Qt(Jt,{brighter:function(t){return new Dn(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Dn(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return new yn(Un(3.1338561*(n=An*Ln(n))-1.6168667*(t=Tn*Ln(t))-.4906146*(e=Sn*Ln(e))),Un(-.9787684*n+1.9161415*t+.033454*e),Un(.0719453*n-.2289914*t+1.4052427*e),this.opacity)}})),Zt(Fn,Yn,Qt(Jt,{brighter:function(t){return new Fn(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Fn(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return zn(this).rgb()}}));var In=-.14861,jn=1.78277,Hn=-.29227,Xn=-.90649,Gn=1.97294,Vn=Gn*Xn,$n=Gn*jn,Wn=jn*Hn-Xn*In;function Zn(t,n,e,r){return 1===arguments.length?function(t){if(t instanceof Qn)return new Qn(t.h,t.s,t.l,t.opacity);t instanceof yn||(t=vn(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(Wn*r+Vn*n-$n*e)/(Wn+Vn-$n),o=r-i,a=(Gn*(e-i)-Hn*o)/Xn,u=Math.sqrt(a*a+o*o)/(Gn*i*(1-i)),c=u?Math.atan2(a,o)*Nn-120:NaN;return new Qn(c<0?c+360:c,u,i,t.opacity)}(t):new Qn(t,n,e,null==r?1:r)}function Qn(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Jn(t,n,e,r,i){var o=t*t,a=o*t;return((1-3*t+3*o-a)*n+(4-6*o+3*a)*e+(1+3*t+3*o-3*a)*r+a*i)/6}function Kn(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],a=r>0?t[r-1]:2*i-o,u=r180||e<-180?e-360*Math.round(e/360):e):ne(isNaN(t)?n:t)}function ie(t){return 1==(t=+t)?oe:function(n,e){return e-n?function(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}(n,e,t):ne(isNaN(n)?e:n)}}function oe(t,n){var e=n-t;return e?ee(t,e):ne(isNaN(t)?n:t)}Zt(Qn,Zn,Qt(Jt,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Qn(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Qn(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Mn,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new yn(255*(n+e*(In*r+jn*i)),255*(n+e*(Hn*r+Xn*i)),255*(n+e*(Gn*r)),this.opacity)}}));var ae=function t(n){var e=ie(n);function r(t,n){var r=e((t=gn(t)).r,(n=gn(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),a=oe(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=a(n),t+""}}return r.gamma=t,r}(1);function ue(t){return function(n){var e,r,i=n.length,o=new Array(i),a=new Array(i),u=new Array(i);for(e=0;eo&&(i=n.slice(o,i),u[a]?u[a]+=i:u[++a]=i),(e=e[0])===(r=r[0])?u[a]?u[a]+=r:u[++a]=r:(u[++a]=null,c.push({i:a,x:he(e,r)})),o=ve.lastIndex;return o180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:he(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}(o.rotate,a.rotate,u,c),function(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:he(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}(o.skewX,a.skewX,u,c),function(t,n,e,r,o,a){if(t!==e||n!==r){var u=o.push(i(o)+"scale(",null,",",null,")");a.push({i:u-4,x:he(t,e)},{i:u-2,x:he(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,u,c),o=a=null,function(t){for(var n,e=-1,r=c.length;++e=0&&n._call.call(null,t),n=n._next;--Ge}function ar(){Qe=(Ze=Ke.now())+Je,Ge=Ve=0;try{or()}finally{Ge=0,function(){var t,n,e=He,r=1/0;for(;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:He=n);Xe=t,cr(r)}(),Qe=0}}function ur(){var t=Ke.now(),n=t-Ze;n>We&&(Je-=n,Ze=t)}function cr(t){Ge||(Ve&&(Ve=clearTimeout(Ve)),t-Qe>24?(t<1/0&&(Ve=setTimeout(ar,t-Ke.now()-Je)),$e&&($e=clearInterval($e))):($e||(Ze=Ke.now(),$e=setInterval(ur,We)),Ge=1,tr(ar)))}function fr(t,n,e){var r=new rr;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}rr.prototype=ir.prototype={constructor:rr,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?nr():+e)+(null==n?0:+n),this._next||Xe===this||(Xe?Xe._next=this:He=this,Xe=this),this._call=t,this._time=e,cr()},stop:function(){this._call&&(this._call=null,this._time=1/0,cr())}};var sr=I("start","end","cancel","interrupt"),lr=[],hr=0,dr=1,pr=2,vr=3,gr=4,yr=5,_r=6;function br(t,n,e,r,i,o){var a=t.__transition;if(a){if(e in a)return}else t.__transition={};!function(t,n,e){var r,i=t.__transition;function o(c){var f,s,l,h;if(e.state!==dr)return u();for(f in i)if((h=i[f]).name===e.name){if(h.state===vr)return fr(o);h.state===gr?(h.state=_r,h.timer.stop(),h.on.call("interrupt",t,t.__data__,h.index,h.group),delete i[f]):+fhr)throw new Error("too late; already scheduled");return e}function xr(t,n){var e=wr(t,n);if(e.state>vr)throw new Error("too late; already running");return e}function wr(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("transition not found");return e}function Mr(t,n){var e,r,i,o=t.__transition,a=!0;if(o){for(i in n=null==n?null:n+"",o)(e=o[i]).name===n?(r=e.state>pr&&e.state=0&&(t=t.slice(0,n)),!t||"start"===t})}(n)?mr:xr;return function(){var a=o(this,t),u=a.on;u!==r&&(i=(r=u).copy()).on(n,e),a.on=i}}(e,t,n))},attr:function(t,n){var e=$(t),r="transform"===e?ke:Ar;return this.attrTween(t,"function"==typeof n?(e.local?function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttributeNS(t.space,t.local))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttributeNS(t.space,t.local)}}:function(t,n,e){var r,i,o;return function(){var a,u,c=e(this);if(null!=c)return(a=this.getAttribute(t))===(u=c+"")?null:a===r&&u===i?o:(i=u,o=n(r=a,c));this.removeAttribute(t)}})(e,r,Nr(this,"attr."+t,n)):null==n?(e.local?function(t){return function(){this.removeAttributeNS(t.space,t.local)}}:function(t){return function(){this.removeAttribute(t)}})(e):(e.local?function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttributeNS(t.space,t.local);return a===o?null:a===r?i:i=n(r=a,e)}}:function(t,n,e){var r,i,o=e+"";return function(){var a=this.getAttribute(t);return a===o?null:a===r?i:i=n(r=a,e)}})(e,r,n))},attrTween:function(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=$(t);return this.tween(e,(r.local?function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttributeNS(t.space,t.local,n(e))}}(t,i)),e}return i._value=n,i}:function(t,n){var e,r;function i(){var i=n.apply(this,arguments);return i!==r&&(e=(r=i)&&function(t,n){return function(e){this.setAttribute(t,n(e))}}(t,i)),e}return i._value=n,i})(r,n))},style:function(t,n,e){var r="transform"==(t+="")?Se:Ar;return null==n?this.styleTween(t,function(t,n){var e,r,i;return function(){var o=ct(this,t),a=(this.style.removeProperty(t),ct(this,t));return o===a?null:o===e&&a===r?i:i=n(e=o,r=a)}}(t,r)).on("end.style."+t,Sr(t)):"function"==typeof n?this.styleTween(t,function(t,n,e){var r,i,o;return function(){var a=ct(this,t),u=e(this),c=u+"";return null==u&&(this.style.removeProperty(t),c=u=ct(this,t)),a===c?null:a===r&&c===i?o:(i=c,o=n(r=a,u))}}(t,r,Nr(this,"style."+t,n))).each(function(t,n){var e,r,i,o,a="style."+n,u="end."+a;return function(){var c=xr(this,t),f=c.on,s=null==c.value[a]?o||(o=Sr(n)):void 0;f===e&&i===s||(r=(e=f).copy()).on(u,i=s),c.on=r}}(this._id,t)):this.styleTween(t,function(t,n,e){var r,i,o=e+"";return function(){var a=ct(this,t);return a===o?null:a===r?i:i=n(r=a,e)}}(t,r,n),e).on("end.style."+t,null)},styleTween:function(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,function(t,n,e){var r,i;function o(){var o=n.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,n,e){return function(r){this.style.setProperty(t,n(r),e)}}(t,o,e)),r}return o._value=n,o}(t,n,null==e?"":e))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var n=t(this);this.textContent=null==n?"":n}}(Nr(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},remove:function(){return this.on("end.remove",(t=this._id,function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}));var t},tween:function(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=wr(this.node(),e).tween,o=0,a=i.length;o0&&(r=o-p),M<0?h=d-v:M>0&&(a=u-v),x=vi,L.attr("cursor",xi.selection),B());break;default:return}di()},!0).on("keyup.brush",function(){switch(t.event.keyCode){case 16:P&&(y=_=P=!1,B());break;case 18:x===yi&&(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=gi,B());break;case 32:x===vi&&(t.event.altKey?(w&&(s=l-p*w,r=o+p*w),M&&(h=d-v*M,a=u+v*M),x=yi):(w<0?s=l:w>0&&(r=o),M<0?h=d:M>0&&(a=u),x=gi),L.attr("cursor",xi[m]),B());break;default:return}di()},!0).on("mousemove.brush",O,!0).on("mouseup.brush",Y,!0);It(t.event.view)}hi(),Mr(b),c.call(b),D.start()}function O(){var t=Ot(b);!P||y||_||(Math.abs(t[0]-R[0])>Math.abs(t[1]-R[1])?_=!0:y=!0),R=t,g=!0,di(),B()}function B(){var t;switch(p=R[0]-z[0],v=R[1]-z[1],x){case vi:case pi:w&&(p=Math.max(S-r,Math.min(E-s,p)),o=r+p,l=s+p),M&&(v=Math.max(k-a,Math.min(C-h,v)),u=a+v,d=h+v);break;case gi:w<0?(p=Math.max(S-r,Math.min(E-r,p)),o=r+p,l=s):w>0&&(p=Math.max(S-s,Math.min(E-s,p)),o=r,l=s+p),M<0?(v=Math.max(k-a,Math.min(C-a,v)),u=a+v,d=h):M>0&&(v=Math.max(k-h,Math.min(C-h,v)),u=a,d=h+v);break;case yi:w&&(o=Math.max(S,Math.min(E,r-p*w)),l=Math.max(S,Math.min(E,s+p*w))),M&&(u=Math.max(k,Math.min(C,a-v*M)),d=Math.max(k,Math.min(C,h+v*M)))}l1e-6)if(Math.abs(s*u-c*f)>1e-6&&i){var h=e-o,d=r-a,p=u*u+c*c,v=h*h+d*d,g=Math.sqrt(p),y=Math.sqrt(l),_=i*Math.tan((Yi-Math.acos((p+l-v)/(2*g*y)))/2),b=_/y,m=_/g;Math.abs(b-1)>1e-6&&(this._+="L"+(t+b*f)+","+(n+b*s)),this._+="A"+i+","+i+",0,0,"+ +(s*h>f*d)+","+(this._x1=t+m*u)+","+(this._y1=n+m*c)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n;var a=(e=+e)*Math.cos(r),u=e*Math.sin(r),c=t+a,f=n+u,s=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+c+","+f:(Math.abs(this._x1-c)>1e-6||Math.abs(this._y1-f)>1e-6)&&(this._+="L"+c+","+f),e&&(l<0&&(l=l%Fi+Fi),l>Ii?this._+="A"+e+","+e+",0,1,"+s+","+(t-a)+","+(n-u)+"A"+e+","+e+",0,1,"+s+","+(this._x1=c)+","+(this._y1=f):l>1e-6&&(this._+="A"+e+","+e+",0,"+ +(l>=Yi)+","+s+","+(this._x1=t+e*Math.cos(i))+","+(this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +r+"h"+-e+"Z"},toString:function(){return this._}};function Zi(){}function Qi(t,n){var e=new Zi;if(t instanceof Zi)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++ir!=d>r&&e<(h-f)*(r-s)/(d-s)+f&&(i=-i)}return i}function so(t,n,e){var r,i,o,a;return function(t,n,e){return(n[0]-t[0])*(e[1]-t[1])==(e[0]-t[0])*(n[1]-t[1])}(t,n,e)&&(i=t[r=+(t[0]===n[0])],o=e[r],a=n[r],i<=o&&o<=a||a<=o&&o<=i)}function lo(){}var ho=[[],[[[1,1.5],[.5,1]]],[[[1.5,1],[1,1.5]]],[[[1.5,1],[.5,1]]],[[[1,.5],[1.5,1]]],[[[1,1.5],[.5,1]],[[1,.5],[1.5,1]]],[[[1,.5],[1,1.5]]],[[[1,.5],[.5,1]]],[[[.5,1],[1,.5]]],[[[1,1.5],[1,.5]]],[[[.5,1],[1,.5]],[[1.5,1],[1,1.5]]],[[[1.5,1],[1,.5]]],[[[.5,1],[1.5,1]]],[[[1,1.5],[1.5,1]]],[[[.5,1],[1,1.5]]],[]];function po(){var t=1,n=1,e=M,r=u;function i(t){var n=e(t);if(Array.isArray(n))n=n.slice().sort(ao);else{var r=s(t),i=r[0],a=r[1];n=w(i,a,n),n=g(Math.floor(i/n)*n,Math.floor(a/n)*n,n)}return n.map(function(n){return o(t,n)})}function o(e,i){var o=[],u=[];return function(e,r,i){var o,u,c,f,s,l,h=new Array,d=new Array;o=u=-1,f=e[0]>=r,ho[f<<1].forEach(p);for(;++o=r,ho[c|f<<1].forEach(p);ho[f<<0].forEach(p);for(;++u=r,s=e[u*t]>=r,ho[f<<1|s<<2].forEach(p);++o=r,l=s,s=e[u*t+o+1]>=r,ho[c|f<<1|s<<2|l<<3].forEach(p);ho[f|s<<3].forEach(p)}o=-1,s=e[u*t]>=r,ho[s<<2].forEach(p);for(;++o=r,ho[s<<2|l<<3].forEach(p);function p(t){var n,e,r=[t[0][0]+o,t[0][1]+u],c=[t[1][0]+o,t[1][1]+u],f=a(r),s=a(c);(n=d[f])?(e=h[s])?(delete d[n.end],delete h[e.start],n===e?(n.ring.push(c),i(n.ring)):h[n.start]=d[e.end]={start:n.start,end:e.end,ring:n.ring.concat(e.ring)}):(delete d[n.end],n.ring.push(c),d[n.end=s]=n):(n=h[s])?(e=d[f])?(delete h[n.start],delete d[e.end],n===e?(n.ring.push(c),i(n.ring)):h[e.start]=d[n.end]={start:e.start,end:n.end,ring:e.ring.concat(n.ring)}):(delete h[n.start],n.ring.unshift(r),h[n.start=f]=n):h[f]=d[s]={start:f,end:s,ring:[r,c]}}ho[s<<3].forEach(p)}(e,i,function(t){r(t,e,i),function(t){for(var n=0,e=t.length,r=t[e-1][1]*t[0][0]-t[e-1][0]*t[0][1];++n0?o.push([t]):u.push(t)}),u.forEach(function(t){for(var n,e=0,r=o.length;e0&&a0&&u0&&o>0))throw new Error("invalid size");return t=r,n=o,i},i.thresholds=function(t){return arguments.length?(e="function"==typeof t?t:Array.isArray(t)?uo(oo.call(t)):uo(t),i):e},i.smooth=function(t){return arguments.length?(r=t?u:lo,i):r===u},i}function vo(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[u-o+a*r]),n.data[u-e+a*r]=c/Math.min(u+1,r-1+o-u,o))}function go(t,n,e){for(var r=t.width,i=t.height,o=1+(e<<1),a=0;a=e&&(u>=o&&(c-=t.data[a+(u-o)*r]),n.data[a+(u-e)*r]=c/Math.min(u+1,i-1+o-u,o))}function yo(t){return t[0]}function _o(t){return t[1]}function bo(){return 1}var mo={},xo={},wo=34,Mo=10,No=13;function Ao(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function To(t){var n=Object.create(null),e=[];return t.forEach(function(t){for(var r in t)r in n||e.push(n[r]=r)}),e}function So(t,n){var e=t+"",r=e.length;return r9999?"+"+So(n,6):So(n,4))+"-"+So(t.getUTCMonth()+1,2)+"-"+So(t.getUTCDate(),2)+(o?"T"+So(e,2)+":"+So(r,2)+":"+So(i,2)+"."+So(o,3)+"Z":i?"T"+So(e,2)+":"+So(r,2)+":"+So(i,2)+"Z":r||e?"T"+So(e,2)+":"+So(r,2)+"Z":"")}function Eo(t){var n=new RegExp('["'+t+"\n\r]"),e=t.charCodeAt(0);function r(t,n){var r,i=[],o=t.length,a=0,u=0,c=o<=0,f=!1;function s(){if(c)return xo;if(f)return f=!1,mo;var n,r,i=a;if(t.charCodeAt(i)===wo){for(;a++=o?c=!0:(r=t.charCodeAt(a++))===Mo?f=!0:r===No&&(f=!0,t.charCodeAt(a)===Mo&&++a),t.slice(i+1,n-1).replace(/""/g,'"')}for(;a=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a,i=d,!(d=d[l=s<<1|f]))return i[l]=p,t;if(u=+t._x.call(null,d.data),c=+t._y.call(null,d.data),n===u&&e===c)return p.next=d,i?i[l]=p:t._root=p,t;do{i=i?i[l]=new Array(4):t._root=new Array(4),(f=n>=(o=(v+y)/2))?v=o:y=o,(s=e>=(a=(g+_)/2))?g=a:_=a}while((l=s<<1|f)==(h=(c>=a)<<1|u>=o));return i[h]=d,i[l]=p,t}function ra(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function ia(t){return t[0]}function oa(t){return t[1]}function aa(t,n,e){var r=new ua(null==n?ia:n,null==e?oa:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function ua(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function ca(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}var fa=aa.prototype=ua.prototype;function sa(t){return t.x+t.vx}function la(t){return t.y+t.vy}function ha(t){return t.index}function da(t,n){var e=t.get(n);if(!e)throw new Error("missing: "+n);return e}function pa(t){return t.x}function va(t){return t.y}fa.copy=function(){var t,n,e=new ua(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=ca(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=ca(n));return e},fa.add=function(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return ea(this.cover(n,e),n,e,t)},fa.addAll=function(t){var n,e,r,i,o=t.length,a=new Array(o),u=new Array(o),c=1/0,f=1/0,s=-1/0,l=-1/0;for(e=0;es&&(s=r),il&&(l=i));if(c>s||f>l)return this;for(this.cover(c,f).cover(s,l),e=0;et||t>=i||r>n||n>=o;)switch(u=(nh||(o=c.y0)>d||(a=c.x1)=y)<<1|t>=g)&&(c=p[p.length-1],p[p.length-1]=p[p.length-1-f],p[p.length-1-f]=c)}else{var _=t-+this._x.call(null,v.data),b=n-+this._y.call(null,v.data),m=_*_+b*b;if(m=(u=(p+g)/2))?p=u:g=u,(s=a>=(c=(v+y)/2))?v=c:y=c,n=d,!(d=d[l=s<<1|f]))return this;if(!d.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;d.data!==t;)if(r=d,!(d=d.next))return this;return(i=d.next)&&delete d.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(d=n[0]||n[1]||n[2]||n[3])&&d===(n[3]||n[2]||n[1]||n[0])&&!d.length&&(e?e[h]=d:this._root=d),this):(this._root=i,this)},fa.removeAll=function(t){for(var n=0,e=t.length;n1?r[0]+r.slice(2):r,+t.slice(e+1)]}function ba(t){return(t=_a(Math.abs(t)))?t[1]:NaN}var ma,xa=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function wa(t){return new Ma(t)}function Ma(t){if(!(n=xa.exec(t)))throw new Error("invalid format: "+t);var n;this.fill=n[1]||" ",this.align=n[2]||">",this.sign=n[3]||"-",this.symbol=n[4]||"",this.zero=!!n[5],this.width=n[6]&&+n[6],this.comma=!!n[7],this.precision=n[8]&&+n[8].slice(1),this.trim=!!n[9],this.type=n[10]||""}function Na(t,n){var e=_a(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}wa.prototype=Ma.prototype,Ma.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+(this.trim?"~":"")+this.type};var Aa={"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Na(100*t,n)},r:Na,s:function(t,n){var e=_a(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(ma=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,a=r.length;return o===a?r:o>a?r+new Array(o-a+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+_a(t,Math.max(0,n+o-1))[0]},X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}};function Ta(t){return t}var Sa,ka=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function Ea(t){var n,e,r=t.grouping&&t.thousands?(n=t.grouping,e=t.thousands,function(t,r){for(var i=t.length,o=[],a=0,u=n[0],c=0;i>0&&u>0&&(c+u+1>r&&(u=Math.max(1,r-c)),o.push(t.substring(i-=u,i+u)),!((c+=u+1)>r));)u=n[a=(a+1)%n.length];return o.reverse().join(e)}):Ta,i=t.currency,o=t.decimal,a=t.numerals?function(t){return function(n){return n.replace(/[0-9]/g,function(n){return t[+n]})}}(t.numerals):Ta,u=t.percent||"%";function c(t){var n=(t=wa(t)).fill,e=t.align,c=t.sign,f=t.symbol,s=t.zero,l=t.width,h=t.comma,d=t.precision,p=t.trim,v=t.type;"n"===v?(h=!0,v="g"):Aa[v]||(null==d&&(d=12),p=!0,v="g"),(s||"0"===n&&"="===e)&&(s=!0,n="0",e="=");var g="$"===f?i[0]:"#"===f&&/[boxX]/.test(v)?"0"+v.toLowerCase():"",y="$"===f?i[1]:/[%p]/.test(v)?u:"",_=Aa[v],b=/[defgprs%]/.test(v);function m(t){var i,u,f,m=g,x=y;if("c"===v)x=_(t)+x,t="";else{var w=(t=+t)<0;if(t=_(Math.abs(t),d),p&&(t=function(t){t:for(var n,e=t.length,r=1,i=-1;r0){if(!+t[r])break t;i=0}}return i>0?t.slice(0,i)+t.slice(n+1):t}(t)),w&&0==+t&&(w=!1),m=(w?"("===c?c:"-":"-"===c||"("===c?"":c)+m,x=("s"===v?ka[8+ma/3]:"")+x+(w&&"("===c?")":""),b)for(i=-1,u=t.length;++i(f=t.charCodeAt(i))||f>57){x=(46===f?o+t.slice(i+1):t.slice(i))+x,t=t.slice(0,i);break}}h&&!s&&(t=r(t,1/0));var M=m.length+t.length+x.length,N=M>1)+m+t+x+N.slice(M);break;default:t=N+m+t+x}return a(t)}return d=null==d?6:/[gprs]/.test(v)?Math.max(1,Math.min(21,d)):Math.max(0,Math.min(20,d)),m.toString=function(){return t+""},m}return{format:c,formatPrefix:function(t,n){var e=c(((t=wa(t)).type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(ba(n)/3))),i=Math.pow(10,-r),o=ka[8+r/3];return function(t){return e(i*t)+o}}}}function Ca(n){return Sa=Ea(n),t.format=Sa.format,t.formatPrefix=Sa.formatPrefix,Sa}function Pa(t){return Math.max(0,-ba(Math.abs(t)))}function za(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(ba(n)/3)))-ba(Math.abs(t)))}function Ra(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,ba(n)-ba(t))+1}function Da(){return new qa}function qa(){this.reset()}Ca({decimal:".",thousands:",",grouping:[3],currency:["$",""]}),qa.prototype={constructor:qa,reset:function(){this.s=this.t=0},add:function(t){Ua(La,t,this.t),Ua(this,La.s,this.s),this.s?this.t+=La.t:this.s=La.t},valueOf:function(){return this.s}};var La=new qa;function Ua(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}var Oa=1e-6,Ba=1e-12,Ya=Math.PI,Fa=Ya/2,Ia=Ya/4,ja=2*Ya,Ha=180/Ya,Xa=Ya/180,Ga=Math.abs,Va=Math.atan,$a=Math.atan2,Wa=Math.cos,Za=Math.ceil,Qa=Math.exp,Ja=Math.log,Ka=Math.pow,tu=Math.sin,nu=Math.sign||function(t){return t>0?1:t<0?-1:0},eu=Math.sqrt,ru=Math.tan;function iu(t){return t>1?0:t<-1?Ya:Math.acos(t)}function ou(t){return t>1?Fa:t<-1?-Fa:Math.asin(t)}function au(t){return(t=tu(t/2))*t}function uu(){}function cu(t,n){t&&su.hasOwnProperty(t.type)&&su[t.type](t,n)}var fu={Feature:function(t,n){cu(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r=0?1:-1,i=r*e,o=Wa(n=(n*=Xa)/2+Ia),a=tu(n),u=_u*a,c=yu*o+u*Wa(i),f=u*r*tu(i);bu.add($a(f,c)),gu=t,yu=o,_u=a}function Tu(t){return[$a(t[1],t[0]),ou(t[2])]}function Su(t){var n=t[0],e=t[1],r=Wa(e);return[r*Wa(n),r*tu(n),tu(e)]}function ku(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function Eu(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function Cu(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function Pu(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function zu(t){var n=eu(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}var Ru,Du,qu,Lu,Uu,Ou,Bu,Yu,Fu,Iu,ju,Hu,Xu,Gu,Vu,$u,Wu,Zu,Qu,Ju,Ku,tc,nc,ec,rc,ic,oc=Da(),ac={point:uc,lineStart:fc,lineEnd:sc,polygonStart:function(){ac.point=lc,ac.lineStart=hc,ac.lineEnd=dc,oc.reset(),xu.polygonStart()},polygonEnd:function(){xu.polygonEnd(),ac.point=uc,ac.lineStart=fc,ac.lineEnd=sc,bu<0?(Ru=-(qu=180),Du=-(Lu=90)):oc>Oa?Lu=90:oc<-Oa&&(Du=-90),Iu[0]=Ru,Iu[1]=qu}};function uc(t,n){Fu.push(Iu=[Ru=t,qu=t]),nLu&&(Lu=n)}function cc(t,n){var e=Su([t*Xa,n*Xa]);if(Yu){var r=Eu(Yu,e),i=Eu([r[1],-r[0],0],r);zu(i),i=Tu(i);var o,a=t-Uu,u=a>0?1:-1,c=i[0]*Ha*u,f=Ga(a)>180;f^(u*UuLu&&(Lu=o):f^(u*Uu<(c=(c+360)%360-180)&&cLu&&(Lu=n)),f?tpc(Ru,qu)&&(qu=t):pc(t,qu)>pc(Ru,qu)&&(Ru=t):qu>=Ru?(tqu&&(qu=t)):t>Uu?pc(Ru,t)>pc(Ru,qu)&&(qu=t):pc(t,qu)>pc(Ru,qu)&&(Ru=t)}else Fu.push(Iu=[Ru=t,qu=t]);nLu&&(Lu=n),Yu=e,Uu=t}function fc(){ac.point=cc}function sc(){Iu[0]=Ru,Iu[1]=qu,ac.point=uc,Yu=null}function lc(t,n){if(Yu){var e=t-Uu;oc.add(Ga(e)>180?e+(e>0?360:-360):e)}else Ou=t,Bu=n;xu.point(t,n),cc(t,n)}function hc(){xu.lineStart()}function dc(){lc(Ou,Bu),xu.lineEnd(),Ga(oc)>Oa&&(Ru=-(qu=180)),Iu[0]=Ru,Iu[1]=qu,Yu=null}function pc(t,n){return(n-=t)<0?n+360:n}function vc(t,n){return t[0]-n[0]}function gc(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nYa?t+Math.round(-t/ja)*ja:t,n]}function Pc(t,n,e){return(t%=ja)?n||e?Ec(Rc(t),Dc(n,e)):Rc(t):n||e?Dc(n,e):Cc}function zc(t){return function(n,e){return[(n+=t)>Ya?n-ja:n<-Ya?n+ja:n,e]}}function Rc(t){var n=zc(t);return n.invert=zc(-t),n}function Dc(t,n){var e=Wa(t),r=tu(t),i=Wa(n),o=tu(n);function a(t,n){var a=Wa(n),u=Wa(t)*a,c=tu(t)*a,f=tu(n),s=f*e+u*r;return[$a(c*i-s*o,u*e-f*r),ou(s*i+c*o)]}return a.invert=function(t,n){var a=Wa(n),u=Wa(t)*a,c=tu(t)*a,f=tu(n),s=f*i-c*o;return[$a(c*i+f*o,u*e+s*r),ou(s*e-u*r)]},a}function qc(t){function n(n){return(n=t(n[0]*Xa,n[1]*Xa))[0]*=Ha,n[1]*=Ha,n}return t=Pc(t[0]*Xa,t[1]*Xa,t.length>2?t[2]*Xa:0),n.invert=function(n){return(n=t.invert(n[0]*Xa,n[1]*Xa))[0]*=Ha,n[1]*=Ha,n},n}function Lc(t,n,e,r,i,o){if(e){var a=Wa(n),u=tu(n),c=r*e;null==i?(i=n+r*ja,o=n-c/2):(i=Uc(a,i),o=Uc(a,o),(r>0?io)&&(i+=r*ja));for(var f,s=i;r>0?s>o:s1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function Bc(t,n){return Ga(t[0]-n[0])=0;--o)i.point((s=f[o])[0],s[1]);else r(h.x,h.p.x,-1,i);h=h.p}f=(h=h.o).z,d=!d}while(!h.v);i.lineEnd()}}}function Ic(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r=0?1:-1,A=N*M,T=A>Ya,S=v*x;if(jc.add($a(S*N*tu(A),g*w+S*Wa(A))),a+=T?M+N*ja:M,T^d>=e^b>=e){var k=Eu(Su(h),Su(_));zu(k);var E=Eu(o,k);zu(E);var C=(T^M>=0?-1:1)*ou(E[2]);(r>C||r===C&&(k[0]||k[1]))&&(u+=T^M>=0?1:-1)}}return(a<-Oa||a0){for(l||(i.polygonStart(),l=!0),i.lineStart(),t=0;t1&&2&c&&h.push(h.pop().concat(h.shift())),a.push(h.filter(Gc))}return h}}function Gc(t){return t.length>1}function Vc(t,n){return((t=t.x)[0]<0?t[1]-Fa-Oa:Fa-t[1])-((n=n.x)[0]<0?n[1]-Fa-Oa:Fa-n[1])}var $c=Xc(function(){return!0},function(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,a){var u=o>0?Ya:-Ya,c=Ga(o-e);Ga(c-Ya)0?Fa:-Fa),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),t.point(o,r),n=0):i!==u&&c>=Ya&&(Ga(e-i)Oa?Va((tu(n)*(o=Wa(r))*tu(e)-tu(r)*(i=Wa(n))*tu(t))/(i*o*a)):(n+r)/2}(e,r,o,a),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(u,r),n=0),t.point(e=o,r=a),i=u},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}},function(t,n,e,r){var i;if(null==t)i=e*Fa,r.point(-Ya,i),r.point(0,i),r.point(Ya,i),r.point(Ya,0),r.point(Ya,-i),r.point(0,-i),r.point(-Ya,-i),r.point(-Ya,0),r.point(-Ya,i);else if(Ga(t[0]-n[0])>Oa){var o=t[0]0,i=Ga(n)>Oa;function o(t,e){return Wa(t)*Wa(e)>n}function a(t,e,r){var i=[1,0,0],o=Eu(Su(t),Su(e)),a=ku(o,o),u=o[0],c=a-u*u;if(!c)return!r&&t;var f=n*a/c,s=-n*u/c,l=Eu(i,o),h=Pu(i,f);Cu(h,Pu(o,s));var d=l,p=ku(h,d),v=ku(d,d),g=p*p-v*(ku(h,h)-1);if(!(g<0)){var y=eu(g),_=Pu(d,(-p-y)/v);if(Cu(_,h),_=Tu(_),!r)return _;var b,m=t[0],x=e[0],w=t[1],M=e[1];x0^_[1]<(Ga(_[0]-m)Ya^(m<=_[0]&&_[0]<=x)){var T=Pu(d,(-p+y)/v);return Cu(T,h),[_,Tu(T)]}}}function u(n,e){var i=r?t:Ya-t,o=0;return n<-i?o|=1:n>i&&(o|=2),e<-i?o|=4:e>i&&(o|=8),o}return Xc(o,function(t){var n,e,c,f,s;return{lineStart:function(){f=c=!1,s=1},point:function(l,h){var d,p=[l,h],v=o(l,h),g=r?v?0:u(l,h):v?u(l+(l<0?Ya:-Ya),h):0;if(!n&&(f=c=v)&&t.lineStart(),v!==c&&(!(d=a(n,p))||Bc(n,d)||Bc(p,d))&&(p[0]+=Oa,p[1]+=Oa,v=o(p[0],p[1])),v!==c)s=0,v?(t.lineStart(),d=a(p,n),t.point(d[0],d[1])):(d=a(n,p),t.point(d[0],d[1]),t.lineEnd()),n=d;else if(i&&n&&r^v){var y;g&e||!(y=a(p,n,!0))||(s=0,r?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&Bc(n,p)||t.point(p[0],p[1]),n=p,c=v,e=g},lineEnd:function(){c&&t.lineEnd(),n=null},clean:function(){return s|(f&&c)<<1}}},function(n,r,i,o){Lc(o,t,e,i,n,r)},r?[0,-t]:[-Ya,t-Ya])}var Zc=1e9,Qc=-Zc;function Jc(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,u,f){var s=0,l=0;if(null==i||(s=a(i,u))!==(l=a(o,u))||c(i,o)<0^u>0)do{f.point(0===s||3===s?t:e,s>1?r:n)}while((s=(s+u+4)%4)!==l);else f.point(o[0],o[1])}function a(r,i){return Ga(r[0]-t)0?0:3:Ga(r[0]-e)0?2:1:Ga(r[1]-n)0?1:0:i>0?3:2}function u(t,n){return c(t.x,n.x)}function c(t,n){var e=a(t,1),r=a(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(a){var c,f,s,l,h,d,p,v,g,y,_,b=a,m=Oc(),x={point:w,lineStart:function(){x.point=M,f&&f.push(s=[]);y=!0,g=!1,p=v=NaN},lineEnd:function(){c&&(M(l,h),d&&g&&m.rejoin(),c.push(m.result()));x.point=w,g&&b.lineEnd()},polygonStart:function(){b=m,c=[],f=[],_=!0},polygonEnd:function(){var n=function(){for(var n=0,e=0,i=f.length;er&&(h-o)*(r-a)>(d-a)*(t-o)&&++n:d<=r&&(h-o)*(r-a)<(d-a)*(t-o)&&--n;return n}(),e=_&&n,i=(c=T(c)).length;(e||i)&&(a.polygonStart(),e&&(a.lineStart(),o(null,null,1,a),a.lineEnd()),i&&Fc(c,u,n,o,a),a.polygonEnd());b=a,c=f=s=null}};function w(t,n){i(t,n)&&b.point(t,n)}function M(o,a){var u=i(o,a);if(f&&s.push([o,a]),y)l=o,h=a,d=u,y=!1,u&&(b.lineStart(),b.point(o,a));else if(u&&g)b.point(o,a);else{var c=[p=Math.max(Qc,Math.min(Zc,p)),v=Math.max(Qc,Math.min(Zc,v))],m=[o=Math.max(Qc,Math.min(Zc,o)),a=Math.max(Qc,Math.min(Zc,a))];!function(t,n,e,r,i,o){var a,u=t[0],c=t[1],f=0,s=1,l=n[0]-u,h=n[1]-c;if(a=e-u,l||!(a>0)){if(a/=l,l<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=i-u,l||!(a<0)){if(a/=l,l<0){if(a>s)return;a>f&&(f=a)}else if(l>0){if(a0)){if(a/=h,h<0){if(a0){if(a>s)return;a>f&&(f=a)}if(a=o-c,h||!(a<0)){if(a/=h,h<0){if(a>s)return;a>f&&(f=a)}else if(h>0){if(a0&&(t[0]=u+f*l,t[1]=c+f*h),s<1&&(n[0]=u+s*l,n[1]=c+s*h),!0}}}}}(c,m,t,n,e,r)?u&&(b.lineStart(),b.point(o,a),_=!1):(g||(b.lineStart(),b.point(c[0],c[1])),b.point(m[0],m[1]),u||b.lineEnd(),_=!1)}p=o,v=a,g=u}return x}}var Kc,tf,nf,ef=Da(),rf={sphere:uu,point:uu,lineStart:function(){rf.point=af,rf.lineEnd=of},lineEnd:uu,polygonStart:uu,polygonEnd:uu};function of(){rf.point=rf.lineEnd=uu}function af(t,n){Kc=t*=Xa,tf=tu(n*=Xa),nf=Wa(n),rf.point=uf}function uf(t,n){t*=Xa;var e=tu(n*=Xa),r=Wa(n),i=Ga(t-Kc),o=Wa(i),a=r*tu(i),u=nf*e-tf*r*o,c=tf*e+nf*r*o;ef.add($a(eu(a*a+u*u),c)),Kc=t,tf=e,nf=r}function cf(t){return ef.reset(),du(t,rf),+ef}var ff=[null,null],sf={type:"LineString",coordinates:ff};function lf(t,n){return ff[0]=t,ff[1]=n,cf(sf)}var hf={Feature:function(t,n){return pf(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++rOa}).map(c)).concat(g(Za(o/d)*d,i,d).filter(function(t){return Ga(t%v)>Oa}).map(f))}return _.lines=function(){return b().map(function(t){return{type:"LineString",coordinates:t}})},_.outline=function(){return{type:"Polygon",coordinates:[s(r).concat(l(a).slice(1),s(e).reverse().slice(1),l(u).reverse().slice(1))]}},_.extent=function(t){return arguments.length?_.extentMajor(t).extentMinor(t):_.extentMinor()},_.extentMajor=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],u=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),u>a&&(t=u,u=a,a=t),_.precision(y)):[[r,u],[e,a]]},_.extentMinor=function(e){return arguments.length?(n=+e[0][0],t=+e[1][0],o=+e[0][1],i=+e[1][1],n>t&&(e=n,n=t,t=e),o>i&&(e=o,o=i,i=e),_.precision(y)):[[n,o],[t,i]]},_.step=function(t){return arguments.length?_.stepMajor(t).stepMinor(t):_.stepMinor()},_.stepMajor=function(t){return arguments.length?(p=+t[0],v=+t[1],_):[p,v]},_.stepMinor=function(t){return arguments.length?(h=+t[0],d=+t[1],_):[h,d]},_.precision=function(h){return arguments.length?(y=+h,c=mf(o,i,90),f=xf(n,t,y),s=mf(u,a,90),l=xf(r,e,y),_):y},_.extentMajor([[-180,-90+Oa],[180,90-Oa]]).extentMinor([[-180,-80-Oa],[180,80+Oa]])}function Mf(t){return t}var Nf,Af,Tf,Sf,kf=Da(),Ef=Da(),Cf={point:uu,lineStart:uu,lineEnd:uu,polygonStart:function(){Cf.lineStart=Pf,Cf.lineEnd=Df},polygonEnd:function(){Cf.lineStart=Cf.lineEnd=Cf.point=uu,kf.add(Ga(Ef)),Ef.reset()},result:function(){var t=kf/2;return kf.reset(),t}};function Pf(){Cf.point=zf}function zf(t,n){Cf.point=Rf,Nf=Tf=t,Af=Sf=n}function Rf(t,n){Ef.add(Sf*t-Tf*n),Tf=t,Sf=n}function Df(){Rf(Nf,Af)}var qf=1/0,Lf=qf,Uf=-qf,Of=Uf,Bf={point:function(t,n){tUf&&(Uf=t);nOf&&(Of=n)},lineStart:uu,lineEnd:uu,polygonStart:uu,polygonEnd:uu,result:function(){var t=[[qf,Lf],[Uf,Of]];return Uf=Of=-(Lf=qf=1/0),t}};var Yf,Ff,If,jf,Hf=0,Xf=0,Gf=0,Vf=0,$f=0,Wf=0,Zf=0,Qf=0,Jf=0,Kf={point:ts,lineStart:ns,lineEnd:is,polygonStart:function(){Kf.lineStart=os,Kf.lineEnd=as},polygonEnd:function(){Kf.point=ts,Kf.lineStart=ns,Kf.lineEnd=is},result:function(){var t=Jf?[Zf/Jf,Qf/Jf]:Wf?[Vf/Wf,$f/Wf]:Gf?[Hf/Gf,Xf/Gf]:[NaN,NaN];return Hf=Xf=Gf=Vf=$f=Wf=Zf=Qf=Jf=0,t}};function ts(t,n){Hf+=t,Xf+=n,++Gf}function ns(){Kf.point=es}function es(t,n){Kf.point=rs,ts(If=t,jf=n)}function rs(t,n){var e=t-If,r=n-jf,i=eu(e*e+r*r);Vf+=i*(If+t)/2,$f+=i*(jf+n)/2,Wf+=i,ts(If=t,jf=n)}function is(){Kf.point=ts}function os(){Kf.point=us}function as(){cs(Yf,Ff)}function us(t,n){Kf.point=cs,ts(Yf=If=t,Ff=jf=n)}function cs(t,n){var e=t-If,r=n-jf,i=eu(e*e+r*r);Vf+=i*(If+t)/2,$f+=i*(jf+n)/2,Wf+=i,Zf+=(i=jf*t-If*n)*(If+t),Qf+=i*(jf+n),Jf+=3*i,ts(If=t,jf=n)}function fs(t){this._context=t}fs.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,ja)}},result:uu};var ss,ls,hs,ds,ps,vs=Da(),gs={point:uu,lineStart:function(){gs.point=ys},lineEnd:function(){ss&&_s(ls,hs),gs.point=uu},polygonStart:function(){ss=!0},polygonEnd:function(){ss=null},result:function(){var t=+vs;return vs.reset(),t}};function ys(t,n){gs.point=_s,ls=ds=t,hs=ps=n}function _s(t,n){ds-=t,ps-=n,vs.add(eu(ds*ds+ps*ps)),ds=t,ps=n}function bs(){this._string=[]}function ms(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function xs(t){return function(n){var e=new ws;for(var r in t)e[r]=t[r];return e.stream=n,e}}function ws(){}function Ms(t,n,e){var r=t.clipExtent&&t.clipExtent();return t.scale(150).translate([0,0]),null!=r&&t.clipExtent(null),du(e,t.stream(Bf)),n(Bf.result()),null!=r&&t.clipExtent(r),t}function Ns(t,n,e){return Ms(t,function(e){var r=n[1][0]-n[0][0],i=n[1][1]-n[0][1],o=Math.min(r/(e[1][0]-e[0][0]),i/(e[1][1]-e[0][1])),a=+n[0][0]+(r-o*(e[1][0]+e[0][0]))/2,u=+n[0][1]+(i-o*(e[1][1]+e[0][1]))/2;t.scale(150*o).translate([a,u])},e)}function As(t,n,e){return Ns(t,[[0,0],n],e)}function Ts(t,n,e){return Ms(t,function(e){var r=+n,i=r/(e[1][0]-e[0][0]),o=(r-i*(e[1][0]+e[0][0]))/2,a=-i*e[0][1];t.scale(150*i).translate([o,a])},e)}function Ss(t,n,e){return Ms(t,function(e){var r=+n,i=r/(e[1][1]-e[0][1]),o=-i*e[0][0],a=(r-i*(e[1][1]+e[0][1]))/2;t.scale(150*i).translate([o,a])},e)}bs.prototype={_radius:4.5,_circle:ms(4.5),pointRadius:function(t){return(t=+t)!==this._radius&&(this._radius=t,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:null==this._circle&&(this._circle=ms(this._radius)),this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}return null}},ws.prototype={constructor:ws,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var ks=16,Es=Wa(30*Xa);function Cs(t,n){return+n?function(t,n){function e(r,i,o,a,u,c,f,s,l,h,d,p,v,g){var y=f-r,_=s-i,b=y*y+_*_;if(b>4*n&&v--){var m=a+h,x=u+d,w=c+p,M=eu(m*m+x*x+w*w),N=ou(w/=M),A=Ga(Ga(w)-1)n||Ga((y*E+_*C)/b-.5)>.3||a*h+u*d+c*p2?t[2]%360*Xa:0,S()):[g*Ha,y*Ha,_*Ha]},A.angle=function(t){return arguments.length?(b=t%360*Xa,S()):b*Ha},A.precision=function(t){return arguments.length?(a=Cs(u,N=t*t),k()):eu(N)},A.fitExtent=function(t,n){return Ns(A,t,n)},A.fitSize=function(t,n){return As(A,t,n)},A.fitWidth=function(t,n){return Ts(A,t,n)},A.fitHeight=function(t,n){return Ss(A,t,n)},function(){return n=t.apply(this,arguments),A.invert=n.invert&&T,S()}}function qs(t){var n=0,e=Ya/3,r=Ds(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*Xa,e=t[1]*Xa):[n*Ha,e*Ha]},i}function Ls(t,n){var e=tu(t),r=(e+tu(n))/2;if(Ga(r)0?n<-Fa+Oa&&(n=-Fa+Oa):n>Fa-Oa&&(n=Fa-Oa);var e=i/Ka(Xs(n),r);return[e*tu(r*t),i-e*Wa(r*t)]}return o.invert=function(t,n){var e=i-n,o=nu(r)*eu(t*t+e*e);return[$a(t,Ga(e))/r*nu(e),2*Va(Ka(i/o,1/r))-Fa]},o}function Vs(t,n){return[t,n]}function $s(t,n){var e=Wa(t),r=t===n?tu(t):(e-Wa(n))/(n-t),i=e/r+t;if(Ga(r)=0;)n+=e[r].value;else n=1;t.value=n}function ll(t,n){var e,r,i,o,a,u=new vl(t),c=+t.value&&(u.value=t.value),f=[u];for(null==n&&(n=hl);e=f.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(a=i.length))for(e.children=new Array(a),o=a-1;o>=0;--o)f.push(r=e.children[o]=new vl(i[o])),r.parent=e,r.depth=e.depth+1;return u.eachBefore(pl)}function hl(t){return t.children}function dl(t){t.data=t.data.data}function pl(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function vl(t){this.data=t,this.depth=this.height=0,this.parent=null}tl.invert=function(t,n){for(var e,r=n,i=r*r,o=i*i*i,a=0;a<12&&(o=(i=(r-=e=(r*(Ws+Zs*i+o*(Qs+Js*i))-n)/(Ws+3*Zs*i+o*(7*Qs+9*Js*i)))*r)*i*i,!(Ga(e)Oa&&--i>0);return[t/(.8707+(o=r*r)*(o*(o*o*o*(.003971-.001529*o)-.013791)-.131979)),r]},il.invert=Ys(ou),ol.invert=Ys(function(t){return 2*Va(t)}),al.invert=function(t,n){return[-n,2*Va(Qa(t))-Fa]},vl.prototype=ll.prototype={constructor:vl,count:function(){return this.eachAfter(sl)},each:function(t){var n,e,r,i,o=this,a=[o];do{for(n=a.reverse(),a=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r=0;--e)i.push(n[e]);return this},sum:function(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})},sort:function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},path:function(t){for(var n=this,e=function(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},descendants:function(){var t=[];return this.each(function(n){t.push(n)}),t},leaves:function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},links:function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n},copy:function(){return ll(this).eachBefore(dl)}};var gl=Array.prototype.slice;function yl(t){for(var n,e,r=0,i=(t=function(t){for(var n,e,r=t.length;r;)e=Math.random()*r--|0,n=t[r],t[r]=t[e],t[e]=n;return t}(gl.call(t))).length,o=[];r0&&e*e>r*r+i*i}function xl(t,n){for(var e=0;e(a*=a)?(r=(f+a-i)/(2*f),o=Math.sqrt(Math.max(0,a/f-r*r)),e.x=t.x-r*u-o*c,e.y=t.y-r*c+o*u):(r=(f+i-a)/(2*f),o=Math.sqrt(Math.max(0,i/f-r*r)),e.x=n.x+r*u-o*c,e.y=n.y+r*c+o*u)):(e.x=n.x+e.r,e.y=n.y)}function Tl(t,n){var e=t.r+n.r-1e-6,r=n.x-t.x,i=n.y-t.y;return e>0&&e*e>r*r+i*i}function Sl(t){var n=t._,e=t.next._,r=n.r+e.r,i=(n.x*e.r+e.x*n.r)/r,o=(n.y*e.r+e.y*n.r)/r;return i*i+o*o}function kl(t){this._=t,this.next=null,this.previous=null}function El(t){if(!(i=t.length))return 0;var n,e,r,i,o,a,u,c,f,s,l;if((n=t[0]).x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;Al(e,n,r=t[2]),n=new kl(n),e=new kl(e),r=new kl(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(u=3;uh&&(h=u),g=s*s*v,(d=Math.max(h/g,g/l))>p){s-=u;break}p=d}y.push(a={value:s,dice:c1?n:1)},e}(Ql);var th=function t(n){function e(t,e,r,i,o){if((a=t._squarify)&&a.ratio===n)for(var a,u,c,f,s,l=-1,h=a.length,d=t.value;++l1?n:1)},e}(Ql);function nh(t,n){return t[0]-n[0]||t[1]-n[1]}function eh(t){for(var n,e,r,i=t.length,o=[0,1],a=2,u=2;u1&&(n=t[o[a-2]],e=t[o[a-1]],r=t[u],(e[0]-n[0])*(r[1]-n[1])-(e[1]-n[1])*(r[0]-n[0])<=0);)--a;o[a++]=u}return o.slice(0,a)}function rh(){return Math.random()}var ih=function t(n){function e(t,e){return t=null==t?0:+t,e=null==e?1:+e,1===arguments.length?(e=t,t=0):e-=t,function(){return n()*e+t}}return e.source=t,e}(rh),oh=function t(n){function e(t,e){var r,i;return t=null==t?0:+t,e=null==e?1:+e,function(){var o;if(null!=r)o=r,r=null;else do{r=2*n()-1,o=2*n()-1,i=r*r+o*o}while(!i||i>1);return t+e*o*Math.sqrt(-2*Math.log(i)/i)}}return e.source=t,e}(rh),ah=function t(n){function e(){var t=oh.source(n).apply(this,arguments);return function(){return Math.exp(t())}}return e.source=t,e}(rh),uh=function t(n){function e(t){return function(){for(var e=0,r=0;rr&&(n=e,e=r,r=n),function(t){return Math.max(e,Math.min(r,t))}}function Mh(t,n,e){var r=t[0],i=t[1],o=n[0],a=n[1];return i2?Nh:Mh,i=o=null,l}function l(n){return isNaN(n=+n)?e:(i||(i=r(a.map(t),u,c)))(t(f(n)))}return l.invert=function(e){return f(n((o||(o=r(u,a.map(t),he)))(e)))},l.domain=function(t){return arguments.length?(a=dh.call(t,_h),f===mh||(f=wh(a)),s()):a.slice()},l.range=function(t){return arguments.length?(u=ph.call(t),s()):u.slice()},l.rangeRound=function(t){return u=ph.call(t),c=_e,s()},l.clamp=function(t){return arguments.length?(f=t?wh(a):mh,l):f!==mh},l.interpolate=function(t){return arguments.length?(c=t,s()):c},l.unknown=function(t){return arguments.length?(e=t,l):e},function(e,r){return t=e,n=r,s()}}function Sh(t,n){return Th()(t,n)}function kh(n,e,r,i){var o,a=w(n,e,r);switch((i=wa(null==i?",f":i)).type){case"s":var u=Math.max(Math.abs(n),Math.abs(e));return null!=i.precision||isNaN(o=za(a,u))||(i.precision=o),t.formatPrefix(i,u);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(o=Ra(a,Math.max(Math.abs(n),Math.abs(e))))||(i.precision=o-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(o=Pa(a))||(i.precision=o-2*("%"===i.type))}return t.format(i)}function Eh(t){var n=t.domain;return t.ticks=function(t){var e=n();return m(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){var r=n();return kh(r[0],r[r.length-1],null==t?10:t,e)},t.nice=function(e){null==e&&(e=10);var r,i=n(),o=0,a=i.length-1,u=i[o],c=i[a];return c0?r=x(u=Math.floor(u/r)*r,c=Math.ceil(c/r)*r,e):r<0&&(r=x(u=Math.ceil(u*r)/r,c=Math.floor(c*r)/r,e)),r>0?(i[o]=Math.floor(u/r)*r,i[a]=Math.ceil(c/r)*r,n(i)):r<0&&(i[o]=Math.ceil(u*r)/r,i[a]=Math.floor(c*r)/r,n(i)),t},t}function Ch(t,n){var e,r=0,i=(t=t.slice()).length-1,o=t[r],a=t[i];return a0){for(;hc)break;v.push(l)}}else for(;h=1;--s)if(!((l=f*s)c)break;v.push(l)}}else v=m(h,d,Math.min(d-h,p)).map(r);return n?v.reverse():v},i.tickFormat=function(n,o){if(null==o&&(o=10===a?".0e":","),"function"!=typeof o&&(o=t.format(o)),n===1/0)return o;null==n&&(n=10);var u=Math.max(1,a*n/i.ticks().length);return function(t){var n=t/r(Math.round(e(t)));return n*a0))return u;do{u.push(a=new Date(+e)),n(e,o),t(e)}while(a=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){if(t>=t)if(r<0)for(;++r<=0;)for(;n(t,-1),!e(t););else for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return Gh.setTime(+n),Vh.setTime(+r),t(Gh),t(Vh),Math.floor(e(Gh,Vh))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t==0}:function(n){return i.count(0,n)%t==0}):i:null}),i}var Wh=$h(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Wh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?$h(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Wh:null};var Zh=Wh.range,Qh=6e4,Jh=6048e5,Kh=$h(function(t){t.setTime(t-t.getMilliseconds())},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),td=Kh.range,nd=$h(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds())},function(t,n){t.setTime(+t+n*Qh)},function(t,n){return(n-t)/Qh},function(t){return t.getMinutes()}),ed=nd.range,rd=$h(function(t){t.setTime(t-t.getMilliseconds()-1e3*t.getSeconds()-t.getMinutes()*Qh)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),id=rd.range,od=$h(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qh)/864e5},function(t){return t.getDate()-1}),ad=od.range;function ud(t){return $h(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Qh)/Jh})}var cd=ud(0),fd=ud(1),sd=ud(2),ld=ud(3),hd=ud(4),dd=ud(5),pd=ud(6),vd=cd.range,gd=fd.range,yd=sd.range,_d=ld.range,bd=hd.range,md=dd.range,xd=pd.range,wd=$h(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),Md=wd.range,Nd=$h(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});Nd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?$h(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var Ad=Nd.range,Td=$h(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Qh)},function(t,n){return(n-t)/Qh},function(t){return t.getUTCMinutes()}),Sd=Td.range,kd=$h(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),Ed=kd.range,Cd=$h(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1}),Pd=Cd.range;function zd(t){return $h(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Jh})}var Rd=zd(0),Dd=zd(1),qd=zd(2),Ld=zd(3),Ud=zd(4),Od=zd(5),Bd=zd(6),Yd=Rd.range,Fd=Dd.range,Id=qd.range,jd=Ld.range,Hd=Ud.range,Xd=Od.range,Gd=Bd.range,Vd=$h(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),$d=Vd.range,Wd=$h(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Wd.every=function(t){return isFinite(t=Math.floor(t))&&t>0?$h(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Zd=Wd.range;function Qd(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function Jd(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function Kd(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function tp(t){var n=t.dateTime,e=t.date,r=t.time,i=t.periods,o=t.days,a=t.shortDays,u=t.months,c=t.shortMonths,f=cp(i),s=fp(i),l=cp(o),h=fp(o),d=cp(a),p=fp(a),v=cp(u),g=fp(u),y=cp(c),_=fp(c),b={a:function(t){return a[t.getDay()]},A:function(t){return o[t.getDay()]},b:function(t){return c[t.getMonth()]},B:function(t){return u[t.getMonth()]},c:null,d:Ep,e:Ep,f:Dp,H:Cp,I:Pp,j:zp,L:Rp,m:qp,M:Lp,p:function(t){return i[+(t.getHours()>=12)]},Q:sv,s:lv,S:Up,u:Op,U:Bp,V:Yp,w:Fp,W:Ip,x:null,X:null,y:jp,Y:Hp,Z:Xp,"%":fv},m={a:function(t){return a[t.getUTCDay()]},A:function(t){return o[t.getUTCDay()]},b:function(t){return c[t.getUTCMonth()]},B:function(t){return u[t.getUTCMonth()]},c:null,d:Gp,e:Gp,f:Qp,H:Vp,I:$p,j:Wp,L:Zp,m:Jp,M:Kp,p:function(t){return i[+(t.getUTCHours()>=12)]},Q:sv,s:lv,S:tv,u:nv,U:ev,V:rv,w:iv,W:ov,x:null,X:null,y:av,Y:uv,Z:cv,"%":fv},x={a:function(t,n,e){var r=d.exec(n.slice(e));return r?(t.w=p[r[0].toLowerCase()],e+r[0].length):-1},A:function(t,n,e){var r=l.exec(n.slice(e));return r?(t.w=h[r[0].toLowerCase()],e+r[0].length):-1},b:function(t,n,e){var r=y.exec(n.slice(e));return r?(t.m=_[r[0].toLowerCase()],e+r[0].length):-1},B:function(t,n,e){var r=v.exec(n.slice(e));return r?(t.m=g[r[0].toLowerCase()],e+r[0].length):-1},c:function(t,e,r){return N(t,n,e,r)},d:bp,e:bp,f:Ap,H:xp,I:xp,j:mp,L:Np,m:_p,M:wp,p:function(t,n,e){var r=f.exec(n.slice(e));return r?(t.p=s[r[0].toLowerCase()],e+r[0].length):-1},Q:Sp,s:kp,S:Mp,u:lp,U:hp,V:dp,w:sp,W:pp,x:function(t,n,r){return N(t,e,n,r)},X:function(t,n,e){return N(t,r,n,e)},y:gp,Y:vp,Z:yp,"%":Tp};function w(t,n){return function(e){var r,i,o,a=[],u=-1,c=0,f=t.length;for(e instanceof Date||(e=new Date(+e));++u53)return null;"w"in o||(o.w=1),"Z"in o?(i=(r=Jd(Kd(o.y))).getUTCDay(),r=i>4||0===i?Dd.ceil(r):Dd(r),r=Cd.offset(r,7*(o.V-1)),o.y=r.getUTCFullYear(),o.m=r.getUTCMonth(),o.d=r.getUTCDate()+(o.w+6)%7):(i=(r=n(Kd(o.y))).getDay(),r=i>4||0===i?fd.ceil(r):fd(r),r=od.offset(r,7*(o.V-1)),o.y=r.getFullYear(),o.m=r.getMonth(),o.d=r.getDate()+(o.w+6)%7)}else("W"in o||"U"in o)&&("w"in o||(o.w="u"in o?o.u%7:"W"in o?1:0),i="Z"in o?Jd(Kd(o.y)).getUTCDay():n(Kd(o.y)).getDay(),o.m=0,o.d="W"in o?(o.w+6)%7+7*o.W-(i+5)%7:o.w+7*o.U-(i+6)%7);return"Z"in o?(o.H+=o.Z/100|0,o.M+=o.Z%100,Jd(o)):n(o)}}function N(t,n,e,r){for(var i,o,a=0,u=n.length,c=e.length;a=c)return-1;if(37===(i=n.charCodeAt(a++))){if(i=n.charAt(a++),!(o=x[i in ep?n.charAt(a++):i])||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}return b.x=w(e,b),b.X=w(r,b),b.c=w(n,b),m.x=w(e,m),m.X=w(r,m),m.c=w(n,m),{format:function(t){var n=w(t+="",b);return n.toString=function(){return t},n},parse:function(t){var n=M(t+="",Qd);return n.toString=function(){return t},n},utcFormat:function(t){var n=w(t+="",m);return n.toString=function(){return t},n},utcParse:function(t){var n=M(t,Jd);return n.toString=function(){return t},n}}}var np,ep={"-":"",_:" ",0:"0"},rp=/^\s*\d+/,ip=/^%/,op=/[\\^$*+?|[\]().{}]/g;function ap(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o68?1900:2e3),e+r[0].length):-1}function yp(t,n,e){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function _p(t,n,e){var r=rp.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function bp(t,n,e){var r=rp.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function mp(t,n,e){var r=rp.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function xp(t,n,e){var r=rp.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function wp(t,n,e){var r=rp.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function Mp(t,n,e){var r=rp.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function Np(t,n,e){var r=rp.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function Ap(t,n,e){var r=rp.exec(n.slice(e,e+6));return r?(t.L=Math.floor(r[0]/1e3),e+r[0].length):-1}function Tp(t,n,e){var r=ip.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Sp(t,n,e){var r=rp.exec(n.slice(e));return r?(t.Q=+r[0],e+r[0].length):-1}function kp(t,n,e){var r=rp.exec(n.slice(e));return r?(t.Q=1e3*+r[0],e+r[0].length):-1}function Ep(t,n){return ap(t.getDate(),n,2)}function Cp(t,n){return ap(t.getHours(),n,2)}function Pp(t,n){return ap(t.getHours()%12||12,n,2)}function zp(t,n){return ap(1+od.count(Nd(t),t),n,3)}function Rp(t,n){return ap(t.getMilliseconds(),n,3)}function Dp(t,n){return Rp(t,n)+"000"}function qp(t,n){return ap(t.getMonth()+1,n,2)}function Lp(t,n){return ap(t.getMinutes(),n,2)}function Up(t,n){return ap(t.getSeconds(),n,2)}function Op(t){var n=t.getDay();return 0===n?7:n}function Bp(t,n){return ap(cd.count(Nd(t),t),n,2)}function Yp(t,n){var e=t.getDay();return t=e>=4||0===e?hd(t):hd.ceil(t),ap(hd.count(Nd(t),t)+(4===Nd(t).getDay()),n,2)}function Fp(t){return t.getDay()}function Ip(t,n){return ap(fd.count(Nd(t),t),n,2)}function jp(t,n){return ap(t.getFullYear()%100,n,2)}function Hp(t,n){return ap(t.getFullYear()%1e4,n,4)}function Xp(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+ap(n/60|0,"0",2)+ap(n%60,"0",2)}function Gp(t,n){return ap(t.getUTCDate(),n,2)}function Vp(t,n){return ap(t.getUTCHours(),n,2)}function $p(t,n){return ap(t.getUTCHours()%12||12,n,2)}function Wp(t,n){return ap(1+Cd.count(Wd(t),t),n,3)}function Zp(t,n){return ap(t.getUTCMilliseconds(),n,3)}function Qp(t,n){return Zp(t,n)+"000"}function Jp(t,n){return ap(t.getUTCMonth()+1,n,2)}function Kp(t,n){return ap(t.getUTCMinutes(),n,2)}function tv(t,n){return ap(t.getUTCSeconds(),n,2)}function nv(t){var n=t.getUTCDay();return 0===n?7:n}function ev(t,n){return ap(Rd.count(Wd(t),t),n,2)}function rv(t,n){var e=t.getUTCDay();return t=e>=4||0===e?Ud(t):Ud.ceil(t),ap(Ud.count(Wd(t),t)+(4===Wd(t).getUTCDay()),n,2)}function iv(t){return t.getUTCDay()}function ov(t,n){return ap(Dd.count(Wd(t),t),n,2)}function av(t,n){return ap(t.getUTCFullYear()%100,n,2)}function uv(t,n){return ap(t.getUTCFullYear()%1e4,n,4)}function cv(){return"+0000"}function fv(){return"%"}function sv(t){return+t}function lv(t){return Math.floor(+t/1e3)}function hv(n){return np=tp(n),t.timeFormat=np.format,t.timeParse=np.parse,t.utcFormat=np.utcFormat,t.utcParse=np.utcParse,np}hv({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var dv=Date.prototype.toISOString?function(t){return t.toISOString()}:t.utcFormat("%Y-%m-%dT%H:%M:%S.%LZ");var pv=+new Date("2000-01-01T00:00:00.000Z")?function(t){var n=new Date(t);return isNaN(n)?null:n}:t.utcParse("%Y-%m-%dT%H:%M:%S.%LZ"),vv=1e3,gv=60*vv,yv=60*gv,_v=24*yv,bv=7*_v,mv=30*_v,xv=365*_v;function wv(t){return new Date(t)}function Mv(t){return t instanceof Date?+t:+new Date(+t)}function Nv(t,n,r,i,o,a,u,c,f){var s=Sh(mh,mh),l=s.invert,h=s.domain,d=f(".%L"),p=f(":%S"),v=f("%I:%M"),g=f("%I %p"),y=f("%a %d"),_=f("%b %d"),b=f("%B"),m=f("%Y"),x=[[u,1,vv],[u,5,5*vv],[u,15,15*vv],[u,30,30*vv],[a,1,gv],[a,5,5*gv],[a,15,15*gv],[a,30,30*gv],[o,1,yv],[o,3,3*yv],[o,6,6*yv],[o,12,12*yv],[i,1,_v],[i,2,2*_v],[r,1,bv],[n,1,mv],[n,3,3*mv],[t,1,xv]];function M(e){return(u(e)=1?fy:t<=-1?-fy:Math.asin(t)}function hy(t){return t.innerRadius}function dy(t){return t.outerRadius}function py(t){return t.startAngle}function vy(t){return t.endAngle}function gy(t){return t&&t.padAngle}function yy(t,n,e,r,i,o,a){var u=t-e,c=n-r,f=(a?o:-o)/ay(u*u+c*c),s=f*c,l=-f*u,h=t+s,d=n+l,p=e+s,v=r+l,g=(h+p)/2,y=(d+v)/2,_=p-h,b=v-d,m=_*_+b*b,x=i-o,w=h*v-p*d,M=(b<0?-1:1)*ay(ry(0,x*x*m-w*w)),N=(w*b-_*M)/m,A=(-w*_-b*M)/m,T=(w*b+_*M)/m,S=(-w*_+b*M)/m,k=N-g,E=A-y,C=T-g,P=S-y;return k*k+E*E>C*C+P*P&&(N=T,A=S),{cx:N,cy:A,x01:-s,y01:-l,x11:N*(i/x-1),y11:A*(i/x-1)}}function _y(t){this._context=t}function by(t){return new _y(t)}function my(t){return t[0]}function xy(t){return t[1]}function wy(){var t=my,n=xy,e=Kg(!0),r=null,i=by,o=null;function a(a){var u,c,f,s=a.length,l=!1;for(null==r&&(o=i(f=Hi())),u=0;u<=s;++u)!(u=s;--l)u.point(g[l],y[l]);u.lineEnd(),u.areaEnd()}v&&(g[f]=+t(h,f,c),y[f]=+e(h,f,c),u.point(n?+n(h,f,c):g[f],r?+r(h,f,c):y[f]))}if(d)return u=null,d+""||null}function f(){return wy().defined(i).curve(a).context(o)}return c.x=function(e){return arguments.length?(t="function"==typeof e?e:Kg(+e),n=null,c):t},c.x0=function(n){return arguments.length?(t="function"==typeof n?n:Kg(+n),c):t},c.x1=function(t){return arguments.length?(n=null==t?null:"function"==typeof t?t:Kg(+t),c):n},c.y=function(t){return arguments.length?(e="function"==typeof t?t:Kg(+t),r=null,c):e},c.y0=function(t){return arguments.length?(e="function"==typeof t?t:Kg(+t),c):e},c.y1=function(t){return arguments.length?(r=null==t?null:"function"==typeof t?t:Kg(+t),c):r},c.lineX0=c.lineY0=function(){return f().x(t).y(e)},c.lineY1=function(){return f().x(t).y(r)},c.lineX1=function(){return f().x(n).y(e)},c.defined=function(t){return arguments.length?(i="function"==typeof t?t:Kg(!!t),c):i},c.curve=function(t){return arguments.length?(a=t,null!=o&&(u=a(o)),c):a},c.context=function(t){return arguments.length?(null==t?o=u=null:u=a(o=t),c):o},c}function Ny(t,n){return nt?1:n>=t?0:NaN}function Ay(t){return t}_y.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Ty=ky(by);function Sy(t){this._curve=t}function ky(t){function n(n){return new Sy(t(n))}return n._curve=t,n}function Ey(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(ky(t)):n()._curve},t}function Cy(){return Ey(wy().curve(Ty))}function Py(){var t=My().curve(Ty),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return Ey(e())},delete t.lineX0,t.lineEndAngle=function(){return Ey(r())},delete t.lineX1,t.lineInnerRadius=function(){return Ey(i())},delete t.lineY0,t.lineOuterRadius=function(){return Ey(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(ky(t)):n()._curve},t}function zy(t,n){return[(n=+n)*Math.cos(t-=Math.PI/2),n*Math.sin(t)]}Sy.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var Ry=Array.prototype.slice;function Dy(t){return t.source}function qy(t){return t.target}function Ly(t){var n=Dy,e=qy,r=my,i=xy,o=null;function a(){var a,u=Ry.call(arguments),c=n.apply(this,u),f=e.apply(this,u);if(o||(o=a=Hi()),t(o,+r.apply(this,(u[0]=c,u)),+i.apply(this,u),+r.apply(this,(u[0]=f,u)),+i.apply(this,u)),a)return o=null,a+""||null}return a.source=function(t){return arguments.length?(n=t,a):n},a.target=function(t){return arguments.length?(e=t,a):e},a.x=function(t){return arguments.length?(r="function"==typeof t?t:Kg(+t),a):r},a.y=function(t){return arguments.length?(i="function"==typeof t?t:Kg(+t),a):i},a.context=function(t){return arguments.length?(o=null==t?null:t,a):o},a}function Uy(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n=(n+r)/2,e,n,i,r,i)}function Oy(t,n,e,r,i){t.moveTo(n,e),t.bezierCurveTo(n,e=(e+i)/2,r,e,r,i)}function By(t,n,e,r,i){var o=zy(n,e),a=zy(n,e=(e+i)/2),u=zy(r,e),c=zy(r,i);t.moveTo(o[0],o[1]),t.bezierCurveTo(a[0],a[1],u[0],u[1],c[0],c[1])}var Yy={draw:function(t,n){var e=Math.sqrt(n/cy);t.moveTo(e,0),t.arc(0,0,e,0,sy)}},Fy={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Iy=Math.sqrt(1/3),jy=2*Iy,Hy={draw:function(t,n){var e=Math.sqrt(n/jy),r=e*Iy;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Xy=Math.sin(cy/10)/Math.sin(7*cy/10),Gy=Math.sin(sy/10)*Xy,Vy=-Math.cos(sy/10)*Xy,$y={draw:function(t,n){var e=Math.sqrt(.8908130915292852*n),r=Gy*e,i=Vy*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var a=sy*o/5,u=Math.cos(a),c=Math.sin(a);t.lineTo(c*e,-u*e),t.lineTo(u*r-c*i,c*r+u*i)}t.closePath()}},Wy={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Zy=Math.sqrt(3),Qy={draw:function(t,n){var e=-Math.sqrt(n/(3*Zy));t.moveTo(0,2*e),t.lineTo(-Zy*e,-e),t.lineTo(Zy*e,-e),t.closePath()}},Jy=Math.sqrt(3)/2,Ky=1/Math.sqrt(12),t_=3*(Ky/2+1),n_={draw:function(t,n){var e=Math.sqrt(n/t_),r=e/2,i=e*Ky,o=r,a=e*Ky+e,u=-o,c=a;t.moveTo(r,i),t.lineTo(o,a),t.lineTo(u,c),t.lineTo(-.5*r-Jy*i,Jy*r+-.5*i),t.lineTo(-.5*o-Jy*a,Jy*o+-.5*a),t.lineTo(-.5*u-Jy*c,Jy*u+-.5*c),t.lineTo(-.5*r+Jy*i,-.5*i-Jy*r),t.lineTo(-.5*o+Jy*a,-.5*a-Jy*o),t.lineTo(-.5*u+Jy*c,-.5*c-Jy*u),t.closePath()}},e_=[Yy,Fy,Hy,Wy,$y,Qy,n_];function r_(){}function i_(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function o_(t){this._context=t}function a_(t){this._context=t}function u_(t){this._context=t}function c_(t,n){this._basis=new o_(t),this._beta=n}o_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:i_(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:i_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},a_.prototype={areaStart:r_,areaEnd:r_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:i_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},u_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:i_(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},c_.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],a=t[e]-i,u=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*a),this._beta*n[c]+(1-this._beta)*(o+r*u));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var f_=function t(n){function e(t){return 1===n?new o_(t):new c_(t,n)}return e.beta=function(n){return t(+n)},e}(.85);function s_(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function l_(t,n){this._context=t,this._k=(1-n)/6}l_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:s_(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:s_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var h_=function t(n){function e(t){return new l_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function d_(t,n){this._context=t,this._k=(1-n)/6}d_.prototype={areaStart:r_,areaEnd:r_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:s_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var p_=function t(n){function e(t){return new d_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function v_(t,n){this._context=t,this._k=(1-n)/6}v_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:s_(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var g_=function t(n){function e(t){return new v_(t,n)}return e.tension=function(n){return t(+n)},e}(0);function y_(t,n,e){var r=t._x1,i=t._y1,o=t._x2,a=t._y2;if(t._l01_a>uy){var u=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*u-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*u-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>uy){var f=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,s=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*f+t._x1*t._l23_2a-n*t._l12_2a)/s,a=(a*f+t._y1*t._l23_2a-e*t._l12_2a)/s}t._context.bezierCurveTo(r,i,o,a,t._x2,t._y2)}function __(t,n){this._context=t,this._alpha=n}__.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:y_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var b_=function t(n){function e(t){return n?new __(t,n):new l_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function m_(t,n){this._context=t,this._alpha=n}m_.prototype={areaStart:r_,areaEnd:r_,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:y_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var x_=function t(n){function e(t){return n?new m_(t,n):new d_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function w_(t,n){this._context=t,this._alpha=n}w_.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:y_(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var M_=function t(n){function e(t){return n?new w_(t,n):new v_(t,0)}return e.alpha=function(n){return t(+n)},e}(.5);function N_(t){this._context=t}function A_(t){return t<0?-1:1}function T_(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),a=(e-t._y1)/(i||r<0&&-0),u=(o*i+a*r)/(r+i);return(A_(o)+A_(a))*Math.min(Math.abs(o),Math.abs(a),.5*Math.abs(u))||0}function S_(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function k_(t,n,e){var r=t._x0,i=t._y0,o=t._x1,a=t._y1,u=(o-r)/3;t._context.bezierCurveTo(r+u,i+u*n,o-u,a-u*e,o,a)}function E_(t){this._context=t}function C_(t){this._context=new P_(t)}function P_(t){this._context=t}function z_(t){this._context=t}function R_(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),a=new Array(r);for(i[0]=0,o[0]=2,a[0]=t[0]+2*t[1],n=1;n=0;--n)i[n]=(a[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n1)for(var e,r,i,o=1,a=t[n[0]],u=a.length;o=0;)e[n]=n;return e}function U_(t,n){return t[n]}function O_(t){var n=t.map(B_);return L_(t).sort(function(t,e){return n[t]-n[e]})}function B_(t){for(var n,e=-1,r=0,i=t.length,o=-1/0;++eo&&(o=n,r=e);return r}function Y_(t){var n=t.map(F_);return L_(t).sort(function(t,e){return n[t]-n[e]})}function F_(t){for(var n,e=0,r=-1,i=t.length;++r0)){if(o/=h,h<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=r-c,h||!(o<0)){if(o/=h,h<0){if(o>l)return;o>s&&(s=o)}else if(h>0){if(o0)){if(o/=d,d<0){if(o0){if(o>l)return;o>s&&(s=o)}if(o=i-f,d||!(o<0)){if(o/=d,d<0){if(o>l)return;o>s&&(s=o)}else if(d>0){if(o0||l<1)||(s>0&&(t[0]=[c+s*h,f+s*d]),l<1&&(t[1]=[c+l*h,f+l*d]),!0)}}}}}function tb(t,n,e,r,i){var o=t[1];if(o)return!0;var a,u,c=t[0],f=t.left,s=t.right,l=f[0],h=f[1],d=s[0],p=s[1],v=(l+d)/2,g=(h+p)/2;if(p===h){if(v=r)return;if(l>d){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]1)if(l>d){if(c){if(c[1]>=i)return}else c=[(e-u)/a,e];o=[(i-u)/a,i]}else{if(c){if(c[1]=r)return}else c=[n,a*n+u];o=[r,a*r+u]}else{if(c){if(c[0]=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}},X_.prototype={constructor:X_,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=W_(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)e===(r=e.U).L?(i=r.R)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(V_(this,e),e=(t=e).U),e.C=!1,r.C=!0,$_(this,r)):(i=r.L)&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&($_(this,e),e=(t=e).U),e.C=!1,r.C=!0,V_(this,r)),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,a=t.R;if(e=o?a?W_(a):o:a,i?i.L===t?i.L=e:i.R=e:this._=e,o&&a?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==a?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=a,a.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r)if(t&&t.C)t.C=!1;else{do{if(t===this._)break;if(t===i.L){if((n=i.R).C&&(n.C=!1,i.C=!0,V_(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,$_(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,V_(this,i),t=this._;break}}else if((n=i.L).C&&(n.C=!1,i.C=!0,$_(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,V_(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,$_(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var ib,ob=[];function ab(){G_(this),this.x=this.y=this.arc=this.site=this.cy=null}function ub(t){var n=t.P,e=t.N;if(n&&e){var r=n.site,i=t.site,o=e.site;if(r!==o){var a=i[0],u=i[1],c=r[0]-a,f=r[1]-u,s=o[0]-a,l=o[1]-u,h=2*(c*l-f*s);if(!(h>=-wb)){var d=c*c+f*f,p=s*s+l*l,v=(l*d-f*p)/h,g=(c*p-s*d)/h,y=ob.pop()||new ab;y.arc=t,y.site=i,y.x=v+a,y.y=(y.cy=g+u)+Math.sqrt(v*v+g*g),t.circle=y;for(var _=null,b=bb._;b;)if(y.yxb)u=u.L;else{if(!((i=o-gb(u,a))>xb)){r>-xb?(n=u.P,e=u):i>-xb?(n=u,e=u.N):n=e=u;break}if(!u.R){n=u;break}u=u.R}!function(t){_b[t.index]={site:t,halfedges:[]}}(t);var c=lb(t);if(yb.insert(n,c),n||e){if(n===e)return cb(n),e=lb(n.site),yb.insert(c,e),c.edge=e.edge=Z_(n.site,c.site),ub(n),void ub(e);if(e){cb(n),cb(e);var f=n.site,s=f[0],l=f[1],h=t[0]-s,d=t[1]-l,p=e.site,v=p[0]-s,g=p[1]-l,y=2*(h*g-d*v),_=h*h+d*d,b=v*v+g*g,m=[(g*_-d*b)/y+s,(h*b-v*_)/y+l];J_(e.edge,f,p,m),c.edge=Z_(f,t,null,m),e.edge=Z_(t,p,null,m),ub(n),ub(e)}else c.edge=Z_(n.site,c.site)}}function vb(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var a=t.P;if(!a)return-1/0;var u=(e=a.site)[0],c=e[1],f=c-n;if(!f)return u;var s=u-r,l=1/o-1/f,h=s/f;return l?(-h+Math.sqrt(h*h-2*l*(s*s/(-2*f)-c+f/2+i-o/2)))/l+r:(r+u)/2}function gb(t,n){var e=t.N;if(e)return vb(e,n);var r=t.site;return r[1]===n?r[0]:1/0}var yb,_b,bb,mb,xb=1e-6,wb=1e-12;function Mb(t,n){return n[1]-t[1]||n[0]-t[0]}function Nb(t,n){var e,r,i,o=t.sort(Mb).pop();for(mb=[],_b=new Array(t.length),yb=new X_,bb=new X_;;)if(i=ib,o&&(!i||o[1]xb||Math.abs(i[0][1]-i[1][1])>xb)||delete mb[o]}(a,u,c,f),function(t,n,e,r){var i,o,a,u,c,f,s,l,h,d,p,v,g=_b.length,y=!0;for(i=0;ixb||Math.abs(v-h)>xb)&&(c.splice(u,0,mb.push(Q_(a,d,Math.abs(p-t)xb?[t,Math.abs(l-t)xb?[Math.abs(h-r)xb?[e,Math.abs(l-e)xb?[Math.abs(h-n)=u)return null;var c=t-i.site[0],f=n-i.site[1],s=c*c+f*f;do{i=o.cells[r=a],a=null,i.halfedges.forEach(function(e){var r=o.edges[e],u=r.left;if(u!==i.site&&u||(u=r.right)){var c=t-u[0],f=n-u[1],l=c*c+f*f;lr?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}Eb.prototype=Sb.prototype,t.version="5.9.2",t.bisect=i,t.bisectRight=i,t.bisectLeft=o,t.ascending=n,t.bisector=e,t.cross=function(t,n,e){var r,i,o,u,c=t.length,f=n.length,s=new Array(c*f);for(null==e&&(e=a),r=o=0;rt?1:n>=t?0:NaN},t.deviation=f,t.extent=s,t.histogram=function(){var t=v,n=s,e=M;function r(r){var o,a,u=r.length,c=new Array(u);for(o=0;ol;)h.pop(),--d;var p,v=new Array(d+1);for(o=0;o<=d;++o)(p=v[o]=[]).x0=o>0?h[o-1]:s,p.x1=o=r.length)return null!=t&&e.sort(t),null!=n?n(e):e;for(var c,f,s,l=-1,h=e.length,d=r[i++],p=Qi(),v=a();++lr.length)return e;var a,u=i[o-1];return null!=n&&o>=r.length?a=e.entries():(a=[],e.each(function(n,e){a.push({key:e,values:t(n,o)})})),null!=u?a.sort(function(t,n){return u(t.key,n.key)}):a}(o(t,0,to,no),0)},key:function(t){return r.push(t),e},sortKeys:function(t){return i[r.length-1]=t,e},sortValues:function(n){return t=n,e},rollup:function(t){return n=t,e}}},t.set=io,t.map=Qi,t.keys=function(t){var n=[];for(var e in t)n.push(e);return n},t.values=function(t){var n=[];for(var e in t)n.push(t[e]);return n},t.entries=function(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n},t.color=hn,t.rgb=gn,t.hsl=mn,t.lab=Rn,t.hcl=Yn,t.lch=function(t,n,e,r){return 1===arguments.length?Bn(t):new Fn(e,n,t,null==r?1:r)},t.gray=function(t,n){return new Dn(t,0,0,null==n?1:n)},t.cubehelix=Zn,t.contours=po,t.contourDensity=function(){var t=yo,n=_o,e=bo,r=960,i=500,o=20,a=2,u=3*o,c=r+2*u>>a,f=i+2*u>>a,s=uo(20);function l(r){var i=new Float32Array(c*f),l=new Float32Array(c*f);r.forEach(function(r,o,s){var l=+t(r,o,s)+u>>a,h=+n(r,o,s)+u>>a,d=+e(r,o,s);l>=0&&l=0&&h>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),vo({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a),vo({width:c,height:f,data:i},{width:c,height:f,data:l},o>>a),go({width:c,height:f,data:l},{width:c,height:f,data:i},o>>a);var d=s(i);if(!Array.isArray(d)){var p=A(i);d=w(0,p,d),(d=g(0,Math.floor(p/d)*d,d)).shift()}return po().thresholds(d).size([c,f])(i).map(h)}function h(t){return t.value*=Math.pow(2,-2*a),t.coordinates.forEach(d),t}function d(t){t.forEach(p)}function p(t){t.forEach(v)}function v(t){t[0]=t[0]*Math.pow(2,a)-u,t[1]=t[1]*Math.pow(2,a)-u}function y(){return c=r+2*(u=3*o)>>a,f=i+2*u>>a,l}return l.x=function(n){return arguments.length?(t="function"==typeof n?n:uo(+n),l):t},l.y=function(t){return arguments.length?(n="function"==typeof t?t:uo(+t),l):n},l.weight=function(t){return arguments.length?(e="function"==typeof t?t:uo(+t),l):e},l.size=function(t){if(!arguments.length)return[r,i];var n=Math.ceil(t[0]),e=Math.ceil(t[1]);if(!(n>=0||n>=0))throw new Error("invalid size");return r=n,i=e,y()},l.cellSize=function(t){if(!arguments.length)return 1<=1))throw new Error("invalid cell size");return a=Math.floor(Math.log(t)/Math.LN2),y()},l.thresholds=function(t){return arguments.length?(s="function"==typeof t?t:Array.isArray(t)?uo(oo.call(t)):uo(t),l):s},l.bandwidth=function(t){if(!arguments.length)return Math.sqrt(o*(o+1));if(!((t=+t)>=0))throw new Error("invalid bandwidth");return o=Math.round((Math.sqrt(4*t*t+1)-1)/2),y()},l},t.dispatch=I,t.drag=function(){var n,e,r,i,o=Gt,a=Vt,u=$t,c=Wt,f={},s=I("start","drag","end"),l=0,h=0;function d(t){t.on("mousedown.drag",p).filter(c).on("touchstart.drag",y).on("touchmove.drag",_).on("touchend.drag touchcancel.drag",b).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function p(){if(!i&&o.apply(this,arguments)){var u=m("mouse",a.apply(this,arguments),Ot,this,arguments);u&&(zt(t.event.view).on("mousemove.drag",v,!0).on("mouseup.drag",g,!0),It(t.event.view),Yt(),r=!1,n=t.event.clientX,e=t.event.clientY,u("start"))}}function v(){if(Ft(),!r){var i=t.event.clientX-n,o=t.event.clientY-e;r=i*i+o*o>h}f.mouse("drag")}function g(){zt(t.event.view).on("mousemove.drag mouseup.drag",null),jt(t.event.view,r),Ft(),f.mouse("end")}function y(){if(o.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=a.apply(this,arguments),u=r.length;for(n=0;nc+d||if+d||ou.index){var p=c-a.x-a.vx,v=f-a.y-a.vy,g=p*p+v*v;gt.r&&(t.r=t[n].r)}function u(){if(n){var r,i,o=n.length;for(e=new Array(o),r=0;r=a)){(t.data!==n||t.next)&&(0===s&&(d+=(s=na())*s),0===l&&(d+=(l=na())*l),d1?(null==e?u.remove(t):u.set(t,d(e)),n):u.get(t)},find:function(n,e,r){var i,o,a,u,c,f=0,s=t.length;for(null==r?r=1/0:r*=r,f=0;f1?(f.on(t,e),n):f.on(t)}}},t.forceX=function(t){var n,e,r,i=ta(.1);function o(t){for(var i,o=0,a=n.length;opc(r[0],r[1])&&(r[1]=i[1]),pc(i[0],r[1])>pc(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(a=-1/0,n=0,r=o[e=o.length-1];n<=e;r=i,++n)i=o[n],(u=pc(r[1],i[0]))>a&&(a=u,Ru=i[0],qu=r[1])}return Fu=Iu=null,Ru===1/0||Du===1/0?[[NaN,NaN],[NaN,NaN]]:[[Ru,Du],[qu,Lu]]},t.geoCentroid=function(t){ju=Hu=Xu=Gu=Vu=$u=Wu=Zu=Qu=Ju=Ku=0,du(t,yc);var n=Qu,e=Ju,r=Ku,i=n*n+e*e+r*r;return i=.12&&i<.234&&r>=-.425&&r<-.214?u:i>=.166&&i<.234&&r>=-.214&&r<-.115?c:a).invert(t)},s.stream=function(e){return t&&n===e?t:(r=[a.stream(n=e),u.stream(e),c.stream(e)],i=r.length,t={point:function(t,n){for(var e=-1;++e2?t[2]+90:90]):[(t=e())[0],t[1],t[2]-90]},e([0,0,90]).scale(159.155)},t.geoTransverseMercatorRaw=al,t.geoRotation=qc,t.geoStream=du,t.geoTransform=function(t){return{stream:xs(t)}},t.cluster=function(){var t=ul,n=1,e=1,r=!1;function i(i){var o,a=0;i.eachAfter(function(n){var e=n.children;e?(n.x=function(t){return t.reduce(cl,0)/t.length}(e),n.y=function(t){return 1+t.reduce(fl,0)}(e)):(n.x=o?a+=t(n,o):0,n.y=0,o=n)});var u=function(t){for(var n;n=t.children;)t=n[0];return t}(i),c=function(t){for(var n;n=t.children;)t=n[n.length-1];return t}(i),f=u.x-t(u,c)/2,s=c.x+t(c,u)/2;return i.eachAfter(r?function(t){t.x=(t.x-i.x)*n,t.y=(i.y-t.y)*e}:function(t){t.x=(t.x-f)/(s-f)*n,t.y=(1-(i.y?t.y/i.y:1))*e})}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.hierarchy=ll,t.pack=function(){var t=null,n=1,e=1,r=Pl;function i(i){return i.x=n/2,i.y=e/2,t?i.eachBefore(Dl(t)).eachAfter(ql(r,.5)).eachBefore(Ll(1)):i.eachBefore(Dl(Rl)).eachAfter(ql(Pl,1)).eachAfter(ql(r,i.r/Math.min(n,e))).eachBefore(Ll(Math.min(n,e)/(2*i.r))),i}return i.radius=function(n){return arguments.length?(t=null==(e=n)?null:Cl(e),i):t;var e},i.size=function(t){return arguments.length?(n=+t[0],e=+t[1],i):[n,e]},i.padding=function(t){return arguments.length?(r="function"==typeof t?t:zl(+t),i):r},i},t.packSiblings=function(t){return El(t),t},t.packEnclose=yl,t.partition=function(){var t=1,n=1,e=0,r=!1;function i(i){var o=i.height+1;return i.x0=i.y0=e,i.x1=t,i.y1=n/o,i.eachBefore(function(t,n){return function(r){r.children&&Ol(r,r.x0,t*(r.depth+1)/n,r.x1,t*(r.depth+2)/n);var i=r.x0,o=r.y0,a=r.x1-e,u=r.y1-e;a0)throw new Error("cycle");return o}return e.id=function(n){return arguments.length?(t=Cl(n),e):t},e.parentId=function(t){return arguments.length?(n=Cl(t),e):n},e},t.tree=function(){var t=Hl,n=1,e=1,r=null;function i(i){var c=function(t){for(var n,e,r,i,o,a=new Wl(t,0),u=[a];n=u.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)u.push(e=n.children[i]=new Wl(r[i],i)),e.parent=n;return(a.parent=new Wl(null,0)).children=[a],a}(i);if(c.eachAfter(o),c.parent.m=-c.z,c.eachBefore(a),r)i.eachBefore(u);else{var f=i,s=i,l=i;i.eachBefore(function(t){t.xs.x&&(s=t),t.depth>l.depth&&(l=t)});var h=f===s?1:t(f,s)/2,d=h-f.x,p=n/(s.x+h+d),v=e/(l.depth||1);i.eachBefore(function(t){t.x=(t.x+d)*p,t.y=t.depth*v})}return i}function o(n){var e=n.children,r=n.parent.children,i=n.i?r[n.i-1]:null;if(e){!function(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)(n=i[o]).z+=e,n.m+=e,e+=n.s+(r+=n.c)}(n);var o=(e[0].z+e[e.length-1].z)/2;i?(n.z=i.z+t(n._,i._),n.m=n.z-o):n.z=o}else i&&(n.z=i.z+t(n._,i._));n.parent.A=function(n,e,r){if(e){for(var i,o=n,a=n,u=e,c=o.parent.children[0],f=o.m,s=a.m,l=u.m,h=c.m;u=Gl(u),o=Xl(o),u&&o;)c=Xl(c),(a=Gl(a)).a=n,(i=u.z+l-o.z-f+t(u._,o._))>0&&(Vl($l(u,n,r),n,i),f+=i,s+=i),l+=u.m,f+=o.m,h+=c.m,s+=a.m;u&&!Gl(a)&&(a.t=u,a.m+=l-s),o&&!Xl(c)&&(c.t=o,c.m+=f-h,r=n)}return r}(n,i,n.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function u(t){t.x*=n,t.y=t.depth*e}return i.separation=function(n){return arguments.length?(t=n,i):t},i.size=function(t){return arguments.length?(r=!1,n=+t[0],e=+t[1],i):r?null:[n,e]},i.nodeSize=function(t){return arguments.length?(r=!0,n=+t[0],e=+t[1],i):r?[n,e]:null},i},t.treemap=function(){var t=Kl,n=!1,e=1,r=1,i=[0],o=Pl,a=Pl,u=Pl,c=Pl,f=Pl;function s(t){return t.x0=t.y0=0,t.x1=e,t.y1=r,t.eachBefore(l),i=[0],n&&t.eachBefore(Ul),t}function l(n){var e=i[n.depth],r=n.x0+e,s=n.y0+e,l=n.x1-e,h=n.y1-e;l=e-1){var s=u[n];return s.x0=i,s.y0=o,s.x1=a,void(s.y1=c)}for(var l=f[n],h=r/2+l,d=n+1,p=e-1;d>>1;f[v]c-o){var _=(i*y+a*g)/r;t(n,d,g,i,o,_,c),t(d,e,y,_,o,a,c)}else{var b=(o*y+c*g)/r;t(n,d,g,i,o,a,b),t(d,e,y,i,b,a,c)}}(0,c,t.value,n,e,r,i)},t.treemapDice=Ol,t.treemapSlice=Zl,t.treemapSliceDice=function(t,n,e,r,i){(1&t.depth?Zl:Ol)(t,n,e,r,i)},t.treemapSquarify=Kl,t.treemapResquarify=th,t.interpolate=ye,t.interpolateArray=se,t.interpolateBasis=Kn,t.interpolateBasisClosed=te,t.interpolateDate=le,t.interpolateDiscrete=function(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}},t.interpolateHue=function(t,n){var e=re(+t,+n);return function(t){var n=e(t);return n-360*Math.floor(n/360)}},t.interpolateNumber=he,t.interpolateObject=de,t.interpolateRound=_e,t.interpolateString=ge,t.interpolateTransformCss=Se,t.interpolateTransformSvg=ke,t.interpolateZoom=De,t.interpolateRgb=ae,t.interpolateRgbBasis=ce,t.interpolateRgbBasisClosed=fe,t.interpolateHsl=Le,t.interpolateHslLong=Ue,t.interpolateLab=function(t,n){var e=oe((t=Rn(t)).l,(n=Rn(n)).l),r=oe(t.a,n.a),i=oe(t.b,n.b),o=oe(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}},t.interpolateHcl=Be,t.interpolateHclLong=Ye,t.interpolateCubehelix=Ie,t.interpolateCubehelixLong=je,t.piecewise=function(t,n){for(var e=0,r=n.length-1,i=n[0],o=new Array(r<0?0:r);e=0;--n)f.push(t[r[o[n]][2]]);for(n=+u;nu!=f>u&&a<(c-e)*(u-r)/(f-r)+e&&(s=!s),c=e,f=r;return s},t.polygonLength=function(t){for(var n,e,r=-1,i=t.length,o=t[i-1],a=o[0],u=o[1],c=0;++r0?a[n-1]:r[0],n=o?[a[o-1],r]:[a[n-1],a[n]]},c.unknown=function(t){return arguments.length?(n=t,c):c},c.thresholds=function(){return a.slice()},c.copy=function(){return t().domain([e,r]).range(u).unknown(n)},sh.apply(Eh(c),arguments)},t.scaleThreshold=function t(){var n,e=[.5],r=[0,1],o=1;function a(t){return t<=t?r[i(e,t,0,o)]:n}return a.domain=function(t){return arguments.length?(e=ph.call(t),o=Math.min(e.length,r.length-1),a):e.slice()},a.range=function(t){return arguments.length?(r=ph.call(t),o=Math.min(e.length,r.length-1),a):r.slice()},a.invertExtent=function(t){var n=r.indexOf(t);return[e[n-1],e[n]]},a.unknown=function(t){return arguments.length?(n=t,a):n},a.copy=function(){return t().domain(e).range(r).unknown(n)},sh.apply(a,arguments)},t.scaleTime=function(){return sh.apply(Nv(Nd,wd,cd,od,rd,nd,Kh,Wh,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)]),arguments)},t.scaleUtc=function(){return sh.apply(Nv(Wd,Vd,Rd,Cd,kd,Td,Kh,Wh,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)]),arguments)},t.scaleSequential=function t(){var n=Eh(Av()(mh));return n.copy=function(){return Tv(n,t())},lh.apply(n,arguments)},t.scaleSequentialLog=function t(){var n=Uh(Av()).domain([1,10]);return n.copy=function(){return Tv(n,t()).base(n.base())},lh.apply(n,arguments)},t.scaleSequentialPow=Sv,t.scaleSequentialSqrt=function(){return Sv.apply(null,arguments).exponent(.5)},t.scaleSequentialSymlog=function t(){var n=Yh(Av());return n.copy=function(){return Tv(n,t()).constant(n.constant())},lh.apply(n,arguments)},t.scaleSequentialQuantile=function t(){var e=[],r=mh;function o(t){if(!isNaN(t=+t))return r((i(e,t)-1)/(e.length-1))}return o.domain=function(t){if(!arguments.length)return e.slice();e=[];for(var r,i=0,a=t.length;i1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return Hg.h=360*t-100,Hg.s=1.5-1.5*n,Hg.l=.8-.9*n,Hg+""},t.interpolateWarm=Ig,t.interpolateCool=jg,t.interpolateSinebow=function(t){var n;return t=(.5-t)*Math.PI,Xg.r=255*(n=Math.sin(t))*n,Xg.g=255*(n=Math.sin(t+Gg))*n,Xg.b=255*(n=Math.sin(t+Vg))*n,Xg+""},t.interpolateViridis=Wg,t.interpolateMagma=Zg,t.interpolateInferno=Qg,t.interpolatePlasma=Jg,t.create=function(t){return zt(W(t).call(document.documentElement))},t.creator=W,t.local=Dt,t.matcher=tt,t.mouse=Ot,t.namespace=$,t.namespaces=V,t.clientPoint=Ut,t.select=zt,t.selectAll=function(t){return"string"==typeof t?new Ct([document.querySelectorAll(t)],[document.documentElement]):new Ct([null==t?[]:t],Et)},t.selection=Pt,t.selector=Q,t.selectorAll=K,t.style=ct,t.touch=Bt,t.touches=function(t,n){null==n&&(n=Lt().touches);for(var e=0,r=n?n.length:0,i=new Array(r);ed;if(u||(u=c=Hi()),huy)if(v>sy-uy)u.moveTo(h*ey(d),h*oy(d)),u.arc(0,0,h,d,p,!g),l>uy&&(u.moveTo(l*ey(p),l*oy(p)),u.arc(0,0,l,p,d,g));else{var y,_,b=d,m=p,x=d,w=p,M=v,N=v,A=a.apply(this,arguments)/2,T=A>uy&&(r?+r.apply(this,arguments):ay(l*l+h*h)),S=iy(ty(h-l)/2,+e.apply(this,arguments)),k=S,E=S;if(T>uy){var C=ly(T/l*oy(A)),P=ly(T/h*oy(A));(M-=2*C)>uy?(x+=C*=g?1:-1,w-=C):(M=0,x=w=(d+p)/2),(N-=2*P)>uy?(b+=P*=g?1:-1,m-=P):(N=0,b=m=(d+p)/2)}var z=h*ey(b),R=h*oy(b),D=l*ey(w),q=l*oy(w);if(S>uy){var L,U=h*ey(m),O=h*oy(m),B=l*ey(x),Y=l*oy(x);if(v1?0:s<-1?cy:Math.acos(s))/2),G=ay(L[0]*L[0]+L[1]*L[1]);k=iy(S,(l-G)/(X-1)),E=iy(S,(h-G)/(X+1))}}N>uy?E>uy?(y=yy(B,Y,z,R,h,E,g),_=yy(U,O,D,q,h,E,g),u.moveTo(y.cx+y.x01,y.cy+y.y01),Euy&&M>uy?k>uy?(y=yy(D,q,U,O,l,-k,g),_=yy(z,R,B,Y,l,-k,g),u.lineTo(y.cx+y.x01,y.cy+y.y01),k0&&(d+=l);for(null!=n?p.sort(function(t,e){return n(v[t],v[e])}):null!=e&&p.sort(function(t,n){return e(a[t],a[n])}),u=0,f=d?(y-h*b)/d:0;u0?l*f:0)+b,v[c]={data:a[c],index:u,value:l,startAngle:g,endAngle:s,padAngle:_};return v}return a.value=function(n){return arguments.length?(t="function"==typeof n?n:Kg(+n),a):t},a.sortValues=function(t){return arguments.length?(n=t,e=null,a):n},a.sort=function(t){return arguments.length?(e=t,n=null,a):e},a.startAngle=function(t){return arguments.length?(r="function"==typeof t?t:Kg(+t),a):r},a.endAngle=function(t){return arguments.length?(i="function"==typeof t?t:Kg(+t),a):i},a.padAngle=function(t){return arguments.length?(o="function"==typeof t?t:Kg(+t),a):o},a},t.areaRadial=Py,t.radialArea=Py,t.lineRadial=Cy,t.radialLine=Cy,t.pointRadial=zy,t.linkHorizontal=function(){return Ly(Uy)},t.linkVertical=function(){return Ly(Oy)},t.linkRadial=function(){var t=Ly(By);return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t},t.symbol=function(){var t=Kg(Yy),n=Kg(64),e=null;function r(){var r;if(e||(e=r=Hi()),t.apply(this,arguments).draw(e,+n.apply(this,arguments)),r)return e=null,r+""||null}return r.type=function(n){return arguments.length?(t="function"==typeof n?n:Kg(n),r):t},r.size=function(t){return arguments.length?(n="function"==typeof t?t:Kg(+t),r):n},r.context=function(t){return arguments.length?(e=null==t?null:t,r):e},r},t.symbols=e_,t.symbolCircle=Yy,t.symbolCross=Fy,t.symbolDiamond=Hy,t.symbolSquare=Wy,t.symbolStar=$y,t.symbolTriangle=Qy,t.symbolWye=n_,t.curveBasisClosed=function(t){return new a_(t)},t.curveBasisOpen=function(t){return new u_(t)},t.curveBasis=function(t){return new o_(t)},t.curveBundle=f_,t.curveCardinalClosed=p_,t.curveCardinalOpen=g_,t.curveCardinal=h_,t.curveCatmullRomClosed=x_,t.curveCatmullRomOpen=M_,t.curveCatmullRom=b_,t.curveLinearClosed=function(t){return new N_(t)},t.curveLinear=by,t.curveMonotoneX=function(t){return new E_(t)},t.curveMonotoneY=function(t){return new C_(t)},t.curveNatural=function(t){return new z_(t)},t.curveStep=function(t){return new D_(t,.5)},t.curveStepAfter=function(t){return new D_(t,1)},t.curveStepBefore=function(t){return new D_(t,0)},t.stack=function(){var t=Kg([]),n=L_,e=q_,r=U_;function i(i){var o,a,u=t.apply(this,arguments),c=i.length,f=u.length,s=new Array(f);for(o=0;o0){for(var e,r,i,o=0,a=t[0].length;o1)for(var e,r,i,o,a,u,c=0,f=t[n[0]].length;c=0?(r[0]=o,r[1]=o+=i):i<0?(r[1]=a,r[0]=a+=i):r[0]=o},t.stackOffsetNone=q_,t.stackOffsetSilhouette=function(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,a=1;adr&&e.name===n)return new Er([[t]],fi,n,+r);return null},t.interrupt=Mr,t.voronoi=function(){var t=j_,n=H_,e=null;function r(r){return new Nb(r.map(function(e,i){var o=[Math.round(t(e,i,r)/xb)*xb,Math.round(n(e,i,r)/xb)*xb];return o.index=i,o.data=e,o}),e)}return r.polygons=function(t){return r(t).polygons()},r.links=function(t){return r(t).links()},r.triangles=function(t){return r(t).triangles()},r.x=function(n){return arguments.length?(t="function"==typeof n?n:I_(+n),r):t},r.y=function(t){return arguments.length?(n="function"==typeof t?t:I_(+t),r):n},r.extent=function(t){return arguments.length?(e=null==t?null:[[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]],r):e&&[[e[0][0],e[0][1]],[e[1][0],e[1][1]]]},r.size=function(t){return arguments.length?(e=null==t?null:[[0,0],[+t[0],+t[1]]],r):e&&[e[1][0]-e[0][0],e[1][1]-e[0][1]]},r},t.zoom=function(){var n,e,r=zb,i=Rb,o=Ub,a=qb,u=Lb,c=[0,1/0],f=[[-1/0,-1/0],[1/0,1/0]],s=250,l=De,h=[],d=I("start","zoom","end"),p=500,v=150,g=0;function y(t){t.property("__zoom",Db).on("wheel.zoom",N).on("mousedown.zoom",A).on("dblclick.zoom",T).filter(u).on("touchstart.zoom",S).on("touchmove.zoom",k).on("touchend.zoom touchcancel.zoom",E).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function _(t,n){return(n=Math.max(c[0],Math.min(c[1],n)))===t.k?t:new Sb(n,t.x,t.y)}function b(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new Sb(t.k,r,i)}function m(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,n,e){t.on("start.zoom",function(){w(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){w(this,arguments).end()}).tween("zoom",function(){var t=arguments,r=w(this,t),o=i.apply(this,t),a=e||m(o),u=Math.max(o[1][0]-o[0][0],o[1][1]-o[0][1]),c=this.__zoom,f="function"==typeof n?n.apply(this,t):n,s=l(c.invert(a).concat(u/c.k),f.invert(a).concat(u/f.k));return function(t){if(1===t)t=f;else{var n=s(t),e=u/n[2];t=new Sb(e,a[0]-n[0]*e,a[1]-n[1]*e)}r.zoom(null,t)}})}function w(t,n){for(var e,r=0,i=h.length;rg}n.zoom("mouse",o(b(n.that.__zoom,n.mouse[0]=Ot(n.that),n.mouse[1]),n.extent,f))},!0).on("mouseup.zoom",function(){i.on("mousemove.zoom mouseup.zoom",null),jt(t.event.view,n.moved),Pb(),n.end()},!0),a=Ot(this),u=t.event.clientX,c=t.event.clientY;It(t.event.view),Cb(),n.mouse=[a,this.__zoom.invert(a)],Mr(this),n.start()}}function T(){if(r.apply(this,arguments)){var n=this.__zoom,e=Ot(this),a=n.invert(e),u=n.k*(t.event.shiftKey?.5:2),c=o(b(_(n,u),e,a),i.apply(this,arguments),f);Pb(),s>0?zt(this).transition().duration(s).call(x,c,e):zt(this).call(y.transform,c)}}function S(){if(r.apply(this,arguments)){var e,i,o,a,u=w(this,arguments),c=t.event.changedTouches,f=c.length;for(Cb(),i=0;i Date: Fri, 3 May 2019 12:26:35 -0700 Subject: [PATCH 25/42] Roughly support stacked area charts Summary: Ref T13279. This adds support for: - Datasets can have types, like "stacked area". - Datasets can have multiple functions. - Charts can store dataset types and datasets with multiple functions. - Adds a "stacked area" dataset. - Makes D3 actually draw a stacked area chart. Lots of rough edges here still, but the result looks slightly more like it's supposed to look. D3 can do some of this logic itself, like adding up the area stacks on top of one another with `d3.stack()`. I'm doing it in PHP instead because I think it's a bit easier to debug, and it gives us more options for things like caching or "export to CSV" or "export to API" or rendering a data table under the chart or whatever. Test Plan: {F6427780} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20498 --- resources/celerity/map.php | 12 +- src/__phutil_library_map__.php | 2 + .../PhabricatorAccumulateChartFunction.php | 5 +- .../fact/chart/PhabricatorChartDataset.php | 69 ++++++-- .../fact/chart/PhabricatorChartFunction.php | 31 +++- .../PhabricatorChartStackedAreaDataset.php | 149 ++++++++++++++++++ .../PhabricatorChartRenderingEngine.php | 40 ++--- .../PhabricatorProjectBurndownChartEngine.php | 44 +++--- webroot/rsrc/js/application/fact/Chart.js | 131 ++++++++------- 9 files changed, 343 insertions(+), 140 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b23ffca37c..a2bc989ea6 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -389,7 +389,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' => 'fcb0c07d', + 'rsrc/js/application/fact/Chart.js' => 'a3516cea', 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', @@ -696,7 +696,7 @@ return array( 'javelin-behavior-user-menu' => '60cd9241', 'javelin-behavior-view-placeholder' => 'a9942052', 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => 'fcb0c07d', + 'javelin-chart' => 'a3516cea', 'javelin-color' => '78f811c9', 'javelin-cookie' => '05d290ef', 'javelin-diffusion-locate-file-source' => '94243d89', @@ -1767,6 +1767,10 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', ), + 'a3516cea' => array( + 'phui-chart-css', + 'd3', + ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -2180,10 +2184,6 @@ return array( 'fa74cc35' => array( 'phui-oi-list-view-css', ), - 'fcb0c07d' => array( - 'phui-chart-css', - 'd3', - ), 'fdc13e4e' => array( 'javelin-install', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4b32787a36..21cd7c90d9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2669,6 +2669,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', 'PhabricatorChartRenderingEngine' => 'applications/fact/engine/PhabricatorChartRenderingEngine.php', + 'PhabricatorChartStackedAreaDataset' => 'applications/fact/chart/PhabricatorChartStackedAreaDataset.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', @@ -8683,6 +8684,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunctionArgument' => 'Phobject', 'PhabricatorChartFunctionArgumentParser' => 'Phobject', 'PhabricatorChartRenderingEngine' => 'Phobject', + 'PhabricatorChartStackedAreaDataset' => 'PhabricatorChartDataset', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', 'PhabricatorChatLogChannel' => array( 'PhabricatorChatLogDAO', diff --git a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php index 074219504c..6ffbb85da9 100644 --- a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php +++ b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php @@ -35,8 +35,9 @@ final class PhabricatorAccumulateChartFunction $datasource_xv = $datasource->newInputValues($empty_query); if (!$datasource_xv) { - // TODO: Maybe this should just be an error? - $datasource_xv = $xv; + // When the datasource has no datapoints, we can't evaluate the function + // anywhere. + return array_fill(0, count($xv), null); } $yv = $datasource->evaluateFunction($datasource_xv); diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php index 48355c3b36..df3984f9ce 100644 --- a/src/applications/fact/chart/PhabricatorChartDataset.php +++ b/src/applications/fact/chart/PhabricatorChartDataset.php @@ -1,42 +1,77 @@ function; + final public function getDatasetTypeKey() { + return $this->getPhobjectClassConstant('DATASETKEY', 32); } - public function setFunction(PhabricatorComposeChartFunction $function) { - $this->function = $function; + final public function getFunctions() { + return $this->functions; + } + + final public function setFunctions(array $functions) { + assert_instances_of($functions, 'PhabricatorComposeChartFunction'); + + $this->functions = $functions; + return $this; } - public static function newFromDictionary(array $map) { + final public static function getAllDatasetTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getDatasetTypeKey') + ->execute(); + } + + final public static function newFromDictionary(array $map) { PhutilTypeSpec::checkMap( $map, array( - 'function' => 'list', + 'type' => 'string', + 'functions' => 'list', )); - $dataset = new self(); + $types = self::getAllDatasetTypes(); - $dataset->function = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($map['function'])); + $dataset_type = $map['type']; + if (!isset($types[$dataset_type])) { + throw new Exception( + pht( + 'Trying to construct a dataset of type "%s", but this type is '. + 'unknown. Supported types are: %s.', + $dataset_type, + implode(', ', array_keys($types)))); + } + + $dataset = id(clone $types[$dataset_type]); + + $functions = array(); + foreach ($map['functions'] as $map) { + $functions[] = PhabricatorChartFunction::newFromDictionary($map); + } + $dataset->setFunctions($functions); return $dataset; } - public function toDictionary() { - // Since we wrap the raw value in a "compose(...)", when deserializing, - // we need to unwrap it when serializing. - $function_raw = head($this->getFunction()->toDictionary()); - + final public function toDictionary() { return array( - 'function' => $function_raw, + 'type' => $this->getDatasetTypeKey(), + 'functions' => mpull($this->getFunctions(), 'toDictionary'), ); } + final public function getWireFormat(PhabricatorChartDataQuery $data_query) { + return $this->newWireFormat($data_query); + } + + abstract protected function newWireFormat( + PhabricatorChartDataQuery $data_query); + + } diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php index b4a66645ad..5b610ae0fa 100644 --- a/src/applications/fact/chart/PhabricatorChartFunction.php +++ b/src/applications/fact/chart/PhabricatorChartFunction.php @@ -43,8 +43,37 @@ abstract class PhabricatorChartFunction return $this; } + final public static function newFromDictionary(array $map) { + PhutilTypeSpec::checkMap( + $map, + array( + 'function' => 'string', + 'arguments' => 'list', + )); + + $functions = self::getAllFunctions(); + + $function_name = $map['function']; + if (!isset($functions[$function_name])) { + throw new Exception( + pht( + 'Attempting to build function "%s" from dictionary, but that '. + 'function is unknown. Known functions are: %s.', + $function_name, + implode(', ', array_keys($functions)))); + } + + $function = id(clone $functions[$function_name]) + ->setArguments($map['arguments']); + + return $function; + } + public function toDictionary() { - return $this->getArgumentParser()->getRawArguments(); + return array( + 'function' => $this->getFunctionKey(), + 'arguments' => $this->getArgumentParser()->getRawArguments(), + ); } public function getSubfunctions() { diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php new file mode 100644 index 0000000000..1683e6e268 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -0,0 +1,149 @@ +getFunctions(); + + $function_points = array(); + foreach ($functions as $function_idx => $function) { + $function_points[$function_idx] = array(); + + $datapoints = $function->newDatapoints($data_query); + foreach ($datapoints as $point) { + $x = $point['x']; + $function_points[$function_idx][$x] = $point; + } + } + + $raw_points = $function_points; + + // 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 + // end up with weird gaps or overlaps between adjacent areas, and won't + // know how much we need to lift each point above the baseline when + // stacking the functions on top of one another. + + $must_define = array(); + foreach ($function_points as $function_idx => $points) { + foreach ($points as $x => $point) { + $must_define[$x] = $x; + } + } + ksort($must_define); + + foreach ($functions as $function_idx => $function) { + $missing = array(); + foreach ($must_define as $x) { + if (!isset($function_points[$function_idx][$x])) { + $missing[$x] = true; + } + } + + if (!$missing) { + continue; + } + + $points = $function_points[$function_idx]; + + $values = array_keys($points); + $cursor = -1; + $length = count($values); + + foreach ($missing as $x => $ignored) { + // Move the cursor forward until we find the last point before "x" + // which is defined. + while ($cursor + 1 < $length && $values[$cursor + 1] < $x) { + $cursor++; + } + + // If this new point is to the left of all defined points, we'll + // assume the value is 0. If the point is to the right of all defined + // points, we assume the value is the same as the last known value. + + // If it's between two defined points, we average them. + + if ($cursor < 0) { + $y = 0; + } else if ($cursor + 1 < $length) { + $xmin = $values[$cursor]; + $xmax = $values[$cursor + 1]; + + $ymin = $points[$xmin]['y']; + $ymax = $points[$xmax]['y']; + + // Fill in the missing point by creating a linear interpolation + // between the two adjacent points. + $distance = ($x - $xmin) / ($xmax - $xmin); + $y = $ymin + (($ymax - $ymin) * $distance); + } else { + $xmin = $values[$cursor]; + $y = $function_points[$function_idx][$xmin]['y']; + } + + $function_points[$function_idx][$x] = array( + 'x' => $x, + 'y' => $y, + ); + } + + ksort($function_points[$function_idx]); + } + + $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; + } + } + + $series[] = $bounds; + } + + $events = array(); + foreach ($raw_points as $function_idx => $points) { + $event_list = array(); + foreach ($points as $point) { + $event_list[] = $point; + } + $events[] = $event_list; + } + + $result = array( + 'type' => $this->getDatasetTypeKey(), + 'data' => $series, + 'events' => $events, + 'color' => array( + 'blue', + 'cyan', + 'green', + ), + ); + + return $result; + } + + +} diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index 7916e35704..1b77d2403f 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -119,7 +119,9 @@ final class PhabricatorChartRenderingEngine $functions = array(); foreach ($datasets as $dataset) { - $functions[] = $dataset->getFunction(); + foreach ($dataset->getFunctions() as $function) { + $functions[] = $function; + } } $subfunctions = array(); @@ -144,39 +146,17 @@ final class PhabricatorChartRenderingEngine ->setMaximumValue($domain_max) ->setLimit(2000); - $datasets = array(); - foreach ($functions as $function) { - $points = $function->newDatapoints($data_query); - - $x = array(); - $y = array(); - - foreach ($points as $point) { - $x[] = $point['x']; - $y[] = $point['y']; - } - - $datasets[] = array( - 'x' => $x, - 'y' => $y, - 'color' => '#ff00ff', - ); - } - - - $y_min = 0; - $y_max = 0; + $wire_datasets = array(); foreach ($datasets as $dataset) { - if (!$dataset['y']) { - continue; - } - - $y_min = min($y_min, min($dataset['y'])); - $y_max = max($y_max, max($dataset['y'])); + $wire_datasets[] = $dataset->getWireFormat($data_query); } + // TODO: Figure these out from the datasets again. + $y_min = -2; + $y_max = 20; + $chart_data = array( - 'datasets' => $datasets, + 'datasets' => $wire_datasets, 'xMin' => $domain_min, 'xMax' => $domain_max, 'yMin' => $y_min, diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index 4b4a99ecf4..fd4a872bbd 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -30,37 +30,33 @@ final class PhabricatorProjectBurndownChartEngine if ($project_phids) { foreach ($project_phids as $project_phid) { $argvs[] = array( - 'sum', - array( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), - ), + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid), + ); + $argvs[] = array( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid), + ); + $argvs[] = array( + 'accumulate', + array('fact', 'tasks.open-count.assign.project', $project_phid), ); } } else { - $argvs[] = array( - 'sum', - array('accumulate', array('fact', 'tasks.open-count.create')), - array('accumulate', array('fact', 'tasks.open-count.status')), - ); + $argvs[] = array('accumulate', array('fact', 'tasks.open-count.create')); + $argvs[] = array('accumulate', array('fact', 'tasks.open-count.status')); + } + + $functions = array(); + foreach ($argvs as $argv) { + $functions[] = id(new PhabricatorComposeChartFunction()) + ->setArguments(array($argv)); } $datasets = array(); - foreach ($argvs as $argv) { - $function = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); - $datasets[] = id(new PhabricatorChartDataset()) - ->setFunction($function); - } + $datasets[] = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions($functions); $chart = id(new PhabricatorFactChart()) ->setDatasets($datasets); diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index ceb3b2ad00..66a9b98d70 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -26,6 +26,10 @@ JX.install('Chart', { } var hardpoint = this._rootNode; + + // Remove the old chart (if one exists) before drawing the new chart. + JX.DOM.setContent(hardpoint, []); + var viewport = JX.Vector.getDim(hardpoint); var config = this._data; @@ -48,22 +52,14 @@ JX.install('Chart', { size.width = size.frameWidth - padding.left - padding.right; size.height = size.frameHeight - padding.top - padding.bottom; - var x = d3.time.scale() + var x = d3.scaleTime() .range([0, size.width]); - var y = d3.scale.linear() + var y = d3.scaleLinear() .range([size.height, 0]); - var xAxis = d3.svg.axis() - .scale(x) - .orient('bottom'); - - var yAxis = d3.svg.axis() - .scale(y) - .orient('left'); - - // Remove the old chart (if one exists) before drawing the new chart. - JX.DOM.setContent(hardpoint, []); + var xAxis = d3.axisBottom(x); + var yAxis = d3.axisLeft(y); var svg = d3.select('#' + hardpoint.id).append('svg') .attr('width', size.frameWidth) @@ -80,11 +76,7 @@ JX.install('Chart', { .attr('width', size.width) .attr('height', size.height); - function as_date(value) { - return new Date(value * 1000); - } - - x.domain([as_date(config.xMin), as_date(config.xMax)]); + x.domain([this._newDate(config.xMin), this._newDate(config.xMax)]); y.domain([config.yMin, config.yMax]); var div = d3.select('body') @@ -95,50 +87,11 @@ JX.install('Chart', { for (var idx = 0; idx < config.datasets.length; idx++) { var dataset = config.datasets[idx]; - var line = d3.svg.line() - .x(function(d) { return x(d.xvalue); }) - .y(function(d) { return y(d.yvalue); }); - - var data = []; - for (var ii = 0; ii < dataset.x.length; ii++) { - data.push( - { - xvalue: as_date(dataset.x[ii]), - yvalue: dataset.y[ii] - }); + switch (dataset.type) { + case 'stacked-area': + this._newStackedArea(g, dataset, x, y, div); + break; } - - g.append('path') - .datum(data) - .attr('class', 'line') - .style('stroke', dataset.color) - .attr('d', line); - - g.selectAll('dot') - .data(data) - .enter() - .append('circle') - .attr('class', 'point') - .attr('r', 3) - .attr('cx', function(d) { return x(d.xvalue); }) - .attr('cy', function(d) { return y(d.yvalue); }) - .on('mouseover', function(d) { - var d_y = d.xvalue.getFullYear(); - - // NOTE: Javascript months are zero-based. See PHI1017. - var d_m = d.xvalue.getMonth() + 1; - - var d_d = d.xvalue.getDate(); - - div - .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.yvalue) - .style('opacity', 0.9) - .style('left', (d3.event.pageX - 60) + 'px') - .style('top', (d3.event.pageY - 38) + 'px'); - }) - .on('mouseout', function() { - div.style('opacity', 0); - }); } g.append('g') @@ -150,7 +103,65 @@ JX.install('Chart', { .attr('class', 'y axis') .attr('transform', css_function('translate', 0, 0)) .call(yAxis); + }, + + _newStackedArea: function(g, dataset, x, y, div) { + 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); }) + .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++) { + g.append('path') + .style('fill', dataset.color[ii % dataset.color.length]) + .style('opacity', '0.15') + .attr('d', area(dataset.data[ii])); + + g.append('path') + .attr('class', 'line') + .attr('d', line(dataset.data[ii])); + + g.selectAll('dot') + .data(dataset.events[ii]) + .enter() + .append('circle') + .attr('class', 'point') + .attr('r', 3) + .attr('cx', function(d) { return x(to_date(d.x)); }) + .attr('cy', function(d) { return y(d.y1); }) + .on('mouseover', function(d) { + var dd = to_date(d.x); + + var d_y = dd.getFullYear(); + + // NOTE: Javascript months are zero-based. See PHI1017. + var d_m = dd.getMonth() + 1; + + var d_d = dd.getDate(); + + div + .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.y1) + .style('opacity', 0.9) + .style('left', (d3.event.pageX - 60) + 'px') + .style('top', (d3.event.pageY - 38) + 'px'); + }) + .on('mouseout', function() { + div.style('opacity', 0); + }); + + } + }, + + _newDate: function(epoch) { + return new Date(epoch * 1000); } + } }); From c6052b41a604c07b437f8036ee194faa566dc953 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 06:50:23 -0700 Subject: [PATCH 26/42] Label important data on charts Summary: Ref T13279. Adds client-side support for rendering function labels on charts, then labels every function as important data. Works okay on mobile, although I'm not planning to target mobile terribly heavily for v0. Test Plan: {F6438860} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20500 --- resources/celerity/map.php | 19 +++-- .../PhabricatorChartRenderingEngine.php | 6 +- webroot/rsrc/css/phui/phui-chart.css | 41 +++++++++ webroot/rsrc/js/application/fact/Chart.js | 79 +++++++++++++---- .../js/application/fact/ChartCurtainView.js | 85 +++++++++++++++++++ 5 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 webroot/rsrc/js/application/fact/ChartCurtainView.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index a2bc989ea6..c344701d4a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -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' => '7853a69b', + 'rsrc/css/phui/phui-chart.css' => '10135a9d', 'rsrc/css/phui/phui-cms.css' => '8c05c41e', 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', @@ -389,7 +389,8 @@ 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' => 'a3516cea', + 'rsrc/js/application/fact/Chart.js' => 'b88a227d', + 'rsrc/js/application/fact/ChartCurtainView.js' => 'd10a3c25', 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', @@ -696,7 +697,8 @@ return array( 'javelin-behavior-user-menu' => '60cd9241', 'javelin-behavior-view-placeholder' => 'a9942052', 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => 'a3516cea', + 'javelin-chart' => 'b88a227d', + 'javelin-chart-curtain-view' => 'd10a3c25', 'javelin-color' => '78f811c9', 'javelin-cookie' => '05d290ef', 'javelin-diffusion-locate-file-source' => '94243d89', @@ -823,7 +825,7 @@ return array( 'phui-calendar-day-css' => '9597d706', 'phui-calendar-list-css' => 'ccd7e4e2', 'phui-calendar-month-css' => 'cb758c42', - 'phui-chart-css' => '7853a69b', + 'phui-chart-css' => '10135a9d', 'phui-cms-css' => '8c05c41e', 'phui-comment-form-css' => '68a2d99a', 'phui-comment-panel-css' => 'ec4e31c0', @@ -1767,10 +1769,6 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', ), - 'a3516cea' => array( - 'phui-chart-css', - 'd3', - ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1935,6 +1933,11 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), + 'b88a227d' => array( + 'phui-chart-css', + 'd3', + 'javelin-chart-curtain-view', + ), 'b9109f8f' => array( 'javelin-behavior', 'javelin-uri', diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index 1b77d2403f..b89e8da861 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -94,10 +94,8 @@ final class PhabricatorChartRenderingEngine 'div', array( 'id' => $chart_node_id, - 'style' => 'background: #ffffff; '. - 'height: 480px; ', - ), - ''); + 'class' => 'chart-hardpoint', + )); $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key); diff --git a/webroot/rsrc/css/phui/phui-chart.css b/webroot/rsrc/css/phui/phui-chart.css index be401b1fe3..350d86014a 100644 --- a/webroot/rsrc/css/phui/phui-chart.css +++ b/webroot/rsrc/css/phui/phui-chart.css @@ -10,6 +10,7 @@ } .chart .axis text { + font: {$basefont}; fill: {$darkgreytext}; } @@ -52,3 +53,43 @@ border-radius: 8px; pointer-events: none; } + +.chart-hardpoint { + min-height: 480px; + overflow: hidden; + position: relative; +} + +.device-desktop .chart-container { + position: absolute; + bottom: 0; + top: 0; + left: 0; + right: 300px; +} + +.device .chart-container { + min-height: 480px; +} + +.device-desktop .chart-curtain { + width: 300px; + position: absolute; + bottom: 0; + top: 0; + right: 0; +} + +.chart-function-label-list { + background: {$lightbluebackground}; + border: 1px solid {$lightblueborder}; + padding: 8px 12px; +} + +.device-desktop .chart-function-label-list { + margin-top: 23px; +} + +.chart-function-label-list-item .phui-icon-view { + margin-right: 8px; +} diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index 66a9b98d70..25703e7521 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -2,6 +2,7 @@ * @provides javelin-chart * @requires phui-chart-css * d3 + * javelin-chart-curtain-view */ JX.install('Chart', { @@ -14,6 +15,8 @@ JX.install('Chart', { members: { _rootNode: null, _data: null, + _chartContainerNode: null, + _curtain: null, setData: function(blob) { this._data = blob; @@ -26,23 +29,42 @@ JX.install('Chart', { } var hardpoint = this._rootNode; + var curtain = this._getCurtain(); + var container_node = this._getChartContainerNode(); + + var content = [ + container_node, + curtain.getNode(), + ]; + + JX.DOM.setContent(hardpoint, content); // Remove the old chart (if one exists) before drawing the new chart. - JX.DOM.setContent(hardpoint, []); + JX.DOM.setContent(container_node, []); - var viewport = JX.Vector.getDim(hardpoint); + var viewport = JX.Vector.getDim(container_node); var config = this._data; function css_function(n) { return n + '(' + JX.$A(arguments).slice(1).join(', ') + ')'; } - var padding = { - top: 24, - left: 48, - bottom: 48, - right: 32 - }; + var padding = {}; + if (JX.Device.isDesktop()) { + padding = { + top: 24, + left: 48, + bottom: 48, + right: 12 + }; + } else { + padding = { + top: 12, + left: 36, + bottom: 24, + right: 4 + }; + } var size = { frameWidth: viewport.x, @@ -61,20 +83,20 @@ JX.install('Chart', { var xAxis = d3.axisBottom(x); var yAxis = d3.axisLeft(y); - var svg = d3.select('#' + hardpoint.id).append('svg') + var svg = d3.select(container_node).append('svg') .attr('width', size.frameWidth) .attr('height', size.frameHeight) .attr('class', 'chart'); var g = svg.append('g') - .attr( - 'transform', - css_function('translate', padding.left, padding.top)); + .attr( + 'transform', + css_function('translate', padding.left, padding.top)); g.append('rect') - .attr('class', 'inner') - .attr('width', size.width) - .attr('height', size.height); + .attr('class', 'inner') + .attr('width', size.width) + .attr('height', size.height); x.domain([this._newDate(config.xMin), this._newDate(config.xMax)]); y.domain([config.yMin, config.yMax]); @@ -84,16 +106,20 @@ JX.install('Chart', { .attr('class', 'chart-tooltip') .style('opacity', 0); + curtain.reset(); + for (var idx = 0; idx < config.datasets.length; idx++) { var dataset = config.datasets[idx]; switch (dataset.type) { case 'stacked-area': - this._newStackedArea(g, dataset, x, y, div); + this._newStackedArea(g, dataset, x, y, div, curtain); break; } } + curtain.redraw(); + g.append('g') .attr('class', 'x axis') .attr('transform', css_function('translate', 0, size.height)) @@ -105,7 +131,7 @@ JX.install('Chart', { .call(yAxis); }, - _newStackedArea: function(g, dataset, x, y, div) { + _newStackedArea: function(g, dataset, x, y, div, curtain) { var to_date = JX.bind(this, this._newDate); var area = d3.area() @@ -155,11 +181,30 @@ JX.install('Chart', { div.style('opacity', 0); }); + curtain.addFunctionLabel('Important Data'); } }, _newDate: function(epoch) { return new Date(epoch * 1000); + }, + + _getCurtain: function() { + if (!this._curtain) { + this._curtain = new JX.ChartCurtainView(); + } + return this._curtain; + }, + + _getChartContainerNode: function() { + if (!this._chartContainerNode) { + var attrs = { + className: 'chart-container' + }; + + this._chartContainerNode = JX.$N('div', attrs); + } + return this._chartContainerNode; } } diff --git a/webroot/rsrc/js/application/fact/ChartCurtainView.js b/webroot/rsrc/js/application/fact/ChartCurtainView.js new file mode 100644 index 0000000000..07e5af930e --- /dev/null +++ b/webroot/rsrc/js/application/fact/ChartCurtainView.js @@ -0,0 +1,85 @@ +/** + * @provides javelin-chart-curtain-view + */ +JX.install('ChartCurtainView', { + + construct: function() { + this._labels = []; + }, + + members: { + _node: null, + _labels: null, + _labelsNode: null, + + getNode: function() { + if (!this._node) { + var attr = { + className: 'chart-curtain' + }; + + this._node = JX.$N('div', attr); + } + return this._node; + }, + + reset: function() { + this._labels = []; + }, + + addFunctionLabel: function(label) { + this._labels.push(label); + return this; + }, + + redraw: function() { + var content = [this._getFunctionLabelsNode()]; + + JX.DOM.setContent(this.getNode(), content); + return this; + }, + + _getFunctionLabelsNode: function() { + if (!this._labels.length) { + return null; + } + + if (!this._labelsNode) { + var list_attrs = { + className: 'chart-function-label-list' + }; + + var labels = JX.$N('ul', list_attrs); + + var items = []; + for (var ii = 0; ii < this._labels.length; ii++) { + items.push(this._newFunctionLabelItem(this._labels[ii])); + } + + JX.DOM.setContent(labels, items); + + this._labelsNode = labels; + } + + return this._labelsNode; + }, + + _newFunctionLabelItem: function(item) { + var item_attrs = { + className: 'chart-function-label-list-item' + }; + + var icon = new JX.PHUIXIconView() + .setIcon('fa-circle'); + + var content = [ + icon.getNode(), + item + ]; + + return JX.$N('li', item_attrs, content); + } + + } + +}); From a80426b339c4f9b42eb7ecf7b7bae6c3eef3d880 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 07:06:14 -0700 Subject: [PATCH 27/42] Provide chart function labels over the wire instead of making them up Summary: Ref T13279. Makes charts incrementally more useful by allowing the server to provide labels and colors for functions. Test Plan: {F6438872} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20501 --- resources/celerity/map.php | 21 ++++--- src/__phutil_library_map__.php | 2 + .../fact/chart/PhabricatorChartFunction.php | 17 ++++++ .../chart/PhabricatorChartFunctionLabel.php | 56 +++++++++++++++++++ .../PhabricatorChartStackedAreaDataset.php | 15 +++-- webroot/rsrc/js/application/fact/Chart.js | 13 ++++- .../js/application/fact/ChartCurtainView.js | 10 +++- .../js/application/fact/ChartFunctionLabel.js | 35 ++++++++++++ 8 files changed, 149 insertions(+), 20 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorChartFunctionLabel.php create mode 100644 webroot/rsrc/js/application/fact/ChartFunctionLabel.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c344701d4a..d682369496 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -389,8 +389,9 @@ 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' => 'b88a227d', - 'rsrc/js/application/fact/ChartCurtainView.js' => 'd10a3c25', + 'rsrc/js/application/fact/Chart.js' => 'eec96de0', + 'rsrc/js/application/fact/ChartCurtainView.js' => '86954222', + 'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab', 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', @@ -697,8 +698,9 @@ return array( 'javelin-behavior-user-menu' => '60cd9241', 'javelin-behavior-view-placeholder' => 'a9942052', 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => 'b88a227d', - 'javelin-chart-curtain-view' => 'd10a3c25', + 'javelin-chart' => 'eec96de0', + 'javelin-chart-curtain-view' => '86954222', + 'javelin-chart-function-label' => '81de1dab', 'javelin-color' => '78f811c9', 'javelin-cookie' => '05d290ef', 'javelin-diffusion-locate-file-source' => '94243d89', @@ -1933,11 +1935,6 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), - 'b88a227d' => array( - 'phui-chart-css', - 'd3', - 'javelin-chart-curtain-view', - ), 'b9109f8f' => array( 'javelin-behavior', 'javelin-uri', @@ -2128,6 +2125,12 @@ 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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 21cd7c90d9..097bb1bb76 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2668,6 +2668,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', + 'PhabricatorChartFunctionLabel' => 'applications/fact/chart/PhabricatorChartFunctionLabel.php', 'PhabricatorChartRenderingEngine' => 'applications/fact/engine/PhabricatorChartRenderingEngine.php', 'PhabricatorChartStackedAreaDataset' => 'applications/fact/chart/PhabricatorChartStackedAreaDataset.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', @@ -8683,6 +8684,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunction' => 'Phobject', 'PhabricatorChartFunctionArgument' => 'Phobject', 'PhabricatorChartFunctionArgumentParser' => 'Phobject', + 'PhabricatorChartFunctionLabel' => 'Phobject', 'PhabricatorChartRenderingEngine' => 'Phobject', 'PhabricatorChartStackedAreaDataset' => 'PhabricatorChartDataset', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php index 5b610ae0fa..ac7ab64650 100644 --- a/src/applications/fact/chart/PhabricatorChartFunction.php +++ b/src/applications/fact/chart/PhabricatorChartFunction.php @@ -4,6 +4,7 @@ abstract class PhabricatorChartFunction extends Phobject { private $argumentParser; + private $functionLabel; final public function getFunctionKey() { return $this->getPhobjectClassConstant('FUNCTIONKEY', 32); @@ -43,6 +44,22 @@ abstract class PhabricatorChartFunction return $this; } + public function setFunctionLabel(PhabricatorChartFunctionLabel $label) { + $this->functionLabel = $label; + return $this; + } + + public function getFunctionLabel() { + if (!$this->functionLabel) { + $this->functionLabel = id(new PhabricatorChartFunctionLabel()) + ->setName(pht('Unlabeled Function')) + ->setColor('rgba(255, 0, 0, 1)') + ->setFillColor('rgba(255, 0, 0, 0.15)'); + } + + return $this->functionLabel; + } + final public static function newFromDictionary(array $map) { PhutilTypeSpec::checkMap( $map, diff --git a/src/applications/fact/chart/PhabricatorChartFunctionLabel.php b/src/applications/fact/chart/PhabricatorChartFunctionLabel.php new file mode 100644 index 0000000000..ad85c49b71 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorChartFunctionLabel.php @@ -0,0 +1,56 @@ +name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + + public function setIcon($icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setFillColor($fill_color) { + $this->fillColor = $fill_color; + return $this; + } + + public function getFillColor() { + return $this->fillColor; + } + + public function toWireFormat() { + return array( + 'name' => $this->getName(), + 'color' => $this->getColor(), + 'icon' => $this->getIcon(), + 'fillColor' => $this->getFillColor(), + ); + } + +} diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php index 1683e6e268..f38ec045b1 100644 --- a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -131,15 +131,20 @@ final class PhabricatorChartStackedAreaDataset $events[] = $event_list; } + $wire_labels = array(); + foreach ($functions as $function_key => $function) { + $label = $function->getFunctionLabel(); + + $label->setName(pht('Important Data %s', $function_key)); + + $wire_labels[] = $label->toWireFormat(); + } + $result = array( 'type' => $this->getDatasetTypeKey(), 'data' => $series, 'events' => $events, - 'color' => array( - 'blue', - 'cyan', - 'green', - ), + 'labels' => $wire_labels, ); return $result; diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index 25703e7521..9ce50822ee 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -3,6 +3,7 @@ * @requires phui-chart-css * d3 * javelin-chart-curtain-view + * javelin-chart-function-label */ JX.install('Chart', { @@ -144,13 +145,19 @@ JX.install('Chart', { .y(function(d) { return y(d.y1); }); for (var ii = 0; ii < dataset.data.length; ii++) { + var label = new JX.ChartFunctionLabel(dataset.labels[ii]); + + var fill_color = label.getFillColor() || label.getColor(); + g.append('path') - .style('fill', dataset.color[ii % dataset.color.length]) - .style('opacity', '0.15') + .style('fill', fill_color) .attr('d', area(dataset.data[ii])); + var stroke_color = label.getColor(); + g.append('path') .attr('class', 'line') + .style('stroke', stroke_color) .attr('d', line(dataset.data[ii])); g.selectAll('dot') @@ -181,7 +188,7 @@ JX.install('Chart', { div.style('opacity', 0); }); - curtain.addFunctionLabel('Important Data'); + curtain.addFunctionLabel(label); } }, diff --git a/webroot/rsrc/js/application/fact/ChartCurtainView.js b/webroot/rsrc/js/application/fact/ChartCurtainView.js index 07e5af930e..95eb825d49 100644 --- a/webroot/rsrc/js/application/fact/ChartCurtainView.js +++ b/webroot/rsrc/js/application/fact/ChartCurtainView.js @@ -64,17 +64,21 @@ JX.install('ChartCurtainView', { return this._labelsNode; }, - _newFunctionLabelItem: function(item) { + _newFunctionLabelItem: function(label) { var item_attrs = { className: 'chart-function-label-list-item' }; var icon = new JX.PHUIXIconView() - .setIcon('fa-circle'); + .setIcon(label.getIcon()); + + // Charts may use custom colors, so we can't rely on the CSS classes + // which only provide standard colors like "red" and "blue". + icon.getNode().style.color = label.getColor(); var content = [ icon.getNode(), - item + label.getName() ]; return JX.$N('li', item_attrs, content); diff --git a/webroot/rsrc/js/application/fact/ChartFunctionLabel.js b/webroot/rsrc/js/application/fact/ChartFunctionLabel.js new file mode 100644 index 0000000000..17a943b240 --- /dev/null +++ b/webroot/rsrc/js/application/fact/ChartFunctionLabel.js @@ -0,0 +1,35 @@ +/** + * @provides javelin-chart-function-label + */ +JX.install('ChartFunctionLabel', { + + construct: function(spec) { + this._name = spec.name; + this._color = spec.color; + this._icon = spec.icon; + this._fillColor = spec.fillColor; + }, + + members: { + _name: null, + _color: null, + _icon: null, + _fillColor: null, + + getColor: function() { + return this._color; + }, + + getName: function() { + return this._name; + }, + + getIcon: function() { + return this._icon || 'fa-circle'; + }, + + getFillColor: function() { + return this._fillColor; + } + } +}); From e90360c289670a0ca6d45d0bdb5225e3d4dcfcfd Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 08:40:30 -0700 Subject: [PATCH 28/42] Wrap "" chart domain pairs in an "Interval" class Summary: Ref T13279. Slightly simplify domain handling by putting all the "[x, y]" stuff in an Interval class. I'm planning to do something similar for ranges next, so this should make that easierr. Test Plan: Viewed chart, saw same chart as before. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20502 --- src/__phutil_library_map__.php | 2 + .../fact/chart/PhabricatorChartInterval.php | 62 +++++++++++++++++++ .../chart/PhabricatorFactChartFunction.php | 8 +-- .../PhabricatorHigherOrderChartFunction.php | 31 +--------- .../PhabricatorChartRenderingEngine.php | 51 +++++---------- 5 files changed, 86 insertions(+), 68 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorChartInterval.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 097bb1bb76..146303c1a5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2669,6 +2669,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', 'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php', 'PhabricatorChartFunctionLabel' => 'applications/fact/chart/PhabricatorChartFunctionLabel.php', + 'PhabricatorChartInterval' => 'applications/fact/chart/PhabricatorChartInterval.php', 'PhabricatorChartRenderingEngine' => 'applications/fact/engine/PhabricatorChartRenderingEngine.php', 'PhabricatorChartStackedAreaDataset' => 'applications/fact/chart/PhabricatorChartStackedAreaDataset.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', @@ -8685,6 +8686,7 @@ phutil_register_library_map(array( 'PhabricatorChartFunctionArgument' => 'Phobject', 'PhabricatorChartFunctionArgumentParser' => 'Phobject', 'PhabricatorChartFunctionLabel' => 'Phobject', + 'PhabricatorChartInterval' => 'Phobject', 'PhabricatorChartRenderingEngine' => 'Phobject', 'PhabricatorChartStackedAreaDataset' => 'PhabricatorChartDataset', 'PhabricatorChatLogApplication' => 'PhabricatorApplication', diff --git a/src/applications/fact/chart/PhabricatorChartInterval.php b/src/applications/fact/chart/PhabricatorChartInterval.php new file mode 100644 index 0000000000..9432d5be76 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorChartInterval.php @@ -0,0 +1,62 @@ +min = $min; + $this->max = $max; + } + + public static function newFromIntervalList(array $intervals) { + $min = null; + $max = null; + foreach ($intervals as $interval) { + if ($interval === null) { + continue; + } + + $interval_min = $interval->getMin(); + if ($interval_min !== null) { + if ($min === null) { + $min = $interval_min; + } else { + $min = min($min, $interval_min); + } + } + + $interval_max = $interval->getMax(); + if ($interval_max !== null) { + if ($max === null) { + $max = $interval_max; + } else { + $max = max($max, $interval_max); + } + } + } + + return new self($min, $max); + } + + public function setMin($min) { + $this->min = $min; + return $this; + } + + public function getMin() { + return $this->min; + } + + public function setMax($max) { + $this->max = $max; + return $this; + } + + public function getMax() { + return $this->max; + } + +} diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php index ae2ba52472..dbb886dd3e 100644 --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -73,10 +73,10 @@ final class PhabricatorFactChartFunction } public function getDomain() { - return array( - head_key($this->map), - last_key($this->map), - ); + $min = head_key($this->map); + $max = last_key($this->map); + + return new PhabricatorChartInterval($min, $max); } public function newInputValues(PhabricatorChartDataQuery $query) { diff --git a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php index aef8f948be..ab160bd10f 100644 --- a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php +++ b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php @@ -4,37 +4,12 @@ abstract class PhabricatorHigherOrderChartFunction extends PhabricatorChartFunction { public function getDomain() { - $minv = array(); - $maxv = array(); + $domains = array(); foreach ($this->getFunctionArguments() as $function) { - $domain = $function->getDomain(); - if ($domain !== null) { - list($min, $max) = $domain; - if ($min !== null) { - $minv[] = $min; - } - if ($max !== null) { - $maxv[] = $max; - } - } + $domains[] = $function->getDomain(); } - if (!$minv && !$maxv) { - return null; - } - - $min = null; - $max = null; - - if ($minv) { - $min = min($minv); - } - - if ($maxv) { - $max = max($maxv); - } - - return array($min, $max); + return PhabricatorChartInterval::newFromIntervalList($domains); } public function newInputValues(PhabricatorChartDataQuery $query) { diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index b89e8da861..c7bf263080 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -133,15 +133,15 @@ final class PhabricatorChartRenderingEngine $subfunction->loadData(); } - list($domain_min, $domain_max) = $this->getDomain($functions); + $domain = $this->getDomain($functions); $axis = id(new PhabricatorChartAxis()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max); + ->setMinimumValue($domain->getMin()) + ->setMaximumValue($domain->getMax()); $data_query = id(new PhabricatorChartDataQuery()) - ->setMinimumValue($domain_min) - ->setMaximumValue($domain_max) + ->setMinimumValue($domain->getMin()) + ->setMaximumValue($domain->getMax()) ->setLimit(2000); $wire_datasets = array(); @@ -155,8 +155,8 @@ final class PhabricatorChartRenderingEngine $chart_data = array( 'datasets' => $wire_datasets, - 'xMin' => $domain_min, - 'xMax' => $domain_max, + 'xMin' => $domain->getMin(), + 'xMax' => $domain->getMax(), 'yMin' => $y_min, 'yMax' => $y_max, ); @@ -165,46 +165,25 @@ final class PhabricatorChartRenderingEngine } private function getDomain(array $functions) { - $domain_min_list = null; - $domain_max_list = null; - + $domains = array(); foreach ($functions as $function) { - $domain = $function->getDomain(); - - list($function_min, $function_max) = $domain; - - if ($function_min !== null) { - $domain_min_list[] = $function_min; - } - - if ($function_max !== null) { - $domain_max_list[] = $function_max; - } + $domains[] = $function->getDomain(); } - $domain_min = null; - $domain_max = null; - - if ($domain_min_list) { - $domain_min = min($domain_min_list); - } - - if ($domain_max_list) { - $domain_max = max($domain_max_list); - } + $domain = PhabricatorChartInterval::newFromIntervalList($domains); // If we don't have any domain data from the actual functions, pick a // plausible domain automatically. - if ($domain_max === null) { - $domain_max = PhabricatorTime::getNow(); + if ($domain->getMax() === null) { + $domain->setMax(PhabricatorTime::getNow()); } - if ($domain_min === null) { - $domain_min = $domain_max - phutil_units('365 days in seconds'); + if ($domain->getMin() === null) { + $domain->setMin($domain->getMax() - phutil_units('365 days in seconds')); } - return array($domain_min, $domain_max); + return $domain; } } From 493a6b72c1c02c1b55711a3a8139cfc452f513b5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 08:53:35 -0700 Subject: [PATCH 29/42] Automatically select the range for charts in a general way Summary: Ref T13279. Replace the hard-coded default range with a range computed by examining the chart data. Instead of having a "Dataset" return a blob of wire data, "Dataset" now returns a structure with raw wire data plus a range. I expect to add more structured data here in future changes (tooltip/hover event data, maybe function labels). Test Plan: {F6439101} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20503 --- src/__phutil_library_map__.php | 2 ++ .../fact/chart/PhabricatorChartDataset.php | 7 ++-- .../chart/PhabricatorChartDisplayData.php | 27 ++++++++++++++++ .../PhabricatorChartStackedAreaDataset.php | 20 ++++++++++-- .../PhabricatorChartRenderingEngine.php | 32 +++++++++++++++---- 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorChartDisplayData.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 146303c1a5..3cdbaff8ec 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2664,6 +2664,7 @@ phutil_register_library_map(array( 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', 'PhabricatorChartDataset' => 'applications/fact/chart/PhabricatorChartDataset.php', + 'PhabricatorChartDisplayData' => 'applications/fact/chart/PhabricatorChartDisplayData.php', 'PhabricatorChartEngine' => 'applications/fact/engine/PhabricatorChartEngine.php', 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php', @@ -8681,6 +8682,7 @@ phutil_register_library_map(array( 'PhabricatorChartAxis' => 'Phobject', 'PhabricatorChartDataQuery' => 'Phobject', 'PhabricatorChartDataset' => 'Phobject', + 'PhabricatorChartDisplayData' => 'Phobject', 'PhabricatorChartEngine' => 'Phobject', 'PhabricatorChartFunction' => 'Phobject', 'PhabricatorChartFunctionArgument' => 'Phobject', diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php index df3984f9ce..9faf02b740 100644 --- a/src/applications/fact/chart/PhabricatorChartDataset.php +++ b/src/applications/fact/chart/PhabricatorChartDataset.php @@ -66,11 +66,12 @@ abstract class PhabricatorChartDataset ); } - final public function getWireFormat(PhabricatorChartDataQuery $data_query) { - return $this->newWireFormat($data_query); + final public function getChartDisplayData( + PhabricatorChartDataQuery $data_query) { + return $this->newChartDisplayData($data_query); } - abstract protected function newWireFormat( + abstract protected function newChartDisplayData( PhabricatorChartDataQuery $data_query); diff --git a/src/applications/fact/chart/PhabricatorChartDisplayData.php b/src/applications/fact/chart/PhabricatorChartDisplayData.php new file mode 100644 index 0000000000..db6d3ac576 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorChartDisplayData.php @@ -0,0 +1,27 @@ +wireData = $wire_data; + return $this; + } + + public function getWireData() { + return $this->wireData; + } + + public function setRange(PhabricatorChartInterval $range) { + $this->range = $range; + return $this; + } + + public function getRange() { + return $this->range; + } + +} diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php index f38ec045b1..95392a029f 100644 --- a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -5,7 +5,8 @@ final class PhabricatorChartStackedAreaDataset const DATASETKEY = 'stacked-area'; - protected function newWireFormat(PhabricatorChartDataQuery $data_query) { + protected function newChartDisplayData( + PhabricatorChartDataQuery $data_query) { $functions = $this->getFunctions(); $function_points = array(); @@ -93,6 +94,9 @@ final class PhabricatorChartStackedAreaDataset ksort($function_points[$function_idx]); } + $range_min = null; + $range_max = null; + $series = array(); $baseline = array(); foreach ($function_points as $function_idx => $points) { @@ -117,6 +121,16 @@ final class PhabricatorChartStackedAreaDataset 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; @@ -147,7 +161,9 @@ final class PhabricatorChartStackedAreaDataset 'labels' => $wire_labels, ); - return $result; + return id(new PhabricatorChartDisplayData()) + ->setWireData($result) + ->setRange(new PhabricatorChartInterval($range_min, $range_max)); } diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index c7bf263080..f241d45628 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -145,20 +145,22 @@ final class PhabricatorChartRenderingEngine ->setLimit(2000); $wire_datasets = array(); + $ranges = array(); foreach ($datasets as $dataset) { - $wire_datasets[] = $dataset->getWireFormat($data_query); + $display_data = $dataset->getChartDisplayData($data_query); + + $ranges[] = $display_data->getRange(); + $wire_datasets[] = $display_data->getWireData(); } - // TODO: Figure these out from the datasets again. - $y_min = -2; - $y_max = 20; + $range = $this->getRange($ranges); $chart_data = array( 'datasets' => $wire_datasets, 'xMin' => $domain->getMin(), 'xMax' => $domain->getMax(), - 'yMin' => $y_min, - 'yMax' => $y_max, + 'yMin' => $range->getMin(), + 'yMax' => $range->getMax(), ); return $chart_data; @@ -186,4 +188,22 @@ final class PhabricatorChartRenderingEngine return $domain; } + private function getRange(array $ranges) { + $range = PhabricatorChartInterval::newFromIntervalList($ranges); + + // Start the Y axis at 0 unless the chart has negative values. + $min = $range->getMin(); + if ($min === null || $min >= 0) { + $range->setMin(0); + } + + // If there's no maximum value, just pick a plausible default. + $max = $range->getMax(); + if ($max === null) { + $range->setMax($range->getMin() + 100); + } + + return $range; + } + } From f190c42bcd2e95da3655299062cacdddf605e9b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 09:29:28 -0700 Subject: [PATCH 30/42] Store charts earlier and build them out a little later Summary: Ref T13279. Currently, we store a fairly low-level description of functions and datasets in a chart. This will create problems with (for example) translating function labels. If you view a chart someone links you, it should say "El Charto" if you speak Spanish, not "The Chart" if the original viewer speaks English. To support this, store a slightly higher level version of the chart: the chart engine key, plus configuration parameters. This is very similar to how SearchEngine works. For example, the burndown chart now stores a list of project PHIDs, instead of a list of `[accumulate [sum [fact task.open ]]]` functions. (This leaves some serialization code with no callsites, but we may eventually have a "CustomChartEngine" which stores raw functions, so I'm leaving it for now.) As a result, function labels provided by the chart engine are now translatable. (Note that the actual chart is meaningless since the underlying facts can't be stacked like they're being stacked, as some are negative in some areas of their accumulation.) Test Plan: {F6439121} Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20504 --- .../PhabricatorChartStackedAreaDataset.php | 3 - .../fact/engine/PhabricatorChartEngine.php | 65 ++++++++-- .../PhabricatorChartRenderingEngine.php | 4 + .../fact/storage/PhabricatorFactChart.php | 29 +---- .../PhabricatorProjectBurndownChartEngine.php | 114 ++++++++++++------ 5 files changed, 139 insertions(+), 76 deletions(-) diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php index 95392a029f..4a91aad635 100644 --- a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -148,9 +148,6 @@ final class PhabricatorChartStackedAreaDataset $wire_labels = array(); foreach ($functions as $function_key => $function) { $label = $function->getFunctionLabel(); - - $label->setName(pht('Important Data %s', $function_key)); - $wire_labels[] = $label->toWireFormat(); } diff --git a/src/applications/fact/engine/PhabricatorChartEngine.php b/src/applications/fact/engine/PhabricatorChartEngine.php index d0ccca2034..f723633d6a 100644 --- a/src/applications/fact/engine/PhabricatorChartEngine.php +++ b/src/applications/fact/engine/PhabricatorChartEngine.php @@ -4,6 +4,10 @@ abstract class PhabricatorChartEngine extends Phobject { private $viewer; + private $engineParameters = array(); + + const KEY_ENGINE = 'engineKey'; + const KEY_PARAMETERS = 'engineParameters'; final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -14,26 +18,65 @@ abstract class PhabricatorChartEngine return $this->viewer; } + final protected function setEngineParameter($key, $value) { + $this->engineParameters[$key] = $value; + return $this; + } + + final protected function getEngineParameter($key, $default = null) { + return idx($this->engineParameters, $key, $default); + } + + final protected function getEngineParameters() { + return $this->engineParameters; + } + + final public static function newFromChart(PhabricatorFactChart $chart) { + $engine_key = $chart->getChartParameter(self::KEY_ENGINE); + + $engine_map = self::getAllChartEngines(); + if (!isset($engine_map[$engine_key])) { + throw new Exception( + pht( + 'Chart uses unknown engine key ("%s") and can not be rendered.', + $engine_key)); + } + + return clone id($engine_map[$engine_key]); + } + + final public static function getAllChartEngines() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getChartEngineKey') + ->execute(); + } + final public function getChartEngineKey() { return $this->getPhobjectClassConstant('CHARTENGINEKEY', 32); } - abstract protected function newChart(); + final public function buildChart(PhabricatorFactChart $chart) { + $map = $chart->getChartParameter(self::KEY_PARAMETERS, array()); + return $this->newChart($chart, $map); + } - final public function buildChart() { + abstract protected function newChart(PhabricatorFactChart $chart, array $map); + + final public function buildChartPanel() { $viewer = $this->getViewer(); - $chart = $this->newChart(); + $parameters = $this->getEngineParameters(); + + $chart = id(new PhabricatorFactChart()) + ->setChartParameter(self::KEY_ENGINE, $this->getChartEngineKey()) + ->setChartParameter(self::KEY_PARAMETERS, $this->getEngineParameters()); $rendering_engine = id(new PhabricatorChartRenderingEngine()) ->setViewer($viewer) ->setChart($chart); - return $rendering_engine->getStoredChart(); - } - - final public function buildChartPanel() { - $chart = $this->buildChart(); + $chart = $rendering_engine->getStoredChart(); $panel_type = id(new PhabricatorDashboardChartPanelType()) ->getPanelTypeKey(); @@ -45,4 +88,10 @@ abstract class PhabricatorChartEngine return $chart_panel; } + final protected function newFunction($name /* , ... */) { + $argv = func_get_args(); + return id(new PhabricatorComposeChartFunction()) + ->setArguments(array($argv)); + } + } diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index f241d45628..b328241ea6 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -113,6 +113,10 @@ final class PhabricatorChartRenderingEngine $chart = $this->getStoredChart(); $chart_key = $chart->getChartKey(); + $chart_engine = PhabricatorChartEngine::newFromChart($chart) + ->setViewer($this->getViewer()); + $chart_engine->buildChart($chart); + $datasets = $chart->getDatasets(); $functions = array(); diff --git a/src/applications/fact/storage/PhabricatorFactChart.php b/src/applications/fact/storage/PhabricatorFactChart.php index 515b5f0a72..0fb04ccaa5 100644 --- a/src/applications/fact/storage/PhabricatorFactChart.php +++ b/src/applications/fact/storage/PhabricatorFactChart.php @@ -7,7 +7,7 @@ final class PhabricatorFactChart protected $chartKey; protected $chartParameters = array(); - private $datasets; + private $datasets = self::ATTACHABLE; protected function getConfiguration() { return array( @@ -54,35 +54,14 @@ final class PhabricatorFactChart return parent::save(); } - public function setDatasets(array $datasets) { + public function attachDatasets(array $datasets) { assert_instances_of($datasets, 'PhabricatorChartDataset'); - - $dataset_list = array(); - foreach ($datasets as $dataset) { - $dataset_list[] = $dataset->toDictionary(); - } - - $this->setChartParameter('datasets', $dataset_list); - $this->datasets = null; - + $this->datasets = $datasets; return $this; } public function getDatasets() { - if ($this->datasets === null) { - $this->datasets = $this->newDatasets(); - } - return $this->datasets; - } - - private function newDatasets() { - $datasets = $this->getChartParameter('datasets', array()); - - foreach ($datasets as $key => $dataset) { - $datasets[$key] = PhabricatorChartDataset::newFromDictionary($dataset); - } - - return $datasets; + return $this->assertAttached($this->datasets); } public function getURI() { diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index fd4a872bbd..35496330e8 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -5,52 +5,89 @@ final class PhabricatorProjectBurndownChartEngine const CHARTENGINEKEY = 'project.burndown'; - private $projects; - public function setProjects(array $projects) { assert_instances_of($projects, 'PhabricatorProject'); - - $this->projects = $projects; - - return $this; + $project_phids = mpull($projects, 'getPHID'); + return $this->setEngineParameter('projectPHIDs', $project_phids); } - public function getProjects() { - return $this->projects; - } + protected function newChart(PhabricatorFactChart $chart, array $map) { + $viewer = $this->getViewer(); - protected function newChart() { - if ($this->projects !== null) { - $project_phids = mpull($this->projects, 'getPHID'); - } else { - $project_phids = null; - } + $map = $map + array( + 'projectPHIDs' => array(), + ); - $argvs = array(); - if ($project_phids) { - foreach ($project_phids as $project_phid) { - $argvs[] = array( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid), - ); - $argvs[] = array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ); - $argvs[] = array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), - ); - } + if ($map['projectPHIDs']) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($map['projectPHIDs']) + ->execute(); + $project_phids = mpull($projects, 'getPHID'); } else { - $argvs[] = array('accumulate', array('fact', 'tasks.open-count.create')); - $argvs[] = array('accumulate', array('fact', 'tasks.open-count.status')); + $project_phids = array(); } $functions = array(); - foreach ($argvs as $argv) { - $functions[] = id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); + if ($project_phids) { + foreach ($project_phids as $project_phid) { + $function = $this->newFunction( + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid)); + + $function->getFunctionLabel() + ->setName(pht('Tasks Created')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $functions[] = $function; + + + $function = $this->newFunction( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid)); + + $function->getFunctionLabel() + ->setName(pht('Tasks Closed / Reopened')) + ->setColor('rgba(200, 0, 200, 1)') + ->setFillColor('rgba(200, 0, 200, 0.15)'); + + $functions[] = $function; + + + $function = $this->newFunction( + 'accumulate', + array('fact', 'tasks.open-count.assign.project', $project_phid)); + + $function->getFunctionLabel() + ->setName(pht('Tasks Rescoped')) + ->setColor('rgba(0, 200, 200, 1)') + ->setFillColor('rgba(0, 200, 200, 0.15)'); + + $functions[] = $function; + } + } else { + $function = $this->newFunction( + '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; } $datasets = array(); @@ -58,10 +95,7 @@ final class PhabricatorProjectBurndownChartEngine $datasets[] = id(new PhabricatorChartStackedAreaDataset()) ->setFunctions($functions); - $chart = id(new PhabricatorFactChart()) - ->setDatasets($datasets); - - return $chart; + $chart->attachDatasets($datasets); } } From f91bef64f163584e0cda7643e078bb1c94b99efa Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 8 May 2019 12:43:55 -0700 Subject: [PATCH 31/42] 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 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorChartStackedAreaDataset.php | 8 ++- .../chart/PhabricatorMaxChartFunction.php | 40 ++++++++++++++ .../chart/PhabricatorMinChartFunction.php | 40 ++++++++++++++ .../fact/daemon/PhabricatorFactDaemon.php | 2 +- .../PhabricatorProjectBurndownChartEngine.php | 55 ++++++++++++++----- 6 files changed, 131 insertions(+), 18 deletions(-) create mode 100644 src/applications/fact/chart/PhabricatorMaxChartFunction.php create mode 100644 src/applications/fact/chart/PhabricatorMinChartFunction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3cdbaff8ec..428d4832b6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3622,6 +3622,7 @@ phutil_register_library_map(array( 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', 'PhabricatorMarkupOneOff' => 'infrastructure/markup/PhabricatorMarkupOneOff.php', 'PhabricatorMarkupPreviewController' => 'infrastructure/markup/PhabricatorMarkupPreviewController.php', + 'PhabricatorMaxChartFunction' => 'applications/fact/chart/PhabricatorMaxChartFunction.php', 'PhabricatorMemeEngine' => 'applications/macro/engine/PhabricatorMemeEngine.php', 'PhabricatorMemeRemarkupRule' => 'applications/macro/markup/PhabricatorMemeRemarkupRule.php', 'PhabricatorMentionRemarkupRule' => 'applications/people/markup/PhabricatorMentionRemarkupRule.php', @@ -3676,6 +3677,7 @@ phutil_register_library_map(array( 'PhabricatorMetronome' => 'infrastructure/util/PhabricatorMetronome.php', 'PhabricatorMetronomeTestCase' => 'infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php', 'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', + 'PhabricatorMinChartFunction' => 'applications/fact/chart/PhabricatorMinChartFunction.php', 'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', 'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', 'PhabricatorMonogramDatasourceEngineExtension' => 'applications/typeahead/engineextension/PhabricatorMonogramDatasourceEngineExtension.php', @@ -9748,6 +9750,7 @@ phutil_register_library_map(array( 'PhabricatorMarkupInterface', ), 'PhabricatorMarkupPreviewController' => 'PhabricatorController', + 'PhabricatorMaxChartFunction' => 'PhabricatorChartFunction', 'PhabricatorMemeEngine' => 'Phobject', 'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', @@ -9814,6 +9817,7 @@ phutil_register_library_map(array( 'PhabricatorMetronome' => 'Phobject', 'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', + 'PhabricatorMinChartFunction' => 'PhabricatorChartFunction', 'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorModularTransactionType' => 'Phobject', 'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php index 4a91aad635..8bf4445984 100644 --- a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -9,8 +9,10 @@ final class PhabricatorChartStackedAreaDataset PhabricatorChartDataQuery $data_query) { $functions = $this->getFunctions(); + $reversed_functions = array_reverse($functions, true); + $function_points = array(); - foreach ($functions as $function_idx => $function) { + foreach ($reversed_functions as $function_idx => $function) { $function_points[$function_idx] = array(); $datapoints = $function->newDatapoints($data_query); @@ -36,7 +38,7 @@ final class PhabricatorChartStackedAreaDataset } ksort($must_define); - foreach ($functions as $function_idx => $function) { + foreach ($reversed_functions as $function_idx => $function) { $missing = array(); foreach ($must_define as $x) { if (!isset($function_points[$function_idx][$x])) { @@ -136,6 +138,8 @@ final class PhabricatorChartStackedAreaDataset $series[] = $bounds; } + $series = array_reverse($series); + $events = array(); foreach ($raw_points as $function_idx => $points) { $event_list = array(); diff --git a/src/applications/fact/chart/PhabricatorMaxChartFunction.php b/src/applications/fact/chart/PhabricatorMaxChartFunction.php new file mode 100644 index 0000000000..c874cef8e8 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorMaxChartFunction.php @@ -0,0 +1,40 @@ +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; + } + +} diff --git a/src/applications/fact/chart/PhabricatorMinChartFunction.php b/src/applications/fact/chart/PhabricatorMinChartFunction.php new file mode 100644 index 0000000000..db1a003811 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorMinChartFunction.php @@ -0,0 +1,40 @@ +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; + } + +} diff --git a/src/applications/fact/daemon/PhabricatorFactDaemon.php b/src/applications/fact/daemon/PhabricatorFactDaemon.php index 57813b3021..29c7904547 100644 --- a/src/applications/fact/daemon/PhabricatorFactDaemon.php +++ b/src/applications/fact/daemon/PhabricatorFactDaemon.php @@ -15,7 +15,7 @@ final class PhabricatorFactDaemon extends PhabricatorDaemon { } $this->log(pht('Zzz...')); - $this->sleep(60 * 5); + $this->sleep(15); } } diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index 35496330e8..092e921c5a 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -32,37 +32,62 @@ final class PhabricatorProjectBurndownChartEngine if ($project_phids) { foreach ($project_phids as $project_phid) { $function = $this->newFunction( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid)); + 'min', + 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)'); + ->setName(pht('Tasks Moved Into Project')) + ->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.project', $project_phid)); + 'min', + array( + 'accumulate', + array('fact', 'tasks.open-count.status.project', $project_phid), + ), + 0); $function->getFunctionLabel() - ->setName(pht('Tasks Closed / Reopened')) + ->setName(pht('Tasks Reopened')) ->setColor('rgba(200, 0, 200, 1)') ->setFillColor('rgba(200, 0, 200, 0.15)'); $functions[] = $function; - $function = $this->newFunction( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid)); + '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 Rescoped')) - ->setColor('rgba(0, 200, 200, 1)') - ->setFillColor('rgba(0, 200, 200, 0.15)'); + ->setName(pht('Tasks Created')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); $functions[] = $function; } From fa4dcaa3aaff508b94973d20534666948231e939 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 21 May 2019 16:57:35 -0700 Subject: [PATCH 32/42] Stabilize sorting of feed stories with similar strength Summary: See PHI1222. When we publish several transactions to feed at once, we sort them by "action strength" to figure out which one gets to be the title story. This sort currently uses `msort()`, which uses `asort()`, which is not a stable sort and has inconsistent behavior across PHP versions: {F6463721} Switch to `msortv()`, which is a stable sort. Previously, see also T6861. If all transactions have the same strength, we'll now consistently pick the first one. This probably (?) does not impact anything in the upstream, but is good from a consistency point of view. Test Plan: Top story was published after this change and uses the chronologically first transaction as the title story. Bottom story was published before this change and uses the chronologically second transaction as the title story. Both stories have two transactions with the same strength ("create" + "add reviewer"). {F6463722} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20540 --- .../storage/PhabricatorAuditTransaction.php | 2 +- .../storage/DifferentialTransaction.php | 2 +- .../DifferentialRevisionActionTransaction.php | 2 +- .../DifferentialRevisionUpdateTransaction.php | 2 +- ...erentialRevisionWrongBuildsTransaction.php | 2 +- ...ferentialRevisionWrongStateTransaction.php | 2 +- .../xaction/ManiphestTaskOwnerTransaction.php | 2 +- .../ManiphestTaskPriorityTransaction.php | 2 +- .../ManiphestTaskStatusTransaction.php | 2 +- .../xaction/ManiphestTaskTitleTransaction.php | 2 +- .../xaction/PholioMockNameTransaction.php | 2 +- .../PhrictionDocumentDeleteTransaction.php | 2 +- .../PhrictionDocumentEditTransaction.php | 2 +- .../PhrictionDocumentMoveToTransaction.php | 2 +- .../PhrictionDocumentTitleTransaction.php | 2 +- ...habricatorApplicationTransactionEditor.php | 5 ++--- .../PhabricatorApplicationTransaction.php | 19 ++++++++++++------- 17 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/applications/audit/storage/PhabricatorAuditTransaction.php b/src/applications/audit/storage/PhabricatorAuditTransaction.php index da312e626a..e6c1062092 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransaction.php +++ b/src/applications/audit/storage/PhabricatorAuditTransaction.php @@ -50,7 +50,7 @@ final class PhabricatorAuditTransaction switch ($type) { case self::TYPE_COMMIT: - return 3.0; + return 300; } return parent::getActionStrength(); diff --git a/src/applications/differential/storage/DifferentialTransaction.php b/src/applications/differential/storage/DifferentialTransaction.php index c49e40f988..c0b6a92914 100644 --- a/src/applications/differential/storage/DifferentialTransaction.php +++ b/src/applications/differential/storage/DifferentialTransaction.php @@ -130,7 +130,7 @@ final class DifferentialTransaction public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_ACTION: - return 3; + return 300; } return parent::getActionStrength(); diff --git a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php index 99dc8bebc6..338fde99b2 100644 --- a/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionActionTransaction.php @@ -40,7 +40,7 @@ abstract class DifferentialRevisionActionTransaction } public function getActionStrength() { - return 3; + return 300; } public function getRevisionActionOrderVector() { diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php index ab544b1c68..38742357de 100644 --- a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php @@ -99,7 +99,7 @@ final class DifferentialRevisionUpdateTransaction } public function getActionStrength() { - return 2; + return 200; } public function getTitle() { diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php index 8d96b600e8..d9ea50f604 100644 --- a/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php @@ -26,7 +26,7 @@ final class DifferentialRevisionWrongBuildsTransaction } public function getActionStrength() { - return 4; + return 400; } public function getTitle() { diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php index 47678e886f..c9948658e1 100644 --- a/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php @@ -22,7 +22,7 @@ final class DifferentialRevisionWrongStateTransaction } public function getActionStrength() { - return 4; + return 400; } public function getTitle() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php index caaf84f542..7f58c5dcab 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskOwnerTransaction.php @@ -31,7 +31,7 @@ final class ManiphestTaskOwnerTransaction } public function getActionStrength() { - return 1.2; + return 120; } public function getActionName() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php index 398808e006..2a53bf7ef6 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskPriorityTransaction.php @@ -27,7 +27,7 @@ final class ManiphestTaskPriorityTransaction } public function getActionStrength() { - return 1.1; + return 110; } public function getActionName() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php index 6f4b558e05..f16315013d 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskStatusTransaction.php @@ -22,7 +22,7 @@ final class ManiphestTaskStatusTransaction } public function getActionStrength() { - return 1.3; + return 130; } public function getActionName() { diff --git a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php index 7dd9217760..c01c263ba6 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php @@ -14,7 +14,7 @@ final class ManiphestTaskTitleTransaction } public function getActionStrength() { - return 1.4; + return 140; } public function getActionName() { diff --git a/src/applications/pholio/xaction/PholioMockNameTransaction.php b/src/applications/pholio/xaction/PholioMockNameTransaction.php index 82fb92fe40..aae0523f32 100644 --- a/src/applications/pholio/xaction/PholioMockNameTransaction.php +++ b/src/applications/pholio/xaction/PholioMockNameTransaction.php @@ -10,7 +10,7 @@ final class PholioMockNameTransaction } public function getActionStrength() { - return 1.4; + return 140; } public function applyInternalEffects($object, $value) { diff --git a/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php index b1d894b96c..66df075806 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentDeleteTransaction.php @@ -19,7 +19,7 @@ final class PhrictionDocumentDeleteTransaction } public function getActionStrength() { - return 1.5; + return 150; } public function getActionName() { diff --git a/src/applications/phriction/xaction/PhrictionDocumentEditTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentEditTransaction.php index 3130658311..d99846fa98 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentEditTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentEditTransaction.php @@ -32,7 +32,7 @@ abstract class PhrictionDocumentEditTransaction } public function getActionStrength() { - return 1.3; + return 130; } public function getActionName() { diff --git a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php index 180c263942..d18c436cd0 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php @@ -37,7 +37,7 @@ final class PhrictionDocumentMoveToTransaction } public function getActionStrength() { - return 1.0; + return 100; } public function getActionName() { diff --git a/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php index 25a77efffe..134a4540ee 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentTitleTransaction.php @@ -21,7 +21,7 @@ final class PhrictionDocumentTitleTransaction } public function getActionStrength() { - return 1.4; + return 140; } public function getActionName() { diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index c9c2c79237..0c1d5f06e2 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3249,7 +3249,7 @@ abstract class PhabricatorApplicationTransactionEditor protected function getStrongestAction( PhabricatorLiskDAO $object, array $xactions) { - return last(msort($xactions, 'getActionStrength')); + return head(msort($xactions, 'newActionStrengthSortVector')); } @@ -3718,8 +3718,7 @@ abstract class PhabricatorApplicationTransactionEditor PhabricatorLiskDAO $object, array $xactions) { - $xactions = msort($xactions, 'getActionStrength'); - $xactions = array_reverse($xactions); + $xactions = msortv($xactions, 'newActionStrengthSortVector'); return array( 'objectPHID' => $object->getPHID(), diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 6fa9446911..9850d66a7f 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1363,35 +1363,35 @@ abstract class PhabricatorApplicationTransaction public function getActionStrength() { if ($this->isInlineCommentTransaction()) { - return 0.25; + return 250; } switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: - return 0.5; + return 500; case PhabricatorTransactions::TYPE_SUBSCRIBERS: if ($this->isSelfSubscription()) { // Make this weaker than TYPE_COMMENT. - return 0.25; + return 250; } if ($this->isApplicationAuthor()) { // When applications (most often: Herald) change subscriptions it // is very uninteresting. - return 0.000000001; + return 1; } // In other cases, subscriptions are more interesting than comments // (which are shown anyway) but less interesting than any other type of // transaction. - return 0.75; + return 750; case PhabricatorTransactions::TYPE_MFA: // We want MFA signatures to render at the top of transaction groups, // on top of the things they signed. - return 10; + return 10000; } - return 1.0; + return 1000; } public function isCommentTransaction() { @@ -1717,6 +1717,11 @@ abstract class PhabricatorApplicationTransaction ->addString($this->getPHID()); } + public function newActionStrengthSortVector() { + return id(new PhutilSortVector()) + ->addInt(-$this->getActionStrength()); + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ From 1eff4fdca332c25c3ff081a815622048bcb635f8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 May 2019 14:04:15 -0700 Subject: [PATCH 33/42] Prevent "Differential Revision: ..." from counting as a mention in commit messages Summary: Ref T13290. Ref T13291. Now that a full URI is a "mention", the full URI in "Differential Revision: ..." also triggers a mention. Stop it from doing that, since these mentions are silly/redundant/unintended. The API here is also slightly odd; simplify it a little bit to get rid of doing "append" with "get + append + set". Test Plan: Used `bin/repository reparse --publish` to republish commits with "Differential Revision: ..." and verified that the revision PHID was properly dropped from the mention list. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13291, T13290 Differential Revision: https://secure.phabricator.com/D20544 --- .../audit/editor/PhabricatorAuditEditor.php | 6 ++---- .../editor/DifferentialTransactionEditor.php | 10 +++------- .../DiffusionUpdateObjectAfterCommitWorker.php | 5 +---- .../PhabricatorRepositoryCommitPublishWorker.php | 15 ++++++++++++++- .../PhabricatorApplicationTransactionEditor.php | 12 ++++++++---- 5 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index d3bd7be7ae..7995e2a36d 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -239,7 +239,7 @@ final class PhabricatorAuditEditor $object); if ($request) { $xactions[] = $request; - $this->setUnmentionablePHIDMap($request->getNewValue()); + $this->addUnmentionablePHIDs($request->getNewValue()); } break; default: @@ -360,7 +360,6 @@ final class PhabricatorAuditEditor $flat_blocks = mpull($changes, 'getNewValue'); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); - $phid_map[] = $this->getUnmentionablePHIDMap(); $monograms = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) @@ -385,7 +384,6 @@ final class PhabricatorAuditEditor ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); - $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) ->parseCorpus($huge_block); $reverts = array_mergev(ipull($reverts_refs, 'monograms')); @@ -408,7 +406,7 @@ final class PhabricatorAuditEditor } $phid_map = array_mergev($phid_map); - $this->setUnmentionablePHIDMap($phid_map); + $this->addUnmentionablePHIDs($phid_map); return $result; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 0150500727..eba641771e 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -846,13 +846,9 @@ final class DifferentialTransactionEditor $revert_phids = array(); } - // See PHI574. Respect any unmentionable PHIDs which were set on the - // Editor by the caller. - $unmentionable_map = $this->getUnmentionablePHIDMap(); - $unmentionable_map += $task_phids; - $unmentionable_map += $rev_phids; - $unmentionable_map += $revert_phids; - $this->setUnmentionablePHIDMap($unmentionable_map); + $this->addUnmentionablePHIDs($task_phids); + $this->addUnmentionablePHIDs($rev_phids); + $this->addUnmentionablePHIDs($revert_phids); $result = array(); foreach ($edges as $type => $specs) { diff --git a/src/applications/diffusion/worker/DiffusionUpdateObjectAfterCommitWorker.php b/src/applications/diffusion/worker/DiffusionUpdateObjectAfterCommitWorker.php index a0a18c0662..b652ae86d6 100644 --- a/src/applications/diffusion/worker/DiffusionUpdateObjectAfterCommitWorker.php +++ b/src/applications/diffusion/worker/DiffusionUpdateObjectAfterCommitWorker.php @@ -139,10 +139,7 @@ final class DiffusionUpdateObjectAfterCommitWorker ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) - ->setUnmentionablePHIDMap( - array( - $commit_phid => $commit_phid, - )); + ->addUnmentionablePHIDs(array($commit_phid)); $editor->applyTransactions($task, $xactions); } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php index 9394ba6b71..17074f8cb4 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitPublishWorker.php @@ -62,12 +62,25 @@ final class PhabricatorRepositoryCommitPublishWorker $acting_phid = $this->getPublishAsPHID($commit); $content_source = $this->newContentSource(); + $revision = DiffusionCommitRevisionQuery::loadRevisionForCommit( + $viewer, + $commit); + + // Prevent the commit from generating a mention of the associated + // revision, if one exists, so we don't double up because of the URI + // in the commit message. + $unmentionable_phids = array(); + if ($revision) { + $unmentionable_phids[] = $revision->getPHID(); + } + $editor = $commit->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($acting_phid) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) - ->setContentSource($content_source); + ->setContentSource($content_source) + ->addUnmentionablePHIDs($unmentionable_phids); try { $raw_patch = $this->loadRawPatchText($repository, $commit); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 0c1d5f06e2..a114eb21b3 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -260,12 +260,14 @@ abstract class PhabricatorApplicationTransactionEditor return $this->isHeraldEditor; } - public function setUnmentionablePHIDMap(array $map) { - $this->unmentionablePHIDMap = $map; + public function addUnmentionablePHIDs(array $phids) { + foreach ($phids as $phid) { + $this->unmentionablePHIDMap[$phid] = true; + } return $this; } - public function getUnmentionablePHIDMap() { + private function getUnmentionablePHIDMap() { return $this->unmentionablePHIDMap; } @@ -2090,12 +2092,14 @@ abstract class PhabricatorApplicationTransactionEditor ->withPHIDs($mentioned_phids) ->execute(); + $unmentionable_map = $this->getUnmentionablePHIDMap(); + $mentionable_phids = array(); if ($this->shouldEnableMentions($object, $xactions)) { foreach ($mentioned_objects as $mentioned_object) { if ($mentioned_object instanceof PhabricatorMentionableInterface) { $mentioned_phid = $mentioned_object->getPHID(); - if (idx($this->getUnmentionablePHIDMap(), $mentioned_phid)) { + if (isset($unmentionable_map[$mentioned_phid])) { continue; } // don't let objects mention themselves From b95bf722d55e66ca0dcbfe8d1bd7c416cf5528e5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 May 2019 14:25:03 -0700 Subject: [PATCH 34/42] Drop the "update revision with commit diff" transaction if the revision is already closed Summary: Ref T13290. Prior to recent changes, if we parsed some commit C which was associated with a revision R, but R was already closed, we'd skip the whole set of updates because the "close the revision" transaction would fail and we'd throw because we did not `setContinueOnNoEffect()`. We now continue on no effect so we can get the edge ("commit has revision" / "revision has commit"), since we want it in all cases, but this means we may also apply an extra "Updated revision to reflect committed changes" transaction and new diff. This can happen even if we're careful about not trying to apply this transaction to closed revisions, since two workers may race. (Today, we aren't too careful about this.) To fix this, just make this transaction no-op itself if the revision is already closed by the time it tries to apply. This happened on D20451 because a merge commit with the same hash as the last diff was pushed, but it's easiest to reproduce by just running `bin/repository reparse --message `, which updates related revisions with a new diff every time. Test Plan: - Ran `bin/repository reparse --messsage ` several times, on a commit with an associated revision. - Before: each run attached a new diff and created a new "updated to reflect committed changes" transaction. - After: repeated runs had no effects. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13290 Differential Revision: https://secure.phabricator.com/D20545 --- .../DifferentialRevisionUpdateTransaction.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php index 38742357de..4ca03e37e2 100644 --- a/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionUpdateTransaction.php @@ -10,6 +10,26 @@ final class DifferentialRevisionUpdateTransaction return $object->getActiveDiffPHID(); } + public function generateNewValue($object, $value) { + // See T13290. If we're updating the revision in response to a commit but + // the revision is already closed, return the old value so we no-op this + // transaction. We don't want to attach more than one commit-diff to a + // revision. + + // Although we can try to bail out earlier so we don't generate this + // transaction in the first place, we may race another worker and end up + // trying to apply it anyway. Here, we have a lock on the object and can + // be certain about the object state. + + if ($this->isCommitUpdate()) { + if ($object->isClosed()) { + return $this->generateOldValue($object); + } + } + + return $value; + } + public function applyInternalEffects($object, $value) { $should_review = $this->shouldRequestReviewAfterUpdate($object); if ($should_review) { From f6af1c43742480c7d17778cce2e71e3ef5f4b2fa Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 May 2019 15:10:19 -0700 Subject: [PATCH 35/42] When creating a Phriction document, mark initial transactions as "create" transactions to fix weird email Summary: Ref T13289. When you create a Phriction document, you currently get an email with the whole new content as a "diff". You also get extra transactions in the email and on the page. This is because Phriction isn't on EditEngine and doesn't mark "create" transactions in a modern way. Get them marked properly to fix these obviously-broken behaviors. This can all go away once Phriction switches to EditEngine, although I don't have any particular plans to do that in the immediate future. Test Plan: - Created a new document, viewed email, no longer saw redundant "edited content" transaction or "CHANGES TO CONTENT" diff. - Updated a document, viewed email, got interdiff. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20548 --- .../controller/PhrictionEditController.php | 18 +++++++++++++++--- .../phriction/storage/PhrictionDocument.php | 5 ++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index 9f59b63121..d86fa1e05b 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -97,6 +97,10 @@ final class PhrictionEditController $content_text = $content->getContent(); $is_draft_mode = ($document->getContent()->getVersion() != $max_version); + $default_view = $document->getViewPolicy(); + $default_edit = $document->getEditPolicy(); + $default_space = $document->getSpacePHID(); + if ($request->isFormPost()) { if ($is_new) { $save_as_draft = false; @@ -122,6 +126,11 @@ final class PhrictionEditController $xactions = array(); + if ($is_new) { + $xactions[] = id(new PhrictionTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); + } + $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); @@ -130,13 +139,16 @@ final class PhrictionEditController ->setNewValue($content_text); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($v_view); + ->setNewValue($v_view) + ->setIsDefaultTransaction($is_new && ($v_view === $default_view)); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) - ->setNewValue($v_edit); + ->setNewValue($v_edit) + ->setIsDefaultTransaction($is_new && ($v_edit === $default_edit)); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SPACE) - ->setNewValue($v_space); + ->setNewValue($v_space) + ->setIsDefaultTransaction($is_new && ($v_space === $default_space)); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => $v_cc)); diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 9f8a4a475b..88c6b142e6 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -78,10 +78,13 @@ final class PhrictionDocument extends PhrictionDAO } if ($parent_doc) { + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $parent_doc); + $document ->setViewPolicy($parent_doc->getViewPolicy()) ->setEditPolicy($parent_doc->getEditPolicy()) - ->setSpacePHID($parent_doc->getSpacePHID()); + ->setSpacePHID($space_phid); } else { $default_view_policy = PhabricatorPolicies::getMostOpenPolicy(); $document From 31e623afcc92726d5c4b261dd7752e13759cdf14 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 May 2019 14:38:20 -0700 Subject: [PATCH 36/42] Use the same transaction group ID for transactions applied indirectly by a sub-editor Summary: Ref T13283. Currently, each Editor sets its own group ID, so if you create a revision and then Herald does some stuff, the two groups of transactions get different group IDs. This means the test console is slightly misleading (it will only pick up the Herald transactions). It's going to be misleading anyway (Herald obviously can't evaluate Herald transactions) but this is at least a little closer to reality and stops Herald actions from masking non-Herald actions. Test Plan: - Created a revision. Herald applied one transaction. - Used the test console. - Before: The test console only picked up the single most recent Herald transaction. - After: The test console picked up the whole transaction group. {F6464059} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13283 Differential Revision: https://secure.phabricator.com/D20546 --- .../PhabricatorApplicationTransactionEditor.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index a114eb21b3..12fbc2a07e 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -54,6 +54,7 @@ abstract class PhabricatorApplicationTransactionEditor private $heraldTranscript; private $subscribers; private $unmentionablePHIDMap = array(); + private $transactionGroupID; private $applicationEmail; private $isPreview; @@ -975,6 +976,14 @@ abstract class PhabricatorApplicationTransactionEditor return $this->cancelURI; } + protected function getTransactionGroupID() { + if ($this->transactionGroupID === null) { + $this->transactionGroupID = Filesystem::readRandomCharacters(32); + } + + return $this->transactionGroupID; + } + final public function applyTransactions( PhabricatorLiskDAO $object, array $xactions) { @@ -1164,7 +1173,7 @@ abstract class PhabricatorApplicationTransactionEditor throw $ex; } - $group_id = Filesystem::readRandomCharacters(32); + $group_id = $this->getTransactionGroupID(); foreach ($xactions as $xaction) { if ($was_locked) { @@ -4666,6 +4675,7 @@ abstract class PhabricatorApplicationTransactionEditor } $editor->mustEncrypt = $this->mustEncrypt; + $editor->transactionGroupID = $this->getTransactionGroupID(); return $editor; } From 2e2dc47f0763e1f6187010a918f8525f5e39ebfe Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 May 2019 14:44:49 -0700 Subject: [PATCH 37/42] In the Herald test console, don't consider transactions that Herald rules applied Summary: Depends on D20546. Ref T13283. Currently, if you do something (transactions "A", "B") and Herald does some things in response (transaction "C"), Herald acts only on the things you did ("A", "B") since the thing it did ("C") didn't exist yet, until it ran. However, if you use the test console to test rules against the object we'll pick up all three transactions since they're all part of the same group. This isn't ideal. To fix this, skip transactions which Herald applied, since it obviously didn't consider them when it was evaluating. Test Plan: - Created a revision, in the presence of a Herald rule that adds reviewers. - Then, ran the revision through the test console. - Before: saw the "Herald added reviewers: ..." transaction in the transaction group Herald evaluated. - After: saw only authentic human transactions. {F6464064} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13283 Differential Revision: https://secure.phabricator.com/D20547 --- .../herald/controller/HeraldTestConsoleController.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/applications/herald/controller/HeraldTestConsoleController.php b/src/applications/herald/controller/HeraldTestConsoleController.php index 5962996bb2..18c22b4c87 100644 --- a/src/applications/herald/controller/HeraldTestConsoleController.php +++ b/src/applications/herald/controller/HeraldTestConsoleController.php @@ -271,6 +271,16 @@ final class HeraldTestConsoleController extends HeraldController { $recent_id = null; $hard_limit = 1000; foreach ($xactions as $xaction) { + + // If this transaction has Herald transcript metadata, it was applied by + // Herald. Exclude it from the list because the Herald rule engine always + // runs before Herald transactions apply, so there's no way that real + // rules would have seen this transaction. + $transcript_id = $xaction->getMetadataValue('herald:transcriptID'); + if ($transcript_id !== null) { + continue; + } + $group_id = $xaction->getTransactionGroupID(); // If this is the first transaction, save the group ID: we want to From f838ad182753f9fe55a6e2f419901e4f2d7140f1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 May 2019 11:52:43 -0700 Subject: [PATCH 38/42] Fix two straggling pagination issues in Drydock Summary: Ref T13289. See . `bin/drydock lease` and the web UI for reviewing all object logs when there is more than one page of logs didn't get fully updated to the new cursors. - Use a cursor pager in `bin/drydock lease`. - Implement `withIDs()` in `LeaseQuery` so the default paging works properly. Test Plan: - Ran `bin/drydock lease`, got a lease with log output along the way. - Set page size to 2, viewed host logs with multiple pages, paged to page 2. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20553 --- .../management/DrydockManagementLeaseWorkflow.php | 6 ++++-- src/applications/drydock/query/DrydockLogQuery.php | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php index 7371232620..08f33c6b5f 100644 --- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php @@ -151,13 +151,15 @@ final class DrydockManagementLeaseWorkflow while (!$is_active) { $lease->reload(); + $pager = id(new AphrontCursorPagerView()) + ->setBeforeID($log_cursor); + // While we're waiting, show the user any logs which the daemons have // generated to give them some clue about what's going on. $logs = id(new DrydockLogQuery()) ->setViewer($viewer) ->withLeasePHIDs(array($lease->getPHID())) - ->setBeforeID($log_cursor) - ->execute(); + ->executeWithCursorPager($pager); if ($logs) { $logs = mpull($logs, null, 'getID'); ksort($logs); diff --git a/src/applications/drydock/query/DrydockLogQuery.php b/src/applications/drydock/query/DrydockLogQuery.php index b73ad371ae..80f47f584f 100644 --- a/src/applications/drydock/query/DrydockLogQuery.php +++ b/src/applications/drydock/query/DrydockLogQuery.php @@ -2,11 +2,17 @@ final class DrydockLogQuery extends DrydockQuery { + private $ids; private $blueprintPHIDs; private $resourcePHIDs; private $leasePHIDs; private $operationPHIDs; + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withBlueprintPHIDs(array $phids) { $this->blueprintPHIDs = $phids; return $this; @@ -126,6 +132,13 @@ final class DrydockLogQuery extends DrydockQuery { protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ls)', + $this->ids); + } + if ($this->blueprintPHIDs !== null) { $where[] = qsprintf( $conn, From aacc62463d619fb4c8bdf8a660b4ede2b0cc8083 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 May 2019 10:55:55 -0700 Subject: [PATCH 39/42] Prevent editing and deleting comments in locked conversations Summary: Ref T13289. This tightens up a couple of corner cases around locked threads. Locking is primarily motivated by two use cases: stopping nonproductive conversations on open source installs (similar to GitHub's feature); and freezing object state for audit/record-keeping purposes. Currently, you can edit or remove comments on a locked thread, but neither use case is well-served by allowing this. Require "CAN_INTERACT" to edit or remove a comment. Administrators can still remove comments from a locked thread to serve "lock a flamewar, then clean it up", since "Remove Comment" on a comment you don't own is fairly unambiguously an administrative action. Test Plan: - On a locked task, tried to edit and remove my comments as a non-administrator. Saw appropriate disabled UI state and error dialogs (actions were disallowed). - On a locked task, tried to remove another user's comments as an administrator. This works. - On a normal task, edited comments normally. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20551 --- ...cationTransactionCommentEditController.php | 19 ++++++++++++++ ...tionTransactionCommentRemoveController.php | 20 +++++++++++++++ ...torApplicationTransactionCommentEditor.php | 4 +++ .../PhabricatorApplicationTransactionView.php | 7 ++++++ src/view/phui/PHUITimelineEventView.php | 25 +++++++++++++++++-- 5 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php index 1682a7d136..4529704c2f 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -31,6 +31,25 @@ final class PhabricatorApplicationTransactionCommentEditController $done_uri = $obj_handle->getURI(); + // If an object is locked, you can't edit comments on it. Two reasons to + // lock threads are to calm contentious issues and to freeze state for + // auditing, and editing comments serves neither goal. + + $object = $xaction->getObject(); + $can_interact = PhabricatorPolicyFilter::hasCapability( + $viewer, + $object, + PhabricatorPolicyCapability::CAN_INTERACT); + if (!$can_interact) { + return $this->newDialog() + ->setTitle(pht('Conversation Locked')) + ->appendParagraph( + pht( + 'You can not edit this comment because the conversation is '. + 'locked.')) + ->addCancelButton($done_uri); + } + if ($request->isFormOrHisecPost()) { $text = $request->getStr('text'); diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php index 381dfe1176..f81535e4ae 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php @@ -32,6 +32,26 @@ final class PhabricatorApplicationTransactionCommentRemoveController $done_uri = $obj_handle->getURI(); + // We allow administrative removal of comments even if an object is locked, + // so you can lock a flamewar and then go clean it up. Locked threads may + // not otherwise be edited, and non-administrators can not remove comments + // from locked threads. + + $object = $xaction->getObject(); + $can_interact = PhabricatorPolicyFilter::hasCapability( + $viewer, + $object, + PhabricatorPolicyCapability::CAN_INTERACT); + if (!$can_interact && !$viewer->getIsAdmin()) { + return $this->newDialog() + ->setTitle(pht('Conversation Locked')) + ->appendParagraph( + pht( + 'You can not remove this comment because the conversation is '. + 'locked.')) + ->addCancelButton($done_uri); + } + if ($request->isFormOrHisecPost()) { $comment = $xaction->getApplicationTransactionCommentObject() ->setContent('') diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php index d963ea2ecb..22acb3312f 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php @@ -189,6 +189,10 @@ final class PhabricatorApplicationTransactionCommentEditor $actor, $xaction, PhabricatorPolicyCapability::CAN_EDIT); + PhabricatorPolicyFilter::requireCapability( + $actor, + $xaction->getObject(), + PhabricatorPolicyCapability::CAN_INTERACT); } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 4d738877b8..7a24bf8ff8 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -505,12 +505,19 @@ class PhabricatorApplicationTransactionView extends AphrontView { if ($has_edit_capability && !$has_removed_comment) { $event->setIsEditable(true); } + if ($has_edit_capability || $viewer->getIsAdmin()) { if (!$has_removed_comment) { $event->setIsRemovable(true); } } } + + $can_interact = PhabricatorPolicyFilter::hasCapability( + $viewer, + $xaction->getObject(), + PhabricatorPolicyCapability::CAN_INTERACT); + $event->setCanInteract($can_interact); } $comment = $this->renderTransactionContent($xaction); diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 5013611084..e4f11b2b1d 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -32,6 +32,7 @@ final class PHUITimelineEventView extends AphrontView { private $isSilent; private $isMFA; private $isLockOverride; + private $canInteract; public function setAuthorPHID($author_phid) { $this->authorPHID = $author_phid; @@ -114,6 +115,15 @@ final class PHUITimelineEventView extends AphrontView { return $this->isEditable; } + public function setCanInteract($can_interact) { + $this->canInteract = $can_interact; + return $this; + } + + public function getCanInteract() { + return $this->canInteract; + } + public function setIsRemovable($is_removable) { $this->isRemovable = $is_removable; return $this; @@ -650,6 +660,10 @@ final class PHUITimelineEventView extends AphrontView { private function getMenuItems($anchor) { $xaction_phid = $this->getTransactionPHID(); + $can_interact = $this->getCanInteract(); + $viewer = $this->getViewer(); + $is_admin = $viewer->getIsAdmin(); + $items = array(); if ($this->getIsEditable()) { @@ -658,6 +672,7 @@ final class PHUITimelineEventView extends AphrontView { ->setHref('/transactions/edit/'.$xaction_phid.'/') ->setName(pht('Edit Comment')) ->addSigil('transaction-edit') + ->setDisabled(!$can_interact) ->setMetadata( array( 'anchor' => $anchor, @@ -727,17 +742,23 @@ final class PHUITimelineEventView extends AphrontView { $items[] = id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER); - $items[] = id(new PhabricatorActionView()) + $remove_item = id(new PhabricatorActionView()) ->setIcon('fa-trash-o') ->setHref('/transactions/remove/'.$xaction_phid.'/') ->setName(pht('Remove Comment')) - ->setColor(PhabricatorActionView::RED) ->addSigil('transaction-remove') ->setMetadata( array( 'anchor' => $anchor, )); + if (!$is_admin && !$can_interact) { + $remove_item->setDisabled(!$is_admin && !$can_interact); + } else { + $remove_item->setColor(PhabricatorActionView::RED); + } + + $items[] = $remove_item; } return $items; From 719dd6d3f44245e935b21291a59338a819938c49 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 May 2019 09:51:23 -0700 Subject: [PATCH 40/42] Remove the "search_documentfield" table Summary: Ref T11741. See PHI1276. After the switch to "Ferret", this table has no remaining readers or writers. Test Plan: - Ran `bin/storage upgrade -f`, no warnings. - Grepped for class name, table name, `stemmedCorpus` column; got no relevant hits. - Did a fulltext search. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T11741 Differential Revision: https://secure.phabricator.com/D20549 --- .../20161130.search.02.rebuild.php | 35 ++--------------- .../20190523.myisam.01.documentfield.sql | 1 + src/__phutil_library_map__.php | 2 - .../PhabricatorSearchDocumentField.php | 39 ------------------- 4 files changed, 5 insertions(+), 72 deletions(-) create mode 100644 resources/sql/autopatches/20190523.myisam.01.documentfield.sql delete mode 100644 src/applications/search/storage/document/PhabricatorSearchDocumentField.php diff --git a/resources/sql/autopatches/20161130.search.02.rebuild.php b/resources/sql/autopatches/20161130.search.02.rebuild.php index d179c44c30..b6c06dabb4 100644 --- a/resources/sql/autopatches/20161130.search.02.rebuild.php +++ b/resources/sql/autopatches/20161130.search.02.rebuild.php @@ -1,34 +1,7 @@ getEngine(); - if ($engine instanceof PhabricatorMySQLFulltextStorageEngine) { - $use_mysql = true; - } -} - -if ($use_mysql) { - $field = new PhabricatorSearchDocumentField(); - $conn = $field->establishConnection('r'); - - // We're only going to require this if the index isn't empty: if you're on a - // fresh install, you don't have to do anything. - $any_documents = queryfx_one( - $conn, - 'SELECT * FROM %T LIMIT 1', - $field->getTableName()); - - if ($any_documents) { - try { - id(new PhabricatorConfigManualActivity()) - ->setActivityType(PhabricatorConfigManualActivity::TYPE_REINDEX) - ->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - // If we've already noted that this activity is required, just move on. - } - } -} +// Later, in T12974, we switched from "InnoDB FULLTEXT" to "Ferret", mostly +// mooting this. The underlying tables and engines were later removed entirely. diff --git a/resources/sql/autopatches/20190523.myisam.01.documentfield.sql b/resources/sql/autopatches/20190523.myisam.01.documentfield.sql new file mode 100644 index 0000000000..c34364b8f2 --- /dev/null +++ b/resources/sql/autopatches/20190523.myisam.01.documentfield.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS {$NAMESPACE}_search.search_documentfield; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 428d4832b6..d10ec7d420 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4530,7 +4530,6 @@ phutil_register_library_map(array( 'PhabricatorSearchDefaultController' => 'applications/search/controller/PhabricatorSearchDefaultController.php', 'PhabricatorSearchDeleteController' => 'applications/search/controller/PhabricatorSearchDeleteController.php', 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', - 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php', 'PhabricatorSearchDocumentFieldType' => 'applications/search/constants/PhabricatorSearchDocumentFieldType.php', 'PhabricatorSearchDocumentQuery' => 'applications/search/query/PhabricatorSearchDocumentQuery.php', 'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php', @@ -10874,7 +10873,6 @@ phutil_register_library_map(array( 'PhabricatorSearchDefaultController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', - 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentFieldType' => 'Phobject', 'PhabricatorSearchDocumentQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', diff --git a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php b/src/applications/search/storage/document/PhabricatorSearchDocumentField.php deleted file mode 100644 index 9c5b839f1a..0000000000 --- a/src/applications/search/storage/document/PhabricatorSearchDocumentField.php +++ /dev/null @@ -1,39 +0,0 @@ - false, - self::CONFIG_IDS => self::IDS_MANUAL, - self::CONFIG_COLUMN_SCHEMA => array( - 'phidType' => 'text4', - 'field' => 'text4', - 'auxPHID' => 'phid?', - 'corpus' => 'fulltext?', - 'stemmedCorpus' => 'fulltext?', - ), - self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - ), - 'key_corpus' => array( - 'columns' => array('corpus', 'stemmedCorpus'), - 'type' => 'FULLTEXT', - ), - ), - ) + parent::getConfiguration(); - } - - public function getIDKey() { - return 'phid'; - } - -} From ce6fc5be9092fd5989653ecaba53014869849b7a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 23 May 2019 11:41:14 -0700 Subject: [PATCH 41/42] Fix a looping workflow when trying to submit a partially-effectless transaction group Summary: Ref T13289. If you do this: - Subscribe to a task (so we don't generate a subscribe side-effect later). - Prepare a transaction group: sign with MFA, change projects (don't make any changes), add a comment. - Submit the transaction group. ...you'll get prompted "Some actions don't have any effect (the non-change to projects), apply remaining effects?". If you confirm, you get MFA'd, but the MFA flow loses the "continue" confirmation, so you get trapped in a workflow loop of confirming and MFA'ing. Instead, retain the "continue" bit through the MFA. Also, don't show "You can't sign an empty transaction group" if there's a comment. See also T13295, since the amount of magic here can probably be reduced. There's likely little reason for "continue" or "hisec" to be magic nowadays. Test Plan: - Went through the workflow above. - Before: looping workflow. - After: "Continue" carries through the MFA gate. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20552 --- src/aphront/AphrontRequest.php | 2 +- .../PhabricatorHighSecurityRequestExceptionHandler.php | 7 +++++++ .../editor/PhabricatorApplicationTransactionEditor.php | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 95534c048d..469e6ba766 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -663,7 +663,7 @@ final class AphrontRequest extends Phobject { } public function isContinueRequest() { - return $this->isFormPost() && $this->getStr('__continue__'); + return $this->isFormOrHisecPost() && $this->getStr('__continue__'); } public function isPreviewRequest() { diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php index 7f4eddad45..5d1d923d0c 100644 --- a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php +++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php @@ -120,6 +120,13 @@ final class PhabricatorHighSecurityRequestExceptionHandler $dialog->addHiddenInput($key, $value); } + // See T13289. If the user hit a "some transactions have no effect" dialog + // and elected to continue, we want to pass that flag through the MFA + // dialog even though it is not normally a passthrough request parameter. + if ($request->isContinueRequest()) { + $dialog->addHiddenInput(AphrontRequest::TYPE_CONTINUE, 1); + } + return $dialog; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 12fbc2a07e..f2ca9883ca 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -2535,7 +2535,7 @@ abstract class PhabricatorApplicationTransactionEditor // If none of the transactions have an effect, the meta-transactions also // have no effect. Add them to the "no effect" list so we get a full set // of errors for everything. - if (!$any_effect) { + if (!$any_effect && !$has_comment) { $no_effect += $meta_xactions; } From 53b9acfb7d58ac5f33424b84df35757a5c68aba1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 May 2019 10:11:09 -0700 Subject: [PATCH 42/42] Test for "CAN_INTERACT" on comment edits in a way that survives objects which only implement "CAN_VIEW" Summary: Ref T13289. See D20551. In D20551, I implemented some "CAN_INTERACT" checks against certain edits, but these checks end up testing "CAN_INTERACT" against objects like Conpherence threads which do not support a distinct "CAN_INTERACT" permission. I misrembered how the "CAN_INTERACT" fallback to "CAN_VIEW" actually works: it's not fully automatic, and needs some explicit "interact, or view if interact is not available" checks. Use the "interact" wrappers to test these policies so they fall back to "CAN_VIEW" if an object does not support "CAN_INTERACT". Generally, objects which have a "locked" state have a separate "CAN_INTERACT" permission; objects which don't have a "locked" state do not. Test Plan: Created and edited comments in Conpherence (or most applications other than Maniphest). Reviewers: amckinley Maniphest Tasks: T13289 Differential Revision: https://secure.phabricator.com/D20558 --- .../policy/filter/PhabricatorPolicyFilter.php | 31 ++++++++++++++----- ...cationTransactionCommentEditController.php | 5 ++- ...torApplicationTransactionCommentEditor.php | 6 ++-- .../PhabricatorApplicationTransactionView.php | 5 ++- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index a5c9f356f4..4ea7ce1549 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -90,6 +90,29 @@ final class PhabricatorPolicyFilter extends Phobject { PhabricatorUser $user, PhabricatorPolicyInterface $object) { + $capabilities = self::getRequiredInteractCapabilities($object); + + foreach ($capabilities as $capability) { + if (!self::hasCapability($user, $object, $capability)) { + return false; + } + } + + return true; + } + + public static function requireCanInteract( + PhabricatorUser $user, + PhabricatorPolicyInterface $object) { + + $capabilities = self::getRequiredInteractCapabilities($object); + foreach ($capabilities as $capability) { + self::requireCapability($user, $object, $capability); + } + } + + private static function getRequiredInteractCapabilities( + PhabricatorPolicyInterface $object) { $capabilities = $object->getCapabilities(); $capabilities = array_fuse($capabilities); @@ -107,13 +130,7 @@ final class PhabricatorPolicyFilter extends Phobject { $require[] = $can_interact; } - foreach ($require as $capability) { - if (!self::hasCapability($user, $object, $capability)) { - return false; - } - } - - return true; + return $require; } public function setViewer(PhabricatorUser $user) { diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php index 4529704c2f..84fcecfa6f 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -36,10 +36,9 @@ final class PhabricatorApplicationTransactionCommentEditController // auditing, and editing comments serves neither goal. $object = $xaction->getObject(); - $can_interact = PhabricatorPolicyFilter::hasCapability( + $can_interact = PhabricatorPolicyFilter::canInteract( $viewer, - $object, - PhabricatorPolicyCapability::CAN_INTERACT); + $object); if (!$can_interact) { return $this->newDialog() ->setTitle(pht('Conversation Locked')) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php index 22acb3312f..b2405d90c4 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php @@ -189,10 +189,10 @@ final class PhabricatorApplicationTransactionCommentEditor $actor, $xaction, PhabricatorPolicyCapability::CAN_EDIT); - PhabricatorPolicyFilter::requireCapability( + + PhabricatorPolicyFilter::requireCanInteract( $actor, - $xaction->getObject(), - PhabricatorPolicyCapability::CAN_INTERACT); + $xaction->getObject()); } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 7a24bf8ff8..209b6baf64 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -513,10 +513,9 @@ class PhabricatorApplicationTransactionView extends AphrontView { } } - $can_interact = PhabricatorPolicyFilter::hasCapability( + $can_interact = PhabricatorPolicyFilter::canInteract( $viewer, - $xaction->getObject(), - PhabricatorPolicyCapability::CAN_INTERACT); + $xaction->getObject()); $event->setCanInteract($can_interact); }