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