1 
  2 /**
  3  * Basic StackedGraph class.
  4  * Contains methods to create and draw stacked graphs from a set of time series.
  5  * Can be extended to ThemeRivers and Streamgraphs by overriding the baselineAt() function. 
  6  * 
  7  * <b>Domains</b><br>
  8  * 
  9  * Different domain-spaces are used to locate data:
 10  * <li> canvas domain					pixel coordinates within canvas
 11  * <li> normalized-canvas domain		canvas domain between 0 and 1
 12  * <li> stacked-graph domain			domain of time series summed up including baseline offset
 13  * <li> summed-series domain			domain of time series summed up
 14  * <li> series domain					domain of one time series
 15  * 
 16  * @class
 17  * @param target
 18  * @returns
 19  */
 20 function StackedGraph(target){
 21 	this.border = 'rgba(0,0,0,0.1)';
 22 	this._timeSeries = new Array();
 23 	
 24 	this.start = null;
 25 	this.end = null;
 26 	this.max = null;
 27 	this.min = null;
 28 	this.colorScale = ['#00FF55', '#FFFF00'];
 29 	
 30 	if(target != null){
 31 		this.target = target;
 32 		this.createCanvas();
 33 		
 34 		var graph = this;
 35 		target.addEventListener("mousemove", function(event){
 36 			var hoveredSeries = graph.timeSeriesAt(event.layerX, event.layerY);
 37 			graph.makeHighlighted(hoveredSeries);
 38 			
 39 		});
 40 		
 41 		
 42 		window.addEventListener("resize", function(event){
 43 			console.log("resize");
 44 			graph.canvas.width = graph.canvas.clientWidth;
 45 			graph.canvas.height = graph.canvas.clientHeight;
 46 			graph.draw();
 47 		});
 48 	}
 49 	
 50 }
 51 
 52 /**
 53  * create a canvas inside the target html tag. 
 54  * the canvas is used to draw the graphs
 55  * 
 56  */
 57 StackedGraph.prototype.createCanvas = function(){
 58 	this.canvas = document.createElement('canvas');
 59 	this.target.appendChild(this.canvas);
 60 	this.canvas.style.width= "100%";
 61 	this.canvas.style.height= "100%";
 62 	this.context = this.canvas.getContext('2d');
 63 	
 64 	this.updateCanvasSize();
 65 };
 66 
 67 /**
 68  * - update the canvas size to fit into the target element.
 69  * 
 70  */
 71 StackedGraph.prototype.updateCanvasSize = function(){
 72 	this.canvas.height = this.canvas.clientHeight;
 73 	this.canvas.width = this.canvas.clientWidth;
 74 	this.context.setTransform(1, 0, 0, 1, 0, 0);
 75 };
 76 
 77 /**
 78  * returns the visible area in the stacked-graph domain
 79  */
 80 Object.defineProperty(StackedGraph.prototype, "stackedGraphWindow", {
 81 	get: function(){
 82 		var start = this.start != null ? this.start : this.minimumX; 
 83 		var end = this.end != null ? this.end : this.maximumX;
 84 		var top = this.maxSampleInRange(start, end);
 85 		var bottom = {"x": start, "y": 0};
 86 		end = end + (end-start)*0.1;
 87 		
 88 		top.y = top.y + this.baselineAt(top.x) ;
 89 		for(var i = 0; i < this._timeSeriesX.length; i++){
 90 			var x = this._timeSeriesX[i];
 91 			if(start <= x && x <= end){
 92 				var bl = this._baselineY[x];
 93 				if(bl <= bottom.y){
 94 					bottom = {"x": x, "y": bl};
 95 				}
 96 			}
 97 		}
 98 		top.y = top.y * 1.1;
 99 		bottom.y = bottom.y * 1.1;
100 		
101 		return {
102 			"start":	start,
103 			"end":		end,
104 			"top":		top,
105 			"bottom":	bottom,
106 			"height":	(top.y-bottom.y)
107 		};
108 	}
109 });
110 
111 /**
112  * returns a "set" of sample positions within the intervall [start, end].
113  * 
114  */
115 StackedGraph.prototype.samplePosInRange = function(start, end){
116 	var x = {};
117 	for(var i = this.timeSeries.length-1; i >= 0; i--){
118 		var series = this.timeSeries[i];
119 		for(var j in series.x){
120 			var value = series.x[j];
121 			if(value >= start && value <= end){
122 				x[value] = null;
123 			}
124 		}
125 	}
126 	
127 	return x;
128 };
129 
130 /**
131  * maximum sample value in summed-series domain 
132  */
133 StackedGraph.prototype.maxSampleInRange = function(start, end){
134 	// retrieve sample positions including start and end
135 	var x = this.samplePosInRange(start, end);
136 	x[start] = null;
137 	x[end] = null;
138 	
139 	var max = {"x": 0, "y": 0};
140 	for(var u in this._timeSeriesSums){
141 		var sum = this._timeSeriesSums[u];
142 		
143 		if(start <= u && u <= end){
144 			if(sum > max.y){
145 				max.x = u;
146 				max.y = sum;
147 			}
148 		}
149 	}
150 	
151 	return max;
152 };
153 
154 /**
155  * draw into canvas
156  */
157 StackedGraph.prototype.draw = function(){
158 	var width = this.canvas.width;
159 	var height = this.canvas.height;
160 	var window = this.stackedGraphWindow;
161 	this.context.clearRect(0,0,width, height);
162 	
163 	// visual properties
164 	this.context.shadowOffsetX = 0;
165 	this.context.shadowOffsetY = 0;
166 	this.context.shadowBlur = 10;
167 	this.context.shadowColor = "rgba(0, 0, 0, 0.5)";
168 	this.context.strokeStyle = this.border;
169 	var scale = chroma.scale(this.colorScale).mode('lab');
170 	this.context.lineWidth = 2;
171 	
172 	// loop through time series
173 	var highlightedSeriesIndex = null;
174 	for(var i = this.timeSeries.length-1; i >= 0; i--){
175 		var series = this.timeSeries[this.toOrderedSeriesIndex(i)];
176 		
177 		// visual properties for this particular time series
178 		var col = scale(i / this.timeSeries.length);
179 		this.context.fillStyle = col.hex();
180 		
181 		// skip highlighted series. it will be drawn afterwards on top
182 		if(series == this.highlightedSeries){
183 			highlightedSeriesIndex = i;
184 			continue;
185 		}
186 		
187 		// define the top of the time series
188 		this.context.beginPath(); 
189 		for(var j in this._timeSeriesX){
190 			var x = this._timeSeriesX[j];
191 			var y = 0;
192 			for(var k = i; k >= 0; k--){
193 				y += this._timeSeriesY[this.toOrderedSeriesIndex(k)][j];
194 			}
195 			var nx = (x - window.start) / window.end;
196 			var ny = ((y + this._baselineY[x]) - window.bottom.y)/window.height;
197 			this.context.lineTo(nx*width, height - ny*height);
198 		}
199 		// new define the bottom
200 		for(var j = this._timeSeriesX.length -1; j >= 0; j--){
201 			var x = this._timeSeriesX[j];
202 			var y = 0;
203 			for(var k = i-1; k >= 0; k--){
204 				y += this._timeSeriesY[this.toOrderedSeriesIndex(k)][j];
205 			}
206 			var nx = (x - window.start) / window.end;
207 			var ny = ((y + this._baselineY[x]) - window.bottom.y)/window.height;
208 			this.context.lineTo(nx*width, height - ny*height);
209 		}
210 		
211 		// and draw it
212 		this.context.fill();
213 		if(this.border != null){
214 			this.context.stroke();
215 		}
216 	}
217 	
218 	// now draw the highlighted series on top
219 	if(highlightedSeriesIndex != null){
220 		var i = highlightedSeriesIndex;
221 		var series = this.timeSeries[this.toOrderedSeriesIndex(i)];
222 		
223 		var borderStyle = this.border;
224 		var col = scale(i / this.timeSeries.length);
225 		col = col.brighter(20);
226 		borderStyle = 'rgba(1,2,2,0.5)';
227 		this.context.fillStyle = col.hex();
228 		this.context.strokeStyle = borderStyle;
229 		this.context.beginPath(); 
230 		
231 		// top of series
232 		for(var j in this._timeSeriesX){
233 			var x = this._timeSeriesX[j];
234 			var y = 0;
235 			for(var k = i; k >= 0; k--){
236 				y += this._timeSeriesY[this.toOrderedSeriesIndex(k)][j];
237 			}
238 			var nx = (x - window.start) / window.end;
239 			var ny = ((y + this._baselineY[x]) - window.bottom.y)/window.height;
240 			this.context.lineTo(nx*width, height - ny*height);
241 		}
242 		// bottom of series
243 		var weighted = 0;
244 		var weightSum = 0;
245 		for(var j = this._timeSeriesX.length -1; j >= 0; j--){
246 			var x = this._timeSeriesX[j];
247 			var y = 0;
248 			for(var k = i-1; k >= 0; k--){
249 				y += this._timeSeriesY[ this.toOrderedSeriesIndex(k)][j];
250 			}
251 			var nx = (x - window.start) / window.end;
252 			var ny = ((y + this._baselineY[x]) - window.bottom.y)/window.height;
253 			
254 			weighted += x * this._timeSeriesY[this.toOrderedSeriesIndex(i)][j];
255 			weightSum += this._timeSeriesY[this.toOrderedSeriesIndex(i)][j];
256 			this.context.lineTo(nx*width, height - ny*height);
257 		}
258 		
259 		// draw
260 		this.context.fill();
261 		if(this.border != null){
262 			this.context.stroke();
263 		}
264 		
265 		{ // draw label
266 			this.context.fillStyle = "black";
267 			this.context.shadowOffsetX = 0;
268 			this.context.shadowOffsetY = 0;
269 			this.context.shadowBlur = 0;
270 			this.context.shadowColor = "rgba(0, 0, 0, 0)";
271 			this.context.strokeStyle = this.border;
272 			
273 			var x = weighted / weightSum; 
274 			var nx = (x - window.start) / window.end;
275 			var y = (this.seriesBottomInStackedGraphDomain(highlightedSeriesIndex, x) + this.seriesTopInStackedGraphDomain(highlightedSeriesIndex, x))/2;
276 			var ny = ((y + this.baselineAt(x)) - window.bottom.y)/window.height;
277 			this.context.textBaseline = "middle";
278 			
279 			this.context.fillStyle = "white";
280 			this.context.strokeStyle = "black";
281 			this.context.font = "12px arial";
282 			this.context.shadowOffsetX = 0;
283 			this.context.shadowOffsetY = 0;
284 			this.context.shadowBlur = 3;
285 			this.context.shadowColor = "rgba(0, 0, 0, 1)";
286 			this.context.strokeText(series.name, nx*width + 10, height - ny*height);
287 			this.context.strokeText(series.name, nx*width + 10, height - ny*height);
288 			this.context.strokeText(series.name, nx*width + 10, height - ny*height);
289 			
290 			this.context.shadowOffsetX = 0;
291 			this.context.shadowOffsetY = 0;
292 			this.context.shadowBlur = 0;
293 			this.context.shadowColor = "rgba(0, 0, 0, 0)";
294 			this.context.font = "12px arial";
295 			this.context.fillText(series.name, nx*width + 10, height - ny*height);
296 			
297 			this.context.strokeStyle = "black";
298 			this.context.fillStyle = "white";
299 			this.context.beginPath();
300 			this.context.arc(nx*width, height - ny*height, 3, 0, 2 * Math.PI, false);
301 			this.context.fill();
302 			this.context.stroke();
303 		}
304 	}
305 };
306 
307 /**
308  * returns f(x) from the bottom of the series in the stacked graph domain
309  */
310 StackedGraph.prototype.seriesBottomInStackedGraphDomain = function(seriesIndex, x){
311 	var y = 0;
312 	for(var k = seriesIndex-1; k >= 0; k--){
313 		y += this.timeSeries[this.toOrderedSeriesIndex(k)].valueAt(x);
314 	}
315 	return y;
316 }
317 
318 /**
319  * returns f(x) from the top of the series in the stacked graph domain
320  */
321 StackedGraph.prototype.seriesTopInStackedGraphDomain = function(seriesIndex, x){
322 	var y = 0;
323 	for(var k = seriesIndex; k >= 0; k--){
324 		y += this.timeSeries[this.toOrderedSeriesIndex(k)].valueAt(x);
325 	}
326 	return y;
327 }
328 
329 Object.defineProperty(StackedGraph.prototype, "minimumX", {
330 	get: function(){
331 		var minX = Infinity;
332 		for(var i = this.timeSeries.length-1; i >= 0; i--){
333 			var series = this.timeSeries[i];
334 			minX = Math.min(minX, series.start);
335 		}
336 		return minX;
337 	}
338 });
339 
340 Object.defineProperty(StackedGraph.prototype, "maximumX", {
341 	get: function(){
342 		var maxX = -Infinity;
343 		for(var i = this.timeSeries.length-1; i >= 0; i--){
344 			var series = this.timeSeries[i];
345 			maxX = Math.max(maxX, series.end);
346 		}
347 		return maxX;
348 	}
349 });
350 
351 Object.defineProperty(StackedGraph.prototype, "minimumY", {
352 	get: function(){
353 		var minY = Infinity;
354 		for(var i = this.timeSeries.length-1; i >= 0; i--){
355 			var series = this.timeSeries[i];
356 			minY = Math.min(minY, series.min);
357 		}
358 		return minY;
359 	}
360 });
361 
362 Object.defineProperty(StackedGraph.prototype, "maximumY", {
363 	get: function(){
364 		var maxY = -Infinity;
365 		for(var i = this.timeSeries.length-1; i >= 0; i--){
366 			var series = this.timeSeries[i];
367 			maxY = Math.max(maxY, series.max);
368 		}
369 		return maxY;
370 	}
371 });
372 
373 /**
374  * set or get a set of timeSeries.
375  */
376 Object.defineProperty(StackedGraph.prototype, "timeSeries", {
377 	get: function(){
378 		return this._timeSeries;
379 	},
380 	set: function(value){
381 		this._timeSeries = value;
382 		
383 		// presample all series and stackedGraph properties in order to 
384 		// boost performance
385 		var steps = 100;
386 		this._timeSeriesY = new Array();
387 		this._timeSeriesX = new Array(steps+1);
388 		this._timeSeriesSums = {};
389 		this._baselineY = {};
390 		
391 		var start = this.start != null ? this.start : this.minimumX; 
392 		var end = this.end != null ? this.end : this.maximumX;
393 		
394 		// presample x values
395 		for(var i = 0; i <= steps; i++){
396 			var u = start+(end-start)*(i/steps);
397 			this._timeSeriesX[i] = u;
398 			this._timeSeriesSums[u] = 0;
399 		}
400 		
401 		// presample y values
402 		for(var j = 0; j < this._timeSeries.length; j++){
403 			var series = this._timeSeries[j];
404 			var samples = new Array(steps+1);
405 			for(var i = 0; i <= steps; i++){
406 				var u = this._timeSeriesX[i];
407 				var value = series.valueAt(u);
408 				samples[i] = value;
409 				this._timeSeriesSums[u] += samples[i];
410 			}
411 			this._timeSeriesY.push(samples);
412 		}
413 		
414 		// sort
415 		var startingPositions = new Array(this._timeSeries.length);
416 		for(var i = 0; i < this._timeSeries.length; i++){
417 			var series = this._timeSeries[i];
418 			var threshold = series.average * 0.01;
419 			
420 			startingPositions[i] = 0;
421 			for(var j = 0; j < this._timeSeriesX.length; j++){
422 				if(this._timeSeriesY[i][j] >= threshold){
423 					startingPositions[i] = j;
424 					break;
425 				}
426 			}
427 			series.startingPosition = startingPositions[i];
428 		}
429 		console.log("startingPositions: " + startingPositions);
430 		var order = new Array(this._timeSeries.length);
431 		for(var i = 0; i < this._timeSeries.length; i++){
432 			var min = Infinity;
433 			var minIndex = 0;
434 			for(var j = 0; j < startingPositions.length; j++){
435 				if(startingPositions[j] < min){
436 					min = startingPositions[j];
437 					minIndex = j;
438 				}
439 			}
440 			startingPositions[minIndex] = Infinity;
441 			order[i] = minIndex;
442 		}
443 		this.timeSeriesOrder = order;
444 		console.log("order: " + order);
445 		
446 		// presample baseline
447 		for(var i = 0; i <= steps; i++){
448 			var u = this._timeSeriesX[i];
449 			this._baselineY[u] = this.baselineAt(u);
450 		}
451 		
452 		this.draw();
453 	}
454 });
455 
456 StackedGraph.prototype.toOrderedSeriesIndex = function(index){
457 	return index;
458 };
459 
460 StackedGraph.prototype.toInsideOutOrderIndex = function(index){
461 	var order = this.timeSeriesOrder;
462 	var ioIndex = order.length - 1 - 2*index;
463 	if(ioIndex < 0){
464 		ioIndex = Math.abs(ioIndex)-1;
465 	}
466 	
467 	return order[ioIndex];
468 };
469 
470 /**
471  * 
472  * @param x
473  * @returns sum of all series at x in summed-series domain
474  */
475 StackedGraph.prototype.sumAt = function(x){
476 	
477 	var sum = this._timeSeriesSums[x];
478 	if(sum == null){
479 		sum = 0;
480 		for(var i = 0; i < this.timeSeries.length; i++){
481 			sum += this.timeSeries[i].valueAt(x);
482 		}
483 		this._timeSeriesSums[x] = sum;
484 	}
485 	
486 	return sum;
487 };
488 
489 StackedGraph.prototype.baselineAt = function(x){
490 	return 0;
491 };
492 
493 /**
494  * 
495  * @param x in canvas space pixel coordinates
496  * @param y in canvas space pixel coordinates
497  * @returns the time series at the specified position 
498  * 
499  */
500 StackedGraph.prototype.timeSeriesAt = function(cx, cy){
501 	var window = this.stackedGraphWindow;
502 	
503 	var nx = cx / this.canvas.width;
504 	var x = (nx * window.end) + window.start;
505 	var ny = 1- cy / this.canvas.height;
506 	var y = ((ny *window.height) + window.bottom.y) - this.baselineAt(x);
507 	
508 	var sum = 0;
509 	for(var i = 0; i < this.timeSeries.length; i++){
510 		var series = this.timeSeries[this.toOrderedSeriesIndex(i)];
511 		var fx = series.valueAt(x);
512 		if(sum <= y && y <= sum + fx){
513 			return series;
514 		}else{
515 			sum += fx;
516 		}
517 	}
518 	
519 	
520 	return null;
521 };
522 
523 /**
524  * tag the given series as highlighted. 
525  * 
526  * @param series
527  */
528 StackedGraph.prototype.makeHighlighted = function(series){
529 	this.highlightedSeries = series;
530 	this.draw();
531 };
532 
533 
534 
535 
536 
537