Add new analytic component "Estimated vs actual time per column"
This commit is contained in:
parent
6cadf82a63
commit
a267aa368b
|
|
@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use Pimple\Container;
|
|||
* @property \Kanboard\Analytic\EstimatedTimeComparisonAnalytic $estimatedTimeComparisonAnalytic
|
||||
* @property \Kanboard\Analytic\AverageLeadCycleTimeAnalytic $averageLeadCycleTimeAnalytic
|
||||
* @property \Kanboard\Analytic\AverageTimeSpentColumnAnalytic $averageTimeSpentColumnAnalytic
|
||||
* @property \Kanboard\Analytic\EstimatedActualColumnAnalytic $estimatedActualColumnAnalytic
|
||||
* @property \Kanboard\Core\Action\ActionManager $actionManager
|
||||
* @property \Kanboard\Core\ExternalLink\ExternalLinkManager $externalLinkManager
|
||||
* @property \Kanboard\Core\ExternalTask\ExternalTaskManager $externalTaskManager
|
||||
|
|
|
|||
|
|
@ -77,6 +77,18 @@ class DateHelper extends Base
|
|||
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.
|
||||
* It's in this format: <1h , NNh, NNd
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
|
|||
'EstimatedTimeComparisonAnalytic',
|
||||
'AverageLeadCycleTimeAnalytic',
|
||||
'AverageTimeSpentColumnAnalytic',
|
||||
'EstimatedActualColumnAnalytic',
|
||||
),
|
||||
'Model' => array(
|
||||
'ActionModel',
|
||||
|
|
|
|||
|
|
@ -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') ?>>
|
||||
<?= $this->modal->replaceLink(t('Estimated vs actual time'), 'AnalyticController', 'timeComparison', array('project_id' => $project['id'])) ?>
|
||||
</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)) ?>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
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.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: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])}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue