Add new analytic component "Estimated vs actual time per column"
This commit is contained in:
committed by
GitHub
parent
6cadf82a63
commit
a267aa368b
49
app/Analytic/EstimatedActualColumnAnalytic.php
Normal file
49
app/Analytic/EstimatedActualColumnAnalytic.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kanboard\Analytic;
|
||||||
|
|
||||||
|
use Kanboard\Core\Base;
|
||||||
|
use Kanboard\Model\TaskModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated vs actual time per column
|
||||||
|
*
|
||||||
|
* @package analytic
|
||||||
|
* @author Frederic Guillot
|
||||||
|
*/
|
||||||
|
class EstimatedActualColumnAnalytic extends Base
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Build report
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param integer $project_id Project id
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function build($project_id)
|
||||||
|
{
|
||||||
|
$rows = $this->db->table(TaskModel::TABLE)
|
||||||
|
->columns('SUM(time_estimated) AS hours_estimated', 'SUM(time_spent) AS hours_spent', 'column_id')
|
||||||
|
->eq('project_id', $project_id)
|
||||||
|
->groupBy('column_id')
|
||||||
|
->findAll();
|
||||||
|
|
||||||
|
$columns = $this->columnModel->getList($project_id);
|
||||||
|
|
||||||
|
$metrics = [];
|
||||||
|
foreach ($columns as $column_id => $column_title) {
|
||||||
|
$metrics[$column_id] = array(
|
||||||
|
'hours_spent' => 0,
|
||||||
|
'hours_estimated' => 0,
|
||||||
|
'title' => $column_title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$metrics[$row['column_id']]['hours_spent'] = (float) $row['hours_spent'];
|
||||||
|
$metrics[$row['column_id']]['hours_estimated'] = (float) $row['hours_estimated'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metrics;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,6 +130,22 @@ class AnalyticController extends BaseController
|
|||||||
$this->commonAggregateMetrics('analytic/burndown', 'score', t('Burndown chart'));
|
$this->commonAggregateMetrics('analytic/burndown', 'score', t('Burndown chart'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimated vs actual time per column
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
*/
|
||||||
|
public function estimatedVsActualByColumn()
|
||||||
|
{
|
||||||
|
$project = $this->getProject();
|
||||||
|
|
||||||
|
$this->response->html($this->helper->layout->analytic('analytic/estimated_actual_column', array(
|
||||||
|
'project' => $project,
|
||||||
|
'metrics' => $this->estimatedActualColumnAnalytic->build($project['id']),
|
||||||
|
'title' => t('Estimated vs actual time per column'),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common method for CFD and Burdown chart
|
* Common method for CFD and Burdown chart
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use Pimple\Container;
|
|||||||
* @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
|
* @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
|
||||||
* @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
|
* @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
|
||||||
* @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
|
* @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
|
||||||
|
* @property \Kanboard\Analytic\EstimatedActualColumnAnalytic $estimatedActualColumnAnalytic
|
||||||
* @property \Kanboard\Core\Action\ActionManager $actionManager
|
* @property \Kanboard\Core\Action\ActionManager $actionManager
|
||||||
* @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
|
* @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
|
||||||
* @property \Kanboard\Core\ExternalTask\ExternalTaskManager $externalTaskManager
|
* @property \Kanboard\Core\ExternalTask\ExternalTaskManager $externalTaskManager
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ class DateHelper extends Base
|
|||||||
return $dtF->diff($dtT)->format($format);
|
return $dtF->diff($dtT)->format($format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get duration in hours into human format
|
||||||
|
*
|
||||||
|
* @access public
|
||||||
|
* @param float $hours
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function durationHours($hours)
|
||||||
|
{
|
||||||
|
return sprintf('%0.2f %s', round($hours, 2), t('hours'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the age of an item in quasi human readable format.
|
* Get the age of an item in quasi human readable format.
|
||||||
* It's in this format: <1h , NNh, NNd
|
* It's in this format: <1h , NNh, NNd
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
|
|||||||
'EstimatedTimeComparisonAnalytic',
|
'EstimatedTimeComparisonAnalytic',
|
||||||
'AverageLeadCycleTimeAnalytic',
|
'AverageLeadCycleTimeAnalytic',
|
||||||
'AverageTimeSpentColumnAnalytic',
|
'AverageTimeSpentColumnAnalytic',
|
||||||
|
'EstimatedActualColumnAnalytic',
|
||||||
),
|
),
|
||||||
'Model' => array(
|
'Model' => array(
|
||||||
'ActionModel',
|
'ActionModel',
|
||||||
|
|||||||
30
app/Template/analytic/estimated_actual_column.php
Normal file
30
app/Template/analytic/estimated_actual_column.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php if (! $is_ajax): ?>
|
||||||
|
<div class="page-header">
|
||||||
|
<h2><?= t('Estimated vs actual time per column') ?></h2>
|
||||||
|
</div>
|
||||||
|
<?php endif ?>
|
||||||
|
|
||||||
|
<?php if (empty($metrics)): ?>
|
||||||
|
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= $this->app->component('chart-project-estimated-actual-column', array(
|
||||||
|
'metrics' => $metrics,
|
||||||
|
'labelSpent' => t('Hours Spent'),
|
||||||
|
'labelEstimated' => t('Hours Estimated'),
|
||||||
|
)) ?>
|
||||||
|
|
||||||
|
<table class="table-striped">
|
||||||
|
<tr>
|
||||||
|
<th><?= t('Column') ?></th>
|
||||||
|
<th><?= t('Hours Spent') ?></th>
|
||||||
|
<th><?= t('Hours Estimated') ?></th>
|
||||||
|
</tr>
|
||||||
|
<?php foreach ($metrics as $column): ?>
|
||||||
|
<tr>
|
||||||
|
<td><?= $this->text->e($column['title']) ?></td>
|
||||||
|
<td><?= $this->dt->durationHours($column['hours_spent']) ?></td>
|
||||||
|
<td><?= $this->dt->durationHours($column['hours_estimated']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach ?>
|
||||||
|
</table>
|
||||||
|
<?php endif ?>
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
<li <?= $this->app->checkMenuSelection('AnalyticController', 'timeComparison') ?>>
|
<li <?= $this->app->checkMenuSelection('AnalyticController', 'timeComparison') ?>>
|
||||||
<?= $this->modal->replaceLink(t('Estimated vs actual time'), 'AnalyticController', 'timeComparison', array('project_id' => $project['id'])) ?>
|
<?= $this->modal->replaceLink(t('Estimated vs actual time'), 'AnalyticController', 'timeComparison', array('project_id' => $project['id'])) ?>
|
||||||
</li>
|
</li>
|
||||||
|
<li <?= $this->app->checkMenuSelection('AnalyticController', 'estimatedVsActualByColumn') ?>>
|
||||||
|
<?= $this->modal->replaceLink(t('Estimated vs actual time per column'), 'AnalyticController', 'estimatedVsActualByColumn', array('project_id' => $project['id'])) ?>
|
||||||
|
</li>
|
||||||
|
|
||||||
<?= $this->hook->render('template:analytic:sidebar', array('project' => $project)) ?>
|
<?= $this->hook->render('template:analytic:sidebar', array('project' => $project)) ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
3
assets/js/app.min.js
vendored
3
assets/js/app.min.js
vendored
@@ -84,7 +84,8 @@ KB.onClick('.task-board *',redirectToTaskView,!0);KB.onClick('.task-board-change
|
|||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[plots],type:'bar'},bar:{width:{ratio:0.5}},axis:{x:{type:'category',categories:categories},y:{tick:{format:KB.utils.formatDuration}}},legend:{show:!1}})}});KB.component('chart-project-burndown',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[[options.labelTotal]];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i<metrics.length;i++){for(var j=0;j<metrics[i].length;j++){var currentValue=metrics[i][j];if(i===0){if(j>0){columns.push([currentValue])}}else{if(j>0){columns[j].push(currentValue);if(typeof columns[0][i]==='undefined'){columns[0].push(0)}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[plots],type:'bar'},bar:{width:{ratio:0.5}},axis:{x:{type:'category',categories:categories},y:{tick:{format:KB.utils.formatDuration}}},legend:{show:!1}})}});KB.component('chart-project-burndown',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[[options.labelTotal]];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i<metrics.length;i++){for(var j=0;j<metrics[i].length;j++){var currentValue=metrics[i][j];if(i===0){if(j>0){columns.push([currentValue])}}else{if(j>0){columns[j].push(currentValue);if(typeof columns[0][i]==='undefined'){columns[0].push(0)}
|
||||||
columns[0][i]+=currentValue}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}}
|
columns[0][i]+=currentValue}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}}
|
||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-cumulative-flow',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[];var groups=[];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i<metrics.length;i++){for(var j=0;j<metrics[i].length;j++){var currentValue=metrics[i][j];if(i===0){if(j>0){groups.push(currentValue);columns.push([currentValue])}}else{if(j>0){columns[j-1].push(currentValue)}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-cumulative-flow',function(containerElement,options){this.render=function(){var metrics=options.metrics;var columns=[];var groups=[];var categories=[];var inputFormat=d3.time.format("%Y-%m-%d");var outputFormat=d3.time.format(options.dateFormat);for(var i=0;i<metrics.length;i++){for(var j=0;j<metrics[i].length;j++){var currentValue=metrics[i][j];if(i===0){if(j>0){groups.push(currentValue);columns.push([currentValue])}}else{if(j>0){columns[j-1].push(currentValue)}else{categories.push(outputFormat(inputFormat.parse(currentValue)))}}}}
|
||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns.reverse(),type:'area-spline',groups:[groups],order:null},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-lead-cycle-time',function(containerElement,options){this.render=function(){var metrics=options.metrics;var cycle=[options.labelCycle];var lead=[options.labelLead];var categories=[];var types={};types[options.labelCycle]='area';types[options.labelLead]='area-spline';var colors={};colors[options.labelLead]='#afb42b';colors[options.labelCycle]='#4e342e';for(var i=0;i<metrics.length;i++){cycle.push(parseInt(metrics[i].avg_cycle_time));lead.push(parseInt(metrics[i].avg_lead_time));categories.push(metrics[i].day)}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns.reverse(),type:'area-spline',groups:[groups],order:null},axis:{x:{type:'category',categories:categories}}})}});KB.component('chart-project-estimated-actual-column',function(containerElement,options){this.render=function(){var spent=[options.labelSpent];var estimated=[options.labelEstimated];var columns=[];for(var column in options.metrics){spent.push(options.metrics[column].hours_spent);estimated.push(options.metrics[column].hours_estimated);columns.push(options.metrics[column].title)}
|
||||||
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[spent,estimated],type:'bar'},bar:{width:{ratio:0.2}},axis:{x:{type:'category',categories:columns}},legend:{show:!0}})}});KB.component('chart-project-lead-cycle-time',function(containerElement,options){this.render=function(){var metrics=options.metrics;var cycle=[options.labelCycle];var lead=[options.labelLead];var categories=[];var types={};types[options.labelCycle]='area';types[options.labelLead]='area-spline';var colors={};colors[options.labelLead]='#afb42b';colors[options.labelCycle]='#4e342e';for(var i=0;i<metrics.length;i++){cycle.push(parseInt(metrics[i].avg_cycle_time));lead.push(parseInt(metrics[i].avg_lead_time));categories.push(metrics[i].day)}
|
||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[lead,cycle],types:types,colors:colors},axis:{x:{type:'category',categories:categories},y:{tick:{format:KB.utils.formatDuration}}}})}});KB.component('chart-project-task-distribution',function(containerElement,options){this.render=function(){var columns=[];for(var i=0;i<options.metrics.length;i++){columns.push([options.metrics[i].column_title,options.metrics[i].nb_tasks])}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[lead,cycle],types:types,colors:colors},axis:{x:{type:'category',categories:categories},y:{tick:{format:KB.utils.formatDuration}}}})}});KB.component('chart-project-task-distribution',function(containerElement,options){this.render=function(){var columns=[];for(var i=0;i<options.metrics.length;i++){columns.push([options.metrics[i].column_title,options.metrics[i].nb_tasks])}
|
||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns,type:'donut'}})}});KB.component('chart-project-time-comparison',function(containerElement,options){this.render=function(){var spent=[options.labelSpent];var estimated=[options.labelEstimated];var categories=[];for(var status in options.metrics){spent.push(options.metrics[status].time_spent);estimated.push(options.metrics[status].time_estimated);categories.push(status==='open'?options.labelOpen:options.labelClosed)}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:columns,type:'donut'}})}});KB.component('chart-project-time-comparison',function(containerElement,options){this.render=function(){var spent=[options.labelSpent];var estimated=[options.labelEstimated];var categories=[];for(var status in options.metrics){spent.push(options.metrics[status].time_spent);estimated.push(options.metrics[status].time_estimated);categories.push(status==='open'?options.labelOpen:options.labelClosed)}
|
||||||
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[spent,estimated],type:'bar'},bar:{width:{ratio:0.2}},axis:{x:{type:'category',categories:categories}},legend:{show:!0}})}});KB.component('chart-project-user-distribution',function(containerElement,options){this.render=function(){var columns=[];for(var i=0;i<options.metrics.length;i++){columns.push([options.metrics[i].user,options.metrics[i].nb_tasks])}
|
KB.dom(containerElement).add(KB.dom('div').attr('id','chart').build());c3.generate({data:{columns:[spent,estimated],type:'bar'},bar:{width:{ratio:0.2}},axis:{x:{type:'category',categories:categories}},legend:{show:!0}})}});KB.component('chart-project-user-distribution',function(containerElement,options){this.render=function(){var columns=[];for(var i=0;i<options.metrics.length;i++){columns.push([options.metrics[i].user,options.metrics[i].nb_tasks])}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
KB.component('chart-project-estimated-actual-column', function (containerElement, options) {
|
||||||
|
|
||||||
|
this.render = function () {
|
||||||
|
var spent = [options.labelSpent];
|
||||||
|
var estimated = [options.labelEstimated];
|
||||||
|
var columns = [];
|
||||||
|
|
||||||
|
for (var column in options.metrics) {
|
||||||
|
spent.push(options.metrics[column].hours_spent);
|
||||||
|
estimated.push(options.metrics[column].hours_estimated);
|
||||||
|
columns.push(options.metrics[column].title);
|
||||||
|
}
|
||||||
|
|
||||||
|
KB.dom(containerElement).add(KB.dom('div').attr('id', 'chart').build());
|
||||||
|
|
||||||
|
c3.generate({
|
||||||
|
data: {
|
||||||
|
columns: [spent, estimated],
|
||||||
|
type: 'bar'
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
width: {
|
||||||
|
ratio: 0.2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axis: {
|
||||||
|
x: {
|
||||||
|
type: 'category',
|
||||||
|
categories: columns
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user