Add cumulative flow diagram
This commit is contained in:
parent
4494566fc7
commit
8bf50d6a7f
|
|
@ -81,4 +81,48 @@ class Analytic extends Base
|
|||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show cumulative flow diagram
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function cfd()
|
||||
{
|
||||
$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(
|
||||
'columns' => array_values($this->board->getColumnsList($project['id'])),
|
||||
'metrics' => $this->projectDailySummary->getRawMetrics($project['id'], $from, $to),
|
||||
'labels' => array(
|
||||
'column' => t('Column'),
|
||||
'day' => t('Date'),
|
||||
'total' => t('Tasks'),
|
||||
)
|
||||
));
|
||||
}
|
||||
else {
|
||||
$this->response->html($this->layout('analytic/cfd', 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('Cumulative flow diagram for "%s"', $project['name']),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,9 @@ abstract class Base
|
|||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
// $this->container['logger']->addDebug(var_export($this->container['db']->getLogMessages(), true));
|
||||
// foreach ($this->container['db']->getLogMessages() as $message) {
|
||||
// $this->container['logger']->addDebug($message);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,6 +175,7 @@ abstract class Base
|
|||
{
|
||||
$models = array(
|
||||
'projectActivity', // Order is important
|
||||
'projectDailySummary',
|
||||
'action',
|
||||
'project',
|
||||
'webhook',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Event;
|
||||
|
||||
/**
|
||||
* Project daily summary listener
|
||||
*
|
||||
* @package event
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectDailySummaryListener extends Base
|
||||
{
|
||||
/**
|
||||
* Execute the action
|
||||
*
|
||||
* @access public
|
||||
* @param array $data Event data dictionary
|
||||
* @return bool True if the action was executed or false when not executed
|
||||
*/
|
||||
public function execute(array $data)
|
||||
{
|
||||
if (isset($data['project_id'])) {
|
||||
return $this->projectDailySummary->updateTotals($data['project_id'], date('Y-m-d'));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ return array(
|
|||
'Column removed successfully.' => 'Colonne supprimée avec succès.',
|
||||
'Edit Project' => 'Modifier le projet',
|
||||
'Github Issue' => 'Ticket Github',
|
||||
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique',
|
||||
'Not enough data to show the graph.' => 'Pas assez de données pour afficher le graphique.',
|
||||
'Previous' => 'Précédent',
|
||||
'The id must be an integer' => 'L\'id doit être un entier',
|
||||
'The project id must be an integer' => 'L\'id du projet doit être un entier',
|
||||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
'This value is required' => 'Cette valeur est obligatoire',
|
||||
'This value must be numeric' => 'Cette valeur doit être numérique',
|
||||
'Unable to create this task.' => 'Impossible de créer cette tâche',
|
||||
'Cumulative flow diagram' => 'Diagramme de flux cumulé',
|
||||
'Cumulative flow diagram for "%s"' => 'Diagramme de flux cumulé pour « %s »',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -592,4 +592,6 @@ return array(
|
|||
// 'This value is required' => '',
|
||||
// 'This value must be numeric' => '',
|
||||
// 'Unable to create this task.' => '',
|
||||
// 'Cumulative flow diagram' => '',
|
||||
// 'Cumulative flow diagram for "%s"' => '',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace Model;
|
||||
|
||||
use Core\Template;
|
||||
use Event\ProjectDailySummaryListener;
|
||||
|
||||
/**
|
||||
* Project daily summary
|
||||
*
|
||||
* @package model
|
||||
* @author Frederic Guillot
|
||||
*/
|
||||
class ProjectDailySummary extends Base
|
||||
{
|
||||
/**
|
||||
* SQL table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const TABLE = 'project_daily_summaries';
|
||||
|
||||
/**
|
||||
* Update daily totals for the project
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $date Record date (YYYY-MM-DD)
|
||||
* @return boolean
|
||||
*/
|
||||
public function updateTotals($project_id, $date)
|
||||
{
|
||||
return $this->db->transaction(function($db) use ($project_id, $date) {
|
||||
|
||||
$column_ids = $db->table(Board::TABLE)->eq('project_id', $project_id)->findAllByColumn('id');
|
||||
|
||||
foreach ($column_ids as $column_id) {
|
||||
|
||||
// This call will fail if the record already exists
|
||||
// (cross database driver hack for INSERT..ON DUPLICATE KEY UPDATE)
|
||||
$db->table(ProjectDailySummary::TABLE)->insert(array(
|
||||
'day' => $date,
|
||||
'project_id' => $project_id,
|
||||
'column_id' => $column_id,
|
||||
'total' => 0,
|
||||
));
|
||||
|
||||
$db->table(ProjectDailySummary::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('column_id', $column_id)
|
||||
->eq('day', $date)
|
||||
->update(array(
|
||||
'total' => $db->table(Task::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->eq('column_id', $column_id)
|
||||
->eq('is_active', Task::STATUS_OPEN)
|
||||
->count()
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of recorded days for the data range
|
||||
*
|
||||
* @access public
|
||||
* @param integer $project_id Project id
|
||||
* @param string $from Start date (ISO format YYYY-MM-DD)
|
||||
* @param string $to End date
|
||||
* @return integer
|
||||
*/
|
||||
public function countDays($project_id, $from, $to)
|
||||
{
|
||||
$rq = $this->db->execute(
|
||||
'SELECT COUNT(DISTINCT day) FROM '.self::TABLE.' WHERE day >= ? AND day <= ?',
|
||||
array($from, $to)
|
||||
);
|
||||
|
||||
return $rq->fetchColumn(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw metrics for the project within a data range
|
||||
*
|
||||
* @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 getRawMetrics($project_id, $from, $to)
|
||||
{
|
||||
return $this->db->table(ProjectDailySummary::TABLE)
|
||||
->columns(
|
||||
ProjectDailySummary::TABLE.'.column_id',
|
||||
ProjectDailySummary::TABLE.'.day',
|
||||
ProjectDailySummary::TABLE.'.total',
|
||||
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)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated metrics for the project within a data range
|
||||
*
|
||||
* [
|
||||
* ['Date', 'Column1', 'Column2'],
|
||||
* ['2014-11-16', 2, 5],
|
||||
* ['2014-11-17', 20, 15],
|
||||
* ]
|
||||
*
|
||||
* @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 getAggregatedMetrics($project_id, $from, $to)
|
||||
{
|
||||
$columns = $this->board->getColumnsList($project_id);
|
||||
$column_ids = array_keys($columns);
|
||||
$metrics = array(array(e('Date')) + $columns);
|
||||
$aggregates = array();
|
||||
|
||||
// Fetch metrics for the project
|
||||
$records = $this->db->table(ProjectDailySummary::TABLE)
|
||||
->eq('project_id', $project_id)
|
||||
->gte('day', $from)
|
||||
->lte('day', $to)
|
||||
->findAll();
|
||||
|
||||
// Aggregate by day
|
||||
foreach ($records as $record) {
|
||||
|
||||
if (! isset($aggregates[$record['day']])) {
|
||||
$aggregates[$record['day']] = array($record['day']);
|
||||
}
|
||||
|
||||
$aggregates[$record['day']][$record['column_id']] = $record['total'];
|
||||
}
|
||||
|
||||
// Aggregate by row
|
||||
foreach ($aggregates as $aggregate) {
|
||||
|
||||
$row = array($aggregate[0]);
|
||||
|
||||
foreach ($column_ids as $column_id) {
|
||||
$row[] = (int) $aggregate[$column_id];
|
||||
}
|
||||
|
||||
$metrics[] = $row;
|
||||
}
|
||||
|
||||
return $metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach events to be able to record the metrics
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
public function attachEvents()
|
||||
{
|
||||
$events = array(
|
||||
Task::EVENT_CREATE,
|
||||
Task::EVENT_CLOSE,
|
||||
Task::EVENT_OPEN,
|
||||
Task::EVENT_MOVE_COLUMN,
|
||||
);
|
||||
|
||||
$listener = new ProjectDailySummaryListener($this->container);
|
||||
|
||||
foreach ($events as $event_name) {
|
||||
$this->event->attach($event_name, $listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -309,8 +309,6 @@ class Task extends Base
|
|||
*/
|
||||
private function savePositions($moved_task_id, array $columns)
|
||||
{
|
||||
$this->db->startTransaction();
|
||||
|
||||
foreach ($columns as $column_id => $column) {
|
||||
|
||||
$position = 1;
|
||||
|
|
@ -336,14 +334,11 @@ class Task extends Base
|
|||
$position++;
|
||||
|
||||
if (! $result) {
|
||||
$this->db->cancelTransaction();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->closeTransaction();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,25 @@ namespace Schema;
|
|||
use PDO;
|
||||
use Core\Security;
|
||||
|
||||
const VERSION = 34;
|
||||
const VERSION = 35;
|
||||
|
||||
function version_35($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_summaries (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
day CHAR(10) NOT NULL,
|
||||
project_id INT NOT NULL,
|
||||
column_id INT NOT NULL,
|
||||
total INT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(id),
|
||||
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB CHARSET=utf8
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||
}
|
||||
|
||||
function version_34($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,24 @@ namespace Schema;
|
|||
use PDO;
|
||||
use Core\Security;
|
||||
|
||||
const VERSION = 15;
|
||||
const VERSION = 16;
|
||||
|
||||
function version_16($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_summaries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
day CHAR(10) NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
column_id INTEGER NOT NULL,
|
||||
total INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||
}
|
||||
|
||||
function version_15($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,24 @@ namespace Schema;
|
|||
use Core\Security;
|
||||
use PDO;
|
||||
|
||||
const VERSION = 34;
|
||||
const VERSION = 35;
|
||||
|
||||
function version_35($pdo)
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE project_daily_summaries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
day TEXT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
column_id INTEGER NOT NULL,
|
||||
total INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
|
||||
$pdo->exec('CREATE UNIQUE INDEX project_daily_column_stats_idx ON project_daily_summaries(day, project_id, column_id)');
|
||||
}
|
||||
|
||||
function version_34($pdo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<div class="page-header">
|
||||
<h2><?= t('Cumulative flow diagram') ?></h2>
|
||||
</div>
|
||||
|
||||
<?php if (! $display_graph): ?>
|
||||
<p class="alert"><?= t('Not enough data to show the graph.') ?></p>
|
||||
<?php else: ?>
|
||||
<section id="analytic-cfd">
|
||||
<div id="chart" data-url="<?= Helper\u('analytic', 'cfd', array('project_id' => $project['id'], 'from' => $values['from'], 'to' => $values['to'])) ?>"></div>
|
||||
</section>
|
||||
<?php endif ?>
|
||||
|
||||
<hr/>
|
||||
|
||||
<form method="post" class="form-inline" action="<?= Helper\u('analytic', 'cfd', array('project_id' => $project['id'])) ?>" autocomplete="off">
|
||||
|
||||
<?= Helper\form_csrf() ?>
|
||||
|
||||
<?= Helper\form_label(t('Start Date'), 'from') ?>
|
||||
<?= Helper\form_text('from', $values, array(), array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
|
||||
<?= Helper\form_label(t('End Date'), 'to') ?>
|
||||
<?= Helper\form_text('to', $values, array(), array('required', 'placeholder="'.Helper\in_list($date_format, $date_formats).'"'), 'form-date') ?>
|
||||
|
||||
<input type="submit" value="<?= t('Execute') ?>" class="btn btn-blue"/>
|
||||
</form>
|
||||
|
|
@ -7,5 +7,8 @@
|
|||
<li>
|
||||
<?= Helper\a(t('User repartition'), 'analytic', 'users', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
<li>
|
||||
<?= Helper\a(t('Cumulative flow diagram'), 'analytic', 'cfd', array('project_id' => $project['id'])) ?>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -33,7 +33,15 @@ ul.no-bullet li {
|
|||
|
||||
.pull-right {
|
||||
text-align: right;
|
||||
}/* links */
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
/* links */
|
||||
a {
|
||||
color: #3366CC;
|
||||
border: none;
|
||||
|
|
@ -280,6 +288,10 @@ ul.form-errors li {
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.form-inline .form-required {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input.form-date {
|
||||
width: 150px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,4 +32,11 @@ ul.no-bullet li {
|
|||
|
||||
.pull-right {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,6 +121,10 @@ ul.form-errors li {
|
|||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.form-inline .form-required {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input.form-date {
|
||||
width: 150px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,64 @@ Kanboard.Analytic = (function() {
|
|||
else if (Kanboard.Exists("analytic-user-repartition")) {
|
||||
Kanboard.Analytic.UserRepartition.Init();
|
||||
}
|
||||
else if (Kanboard.Exists("analytic-cfd")) {
|
||||
Kanboard.Analytic.CFD.Init();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Kanboard.Analytic.CFD = (function() {
|
||||
|
||||
function fetchData()
|
||||
{
|
||||
jQuery.getJSON($("#chart").attr("data-url"), function(data) {
|
||||
drawGraph(data.metrics, data.labels, data.columns);
|
||||
});
|
||||
}
|
||||
|
||||
function drawGraph(metrics, labels, columns)
|
||||
{
|
||||
var series = prepareSeries(metrics, labels);
|
||||
|
||||
var svg = dimple.newSvg("#chart", 800, 380);
|
||||
var chart = new dimple.chart(svg, series);
|
||||
|
||||
var x = chart.addCategoryAxis("x", labels['day']);
|
||||
x.addOrderRule("Date");
|
||||
|
||||
chart.addMeasureAxis("y", labels['total']);
|
||||
|
||||
var s = chart.addSeries(labels['column'], dimple.plot.area);
|
||||
s.addOrderRule(columns.reverse());
|
||||
|
||||
chart.addLegend(10, 10, 500, 30, "left");
|
||||
chart.draw();
|
||||
}
|
||||
|
||||
function prepareSeries(metrics, labels)
|
||||
{
|
||||
var series = [];
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
|
||||
var row = {};
|
||||
row[labels['column']] = metrics[i]['column_title'];
|
||||
row[labels['day']] = metrics[i]['day'];
|
||||
row[labels['total']] = metrics[i]['total'];
|
||||
series.push(row);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
return {
|
||||
Init: fetchData
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Kanboard.Analytic.TaskRepartition = (function() {
|
||||
|
||||
function fetchData()
|
||||
|
|
|
|||
|
|
@ -279,11 +279,64 @@ Kanboard.Analytic = (function() {
|
|||
else if (Kanboard.Exists("analytic-user-repartition")) {
|
||||
Kanboard.Analytic.UserRepartition.Init();
|
||||
}
|
||||
else if (Kanboard.Exists("analytic-cfd")) {
|
||||
Kanboard.Analytic.CFD.Init();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Kanboard.Analytic.CFD = (function() {
|
||||
|
||||
function fetchData()
|
||||
{
|
||||
jQuery.getJSON($("#chart").attr("data-url"), function(data) {
|
||||
drawGraph(data.metrics, data.labels, data.columns);
|
||||
});
|
||||
}
|
||||
|
||||
function drawGraph(metrics, labels, columns)
|
||||
{
|
||||
var series = prepareSeries(metrics, labels);
|
||||
|
||||
var svg = dimple.newSvg("#chart", 800, 380);
|
||||
var chart = new dimple.chart(svg, series);
|
||||
|
||||
var x = chart.addCategoryAxis("x", labels['day']);
|
||||
x.addOrderRule("Date");
|
||||
|
||||
chart.addMeasureAxis("y", labels['total']);
|
||||
|
||||
var s = chart.addSeries(labels['column'], dimple.plot.area);
|
||||
s.addOrderRule(columns.reverse());
|
||||
|
||||
chart.addLegend(10, 10, 500, 30, "left");
|
||||
chart.draw();
|
||||
}
|
||||
|
||||
function prepareSeries(metrics, labels)
|
||||
{
|
||||
var series = [];
|
||||
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
|
||||
var row = {};
|
||||
row[labels['column']] = metrics[i]['column_title'];
|
||||
row[labels['day']] = metrics[i]['day'];
|
||||
row[labels['total']] = metrics[i]['total'];
|
||||
series.push(row);
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
return {
|
||||
Init: fetchData
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
Kanboard.Analytic.TaskRepartition = (function() {
|
||||
|
||||
function fetchData()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require __DIR__.'/../app/common.php';
|
||||
|
||||
use Model\ProjectDailySummary;
|
||||
use Model\Task;
|
||||
|
||||
$pds = new ProjectDailySummary($container);
|
||||
$taskModel = new Task($container);
|
||||
|
||||
for ($i = 1; $i <= 15; $i++) {
|
||||
|
||||
$task = array(
|
||||
'title' => 'Task #'.$i,
|
||||
'project_id' => 1,
|
||||
'column_id' => 1,
|
||||
);
|
||||
|
||||
$taskModel->create($task);
|
||||
}
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-7 days')));
|
||||
|
||||
$taskModel->movePosition(1, 1, 2, 1);
|
||||
$taskModel->movePosition(1, 2, 2, 1);
|
||||
$taskModel->movePosition(1, 3, 2, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-6 days')));
|
||||
|
||||
$taskModel->movePosition(1, 3, 3, 1);
|
||||
$taskModel->movePosition(1, 4, 3, 1);
|
||||
$taskModel->movePosition(1, 5, 3, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-5 days')));
|
||||
|
||||
$taskModel->movePosition(1, 5, 4, 1);
|
||||
$taskModel->movePosition(1, 6, 4, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-4 days')));
|
||||
|
||||
$taskModel->movePosition(1, 7, 4, 1);
|
||||
$taskModel->movePosition(1, 8, 4, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-3 days')));
|
||||
|
||||
$taskModel->movePosition(1, 9, 3, 1);
|
||||
$taskModel->movePosition(1, 10, 2, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||
|
||||
$taskModel->create(array('title' => 'Random task', 'project_id' => 1));
|
||||
$taskModel->movePosition(1, 11, 2, 1);
|
||||
$taskModel->movePosition(1, 12, 4, 1);
|
||||
$taskModel->movePosition(1, 13, 4, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-2 days')));
|
||||
|
||||
$taskModel->movePosition(1, 14, 3, 1);
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-1 days')));
|
||||
|
||||
$taskModel->movePosition(1, 15, 4, 1);
|
||||
$taskModel->movePosition(1, 16, 4, 1);
|
||||
|
||||
$taskModel->create(array('title' => 'Random task', 'project_id' => 1));
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d'));
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
require_once __DIR__.'/Base.php';
|
||||
|
||||
use Model\Project;
|
||||
use Model\ProjectDailySummary;
|
||||
use Model\Task;
|
||||
|
||||
class ProjectDailySummaryTest extends Base
|
||||
{
|
||||
public function testUpdateTotals()
|
||||
{
|
||||
$p = new Project($this->container);
|
||||
$pds = new ProjectDailySummary($this->container);
|
||||
$t = new Task($this->container);
|
||||
|
||||
$this->assertEquals(1, $p->create(array('name' => 'UnitTest')));
|
||||
$this->assertEquals(0, $pds->countDays(1, date('Y-m-d', strtotime('-2days')), date('Y-m-d')));
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 1)));
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 4)));
|
||||
}
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-2days')));
|
||||
|
||||
for ($i = 0; $i < 15; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 3)));
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 25; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 2)));
|
||||
}
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d', strtotime('-1 day')));
|
||||
|
||||
$this->assertNotFalse($t->close(1));
|
||||
$this->assertNotFalse($t->close(2));
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 3)));
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 2)));
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$this->assertNotFalse($t->create(array('title' => 'Task #'.$i, 'project_id' => 1, 'column_id' => 4)));
|
||||
}
|
||||
|
||||
$pds->updateTotals(1, date('Y-m-d'));
|
||||
|
||||
$this->assertEquals(3, $pds->countDays(3, date('Y-m-d', strtotime('-2days')), date('Y-m-d')));
|
||||
$metrics = $pds->getAggregatedMetrics(1, date('Y-m-d', strtotime('-2days')), date('Y-m-d'));
|
||||
|
||||
$this->assertNotEmpty($metrics);
|
||||
$this->assertEquals(4, count($metrics));
|
||||
$this->assertEquals(5, count($metrics[0]));
|
||||
$this->assertEquals('Backlog', $metrics[0][1]);
|
||||
|
||||
$this->assertEquals(date('Y-m-d', strtotime('-2days')), $metrics[1][0]);
|
||||
$this->assertEquals(10, $metrics[1][1]);
|
||||
$this->assertEquals(0, $metrics[1][2]);
|
||||
$this->assertEquals(0, $metrics[1][3]);
|
||||
$this->assertEquals(5, $metrics[1][4]);
|
||||
|
||||
$this->assertEquals(date('Y-m-d', strtotime('-1day')), $metrics[2][0]);
|
||||
$this->assertEquals(10, $metrics[2][1]);
|
||||
$this->assertEquals(25, $metrics[2][2]);
|
||||
$this->assertEquals(15, $metrics[2][3]);
|
||||
$this->assertEquals(5, $metrics[2][4]);
|
||||
|
||||
$this->assertEquals(date('Y-m-d'), $metrics[3][0]);
|
||||
$this->assertEquals(8, $metrics[3][1]);
|
||||
$this->assertEquals(30, $metrics[3][2]);
|
||||
$this->assertEquals(18, $metrics[3][3]);
|
||||
$this->assertEquals(9, $metrics[3][4]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue