[SPIP] ~version 3.0.7-->3.0.10
[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 noColumns: 3
66 },
67 bars: {fill:false},
68 yaxis: { min: 0 },
69 selection: { mode: "x" }
70 }
71 }
72 $.extend(true, options, settings);
73
74
75 $(this).each(function(){
76
77 // identifiant unique pour tous les graphs
78 // creer les cadres
79 // .graphique
80 // .graphResult
81 // .graphInfos
82 // .graphLegend
83 // .graphOverview
84 $(this).hide().wrap("<div class='graphique' id='graphique"+idGraph+"'></div>");
85 graphique = $(this).parent();
86 values = parseTable(this, options.parse);
87 $.extend(true, values.options, options.flot);
88
89 graph = $("<div class='graphResult' style='width:" + options.width + ";height:" + options.height + ";'></div>").appendTo(graphique);
90 graph.wrap("<div class='graphResult-wrap'></div>");
91 gInfo = $("<div class='graphInfo'></div>").appendTo(graphique);
92
93 // legende en dehors du dessin ?
94 if (options.legendeExterne) {
95 legend = $("<div class='graphLegend' id='grapLegend"+idGraph+"'></div>").appendTo(gInfo);
96 values.options.legend.container = legend;
97 }
98 // legende avec items clicables pour desactiver certaines series
99 if (options.legendeExterne && options.legendeActions) {
100 values.options.legend.labelFormatter = function(label) {
101 return '<a href="#label">' + label + '</a>';
102 }
103 }
104 // si mode time, on calcule des timestamp
105 // les series sont alors de la forme [[timestamp, valeur],...]
106 // et pas besoin de ticks declare
107 if (options.modeDate) {
108 timestamps = [];
109 // calcul des timestamps
110 $.each(values.options.xaxis.ticks, function(i, val){
111 timestamps.push([val[0], (new Date(val[1])).getTime()]);
112 });
113 // les remettre dans les series
114 $.each(values.series, function(i, val){
115 data = [];
116 $.each(val.data, function (j, d){
117 data.push([timestamps[j][1], d[1]]);
118 });
119 values.series[i].data = data;
120 });
121 // plus besoin du ticks
122 // mais toujours besoin des valeurs completes...
123 values.options.xaxis = $.extend(true, {
124 mode: "time",
125 timeformat: "%d/%m/%y"
126 },
127 values.options.xaxis,
128 {ticks: null}
129 );
130 if (options.grille.weekend) {
131 values.options.grid = { markings: weekendAreas }
132 }
133 if (options.grille.years) {
134 values.options.grid = { markings: yearsArea }
135 }
136 }
137
138 // en cas de moyenne glissante, on la calcule
139 if (options.moyenneGlissante.show) {
140 values.series = moyenneGlissante(values.series, options.moyenneGlissante);
141 }
142
143 // si infobulles, les ajouter
144 if (options.infobulle.show) {
145 $.extend(true, options.infobulle, {date:options.modeDate});
146 infobulle($('#graphique'+idGraph), options.infobulle);
147 $.extend(true, values.options, {
148 grid:{hoverable:true}
149 });
150 }
151
152
153 // dessiner
154 plots[idGraph] = $.plot(graph, values.series, values.options);
155
156 // prevoir les actions sur les labels
157 if (options.legendeExterne && options.legendeActions) {
158 $.extend(values.options, {legend:{container:null, show:false}});
159 actionsLegendes($('#graphique'+idGraph));
160 }
161
162 // ajouter une mini vue si demandee
163 if (options.vignette.show) {
164 $("<div class='graphVignette' id='#graphVignette"+idGraph
165 + "' style='width:" + options.vignette.width + ";height:"
166 + options.vignette.height + ";'></div>").appendTo(gInfo);
167 creerVignette($('#graphique'+idGraph), values.series, values.options, options.vignette);
168 }
169 // autoriser les zoom
170 if (options.zoom) {
171 zoomGraphique($('#graphique'+idGraph));
172 }
173
174 // stocker les valeurs
175 collections.push({id:idGraph, values:values}); // sources
176 collectionsActives = $.extend(true, {}, collections); // affiches
177
178
179 ++idGraph;
180 });
181
182 /*
183 * Prendre une table HTML
184 * et calculer les donnees d'un graph jQuery.plot
185 */
186 function parseTable(table, settings){
187 var options;
188 flot = [];
189
190 options = {
191 ticks:[], // [1:"label 1", 2:"label 2"]
192 orientation:'row', // 'column'
193 ticksReels:[], // on sauve les vraies donnees pour les infobulles (1 janvier 2008) et non le code de date (1/1/2008)
194 axeOnTitle:false,
195 defaultSerie:{
196 bars: {
197 barWidth: 0.9,
198 align: "center",
199 show:true,
200 fill:false
201 },
202 lines: {
203 show:false,
204 fill:false
205 }
206 }
207 }
208 $.extend(options, settings);
209
210 row = (options.orientation == 'row');
211
212 //
213 // recuperer les points d'axes
214 //
215
216 //
217 // Une fonction pour simplifier la recup
218 //
219 function getValue(element) {
220 if (options.axeOnTitle) {
221 return element.attr('title');
222 } else {
223 return element.text();
224 }
225 }
226
227 axe=0;
228 if (row) {
229 // dans le th de chaque tr
230 $(table).find('tr:not(:first)').each(function(){
231 $(this).find('th:first').each(function(){
232 options.ticks.push([++axe, getValue($(this))]);
233 options.ticksReels.push([axe, $(this).text()]);
234 });
235 });
236
237 } else {
238 // dans les th du premier tr
239 $(table).find('tr:first th:not(:first)').each(function(){
240 options.ticks.push([++axe, getValue($(this))]);
241 options.ticksReels.push([axe, $(this).text()]);
242 });
243 }
244
245
246 //
247 // recuperer les noms de series
248 //
249 axe = (axe ? 1 : 0);
250
251 if (row) {
252 // si axes definis, on saute une ligne
253 if (axe) {
254 columns = $(table).find('tr:first th:not(:first)');
255 } else {
256 columns = $(table).find('tr:first th');
257 }
258 // chaque colonne est une serie
259
260 for(i=0; i<columns.length; i++){
261 cpt=0, data=[];
262 th = $(table).find('tr:first th:eq(' + (i + axe) + ')');
263 label = th.text();
264 serieOptions = optionsCss(th);
265 $(table).find('tr td:nth-child(' + (i + 1 + axe) +')').each(function(){
266 val = parseFloat($(this).text());
267 data.push( [++cpt, val] );
268 });
269 serie = {label:label, data:data};
270 $.extend(serie, serieOptions);
271 flot.push(serie);
272 }
273
274
275 } else {
276 // si axes definis, on saute une colonne
277 if (axe) {
278 rows = $(table).find('tr:not(:first)');
279 } else {
280 rows = $(table).find('tr');
281 }
282 // chaque ligne est une serie
283 rows.each(function(){
284 cpt=0, data=[];
285 th = $(this).find('th');
286 label = th.text();
287 serieOptions = optionsCss(th);
288 // recuperer les valeurs
289 $(this).find('td').each(function(){
290 val = parseFloat($(this).text());
291 data.push( [++cpt, val] );
292 });
293 serie = {label:label, data:data};
294 $.extend(serie, serieOptions);
295 flot.push(serie);
296 });
297 }
298
299 //
300 // mettre les options dans les series
301 //
302
303 color=0;
304 $.each(flot, function(i, serie) {
305 if (!serie.color) {
306 serie.color = color++;
307 }
308 serie = $.extend(true, {}, options.defaultSerie, serie);
309 flot[i] = serie;
310 });
311
312 opt = {
313 xaxis: {}
314 }
315 if (options.ticks.length) {
316 opt.xaxis.ticks = options.ticks;
317 opt.xaxis.ticksReels = options.ticksReels;
318 }
319
320 return {series:flot, options:opt};
321 }
322
323 /*
324 *
325 * Recuperer les options en fonctions de CSS
326 *
327 */
328 function optionsCss(element) {
329 var options = {};
330 $element = $(element);
331 if ($element.data('serie') == 'line') {
332 $.extend(true, options, {
333 lines:{show:true},
334 bars:{show:false},
335 points:{show:false}
336 });
337 }
338 if ($element.data('serie') == 'bar') {
339 $.extend(true, options, {
340 lines:{show:false},
341 bars:{show:true},
342 points:{show:false}
343 });
344 }
345 if ($element.data('serie') == 'lineBar') {
346 $.extend(true, options, {
347 lines:{show:true,steps:true},
348 bars:{show:false},
349 points:{show:false}
350 });
351 }
352 if ($element.data('fill')) {
353 $.extend(true, options, {
354 lines:{
355 fill:true,
356 fillColor: { colors: [ { opacity: 0.9 }, { opacity: 0.9 } ] }
357 },
358 bars:{
359 fill:true,
360 fillColor: { colors: [ { opacity: 0.9 }, { opacity: 0.9 } ] }
361 }
362 });
363 }
364 if (color = $element.data('color')) {
365 options.color = color;
366 }
367 return options;
368 }
369
370 /*
371 *
372 * calcul d'une moyenne glissante
373 *
374 */
375 function moyenneGlissante(lesSeries, settings) {
376 var options;
377 options = {
378 plage: 7,
379 texte:"Moyenne glissante"
380 }
381 $.extend(options, settings);
382
383 g = options.plage;
384 series = [];
385 color = 0;
386 $.each(lesSeries, function(i, val){
387 // recuperer le numero de couleur max
388 color = ParseInt(Math.max(color,val.color));
389
390 data = [], moy = [];
391 $.each(val.data, function (j, d){
392 // ajout du nouvel element
393 // et retrait du trop vieux
394 moy.push(parseInt(d[1]));
395 if (moy.length>=g) { moy.shift();}
396
397 // calcul de la somme et ajout de la moyenne
398 for(var k=0,sum=0;k<moy.length;sum+=moy[k++]);
399 data.push([d[0], Math.round(sum/moy.length)]);
400 });
401
402 serieG = $.extend(true, {}, val, {
403 data:data,
404 label:val.label + " ("+options.texte+")",
405 lines:{
406 show:true,
407 fill:false
408 },
409 bars:{show:false}
410 });
411 series.push(val);
412 series.push(serieG);
413 });
414 // remettre les couleurs
415 $.each(series, function(i, val) {
416 if (!val.color) {
417 val.color = color++;
418 }
419 });
420 return series;
421 }
422
423 //
424 // Permettre de cacher certaines series
425 //
426 function actionsLegendes(graph) {
427 // actions sur les items de legende
428 // pour masquer / afficher certaines series
429 // a ne charger qu'une fois par graph !!!
430 $(graph).find('.legendLabel a').click(function(){
431 tr = $(this).parent().prev('.legendColorBox').toggleClass('cacher').parent();
432
433 master = tr.closest('.graphique');
434 pid = master.attr('id').substr(9); // enlever 'graphique'
435
436 var seriesActives = [];
437 tr.find('.legendColorBox:not(.cacher)').each(function(){
438 nom = $(this).next('.legendLabel').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.closest('.graphique').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.closest('.graphique').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).closest('.graphique');
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.closest('.graphique').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).closest('.graphique');
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.closest('.graphique').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: '#e8e8e8' });
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 var width_year = jour;
656 if (axes.xaxis.options.minTickSize[1]=="month")
657 width_year = 30.4*jour;
658
659 // les mois et les ans...
660 d = new Date(axes.xaxis.min);
661 y = d.getUTCFullYear();
662 m = d.getUTCMonth();
663 if (++m == 12) {m=0; ++y;}
664 d = new Date(Date.UTC(y,m,1,0,0,0));
665 do {
666 i = d.getTime();
667 if (m == 0) {couleur = '#CA5F18';}
668 else {couleur = '#D7C2AF'; }
669 markings.push({ xaxis: { from: i, to: i + (m==0?width_year:jour)}, color: couleur });
670 if (++m == 12) {m=0; ++y;}
671 d = new Date(Date.UTC(y,m,1,0,0,0));
672 } while (d.getTime() < axes.xaxis.max);
673
674 return markings;
675 }
676
677 /**
678 * Exemple adapte du site de Flot (exemple d'interactions)
679 * montrer les informations des points
680 *
681 * Arguments :
682 * x (Float) Coordonnee longitudinale de la bulle
683 * y (Float) Coordonnee latitudinale de la bulle
684 * contents (String) Le contenu de la bulle
685 * color La couleur de fond de l'infobulles
686 */
687 function showTooltip(x, y, contents, color) {
688 $('<div id="tooltip">' + contents + '</div>').css( {
689 top: y + 5,
690 left: x + 5,
691 opacity: 0.80,
692 background:color
693 }).addClass('tooltip_statistiques').appendTo("body").fadeIn(200);
694 }
695
696
697 // copie de la fonction de jquery.flot.js
698 // pour utilisation dans infobulle
699 function formatDate(d, fmt, monthNames) {
700 var leftPad = function(n) {
701 n = "" + n;
702 return n.length == 1 ? "0" + n : n;
703 };
704
705 var r = [];
706 var escape = false;
707 if (monthNames == null)
708 monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
709 for (var i = 0; i < fmt.length; ++i) {
710 var c = fmt.charAt(i);
711
712 if (escape) {
713 switch (c) {
714 case 'h': c = "" + d.getUTCHours(); break;
715 case 'H': c = leftPad(d.getUTCHours()); break;
716 case 'M': c = leftPad(d.getUTCMinutes()); break;
717 case 'S': c = leftPad(d.getUTCSeconds()); break;
718 case 'd': c = "" + d.getUTCDate(); break;
719 case 'm': c = "" + (d.getUTCMonth() + 1); break;
720 case 'y': c = "" + d.getUTCFullYear(); break;
721 case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
722 }
723 r.push(c);
724 escape = false;
725 }
726 else {
727 if (c == "%")
728 escape = true;
729 else
730 r.push(c);
731 }
732 }
733 return r.join("");
734 }
735
736 })(jQuery);