c341793b07f9ea28e9da1ceb65439c6093e22dc4
[platform/framework/web/crosswalk.git] / src / third_party / trace-viewer / third_party / tvcm / src / tvcm / ui / sunburst_chart.html
1 <!DOCTYPE html>
2 <!--
3 Copyright (c) 2014 The Chromium Authors. All rights reserved.
4 Use of this source code is governed by a BSD-style license that can be
5 found in the LICENSE file.
6 -->
7 <link rel="import" href="/tvcm/range.html">
8 <link rel="import" href="/tvcm/ui/d3.html">
9 <link rel="import" href="/tvcm/ui/dom_helpers.html">
10 <link rel="import" href="/tvcm/ui/chart_base.html">
11 <link rel="stylesheet" href="/tvcm/ui/sunburst_chart.css">
12 <script>
13 'use strict';
14
15 tvcm.exportTo('tvcm.ui', function() {
16   var ChartBase = tvcm.ui.ChartBase;
17   var getColorOfKey = tvcm.ui.getColorOfKey;
18
19   var MIN_RADIUS = 100;
20
21   /**
22    * @constructor
23    */
24   var SunburstChart = tvcm.ui.define('sunburst-chart', ChartBase);
25
26   SunburstChart.prototype = {
27     __proto__: ChartBase.prototype,
28
29     decorate: function() {
30       ChartBase.prototype.decorate.call(this);
31       this.classList.add('sunburst-chart');
32
33       this.data_ = undefined;
34       this.seriesKeys_ = undefined;
35
36       this.yDomainMin_ = 0.0;
37       this.yDomainMax_ = 0.0;
38       this.xDomainScale_ = undefined;
39       this.yDomainScale_ = undefined;
40       this.radius_ = undefined;
41       this.arc_ = undefined;
42       this.selectedNode_ = null;
43       this.vis_ = undefined;
44       this.nodes_ = undefined;
45       this.minX_ = 0.0;
46       this.maxX_ = 1.0;
47       this.minY_ = 0.0;
48       this.clickedY_ = 0;
49
50       var chartAreaSel = d3.select(this.chartAreaElement);
51       this.legendSel_ = chartAreaSel.append('g');
52
53       var pieGroupSel = chartAreaSel.append('g')
54         .attr('class', 'pie-group');
55       this.pieGroup_ = pieGroupSel.node();
56
57       this.backSel_ = pieGroupSel.append('g');
58
59
60       this.pathsGroup_ = pieGroupSel.append('g')
61         .attr('class', 'paths')
62         .node();
63     },
64
65     get data() {
66       return this.data_;
67     },
68
69
70     /**
71      * @param {Data} Data for the chart, where data must be of the
72      * form {category: str, name: str, (size: number or children: [])} .
73      */
74     set data(data) {
75       this.data_ = data;
76       this.updateContents_();
77     },
78
79     get margin() {
80       var margin = {top: 0, right: 0, bottom: 0, left: 0};
81       if (this.chartTitle_)
82         margin.top += 40;
83       return margin;
84     },
85
86     set selectedNodeID(id) {
87       this.zoomToID_(id);
88     },
89
90     get selectedNodeID() {
91       if (this.selectedNode_ != null)
92         return this.selectedNode_.id;
93       return null;
94     },
95
96     get selectedNode() {
97       if (this.selectedNode_ != null)
98         return this.selectedNode_;
99       return null;
100     },
101
102     getMinSize: function() {
103       if (!tvcm.ui.isElementAttachedToDocument(this))
104         throw new Error('Cannot measure when unattached');
105       this.updateContents_();
106
107       var titleWidth = this.querySelector(
108           '#title').getBoundingClientRect().width;
109       var margin = this.margin;
110       var marginWidth = margin.left + margin.right;
111       var marginHeight = margin.top + margin.bottom;
112
113       // TODO(vmiura): Calc this when we're done with layout.
114       return {
115         width: 600,
116         height: 600
117       };
118     },
119
120     getLegendKeys_: function() {
121       // This class creates its own legend, instead of using ChartBase.
122       return undefined;
123     },
124
125     updateScales_: function(width, height) {
126       if (this.data_ === undefined)
127         return;
128     },
129
130     // Interpolate the scales!
131     arcTween_: function(minX, maxX, minY) {
132       var that = this;
133       var xd, yd, yr;
134
135       if (minY > 0) {
136         xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
137         yd = d3.interpolate(
138             that.yDomainScale_.domain(), [minY, that.yDomainMax_]);
139         yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
140       }
141       else {
142         xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
143         yd = d3.interpolate(that.yDomainScale_.domain(),
144                             [that.yDomainMin_, that.yDomainMax_]);
145         yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
146       }
147
148       return function(d, i) {
149         return i ? function(t) { return that.arc_(d); }
150             : function(t) {
151               that.xDomainScale_.domain(xd(t));
152               that.yDomainScale_.domain(yd(t)).range(yr(t));
153               return that.arc_(d);
154             };
155       };
156     },
157
158     getNodeById_: function(id) {
159       if (!this.nodes_)
160         return null;
161
162       if (id < 0 || id > this.nodes_.length)
163         return null;
164
165       return this.nodes_[id];
166     },
167
168     zoomOut_: function() {
169       window.history.back();
170     },
171
172     // This function assumes that, till the given depth,
173     // the tree is linear. (i.e, a single string with no branches.)
174     zoomToDepth: function(depth) {
175       var node = this.data_.nodes;
176       while (node.depth !== depth) {
177         if (node.children.length !== 1)
178           throw new Error("zoomToDepth requires the tree to be linear " +
179                           "till the specified depth.");
180         node = node.children[0];
181       }
182       return this.zoomToID_(node.id);
183     },
184
185     zoomToID_: function(id) {
186       var d = this.getNodeById_(id);
187
188       if (d) {
189         this.clickedY_ = d.y;
190         this.minX_ = d.x;
191         this.maxX_ = d.x + d.dx;
192         this.minY_ = d.y;
193       }
194       else {
195         this.clickedY_ = -1;
196         this.minX_ = 0.0;
197         this.maxX_ = 1.0;
198         this.minY_ = 0.0;
199       }
200
201       this.selectedNode_ = d;
202       this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
203       var path = this.vis_.selectAll('path');
204
205       path.transition()
206         .duration(750)
207         .attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
208
209       this.showBreadcrumbs_(d);
210
211       var e = new Event('node-selected');
212       e.node = d;
213       this.dispatchEvent(e);
214     },
215
216     click_: function(d) {
217       if (d3.event.shiftKey) {
218         // Zoom partially onto the selected range
219         var diff_x = (this.maxX_ - this.minX_) * 0.5;
220         this.minX_ = d.x + d.dx * 0.5 - diff_x * 0.5;
221         this.minX_ = this.minX_ < 0.0 ? 0.0 : this.minX_;
222         this.maxX_ = this.minX_ + diff_x;
223         this.maxX_ = this.maxX_ > 1.0 ? 1.0 : this.maxX_;
224         this.minX_ = this.maxX_ - diff_x;
225
226         this.selectedNode_ = d;
227         this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
228
229         var path = this.vis_.selectAll('path');
230         path.transition()
231           .duration(750)
232           .attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
233
234         return;
235       }
236
237       this.selectedNodeID = d.id;
238
239       var e = new Event('node-clicked');
240       e.node = d;
241       this.dispatchEvent(e);
242     },
243
244     // Given a node in a partition layout, return an array of all of its
245     // ancestor nodes, highest first, but excluding the root.
246     getAncestors_: function(node) {
247       var path = [];
248       var current = node;
249       while (current.parent) {
250         path.unshift(current);
251         current = current.parent;
252       }
253       return path;
254     },
255
256     showBreadcrumbs_: function(d) {
257       var sequenceArray = this.getAncestors_(d);
258
259       // Fade all the segments.
260       this.vis_.selectAll('path')
261         .style('opacity', function(d) {
262             return sequenceArray.indexOf(d) >= 0 ? 0.7 : 1.0;
263           });
264
265       var e = new Event('node-highlighted');
266       e.node = d;
267       this.dispatchEvent(e);
268
269       //if (this.data_.onNodeHighlighted != undefined)
270       //  this.data_.onNodeHighlighted(this, d);
271     },
272
273     mouseOver_: function(d) {
274       this.showBreadcrumbs_(d);
275     },
276
277     // Restore everything to full opacity when moving off the
278     // visualization.
279     mouseLeave_: function(d) {
280       var that = this;
281       // Hide the breadcrumb trail
282       if (that.selectedNode_ != null)
283         that.showBreadcrumbs_(that.selectedNode_);
284       else {
285         // Deactivate all segments during transition.
286         that.vis_.selectAll('path')
287           .on('mouseover', null);
288
289         // Transition each segment to full opacity and then reactivate it.
290         that.vis_.selectAll('path')
291           .transition()
292           .duration(300)
293           .style('opacity', 1)
294           .each('end', function() {
295               d3.select(that).on('mouseover', function(d) {
296                 that.mouseOver_(d);
297               });
298             });
299       }
300     },
301
302     // Update visible segments between new min/max ranges.
303     redrawSegments_: function(minX, maxX, minY) {
304       var that = this;
305       var scale = maxX - minX;
306       var visible_nodes = that.nodes_.filter(function(d) {
307         return d.depth &&
308             (d.y >= minY) &&
309             (d.x < maxX) &&
310             (d.x + d.dx > minX) &&
311             (d.dx / scale > 0.001);
312       });
313       var path = that.vis_.data([that.data_.nodes]).selectAll('path')
314         .data(visible_nodes, function(d) { return d.id; });
315
316       path.enter().insert('svg:path')
317         .attr('d', that.arc_)
318         .attr('fill-rule', 'evenodd')
319         .style('fill', function(dd) { return getColorOfKey(dd.category); })
320         .style('opacity', 1.0)
321         .on('mouseover', function(d) { that.mouseOver_(d); })
322         .on('click', function(d) { that.click_(d); });
323
324       path.exit().remove();
325       return path;
326     },
327
328     updateContents_: function() {
329       ChartBase.prototype.updateContents_.call(this);
330       if (!this.data_)
331         return;
332
333       var that = this;
334
335       // Partition data into d3 nodes.
336       var partition = d3.layout.partition()
337           .size([1, 1])
338           .value(function(d) { return d.size; });
339       that.nodes_ = partition.nodes(that.data_.nodes);
340
341       // Allocate an id to each node.  Gather all categories.
342       var categoryDict = {};
343       that.nodes_.forEach(function f(d, i) {
344         d.id = i;
345         categoryDict[d.category] = null;
346       });
347
348       // Create legend.
349       var li = {
350         w: 85, h: 20, s: 3, r: 3
351       };
352
353       var legend = that.legendSel_.append('svg:svg')
354           .attr('width', li.w)
355           .attr('height', d3.keys(categoryDict).length * (li.h + li.s));
356
357       var g = legend.selectAll('g')
358           .data(d3.keys(categoryDict))
359           .enter().append('svg:g')
360           .attr('transform', function(d, i) {
361             return 'translate(0,' + i * (li.h + li.s) + ')';
362           });
363
364       g.append('svg:rect')
365           .attr('rx', li.r)
366           .attr('ry', li.r)
367           .attr('width', li.w)
368           .attr('height', li.h)
369           .style('fill', function(d) { return getColorOfKey(d); });
370
371       g.append('svg:text')
372           .attr('x', li.w / 2)
373           .attr('y', li.h / 2)
374           .attr('dy', '0.35em')
375           .attr('text-anchor', 'middle')
376           .attr('fill', '#fff')
377           .attr('font-size', '12px')
378           .text(function(d) { return d; });
379
380       // Create sunburst visualization.
381       var width = that.chartAreaSize.width;
382       var height = that.chartAreaSize.height;
383       that.radius_ = Math.max(MIN_RADIUS, Math.min(width, height) / 2);
384
385       d3.select(that.pieGroup_).attr(
386           'transform',
387           'translate(' + width / 2 + ',' + height / 2 + ')');
388
389       that.selectedNode_ = null;
390
391       var depth = 1.0 + d3.max(that.nodes_, function(d) { return d.depth; });
392       that.yDomainMin_ = 1.0 / depth;
393       that.yDomainMax_ = Math.min(Math.max(depth, 20), 50) / depth;
394
395       that.xDomainScale_ = d3.scale.linear()
396           .range([0, 2 * Math.PI]);
397
398       that.yDomainScale_ = d3.scale.sqrt()
399           .domain([that.yDomainMin_, that.yDomainMax_])
400           .range([50, that.radius_]);
401
402       that.arc_ = d3.svg.arc()
403           .startAngle(function(d) {
404             return Math.max(0, Math.min(2 * Math.PI, that.xDomainScale_(d.x)));
405           })
406           .endAngle(function(d) {
407             return Math.max(0,
408                 Math.min(2 * Math.PI, that.xDomainScale_(d.x + d.dx)));
409           })
410           .innerRadius(function(d) {
411             return Math.max(0, that.yDomainScale_((d.y)));
412           })
413           .outerRadius(function(d) {
414             return Math.max(0, that.yDomainScale_((d.y + d.dy)));
415           });
416
417
418       // Bounding circle underneath the sunburst, to make it easier to detect
419       // when the mouse leaves the parent g.
420       that.backSel_.append('svg:circle')
421           .attr('r', that.radius_)
422           .style('opacity', 0.0)
423           .on('click', function() { that.zoomOut_(); });
424
425
426       that.vis_ = d3.select(that.pathsGroup_);
427       that.selectedNodeID = 0;
428       that.vis_.on('mouseleave', function(d) { that.mouseLeave_(d); });
429     },
430
431     updateHighlight_: function() {
432       ChartBase.prototype.updateHighlight_.call(this);
433       // Update color of pie segments.
434       var pathsGroupSel = d3.select(this.pathsGroup_);
435       var that = this;
436       pathsGroupSel.selectAll('.arc').each(function(d, i) {
437         var origData = that.data_[i];
438         var highlighted = origData.label == that.currentHighlightedLegendKey;
439         var color = getColorOfKey(origData.label, highlighted);
440         this.style.fill = getColorOfKey(origData.label, highlighted);
441       });
442     }
443   };
444
445   return {
446     SunburstChart: SunburstChart
447   };
448 });
449 </script>