Add new analytic component "Estimated vs actual time per column"

This commit is contained in:
Patrick Kuijvenhoven 2021-02-22 05:22:45 +01:00 committed by GitHub
parent 6cadf82a63
commit a267aa368b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 151 additions and 1 deletions

View 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;
}
}

View File

@ -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
*

View File

@ -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

View File

@ -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

View File

@ -24,6 +24,7 @@ class ClassProvider implements ServiceProviderInterface
'EstimatedTimeComparisonAnalytic',
'AverageLeadCycleTimeAnalytic',
'AverageTimeSpentColumnAnalytic',
'EstimatedActualColumnAnalytic',
),
'Model' => array(
'ActionModel',

View 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 ?>

View File

@ -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>

View File

@ -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])}

View File

@ -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
}
});
};
});