1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 tvcm.require('tvcm.range');
8 tvcm.require('tvcm.ui.d3');
9 tvcm.require('tvcm.ui.dom_helpers');
10 tvcm.require('tvcm.ui.chart_base');
12 tvcm.requireStylesheet('tvcm.ui.sunburst_chart');
14 tvcm.exportTo('tvcm.ui', function() {
15 var ChartBase = tvcm.ui.ChartBase;
16 var getColorOfKey = tvcm.ui.getColorOfKey;
23 var SunburstChart = tvcm.ui.define('sunburst-chart', ChartBase);
25 SunburstChart.prototype = {
26 __proto__: ChartBase.prototype,
28 decorate: function() {
29 ChartBase.prototype.decorate.call(this);
30 this.classList.add('sunburst-chart');
32 this.data_ = undefined;
33 this.seriesKeys_ = undefined;
35 var chartAreaSel = d3.select(this.chartAreaElement);
36 var pieGroupSel = chartAreaSel.append('g')
37 .attr('class', 'pie-group');
38 this.pieGroup_ = pieGroupSel.node();
40 this.backSel_ = pieGroupSel.append('g');
42 this.pathsGroup_ = pieGroupSel.append('g')
43 .attr('class', 'paths')
53 * @param {Data} Data for the chart, where data must be of the
54 * form {category: str, name: str, (size: number or children: [])} .
58 this.updateContents_();
62 var margin = {top: 0, right: 0, bottom: 0, left: 0};
68 getMinSize: function() {
69 if (!tvcm.ui.isElementAttachedToDocument(this))
70 throw new Error('Cannot measure when unattached');
71 this.updateContents_();
73 var titleWidth = this.querySelector(
74 '#title').getBoundingClientRect().width;
75 var margin = this.margin;
76 var marginWidth = margin.left + margin.right;
77 var marginHeight = margin.top + margin.bottom;
79 // TODO(vmiura): Calc this when we're done with layout.
86 getLegendKeys_: function() {
87 // This class creates its own legend, instead of using ChartBase.
91 updateScales_: function(width, height) {
92 if (this.data_ === undefined)
96 updateContents_: function() {
97 ChartBase.prototype.updateContents_.call(this);
101 var width = this.chartAreaSize.width;
102 var height = this.chartAreaSize.height;
103 var radius = Math.max(MIN_RADIUS, Math.min(width, height) / 2);
105 d3.select(this.pieGroup_).attr(
107 'translate(' + width / 2 + ',' + height / 2 + ')');
110 /////////////////////////////////////////
111 // Mapping of step names to colors.
115 'GPU Driver': '#ff0000',
117 'Android': '#6ab975',
119 'Standard Lib': '#bbbbbb',
121 '<unknown>': '#444444'
125 var json = this.data_;
126 var partition = d3.layout.partition()
127 .size([1, 1]) // radius * radius
128 .value(function(d) { return d.size; });
130 // For efficiency, filter nodes to keep only those large enough to see.
131 var nodes = partition.nodes(json);
132 nodes.forEach(function f(d, i) { d.id = i; });
133 var totalSize = nodes[0].value;
134 var depth = 1.0 + d3.max(nodes, function(d) { return d.depth; });
135 var yDomainMin = 1.0 / depth;
136 var yDomainMax = Math.min(Math.max(depth, 20), 50) / depth;
138 var x = d3.scale.linear()
139 .range([0, 2 * Math.PI]);
141 var y = d3.scale.sqrt()
142 .domain([yDomainMin, yDomainMax])
143 .range([50, radius]);
145 var arc = d3.svg.arc()
146 .startAngle(function(d) {
147 return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
149 .endAngle(function(d) {
150 return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
152 .innerRadius(function(d) { return Math.max(0, y((d.y))); })
153 .outerRadius(function(d) { return Math.max(0, y((d.y + d.dy))); });
155 // Interpolate the scales!
156 function arcTween(minX, maxX, minY) {
160 xd = d3.interpolate(x.domain(), [minX, maxX]);
161 yd = d3.interpolate(y.domain(), [minY, yDomainMax]);
162 yr = d3.interpolate(y.range(), [50, radius]);
165 xd = d3.interpolate(x.domain(), [minX, maxX]);
166 yd = d3.interpolate(y.domain(), [yDomainMin, yDomainMax]);
167 yr = d3.interpolate(y.range(), [50, radius]);
170 return function(d, i) {
171 return i ? function(t) { return arc(d); }
173 x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d);
179 var clickedNode = null;
180 var click_stack = new Array();
183 function zoomout(d) {
184 if (click_stack.length > 1)
186 zoomto(click_stack[click_stack.length - 1]);
189 // Bounding circle underneath the sunburst, to make it easier to detect
190 // when the mouse leaves the parent g.
191 this.backSel_.append('svg:circle')
193 .style('opacity', 0.0)
194 .on('click', zoomout);
197 var vis = d3.select(this.pathsGroup_);
199 function getNode(id) {
200 for (var i = 0; i < nodes.length; i++) {
201 if (nodes[i].id == id)
212 function zoomto(id) {
229 redraw(minX, maxX, minY);
230 var path = vis.selectAll('path');
234 .attrTween('d', arcTween(minX, maxX, minY));
240 if (d3.event.shiftKey) {
241 // Zoom partially onto the selected range
242 var diff_x = (maxX - minX) * 0.5;
243 minX = d.x + d.dx * 0.5 - diff_x * 0.5;
244 minX = minX < 0.0 ? 0.0 : minX;
245 maxX = minX + diff_x;
246 maxX = maxX > 1.0 ? 1.0 : maxX;
247 minX = maxX - diff_x;
249 redraw(minX, maxX, minY);
251 var path = vis.selectAll('path');
256 .attrTween('d', arcTween(minX, maxX, minY));
261 if (click_stack[click_stack.length - 1] != d.id) {
262 click_stack.push(d.id);
267 // Given a node in a partition layout, return an array of all of its
268 // ancestor nodes, highest first, but excluding the root.
269 function getAncestors(node) {
272 while (current.parent) {
273 path.unshift(current);
274 current = current.parent;
279 function showBreadcrumbs(d) {
280 var sequenceArray = getAncestors(d);
282 // Fade all the segments.
283 vis.selectAll('path')
284 .style('opacity', 0.7);
286 // Then highlight only those that are an ancestor of the current
288 vis.selectAll('path')
289 .filter(function(node) {
290 return (sequenceArray.indexOf(node) >= 0);
292 .style('opacity', 1);
295 function mouseover(d) {
299 // Restore everything to full opacity when moving off the
301 function mouseleave(d) {
302 // Hide the breadcrumb trail
303 if (clickedNode != null)
304 showBreadcrumbs(clickedNode);
306 // Deactivate all segments during transition.
307 vis.selectAll('path')
308 .on('mouseover', null);
310 // Transition each segment to full opacity and then reactivate it.
311 vis.selectAll('path')
315 .each('end', function() {
316 d3.select(this).on('mouseover', mouseover);
321 // Update visible segments between new min/max ranges.
322 function redraw(minX, maxX, minY) {
323 var scale = maxX - minX;
324 var visible_nodes = nodes.filter(function(d) {
328 (d.x + d.dx > minX) &&
329 (d.dx / scale > 0.001);
331 var path = vis.data([json]).selectAll('path')
332 .data(visible_nodes, function(d) { return d.id; });
334 path.enter().insert('svg:path')
336 .attr('fill-rule', 'evenodd')
337 .style('fill', function(dd) { return colors[dd.category]; })
338 .style('opacity', 0.7)
339 .on('mouseover', mouseover)
342 path.exit().remove();
347 vis.on('mouseleave', mouseleave);
350 updateHighlight_: function() {
351 ChartBase.prototype.updateHighlight_.call(this);
352 // Update color of pie segments.
353 var pathsGroupSel = d3.select(this.pathsGroup_);
355 pathsGroupSel.selectAll('.arc').each(function(d, i) {
356 var origData = that.data_[i];
357 var highlighted = origData.label == that.currentHighlightedLegendKey;
358 var color = getColorOfKey(origData.label, highlighted);
359 this.style.fill = color;
365 SunburstChart: SunburstChart