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<link rel="import" href="/tracing/base/range.html"> 8<link rel="import" href="/tracing/ui/base/d3.html"> 9<link rel="import" href="/tracing/ui/base/dom_helpers.html"> 10<link rel="import" href="/tracing/ui/base/chart_base.html"> 11<link rel="stylesheet" href="/tracing/ui/base/pie_chart.css"> 12 13<script> 14'use strict'; 15 16tr.exportTo('tr.ui.b', function() { 17 var ChartBase = tr.ui.b.ChartBase; 18 var getColorOfKey = tr.ui.b.getColorOfKey; 19 20 var MIN_RADIUS = 100; 21 22 /** 23 * @constructor 24 */ 25 var PieChart = tr.ui.b.define('pie-chart', ChartBase); 26 27 PieChart.prototype = { 28 __proto__: ChartBase.prototype, 29 30 decorate: function() { 31 ChartBase.prototype.decorate.call(this); 32 this.classList.add('pie-chart'); 33 34 this.data_ = undefined; 35 this.seriesKeys_ = undefined; 36 37 var chartAreaSel = d3.select(this.chartAreaElement); 38 var pieGroupSel = chartAreaSel.append('g') 39 .attr('class', 'pie-group'); 40 this.pieGroup_ = pieGroupSel.node(); 41 42 this.pathsGroup_ = pieGroupSel.append('g') 43 .attr('class', 'paths') 44 .node(); 45 this.labelsGroup_ = pieGroupSel.append('g') 46 .attr('class', 'labels') 47 .node(); 48 this.linesGroup_ = pieGroupSel.append('g') 49 .attr('class', 'lines') 50 .node(); 51 }, 52 53 get data() { 54 return this.data_; 55 }, 56 57 58 /** 59 * @param {Array} data Data for the chart, where each element in the array 60 * must be of the form {label: str, value: number}. 61 */ 62 set data(data) { 63 if (data !== undefined) { 64 // Figure out the label values in the data set. E.g. from 65 // [{label: 'a', ...}, {label: 'b', ...}] 66 // we would commpute ['a', 'y']. These become the series keys. 67 var seriesKeys = []; 68 var seenSeriesKeys = {}; 69 data.forEach(function(d) { 70 var k = d.label; 71 if (seenSeriesKeys[k]) 72 throw new Error('Label ' + k + ' has been used already'); 73 seriesKeys.push(k); 74 seenSeriesKeys[k] = true; 75 }, this); 76 this.seriesKeys_ = seriesKeys; 77 } else { 78 this.seriesKeys_ = undefined; 79 } 80 this.data_ = data; 81 this.updateContents_(); 82 }, 83 84 get margin() { 85 var margin = {top: 0, right: 0, bottom: 0, left: 0}; 86 if (this.chartTitle_) 87 margin.top += 40; 88 return margin; 89 }, 90 91 getMinSize: function() { 92 this.updateContents_(); 93 94 var labelSel = d3.select(this.labelsGroup_).selectAll('.label'); 95 var maxLabelWidth = -Number.MAX_VALUE; 96 var leftTextHeightSum = 0; 97 var rightTextHeightSum = 0; 98 labelSel.each(function(l) { 99 var r = this.getBoundingClientRect(); 100 maxLabelWidth = Math.max(maxLabelWidth, r.width + 32); 101 if (this.style.textAnchor == 'end') { 102 leftTextHeightSum += r.height; 103 } else { 104 rightTextHeightSum += r.height; 105 } 106 }); 107 108 var titleWidth = this.querySelector( 109 '#title').getBoundingClientRect().width; 110 var margin = this.margin; 111 var marginWidth = margin.left + margin.right; 112 var marginHeight = margin.top + margin.bottom; 113 return { 114 width: Math.max(2 * MIN_RADIUS + 2 * maxLabelWidth, 115 titleWidth * 1.1) + marginWidth, 116 height: marginHeight + Math.max(2 * MIN_RADIUS, 117 leftTextHeightSum, 118 rightTextHeightSum) * 1.25 119 }; 120 }, 121 122 123 getLegendKeys_: function() { 124 // This class creates its own legend, instead of using ChartBase. 125 return undefined; 126 }, 127 128 updateScales_: function(width, height) { 129 if (this.data_ === undefined) 130 return; 131 }, 132 133 updateContents_: function() { 134 ChartBase.prototype.updateContents_.call(this); 135 if (!this.data_) 136 return; 137 138 var width = this.chartAreaSize.width; 139 var height = this.chartAreaSize.height; 140 var radius = Math.max(MIN_RADIUS, Math.min(width, height * 0.95) / 2); 141 142 d3.select(this.pieGroup_).attr( 143 'transform', 144 'translate(' + width / 2 + ',' + height / 2 + ')'); 145 146 // Bind the pie layout to its data 147 var pieLayout = d3.layout.pie() 148 .value(function(d) { return d.value; }) 149 .sort(null); 150 151 var piePathsSel = d3.select(this.pathsGroup_) 152 .datum(this.data_) 153 .selectAll('path') 154 .data(pieLayout); 155 156 function midAngle(d) { 157 return d.startAngle + (d.endAngle - d.startAngle) / 2; 158 } 159 160 var pathsArc = d3.svg.arc() 161 .innerRadius(0) 162 .outerRadius(radius - 30); 163 164 var valueLabelArc = d3.svg.arc() 165 .innerRadius(radius - 100) 166 .outerRadius(radius - 30); 167 168 var lineBeginArc = d3.svg.arc() 169 .innerRadius(radius - 50) 170 .outerRadius(radius - 50); 171 172 var lineEndArc = d3.svg.arc() 173 .innerRadius(radius) 174 .outerRadius(radius); 175 176 // Paths. 177 piePathsSel.enter().append('path') 178 .attr('class', 'arc') 179 .attr('fill', function(d, i) { 180 var origData = this.data_[i]; 181 var highlighted = (origData.label === 182 this.currentHighlightedLegendKey); 183 return getColorOfKey(origData.label, highlighted); 184 }.bind(this)) 185 .attr('d', pathsArc) 186 .on('click', function(d, i) { 187 var origData = this.data_[i]; 188 var event = new tr.b.Event('item-click'); 189 event.data = origData; 190 event.index = i; 191 this.dispatchEvent(event); 192 d3.event.stopPropagation(); 193 }.bind(this)) 194 .on('mouseenter', function(d, i) { 195 var origData = this.data_[i]; 196 this.pushTempHighlightedLegendKey(origData.label); 197 }.bind(this)) 198 .on('mouseleave', function(d, i) { 199 var origData = this.data_[i]; 200 this.popTempHighlightedLegendKey(origData.label); 201 }.bind(this)); 202 203 // Value labels. 204 piePathsSel.enter().append('text') 205 .attr('class', 'arc-text') 206 .attr('transform', function(d) { 207 return 'translate(' + valueLabelArc.centroid(d) + ')'; 208 }) 209 .attr('dy', '.35em') 210 .style('text-anchor', 'middle') 211 .text(function(d, i) { 212 var origData = this.data_[i]; 213 if (origData.valueText === undefined) 214 return ''; 215 216 if (d.endAngle - d.startAngle < 0.4) 217 return ''; 218 return origData.valueText; 219 }.bind(this)); 220 221 piePathsSel.exit().remove(); 222 223 // Labels. 224 var labelSel = d3.select(this.labelsGroup_).selectAll('.label') 225 .data(pieLayout(this.data_)); 226 labelSel.enter() 227 .append('text') 228 .attr('class', 'label') 229 .attr('dy', '.35em'); 230 231 labelSel.text(function(d) { 232 if (d.data.label.length > 40) 233 return d.data.label.substr(0, 40) + '...'; 234 return d.data.label; 235 }); 236 labelSel.attr('transform', function(d) { 237 var pos = lineEndArc.centroid(d); 238 pos[0] = radius * (midAngle(d) < Math.PI ? 1 : -1); 239 return 'translate(' + pos + ')'; 240 }); 241 labelSel.style('text-anchor', function(d) { 242 return midAngle(d) < Math.PI ? 'start' : 'end'; 243 }); 244 245 // Lines. 246 var lineSel = d3.select(this.linesGroup_).selectAll('.line') 247 .data(pieLayout(this.data_)); 248 lineSel.enter() 249 .append('polyline') 250 .attr('class', 'line') 251 .attr('dy', '.35em'); 252 lineSel.attr('points', function(d) { 253 var pos = lineEndArc.centroid(d); 254 pos[0] = radius * 0.95 * (midAngle(d) < Math.PI ? 1 : -1); 255 return [lineBeginArc.centroid(d), lineEndArc.centroid(d), pos]; 256 }); 257 }, 258 259 updateHighlight_: function() { 260 ChartBase.prototype.updateHighlight_.call(this); 261 // Update color of pie segments. 262 var pathsGroupSel = d3.select(this.pathsGroup_); 263 var that = this; 264 pathsGroupSel.selectAll('.arc').each(function(d, i) { 265 var origData = that.data_[i]; 266 var highlighted = origData.label == that.currentHighlightedLegendKey; 267 var color = getColorOfKey(origData.label, highlighted); 268 this.style.fill = color; 269 }); 270 } 271 }; 272 273 return { 274 PieChart: PieChart 275 }; 276}); 277</script> 278