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