toolkit/exemples/libraries/nco/streamgraph.js
changeset 47 c0b4a8b5a012
child 50 f68ecaf5265e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/exemples/libraries/nco/streamgraph.js	Thu Apr 10 14:20:23 2014 +0200
@@ -0,0 +1,453 @@
+/**
+ * Streamgraph
+ * 
+ * Faut-il remettre la grille pour que le curseur puisse y coller
+ * Faut-il un template html pour la timeline? 
+ */
+
+function Streamgraph(json, parametres) {
+	this.parametres = {
+		name: null,
+		hauteur: 400,
+		largeur: 900,
+		selector: "#streamgraph",
+		
+		couleurs: null,
+		relations: null,
+		activerSelection: false,
+		activerCurseur: false,
+		activerTimeline: false,
+	
+		transitionDuration: 1000
+	};
+	
+	this.etendre(parametres);
+	
+	/* -- Gestion des valeurs par défaut -- */
+	if (null==this.parametres.name) {
+		this.parametres.name = this.parametres.selector;
+	}
+	
+	/* -- Initialisation d'attributs -- */
+	var object = this;
+	this.streamgraph = null;
+	this.visuel = null;
+	
+	this.area = d3.svg.area()
+		.x(function(d) { 
+			return d.x * object.parametres.largeur / object.mx; 
+		})
+		.y0(function(d) { 
+			return object.parametres.hauteur 
+				- d.y0 * object.parametres.hauteur / object.my; 
+		})
+		.y1(function(d) { 
+			return object.parametres.hauteur
+				- (d.y + d.y0) * object.parametres.hauteur / object.my;
+		});
+	
+
+	this.dataJSON = json;	
+	this.dataStreamgraph = this.getDataStreamgraph(this.dataJSON);
+	
+	this.duree = this.dataStreamgraph[0].length;
+	this.nbClusters = this.dataStreamgraph.length;
+	
+	if (null==this.parametres.couleurs) {
+		this.parametres.couleurs = new Couleurs(this.nbClusters);
+	}
+
+	this.pas =  this.parametres.largeur/this.duree;
+	this.mx = this.dataStreamgraph[0].length - 1;
+	this.my = d3.max(this.dataStreamgraph, function(d) {
+		  return d3.max(d, function(d) {
+		    return d.y0 + d.y;
+		  });
+		});
+	
+	/* -- Create the streamgraph -- */
+	
+	/* -- Création des éléments HTML -- */
+	
+		/* -- Création des barres de sélection -- */
+	if (this.parametres.activerSelection) {
+		jQuery(this.parametres.selector)
+			.prepend("<div class='cache gauche'></div>")
+			.prepend("<div class='cache droite'></div>")
+			.prepend("<div class='selecteur gauche'></div>")
+			.prepend("<div class='selection'></div>")
+			.prepend("<div class='selecteur droite'></div>");
+		
+		// Sélecteurs latéraux
+		this.selecteurGauche = jQuery(this.parametres.selector + ' .selecteur.gauche');
+		this.selecteurDroit = jQuery(this.parametres.selector + ' .selecteur.droite');
+		this.selection = jQuery(this.parametres.selector + ' .selection');		
+		this.cacheDroit = jQuery(this.parametres.selector + ' .cache.droite');
+		this.cacheGauche = jQuery(this.parametres.selector + ' .cache.gauche');
+	}
+	
+		/* -- Création d'un curseur -- */
+	if (this.parametres.activerCurseur) {
+		this.curseur = d3.select(this.parametres.selector)
+			.append("div").classed("curseur", true);
+	}
+	
+		/* -- Création de l'élément streamgraph -- */
+	this.visuel = d3.select(this.parametres.selector)
+		.style("width", this.parametres.largeur + "px")
+		.style("height", this.parametres.hauteur + "px")
+		.append("svg:svg");
+	
+	this.visuel.selectAll("path")
+		.data(this.dataStreamgraph).enter().append("svg:path")
+			.style("fill", function(datum, index) {
+				return object.parametres.couleurs.get(index); 
+			})
+			.attr("d", this.area);
+	
+	if (null!=this.parametres.relations) {
+		this.parametres.relations.add(this.parametres.name, this);
+		
+		this.visuel.selectAll("path")
+			.each(function(datum, index) { 
+				var item = d3.select(this).attr("id", index);
+				datum["relationName"] = object.parametres.name +'.cluster.'+ index;
+				object.parametres.relations.add(datum.relationName, item);
+			});
+	}
+	
+		/* -- Création d'une timeline -- */
+	if (this.parametres.activerTimeline) {
+		this.timeline = d3.select(this.parametres.selector)
+			.append("div").classed("timeline", true);
+		this.generateTimeline();
+	}
+	
+	/* -- Configuration des barres de sélection -- */
+	if (this.parametres.activerSelection) {		
+		// Sélecteur gauche
+		this.selecteurGauche.draggable({
+			axis : 'x',
+			cursor: "move",
+			containment: [0, 0,
+			    this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), 
+			    this.parametres.hauteur],
+			drag: function() { object.followSelecteur(object.parametres.selector, object.parametres.largeur); },
+			stop: function(e, ui){
+				object.calculerPositions();
+				object.followSelecteur(object.parametres.selector, object.parametres.largeur);	
+				object.contain(object.parametres.selector, object.parametres.largeur	);
+				if (undefined!=object.__onselectionResize) {
+					object.__onselectionResize(
+						object.positionsBarres[0], object.positionsBarres[1]);
+				}
+			}
+		});
+		
+		// Sélecteur droit
+		this.selecteurDroit.draggable({
+			axis : 'x',
+			cursor: "move",
+			containment: [0, 0,
+			    this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), 
+			    this.parametres.hauteur],
+			drag: function() { object.followSelecteur(object.parametres.selector, object.parametres.largeur); },
+			stop: function(e, ui){
+				object.calculerPositions();
+				object.followSelecteur(object.parametres.selector, object.parametres.largeur);	
+				object.contain(object.parametres.selector, object.parametres.largeur	);
+				if (undefined!=object.__onselectionResize) {
+					object.__onselectionResize(
+						object.positionsBarres[0], object.positionsBarres[1]);
+				}
+			}
+		});
+		
+		// Barre de déplacement de la sélection
+		this.selection.draggable({
+			axis : 'x',
+			cursor: "move",
+			containment: [
+			    parseInt(this.selecteurGauche.css("width")), 
+			    0,
+			    this.parametres.largeur - parseInt(this.selecteurDroit.css("width")), 
+			    this.parametres.hauteur],
+			drag: function() { object.followSelection(object.parametres.selector, object.parametres.largeur); },
+			stop: function(e, ui) {
+				object.followSelection(object.parametres.selector, object.parametres.largeur);
+				object.calculerPositions();	
+				object.contain(object.parametres.selector, object.parametres.largeur);
+				if (undefined!=object.__onselectionResize) {			
+					object.__onselectionResize(
+						object.positionsBarres[0], object.positionsBarres[1]);
+				}
+			}
+		});
+		
+		/* -- Placement initial des éléments -- */
+		this.selecteurGauche.css("left", "0px");
+		this.selecteurDroit.css("left", this.parametres.largeur - parseInt(this.selecteurDroit.css("width")) + "px");
+		this.cacheGauche.css({"left": "0px", "width": "0px"});
+		this.cacheDroit.css({"left": this.largeur + "px", "width": "0px"});
+		
+		this.positionsBarres = [0, this.duree];
+	}
+	
+	/* -- Configuration du curseur -- */
+	if (this.parametres.activerCurseur) {
+		if (undefined!=this.parametres.relations) {
+			this.parametres.relations.add(this.parametres.name + ".curseur");
+		}
+	}
+};
+
+Streamgraph.prototype = {
+	constructor: Streamgraph,
+	
+	etendre: function(parametres) {
+		if (null==parametres || "object"!=typeof(parametres)) {
+			return;
+		}
+		
+		for (var cle in parametres) {
+			this.parametres[cle] = parametres[cle];
+		}
+	},
+
+	getDataStreamgraph : function(clusters) {
+		var data = new Array(),
+			duree = clusters[0].timeline.length;
+		
+		var dataCluster, timeline;
+		for (var i=0, end=clusters.length; i<end; i++) {
+			timeline = clusters[i].timeline;
+			dataCluster = [];
+			
+			for (var t=0; t<duree; t++) {
+				dataCluster.push({ x: t, y: timeline[t].length });
+			}
+			data.push(dataCluster);
+		}
+		
+		// Smooth the data
+		
+		// Compute the wiggle for stacks
+		return d3.layout.stack().offset("wiggle")(data);
+	},
+	
+	focus: function(stream) {
+		stream.style("stroke", "black")
+				.transition()
+				.duration(300)
+				.delay(50)
+				.style("opacity", 0.5);
+	},
+	
+	blur: function(stream) {
+		stream.style("stroke", "none")
+				.transition()
+				.duration(300)
+				.delay(75)
+				.style("opacity", 1);
+	},
+	
+	calculerPositions: function() { //recalcul des valeurs
+		this.positionsBarres[0] = Math.round(
+			parseInt(this.selecteurGauche.css("left"))
+			/this.pas
+		);
+		this.positionsBarres[1] = Math.round(
+			(parseInt(this.selecteurDroit.css('left')) 
+				+ parseInt(this.selecteurDroit.css('width')))
+			/this.pas
+		);
+	},
+	
+	//déplacement des caches avec le drag'n'drop
+	followSelecteur: function(selector, largeur, gauche) {
+		this.cacheGauche.css("width", this.selecteurGauche.css("left"));
+		
+		this.cacheDroit
+			.css("left", parseInt(this.selecteurDroit.css("left")) 
+					+ parseInt(this.selecteurDroit.css("width")) + "px")
+			.css("width", largeur - parseInt(this.cacheDroit.css('left')) + "px");
+		
+		this.selection
+			.css("left", parseInt(this.selecteurGauche.css("left")) 
+					+ parseInt(this.selecteurGauche.css("width")) + "px"
+			)
+			.css("width", parseInt(this.selecteurDroit.css("left")) 
+					- parseInt(this.selecteurGauche.css("left"))
+					- parseInt(this.selecteurGauche.css("width"))
+					+ "px"
+			);
+	},
+	
+	followSelection: function(selector, largeur) {		
+		var positionSelection = parseInt(this.selection.css("left")),
+			largeurSelection =  parseInt(this.selection.css("width")),
+			object = this;
+		
+		this.selecteurGauche.css("left", 
+			positionSelection - parseInt(object.selecteurGauche.css("width")) + "px"
+		);
+		
+		d3.select(selector + ' div.cache.gauche').style("width", function() {
+			return parseInt(object.selecteurGauche.css("left")) + "px";
+		});
+		
+		this.selecteurDroit.css("left", 
+			positionSelection + largeurSelection + "px"
+		);
+		
+		d3.select(selector + ' div.cache.droite')
+			.style("left", positionSelection + largeurSelection 
+					+ parseInt(object.selecteurDroit.css("width")) + "px")
+			.style("width", largeur - (positionSelection + largeurSelection 
+						+ parseInt(object.selecteurDroit.css("width"))) + "px");
+	},
+	
+	
+	contain: function(selector, largeur) {
+		var separation = 2*this.pas - parseInt(this.selecteurDroit.css("width"));
+		if (separation <0) {
+			separation = 0;
+		}
+		
+		//limite pour gauche
+		var ar = this.selecteurGauche.draggable("option", "containment");
+		ar[2] = parseInt(this.selecteurDroit.css("left")) - separation;
+	
+		//limite pour droite
+		ar = this.selecteurDroit.draggable("option", "containment");
+		ar[0] = parseInt(this.selecteurGauche.css("left")) + separation;
+		
+		// Limites de la selection
+		ar = this.selection.draggable("option", "containment");
+		ar[2] = largeur - parseInt(this.selection.css("width")) 
+			- parseInt(this.selecteurDroit.css("width"));
+	},	
+	
+	resize: function(start, end) {
+		var data = new Array(),
+			clusters = this.dataJSON,
+			object = this;
+
+		var dataCluster, timeline;
+		for (var i=0, _end=clusters.length; i<_end; i++) {
+			timeline = clusters[i].timeline;
+			dataCluster = [];
+			var compteur = 0;
+
+			for (var t=start; t<end; t++) {
+				dataCluster.push({ x: compteur, y: timeline[t].length });
+				compteur++;
+			}
+			data.push(dataCluster);
+		}
+		
+		this.dataStreamgraph = d3.layout.stack().offset("wiggle")(data);
+		
+		this.mx = this.dataStreamgraph[0].length - 1;
+		this.my = d3.max(this.dataStreamgraph, function(d) {
+			  return d3.max(d, function(d) {
+			    return d.y0 + d.y;
+			  });
+			});
+		
+		var comptage = this.nbClusters;
+		this.lockStreams();
+		this.visuel.selectAll("path")
+			.data(this.dataStreamgraph)
+			.transition()
+				.duration(this.parametres.transitionDuration)
+				.attr("d", this.area)
+				.each("end", function() {
+					if(0==--comptage) { 
+						object.unlockStreams();
+					}
+				});	
+	},
+	
+	showCursor: function() {
+		this.curseur.style("visibility", "visible");
+	},
+	
+	hideCursor: function() {
+		this.curseur.style("visibility", "hidden");
+	},
+	
+	placerCurseur: function(position, mode) {
+		switch(mode) {
+		case "index":
+			if (position > this.duree) {
+				position = this.duree;
+			}
+			else if (position < 0) {
+				position = 0;
+			}
+			
+			this.curseur.style("left", position*this.pas + "px");
+			break;
+			
+		case "distance":
+		default:
+			if (position>this.parametres.largeur) {
+				position = this.parametres.largeur;
+			}
+			else if (position<0) {
+				position = 0;
+			}
+		
+			this.curseur.style("left", position + "px");
+		}
+	},
+	
+	generateTimeline: function() {
+		var dateInitiale = 0,
+			dateFinale = this.duree,
+			pas = 5;
+		for (var t = dateInitiale; t<dateFinale; t+=pas) {
+			this.timeline.append("p").classed("time-unit", true).
+				html("date " + t + "-" + (t+pas > dateFinale? dateFinale: t+pas-1));
+		}
+	},
+	
+	lockStreams: function() {
+		var object = this;
+		if (this.parametres.relations) {
+			this.visuel.selectAll("path")
+				.each(function(datum, index) { 
+					object.parametres.relations.get(
+						object.parametres.name +'.cluster.'+ index).lock();
+				});
+		}
+	},
+	
+	unlockStreams: function() {
+		var object = this;
+		if (this.parametres.relations) {
+			this.visuel.selectAll("path")
+				.each(function(datum, index) { 
+					object.parametres.relations.get(
+						object.parametres.name +'.cluster.'+ index).unlock();
+				});
+		}
+	},
+	
+	on: function(event, action) {
+		switch(event) {
+		case "selectionResize":
+			this["__on" + event] = function() {
+				try {
+					action.apply(this, arguments);
+				}
+				finally {}
+			};
+			break;
+			
+		default:
+			this.visuel.on(event, action);
+		}
+	}
+};