Creando un dashboard en una home page para un rol
El resultado final de la home queda así:
Y haciendo un drill-down sobre la primera columna:
Para generar los gráficos, usamos la librería Highcharts.
La estrategia es la siguiente:
1) Creamos una home page dinamica y le asignamos un rol. En la home page, incluimos el CSS y los javascript necesarios para que funcione el Higcharts.
2) Creamos un javascript especifico para esta home page, que cuando este listo el DOM traerá los datos via ajax.
3) Creamos un presentation, donde se implementan los métodos de ajax que llama el javascript en la parte 2)
Home Page Dinámica:
<?php
if(!class_exists('home_terminales'))
{
class home_terminales {
public function Render($context)
{
global $sess;
$html = '
<script type="text/javascript" src="'.WEB_PATH.'/common/Highcharts-2.1.9/js/highcharts.js"></script>
<script type="text/javascript" src="'.WEB_PATH.'/includes/homepage/comp/home_terminales.js"></script>
<style>
.botones {text-align:left; width:950px;margin:0 auto;}
#phome {text-align:left; width:950px; margin:0 auto; min-height:800px;}
#shortcuts {display:none;}
#graf_eventos {width:950px;height:320px;margin-top:10px;}
#graf_sitios {width:950px;height:320px;;margin-top:10px;}
#botones {border:solid 1px #ddd; border-radius:5px; padding:10px;text-align:right;height:50px;}
#navegador {font: 18px Helvetica,Arial;font-weight:bold;float:left;}
#fechas, #totales {font: 12px Helvetica,Arial;float:left;clear:left;}
#volver {display:none;}
#cambio_fecha {display:inline;}
</style>
<style media="print">
#botones button, #volver, #main {display:none;}
</style>
<div class="botones">
</div>
<div id="phome">
<div id="botones">
<div id="navegador"></div>
<div id="fechas"></div>
<div id="totales"></div>
<div id="cambio_fecha">
<button onclick="setDias(1)">Dia</button>
<button onclick="setDias(7)">Semana</button>
<button onclick="setDias(15)">Quincena</button>
<button onclick="setDias(30)">Mes</button>
<button onclick="setDias(180)">Semestre</button>
</div>
<button class="btn_imprimir" onclick="print()">Imprimir</button>
<button id="volver" onclick="goHome()">Volver atrás</button>
</div>
<div id="graf_eventos"></div>
<div id="graf_sitios"></div>
</div>
';
$content["home_terminales"] = $html;
$content["upload"] = "";
return array( $content, array() );
}
}
}
Como se ve, la home page es sencilla. Sigue la convención de estar sobre /includes/homepage/comp y el archivo llamarse como la clase que implementa, en este caso, 'home_terminales.php'
Luego vamos a saltar al presentation, quien se encarga de buscar los datos que luego se usan para hacer los gráficos. La misión es bien especifica, generar un objeto JSON con los datasets.
<?php
include_once "common/cdatatypes.php";
class CDH_HOME_TERMINALES_AJAX extends CDataHandler
{
function __construct($parent)
{
parent::__construct($parent);
}
function render_eventos($p) {
global $terminales_db;
$total = 0;
$bars = array();
$sql = "select count(*), organismo from log l join terminales t on l.ip=t.ip where fecha>=(NOW() - INTERVAL {$p} DAY) group by organismo order by 1 desc";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$bars[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
$total+=intval($row[0]);
}
$pie = array();
$sql = "select count(*), sitio from log l join terminales t on l.ip=t.ip where fecha>=(NOW() - INTERVAL {$p} DAY) group by sitio order by 1";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$pie[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
}
return json_encode((object) Array("bars"=>$bars, "pie"=>$pie, "total"=>$total));
}
function render_eventos_tiempo($p) {
global $terminales_db;
list($dias, $terminal) = explode("|",$p);
$total=0;
$spline = array();
$sql = "select count(*), UNIX_TIMESTAMP(DATE(fecha)) from log l join terminales t on l.ip=t.ip where t.organismo='{$terminal}' and fecha>=(NOW() - INTERVAL {$dias} DAY) group by TO_DAYS(fecha) order by 2";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$spline[] = array("cant"=>intval($row[0]), "fecha"=>intval($row[1]));
$total+=intval($row[0]);
}
$pie = array();
$sql = "select count(*), sitio from log l join terminales t on l.ip=t.ip where t.organismo='{$terminal}' and fecha>=(NOW() - INTERVAL {$dias} DAY) group by sitio order by 1";
$rs = $terminales_db->do_execute($sql);
while( $row = $terminales_db->_fetch_row($rs) ) {
$pie[] = array("cant"=>intval($row[0]), "organismo"=>$row[1]);
}
return json_encode((object) Array("spline"=>$spline, "pie"=>$pie, "total"=>$total));
}
}
En este caso, hay dos funciones declaradas, la primera genera el Dashboard inicial, y la segunda genera la vista de drill-down. En este problema en particular, la consulta a la base de datos es trivial. Esa es la parte donde hay que evaluar con mucho cuidado si conviene usar la base transaccional o pensar un esquema de datamart, donde los resultados esten precalculados por otro script que se ejecuta cada X tiempo.
Finalmente, se junta una cosa con otra mediante el javascript que se ejecuta al completar el DOM.
var chart1 = null;
var chart2 = null;
var dias = 7;
var drilling = "";
$(document).ready(function() {
Highcharts.setOptions({
global: {
useUTC: false
}
});
setDias(7);
});
function actualiza_eventos() {
var j = new rem_request(this,function(obj,json){
var jdata = eval("("+json+")");
$("#totales").html("Total eventos: "+jdata.total);
$("#cambio_fecha").show();
chart1=null;
var options = {
chart: {
renderTo: 'graf_eventos',
defaultSeriesType: 'column',
height: 300,
width: 950
},
title: {
text: 'Eventos por terminal'
},
xAxis: {
categories: ['Terminal']
},
yAxis: {
title: {
text: 'Eventos'
}
},
series: [],
credits: { enabled: false },
plotOptions: {
column: {
cursor: 'pointer',
point: { events: {'click':drill_eventos} }
}
}
};
var bars = jdata.bars;
for( var k=0; k<bars.length; k++) {
var n = bars[k].organismo;
var v = bars[k].cant;
options.series.push( {"name":n, "data":[v], dataLabels: {
enabled: true,
rotation: 0,
color: '#444444',
align: 'right',
x: -3,
y: -3,
formatter: function() {
return this.y;
},
style: {
font: 'normal 13px Verdana, sans-serif'
}
} } );
}
chart1 = new Highcharts.Chart(options);
chart2=null;
var options = {
chart: {
renderTo: 'graf_sitios',
defaultSeriesType: 'pie',
height: 300,
width: 950
},
title: {
text: 'Eventos por sitio'
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
formatter: function() {
return '<b>'+ this.point.name +'</b>: '+ Highcharts.numberFormat(this.percentage, 0) +' %';
}
}
}
},
series: [{"name":"pie", "type":"pie", "data":[] }],
credits: { enabled: false },
tooltip: {
formatter: function() {
return Highcharts.numberFormat(this.y, 0)+ " eventos";
}
}
};
var pie = jdata.pie;
for( var k=0; k<pie.length; k++) {
var n = pie[k].organismo;
var v = pie[k].cant;
options.series[0].data.push( {"name":n, "y":v} );
}
chart2 = new Highcharts.Chart(options);
},"HOME_TERMINALES_AJAX","render_eventos",dias);
}
function setDias(cant) {
dias = cant;
chart1=null;
chart2=null;
actualiza_eventos();
if(cant==1) {
$("#navegador").html("Datos de las últimas 24hs.");
var hoy = new Date();
$("#fechas").html( date_to_string(hoy) );
}
if(cant==7) {
$("#navegador").html("Datos de los últimos 7 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-7);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==15) {
$("#navegador").html("Datos de los últimos 15 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-15);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==30) {
$("#navegador").html("Datos de los últimos 30 días.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-30);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
if(cant==180) {
$("#navegador").html("Datos de los últimos 6 meses.");
var hasta = new Date();
var desde = new Date();
desde.setDate(desde.getDate()-180);
$("#fechas").html( date_to_string(desde) + " al " + date_to_string(hasta) );
}
}
function date_to_string( d) {
var e = "DomLunMarMieJueVieSab";
return e.substr(d.getDay()*3, 3) + " " + d.getDate() + "/" + (d.getMonth()+1.0) + "/" + d.getFullYear();
}
function drill_eventos(e) {
drilling = e.point.series.name;
var j = new rem_request(this,function(obj,json){
var jdata = eval("("+json+")");
$("#totales").html("Total eventos: "+jdata.total);
$("#volver").show();
$("#cambio_fecha").hide();
chart1 = null;
var options = {
chart: {
renderTo: 'graf_eventos',
defaultSeriesType: 'spline',
height: 300,
width: 950
},
title: {
text: 'Eventos de terminal '+drilling
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150
},
yAxis: {
title: {text: 'Eventos'},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
series: [{name:"Eventos", data:[] }],
credits: { enabled: false },
tooltip: {
formatter: function() {
return Dia(Highcharts.dateFormat('%a', this.x)) + " " + Highcharts.dateFormat('%d-%m-%Y', this.x) +'<br/>Eventos: '+ Highcharts.numberFormat(this.y, 2);
}
}
};
var spline = jdata.spline;
for( var k=0; k<spline.length; k++) {
var t = spline[k].fecha; //Formato unix timestamp
var v = spline[k].cant;
options.series[0].data.push( {'x':t*1000,'y':v} );
}
chart1 = new Highcharts.Chart(options);
chart2=null;
var options = {
chart: {
renderTo: 'graf_sitios',
defaultSeriesType: 'pie',
height: 300,
width: 950
},
title: {
text: 'Eventos por sitio de ' + drilling
},
plotOptions: {
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
formatter: function() {
return '<b>'+ this.point.name +'</b>: '+ Highcharts.numberFormat(this.percentage, 0) +' %';
}
}
}
},
series: [{"name":"pie", "type":"pie", "data":[] }],
credits: { enabled: false },
tooltip: {
formatter: function() {
return Highcharts.numberFormat(this.y, 0)+" eventos";
}
}
};
var pie = jdata.pie;
for( var k=0; k<pie.length; k++) {
var n = pie[k].organismo;
var v = pie[k].cant;
options.series[0].data.push( {"name":n, "y":v} );
}
chart2 = new Highcharts.Chart(options);
},"HOME_TERMINALES_AJAX","render_eventos_tiempo",dias+"|"+drilling);
}
function Dia(eng) {
var i = "MonTueWedThuFriSatSun";
var e = "LunMarMieJueVieSabDom";
return e.substr(i.indexOf(eng, 0), 3);
}
function goHome() {
$("#volver").hide();
setDias(dias);
}
En este caso, los datos que llegan por JSON son la información pura. Toda la data para formatear los gráficos, se declara en este objeto Javascript.
Recomiendo ver los ejemplos de Highchart sobre la página de la librería y eventualmente cortar y pegar el ejemplo.
http://www.highcharts.com/demo/