toolkit/javascript/nco/streamgraph.js
changeset 47 c0b4a8b5a012
equal deleted inserted replaced
46:efd9c589177a 47:c0b4a8b5a012
       
     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 };