| <!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/iteration_helpers.html"> |
| <link rel="import" href="/tracing/base/raf.html"> |
| <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); |
| Polymer.dom(this).classList.add('chart-base-2d'); |
| this.xScale_ = d3.scale.linear(); |
| this.yScale_ = d3.scale.linear(); |
| this.isYLogScale_ = false; |
| this.yLogScaleMin_ = undefined; |
| this.dataRange_ = new tr.b.Range(); |
| this.hideXAxis_ = false; |
| this.hideYAxis_ = false; |
| this.data_ = []; |
| |
| 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 hideXAxis() { |
| return this.hideXAxis_; |
| }, |
| |
| set hideXAxis(h) { |
| this.hideXAxis_ = h; |
| this.updateContents_(); |
| }, |
| |
| get hideYAxis() { |
| return this.hideYAxis_; |
| }, |
| |
| set hideYAxis(h) { |
| this.hideYAxis_ = h; |
| this.updateContents_(); |
| }, |
| |
| 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.updateDataRange_(); |
| this.updateContents_(); |
| }, |
| |
| set isYLogScale(logScale) { |
| if (logScale) |
| this.yScale_ = d3.scale.log(10); |
| else |
| this.yScale_ = d3.scale.linear(); |
| this.isYLogScale_ = logScale; |
| }, |
| |
| getYScaleMin_: function() { |
| return this.isYLogScale_ ? this.yLogScaleMin_ : 0; |
| }, |
| |
| getYScaleDomain_: function(minValue, maxValue) { |
| if (this.isYLogScale_) |
| return [this.getYScaleMin_(), maxValue]; |
| return [Math.min(minValue, this.getYScaleMin_()), maxValue]; |
| }, |
| |
| 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; |
| }, |
| |
| updateSeriesKeys_: function() { |
| // Don't clear seriesByKey_; the caller might have put state in it using |
| // getDataSeries() before setting data. |
| this.data_.forEach(function(datum) { |
| Object.keys(datum).forEach(function(key) { |
| if (this.isDatumFieldSeries_(key)) |
| this.getDataSeries(key); |
| }, this); |
| }, this); |
| }, |
| |
| 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(); |
| for (var i = 0; i < this.data_.length; i++) { |
| for (var key in this.data_[i]) { |
| if (!isNaN(Math.max(this.data_[i][key]))) |
| yRange.addValue(this.data_[i][key]); |
| } |
| } |
| |
| 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; |
| if (this.hideXAxis) |
| return; |
| |
| this.drawXAxis_(xAxis); |
| }, |
| |
| drawXAxis_: function(xAxis) { |
| xAxis.attr('transform', 'translate(0,' + this.chartAreaSize.height + ')') |
| .call(d3.svg.axis() |
| .scale(this.xScale_) |
| .orient('bottom')); |
| tr.b.requestAnimationFrame(this.drawXAxisTicks_.bind(this, xAxis)); |
| }, |
| |
| drawXAxisTicks_: function(xAxis) { |
| 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; |
| }, |
| |
| updateDataRange_: function() { |
| var dataBySeriesKey = this.getDataBySeriesKey_(); |
| this.dataRange_.reset(); |
| tr.b.iterItems(dataBySeriesKey, function(series, values) { |
| for (var i = 0; i < values.length; i++) { |
| this.dataRange_.addValue(values[i][series]); |
| } |
| }, this); |
| |
| // Choose the closest power of 10, rounded down, as the smallest tick |
| // to display. |
| this.yLogScaleMin_ = undefined; |
| if (this.dataRange_.min !== undefined) { |
| var minValue = this.dataRange_.min; |
| if (minValue == 0) |
| minValue = 1; |
| |
| var onePowerLess = Math.floor( |
| Math.log(minValue) / Math.log(10)) - 1; |
| this.yLogScaleMin_ = Math.pow(10, onePowerLess); |
| } |
| }, |
| |
| updateYAxis_: function(yAxis) { |
| yAxis.selectAll('*').remove(); |
| yAxis[0][0].style.opacity = 0; |
| if (this.hideYAxis) |
| return; |
| |
| this.drawYAxis_(yAxis); |
| }, |
| |
| drawYAxis_: function(yAxis) { |
| var axisModifier = d3.svg.axis() |
| .scale(this.yScale_) |
| .orient('left'); |
| |
| if (this.isYLogScale_) { |
| if (this.yLogScaleMin_ === undefined) |
| return; |
| var minValue = this.dataRange_.min; |
| if (minValue == 0) |
| minValue = 1; |
| |
| var largestPower = Math.ceil( |
| Math.log(this.dataRange_.max) / Math.log(10)) + 1; |
| var smallestPower = Math.floor( |
| Math.log(minValue) / Math.log(10)); |
| var tickValues = []; |
| for (var i = smallestPower; i < largestPower; i++) { |
| tickValues.push(Math.pow(10, i)); |
| } |
| |
| axisModifier = axisModifier |
| .tickValues(tickValues) |
| .tickFormat(function(d) { |
| return d; |
| }); |
| } |
| |
| yAxis.call(axisModifier); |
| |
| tr.b.requestAnimationFrame(this.drawYAxisTicks_.bind(this, yAxis)); |
| }, |
| |
| drawYAxisTicks_: function(yAxis) { |
| 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; |
| } |
| }); |
| |
| leftMargin = parseInt(Math.ceil(leftMargin)); |
| if (leftMargin > this.margin.left) { |
| this.margin.left = leftMargin; |
| this.updateContents_(); |
| } else { |
| yAxis[0][0].style.opacity = 1; |
| } |
| }, |
| |
| 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 = {}; |
| for (var [key, series] of this.seriesByKey_) { |
| dataBySeriesKey[key] = []; |
| } |
| |
| 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; |
| |
| if (!this.isDatumFieldSeries_(seriesKey)) |
| return; |
| |
| var singleSeriesDatum = {x: x}; |
| singleSeriesDatum[seriesKey] = multiSeriesDatum[seriesKey]; |
| dataBySeriesKey[seriesKey].push(singleSeriesDatum); |
| }, this); |
| }, this); |
| |
| return dataBySeriesKey; |
| }, |
| |
| getChartPointAtClientPoint_: function(clientPoint) { |
| var rect = this.getBoundingClientRect(); |
| return { |
| x: clientPoint.x - rect.left - this.margin.left, |
| y: clientPoint.y - rect.top - this.margin.top |
| }; |
| }, |
| |
| getDataPointAtChartPoint_: function(chartPoint) { |
| return { |
| x: tr.b.clamp(this.xScale_.invert(chartPoint.x), |
| this.xScale_.domain()[0], this.xScale_.domain()[1]), |
| y: tr.b.clamp(this.yScale_.invert(chartPoint.y), |
| this.yScale_.domain()[0], this.yScale_.domain()[1]) |
| }; |
| }, |
| |
| getDataPointAtClientPoint_: function(clientX, clientY) { |
| var chartPoint = this.getChartPointAtClientPoint_( |
| {x: clientX, y: clientY}); |
| return this.getDataPointAtChartPoint_(chartPoint); |
| }, |
| |
| 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; |
| Polymer.dom(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); |
| Polymer.dom(this).classList.remove('updating-brushing-state'); |
| } |
| }; |
| |
| return { |
| ChartBase2D: ChartBase2D |
| }; |
| }); |
| </script> |