0
|
1 /*
|
|
2 * FuzzyTimelineDensity.js
|
|
3 *
|
|
4 * Copyright (c) 2013, Sebastian Kruse. All rights reserved.
|
|
5 *
|
|
6 * This library is free software; you can redistribute it and/or
|
|
7 * modify it under the terms of the GNU Lesser General Public
|
|
8 * License as published by the Free Software Foundation; either
|
|
9 * version 3 of the License, or (at your option) any later version.
|
|
10 *
|
|
11 * This library is distributed in the hope that it will be useful,
|
|
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
14 * Lesser General Public License for more details.
|
|
15 *
|
|
16 * You should have received a copy of the GNU Lesser General Public
|
|
17 * License along with this library; if not, write to the Free Software
|
|
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19 * MA 02110-1301 USA
|
|
20 */
|
|
21
|
|
22 /**
|
|
23 * @class FuzzyTimelineDensity
|
|
24 * Implementation for a fuzzy time-ranges density plot
|
|
25 * @author Sebastian Kruse (skruse@mpiwg-berlin.mpg.de)
|
|
26 *
|
|
27 * @param {HTML object} parent div to append the FuzzyTimeline
|
|
28 */
|
|
29 function FuzzyTimelineDensity(parent,div) {
|
|
30
|
|
31 this.index;
|
|
32 this.fuzzyTimeline = this;
|
|
33 this.singleTickWidth;
|
|
34 this.singleTickCenter = function(){return this.singleTickWidth/2;};
|
|
35 //contains all data
|
|
36 this.datasetsPlot;
|
|
37 this.datasetsHash;
|
|
38 this.highlightedDatasetsPlot;
|
|
39 this.yValMin;
|
|
40 this.yValMax;
|
|
41 this.displayType;
|
|
42 //contains selected data
|
|
43 this.selected = undefined;
|
|
44 //contains the last selected "date"
|
|
45 this.highlighted;
|
|
46
|
|
47 this.parent = parent;
|
|
48 this.div = div;
|
|
49 this.options = parent.options;
|
|
50 this.plot;
|
|
51 this.maxTickCount = this.options.maxDensityTicks;
|
|
52
|
|
53 this.datasets;
|
|
54 }
|
|
55
|
|
56 FuzzyTimelineDensity.prototype = {
|
|
57
|
|
58 initialize : function(datasets) {
|
|
59 var density = this;
|
|
60
|
|
61 density.datasets = datasets;
|
|
62 density.selected = [];
|
|
63 },
|
|
64
|
|
65 createPlot : function(data){
|
|
66 density = this;
|
|
67 var chartData = [];
|
|
68
|
|
69 chartData.push([density.parent.overallMin,0]);
|
|
70 $.each(data, function(name,val){
|
|
71 var tickCenterTime = density.parent.overallMin+name*density.singleTickWidth+density.singleTickCenter();
|
|
72 var dateObj = moment(tickCenterTime);
|
|
73 chartData.push([dateObj,val]);
|
|
74 });
|
|
75 var maxPlotedDate = chartData[chartData.length-1][0];
|
|
76 if (density.parent.overallMax > maxPlotedDate){
|
|
77 chartData.push([density.parent.overallMax,0]);
|
|
78 } else {
|
|
79 chartData.push([maxPlotedDate+1,0]);
|
|
80 }
|
|
81
|
|
82
|
|
83
|
|
84 return chartData;
|
|
85 },
|
|
86
|
|
87 //uniform distribution (UD)
|
|
88 createUDData : function(datasets) {
|
|
89 var density = this;
|
|
90 var plots = [];
|
|
91 var objectHashes = [];
|
|
92 $(datasets).each(function(){
|
|
93 var chartDataCounter = new Object();
|
|
94 var objectHash = new Object();
|
|
95
|
|
96 for (var i = 0; i < density.tickCount; i++){
|
|
97 chartDataCounter[i]=0;
|
|
98 }
|
|
99 //check if we got "real" datasets, or just array of objects
|
|
100 var datasetObjects = this;
|
|
101 if (typeof this.objects !== "undefined")
|
|
102 datasetObjects = this.objects;
|
|
103 $(datasetObjects).each(function(){
|
|
104 var ticks = density.parent.getTicks(this, density.singleTickWidth);
|
|
105 if (typeof ticks !== "undefined"){
|
|
106 var exactTickCount =
|
|
107 ticks.firstTickPercentage+
|
|
108 ticks.lastTickPercentage+
|
|
109 (ticks.lastTick-ticks.firstTick-1);
|
|
110 for (var i = ticks.firstTick; i <= ticks.lastTick; i++){
|
|
111 var weight = 0;
|
|
112 //calculate the weight for each span, that the object overlaps
|
|
113 if (density.parent.options.timelineMode == 'fuzzy'){
|
|
114 //in fuzzy mode, each span gets just a fraction of the complete weight
|
|
115 if (i == ticks.firstTick)
|
|
116 weight = this.weight * ticks.firstTickPercentage/exactTickCount;
|
|
117 else if (i == ticks.lastTick)
|
|
118 weight = this.weight * ticks.lastTickPercentage/exactTickCount;
|
|
119 else
|
|
120 weight = this.weight * 1/exactTickCount;
|
|
121 } else if (density.parent.options.timelineMode == 'stacking'){
|
|
122 //in stacking mode each span gets the same amount.
|
|
123 //(besides first and last..)
|
|
124 if (i == ticks.firstTick)
|
|
125 weight = this.weight * ticks.firstTickPercentage;
|
|
126 else if (i == ticks.lastTick)
|
|
127 weight = this.weight * ticks.lastTickPercentage;
|
|
128 else
|
|
129 weight = this.weight;
|
|
130 }
|
|
131
|
|
132 chartDataCounter[i] += weight;
|
|
133 //add this object to the hash
|
|
134 if (typeof objectHash[i] === "undefined")
|
|
135 objectHash[i] = [];
|
|
136 objectHash[i].push(this);
|
|
137 }
|
|
138 }
|
|
139 });
|
|
140
|
|
141 //scale according to selected type
|
|
142 chartDataCounter = density.parent.scaleData(chartDataCounter);
|
|
143
|
|
144 var udChartData = density.createPlot(chartDataCounter);
|
|
145 if (udChartData.length > 0)
|
|
146 plots.push(udChartData);
|
|
147
|
|
148 objectHashes.push(objectHash);
|
|
149 });
|
|
150
|
|
151 return {plots:plots, hashs:objectHashes};
|
|
152 },
|
|
153
|
|
154 showPlot : function() {
|
|
155 var density = this;
|
|
156 var plot = density.datasetsPlot;
|
|
157 var highlight_select_plot = $.merge([],plot);
|
|
158
|
|
159 //see if there are selected/highlighted values
|
|
160 if (density.highlightedDatasetsPlot instanceof Array){
|
|
161 //check if plot is some other - external - graph
|
|
162 if (plot === density.datasetsPlot)
|
|
163 highlight_select_plot = $.merge(highlight_select_plot,density.highlightedDatasetsPlot);
|
|
164 }
|
|
165
|
|
166 var axisFormatString = "%Y";
|
|
167 var tooltipFormatString = "YYYY";
|
|
168 if (density.singleTickWidth<60*1000){
|
|
169 axisFormatString = "%Y/%m/%d %H:%M:%S";
|
|
170 tooltipFormatString = "YYYY/MM/DD HH:mm:ss";
|
|
171 } else if (density.singleTickWidth<60*60*1000) {
|
|
172 axisFormatString = "%Y/%m/%d %H:%M";
|
|
173 tooltipFormatString = "YYYY/MM/DD HH:mm";
|
|
174 } else if (density.singleTickWidth<24*60*60*1000){
|
|
175 axisFormatString = "%Y/%m/%d %H";
|
|
176 tooltipFormatString = "YYYY/MM/DD HH";
|
|
177 } else if (density.singleTickWidth<31*24*60*60*1000){
|
|
178 axisFormatString = "%Y/%m/%d";
|
|
179 tooltipFormatString = "YYYY/MM/DD";
|
|
180 } else if (density.singleTickWidth<12*31*24*60*60*1000){
|
|
181 axisFormatString = "%Y/%m";
|
|
182 tooltipFormatString = "YYYY/MM";
|
|
183 }
|
|
184
|
|
185 //credits: Pimp Trizkit @ http://stackoverflow.com/a/13542669
|
|
186 function shadeRGBColor(color, percent) {
|
|
187 var f=color.split(","),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=parseInt(f[0].slice(4)),G=parseInt(f[1]),B=parseInt(f[2]);
|
|
188 return "rgb("+(Math.round((t-R)*p)+R)+","+(Math.round((t-G)*p)+G)+","+(Math.round((t-B)*p)+B)+")";
|
|
189 }
|
|
190
|
|
191 //credits: Tupak Goliam @ http://stackoverflow.com/a/3821786
|
|
192 var drawLines = function(plot, ctx) {
|
|
193 var data = plot.getData();
|
|
194 var axes = plot.getAxes();
|
|
195 var offset = plot.getPlotOffset();
|
|
196 for (var i = 0; i < data.length; i++) {
|
|
197 var series = data[i];
|
|
198 var lineWidth = 1;
|
|
199
|
|
200 for (var j = 0; j < series.data.length-1; j++) {
|
|
201 var d = (series.data[j]);
|
|
202 var d2 = (series.data[j+1]);
|
|
203
|
|
204 var x = offset.left + axes.xaxis.p2c(d[0]);
|
|
205 var y = offset.top + axes.yaxis.p2c(d[1]);
|
|
206
|
|
207 var x2 = offset.left + axes.xaxis.p2c(d2[0]);
|
|
208 var y2 = offset.top + axes.yaxis.p2c(d2[1]);
|
|
209
|
|
210 //hide lines that "connect" 0 and 0
|
|
211 //essentially blanking out the 0 values
|
|
212 if ((d[1]==0)&&(d2[1]==0)){
|
|
213 continue;
|
|
214 }
|
|
215
|
|
216 ctx.strokeStyle=series.color;
|
|
217 ctx.lineWidth = lineWidth;
|
|
218 ctx.beginPath();
|
|
219 ctx.moveTo(x,y);
|
|
220 ctx.lineTo(x2,y2);
|
|
221
|
|
222 //add shadow (esp. to make background lines more visible)
|
|
223 ctx.shadowColor = shadeRGBColor(series.color,-0.3);
|
|
224 ctx.shadowBlur=1;
|
|
225 ctx.shadowOffsetX = 1;
|
|
226 ctx.shadowOffsetY = 1;
|
|
227
|
|
228 ctx.stroke();
|
|
229 }
|
|
230 }
|
|
231 };
|
|
232
|
|
233 var options = {
|
|
234 series:{
|
|
235 //width:0 because line is drawn in own routine above
|
|
236 //but everything else (data points, shadow) should be drawn
|
|
237 lines:{show: true, lineWidth: 0, shadowSize: 0},
|
|
238 },
|
|
239 grid: {
|
|
240 hoverable: true,
|
|
241 clickable: true,
|
|
242 backgroundColor: density.parent.options.backgroundColor,
|
|
243 borderWidth: 0,
|
|
244 minBorderMargin: 0,
|
|
245 },
|
|
246 legend: {
|
|
247 },
|
|
248 tooltip: true,
|
|
249 tooltipOpts: {
|
|
250 content: function(label, xval, yval, flotItem){
|
|
251 highlightString = moment(xval-density.singleTickCenter()).format(tooltipFormatString) + " - " +
|
|
252 moment(xval+density.singleTickCenter()).format(tooltipFormatString) + " : ";
|
|
253 //(max.)2 Nachkomma-Stellen von y-Wert anzeigen
|
|
254 highlightString += Math.round(yval*100)/100;
|
|
255
|
|
256 return highlightString;
|
|
257 }
|
|
258 },
|
|
259 selection: {
|
|
260 mode: "x"
|
|
261 },
|
|
262 xaxis: {
|
|
263 mode: "time",
|
|
264 timeformat:axisFormatString,
|
|
265 min : density.parent.overallMin,
|
|
266 max : density.parent.overallMax,
|
|
267 },
|
|
268 yaxis: {
|
|
269 min : density.yValMin,
|
|
270 max : density.yValMax*1.05
|
|
271 },
|
|
272 hooks: {
|
|
273 draw : drawLines
|
|
274 },
|
|
275 };
|
|
276 if (!density.parent.options.showYAxis)
|
|
277 options.yaxis.show=false;
|
|
278
|
|
279 var highlight_select_plot_colors = [];
|
|
280 var i = 0;
|
|
281 $(highlight_select_plot).each(function(){
|
|
282 var color;
|
|
283 if (i < GeoTemConfig.datasets.length){
|
|
284 var datasetColors = GeoTemConfig.getColor(i);
|
|
285 if (highlight_select_plot.length>GeoTemConfig.datasets.length)
|
|
286 color = "rgb("+datasetColors.r0+","+datasetColors.g0+","+datasetColors.b0+")";
|
|
287 else
|
|
288 color = "rgb("+datasetColors.r1+","+datasetColors.g1+","+datasetColors.b1+")";
|
|
289 } else {
|
|
290 var datasetColors = GeoTemConfig.getColor(i-GeoTemConfig.datasets.length);
|
|
291 color = "rgb("+datasetColors.r1+","+datasetColors.g1+","+datasetColors.b1+")";
|
|
292 }
|
|
293
|
|
294 highlight_select_plot_colors.push({
|
|
295 color : color,
|
|
296 data : this
|
|
297 });
|
|
298 i++;
|
|
299 });
|
|
300
|
|
301 density.plot = $.plot($(density.div), highlight_select_plot_colors, options);
|
|
302 density.parent.drawHandles();
|
|
303
|
|
304 var rangeBars = density.parent.rangeBars;
|
|
305 if (typeof rangeBars !== "undefined")
|
|
306 $(density.div).unbind("plothover", rangeBars.hoverFunction);
|
|
307 $(density.div).unbind("plothover", density.hoverFunction);
|
|
308 $(density.div).bind("plothover", density.hoverFunction);
|
|
309
|
|
310 //this var prevents the execution of the plotclick event after a select event
|
|
311 density.wasSelection = false;
|
|
312 $(density.div).unbind("plotclick");
|
|
313 $(density.div).bind("plotclick", density.clickFunction);
|
|
314
|
|
315 $(density.div).unbind("plotselected");
|
|
316 $(density.div).bind("plotselected", density.selectFuntion);
|
|
317 },
|
|
318
|
|
319 hoverFunction : function (event, pos, item) {
|
|
320 var hoverPoint;
|
|
321 //TODO: this could be wanted (if negative weight is used)
|
|
322 if ((item)&&(item.datapoint[1] != 0)) {
|
|
323 //at begin and end of plot there are added 0 points
|
|
324 hoverPoint = item.dataIndex-1;
|
|
325 }
|
|
326 //remember last point, so that we don't redraw the current state
|
|
327 //that "hoverPoint" may be undefined is on purpose
|
|
328 if (density.highlighted !== hoverPoint){
|
|
329 density.highlighted = hoverPoint;
|
|
330 density.triggerHighlight(hoverPoint);
|
|
331 }
|
|
332 },
|
|
333
|
|
334 clickFunction : function (event, pos, item) {
|
|
335 if (density.wasSelection)
|
|
336 density.wasSelection = false;
|
|
337 else {
|
|
338 //remove selection handles (if there were any)
|
|
339 density.parent.clearHandles();
|
|
340
|
|
341 var selectPoint;
|
|
342 //that date may be undefined is on purpose
|
|
343 //TODO: ==0 could be wanted (if negative weight is used)
|
|
344 if ((item)&&(item.datapoint[1] != 0)) {
|
|
345 //at begin and end of plot there are added 0 points
|
|
346 selectPoint = item.dataIndex-1;
|
|
347 }
|
|
348 density.triggerSelection(selectPoint);
|
|
349 }
|
|
350 },
|
|
351
|
|
352 selectFuntion : function(event, ranges) {
|
|
353 var spanArray = density.parent.getSpanArray(density.singleTickWidth);
|
|
354 var startSpan, endSpan;
|
|
355 for (var i = 0; i < spanArray.length-1; i++){
|
|
356 if ((typeof startSpan === "undefined") && (ranges.xaxis.from <= spanArray[i+1]))
|
|
357 startSpan = i;
|
|
358 if ((typeof endSpan === "undefined") && (ranges.xaxis.to <= spanArray[i+1]))
|
|
359 endSpan = i;
|
|
360 }
|
|
361
|
|
362 if ((typeof startSpan !== "undefined") && (typeof endSpan !== "undefined")){
|
|
363 density.triggerSelection(startSpan, endSpan);
|
|
364 density.wasSelection = true;
|
|
365
|
|
366 density.parent.clearHandles();
|
|
367 var xaxis = density.plot.getAxes().xaxis;
|
|
368 var x1 = density.plot.pointOffset({x:ranges.xaxis.from,y:0}).left;
|
|
369 var x2 = density.plot.pointOffset({x:ranges.xaxis.to,y:0}).left;
|
|
370
|
|
371 density.parent.addHandle(x1,x2);
|
|
372 }
|
|
373 },
|
|
374
|
|
375 selectByX : function(x1, x2){
|
|
376 density = this;
|
|
377 var xaxis = density.plot.getAxes().xaxis;
|
|
378 var offset = density.plot.getPlotOffset().left;
|
|
379 var from = xaxis.c2p(x1-offset);
|
|
380 var to = xaxis.c2p(x2-offset);
|
|
381
|
|
382 var spanArray = density.parent.getSpanArray(density.singleTickWidth);
|
|
383 var startSpan, endSpan;
|
|
384 for (var i = 0; i < spanArray.length-1; i++){
|
|
385 if ((typeof startSpan === "undefined") && (from <= spanArray[i+1]))
|
|
386 startSpan = i;
|
|
387 if ((typeof endSpan === "undefined") && (to <= spanArray[i+1]))
|
|
388 endSpan = i;
|
|
389 }
|
|
390
|
|
391 if ((typeof startSpan !== "undefined") && (typeof endSpan !== "undefined")){
|
|
392 density.triggerSelection(startSpan, endSpan);
|
|
393 }
|
|
394 },
|
|
395
|
|
396 drawDensityPlot : function(datasets, tickWidth) {
|
|
397 var density = this;
|
|
398 //calculate tick width (will be in ms)
|
|
399 delete density.tickCount;
|
|
400 delete density.singleTickWidth;
|
|
401 delete density.highlightedDatasetsPlot;
|
|
402 density.parent.zoomPlot(1);
|
|
403 if (typeof tickWidth !== "undefined"){
|
|
404 density.singleTickWidth = tickWidth;
|
|
405 density.tickCount = Math.ceil((density.parent.overallMax-density.parent.overallMin)/tickWidth);
|
|
406 }
|
|
407 if ((typeof density.tickCount === "undefined") || (density.tickCount > density.maxTickCount)){
|
|
408 density.tickCount = density.maxTickCount;
|
|
409 density.singleTickWidth = (density.parent.overallMax-density.parent.overallMin)/density.tickCount;
|
|
410 if (density.singleTickWidth === 0)
|
|
411 density.singleTickWidth = 1;
|
|
412 }
|
|
413
|
|
414 var hashAndPlot = density.createUDData(datasets);
|
|
415
|
|
416 density.datasetsPlot = hashAndPlot.plots;
|
|
417 density.datasetsHash = hashAndPlot.hashs;
|
|
418
|
|
419 density.yValMin = 0;
|
|
420 density.yValMax = 0;
|
|
421
|
|
422 density.combinedDatasetsPlot = [];
|
|
423 for (var i = 0; i < density.datasetsPlot.length; i++){
|
|
424 for (var j = 0; j < density.datasetsPlot[i].length; j++){
|
|
425 var val = density.datasetsPlot[i][j][1];
|
|
426
|
|
427 if (val < density.yValMin)
|
|
428 density.yValMin = val;
|
|
429 if (val > density.yValMax)
|
|
430 density.yValMax = val;
|
|
431 }
|
|
432 }
|
|
433
|
|
434 density.showPlot();
|
|
435 },
|
|
436
|
|
437 triggerHighlight : function(hoverPoint) {
|
|
438 var density = this;
|
|
439 var highlightedObjects = [];
|
|
440
|
|
441
|
|
442 if (typeof hoverPoint !== "undefined") {
|
|
443 $(density.datasetsHash).each(function(){
|
|
444 if (typeof this[hoverPoint] !== "undefined")
|
|
445 highlightedObjects.push(this[hoverPoint]);
|
|
446 else
|
|
447 highlightedObjects.push([]);
|
|
448 });
|
|
449 } else {
|
|
450 for (var i = 0; i < GeoTemConfig.datasets.length; i++)
|
|
451 highlightedObjects.push([]);
|
|
452 }
|
|
453 this.parent.core.triggerHighlight(highlightedObjects);
|
|
454 },
|
|
455
|
|
456 triggerSelection : function(startPoint, endPoint) {
|
|
457 var density = this;
|
|
458 var selection;
|
|
459 if (typeof startPoint !== "undefined") {
|
|
460 if (typeof endPoint === "undefined")
|
|
461 endPoint = startPoint;
|
|
462 density.selected = [];
|
|
463 $(density.datasetsHash).each(function(){
|
|
464 var objects = [];
|
|
465 for (var i = startPoint; i <= endPoint; i++){
|
|
466 $(this[i]).each(function(){
|
|
467 if ($.inArray(this, objects) == -1){
|
|
468 objects.push(this);
|
|
469 }
|
|
470 });
|
|
471 }
|
|
472 density.selected.push(objects);
|
|
473 });
|
|
474
|
|
475 selection = new Selection(density.selected, density.parent);
|
|
476 } else {
|
|
477 //empty selection
|
|
478 density.selected = [];
|
|
479 for (var i = 0; i < GeoTemConfig.datasets.length; i++)
|
|
480 density.selected.push([]);
|
|
481 selection = new Selection(density.selected);
|
|
482 }
|
|
483
|
|
484 this.parent.selectionChanged(selection);
|
|
485 this.parent.core.triggerSelection(selection);
|
|
486 },
|
|
487
|
|
488 highlightChanged : function(objects) {
|
|
489 if( !GeoTemConfig.highlightEvents ){
|
|
490 return;
|
|
491 }
|
|
492 var density = this;
|
|
493 var emptyHighlight = true;
|
|
494 var selected_highlighted = objects;
|
|
495 if (typeof density.selected !== "undefined")
|
|
496 selected_highlighted = GeoTemConfig.mergeObjects(objects,density.selected);
|
|
497 $(selected_highlighted).each(function(){
|
|
498 if ((this instanceof Array) && (this.length > 0)){
|
|
499 emptyHighlight = false;
|
|
500 return false;
|
|
501 }
|
|
502 });
|
|
503 if (emptyHighlight && (typeof density.selected === "undefined")){
|
|
504 density.highlightedDatasetsPlot = [];
|
|
505 } else {
|
|
506 density.highlightedDatasetsPlot = density.createUDData(selected_highlighted).plots;
|
|
507 }
|
|
508 density.showPlot();
|
|
509 },
|
|
510
|
|
511 selectionChanged : function(objects) {
|
|
512 if( !GeoTemConfig.selectionEvents ){
|
|
513 return;
|
|
514 }
|
|
515 var density = this;
|
|
516 density.selected = objects;
|
|
517 density.highlightChanged([]);
|
|
518 },
|
|
519
|
|
520 deselection : function() {
|
|
521 },
|
|
522
|
|
523 filtering : function() {
|
|
524 },
|
|
525
|
|
526 inverseFiltering : function() {
|
|
527 },
|
|
528
|
|
529 triggerRefining : function() {
|
|
530 },
|
|
531
|
|
532 reset : function() {
|
|
533 },
|
|
534
|
|
535 show : function() {
|
|
536 },
|
|
537
|
|
538 hide : function() {
|
|
539 }
|
|
540 };
|