Analytics: add the first graph (task repartition)

This commit is contained in:
Frédéric Guillot 2014-11-09 17:59:02 -05:00
parent 3df63e051f
commit e89ba5e9e6
29 changed files with 392 additions and 10 deletions

View File

@ -0,0 +1,56 @@
<?php
namespace Controller;
/**
* Project Anaytic controller
*
* @package controller
* @author Frederic Guillot
*/
class Analytic extends Base
{
/**
* Common layout for analytic views
*
* @access private
* @param string $template Template name
* @param array $params Template parameters
* @return string
*/
private function layout($template, array $params)
{
$params['board_selector'] = $this->projectPermission->getAllowedProjects($this->acl->getUserId());
$params['analytic_content_for_layout'] = $this->template->load($template, $params);
return $this->template->layout('analytic/layout', $params);
}
/**
* Show task distribution graph
*
* @access public
*/
public function repartition()
{
$project = $this->getProject();
$metrics = $this->projectAnalytic->getTaskRepartition($project['id']);
if ($this->request->isAjax()) {
$this->response->json(array(
'metrics' => $metrics,
'labels' => array(
'column_title' => t('Column'),
'nb_tasks' => t('Number of tasks'),
)
));
}
else {
$this->response->html($this->layout('analytic/repartition', array(
'project' => $project,
'metrics' => $metrics,
'title' => t('Task repartition for "%s"', $project['name']),
)));
}
}
}

View File

@ -26,6 +26,7 @@ use Model\LastLogin;
* @property \Model\Notification $notification
* @property \Model\Project $project
* @property \Model\ProjectPermission $projectPermission
* @property \Model\ProjectAnalytic $projectAnalytic
* @property \Model\SubTask $subTask
* @property \Model\Task $task
* @property \Model\TaskHistory $taskHistory

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
'Columns' => 'Colonnes',
'Task' => 'Tâche',
'Your are not member of any project.' => 'Vous n\'êtes membre d\'aucun projet.',
'Percentage' => 'Pourcentage',
'Number of tasks' => 'Nombre de tâches',
'Task distribution' => 'Répartition des tâches',
'Reportings' => 'Rapports',
'Task repartition for "%s"' => 'Répartition des tâches pour « %s »',
'Analytics' => 'Analytique',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -565,4 +565,10 @@ return array(
// 'Columns' => '',
// 'Task' => '',
// 'Your are not member of any project.' => '',
// 'Percentage' => '',
// 'Number of tasks' => '',
// 'Task distribution' => '',
// 'Reportings' => '',
// 'Task repartition for "%s"' => '',
// 'Analytics' => '',
);

View File

@ -41,6 +41,7 @@ class Acl extends Base
'task' => array('show', 'create', 'save', 'edit', 'update', 'close', 'open', 'duplicate', 'remove', 'description', 'move', 'copy', 'time'),
'category' => array('index', 'save', 'edit', 'update', 'confirm', 'remove'),
'action' => array('index', 'event', 'params', 'create', 'confirm', 'remove'),
'analytic' => array('repartition'),
);
/**

View File

@ -0,0 +1,43 @@
<?php
namespace Model;
/**
* Project analytic model
*
* @package model
* @author Frederic Guillot
*/
class ProjectAnalytic extends Base
{
/**
* Get task repartition
*
* @access public
* @param integer $project_id Project id
* @return array
*/
public function getTaskRepartition($project_id)
{
$metrics = array();
$total = 0;
$columns = $this->board->getColumns($project_id);
foreach ($columns as $column) {
$nb_tasks = $this->taskFinder->countByColumnId($project_id, $column['id']);
$total += $nb_tasks;
$metrics[] = array(
'column_title' => $column['title'],
'nb_tasks' => $nb_tasks,
);
}
foreach ($metrics as &$metric) {
$metric['percentage'] = round(($metric['nb_tasks'] * 100) / $total, 2);
}
return $metrics;
}
}

View File

@ -0,0 +1,18 @@
<?= Helper\js('assets/js/d3.v3.4.8.min.js') ?>
<?= Helper\js('assets/js/dimple.v2.1.0.min.js') ?>
<section id="main">
<div class="page-header">
<ul>
<li><i class="fa fa-table fa-fw"></i><?= Helper\a(t('Back to the board'), 'board', 'show', array('project_id' => $project['id'])) ?></li>
</ul>
</div>
<section class="sidebar-container" id="analytic-section">
<?= Helper\template('analytic/sidebar', array('project' => $project)) ?>
<div class="sidebar-content">
<?= $analytic_content_for_layout ?>
</div>
</section>
</section>

View File

@ -0,0 +1,29 @@
<div class="page-header">
<h2><?= t('Task distribution') ?></h2>
</div>
<section id="analytic-repartition">
<div id="chart" data-url="<?= Helper\u('analytic', 'repartition', array('project_id' => $project['id'])) ?>"></div>
<table>
<tr>
<th><?= t('Column') ?></th>
<th><?= t('Number of tasks') ?></th>
<th><?= t('Percentage') ?></th>
</tr>
<?php foreach ($metrics as $metric): ?>
<tr>
<td>
<?= Helper\escape($metric['column_title']) ?>
</td>
<td>
<?= $metric['nb_tasks'] ?>
</td>
<td>
<?= n($metric['percentage']) ?>%
</td>
</tr>
<?php endforeach ?>
</table>
</section>

View File

@ -0,0 +1,8 @@
<div class="sidebar">
<h2><?= t('Reportings') ?></h2>
<ul>
<li>
<?= Helper\a(t('Task distribution'), 'analytic', 'repartition', array('project_id' => $project['id'])) ?>
</li>
</ul>
</div>

View File

@ -1,8 +1,4 @@
<section id="main">
<div class="page-header">
<h2><?= t('Page not found') ?></h2>
</div>
<p class="alert alert-error">
<?= t('Sorry, I didn\'t found this information in my database!') ?>
</p>

View File

@ -24,6 +24,10 @@
<i class="fa fa-dashboard fa-fw"></i>
<?= Helper\a(t('Activity'), 'project', 'activity', array('project_id' => $current_project_id)) ?>
</li>
<li>
<i class="fa fa-line-chart fa-fw"></i>
<?= Helper\a(t('Analytics'), 'analytic', 'repartition', array('project_id' => $current_project_id)) ?>
</li>
<?php if (Helper\is_admin()): ?>
<li><i class="fa fa-cog fa-fw"></i>
<?= Helper\a(t('Configure'), 'project', 'show', array('project_id' => $current_project_id)) ?>

58
assets/js/analytic.js Normal file
View File

@ -0,0 +1,58 @@
Kanboard.Analytic = (function() {
return {
Init: function() {
if (Kanboard.Exists("analytic-repartition")) {
Kanboard.Analytic.Repartition.Init();
}
}
};
})();
Kanboard.Analytic.Repartition = (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", 700, 350);
var chart = new dimple.chart(svg, series);
chart.addMeasureAxis("p", labels["nb_tasks"]);
var ring = chart.addSeries(labels["column_title"], dimple.plot.pie);
ring.innerRadius = "50%";
chart.addLegend(0, 0, 100, 100, "left");
chart.draw();
}
function prepareSeries(metrics, labels)
{
var series = [];
for (var i = 0; i < metrics.length; i++) {
var serie = {};
serie[labels["nb_tasks"]] = metrics[i]["nb_tasks"];
serie[labels["column_title"]] = metrics[i]["column_title"];
series.push(serie);
}
return series;
}
return {
Init: fetchData
};
})();

View File

@ -18,6 +18,15 @@ var Kanboard = (function() {
return {
// Return true if the element#id exists
Exists: function(id) {
if (document.getElementById(id)) {
return true;
}
return false;
},
// Display a popup
Popover: function(e, callback) {
e.preventDefault();
@ -264,15 +273,76 @@ Kanboard.Task = (function() {
}
};
})();// Initialization
})();
Kanboard.Analytic = (function() {
return {
Init: function() {
if (Kanboard.Exists("analytic-repartition")) {
Kanboard.Analytic.Repartition.Init();
}
}
};
})();
Kanboard.Analytic.Repartition = (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", 700, 350);
var chart = new dimple.chart(svg, series);
chart.addMeasureAxis("p", labels["nb_tasks"]);
var ring = chart.addSeries(labels["column_title"], dimple.plot.pie);
ring.innerRadius = "50%";
chart.addLegend(0, 0, 100, 100, "left");
chart.draw();
}
function prepareSeries(metrics, labels)
{
var series = [];
for (var i = 0; i < metrics.length; i++) {
var serie = {};
serie[labels["nb_tasks"]] = metrics[i]["nb_tasks"];
serie[labels["column_title"]] = metrics[i]["column_title"];
series.push(serie);
}
return series;
}
return {
Init: fetchData
};
})();
// Initialization
$(function() {
Kanboard.Init();
if ($("#board").length) {
if (Kanboard.Exists("board")) {
Kanboard.Board.Init();
}
else if ($("#task-section").length) {
else if (Kanboard.Exists("task-section")) {
Kanboard.Task.Init();
}
else if (Kanboard.Exists("analytic-section")) {
Kanboard.Analytic.Init();
}
});

View File

@ -3,6 +3,15 @@ var Kanboard = (function() {
return {
// Return true if the element#id exists
Exists: function(id) {
if (document.getElementById(id)) {
return true;
}
return false;
},
// Display a popup
Popover: function(e, callback) {
e.preventDefault();

5
assets/js/d3.v3.4.8.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
assets/js/dimple.v2.1.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,10 +3,13 @@ $(function() {
Kanboard.Init();
if ($("#board").length) {
if (Kanboard.Exists("board")) {
Kanboard.Board.Init();
}
else if ($("#task-section").length) {
else if (Kanboard.Exists("task-section")) {
Kanboard.Task.Init();
}
else if (Kanboard.Exists("analytic-section")) {
Kanboard.Analytic.Init();
}
});

View File

@ -1,7 +1,7 @@
#!/bin/bash
css="base links title table form button alert header board project task comment subtask markdown listing activity dashboard pagination popover confirm sidebar responsive font-awesome.min jquery-ui-1.10.4.custom chosen.min"
js="jquery-1.11.1.min jquery-ui-1.10.4.custom.min jquery.ui.touch-punch.min chosen.jquery.min base board task init"
js="jquery-1.11.1.min jquery-ui-1.10.4.custom.min jquery.ui.touch-punch.min chosen.jquery.min base board task analytic init"
rm -f assets/css/app.css
echo "/* DO NOT EDIT: auto-generated file */" > assets/css/app.css