diff --git a/README.markdown b/README.markdown index e98c9f93e..de593e020 100644 --- a/README.markdown +++ b/README.markdown @@ -75,6 +75,7 @@ Documentation - [Swimlanes](docs/swimlanes.markdown) - [Calendar](docs/calendar.markdown) - [Budget](docs/budget.markdown) +- [Analytics](docs/analytics.markdown) #### Working with tasks diff --git a/app/Controller/Analytic.php b/app/Controller/Analytic.php index 8b0684d44..e7578da9c 100644 --- a/app/Controller/Analytic.php +++ b/app/Controller/Analytic.php @@ -125,4 +125,46 @@ class Analytic extends Base ))); } } + + /** + * Show burndown chart + * + * @access public + */ + public function burndown() + { + $project = $this->getProject(); + $values = $this->request->getValues(); + + $from = $this->request->getStringParam('from', date('Y-m-d', strtotime('-1week'))); + $to = $this->request->getStringParam('to', date('Y-m-d')); + + if (! empty($values)) { + $from = $values['from']; + $to = $values['to']; + } + + if ($this->request->isAjax()) { + $this->response->json(array( + 'metrics' => $this->projectDailySummary->getRawMetricsByDay($project['id'], $from, $to), + 'labels' => array( + 'day' => t('Date'), + 'score' => t('Complexity'), + ) + )); + } + else { + $this->response->html($this->layout('analytic/burndown', array( + 'values' => array( + 'from' => $from, + 'to' => $to, + ), + 'display_graph' => $this->projectDailySummary->countDays($project['id'], $from, $to) >= 2, + 'project' => $project, + 'date_format' => $this->config->get('application_date_format'), + 'date_formats' => $this->dateParser->getAvailableFormats(), + 'title' => t('Burndown chart for "%s"', $project['name']), + ))); + } + } } diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php index 2e543e483..e9725a002 100644 --- a/app/Locale/da_DK/translations.php +++ b/app/Locale/da_DK/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php index 558000717..cd0b4b7f6 100644 --- a/app/Locale/de_DE/translations.php +++ b/app/Locale/de_DE/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php index f0a2ef4cb..efbf589e2 100644 --- a/app/Locale/es_ES/translations.php +++ b/app/Locale/es_ES/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php index 48a38f46d..e8df52d05 100644 --- a/app/Locale/fi_FI/translations.php +++ b/app/Locale/fi_FI/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php index a99e19ae7..63270415c 100644 --- a/app/Locale/fr_FR/translations.php +++ b/app/Locale/fr_FR/translations.php @@ -852,4 +852,7 @@ return array( 'uploaded by: %s' => 'Télécharger par : %s', 'uploaded on: %s' => 'Télécharger le : %s', 'size: %s' => 'Taille : %s', + 'Burndown chart for "%s"' => 'Graphique d\'avancement pour « %s »', + 'Burndown chart' => 'Graphique d\'avancement', + 'This chart show the task complexity over the time (Work Remaining).' => 'Ce graphique représente la complexité des tâches en fonction du temps (travail restant).', ); diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php index f398629ee..ca6e540f5 100644 --- a/app/Locale/hu_HU/translations.php +++ b/app/Locale/hu_HU/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php index 385339f07..7c932d02a 100644 --- a/app/Locale/it_IT/translations.php +++ b/app/Locale/it_IT/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php index 9cedf534f..4b56298a8 100644 --- a/app/Locale/ja_JP/translations.php +++ b/app/Locale/ja_JP/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php index 31a8006c0..374bfe20d 100644 --- a/app/Locale/nl_NL/translations.php +++ b/app/Locale/nl_NL/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php index 8dee492e2..99de94603 100644 --- a/app/Locale/pl_PL/translations.php +++ b/app/Locale/pl_PL/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php index f855edb0b..e02489a6b 100644 --- a/app/Locale/pt_BR/translations.php +++ b/app/Locale/pt_BR/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php index d67de097e..8f2ed8256 100644 --- a/app/Locale/ru_RU/translations.php +++ b/app/Locale/ru_RU/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php index 26a3599a5..8245d1771 100644 --- a/app/Locale/sr_Latn_RS/translations.php +++ b/app/Locale/sr_Latn_RS/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php index 90de74705..7b933178a 100644 --- a/app/Locale/sv_SE/translations.php +++ b/app/Locale/sv_SE/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php index f48999cb6..72edd63b6 100644 --- a/app/Locale/th_TH/translations.php +++ b/app/Locale/th_TH/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php index fb8bc0807..fad993d1c 100644 --- a/app/Locale/tr_TR/translations.php +++ b/app/Locale/tr_TR/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php index 10d4c6049..8c4548070 100644 --- a/app/Locale/zh_CN/translations.php +++ b/app/Locale/zh_CN/translations.php @@ -850,4 +850,7 @@ return array( // 'uploaded by: %s' => '', // 'uploaded on: %s' => '', // 'size: %s' => '', + // 'Burndown chart for "%s"' => '', + // 'Burndown chart' => '', + // 'This chart show the task complexity over the time (Work Remaining).' => '', ); diff --git a/app/Model/ProjectAnalytic.php b/app/Model/ProjectAnalytic.php index 46f2242de..a663f921c 100644 --- a/app/Model/ProjectAnalytic.php +++ b/app/Model/ProjectAnalytic.php @@ -83,6 +83,8 @@ class ProjectAnalytic extends Base $metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2); } + ksort($metrics); + return array_values($metrics); } } diff --git a/app/Model/ProjectDailySummary.php b/app/Model/ProjectDailySummary.php index 0a06bbd42..9e7c836a1 100644 --- a/app/Model/ProjectDailySummary.php +++ b/app/Model/ProjectDailySummary.php @@ -20,6 +20,9 @@ class ProjectDailySummary extends Base /** * Update daily totals for the project * + * "total" is the number open of tasks in the column + * "score" is the sum of tasks score in the column + * * @access public * @param integer $project_id Project id * @param string $date Record date (YYYY-MM-DD) @@ -40,6 +43,7 @@ class ProjectDailySummary extends Base 'project_id' => $project_id, 'column_id' => $column_id, 'total' => 0, + 'score' => 0, )); $db->table(ProjectDailySummary::TABLE) @@ -47,6 +51,11 @@ class ProjectDailySummary extends Base ->eq('column_id', $column_id) ->eq('day', $date) ->update(array( + 'score' => $db->table(Task::TABLE) + ->eq('project_id', $project_id) + ->eq('column_id', $column_id) + ->eq('is_active', Task::STATUS_OPEN) + ->sum('score'), 'total' => $db->table(Task::TABLE) ->eq('project_id', $project_id) ->eq('column_id', $column_id) @@ -92,12 +101,39 @@ class ProjectDailySummary extends Base ProjectDailySummary::TABLE.'.column_id', ProjectDailySummary::TABLE.'.day', ProjectDailySummary::TABLE.'.total', + ProjectDailySummary::TABLE.'.score', Board::TABLE.'.title AS column_title' ) ->join(Board::TABLE, 'id', 'column_id') ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) ->gte('day', $from) ->lte('day', $to) + ->asc(ProjectDailySummary::TABLE.'.day') + ->findAll(); + } + + /** + * Get raw metrics for the project within a data range grouped by day + * + * @access public + * @param integer $project_id Project id + * @param string $from Start date (ISO format YYYY-MM-DD) + * @param string $to End date + * @return array + */ + public function getRawMetricsByDay($project_id, $from, $to) + { + return $this->db->table(ProjectDailySummary::TABLE) + ->columns( + ProjectDailySummary::TABLE.'.day', + 'SUM('.ProjectDailySummary::TABLE.'.total) AS total', + 'SUM('.ProjectDailySummary::TABLE.'.score) AS score' + ) + ->eq(ProjectDailySummary::TABLE.'.project_id', $project_id) + ->gte('day', $from) + ->lte('day', $to) + ->asc(ProjectDailySummary::TABLE.'.day') + ->groupBy(ProjectDailySummary::TABLE.'.day') ->findAll(); } diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php index c2bfc97a1..6ad6dc518 100644 --- a/app/Schema/Mysql.php +++ b/app/Schema/Mysql.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 63; +const VERSION = 64; + +function version_64($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INT NOT NULL DEFAULT 0'); +} function version_63($pdo) { diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php index 80f292190..b5cf72a60 100644 --- a/app/Schema/Postgres.php +++ b/app/Schema/Postgres.php @@ -6,7 +6,12 @@ use PDO; use Core\Security; use Model\Link; -const VERSION = 44; +const VERSION = 45; + +function version_45($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0'); +} function version_44($pdo) { diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php index d244cd89d..fb1d7d29a 100644 --- a/app/Schema/Sqlite.php +++ b/app/Schema/Sqlite.php @@ -6,7 +6,12 @@ use Core\Security; use PDO; use Model\Link; -const VERSION = 62; +const VERSION = 63; + +function version_63($pdo) +{ + $pdo->exec('ALTER TABLE project_daily_summaries ADD COLUMN score INTEGER NOT NULL DEFAULT 0'); +} function version_62($pdo) { diff --git a/app/Subscriber/ProjectDailySummarySubscriber.php b/app/Subscriber/ProjectDailySummarySubscriber.php index 6d7377343..f865c0368 100644 --- a/app/Subscriber/ProjectDailySummarySubscriber.php +++ b/app/Subscriber/ProjectDailySummarySubscriber.php @@ -12,6 +12,7 @@ class ProjectDailySummarySubscriber extends Base implements EventSubscriberInter { return array( Task::EVENT_CREATE => array('execute', 0), + Task::EVENT_UPDATE => array('execute', 0), Task::EVENT_CLOSE => array('execute', 0), Task::EVENT_OPEN => array('execute', 0), Task::EVENT_MOVE_COLUMN => array('execute', 0), diff --git a/app/Template/analytic/burndown.php b/app/Template/analytic/burndown.php new file mode 100644 index 000000000..5ebe1032b --- /dev/null +++ b/app/Template/analytic/burndown.php @@ -0,0 +1,34 @@ + + + +

+ +
+
+
+ + +
+ +
+ + formCsrf() ?> + +
+ formLabel(t('Start Date'), 'from') ?> + formText('from', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ formLabel(t('End Date'), 'to') ?> + formText('to', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?> +
+ +
+ +
+
+ +

diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index a7076db9d..f35152810 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -10,5 +10,8 @@
  • a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
  • +
  • + a(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?> +
  • \ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js index bb576df20..e99f5dfcd 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -160,7 +160,8 @@ Kanboard.Calendar=function(){function a(a){var b=$("#calendar").data("save-url") a.fullCalendar("addEventSource",b);a.fullCalendar("rerenderEvents")})}function b(a){var b=$("#calendar"),c=b.data("check-url"),d={start:b.fullCalendar("getView").start.format(),end:b.fullCalendar("getView").end.format()};jQuery.extend(d,a);for(var e in d)c+="&"+e+"="+d[e];$.getJSON(c,function(a){b.fullCalendar("removeEvents");b.fullCalendar("addEventSource",a);b.fullCalendar("rerenderEvents")})}function d(){var a=Kanboard.GetStorageItem(f);if(""!==a){var a=JSON.parse(a),c;for(c in a)$("select[name="+ c+"]").val(a[c])}b(a||{});$(".calendar-filter").change(e)}function e(){var a={};$(".calendar-filter").each(function(){a[$(this).attr("name")]=$(this).val()});Kanboard.SetStorageItem(f,JSON.stringify(a));b(a)}var f="";jQuery(document).ready(function(){Kanboard.Exists("calendar")?(f="calendar_filters_"+$("#calendar").data("project-id"),$("#calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,defaultView:"month",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"}, viewRender:d,eventDrop:a}),d()):Kanboard.Exists("user-calendar")&&$("#user-calendar").fullCalendar({lang:$("body").data("js-lang"),editable:!0,eventLimit:!0,height:Kanboard.Exists("dashboard-calendar")?500:"auto",defaultView:"agendaWeek",header:{left:"prev,next today",center:"title",right:"month,agendaWeek,agendaDay"},viewRender:c,eventDrop:a})})}(); -Kanboard.Analytic=function(){jQuery(document).ready(function(){Kanboard.Exists("analytic-task-repartition")?Kanboard.Analytic.TaskRepartition.Init():Kanboard.Exists("analytic-user-repartition")?Kanboard.Analytic.UserRepartition.Init():Kanboard.Exists("analytic-cfd")&&Kanboard.Analytic.CFD.Init()});return{}}(); +Kanboard.Analytic=function(){jQuery(document).ready(function(){Kanboard.Exists("analytic-task-repartition")?Kanboard.Analytic.TaskRepartition.Init():Kanboard.Exists("analytic-user-repartition")?Kanboard.Analytic.UserRepartition.Init():Kanboard.Exists("analytic-cfd")?Kanboard.Analytic.CFD.Init():Kanboard.Exists("analytic-burndown")&&Kanboard.Analytic.Burndown.Init()});return{}}(); +Kanboard.Analytic.Burndown=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.metrics;a=[];for(var d=0;d=5.3", - "ext-mbstring": "*" + "ext-mbstring": "*", + "ext-gd": "*" }, "platform-dev": [] } diff --git a/docs/analytics.markdown b/docs/analytics.markdown new file mode 100644 index 000000000..e088a2217 --- /dev/null +++ b/docs/analytics.markdown @@ -0,0 +1,33 @@ +Analytics +========= + +User repartition +---------------- + +![User repartition](http://kanboard.net/screenshots/documentation/user-repartition.png) + +This pie chart show the number of open tasks assigned per user. + +Task distribution +----------------- + +![Task distribution](http://kanboard.net/screenshots/documentation/task-distribution.png) + +This pie chart gives an overview of the number of open tasks per column. + +Cumulative flow diagram +----------------------- + +![Cumulative flow diagram](http://kanboard.net/screenshots/documentation/cfd.png) + +This chart show the number of tasks cumulatively for each column over the time. + +Burndown chart +-------------- + +![Burndown chart](http://kanboard.net/screenshots/documentation/burndown-chart.png) + +The [burn down chart](http://en.wikipedia.org/wiki/Burn_down_chart) is available for each project. +This chart is a graphical representation of work left to do versus time. + +Kanboard use the complexity or story point to generate this diagram. \ No newline at end of file diff --git a/scripts/create-sample-burndown.php b/scripts/create-sample-burndown.php new file mode 100755 index 000000000..ae0b26271 --- /dev/null +++ b/scripts/create-sample-burndown.php @@ -0,0 +1,55 @@ +#!/usr/bin/env php + 'Task #'.$i, + 'project_id' => 1, + 'column_id' => rand(1, 4), + 'score' => rand(1, 21) + ); + + $taskCreation->create($task); +} + +$pds->updateTotals(1, date('Y-m-d', strtotime('-7 days'))); + +$taskStatus->close(1); +$pds->updateTotals(1, date('Y-m-d', strtotime('-6 days'))); + +$taskStatus->close(2); +$taskStatus->close(3); +$pds->updateTotals(1, date('Y-m-d', strtotime('-5 days'))); + +$taskStatus->close(4); +$pds->updateTotals(1, date('Y-m-d', strtotime('-4 days'))); + +$taskStatus->close(5); +$pds->updateTotals(1, date('Y-m-d', strtotime('-3 days'))); + +$taskStatus->close(6); +$taskStatus->close(7); +$taskStatus->close(8); +$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days'))); + +$taskStatus->close(9); +$taskStatus->close(10); +$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days'))); + +$taskStatus->close(12); +$taskStatus->close(13); +$pds->updateTotals(1, date('Y-m-d', strtotime('-1 days'))); + +$taskStatus->close(1); +$pds->updateTotals(1, date('Y-m-d')); \ No newline at end of file