diff options
author | Frederic Guillot <fred@kanboard.net> | 2015-04-11 23:01:17 -0400 |
---|---|---|
committer | Frederic Guillot <fred@kanboard.net> | 2015-04-11 23:01:17 -0400 |
commit | 9ca2ba21272ddb1958e0b5b2f5842cde42508139 (patch) | |
tree | c6db297008d728a44040af5804386802b457219e | |
parent | 7df055aff1e1056d87bb720531d60cb079805f94 (diff) |
Add burndown chart
32 files changed, 351 insertions, 26 deletions
diff --git a/README.markdown b/README.markdown index e98c9f93..de593e02 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 8b0684d4..e7578da9 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 2e543e48..e9725a00 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 55800071..cd0b4b7f 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 f0a2ef4c..efbf589e 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 48a38f46..e8df52d0 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 a99e19ae..63270415 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 f398629e..ca6e540f 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 385339f0..7c932d02 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 9cedf534..4b56298a 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 31a8006c..374bfe20 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 8dee492e..99de9460 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 f855edb0..e02489a6 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 d67de097..8f2ed825 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 26a3599a..8245d177 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 90de7470..7b933178 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 f48999cb..72edd63b 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 fb8bc080..fad993d1 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 10d4c604..8c454807 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 46f2242d..a663f921 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 0a06bbd4..9e7c836a 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 c2bfc97a..6ad6dc51 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 80f29219..b5cf72a6 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 d244cd89..fb1d7d29 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 6d737734..f865c036 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 00000000..5ebe1032 --- /dev/null +++ b/app/Template/analytic/burndown.php @@ -0,0 +1,34 @@ +<div class="page-header"> + <h2><?= t('Burndown chart') ?></h2> +</div> + +<?php if (! $display_graph): ?> + <p class="alert"><?= t('Not enough data to show the graph.') ?></p> +<?php else: ?> + <section id="analytic-burndown"> + <div id="chart" data-url="<?= $this->u('analytic', 'burndown', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div> + </section> +<?php endif ?> + +<hr/> + +<form method="post" class="form-inline" action="<?= $this->u('analytic', 'burndown', array('project_id' => $project['id'])) ?>" autocomplete="off"> + + <?= $this->formCsrf() ?> + + <div class="form-inline-group"> + <?= $this->formLabel(t('Start Date'), 'from') ?> + <?= $this->formText('from', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <?= $this->formLabel(t('End Date'), 'to') ?> + <?= $this->formText('to', $values, array(), array('required', 'placeholder="'.$this->inList($date_format, $date_formats).'"'), 'form-date') ?> + </div> + + <div class="form-inline-group"> + <input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/> + </div> +</form> + +<p class="alert alert-info"><?= t('This chart show the task complexity over the time (Work Remaining).') ?></p> diff --git a/app/Template/analytic/sidebar.php b/app/Template/analytic/sidebar.php index a7076db9..f3515281 100644 --- a/app/Template/analytic/sidebar.php +++ b/app/Template/analytic/sidebar.php @@ -10,5 +10,8 @@ <li> <?= $this->a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?> </li> + <li> + <?= $this->a(t('Burndown chart'), 'analytic', 'burndown', array('project_id' => $project['id'])) ?> + </li> </ul> </div>
\ No newline at end of file diff --git a/assets/js/app.js b/assets/js/app.js index bb576df2..e99f5dfc 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<b.length;d++){var e={},f=parseInt(b[d].score);e[c.day]=b[d].day;e[c.score]=f;a.push(e)}b=dimple.newSvg("#chart","100%",380);a=new dimple.chart(b,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.score);a.addSeries(null,dimple.plot.line);a.draw()})}}}(); Kanboard.Analytic.CFD=function(){return{Init:function(){jQuery.getJSON($("#chart").attr("data-url"),function(a){var c=a.labels,b=a.columns,d=a.metrics;a=[];for(var e=0;e<d.length;e++){var f={};f[c.column]=d[e].column_title;f[c.day]=d[e].day;f[c.total]=d[e].total;a.push(f)}d=dimple.newSvg("#chart","100%",380);a=new dimple.chart(d,a);a.addCategoryAxis("x",c.day).addOrderRule("Date");a.addMeasureAxis("y",c.total);a.addSeries(c.column,dimple.plot.area).addOrderRule(b.reverse());a.addLegend(10,10,500, 30,"left");a.draw()})}}}();Kanboard.Analytic.TaskRepartition=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<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.column_title]=b[d].column_title;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.column_title,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}(); Kanboard.Analytic.UserRepartition=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<b.length;d++){var e={};e[c.nb_tasks]=b[d].nb_tasks;e[c.user]=b[d].user;a.push(e)}b=dimple.newSvg("#chart","100%",350);a=new dimple.chart(b,a);a.addMeasureAxis("p",c.nb_tasks);a.addSeries(c.user,dimple.plot.pie).innerRadius="50%";a.addLegend(0,0,100,"100%","left");a.draw()})}}}(); diff --git a/assets/js/src/analytic.js b/assets/js/src/analytic.js index 26050a49..0912bbcb 100644 --- a/assets/js/src/analytic.js +++ b/assets/js/src/analytic.js @@ -2,7 +2,7 @@ Kanboard.Analytic = (function() { jQuery(document).ready(function() { - + if (Kanboard.Exists("analytic-task-repartition")) { Kanboard.Analytic.TaskRepartition.Init(); } @@ -12,12 +12,62 @@ Kanboard.Analytic = (function() { else if (Kanboard.Exists("analytic-cfd")) { Kanboard.Analytic.CFD.Init(); } + else if (Kanboard.Exists("analytic-burndown")) { + Kanboard.Analytic.Burndown.Init(); + } }); return {}; })(); +Kanboard.Analytic.Burndown = (function() { + + function fetchData() + { + jQuery.getJSON($("#chart").attr("data-url"), function(data) { + drawGraph(data.metrics, data.labels); + }); + } + + function drawGraph(metrics, labels) + { + var series = prepareSeries(metrics, labels); + + var svg = dimple.newSvg("#chart", "100%", 380); + var chart = new dimple.chart(svg, series); + + var x = chart.addCategoryAxis("x", labels['day']); + x.addOrderRule("Date"); + + chart.addMeasureAxis("y", labels['score']); + chart.addSeries(null, dimple.plot.line); + + chart.draw(); + } + + function prepareSeries(metrics, labels) + { + var series = []; + + for (var i = 0; i < metrics.length; i++) { + + var row = {}; + var score = parseInt(metrics[i]['score']); + row[labels['day']] = metrics[i]['day']; + row[labels['score']] = score; + series.push(row); + } + + return series; + } + + return { + Init: fetchData + }; + +})(); + Kanboard.Analytic.CFD = (function() { function fetchData() diff --git a/composer.lock b/composer.lock index 360ed751..8058424a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "466ee3928d7d3b0bbee15de3b4c76676", + "hash": "01ebe465ed3a59d8350670ebd4ef8793", "packages": [ { "name": "christian-riesen/base32", @@ -190,12 +190,12 @@ "source": { "type": "git", "url": "https://github.com/fguillot/picoDb.git", - "reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844" + "reference": "35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/picoDb/zipball/cd6a571d2de5c0b30d538d7cd6603dc16b25b844", - "reference": "cd6a571d2de5c0b30d538d7cd6603dc16b25b844", + "url": "https://api.github.com/repos/fguillot/picoDb/zipball/35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac", + "reference": "35c8d2d3f70b713f66e1dc14c1d6481abe5db3ac", "shasum": "" }, "require": { @@ -219,7 +219,7 @@ ], "description": "Minimalist database query builder", "homepage": "https://github.com/fguillot/picoDb", - "time": "2015-03-27 02:21:18" + "time": "2015-04-12 02:46:43" }, { "name": "fguillot/simple-validator", @@ -227,12 +227,12 @@ "source": { "type": "git", "url": "https://github.com/fguillot/simpleValidator.git", - "reference": "5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7" + "reference": "41655dc7b9224395f5bb3b5623f6e428fe6d64e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7", - "reference": "5ebdb6df4c5f3aa2539b633eb4ae94c9e8c4ada7", + "url": "https://api.github.com/repos/fguillot/simpleValidator/zipball/41655dc7b9224395f5bb3b5623f6e428fe6d64e8", + "reference": "41655dc7b9224395f5bb3b5623f6e428fe6d64e8", "shasum": "" }, "require": { @@ -256,7 +256,7 @@ ], "description": "The most easy to use validator library for PHP :)", "homepage": "https://github.com/fguillot/simpleValidator", - "time": "2015-02-14 21:04:14" + "time": "2015-04-05 21:44:06" }, { "name": "fguillot/simpleLogger", @@ -495,17 +495,17 @@ }, { "name": "symfony/console", - "version": "v2.6.5", + "version": "v2.6.6", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "53f86497ccd01677e22435cfb7262599450a90d1" + "reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/53f86497ccd01677e22435cfb7262599450a90d1", - "reference": "53f86497ccd01677e22435cfb7262599450a90d1", + "url": "https://api.github.com/repos/symfony/Console/zipball/5b91dc4ed5eb08553f57f6df04c4730a73992667", + "reference": "5b91dc4ed5eb08553f57f6df04c4730a73992667", "shasum": "" }, "require": { @@ -549,11 +549,11 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2015-03-13 17:37:22" + "time": "2015-03-30 15:54:10" }, { "name": "symfony/event-dispatcher", - "version": "v2.6.5", + "version": "v2.6.6", "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", @@ -614,17 +614,17 @@ "packages-dev": [ { "name": "symfony/stopwatch", - "version": "v2.6.5", + "version": "v2.6.6", "target-dir": "Symfony/Component/Stopwatch", "source": { "type": "git", "url": "https://github.com/symfony/Stopwatch.git", - "reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5" + "reference": "5f196e84b5640424a166d2ce9cca161ce1e9d912" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/ba4e774f71e2ce3e3f65cabac4031b9029972af5", - "reference": "ba4e774f71e2ce3e3f65cabac4031b9029972af5", + "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/5f196e84b5640424a166d2ce9cca161ce1e9d912", + "reference": "5f196e84b5640424a166d2ce9cca161ce1e9d912", "shasum": "" }, "require": { @@ -660,7 +660,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "http://symfony.com", - "time": "2015-02-24 11:52:21" + "time": "2015-03-22 16:55:57" } ], "aliases": [], @@ -674,7 +674,8 @@ "prefer-stable": false, "platform": { "php": ">=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 00000000..e088a221 --- /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 00000000..ae0b2627 --- /dev/null +++ b/scripts/create-sample-burndown.php @@ -0,0 +1,55 @@ +#!/usr/bin/env php +<?php + +require __DIR__.'/../app/common.php'; + +use Model\ProjectDailySummary; +use Model\TaskCreation; +use Model\TaskStatus; + +$pds = new ProjectDailySummary($container); +$taskCreation = new TaskCreation($container); +$taskStatus = new TaskStatus($container); + +for ($i = 1; $i <= 15; $i++) { + + $task = array( + 'title' => '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 |