| <!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="/base/ui.html"> |
| <link rel="import" href="/base/ui/color_scheme.html"> |
| <link rel="import" href="/base/ui/d3.html"> |
| |
| <style> |
| * /deep/ .chart-base #title { |
| font-size: 16pt; |
| } |
| |
| * /deep/ .chart-base { |
| font-size: 12pt; |
| -webkit-user-select: none; |
| cursor: default; |
| } |
| |
| * /deep/ .chart-base .axis path, |
| * /deep/ .chart-base .axis line { |
| fill: none; |
| shape-rendering: crispEdges; |
| stroke: #000; |
| } |
| </style> |
| |
| <template id="chart-base-template"> |
| <svg> <!-- svg tag is dropped by ChartBase.decorate. --> |
| <g xmlns="http://www.w3.org/2000/svg" id="chart-area"> |
| <g class="x axis"></g> |
| <g class="y axis"></g> |
| <text id="title"></text> |
| </g> |
| </svg> |
| </template> |
| |
| <script> |
| 'use strict'; |
| |
| tv.exportTo('tv.b.ui', function() { |
| var THIS_DOC = document.currentScript.ownerDocument; |
| |
| var svgNS = 'http://www.w3.org/2000/svg'; |
| var highlightIdBoost = tv.b.ui.getColorPaletteHighlightIdBoost(); |
| |
| function getColorOfKey(key, selected) { |
| var id = tv.b.ui.getColorIdForGeneralPurposeString(key); |
| if (selected) |
| id += highlightIdBoost; |
| return tv.b.ui.getColorPalette()[id]; |
| } |
| |
| /** |
| * A virtual base class for basic charts that provides X and Y axes, if |
| * needed, a title, and legend. |
| * |
| * @constructor |
| */ |
| var ChartBase = tv.b.ui.define('svg', undefined, svgNS); |
| |
| ChartBase.prototype = { |
| __proto__: HTMLUnknownElement.prototype, |
| |
| decorate: function() { |
| this.classList.add('chart-base'); |
| this.chartTitle_ = undefined; |
| this.data_ = undefined; |
| this.seriesKeys_ = undefined; |
| this.width_ = 400; |
| this.height_ = 300; |
| |
| // This should use tv.b.instantiateTemplate. However, creating |
| // svg-namespaced elements inside a template isn't possible. Thus, this |
| // hack. |
| var template = THIS_DOC.querySelector('#chart-base-template'); |
| var svgEl = template.content.querySelector('svg'); |
| for (var i = 0; i < svgEl.children.length; i++) |
| this.appendChild(svgEl.children[i].cloneNode(true)); |
| |
| // svg likes to take over width & height properties for some reason. This |
| // works around it. |
| Object.defineProperty( |
| this, 'width', { |
| get: function() { |
| return this.width_; |
| }, |
| set: function(width) { |
| this.width_ = width; |
| this.updateContents_(); |
| } |
| }); |
| Object.defineProperty( |
| this, 'height', { |
| get: function() { |
| return this.height_; |
| }, |
| set: function(height) { |
| this.height_ = height; |
| this.updateContents_(); |
| } |
| }); |
| }, |
| |
| get chartTitle() { |
| return chartTitle_; |
| }, |
| |
| set chartTitle(chartTitle) { |
| this.chartTitle_ = chartTitle; |
| this.updateContents_(); |
| }, |
| |
| get chartAreaElement() { |
| return this.querySelector('#chart-area'); |
| }, |
| |
| get data() { |
| return this.data_; |
| }, |
| |
| setSize: function(size) { |
| this.width_ = size.width; |
| this.height_ = size.height; |
| this.updateContents_(); |
| }, |
| |
| get margin() { |
| var margin = {top: 20, right: 20, bottom: 30, left: 50}; |
| if (this.chartTitle_) |
| margin.top += 20; |
| return margin; |
| }, |
| |
| get chartAreaSize() { |
| var margin = this.margin; |
| return { |
| width: this.width_ - margin.left - margin.right, |
| height: this.height_ - margin.top - margin.bottom |
| }; |
| }, |
| |
| getLegendKeys_: function() { |
| throw new Error('Not implemented'); |
| }, |
| |
| updateScales_: function(width, height) { |
| throw new Error('Not implemented'); |
| }, |
| |
| updateContents_: function() { |
| var margin = this.margin; |
| var width = this.chartAreaSize.width; |
| var height = this.chartAreaSize.height; |
| |
| var thisSel = d3.select(this); |
| thisSel.attr('width', this.width_); |
| thisSel.attr('height', this.height_); |
| |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| chartAreaSel.attr( |
| 'transform', |
| 'translate(' + margin.left + ',' + margin.top + ')'); |
| |
| this.updateScales_(width, height); |
| |
| // Axes. |
| if (this.xScale_ && this.yScale_) { |
| var xAxisRenderer = d3.svg.axis() |
| .scale(this.xScale_) |
| .orient('bottom'); |
| |
| var yAxisRenderer = d3.svg.axis() |
| .scale(this.yScale_) |
| .orient('left'); |
| |
| chartAreaSel.select('.x.axis') |
| .attr('transform', 'translate(0,' + height + ')') |
| .call(xAxisRenderer); |
| |
| chartAreaSel.select('.y.axis') |
| .call(yAxisRenderer); |
| } |
| |
| // Title. |
| var titleSel = chartAreaSel.select('#title'); |
| if (this.chartTitle_) { |
| titleSel.attr('transform', 'translate(' + width * 0.5 + ',-5)') |
| .style('display', undefined) |
| .style('text-anchor', 'middle') |
| .attr('class', 'title') |
| .attr('width', width) |
| .text(this.chartTitle_); |
| } else { |
| titleSel.style('display', 'none'); |
| } |
| |
| // Basics |
| this.updateLegend_(); |
| }, |
| |
| updateLegend_: function() { |
| var keys = this.getLegendKeys_(); |
| if (keys === undefined) |
| return; |
| |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| var chartAreaSize = this.chartAreaSize; |
| |
| var legendEntriesSel = chartAreaSel.selectAll('.legend') |
| .data(keys.slice().reverse()); |
| |
| legendEntriesSel.enter() |
| .append('g') |
| .attr('class', 'legend') |
| .attr('transform', function(d, i) { |
| return 'translate(0,' + i * 20 + ')'; |
| }).append('text').text(function(key) { |
| return key; |
| }); |
| legendEntriesSel.exit().remove(); |
| |
| legendEntriesSel.attr('x', chartAreaSize.width - 18) |
| .attr('width', 18) |
| .attr('height', 18) |
| .style('fill', function(key) { |
| var selected = this.currentHighlightedLegendKey === key; |
| return getColorOfKey(key, selected); |
| }.bind(this)); |
| |
| legendEntriesSel.selectAll('text') |
| .attr('x', chartAreaSize.width - 24) |
| .attr('y', 9) |
| .attr('dy', '.35em') |
| .style('text-anchor', 'end') |
| .text(function(d) { return d; }); |
| }, |
| |
| get highlightedLegendKey() { |
| return this.highlightedLegendKey_; |
| }, |
| |
| set highlightedLegendKey(highlightedLegendKey) { |
| this.highlightedLegendKey_ = highlightedLegendKey; |
| this.updateHighlight_(); |
| }, |
| |
| get currentHighlightedLegendKey() { |
| if (this.tempHighlightedLegendKey_) |
| return this.tempHighlightedLegendKey_; |
| return this.highlightedLegendKey_; |
| }, |
| |
| pushTempHighlightedLegendKey: function(key) { |
| if (this.tempHighlightedLegendKey_) |
| throw new Error('push cannot nest'); |
| this.tempHighlightedLegendKey_ = key; |
| this.updateHighlight_(); |
| }, |
| |
| popTempHighlightedLegendKey: function(key) { |
| if (this.tempHighlightedLegendKey_ != key) |
| throw new Error('pop cannot happen'); |
| this.tempHighlightedLegendKey_ = undefined; |
| this.updateHighlight_(); |
| }, |
| |
| updateHighlight_: function() { |
| // Update label colors. |
| var chartAreaSel = d3.select(this.chartAreaElement); |
| var legendEntriesSel = chartAreaSel.selectAll('.legend'); |
| |
| var that = this; |
| legendEntriesSel.each(function(key) { |
| var highlighted = key == that.currentHighlightedLegendKey; |
| var color = getColorOfKey(key, highlighted); |
| this.style.fill = color; |
| if (highlighted) |
| this.style.fontWeight = 'bold'; |
| else |
| this.style.fontWeight = ''; |
| }); |
| } |
| }; |
| |
| return { |
| getColorOfKey: getColorOfKey, |
| ChartBase: ChartBase |
| }; |
| }); |
| </script> |