1/** 2 * 3 * @param {HTMLElement} element graph container 4 * @param {Function} callback this callback will be called when values are 5 * changed, ie the graph is zoomed or panned using mouse or buttons 6 * Arguments are the updated `from` and `to` values in DD-MM-YYYY format 7 * @param {Object} options 8 * @param {bool} options.showAsHistogram 9 */ 10function TimeLine(element, callback, options) { 11 var me = this; 12 13 var timelineSelection = null; 14 var timelineScale = 1; 15 var timelineWidth; 16 var timelineHeight; 17 var timelineX; 18 var timelineY; 19 var timelineArea; 20 var timelineXAxis; 21 var timelineXTranslate = 0; 22 var timelineXScale = 1; 23 var lastQuery = null; 24 var maxDomain; 25 var maxValue = 0; 26 27 this.initialized = false; 28 this.graphMaxData = null; 29 this.graphData = null; 30 this.model = {}; 31 this.callback = callback; 32 this.options = options || {}; 33 34 var zoom; 35 var margin = { 36 top: 0, 37 right: 5, 38 bottom: 10, 39 left: 0 40 }; 41 42 this.initialize = function() { 43 44 // prevent multi initialization 45 if (this.initialized) { return; } 46 47 var container = d3.select(element); 48 timelineWidth = container.node().offsetWidth - margin.left - margin.right; 49 timelineHeight = container.node().offsetHeight - margin.top - margin.bottom - /* scroll */ 15; 50 51 // check that we have enough space to initialize; otherwise postpone it 52 // (the user will have to call recomputeSize) 53 if (timelineHeight <= 0) { 54 return; 55 } 56 57 // Compute X axis 58 var current_first_time = Number.MAX_VALUE; 59 var timeExtent = d3.extent(this.graphMaxData, function(d) { 60 var begin = d.begin; 61 var end = d.end; 62 if (begin < current_first_time) { 63 current_first_time = begin; 64 return begin; 65 } else { 66 return end; 67 } 68 }); 69 70 timelineX = d3.time.scale() 71 .range([0, timelineWidth * timelineScale]) 72 .domain(timeExtent); 73 maxDomain = timeExtent; 74 75 var timeFormat = d3.time.format.multi([ 76 [".%L", function(d) { 77 return d.getMilliseconds(); 78 }], 79 [":%S", function(d) { 80 return d.getSeconds(); 81 }], 82 ["%I:%M", function(d) { 83 return d.getMinutes(); 84 }], 85 ["%I %p", function(d) { 86 return d.getHours(); 87 }], 88 ["%a %d", function(d) { 89 return d.getDay() && d.getDate() != 1; 90 }], 91 ["%b %d", function(d) { 92 return d.getDate() != 1; 93 }], 94 ["%b", function(d) { 95 return d.getMonth(); 96 }], 97 ["%Y", function() { 98 return true; 99 }] 100 ]); 101 102 timelineXAxis = d3.svg.axis() 103 .scale(timelineX) 104 .orient('bottom') 105 .ticks(6) 106 .tickPadding(8) 107 .tickFormat(timeFormat); 108 109 110 // Compute Y axis 111 var valueExtent = d3.extent(this.graphMaxData, function(d) { 112 return d.value; 113 }); 114 115 timelineY = d3.scale.linear() 116 .range([timelineHeight, 0]) 117 .domain([0, valueExtent[1]]); 118 119 var changeRequest; 120 zoom = d3.behavior.zoom().x(timelineX) 121 .y(timelineY) 122 .scaleExtent([0.5, timelineWidth]) 123 .on("zoom", timelineZoom) 124 .on('zoomstart', function() { 125 clearTimeout(changeRequest); 126 }) 127 .on('zoomend', function() { 128 clearTimeout(changeRequest); 129 timelineSelection = timelineX.domain(); 130 changeRequest = setTimeout(function() { 131 var timeQuery = null; 132 if (timelineSelection != null && timelineSelection.length > 0) { 133 timeQuery = (+timelineSelection[0]) + "," + (+timelineSelection[1]); 134 } 135 if (lastQuery != timeQuery) { 136 lastQuery = timeQuery; 137 me.model.from = moment(timelineSelection[0]).format('DD-MM-YYYY'); 138 me.model.to = moment(timelineSelection[1]).format('DD-MM-YYYY'); 139 me.callback(me.model.from, me.model.to); 140 } 141 }, 500); 142 143 }); 144 timelineSvg = container.append('svg') 145 .attr('width', timelineWidth * timelineScale + margin.left + margin.right) 146 .attr('height', timelineHeight + margin.top + margin.bottom) 147 .call(zoom); 148 149 var root = timelineSvg.append('g') 150 .attr('class', 'context') 151 .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') 152 root.append('g').attr('class', 'all-data-root'); 153 root.append('g').attr('class', 'data-root'); 154 155 timelineArea = d3.svg.area() 156 .x(function(d) { 157 return timelineX(d.begin); 158 }) 159 .y0(timelineHeight) 160 .y1(function(d) { 161 return timelineY(d.value); 162 }); 163 164 timelineSelection = timelineX.domain(); 165 166 refreshGraphData(); 167 168 timelineSvg 169 .append('g') 170 .attr('class', 'x timeline') 171 .attr('transform', 'scale(1)') 172 .selectAll('rect') 173 .attr('y',0) 174 .attr('height', timelineHeight); 175 176 timelineSvg 177 .append('g') 178 .attr('class', 'x axis') 179 .attr('transform', 'translate(0,' + (timelineHeight) + ')') 180 .call(timelineXAxis); 181
182 initAppControls(); 183 184 this.initialized = true; 185 }; 186 187 this.recomputeSize = function () { 188 // if we have never been initialized: do it now 189 if (!this.initialized) { 190 this.initialize(this.graphData, this.callback); 191 } 192 }; 193 194 function initAppControls() { 195 d3.select(element).selectAll('.zoomBtn').on('click', function(event) { 196 var scale = zoom.scale(), 197 extent = zoom.scaleExtent(), 198 translate = zoom.translate(), 199 x = translate[0], 200 y = translate[1], 201 factor = (this.getAttribute('rel') === 'zoomIn') ? 1.5 : 1 / 1.5, 202 target_scale = scale * factor; 203 204 // If we're already at an extent, done 205 if (target_scale === extent[0] || target_scale === extent[1]) { 206 return false; 207 } 208 209 // If the factor is too much, scale it down to reach the extent exactly 210 var clamped_target_scale = Math.max(extent[0], Math.min(extent[1], target_scale)); 211 if (clamped_target_scale != target_scale) { 212 target_scale = clamped_target_scale; 213 factor = target_scale / scale; 214 } 215 216 // Center each vector, stretch, then put back 217 center = [timelineWidth / 2, timelineHeight / 2]; 218 x = (x - center[0]) * factor + center[0]; 219 y = (y - center[1]) * factor + center[1]; 220 221 // Transition to the new view over 350ms 222 d3.transition().duration(350).tween("zoom", function() { 223 var interpolate_scale = d3.interpolate(scale, target_scale), 224 interpolate_trans = d3.interpolate(translate, [x, y]); 225 timelineXTranslate = translate[0]; 226 timelineXScale = target_scale; 227 228 return function(t) { 229 zoom.scale(interpolate_scale(t)) 230 .translate(interpolate_trans(t)); 231 timelineZoom(); 232 timelineSvg.call(zoom.event); 233 }; 234 }); 235 }); 236 } 237 238 function timelineZoom() { 239 // prevent event propagation if possible 240 if (d3.event && d3.event.sourceEvent) { 241 d3.event.sourceEvent.preventDefault(); 242 d3.event.sourceEvent.stopPropagation(); 243 } 244 245 // get transformation 246 if (d3.event !== null) { 247 timelineXTranslate = d3.event.translate[0]; 248 timelineXScale = d3.event.scale; 249 applyZoom(); 250 251 } else { 252 var container = d3.select(element); 253 254 // calculate the zone 255 refreshGraphData(); 256 container.select('svg').select(".x.axis").call(timelineXAxis); 257 } 258 } 259 260 function applyZoom() { 261 // calculate zone 262 refreshGraphData(); 263 setZoom(timelineXTranslate, timelineXScale); 264 } 265 266 function setZoom(translate, scale) { 267 var container = d3.select(element); 268 var context = container.select('svg').select('g'); 269 container.select('svg').select(".x.axis").call(timelineXAxis); 270 if (!me.options.showAsHistogram) { 271 context.select(".all-data-root").attr("transform", "translate(" + translate + ",0)scale(" + scale + ", 1)"); 272 } 273 } 274 275 // will be used to filter out invisible blocks 276 function isVisible(d) { 277 var begin = timelineX(d.begin); 278 var end = timelineX(d.end); 279 return end >= 0 && begin <= timelineWidth; 280 } 281 282 function refreshGraphMaxData() { 283 var container = d3.select(element); 284 var context = container.select('svg').select('g.all-data-root'); 285 286 if (me.options.showAsHistogram) { 287 var all = context.selectAll('rect.areaAll') 288 .data(me.graphMaxData, function(d) { return d.begin; }); 289 all.enter() 290 .append('rect').attr('class', 'areaAll'); 291 all.style('display', 'none') 292 .filter(isVisible).style('display', 'block') 293 .attr("x", function(d) { return timelineX(d.begin); }) 294 .attr("width", function(d) { 295 return Math.max( 296 // min width is 3px 297 timelineX(d.end) - timelineX(d.begin), 298 3 / timelineScale 299 ); 300 }) 301 .attr("y", function(d) { return timelineY(d.value); }) 302 .attr("height", function(d) { return timelineHeight - timelineY(d.value); }); 303 all.exit() 304 .remove(); 305 } else { 306 context 307 .append("path") 308 .datum(me.graphMaxData) 309 .attr("class", "areaAll") 310 .attr("d", timelineArea); 311 } 312 } 313 314 function refreshGraphData() { 315 var container = d3.select(element); 316 var context = container.select('svg').select('g.data-root'); 317 318 var valueExtent = d3.extent(me.graphData, function(d) { 319 return d.value; 320 }); 321 if (valueExtent[1] > maxValue) { 322 me.graphMaxData = me.graphData; 323 maxValue = valueExtent[1]; 324 timelineY = d3.scale.linear() 325 .range([timelineHeight, 0]) 326 .domain([0, maxValue]); 327 } 328 refreshGraphMaxData(); 329 330 if (me.options.showAsHistogram) { 331 var all = context.selectAll('rect.area') 332 .data(me.graphData, function(d) { return d.begin; }); 333 all.enter() 334 .append('rect').attr('class', 'area'); 335 all.style('display', 'none') 336 .filter(isVisible).style('display', 'block') 337 .attr("x", function(d) { return timelineX(d.begin); }) 338 .attr("width", function(d) { 339 // min width is 3px 340 return Math.max( 341 timelineX(d.end) - timelineX(d.begin), 342 3 / timelineScale 343 ); 344 }) 345 .attr("y", function(d) { return timelineY(d.value); }) 346 .attr("height", function(d) { return timelineHeight - timelineY(d.value); }); 347 all.exit() 348 .remove(); 349 } else { 350 context.selectAll('path.area').remove(); 351 context 352 .append("path") 353 .datum(me.graphData) 354 .attr("class", "area") 355 .attr("d", timelineArea); 356 } 357 } 358 359 this.setTimeline = function (data) { 360 this.graphMaxData = this.graphMaxData || data; 361 this.graphData = data; 362 363 // initialize if it hasn't been done 364 if (!this.initialized) { 365 this.initialize(data, this.callback); 366 } else { 367 refreshGraphData(); 368 } 369 }; 370 371 // set the dates range; `from` and `to` are dates in DD-MM-YYYY format 372 // if a date is null it is assumed to be the min/max boundary 373 this.setDateRange = function (from, to) { 374 if (!this.initialized) { return; } 375 376 var domain = maxDomain; 377 var currentBegin = moment(timelineX.domain()[0]).startOf('day').valueOf(); 378 var currentEnd = moment(timelineX.domain()[1]).startOf('day').valueOf(); 379 var begin = from === null ? domain[0] : moment(from, 'DD-MM-YYYY').valueOf(); 380 var end = to === null ? domain[1] : moment(to, 'DD-MM-YYYY').valueOf(); 381 382 if (currentBegin == begin && currentEnd == end) { 383 return; 384 } 385 386 var graphCenter = [timelineWidth / 2, timelineHeight / 2]; 387 var targetCenter = (begin + end) / 2; 388 var targetScale = (domain[1] - domain[0]) / (end - begin); 389 var targetTranslate = [ 390 - (begin - domain[0]) / (domain[1] - domain[0]) * timelineWidth * targetScale, 391 graphCenter[1] 392 ]; 393 394 // apply scale & translate
395 zoom.scale(targetScale).translate(targetTranslate); 396 timelineZoom(); 397 timelineXTranslate = targetTranslate[0]; 398 timelineXScale = targetScale; 399 applyZoom(); 400 }; 401}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.