From e9dedb0c88cd914fd5e1c10521941981091c38be Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 1 Mar 2012 14:19:11 -0800 Subject: [PATCH] Iterate on Maniphest reports Summary: - These are still slow, awkward and hideous -- but slightly better than before. - Allow "open" reports to be sorted. - Add a "burn" chart/table for assessing project volatility. - Add navigation. Test Plan: Looked at reports. Reviewers: btrahan Reviewed By: btrahan CC: aran, epriestley Maniphest Tasks: T923 Differential Revision: https://secure.phabricator.com/D1737 --- src/__celerity_resource_map__.php | 43 +- .../priority/ManiphestTaskPriority.php | 13 +- .../controller/base/ManiphestController.php | 22 + .../maniphest/controller/base/__init__.php | 2 + .../report/ManiphestReportController.php | 389 +++++++++++++++++- .../maniphest/controller/report/__init__.php | 15 + .../tasklist/ManiphestTaskListController.php | 15 +- .../controller/tasklist/__init__.php | 1 - .../rsrc/css/application/maniphest/report.css | 32 ++ .../maniphest/behavior-burn-chart.js | 93 +++++ 10 files changed, 583 insertions(+), 42 deletions(-) create mode 100644 webroot/rsrc/css/application/maniphest/report.css create mode 100644 webroot/rsrc/js/application/maniphest/behavior-burn-chart.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 3ce61ccbc9..520fb65fa3 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -411,6 +411,18 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-buoyant.js', ), + 'javelin-behavior-burn-chart' => + array( + 'uri' => '/res/ed1bf018/rsrc/js/application/maniphest/behavior-burn-chart.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-vector', + ), + 'disk' => '/rsrc/js/application/maniphest/behavior-burn-chart.js', + ), 'javelin-behavior-countdown-timer' => array( 'uri' => '/res/5ee9cb13/rsrc/js/application/countdown/timer.js', @@ -1319,6 +1331,15 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/maniphest/batch-editor.css', ), + 'maniphest-report-css' => + array( + 'uri' => '/res/2e633fcf/rsrc/css/application/maniphest/report.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/maniphest/report.css', + ), 'maniphest-task-edit-css' => array( 'uri' => '/res/68c7863e/rsrc/css/application/maniphest/task-edit.css', @@ -1637,6 +1658,17 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/application/slowvote/slowvote.css', ), + 0 => + array( + 'uri' => '/res/b6096fdd/rsrc/js/javelin/lib/__tests__/URI.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-uri', + 1 => 'javelin-php-serializer', + ), + 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', + ), 'phabricator-standard-page-view' => array( 'uri' => '/res/7e09bbfc/rsrc/css/application/base/standard-page-view.css', @@ -1860,17 +1892,6 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/css/core/syntax.css', ), - 0 => - array( - 'uri' => '/res/b6096fdd/rsrc/js/javelin/lib/__tests__/URI.js', - 'type' => 'js', - 'requires' => - array( - 0 => 'javelin-uri', - 1 => 'javelin-php-serializer', - ), - 'disk' => '/rsrc/js/javelin/lib/__tests__/URI.js', - ), ), array( 'packages' => array( diff --git a/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php b/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php index afa75e3eb4..3e85622be2 100644 --- a/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php +++ b/src/applications/maniphest/constants/priority/ManiphestTaskPriority.php @@ -1,7 +1,7 @@ 16, + self::PRIORITY_TRIAGE => 8, + self::PRIORITY_HIGH => 4, + self::PRIORITY_NORMAL => 2, + self::PRIORITY_LOW => 1, + self::PRIORITY_WISH => 0, + ); + } + public static function getTaskPriorityName($priority) { return idx(self::getTaskPriorityMap(), $priority, '???'); } diff --git a/src/applications/maniphest/controller/base/ManiphestController.php b/src/applications/maniphest/controller/base/ManiphestController.php index 69e515651b..10a6899fb9 100644 --- a/src/applications/maniphest/controller/base/ManiphestController.php +++ b/src/applications/maniphest/controller/base/ManiphestController.php @@ -35,4 +35,26 @@ abstract class ManiphestController extends PhabricatorController { return $response->setContent($page->render()); } + protected function buildBaseSideNav() { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI('/maniphest/view/')); + $nav->addLabel('User Tasks'); + $nav->addFilter('action', 'Assigned'); + $nav->addFilter('created', 'Created'); + $nav->addFilter('subscribed', 'Subscribed'); + $nav->addFilter('triage', 'Need Triage'); + $nav->addSpacer(); + $nav->addLabel('All Tasks'); + $nav->addFilter('alltriage', 'Need Triage'); + $nav->addFilter('all', 'All Tasks'); + $nav->addSpacer(); + $nav->addLabel('Custom'); + $nav->addFilter('custom', 'Custom Query'); + $nav->addSpacer(); + $nav->addLabel('Reports'); + $nav->addFilter('report', 'Reports', '/maniphest/report/'); + + return $nav; + } + } diff --git a/src/applications/maniphest/controller/base/__init__.php b/src/applications/maniphest/controller/base/__init__.php index 12d7fc510e..304e1c1f86 100644 --- a/src/applications/maniphest/controller/base/__init__.php +++ b/src/applications/maniphest/controller/base/__init__.php @@ -9,7 +9,9 @@ phutil_require_module('phabricator', 'aphront/response/webpage'); phutil_require_module('phabricator', 'applications/base/controller/base'); phutil_require_module('phabricator', 'applications/search/constants/scope'); +phutil_require_module('phabricator', 'view/layout/sidenavfilter'); +phutil_require_module('phutil', 'parser/uri'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/maniphest/controller/report/ManiphestReportController.php b/src/applications/maniphest/controller/report/ManiphestReportController.php index e871e80efe..f26dc54e07 100644 --- a/src/applications/maniphest/controller/report/ManiphestReportController.php +++ b/src/applications/maniphest/controller/report/ManiphestReportController.php @@ -28,20 +28,347 @@ final class ManiphestReportController extends ManiphestController { } public function processRequest() { - $request = $this->getRequest(); $user = $request->getUser(); + if ($request->isFormPost()) { + $uri = $request->getRequestURI(); + + $project = head($request->getArr('set_project')); + $uri = $uri->alter('project', $project); + + return id(new AphrontRedirectResponse())->setURI($uri); + } + + + $base_nav = $this->buildBaseSideNav(); + $base_nav->selectFilter('report', 'report'); + $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI('/maniphest/report/')); - $nav->addFilter('user', 'User'); - $nav->addFilter('project', 'Project'); + $nav->addLabel('Open Tasks'); + $nav->addFilter('user', 'By User'); + $nav->addFilter('project', 'By Project'); + $nav->addSpacer(); + $nav->addLabel('Burnup'); + $nav->addFilter('burn', 'Burnup Rate'); $this->view = $nav->selectFilter($this->view, 'user'); - $tasks = id(new ManiphestTaskQuery()) - ->withStatus(ManiphestTaskQuery::STATUS_OPEN) - ->execute(); + require_celerity_resource('maniphest-report-css'); + + switch ($this->view) { + case 'burn': + $core = $this->renderBurn(); + break; + case 'user': + case 'project': + $core = $this->renderOpenTasks(); + break; + default: + return new Aphront404Response(); + } + + $nav->appendChild($core); + $base_nav->appendChild($nav); + + return $this->buildStandardPageResponse( + $base_nav, + array( + 'title' => 'Maniphest Reports', + )); + } + + public function renderBurn() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $handle = null; + + $project_phid = $request->getStr('project'); + if ($project_phid) { + $phids = array($project_phid); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + $handle = $handles[$project_phid]; + } + + $table = new ManiphestTransaction(); + $conn = $table->establishConnection('r'); + + $joins = ''; + if ($project_phid) { + $joins = qsprintf( + $conn, + 'JOIN %T t ON x.taskID = t.id + JOIN %T p ON p.taskPHID = t.phid AND p.projectPHID = %s', + id(new ManiphestTask())->getTableName(), + id(new ManiphestTaskProject())->getTableName(), + $project_phid); + } + + $data = queryfx_all( + $conn, + 'SELECT x.newValue, x.dateCreated FROM %T x %Q WHERE transactionType = %s + ORDER BY x.dateCreated ASC', + $table->getTableName(), + $joins, + ManiphestTransactionType::TYPE_STATUS); + + $stats = array(); + $day_buckets = array(); + + foreach ($data as $row) { + $is_close = $row['newValue']; + $day_bucket = __phabricator_format_local_time( + $row['dateCreated'], + $user, + 'z'); + $day_buckets[$day_bucket] = $row['dateCreated']; + if (empty($stats[$day_bucket])) { + $stats[$day_bucket] = array( + 'open' => 0, + 'close' => 0, + ); + } + $stats[$day_bucket][$is_close ? 'close' : 'open']++; + } + + $template = array( + 'open' => 0, + 'close' => 0, + ); + + $rows = array(); + $rowc = array(); + $last_month = null; + $last_month_epoch = null; + $last_week = null; + $last_week_epoch = null; + $week = null; + $month = null; + + $last = key($stats) - 1; + $period = $template; + + foreach ($stats as $bucket => $info) { + $epoch = $day_buckets[$bucket]; + + $week_bucket = __phabricator_format_local_time( + $epoch, + $user, + 'W'); + if ($week_bucket != $last_week) { + if ($week) { + $rows[] = $this->formatBurnRow( + 'Week of '.phabricator_date($last_week_epoch, $user), + $week); + $rowc[] = 'week'; + } + $week = $template; + $last_week = $week_bucket; + $last_week_epoch = $epoch; + } + + $month_bucket = __phabricator_format_local_time( + $epoch, + $user, + 'm'); + if ($month_bucket != $last_month) { + if ($month) { + $rows[] = $this->formatBurnRow( + __phabricator_format_local_time($last_month_epoch, $user, 'F, Y'), + $month); + $rowc[] = 'month'; + } + $month = $template; + $last_month = $month_bucket; + $last_month_epoch = $epoch; + } + + $rows[] = $this->formatBurnRow(phabricator_date($epoch, $user), $info); + $rowc[] = null; + $week['open'] += $info['open']; + $week['close'] += $info['close']; + $month['open'] += $info['open']; + $month['close'] += $info['close']; + $period['open'] += $info['open']; + $period['close'] += $info['close']; + } + + if ($week) { + $rows[] = $this->formatBurnRow( + 'Week To Date', + $week); + $rowc[] = 'week'; + } + + if ($month) { + $rows[] = $this->formatBurnRow( + 'Month To Date', + $month); + $rowc[] = 'month'; + } + + $rows[] = $this->formatBurnRow( + 'All Time', + $period); + $rowc[] = 'aggregate'; + + $rows = array_reverse($rows); + $rowc = array_reverse($rowc); + + $table = new AphrontTableView($rows); + $table->setRowClasses($rowc); + $table->setHeaders( + array( + 'Period', + 'Opened', + 'Closed', + 'Change', + )); + $table->setColumnClasses( + array( + 'right wide', + 'n', + 'n', + 'n', + )); + + if ($handle) { + $header = "Task Burn Rate for Project ".$handle->renderLink(); + $caption = "

NOTE: This table reflects tasks currently in ". + "the project. If a task was opened in the past but added to ". + "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.

"; + } else { + $header = "Task Burn Rate for All Tasks"; + $caption = null; + } + + $panel = new AphrontPanelView(); + $panel->setHeader($header); + $panel->setCaption($caption); + $panel->appendChild($table); + + $tokens = array(); + if ($handle) { + $tokens = array( + $handle->getPHID() => $handle->getFullName(), + ); + } + + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/projects/') + ->setLabel('Project') + ->setLimit(1) + ->setName('set_project') + ->setValue($tokens)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Filter By Project')); + + $filter = new AphrontListFilterView(); + $filter->appendChild($form); + + $id = celerity_generate_unique_node_id(); + $chart = phutil_render_tag( + 'div', + array( + 'id' => $id, + 'style' => 'border: 1px solid #6f6f6f; '. + 'margin: 1em 2em; '. + 'height: 400px; ', + ), + ''); + + list($open_x, $close_x, $open_y, $close_y) = $this->buildSeries($data); + + require_celerity_resource('raphael-core'); + require_celerity_resource('raphael-g'); + require_celerity_resource('raphael-g-line'); + + Javelin::initBehavior('burn-chart', array( + 'hardpoint' => $id, + 'x' => array( + $open_x, + $close_x, + ), + 'y' => array( + $open_y, + $close_y, + ), + )); + + return array($filter, $chart, $panel); + } + + private function buildSeries(array $data) { + $open_count = 0; + $close_count = 0; + + $open_x = array(); + $open_y = array(); + $close_x = array(); + $close_y = array(); + + $start = (int)idx(head($data), 'dateCreated', time()); + + $open_x[] = $start; + $open_y[] = $open_count; + $close_x[] = $start; + $close_y[] = $close_count; + + foreach ($data as $row) { + $t = (int)$row['dateCreated']; + if ($row['newValue']) { + ++$close_count; + $close_x[] = $t; + $close_y[] = $close_count; + } else { + ++$open_count; + $open_x[] = $t; + $open_y[] = $open_count; + } + } + + $close_x[] = time(); + $close_y[] = $close_count; + $open_x[] = time(); + $open_y[] = $open_count; + + return array($open_x, $close_x, $open_y, $close_y); + } + + private function formatBurnRow($label, $info) { + $delta = $info['open'] - $info['close']; + $fmt = number_format($delta); + if ($delta > 0) { + $fmt = '+'.$fmt; + $fmt = ''.$fmt.''; + } else { + $fmt = ''.$fmt.''; + } + + return array( + $label, + number_format($info['open']), + number_format($info['close']), + $fmt); + } + + public function renderOpenTasks() { + $request = $this->getRequest(); + $user = $request->getUser(); + + $query = id(new ManiphestTaskQuery()) + ->withStatus(ManiphestTaskQuery::STATUS_OPEN); + + $tasks = $query->execute(); $date = phabricator_date(time(), $user); @@ -55,7 +382,7 @@ final class ManiphestReportController extends ManiphestController { array( 'href' => '/maniphest/?users=PHID-!!!!-UP-FOR-GRABS', ), - 'Up For Grabs'); + '(Up For Grabs)'); $col_header = 'User'; $header = 'Open Tasks by User and Priority ('.$date.')'; $link = '/maniphest/?users='; @@ -72,18 +399,24 @@ final class ManiphestReportController extends ManiphestController { $leftover[] = $task; } } - $leftover_name = 'Uncategorized'; + $leftover_name = phutil_render_tag( + 'a', + array( + 'href' => '/maniphest/view/all/?projects=PHID-!!!!-NO_PROJECT', + ), + '(No Project)'); $col_header = 'Project'; $header = 'Open Tasks by Project and Priority ('.$date.')'; $link = '/maniphest/view/all/?projects='; break; } - $phids = array_keys($result); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $handles = msort($handles, 'getName'); + $order = $request->getStr('order', 'name'); + $rows = array(); $pri_total = array(); foreach (array_merge($handles, array(null)) as $handle) { @@ -116,9 +449,24 @@ final class ManiphestReportController extends ManiphestController { } $row[] = number_format($total); + switch ($order) { + case 'total': + $row['sort'] = $total; + break; + case 'name': + default: + $row['sort'] = $handle ? $handle->getName() : '~'; + break; + } + $rows[] = $row; } + $rows = isort($rows, 'sort'); + foreach ($rows as $k => $row) { + unset($rows[$k]['sort']); + } + $cname = array($col_header); $cclass = array('pri right wide'); foreach (ManiphestTaskPriority::getTaskPriorityMap() as $pri => $label) { @@ -136,13 +484,24 @@ final class ManiphestReportController extends ManiphestController { $panel->setHeader($header); $panel->appendChild($table); - $nav->appendChild($panel); + $form = id(new AphrontFormView()) + ->setUser($user) + ->appendChild( + id(new AphrontFormToggleButtonsControl()) + ->setLabel('Order') + ->setValue($order) + ->setBaseURI($request->getRequestURI(), 'order') + ->setButtons( + array( + 'name' => 'Name', + 'total' => 'Total', + ))); - return $this->buildStandardPageResponse( - $nav, - array( - 'title' => 'Maniphest Reports', - )); + + $filter = new AphrontListFilterView(); + $filter->appendChild($form); + + return array($filter, $panel); } } diff --git a/src/applications/maniphest/controller/report/__init__.php b/src/applications/maniphest/controller/report/__init__.php index 5ebc991da6..985eddbc18 100644 --- a/src/applications/maniphest/controller/report/__init__.php +++ b/src/applications/maniphest/controller/report/__init__.php @@ -6,11 +6,26 @@ +phutil_require_module('phabricator', 'aphront/response/404'); +phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/maniphest/constants/priority'); +phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype'); phutil_require_module('phabricator', 'applications/maniphest/controller/base'); phutil_require_module('phabricator', 'applications/maniphest/query'); +phutil_require_module('phabricator', 'applications/maniphest/storage/task'); +phutil_require_module('phabricator', 'applications/maniphest/storage/taskproject'); +phutil_require_module('phabricator', 'applications/maniphest/storage/transaction'); phutil_require_module('phabricator', 'applications/phid/handle/data'); +phutil_require_module('phabricator', 'infrastructure/celerity/api'); +phutil_require_module('phabricator', 'infrastructure/javelin/api'); +phutil_require_module('phabricator', 'storage/qsprintf'); +phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phabricator', 'view/control/table'); +phutil_require_module('phabricator', 'view/form/base'); +phutil_require_module('phabricator', 'view/form/control/submit'); +phutil_require_module('phabricator', 'view/form/control/togglebuttons'); +phutil_require_module('phabricator', 'view/form/control/tokenizer'); +phutil_require_module('phabricator', 'view/layout/listfilter'); phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phabricator', 'view/layout/sidenavfilter'); phutil_require_module('phabricator', 'view/utils'); diff --git a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php index 8f2f42645a..36c32315c2 100644 --- a/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php +++ b/src/applications/maniphest/controller/tasklist/ManiphestTaskListController.php @@ -57,20 +57,7 @@ class ManiphestTaskListController extends ManiphestController { return id(new AphrontRedirectResponse())->setURI($uri); } - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI('/maniphest/view/')); - $nav->addLabel('User Tasks'); - $nav->addFilter('action', 'Assigned'); - $nav->addFilter('created', 'Created'); - $nav->addFilter('subscribed', 'Subscribed'); - $nav->addFilter('triage', 'Need Triage'); - $nav->addSpacer(); - $nav->addLabel('All Tasks'); - $nav->addFilter('alltriage', 'Need Triage'); - $nav->addFilter('all', 'All Tasks'); - $nav->addSpacer(); - $nav->addLabel('Custom'); - $nav->addFilter('custom', 'Custom Query'); + $nav = $this->buildBaseSideNav(); $this->view = $nav->selectFilter($this->view, 'action'); diff --git a/src/applications/maniphest/controller/tasklist/__init__.php b/src/applications/maniphest/controller/tasklist/__init__.php index dd8c5a941c..5a0301fb21 100644 --- a/src/applications/maniphest/controller/tasklist/__init__.php +++ b/src/applications/maniphest/controller/tasklist/__init__.php @@ -25,7 +25,6 @@ phutil_require_module('phabricator', 'view/form/control/text'); phutil_require_module('phabricator', 'view/form/control/togglebuttons'); phutil_require_module('phabricator', 'view/form/control/tokenizer'); phutil_require_module('phabricator', 'view/layout/listfilter'); -phutil_require_module('phabricator', 'view/layout/sidenavfilter'); phutil_require_module('phabricator', 'view/null'); phutil_require_module('phutil', 'markup'); diff --git a/webroot/rsrc/css/application/maniphest/report.css b/webroot/rsrc/css/application/maniphest/report.css new file mode 100644 index 0000000000..0da5632217 --- /dev/null +++ b/webroot/rsrc/css/application/maniphest/report.css @@ -0,0 +1,32 @@ +/** + * @provides maniphest-report-css + */ + +table.aphront-table-view tr.aggregate, +table.aphront-table-view tr.alt-aggregate { + background: #bb5577; +} + +table.aphront-table-view tr.month, +table.aphront-table-view tr.alt-month { + background: #ee77aa; +} + +table.aphront-table-view tr.week, +table.aphront-table-view tr.alt-week { + background: #ffccdd; +} + +span.red { + color: #aa0000; + font-weight: bold; +} + +span.green { + color: #00aa00; + font-weight: bold; +} + +.aphront-panel-view-caption p { + padding: 6px 0 0; +} diff --git a/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js b/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js new file mode 100644 index 0000000000..7d58030c7d --- /dev/null +++ b/webroot/rsrc/js/application/maniphest/behavior-burn-chart.js @@ -0,0 +1,93 @@ +/** + * @provides javelin-behavior-burn-chart + * @requires javelin-behavior + * javelin-dom + * javelin-vector + */ + +JX.behavior('burn-chart', function(config) { + + var h = JX.$(config.hardpoint); + var p = JX.$V(h); + var d = JX.Vector.getDim(h); + var mx = 60; + var my = 30; + + var r = Raphael(p.x, p.y, d.x, d.y); + + var l = r.linechart( + mx, my, + d.x - (2 * mx), d.y - (2 * my), + config.x, + config.y, + { + nostroke: false, + axis: "0 0 1 1", + shade: true, + gutter: 1, + colors: ['#d00', '#090'] + }); + + + // Convert the epoch timestamps on the X axis into readable dates. + + var n = 2; + var ii = 0; + var text = l.axis[0].text.items; + for (var k in text) { + if (ii++ % n) { + text[k].attr({text: ''}); + } else { + var cur = text[k].attr('text'); + var date = new Date(parseInt(cur, 10) * 1000); + var str = date.toLocaleDateString(); + text[k].attr({text: str}); + } + } + + // Get rid of the green shading below closed tasks. + + l.shades[1].attr({fill: '#fff', opacity: 1}); + + l.hoverColumn(function() { + + + var open = 0; + for (var ii = 0; ii < config.x[0].length; ii++) { + if (config.x[0][ii] > this.axis) { + break; + } + open = config.y[0][ii]; + } + + var closed = 0; + for (var ii = 0; ii < config.x[1].length; ii++) { + if (config.x[1][ii] > this.axis) { + break; + } + closed = config.y[1][ii]; + } + + + var date = new Date(parseInt(this.axis, 10) * 1000).toLocaleDateString(); + var total = open + " Total Tasks"; + var pain = (open - closed) + " Open Tasks"; + + var tag = r.tag( + this.x, + this.y[0], + [date, total, pain].join("\n"), + 180, + 24); + tag + .insertBefore(this) + .attr([{fill : '#fff'}, {fill: '#000'}]); + + this.tags = r.set(); + this.tags.push(tag); + }, function() { + this.tags && this.tags.remove(); + }); + +}); +