|
1 /** |
|
2 * Streamgraph |
|
3 * |
|
4 * Faut-il remettre la grille pour que le curseur puisse y coller |
|
5 * Faut-il un template html pour la timeline? |
|
6 */ |
|
7 |
|
8 function Streamgraph(json, parametres) { |
|
9 this.parametres = { |
|
10 name: null, |
|
11 hauteur: 400, |
|
12 largeur: 900, |
|
13 selector: "#streamgraph", |
|
14 |
|
15 couleurs: null, |
|
16 relations: null, |
|
17 activerSelection: false, |
|
18 activerCurseur: false, |
|
19 activerTimeline: false, |
|
20 |
|
21 transitionDuration: 1000 |
|
22 }; |
|
23 |
|
24 this.etendre(parametres); |
|
25 |
|
26 /* -- Gestion des valeurs par défaut -- */ |
|
27 if (null==this.parametres.name) { |
|
28 this.parametres.name = this.parametres.selector; |
|
29 } |
|
30 |
|
31 /* -- Initialisation d'attributs -- */ |
|
32 var object = this; |
|
33 this.streamgraph = null; |
|
34 this.visuel = null; |
|
35 |
|
36 this.area = d3.svg.area() |
|
37 .x(function(d) { |
|
38 return d.x * object.parametres.largeur / object.mx; |
|
39 }) |
|
40 .y0(function(d) { |
|
41 return object.parametres.hauteur |
|
42 - d.y0 * object.parametres.hauteur / object.my; |
|
43 }) |
|
44 .y1(function(d) { |
|
45 return object.parametres.hauteur |
|
46 - (d.y + d.y0) * object.parametres.hauteur / object.my; |
|
47 }); |
|
48 |
|
49 |
|
50 this.dataJSON = json; |
|
51 this.dataStreamgraph = this.getDataStreamgraph(this.dataJSON); |
|
52 |
|
53 this.duree = this.dataStreamgraph[0].length; |
|
54 this.nbClusters = this.dataStreamgraph.length; |
|
55 |
|
56 if (null==this.parametres.couleurs) { |
|
57 this.parametres.couleurs = new Couleurs(this.nbClusters); |
|
58 } |
|
59 |
|
60 this.pas = this.parametres.largeur/this.duree; |
|
61 this.mx = this.dataStreamgraph[0].length - 1; |
|
62 this.my = d3.max(this.dataStreamgraph, function(d) { |
|
63 return d3.max(d, function(d) { |
|
64 return d.y0 + d.y; |
|
65 }); |
|
66 }); |
|
67 |
|
68 /* -- Create the streamgraph -- */ |
|
69 |
|
70 /* -- Création des éléments HTML -- */ |
|
71 |
|
72 /* -- Création des barres de sélection -- */ |
|
73 if (this.parametres.activerSelection) { |
|
74 jQuery(this.parametres.selector) |
|
75 .prepend("<div class='cache gauche'></div>") |
|
76 .prepend("<div class='cache droite'></div>") |
|
77 .prepend("<div class='selecteur gauche'></div>") |
|
78 .prepend("<div class='selection'></div>") |
|
79 .prepend("<div class='selecteur droite'></div>"); |
|
80 |
|
81 // Sélecteurs latéraux |
|
82 this.selecteurGauche = jQuery(this.parametres.selector + ' .selecteur.gauche'); |
|
83 this.selecteurDroit = jQuery(this.parametres.selector + ' .selecteur.droite'); |
|
84 this.selection = jQuery(this.parametres.selector + ' .selection'); |
|
85 this.cacheDroit = jQuery(this.parametres.selector + ' .cache.droite'); |
|
86 this.cacheGauche = jQuery(this.parametres.selector + ' .cache.gauche'); |
|
87 } |
|
88 |
|
89 /* -- Création d'un curseur -- */ |
|
90 if (this.parametres.activerCurseur) { |
|
91 this.curseur = d3.select(this.parametres.selector) |
|
92 .append("div").classed("curseur", true); |
|
93 } |
|
94 |
|
95 /* -- Création de l'élément streamgraph -- */ |
|
96 this.visuel = d3.select(this.parametres.selector) |
|
97 .style("width", this.parametres.largeur + "px") |
|
98 .style("height", this.parametres.hauteur + "px") |
|
99 .append("svg:svg"); |
|
100 |
|
101 this.visuel.selectAll("path") |
|
102 .data(this.dataStreamgraph).enter().append("svg:path") |
|
103 .style("fill", function(datum, index) { |
|
104 return object.parametres.couleurs.get(index); |
|
105 }) |
|
106 .attr("d", this.area); |
|
107 |
|
108 if (null!=this.parametres.relations) { |
|
109 this.parametres.relations.add(this.parametres.name, this); |
|
110 |
|
111 this.visuel.selectAll("path") |
|
112 .each(function(datum, index) { |
|
113 var item = d3.select(this).attr("id", index); |
|
114 datum["relationName"] = object.parametres.name +'.cluster.'+ index; |
|
115 object.parametres.relations.add(datum.relationName, item); |
|
116 }); |
|
117 } |
|
118 |
|
119 /* -- Création d'une timeline -- */ |
|
120 if (this.parametres.activerTimeline) { |
|
121 this.timeline = d3.select(this.parametres.selector) |
|
122 .append("div").classed("timeline", true); |
|
123 this.generateTimeline(); |
|
124 } |
|
125 |
|
126 /* -- Configuration des barres de sélection -- */ |
|
127 if (this.parametres.activerSelection) { |
|
128 // Sélecteur gauche |
|
129 this.selecteurGauche.draggable({ |
|
130 axis : 'x', |
|
131 cursor: "move", |
|
132 containment: [0, 0, |
|
133 this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), |
|
134 this.parametres.hauteur], |
|
135 drag: function() { object.followSelecteur(object.parametres.selector, object.parametres.largeur); }, |
|
136 stop: function(e, ui){ |
|
137 object.calculerPositions(); |
|
138 object.followSelecteur(object.parametres.selector, object.parametres.largeur); |
|
139 object.contain(object.parametres.selector, object.parametres.largeur ); |
|
140 if (undefined!=object.__onselectionResize) { |
|
141 object.__onselectionResize( |
|
142 object.positionsBarres[0], object.positionsBarres[1]); |
|
143 } |
|
144 } |
|
145 }); |
|
146 |
|
147 // Sélecteur droit |
|
148 this.selecteurDroit.draggable({ |
|
149 axis : 'x', |
|
150 cursor: "move", |
|
151 containment: [0, 0, |
|
152 this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), |
|
153 this.parametres.hauteur], |
|
154 drag: function() { object.followSelecteur(object.parametres.selector, object.parametres.largeur); }, |
|
155 stop: function(e, ui){ |
|
156 object.calculerPositions(); |
|
157 object.followSelecteur(object.parametres.selector, object.parametres.largeur); |
|
158 object.contain(object.parametres.selector, object.parametres.largeur ); |
|
159 if (undefined!=object.__onselectionResize) { |
|
160 object.__onselectionResize( |
|
161 object.positionsBarres[0], object.positionsBarres[1]); |
|
162 } |
|
163 } |
|
164 }); |
|
165 |
|
166 // Barre de déplacement de la sélection |
|
167 this.selection.draggable({ |
|
168 axis : 'x', |
|
169 cursor: "move", |
|
170 containment: [ |
|
171 parseInt(this.selecteurGauche.css("width")), |
|
172 0, |
|
173 this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), |
|
174 this.parametres.hauteur], |
|
175 drag: function() { object.followSelection(object.parametres.selector, object.parametres.largeur); }, |
|
176 stop: function(e, ui) { |
|
177 object.followSelection(object.parametres.selector, object.parametres.largeur); |
|
178 object.calculerPositions(); |
|
179 object.contain(object.parametres.selector, object.parametres.largeur); |
|
180 if (undefined!=object.__onselectionResize) { |
|
181 object.__onselectionResize( |
|
182 object.positionsBarres[0], object.positionsBarres[1]); |
|
183 } |
|
184 } |
|
185 }); |
|
186 |
|
187 /* -- Placement initial des éléments -- */ |
|
188 this.selecteurGauche.css("left", "0px"); |
|
189 this.selecteurDroit.css("left", this.parametres.largeur - parseInt(this.selecteurDroit.css("width")) + "px"); |
|
190 this.cacheGauche.css({"left": "0px", "width": "0px"}); |
|
191 this.cacheDroit.css({"left": this.largeur + "px", "width": "0px"}); |
|
192 |
|
193 this.positionsBarres = [0, this.duree]; |
|
194 } |
|
195 |
|
196 /* -- Configuration du curseur -- */ |
|
197 if (this.parametres.activerCurseur) { |
|
198 if (undefined!=this.parametres.relations) { |
|
199 this.parametres.relations.add(this.parametres.name + ".curseur"); |
|
200 } |
|
201 } |
|
202 }; |
|
203 |
|
204 Streamgraph.prototype = { |
|
205 constructor: Streamgraph, |
|
206 |
|
207 etendre: function(parametres) { |
|
208 if (null==parametres || "object"!=typeof(parametres)) { |
|
209 return; |
|
210 } |
|
211 |
|
212 for (var cle in parametres) { |
|
213 this.parametres[cle] = parametres[cle]; |
|
214 } |
|
215 }, |
|
216 |
|
217 getDataStreamgraph : function(clusters) { |
|
218 var data = new Array(), |
|
219 duree = clusters[0].timeline.length; |
|
220 |
|
221 var dataCluster, timeline; |
|
222 for (var i=0, end=clusters.length; i<end; i++) { |
|
223 timeline = clusters[i].timeline; |
|
224 dataCluster = []; |
|
225 |
|
226 for (var t=0; t<duree; t++) { |
|
227 dataCluster.push({ x: t, y: timeline[t].length }); |
|
228 } |
|
229 data.push(dataCluster); |
|
230 } |
|
231 |
|
232 // Smooth the data |
|
233 |
|
234 // Compute the wiggle for stacks |
|
235 return d3.layout.stack().offset("wiggle")(data); |
|
236 }, |
|
237 |
|
238 focus: function(stream) { |
|
239 stream.style("stroke", "black") |
|
240 .transition() |
|
241 .duration(300) |
|
242 .delay(50) |
|
243 .style("opacity", 0.5); |
|
244 }, |
|
245 |
|
246 blur: function(stream) { |
|
247 stream.style("stroke", "none") |
|
248 .transition() |
|
249 .duration(300) |
|
250 .delay(75) |
|
251 .style("opacity", 1); |
|
252 }, |
|
253 |
|
254 calculerPositions: function() { //recalcul des valeurs |
|
255 this.positionsBarres[0] = Math.round( |
|
256 parseInt(this.selecteurGauche.css("left")) |
|
257 /this.pas |
|
258 ); |
|
259 this.positionsBarres[1] = Math.round( |
|
260 (parseInt(this.selecteurDroit.css('left')) |
|
261 + parseInt(this.selecteurDroit.css('width'))) |
|
262 /this.pas |
|
263 ); |
|
264 }, |
|
265 |
|
266 //déplacement des caches avec le drag'n'drop |
|
267 followSelecteur: function(selector, largeur, gauche) { |
|
268 this.cacheGauche.css("width", this.selecteurGauche.css("left")); |
|
269 |
|
270 this.cacheDroit |
|
271 .css("left", parseInt(this.selecteurDroit.css("left")) |
|
272 + parseInt(this.selecteurDroit.css("width")) + "px") |
|
273 .css("width", largeur - parseInt(this.cacheDroit.css('left')) + "px"); |
|
274 |
|
275 this.selection |
|
276 .css("left", parseInt(this.selecteurGauche.css("left")) |
|
277 + parseInt(this.selecteurGauche.css("width")) + "px" |
|
278 ) |
|
279 .css("width", parseInt(this.selecteurDroit.css("left")) |
|
280 - parseInt(this.selecteurGauche.css("left")) |
|
281 - parseInt(this.selecteurGauche.css("width")) |
|
282 + "px" |
|
283 ); |
|
284 }, |
|
285 |
|
286 followSelection: function(selector, largeur) { |
|
287 var positionSelection = parseInt(this.selection.css("left")), |
|
288 largeurSelection = parseInt(this.selection.css("width")), |
|
289 object = this; |
|
290 |
|
291 this.selecteurGauche.css("left", |
|
292 positionSelection - parseInt(object.selecteurGauche.css("width")) + "px" |
|
293 ); |
|
294 |
|
295 d3.select(selector + ' div.cache.gauche').style("width", function() { |
|
296 return parseInt(object.selecteurGauche.css("left")) + "px"; |
|
297 }); |
|
298 |
|
299 this.selecteurDroit.css("left", |
|
300 positionSelection + largeurSelection + "px" |
|
301 ); |
|
302 |
|
303 d3.select(selector + ' div.cache.droite') |
|
304 .style("left", positionSelection + largeurSelection |
|
305 + parseInt(object.selecteurDroit.css("width")) + "px") |
|
306 .style("width", largeur - (positionSelection + largeurSelection |
|
307 + parseInt(object.selecteurDroit.css("width"))) + "px"); |
|
308 }, |
|
309 |
|
310 |
|
311 contain: function(selector, largeur) { |
|
312 var separation = 2*this.pas - parseInt(this.selecteurDroit.css("width")); |
|
313 if (separation <0) { |
|
314 separation = 0; |
|
315 } |
|
316 |
|
317 //limite pour gauche |
|
318 var ar = this.selecteurGauche.draggable("option", "containment"); |
|
319 ar[2] = parseInt(this.selecteurDroit.css("left")) - separation; |
|
320 |
|
321 //limite pour droite |
|
322 ar = this.selecteurDroit.draggable("option", "containment"); |
|
323 ar[0] = parseInt(this.selecteurGauche.css("left")) + separation; |
|
324 |
|
325 // Limites de la selection |
|
326 ar = this.selection.draggable("option", "containment"); |
|
327 ar[2] = largeur - parseInt(this.selection.css("width")) |
|
328 - parseInt(this.selecteurDroit.css("width")); |
|
329 }, |
|
330 |
|
331 resize: function(start, end) { |
|
332 var data = new Array(), |
|
333 clusters = this.dataJSON, |
|
334 object = this; |
|
335 |
|
336 var dataCluster, timeline; |
|
337 for (var i=0, _end=clusters.length; i<_end; i++) { |
|
338 timeline = clusters[i].timeline; |
|
339 dataCluster = []; |
|
340 var compteur = 0; |
|
341 |
|
342 for (var t=start; t<end; t++) { |
|
343 dataCluster.push({ x: compteur, y: timeline[t].length }); |
|
344 compteur++; |
|
345 } |
|
346 data.push(dataCluster); |
|
347 } |
|
348 |
|
349 this.dataStreamgraph = d3.layout.stack().offset("wiggle")(data); |
|
350 |
|
351 this.mx = this.dataStreamgraph[0].length - 1; |
|
352 this.my = d3.max(this.dataStreamgraph, function(d) { |
|
353 return d3.max(d, function(d) { |
|
354 return d.y0 + d.y; |
|
355 }); |
|
356 }); |
|
357 |
|
358 var comptage = this.nbClusters; |
|
359 this.lockStreams(); |
|
360 this.visuel.selectAll("path") |
|
361 .data(this.dataStreamgraph) |
|
362 .transition() |
|
363 .duration(this.parametres.transitionDuration) |
|
364 .attr("d", this.area) |
|
365 .each("end", function() { |
|
366 if(0==--comptage) { |
|
367 object.unlockStreams(); |
|
368 } |
|
369 }); |
|
370 }, |
|
371 |
|
372 showCursor: function() { |
|
373 this.curseur.style("visibility", "visible"); |
|
374 }, |
|
375 |
|
376 hideCursor: function() { |
|
377 this.curseur.style("visibility", "hidden"); |
|
378 }, |
|
379 |
|
380 placerCurseur: function(position, mode) { |
|
381 switch(mode) { |
|
382 case "index": |
|
383 if (position > this.duree) { |
|
384 position = this.duree; |
|
385 } |
|
386 else if (position < 0) { |
|
387 position = 0; |
|
388 } |
|
389 |
|
390 this.curseur.style("left", position*this.pas + "px"); |
|
391 break; |
|
392 |
|
393 case "distance": |
|
394 default: |
|
395 if (position>this.parametres.largeur) { |
|
396 position = this.parametres.largeur; |
|
397 } |
|
398 else if (position<0) { |
|
399 position = 0; |
|
400 } |
|
401 |
|
402 this.curseur.style("left", position + "px"); |
|
403 } |
|
404 }, |
|
405 |
|
406 generateTimeline: function() { |
|
407 var dateInitiale = 0, |
|
408 dateFinale = this.duree, |
|
409 pas = 5; |
|
410 for (var t = dateInitiale; t<dateFinale; t+=pas) { |
|
411 this.timeline.append("p").classed("time-unit", true). |
|
412 html("date " + t + "-" + (t+pas > dateFinale? dateFinale: t+pas-1)); |
|
413 } |
|
414 }, |
|
415 |
|
416 lockStreams: function() { |
|
417 var object = this; |
|
418 if (this.parametres.relations) { |
|
419 this.visuel.selectAll("path") |
|
420 .each(function(datum, index) { |
|
421 object.parametres.relations.get( |
|
422 object.parametres.name +'.cluster.'+ index).lock(); |
|
423 }); |
|
424 } |
|
425 }, |
|
426 |
|
427 unlockStreams: function() { |
|
428 var object = this; |
|
429 if (this.parametres.relations) { |
|
430 this.visuel.selectAll("path") |
|
431 .each(function(datum, index) { |
|
432 object.parametres.relations.get( |
|
433 object.parametres.name +'.cluster.'+ index).unlock(); |
|
434 }); |
|
435 } |
|
436 }, |
|
437 |
|
438 on: function(event, action) { |
|
439 switch(event) { |
|
440 case "selectionResize": |
|
441 this["__on" + event] = function() { |
|
442 try { |
|
443 action.apply(this, arguments); |
|
444 } |
|
445 finally {} |
|
446 }; |
|
447 break; |
|
448 |
|
449 default: |
|
450 this.visuel.on(event, action); |
|
451 } |
|
452 } |
|
453 }; |