| <!DOCTYPE html> |
| <!-- |
| Copyright (c) 2014 The Chromium Authors. All rights reserved. |
| Use of this source code is governed by a BSD-style license that can be |
| found in the LICENSE file. |
| --> |
| |
| <link rel="import" href="/tracing/base/range.html"> |
| <link rel="import" href="/tracing/ui/base/chart_base.html"> |
| <link rel="import" href="/tracing/ui/base/mouse_tracker.html"> |
| |
| <style> |
| * /deep/ .chart-base-2d.updating-brushing-state #brushes > * { |
| fill: rgb(103, 199, 165) |
| } |
| |
| * /deep/ .chart-base-2d #brushes { |
| fill: rgb(213, 236, 229) |
| } |
| </style> |
| |
| <script> |
| 'use strict'; |
| |
| tr.exportTo('tr.ui.b', function() { |
| var ChartBase = tr.ui.b.ChartBase; |
| var ChartBase2D = tr.ui.b.define('chart-base-2d', ChartBase); |
| |
| ChartBase2D.prototype = { |
| __proto__: ChartBase.prototype, |
| |
| decorate: function() { |
| ChartBase.prototype.decorate.call(this); |
| this.classList.add('chart-base-2d'); |
| this.xScale_ = d3.scale.linear(); |
| this.yScale_ = d3.scale.linear(); |
| |
| this.data_ = []; |
| this.seriesKeys_ = []; |
| this.leftMargin_ = 50; |
| |
| d3.select(this.chartAreaElement) |
| .append('g') |
| .attr('id', 'brushes'); |
| d3.select(this.chartAreaElement) |
| .append('g') |
| .attr('id', 'series'); |
| |
| this.addEventListener('mousedown', this.onMouseDown_.bind(this)); |
| }, |
| |
| get data() { |
| return this.data_; |
| }, |
| |
| /** |
| * Sets the data array for the object |
| * |
| * @param {Array} data The data. Each element must be an object, with at |
| * least an x property. All other properties become series names in the |
| * chart. The data can be sparse (i.e. every x value does not have to |
| * contain data for every series). |
| */ |
| set data(data) { |
| if (data === undefined) |
| throw new Error('data must be an Array'); |
| |
| this.data_ = data; |
| this.updateSeriesKeys_(); |
| this.updateContents_(); |
| }, |
| |
| getSampleWidth_: function(data, index, leftSide) { |
| var leftIndex, rightIndex; |
| if (leftSide) { |
| leftIndex = Math.max(index - 1, 0); |
| rightIndex = index; |
| } else { |
| leftIndex = index; |
| rightIndex = Math.min(index + 1, data.length - 1); |
| } |
| var leftWidth = this.getXForDatum_(data[index], index) - |
| this.getXForDatum_(data[leftIndex], leftIndex); |
| var rightWidth = this.getXForDatum_(data[rightIndex], rightIndex) - |
| this.getXForDatum_(data[index], index); |
| return leftWidth * 0.5 + rightWidth * 0.5; |
| }, |
| |
| getLegendKeys_: function() { |
| if (this.seriesKeys_ && |
| this.seriesKeys_.length > 1) |
| return this.seriesKeys_.slice(); |
| return []; |
| }, |
| |
| updateSeriesKeys_: function() { |
| // Accumulate the keys on each data point. |
| var keySet = {}; |
| this.data_.forEach(function(datum) { |
| Object.keys(datum).forEach(function(key) { |
| if (this.isDatumFieldSeries_(key)) |
| keySet[key] = true; |
| }, this); |
| }, this); |
| this.seriesKeys_ = Object.keys(keySet); |
| }, |
| |
| isDatumFieldSeries_: function(fieldName) { |
| throw new Error('Not implemented'); |
| }, |
| |
| getXForDatum_: function(datum, index) { |
| throw new Error('Not implemented'); |
| }, |
| |
| updateScales_: function() { |
| if (this.data_.length === 0) |
| return; |
| |
| var width = this.chartAreaSize.width; |
| var height = this.chartAreaSize.height; |
| |
| // X. |
| this.xScale_.range([0, width]); |
| this.xScale_.domain(d3.extent(this.data_, this.getXForDatum_.bind(this))); |
| |
| // Y. |
| var yRange = new tr.b.Range(); |
| this.data_.forEach(function(datum) { |
| this.seriesKeys_.forEach(function(key) { |
| // Allow for sparse data |
| if (datum[key] !== undefined) |
| yRange.addValue(datum[key]); |
| }); |
| }, this); |
| |
| this.yScale_.range([height, 0]); |
| this.yScale_.domain([yRange.min, yRange.max]); |
| }, |
| |
| updateBrushContents_: function(brushSel) { |
| brushSel.selectAll('*').remove(); |
| }, |
| |
| updateXAxis_: function(xAxis) { |
| xAxis.selectAll('*').remove(); |
| xAxis[0][0].style.opacity = 0; |
| xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')') |
| .call(d3.svg.axis() |
| .scale(this.xScale_) |
| .orient('bottom')); |
| window.requestAnimationFrame(function() { |
| var previousRight = undefined; |
| xAxis.selectAll('.tick')[0].forEach(function(tick) { |
| var currentLeft = tick.transform.baseVal[0].matrix.e; |
| if ((previousRight === undefined) || |
| (currentLeft > (previousRight + 3))) { |
| var currentWidth = tick.getBBox().width; |
| previousRight = currentLeft + currentWidth; |
| } else { |
| tick.style.opacity = 0; |
| } |
| }); |
| xAxis[0][0].style.opacity = 1; |
| }); |
| }, |
| |
| getMargin_: function() { |
| var margin = ChartBase.prototype.getMargin_.call(this); |
| margin.left = this.leftMargin_; |
| return margin; |
| }, |
| |
| updateYAxis_: function(yAxis) { |
| yAxis.selectAll('*').remove(); |
| yAxis[0][0].style.opacity = 0; |
| yAxis.call(d3.svg.axis() |
| .scale(this.yScale_) |
| .orient('left')); |
| window.requestAnimationFrame(function() { |
| var previousTop = undefined; |
| var leftMargin = 0; |
| yAxis.selectAll('.tick')[0].forEach(function(tick) { |
| var bbox = tick.getBBox(); |
| leftMargin = Math.max(leftMargin, bbox.width); |
| var currentTop = tick.transform.baseVal[0].matrix.f; |
| var currentBottom = currentTop + bbox.height; |
| if ((previousTop === undefined) || |
| (previousTop > (currentBottom + 3))) { |
| previousTop = currentTop; |
| } else { |
| tick.style.opacity = 0; |
| } |
| }); |
| if (leftMargin > this.leftMargin_) { |
| this.leftMargin_ = leftMargin; |
| this.updateContents_(); |
| } else { |
| yAxis[0][0].style.opacity = 1; |
| } |
| }.bind(this)); |
| }, |
| |
| updateContents_: function() { |
| ChartBase.prototype.updateContents_.call(this); |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| this.updateXAxis_(chartAreaSel.select('.x.axis')); |
| this.updateYAxis_(chartAreaSel.select('.y.axis')); |
| this.updateBrushContents_(chartAreaSel.select('#brushes')); |
| this.updateDataContents_(chartAreaSel.select('#series')); |
| }, |
| |
| updateDataContents_: function(seriesSel) { |
| throw new Error('Not implemented'); |
| }, |
| |
| /** |
| * Returns a map of series key to the data for that series. |
| * |
| * Example: |
| * // returns {y: [{x: 1, y: 1}, {x: 3, y: 3}], z: [{x: 2, z: 2}]} |
| * this.data_ = [{x: 1, y: 1}, {x: 2, z: 2}, {x: 3, y: 3}]; |
| * this.getDataBySeriesKey_(); |
| * @return {Object} A map of series data by series key. |
| */ |
| getDataBySeriesKey_: function() { |
| var dataBySeriesKey = {}; |
| this.seriesKeys_.forEach(function(seriesKey) { |
| dataBySeriesKey[seriesKey] = []; |
| }); |
| |
| this.data_.forEach(function(multiSeriesDatum, index) { |
| var x = this.getXForDatum_(multiSeriesDatum, index); |
| |
| d3.keys(multiSeriesDatum).forEach(function(seriesKey) { |
| // Skip 'x' - it's not a series |
| if (seriesKey === 'x') |
| return; |
| |
| if (multiSeriesDatum[seriesKey] === undefined) |
| return; |
| |
| var singleSeriesDatum = {x: x}; |
| singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey]; |
| dataBySeriesKey[seriesKey].push(singleSeriesDatum); |
| }); |
| }, this); |
| |
| return dataBySeriesKey; |
| }, |
| |
| getDataPointAtClientPoint_: function(clientX, clientY) { |
| var rect = this.getBoundingClientRect(); |
| var margin = this.margin; |
| var x = clientX - rect.left - margin.left; |
| var y = clientY - rect.top - margin.top; |
| x = this.xScale_.invert(x); |
| y = this.yScale_.invert(y); |
| x = tr.b.clamp(x, this.xScale_.domain()[0], this.xScale_.domain()[1]); |
| y = tr.b.clamp(y, this.yScale_.domain()[0], this.yScale_.domain()[1]); |
| return {x: x, y: y}; |
| }, |
| |
| prepareDataEvent_: function(mouseEvent, dataEvent) { |
| var dataPoint = this.getDataPointAtClientPoint_( |
| mouseEvent.clientX, mouseEvent.clientY); |
| dataEvent.x = dataPoint.x; |
| dataEvent.y = dataPoint.y; |
| }, |
| |
| onMouseDown_: function(mouseEvent) { |
| tr.ui.b.trackMouseMovesUntilMouseUp( |
| this.onMouseMove_.bind(this, mouseEvent.button), |
| this.onMouseUp_.bind(this, mouseEvent.button)); |
| mouseEvent.preventDefault(); |
| mouseEvent.stopPropagation(); |
| var dataEvent = new tr.b.Event('item-mousedown'); |
| dataEvent.button = mouseEvent.button; |
| this.classList.add('updating-brushing-state'); |
| this.prepareDataEvent_(mouseEvent, dataEvent); |
| this.dispatchEvent(dataEvent); |
| }, |
| |
| onMouseMove_: function(button, mouseEvent) { |
| if (mouseEvent.buttons !== undefined) { |
| mouseEvent.preventDefault(); |
| mouseEvent.stopPropagation(); |
| } |
| var dataEvent = new tr.b.Event('item-mousemove'); |
| dataEvent.button = button; |
| this.prepareDataEvent_(mouseEvent, dataEvent); |
| this.dispatchEvent(dataEvent); |
| }, |
| |
| onMouseUp_: function(button, mouseEvent) { |
| mouseEvent.preventDefault(); |
| mouseEvent.stopPropagation(); |
| var dataEvent = new tr.b.Event('item-mouseup'); |
| dataEvent.button = button; |
| this.prepareDataEvent_(mouseEvent, dataEvent); |
| this.dispatchEvent(dataEvent); |
| this.classList.remove('updating-brushing-state'); |
| } |
| }; |
| |
| return { |
| ChartBase2D: ChartBase2D |
| }; |
| }); |
| </script> |