blob: cf4fb77b972bb043337b5ce65d5203d7ee70c024 [file] [log] [blame]
<!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/range.html">
<link rel="import" href="/base/ui/d3.html">
<link rel="import" href="/base/ui/dom_helpers.html">
<link rel="import" href="/base/ui/chart_base.html">
<link rel="stylesheet" href="/base/ui/sunburst_chart.css">
<script>
'use strict';
tr.exportTo('tr.b.ui', function() {
var ChartBase = tr.b.ui.ChartBase;
var getColorOfKey = tr.b.ui.getColorOfKey;
var MIN_RADIUS = 100;
/**
* @constructor
*/
var SunburstChart = tr.b.ui.define('sunburst-chart', ChartBase);
SunburstChart.prototype = {
__proto__: ChartBase.prototype,
decorate: function() {
ChartBase.prototype.decorate.call(this);
this.classList.add('sunburst-chart');
this.data_ = undefined;
this.seriesKeys_ = undefined;
this.yDomainMin_ = 0.0;
this.yDomainMax_ = 0.0;
this.xDomainScale_ = undefined;
this.yDomainScale_ = undefined;
this.radius_ = undefined;
this.arc_ = undefined;
this.selectedNode_ = null;
this.vis_ = undefined;
this.nodes_ = undefined;
this.minX_ = 0.0;
this.maxX_ = 1.0;
this.minY_ = 0.0;
this.clickedY_ = 0;
var chartAreaSel = d3.select(this.chartAreaElement);
this.legendSel_ = chartAreaSel.append('g');
var pieGroupSel = chartAreaSel.append('g')
.attr('class', 'pie-group');
this.pieGroup_ = pieGroupSel.node();
this.backSel_ = pieGroupSel.append('g');
this.pathsGroup_ = pieGroupSel.append('g')
.attr('class', 'paths')
.node();
},
get data() {
return this.data_;
},
/**
* @param {Data} Data for the chart, where data must be of the
* form {category: str, name: str, (size: number or children: [])} .
*/
set data(data) {
this.data_ = data;
this.updateContents_();
},
get margin() {
var margin = {top: 0, right: 0, bottom: 0, left: 0};
if (this.chartTitle_)
margin.top += 40;
return margin;
},
set selectedNodeID(id) {
this.zoomToID_(id);
},
get selectedNodeID() {
if (this.selectedNode_ != null)
return this.selectedNode_.id;
return null;
},
get selectedNode() {
if (this.selectedNode_ != null)
return this.selectedNode_;
return null;
},
getMinSize: function() {
if (!tr.b.ui.isElementAttachedToDocument(this))
throw new Error('Cannot measure when unattached');
this.updateContents_();
var titleWidth = this.querySelector(
'#title').getBoundingClientRect().width;
var margin = this.margin;
var marginWidth = margin.left + margin.right;
var marginHeight = margin.top + margin.bottom;
// TODO(vmiura): Calc this when we're done with layout.
return {
width: 600,
height: 600
};
},
getLegendKeys_: function() {
// This class creates its own legend, instead of using ChartBase.
return undefined;
},
updateScales_: function(width, height) {
if (this.data_ === undefined)
return;
},
// Interpolate the scales!
arcTween_: function(minX, maxX, minY) {
var that = this;
var xd, yd, yr;
if (minY > 0) {
xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
yd = d3.interpolate(
that.yDomainScale_.domain(), [minY, that.yDomainMax_]);
yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
}
else {
xd = d3.interpolate(that.xDomainScale_.domain(), [minX, maxX]);
yd = d3.interpolate(that.yDomainScale_.domain(),
[that.yDomainMin_, that.yDomainMax_]);
yr = d3.interpolate(that.yDomainScale_.range(), [50, that.radius_]);
}
return function(d, i) {
return i ? function(t) { return that.arc_(d); }
: function(t) {
that.xDomainScale_.domain(xd(t));
that.yDomainScale_.domain(yd(t)).range(yr(t));
return that.arc_(d);
};
};
},
getNodeById_: function(id) {
if (!this.nodes_)
return null;
if (id < 0 || id > this.nodes_.length)
return null;
return this.nodes_[id];
},
zoomOut_: function() {
window.history.back();
},
// This function assumes that, till the given depth,
// the tree is linear. (i.e, a single string with no branches.)
zoomToDepth: function(depth) {
var node = this.data_.nodes;
while (node.depth !== depth) {
if (node.children.length !== 1)
throw new Error('zoomToDepth requires the tree to be linear ' +
'till the specified depth.');
node = node.children[0];
}
return this.zoomToID_(node.id);
},
zoomToID_: function(id) {
var d = this.getNodeById_(id);
if (d) {
this.clickedY_ = d.y;
this.minX_ = d.x;
this.maxX_ = d.x + d.dx;
this.minY_ = d.y;
}
else {
this.clickedY_ = -1;
this.minX_ = 0.0;
this.maxX_ = 1.0;
this.minY_ = 0.0;
}
this.selectedNode_ = d;
this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
var path = this.vis_.selectAll('path');
path.transition()
.duration(750)
.attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
this.showBreadcrumbs_(d);
var e = new Event('node-selected');
e.node = d;
this.dispatchEvent(e);
},
click_: function(d) {
if (d3.event.shiftKey) {
// Zoom partially onto the selected range
var diff_x = (this.maxX_ - this.minX_) * 0.5;
this.minX_ = d.x + d.dx * 0.5 - diff_x * 0.5;
this.minX_ = this.minX_ < 0.0 ? 0.0 : this.minX_;
this.maxX_ = this.minX_ + diff_x;
this.maxX_ = this.maxX_ > 1.0 ? 1.0 : this.maxX_;
this.minX_ = this.maxX_ - diff_x;
this.selectedNode_ = d;
this.redrawSegments_(this.minX_, this.maxX_, this.minY_);
var path = this.vis_.selectAll('path');
path.transition()
.duration(750)
.attrTween('d', this.arcTween_(this.minX_, this.maxX_, this.minY_));
return;
}
this.selectedNodeID = d.id;
var e = new Event('node-clicked');
e.node = d;
this.dispatchEvent(e);
},
// Given a node in a partition layout, return an array of all of its
// ancestor nodes, highest first, but excluding the root.
getAncestors_: function(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
},
showBreadcrumbs_: function(d) {
var sequenceArray = this.getAncestors_(d);
// Fade all the segments.
this.vis_.selectAll('path')
.style('opacity', function(d) {
return sequenceArray.indexOf(d) >= 0 ? 0.7 : 1.0;
});
var e = new Event('node-highlighted');
e.node = d;
this.dispatchEvent(e);
//if (this.data_.onNodeHighlighted != undefined)
// this.data_.onNodeHighlighted(this, d);
},
mouseOver_: function(d) {
this.showBreadcrumbs_(d);
},
// Restore everything to full opacity when moving off the
// visualization.
mouseLeave_: function(d) {
var that = this;
// Hide the breadcrumb trail
if (that.selectedNode_ != null)
that.showBreadcrumbs_(that.selectedNode_);
else {
// Deactivate all segments during transition.
that.vis_.selectAll('path')
.on('mouseover', null);
// Transition each segment to full opacity and then reactivate it.
that.vis_.selectAll('path')
.transition()
.duration(300)
.style('opacity', 1)
.each('end', function() {
d3.select(that).on('mouseover', function(d) {
that.mouseOver_(d);
});
});
}
},
// Update visible segments between new min/max ranges.
redrawSegments_: function(minX, maxX, minY) {
var that = this;
var scale = maxX - minX;
var visible_nodes = that.nodes_.filter(function(d) {
return d.depth &&
(d.y >= minY) &&
(d.x < maxX) &&
(d.x + d.dx > minX) &&
(d.dx / scale > 0.001);
});
var path = that.vis_.data([that.data_.nodes]).selectAll('path')
.data(visible_nodes, function(d) { return d.id; });
path.enter().insert('svg:path')
.attr('d', that.arc_)
.attr('fill-rule', 'evenodd')
.style('fill', function(dd) { return getColorOfKey(dd.category); })
.style('opacity', 1.0)
.on('mouseover', function(d) { that.mouseOver_(d); })
.on('click', function(d) { that.click_(d); });
path.exit().remove();
return path;
},
updateContents_: function() {
ChartBase.prototype.updateContents_.call(this);
if (!this.data_)
return;
var that = this;
// Partition data into d3 nodes.
var partition = d3.layout.partition()
.size([1, 1])
.value(function(d) { return d.size; });
that.nodes_ = partition.nodes(that.data_.nodes);
// Allocate an id to each node. Gather all categories.
var categoryDict = {};
that.nodes_.forEach(function f(d, i) {
d.id = i;
categoryDict[d.category] = null;
});
// Create legend.
var li = {
w: 85, h: 20, s: 3, r: 3
};
var legend = that.legendSel_.append('svg:svg')
.attr('width', li.w)
.attr('height', d3.keys(categoryDict).length * (li.h + li.s));
var g = legend.selectAll('g')
.data(d3.keys(categoryDict))
.enter().append('svg:g')
.attr('transform', function(d, i) {
return 'translate(0,' + i * (li.h + li.s) + ')';
});
g.append('svg:rect')
.attr('rx', li.r)
.attr('ry', li.r)
.attr('width', li.w)
.attr('height', li.h)
.style('fill', function(d) { return getColorOfKey(d); });
g.append('svg:text')
.attr('x', li.w / 2)
.attr('y', li.h / 2)
.attr('dy', '0.35em')
.attr('text-anchor', 'middle')
.attr('fill', '#fff')
.attr('font-size', '12px')
.text(function(d) { return d; });
// Create sunburst visualization.
var width = that.chartAreaSize.width;
var height = that.chartAreaSize.height;
that.radius_ = Math.max(MIN_RADIUS, Math.min(width, height) / 2);
d3.select(that.pieGroup_).attr(
'transform',
'translate(' + width / 2 + ',' + height / 2 + ')');
that.selectedNode_ = null;
var depth = 1.0 + d3.max(that.nodes_, function(d) { return d.depth; });
that.yDomainMin_ = 1.0 / depth;
that.yDomainMax_ = Math.min(Math.max(depth, 20), 50) / depth;
that.xDomainScale_ = d3.scale.linear()
.range([0, 2 * Math.PI]);
that.yDomainScale_ = d3.scale.sqrt()
.domain([that.yDomainMin_, that.yDomainMax_])
.range([50, that.radius_]);
that.arc_ = d3.svg.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, that.xDomainScale_(d.x)));
})
.endAngle(function(d) {
return Math.max(0,
Math.min(2 * Math.PI, that.xDomainScale_(d.x + d.dx)));
})
.innerRadius(function(d) {
return Math.max(0, that.yDomainScale_((d.y)));
})
.outerRadius(function(d) {
return Math.max(0, that.yDomainScale_((d.y + d.dy)));
});
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
that.backSel_.append('svg:circle')
.attr('r', that.radius_)
.style('opacity', 0.0)
.on('click', function() { that.zoomOut_(); });
that.vis_ = d3.select(that.pathsGroup_);
that.selectedNodeID = 0;
that.vis_.on('mouseleave', function(d) { that.mouseLeave_(d); });
},
updateHighlight_: function() {
ChartBase.prototype.updateHighlight_.call(this);
// Update color of pie segments.
var pathsGroupSel = d3.select(this.pathsGroup_);
var that = this;
pathsGroupSel.selectAll('.arc').each(function(d, i) {
var origData = that.data_[i];
var highlighted = origData.label == that.currentHighlightedLegendKey;
var color = getColorOfKey(origData.label, highlighted);
this.style.fill = getColorOfKey(origData.label, highlighted);
});
}
};
return {
SunburstChart: SunburstChart
};
});
</script>