d6307d96193756ac095e6687c857623e52184bdd
[ptitvelo/web/www.git] / www / plugins-dist / statistiques / javascript / jquery.tflot.js
1 /**
2 * Librairie tFlot pour jQuery et jQuery.flot
3 * Licence GNU/GPL - Matthieu Marcillaud
4 * Version 1.4.0
5 */
6
7 (function($){
8
9 /**
10 * Des variables a garder globalement
11 *
12 * collections : stockage de l'ensemble de toutes les valeurs de tous les graphs et leurs options
13 * collectionsActives : stockage des series actives
14 * plots : stockage des graphiques
15 * vignettes : stockage des vignettes
16 * idGraph : identifiant unique pour tous les graphs
17 */
18 collections = [];
19 collectionsActives = [];
20 plots = [];
21 vignettes = [];
22 vignettesSelection = [];
23 idGraph = 0;
24
25 /*
26 * Fait un graphique d'un tableau donne
27 * $("table.graph").tFlot();
28 * necessite la librairie "jQuery flot".
29 * http://code.google.com/p/flot/
30 */
31 $.fn.tFlot = function(settings) {
32 var options, flot;
33
34
35 options = {
36 width:'500px',
37 height:'250px',
38 parse:{
39 orientation:'row', // 'column' : tableaux verticaux par defaut...
40 axeOnTitle:false // les coordonnees x d'axe sont donnes dans l'attribut title du <th> et non dans le <th> ?
41 },
42 legendeExterne:false,
43 legendeActions:false, // ne fonctionne qu'avec l'option legende externe
44 modeDate:false, // pour calculer les timestamp automatiquement
45 moyenneGlissante:{
46 show:false, // pour calculer une moyenne glissante automatiquement
47 plage:7 // plage de glissement (nombre impair !)
48 },
49 grille:{weekend:false},
50 infobulle:{
51 show:false,
52 serie_color:false // utiliser comme couleur de fond la même couleur que les lignes du graph
53 },
54 zoom:false,
55 vignette:{
56 show:false,
57 width:'160px',
58 height:'100px'
59 },
60 flot:{
61 legend:{
62 show:true,
63 container:null,
64 labelFormatter:null
65 },
66 bars: {fill:false},
67 yaxis: { min: 0 },
68 selection: { mode: "x" }
69 }
70 }
71 $.extend(true, options, settings);
72
73
74 $(this).each(function(){
75
76 // identifiant unique pour tous les graphs
77 // creer les cadres
78 // .graphique
79 // .graphResult
80 // .graphInfos
81 // .graphLegend
82 // .graphOverview
83 $(this).hide().wrap("<div class='graphique' id='graphique"+idGraph+"'></div>");
84 graphique = $(this).parent();
85 values = parseTable(this, options.parse);
86 $.extend(true, values.options, options.flot);
87
88 graph = $("<div class='graphResult' style='width:" + options.width + ";height:" + options.height + ";'></div>").appendTo(graphique);
89 gInfo = $("<div class='graphInfo'></div>").appendTo(graphique);
90
91 // legende en dehors du dessin ?
92 if (options.legendeExterne) {
93 legend = $("<div class='graphLegend' id='grapLegend"+idGraph+"'></div>").appendTo(gInfo);
94 values.options.legend.container = legend;
95 }
96 // legende avec items clicables pour desactiver certaines series
97 if (options.legendeActions) {
98 values.options.legend.labelFormatter = function(label) {
99 return '<a href="#label">' + label + '</a>';
100 }
101 }
102 // si mode time, on calcule des timestamp
103 // les series sont alors de la forme [[timestamp, valeur],...]
104 // et pas besoin de ticks declare
105 if (options.modeDate) {
106 timestamps = [];
107 // calcul des timestamps
108 $.each(values.options.xaxis.ticks, function(i, val){
109 timestamps.push([val[0], (new Date(val[1])).getTime()]);
110 });
111 // les remettre dans les series
112 $.each(values.series, function(i, val){
113 data = [];
114 $.each(val.data, function (j, d){
115 data.push([timestamps[j][1], d[1]]);
116 });
117 values.series[i].data = data;
118 });
119 // plus besoin du ticks
120 // mais toujours besoin des valeurs completes...
121 values.options.xaxis = $.extend(true, {
122 mode: "time",
123 timeformat: "%d/%m/%y"
124 },
125 values.options.xaxis,
126 {ticks: null}
127 );
128 if (options.grille.weekend) {
129 values.options.grid = { markings: weekendAreas }
130 }
131 if (options.grille.years) {
132 values.options.grid = { markings: yearsArea }
133 }
134 }
135
136 // en cas de moyenne glissante, on la calcule
137 if (options.moyenneGlissante.show) {
138 values.series = moyenneGlissante(values.series, options.moyenneGlissante);
139 }
140
141 // si infobulles, les ajouter
142 if (options.infobulle.show) {
143 $.extend(true, options.infobulle, {date:options.modeDate});
144 infobulle($('#graphique'+idGraph), options.infobulle);
145 $.extend(true, values.options, {
146 grid:{hoverable:true}
147 });
148 }
149
150
151 // dessiner
152 plots[idGraph] = $.plot(graph, values.series, values.options);
153
154 // prevoir les actions sur les labels
155 if (options.legendeActions) {
156 $.extend(values.options, {legend:{container:null, show:false}});
157 actionsLegendes($('#graphique'+idGraph));
158 }
159
160 // ajouter une mini vue si demandee
161 if (options.vignette.show) {
162 $("<div class='graphVignette' id='#graphVignette"+idGraph
163 + "' style='width:" + options.vignette.width + ";height:"
164 + options.vignette.height + ";'></div>").appendTo(gInfo);
165 creerVignette($('#graphique'+idGraph), values.series, values.options, options.vignette);
166 }
167 // autoriser les zoom
168 if (options.zoom) {
169 zoomGraphique($('#graphique'+idGraph));
170 }
171
172 // stocker les valeurs
173 collections.push({id:idGraph, values:values}); // sources
174 collectionsActives = $.extend(true, {}, collections); // affiches
175
176
177 ++idGraph;
178 });
179
180 /*
181 * Prendre une table HTML
182 * et calculer les donnees d'un graph jQuery.plot
183 */
184 function parseTable(table, settings){
185 var options;
186 flot = [];
187
188 options = {
189 ticks:[], // [1:"label 1", 2:"label 2"]
190 orientation:'row', // 'column'
191 ticksReels:[], // on sauve les vraies donnees pour les infobulles (1 janvier 2008) et non le code de date (1/1/2008)
192 axeOnTitle:false,
193 defaultSerie:{
194 bars: {
195 barWidth: 0.9,
196 align: "center",
197 show:true,
198 fill:false
199 },
200 lines: {
201 show:false,
202 fill:false
203 }
204 }
205 }
206 $.extend(options, settings);
207
208 row = (options.orientation == 'row');
209
210 //
211 // recuperer les points d'axes
212 //
213
214 //
215 // Une fonction pour simplifier la recup
216 //
217 function getValue(element) {
218 if (options.axeOnTitle) {
219 return element.attr('title');
220 } else {
221 return element.text();
222 }
223 }
224
225 axe=0;
226 if (row) {
227 // dans le th de chaque tr
228 $(table).find('tr:not(:first)').each(function(){
229 $(this).find('th:first').each(function(){
230 options.ticks.push([++axe, getValue($(this))]);
231 options.ticksReels.push([axe, $(this).text()]);
232 });
233 });
234
235 } else {
236 // dans les th du premier tr
237 $(table).find('tr:first th:not(:first)').each(function(){
238 options.ticks.push([++axe, getValue($(this))]);
239 options.ticksReels.push([axe, $(this).text()]);
240 });
241 }
242
243
244 //
245 // recuperer les noms de series
246 //
247 axe = (axe ? 1 : 0);
248
249 if (row) {
250 // si axes definis, on saute une ligne
251 if (axe) {
252 columns = $(table).find('tr:first th:not(:first)');
253 } else {
254 columns = $(table).find('tr:first th');
255 }
256 // chaque colonne est une serie
257
258 for(i=0; i<columns.length; i++){
259 cpt=0, data=[];
260 th = $(table).find('tr:first th:eq(' + (i + axe) + ')');
261 label = th.text();
262 serieOptions = optionsCss(th);
263 $(table).find('tr td:nth-child(' + (i + 1 + axe) +')').each(function(){
264 val = parseFloat($(this).text());
265 data.push( [++cpt, val] );
266 });
267 serie = {label:label, data:data};
268 $.extend(serie, serieOptions);
269 flot.push(serie);
270 }
271
272
273 } else {
274 // si axes definis, on saute une colonne
275 if (axe) {
276 rows = $(table).find('tr:not(:first)');
277 } else {
278 rows = $(table).find('tr');
279 }
280 // chaque ligne est une serie
281 rows.each(function(){
282 cpt=0, data=[];
283 th = $(this).find('th');
284 label = th.text();
285 serieOptions = optionsCss(th);
286 // recuperer les valeurs
287 $(this).find('td').each(function(){
288 val = parseFloat($(this).text());
289 data.push( [++cpt, val] );
290 });
291 serie = {label:label, data:data};
292 $.extend(serie, serieOptions);
293 flot.push(serie);
294 });
295 }
296
297 //
298 // mettre les options dans les series
299 //
300
301 color=0;
302 $.each(flot, function(i, serie) {
303 if (!serie.color) {
304 serie.color = color++;
305 }
306 serie = $.extend(true, {}, options.defaultSerie, serie);
307 flot[i] = serie;
308 });
309
310 opt = {
311 xaxis: {}
312 }
313 if (options.ticks.length) {
314 opt.xaxis.ticks = options.ticks;
315 opt.xaxis.ticksReels = options.ticksReels;
316 }
317
318 return {series:flot, options:opt};
319 }
320
321 /*
322 *
323 * Recuperer les options en fonctions de CSS
324 *
325 */
326 function optionsCss(element) {
327 var options = {};
328 $element = $(element);
329 if ($element.data('serie') == 'line') {
330 $.extend(true, options, {
331 lines:{show:true},
332 bars:{show:false},
333 points:{show:false}
334 });
335 }
336 if ($element.data('serie') == 'bar') {
337 $.extend(true, options, {
338 lines:{show:false},
339 bars:{show:true},
340 points:{show:false}
341 });
342 }
343 if ($element.data('serie') == 'lineBar') {
344 $.extend(true, options, {
345 lines:{show:true,steps:true},
346 bars:{show:false},
347 points:{show:false}
348 });
349 }
350 if ($element.data('fill')) {
351 $.extend(true, options, {
352 lines:{
353 fill:true,
354 fillColor: { colors: [ { opacity: 0.7 }, { opacity: 0 } ] }
355 },
356 bars:{
357 fill:true,
358 fillColor: { colors: [ { opacity: 0.7 }, { opacity: 0 } ] }
359 }
360 });
361 }
362 if (color = $element.data('color')) {
363 options.color = color;
364 }
365 return options;
366 }
367
368 /*
369 *
370 * calcul d'une moyenne glissante
371 *
372 */
373 function moyenneGlissante(lesSeries, settings) {
374 var options;
375 options = {
376 plage: 7,
377 texte:"Moyenne glissante"
378 }
379 $.extend(options, settings);
380
381 g = options.plage;
382 series = [];
383 color = 0;
384 $.each(lesSeries, function(i, val){
385 // recuperer le numero de couleur max
386 color = ParseInt(Math.max(color,val.color));
387
388 data = [], moy = [];
389 $.each(val.data, function (j, d){
390 // ajout du nouvel element
391 // et retrait du trop vieux
392 moy.push(parseInt(d[1]));
393 if (moy.length>=g) { moy.shift();}
394
395 // calcul de la somme et ajout de la moyenne
396 for(var k=0,sum=0;k<moy.length;sum+=moy[k++]);
397 data.push([d[0], Math.round(sum/moy.length)]);
398 });
399
400 serieG = $.extend(true, {}, val, {
401 data:data,
402 label:val.label + " ("+options.texte+")",
403 lines:{
404 show:true,
405 fill:false
406 },
407 bars:{show:false}
408 });
409 series.push(val);
410 series.push(serieG);
411 });
412 // remettre les couleurs
413 $.each(series, function(i, val) {
414 if (!val.color) {
415 val.color = color++;
416 }
417 });
418 return series;
419 }
420
421 //
422 // Permettre de cacher certaines series
423 //
424 function actionsLegendes(graph) {
425 // actions sur les items de legende
426 // pour masquer / afficher certaines series
427 // a ne charger qu'une fois par graph !!!
428 $(graph).find('.legendLabel a').click(function(){
429 tr = $(this).parent().parent();
430 tr.toggleClass('cacher');
431
432 // bof bof tous ces parent() et ca marche qu'avec legendeExterne:true
433 master = tr.parent().parent().parent().parent().parent();
434 pid = master.attr('id').substr(9); // enlever 'graphique'
435
436 var seriesActives = [];
437 tr.parent().find('tr:not(.cacher)').each(function(){
438 nom = $(this).find('a').text();
439 n = collections[pid].values.series.length;
440 for(i=0;i<n;i++) {
441 if (collections[pid].values.series[i].label == nom) {
442 seriesActives.push(collections[pid].values.series[i]);
443 break;
444 }
445 }
446 });
447 collectionsActives[pid].values.series = seriesActives;
448
449 $.plot(master.find('.graphResult'), seriesActives, collections[pid].values.options);
450 // vignettes
451 if (master.find('.graphVignette').length) {
452 creerVignette(master, seriesActives, collections[pid].values.options);
453 }
454
455 });
456 }
457
458 //
459 // Afficher une miniature
460 //
461 function creerVignette(graphique, series, optionsParents, settings) {
462 var options;
463 options = {
464 show:true,
465 zoom:true,
466 flot:{
467 legend: { show: false },
468 shadowSize: 0,
469 lines: { show: true, lineWidth: 1 },
470 grid: { color: "#999", hoverable:null },
471 selection: { mode: "x" },
472 xaxis:{min:null, max:null},
473 yaxis:{min:null, max:null}
474 }
475 };
476 $.extend(true, options, settings);
477 options.flot = $.extend(true, {}, optionsParents, options.flot);
478
479 // demarrer la vignette
480 vignette = $(graphique).find('.graphVignette');
481 pid = vignette.parent().parent().attr('id').substr(9);
482 vignettes[pid] = $.plot(vignette, series, options.flot);
483
484 if (vignettesSelection[pid] !== undefined) {
485 vignettes[pid].setSelection(vignettesSelection[pid]);
486 }
487 }
488
489
490
491 //
492 // Permettre le zoom sur le graphique
493 // et sur la miniature
494 //
495 function zoomGraphique(graphique) {
496 pid = $(graphique).attr('id').substr(9);
497
498 $(graphique).find('.graphResult').bind("plotselected", function (event, ranges) {
499 graph = $(event.target);
500 pid = graph.parent().attr('id').substr(9);
501
502 // clamp the zooming to prevent eternal zoom
503 if (ranges.xaxis.to - ranges.xaxis.from < 0.00001)
504 ranges.xaxis.to = ranges.xaxis.from + 0.00001;
505 if (ranges.yaxis.to - ranges.yaxis.from < 0.00001)
506 ranges.yaxis.to = ranges.yaxis.from + 0.00001;
507
508 // do the zooming
509 // et sauver les parametres du zoom
510 plots[pid] = $.plot(graph, collectionsActives[pid].values.series,
511 $.extend(true, collections[pid].values.options, {
512 xaxis: { min: ranges.xaxis.from, max: ranges.xaxis.to },
513 yaxis: { min: ranges.yaxis.from, max: ranges.yaxis.to }
514 }));
515
516 // don't fire event on the overview to prevent eternal loop
517 if (vignettes[pid] !== undefined) {
518 vignettes[pid].setSelection(ranges, true);
519 }
520 });
521
522 // raz sur double clic
523 $(graphique).find('.graphResult').dblclick(function (event) {
524 var graphique;
525 graphique = $(event.target).parent().parent();
526 pid = graphique.attr('id').substr(9);
527 vignettesSelection[pid] = undefined;
528 if (vignettes[pid] != undefined) {
529 vignettes[pid].clearSelection();
530 }
531 plots[pid] = $.plot(graphique.find('.graphResult'),
532 collectionsActives[pid].values.series,
533 $.extend(true, collections[pid].values.options, {
534 xaxis: { min: null, max: null },
535 yaxis: { min: null, max: null }
536 }));
537
538 });
539
540
541 // si une vignette est presente
542 vignette = $(graphique).find('.graphVignette');
543
544 if (vignette.length) {
545
546 // zoom depuis la miniature
547 vignette.bind("plotselected", function (event, ranges) {
548 graph = $(event.target);
549 pid = graph.parent().parent().attr('id').substr(9);
550 vignettesSelection[pid] = ranges;
551 plots[pid].setSelection(ranges);
552 });
553
554 // raz depuis la miniature sur double clic
555 vignette.dblclick(function (event) {
556 var graphique;
557 graphique = $(event.target).parent().parent().parent();
558 pid = graphique.attr('id').substr(9);
559 vignettesSelection[pid] = undefined;
560
561 plots[pid] = $.plot(graphique.find('.graphResult'),
562 collectionsActives[pid].values.series,
563 $.extend(true, collections[pid].values.options, {
564 xaxis: { min: null, max: null },
565 yaxis: { min: null, max: null }
566 }));
567 });
568 }
569
570 }
571
572 /*
573 *
574 * Infobulles
575 *
576 */
577 var previousPoint = null;
578 function infobulle(graph, settings) {
579 var options;
580 options = {
581 show:true
582 };
583 $.extend(true, options, settings);
584
585 $(graph).bind("plothover", function (event, pos, item) {
586 $("#x").text(pos.x.toFixed(2));
587 $("#y").text(pos.y.toFixed(2));
588 graph = $(event.target);
589 pid = graph.parent().attr('id').substr(9);
590
591 if (options.show) {
592 if (item) {
593 if (previousPoint != item.datapoint) {
594 previousPoint = item.datapoint;
595
596 $("#tooltip").remove();
597 var x = item.datapoint[0],
598 y = item.datapoint[1];
599
600 var color = '';
601 if(options.serie_color){
602 color = item.series.color;
603 }
604 x = collectionsActives[pid].values.options.xaxis.ticksReels[item.dataIndex][1];
605
606 showTooltip(item.pageX, item.pageY,
607 item.series.label + " [" + x + "] = " + y,
608 color);
609 }
610 }
611 else {
612 $("#tooltip").remove();
613 previousPoint = null;
614 }
615 }
616 });
617 }
618 }
619
620
621 // Adapte du site de Flot (exemple de visites)
622 // helper for returning the weekends in a period
623 function weekendAreas(axes) {
624 var markings = [];
625 var heure = 60 * 60 * 1000;
626 var jour = 24 * heure;
627
628 // les week ends
629 // go to the first Saturday
630 var d = new Date(axes.xaxis.min);
631 d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 1) % 7))
632 d.setUTCSeconds(0);
633 d.setUTCMinutes(0);
634 d.setUTCHours(0);
635 var i = d.getTime();
636 do {
637 markings.push({ xaxis: { from: i, to: i + 2*jour }, color: '#f6f6f6' });
638 i += 7*jour;
639 } while (i < axes.xaxis.max);
640
641
642 // les mois et les ans...
643 $.each(yearsArea(axes), function(i,j){
644 markings.push(j);
645 });
646
647 return markings;
648 }
649
650 // une grille pour afficher les mois et les ans...
651 function yearsArea(axes){
652 var markings = [];
653 var heure = 60 * 60 * 1000;
654 var jour = 24 * heure;
655
656 // les mois et les ans...
657 d = new Date(axes.xaxis.min);
658 y = d.getUTCFullYear();
659 m = d.getUTCMonth();
660 if (++m == 12) {m=0; ++y;}
661 d = new Date(Date.UTC(y,m,1,0,0,0));
662 do {
663 i = d.getTime();
664 if (m == 0) {couleur = '#CA5F18';}
665 else {couleur = '#D7C2AF'; }
666 markings.push({ xaxis: { from: i, to: i + jour}, color: couleur });
667 if (++m == 12) {m=0; ++y;}
668 d = new Date(Date.UTC(y,m,1,0,0,0));
669 } while (d.getTime() < axes.xaxis.max);
670
671 return markings;
672 }
673
674 /**
675 * Exemple adapte du site de Flot (exemple d'interactions)
676 * montrer les informations des points
677 *
678 * Arguments :
679 * x (Float) Coordonnee longitudinale de la bulle
680 * y (Float) Coordonnee latitudinale de la bulle
681 * contents (String) Le contenu de la bulle
682 * color La couleur de fond de l'infobulles
683 */
684 function showTooltip(x, y, contents, color) {
685 $('<div id="tooltip">' + contents + '</div>').css( {
686 top: y + 5,
687 left: x + 5,
688 opacity: 0.80,
689 background:color
690 }).addClass('tooltip_statistiques').appendTo("body").fadeIn(200);
691 }
692
693
694 // copie de la fonction de jquery.flot.js
695 // pour utilisation dans infobulle
696 function formatDate(d, fmt, monthNames) {
697 var leftPad = function(n) {
698 n = "" + n;
699 return n.length == 1 ? "0" + n : n;
700 };
701
702 var r = [];
703 var escape = false;
704 if (monthNames == null)
705 monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
706 for (var i = 0; i < fmt.length; ++i) {
707 var c = fmt.charAt(i);
708
709 if (escape) {
710 switch (c) {
711 case 'h': c = "" + d.getUTCHours(); break;
712 case 'H': c = leftPad(d.getUTCHours()); break;
713 case 'M': c = leftPad(d.getUTCMinutes()); break;
714 case 'S': c = leftPad(d.getUTCSeconds()); break;
715 case 'd': c = "" + d.getUTCDate(); break;
716 case 'm': c = "" + (d.getUTCMonth() + 1); break;
717 case 'y': c = "" + d.getUTCFullYear(); break;
718 case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
719 }
720 r.push(c);
721 escape = false;
722 }
723 else {
724 if (c == "%")
725 escape = true;
726 else
727 r.push(c);
728 }
729 }
730 return r.join("");
731 }
732
733 })(jQuery);