toolkit/exemples/libraries/nco/streamgraph.js
author Nicolas Sauret <nicolas.sauret@iri.centrepompidou.fr>
Wed, 16 Apr 2014 14:59:23 +0200
changeset 50 f68ecaf5265e
parent 47 c0b4a8b5a012
permissions -rw-r--r--
add visualisation dossiers + general editing

/**
 * 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: 830,
		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);
		}
	}
};