1<!DOCTYPE html> 2<!-- 3Copyright (c) 2014 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/iteration_helpers.html"> 9<link rel="import" href="/tracing/base/range.html"> 10<link rel="import" href="/tracing/ui/base/chart_base.html"> 11<link rel="import" href="/tracing/ui/base/mouse_tracker.html"> 12 13<style> 14 * /deep/ .chart-base-2d.updating-brushing-state #brushes > * { 15 fill: rgb(103, 199, 165) 16 } 17 18 * /deep/ .chart-base-2d #brushes { 19 fill: rgb(213, 236, 229) 20 } 21</style> 22 23<script> 24'use strict'; 25 26tr.exportTo('tr.ui.b', function() { 27 var ChartBase = tr.ui.b.ChartBase; 28 var ChartBase2D = tr.ui.b.define('chart-base-2d', ChartBase); 29 30 ChartBase2D.prototype = { 31 __proto__: ChartBase.prototype, 32 33 decorate: function() { 34 ChartBase.prototype.decorate.call(this); 35 this.classList.add('chart-base-2d'); 36 this.xScale_ = d3.scale.linear(); 37 this.yScale_ = d3.scale.linear(); 38 this.isYLogScale_ = false; 39 this.yLogScaleMin_ = undefined; 40 this.dataRange_ = new tr.b.Range(); 41 42 this.data_ = []; 43 this.seriesKeys_ = []; 44 this.leftMargin_ = 50; 45 46 d3.select(this.chartAreaElement) 47 .append('g') 48 .attr('id', 'brushes'); 49 d3.select(this.chartAreaElement) 50 .append('g') 51 .attr('id', 'series'); 52 53 this.addEventListener('mousedown', this.onMouseDown_.bind(this)); 54 }, 55 56 get data() { 57 return this.data_; 58 }, 59 60 /** 61 * Sets the data array for the object 62 * 63 * @param {Array} data The data. Each element must be an object, with at 64 * least an x property. All other properties become series names in the 65 * chart. The data can be sparse (i.e. every x value does not have to 66 * contain data for every series). 67 */ 68 set data(data) { 69 if (data === undefined) 70 throw new Error('data must be an Array'); 71 72 this.data_ = data; 73 this.updateSeriesKeys_(); 74 this.updateDataRange_(); 75 this.updateContents_(); 76 }, 77 78 set isYLogScale(logScale) { 79 if (logScale) 80 this.yScale_ = d3.scale.log(10); 81 else 82 this.yScale_ = d3.scale.linear(); 83 this.isYLogScale_ = logScale; 84 }, 85 86 getYScaleMin_: function() { 87 return this.isYLogScale_ ? this.yLogScaleMin_ : 0; 88 }, 89 90 getYScaleDomain_: function(minValue, maxValue) { 91 if (this.isYLogScale_) 92 return [this.getYScaleMin_(), maxValue]; 93 return [Math.min(minValue, this.getYScaleMin_()), maxValue]; 94 }, 95 96 getSampleWidth_: function(data, index, leftSide) { 97 var leftIndex, rightIndex; 98 if (leftSide) { 99 leftIndex = Math.max(index - 1, 0); 100 rightIndex = index; 101 } else { 102 leftIndex = index; 103 rightIndex = Math.min(index + 1, data.length - 1); 104 } 105 var leftWidth = this.getXForDatum_(data[index], index) - 106 this.getXForDatum_(data[leftIndex], leftIndex); 107 var rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) - 108 this.getXForDatum_(data[index], index); 109 return leftWidth * 0.5 + rightWidth * 0.5; 110 }, 111 112 getLegendKeys_: function() { 113 if (this.seriesKeys_ && 114 this.seriesKeys_.length > 1) 115 return this.seriesKeys_.slice(); 116 return []; 117 }, 118 119 updateSeriesKeys_: function() { 120 // Accumulate the keys on each data point. 121 var keySet = {}; 122 this.data_.forEach(function(datum) { 123 Object.keys(datum).forEach(function(key) { 124 if (this.isDatumFieldSeries_(key)) 125 keySet[key] = true; 126 }, this); 127 }, this); 128 this.seriesKeys_ = Object.keys(keySet); 129 }, 130 131 isDatumFieldSeries_: function(fieldName) { 132 throw new Error('Not implemented'); 133 }, 134 135 getXForDatum_: function(datum, index) { 136 throw new Error('Not implemented'); 137 }, 138 139 updateScales_: function() { 140 if (this.data_.length === 0) 141 return; 142 143 var width = this.chartAreaSize.width; 144 var height = this.chartAreaSize.height; 145 146 // X. 147 this.xScale_.range([0, width]); 148 this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this))); 149 150 // Y. 151 var yRange = new tr.b.Range(); 152 this.data_.forEach(function(datum) { 153 this.seriesKeys_.forEach(function(key) { 154 // Allow for sparse data 155 if (datum[key] !== undefined) 156 yRange.addValue(datum[key]); 157 }); 158 }, this); 159 160 this.yScale_.range([height, 0]); 161 this.yScale_.domain([yRange.min, yRange.max]); 162 }, 163 164 updateBrushContents_: function(brushSel) { 165 brushSel.selectAll('*').remove(); 166 }, 167 168 updateXAxis_: function(xAxis) { 169 xAxis.selectAll('*').remove(); 170 xAxis[0][0].style.opacity = 0; 171 xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')') 172 .call(d3.svg.axis() 173 .scale(this.xScale_) 174 .orient('bottom')); 175 window.requestAnimationFrame(function() { 176 var previousRight = undefined; 177 xAxis.selectAll('.tick')[0].forEach(function(tick) { 178 var currentLeft = tick.transform.baseVal[0].matrix.e; 179 if ((previousRight === undefined) || 180 (currentLeft > (previousRight + 3))) { 181 var currentWidth = tick.getBBox().width; 182 previousRight = currentLeft + currentWidth; 183 } else { 184 tick.style.opacity = 0; 185 } 186 }); 187 xAxis[0][0].style.opacity = 1; 188 }); 189 }, 190 191 getMargin_: function() { 192 var margin = ChartBase.prototype.getMargin_.call(this); 193 margin.left = this.leftMargin_; 194 return margin; 195 }, 196 197 updateDataRange_: function() { 198 var dataBySeriesKey = this.getDataBySeriesKey_(); 199 this.dataRange_.reset(); 200 tr.b.iterItems(dataBySeriesKey, function(series, values) { 201 for (var i = 0; i < values.length; i++) { 202 this.dataRange_.addValue(values[i][series]); 203 } 204 }, this); 205 206 // Choose the closest power of 10, rounded down, as the smallest tick 207 // to display. 208 this.yLogScaleMin_ = undefined; 209 if (this.dataRange_.min !== undefined) { 210 var minValue = this.dataRange_.min; 211 if (minValue == 0) 212 minValue = 1; 213 214 var onePowerLess = Math.floor( 215 Math.log(minValue) / Math.log(10)) - 1; 216 this.yLogScaleMin_ = Math.pow(10, onePowerLess); 217 } 218 }, 219 220 updateYAxis_: function(yAxis) { 221 yAxis.selectAll('*').remove(); 222 yAxis[0][0].style.opacity = 0; 223 224 var axisModifier = d3.svg.axis() 225 .scale(this.yScale_) 226 .orient('left'); 227 228 if (this.isYLogScale_) { 229 if (this.yLogScaleMin_ === undefined) 230 return; 231 var minValue = this.dataRange_.min; 232 if (minValue == 0) 233 minValue = 1; 234 235 var largestPower = Math.ceil( 236 Math.log(this.dataRange_.max) / Math.log(10)) + 1; 237 var smallestPower = Math.floor( 238 Math.log(minValue) / Math.log(10)); 239 var tickValues = []; 240 for (var i = smallestPower; i < largestPower; i++) { 241 tickValues.push(Math.pow(10, i)); 242 } 243 244 axisModifier = axisModifier 245 .tickValues(tickValues) 246 .tickFormat(function(d) { 247 return d; 248 }); 249 } 250 251 yAxis.call(axisModifier); 252 253 window.requestAnimationFrame(function() { 254 var previousTop = undefined; 255 var leftMargin = 0; 256 yAxis.selectAll('.tick')[0].forEach(function(tick) { 257 var bbox = tick.getBBox(); 258 leftMargin = Math.max(leftMargin, bbox.width); 259 var currentTop = tick.transform.baseVal[0].matrix.f; 260 var currentBottom = currentTop + bbox.height; 261 if ((previousTop === undefined) || 262 (previousTop > (currentBottom + 3))) { 263 previousTop = currentTop; 264 } else { 265 tick.style.opacity = 0; 266 } 267 }); 268 if (leftMargin > this.leftMargin_) { 269 this.leftMargin_ = leftMargin; 270 this.updateContents_(); 271 } else { 272 yAxis[0][0].style.opacity = 1; 273 } 274 }.bind(this)); 275 }, 276 277 updateContents_: function() { 278 ChartBase.prototype.updateContents_.call(this); 279 var chartAreaSel = d3.select(this.chartAreaElement); 280 this.updateXAxis_(chartAreaSel.select('.x.axis')); 281 this.updateYAxis_(chartAreaSel.select('.y.axis')); 282 this.updateBrushContents_(chartAreaSel.select('#brushes')); 283 this.updateDataContents_(chartAreaSel.select('#series')); 284 }, 285 286 updateDataContents_: function(seriesSel) { 287 throw new Error('Not implemented'); 288 }, 289 290 /** 291 * Returns a map of series key to the data for that series. 292 * 293 * Example: 294 * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]} 295 * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}]; 296 * this.getDataBySeriesKey_(); 297 * @return {Object} A map of series data by series key. 298 */ 299 getDataBySeriesKey_: function() { 300 var dataBySeriesKey = {}; 301 this.seriesKeys_.forEach(function(seriesKey) { 302 dataBySeriesKey[seriesKey] = []; 303 }); 304 305 this.data_.forEach(function(multiSeriesDatum, index) { 306 var x = this.getXForDatum_(multiSeriesDatum, index); 307 308 d3.keys(multiSeriesDatum).forEach(function(seriesKey) { 309 // Skip 'x' - it's not a series 310 if (seriesKey === 'x') 311 return; 312 313 if (multiSeriesDatum[seriesKey] === undefined) 314 return; 315 316 if (!this.isDatumFieldSeries_(seriesKey)) 317 return; 318 319 var singleSeriesDatum = {x: x}; 320 singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey]; 321 dataBySeriesKey[seriesKey].push(singleSeriesDatum); 322 }, this); 323 }, this); 324 325 return dataBySeriesKey; 326 }, 327 328 getDataPointAtClientPoint_: function(clientX, clientY) { 329 var rect = this.getBoundingClientRect(); 330 var margin = this.margin; 331 var x = clientX - rect.left - margin.left; 332 var y = clientY - rect.top - margin.top; 333 x = this.xScale_.invert(x); 334 y = this.yScale_.invert(y); 335 x = tr.b.clamp(x, this.xScale_.domain()[0], this.xScale_.domain()[1]); 336 y = tr.b.clamp(y, this.yScale_.domain()[0], this.yScale_.domain()[1]); 337 return {x: x, y: y}; 338 }, 339 340 prepareDataEvent_: function(mouseEvent, dataEvent) { 341 var dataPoint = this.getDataPointAtClientPoint_( 342 mouseEvent.clientX, mouseEvent.clientY); 343 dataEvent.x = dataPoint.x; 344 dataEvent.y = dataPoint.y; 345 }, 346 347 onMouseDown_: function(mouseEvent) { 348 tr.ui.b.trackMouseMovesUntilMouseUp( 349 this.onMouseMove_.bind(this, mouseEvent.button), 350 this.onMouseUp_.bind(this, mouseEvent.button)); 351 mouseEvent.preventDefault(); 352 mouseEvent.stopPropagation(); 353 var dataEvent = new tr.b.Event('item-mousedown'); 354 dataEvent.button = mouseEvent.button; 355 this.classList.add('updating-brushing-state'); 356 this.prepareDataEvent_(mouseEvent, dataEvent); 357 this.dispatchEvent(dataEvent); 358 }, 359 360 onMouseMove_: function(button, mouseEvent) { 361 if (mouseEvent.buttons !== undefined) { 362 mouseEvent.preventDefault(); 363 mouseEvent.stopPropagation(); 364 } 365 var dataEvent = new tr.b.Event('item-mousemove'); 366 dataEvent.button = button; 367 this.prepareDataEvent_(mouseEvent, dataEvent); 368 this.dispatchEvent(dataEvent); 369 }, 370 371 onMouseUp_: function(button, mouseEvent) { 372 mouseEvent.preventDefault(); 373 mouseEvent.stopPropagation(); 374 var dataEvent = new tr.b.Event('item-mouseup'); 375 dataEvent.button = button; 376 this.prepareDataEvent_(mouseEvent, dataEvent); 377 this.dispatchEvent(dataEvent); 378 this.classList.remove('updating-brushing-state'); 379 } 380 }; 381 382 return { 383 ChartBase2D: ChartBase2D 384 }; 385}); 386</script> 387