<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rotating Donut</title>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
<script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
<script src="data.js"></script>
<script src="rotating_donut.js"></script>
<script src="pie_selection_rotation.js"></script>
<script src="pie_transitions.js"></script>
<script src="basic_legend.js"></script>
<link rel="stylesheet" href="button.css">
<link rel="stylesheet" href="legend.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="donuts">
<div class="donut" id="donut1"></div>
<div class="slider">
<label for="donut-size-1">Size</label>
<input type="range"
class="donut-size"
id="donut-size-1"
title="donut-size"
data-target="#donut1"
value="100"
max="150">
</div>
<div class="donut" id="donut2"></div>
<div class="slider">
<label for="donut-size-2">Size</label>
<input type="range"
class="donut-size"
id="donut-size-2"
title="donut-size"
data-target="#donut2"
max="150"
value="150">
</div>
</div>
<div id="legend"></div>
<button>Randomize Data</button>
</body>
<script src="app.js"></script>
</html>
document.addEventListener('DOMContentLoaded', function() {
'use strict';
var donut,
legend,
events;
function build() {
donut = APP.rotatingDonut()
.alignmentAngle(90)
.thickness(0.5)
.value(function(d) {return d.value;})
.color(function(d) {return d.color;})
.key(function(d) {return d.id;})
.sort(function(a, b) {return a.id - b.id;});
legend = APP.basicLegend()
.label(function(d) {return d.label;})
.color(function(d) {return d.color;})
.key(function(d) {return d.id;});
}
function addToDom() {
d3.select('#donut1')
.datum(APP.generateData())
.call(donut.label, 'Smith')
.transition()
.duration(0)
.call(donut);
d3.select('#donut2')
.datum(APP.generateData())
.call(donut.label, 'Jones')
.transition()
.duration(0)
.call(donut);
d3.select('#legend')
.datum(APP.generateData())
.call(legend);
}
function addListeners() {
donut.on('click', events.donutClick)
.on('mouseenter', events.donutMouseEnter)
.on('mouseleave', events.donutMouseLeave);
legend.on('click', events.legendClick);
d3.select('button').on('click', events.dataButtonClick);
d3.selectAll('.donut-size').on('change', events.resizeSliderChange);
}
events = {
dataButtonClick: function() {
d3.select('#donut1')
.datum(APP.generateData(true))
.transition()
.duration(600)
.call(donut);
d3.select('#donut2')
.datum(APP.generateData(true))
.transition()
.delay(400)
.duration(200)
.call(donut);
},
donutClick: function(d) {
var container = this;
d3.selectAll('.donut')
.filter(function() {return this !== container;})
.call(donut.selectedSegment, d)
.call(donut);
d3.select('#legend')
.call(legend.selectedItem, d)
.call(legend);
},
donutMouseEnter: function(d) {
d3.select('#legend')
.call(legend.highlight, d)
},
donutMouseLeave: function(d) {
d3.select('#legend')
.call(legend.unhighlight, d)
},
legendClick: function(d) {
d3.selectAll('.donut')
.call(donut.selectedSegment, d)
.call(donut);
},
resizeSliderChange: function() {
var target = d3.select(this).attr('data-target'),
value = this.value * 2;
d3.selectAll(target)
.call(donut.dimensions, {width: value, height: value})
.call(donut)
.transition()
.duration(donut.animationDuration())
.style('width', value + 'px')
.style('height', value + 'px');
}
};
build();
addToDom();
addListeners();
});
body {
display: flex;
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
font-weight: 300;
height: 600px;
margin: 8px;
}
path {
cursor: pointer;
}
.donuts {
display: inline-block;
position: relative;
}
.donuts {
width: 300px;
text-align: right;
overflow: hidden;
}
.donut {
margin-left: auto;
}
#donut1 {
width: 200px;
}
#donut2 {
width: 300px;
height: 300px;
}
#donut1,
#description1 {
height: 200px;
}
#donut2,
#description2 {
height: 300px;
}
.donut-label {
font-weight: bold;
}
.slider {
display: inline-block;
height: 20px;
margin: 4px auto 16px;
}
#donut-size {
margin-left: 8px;
}
button {
background-color: #eee;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 11px;
padding: 6px 10px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: pointer;
position: absolute;
right: 16px;
}
button:hover {
background-color: #ddd;
border-color: #ccc
}
button:active {
background-color: #ccc;
}
button:focus {
outline:0;
}
#legend {
position: relative;
margin-top: 20px;
width: 140px;
}
#legend li.legend-label {
display: flex;
align-items: center;
position: absolute;
padding: 2px 8px;
width: calc(100% - 20px);
cursor: pointer;
}
#legend li.legend-label svg {
margin-right: 8px;
}
#legend .hovered {
stroke: black;
z-index: 1;
}
#legend rect.hovered {
stroke-width: 2px;
}
#legend li.hovered,
#legend li.selected {
background-color: #e2e8ff;
}
#legend li.hovered svg rect {
stroke-width: 1px;
}
if(typeof APP === 'undefined') {APP = {};}
APP.basicLegend = function () {
'use strict';
var events = d3.dispatch('mouseenter', 'mouseleave', 'click'),
selectedItem = d3.local();
var o = {
label: null,
key: null,
color: null
};
function legend(group) {
group.each(function(data) {
render.call(this, data, group)
});
}
function render(data, group) {
var context = d3.select(this),
t,
labels,
labelsEnter;
if (group instanceof d3.transition) {
t = d3.transition(group);
} else {
t = d3.transition();
}
context
.selectAll('ul')
.data([data])
.enter()
.append('ul')
.attr('class', 'legend');
labels = context
.selectAll('ul')
.selectAll('li.legend-label')
.data(Object, o.key);
labelsEnter = labels.enter()
.append('li')
.attr('class', 'legend-label')
.attr('data-id', o.key)
.on('mouseenter mouseleave', listeners(context).mouseMovement)
.on('click', listeners(context).labelClick)
.call(labelInitialAttributes);
labelsEnter
.append('svg')
.attr('width', 22)
.attr('height', 22)
.append('rect')
.attr('fill', o.color)
.attr('width', 20)
.attr('height', 20)
.attr('x', 1)
.attr('y', 1);
labelsEnter
.append('span')
.text(o.label);
labelsEnter
.merge(labels)
.classed('selected', isSelected)
.transition(t)
.style('top', function(d, i) {return (i * 22) + 'px';})
.style('opacity', 1)
.style('left', '12px');
labels.exit()
.transition(t)
.call(labelInitialAttributes)
.remove();
}
function listeners(context) {
return {
labelClick: function(d) {
selectedItem.set(context.node(), d);
context.call(legend);
events.call('click', context.node(), d);
},
mouseMovement: function(d) {
context.call(highlight, d, d3.event.type);
events.call(d3.event.type, context.node(), d);
}
}
}
function highlight(selection, d, action) {
selection
.selectAll('li[data-id="' + o.key(d) + '"]')
.classed('hovered', action === 'mouseenter');
}
function labelInitialAttributes(selection) {
selection
.style('left', '-12px')
.style('opacity', 0);
}
function isSelected(d) {
return selectedItem.get(this) && o.key(d) === o.key(selectedItem.get(this));
}
legend.label = function(_) {
if (!arguments.length) {return o.label;}
o.label = _;
return legend;
};
legend.key = function(_) {
if (!arguments.length) {return o.key;}
o.key = _;
return legend;
};
legend.color = function(_) {
if (!arguments.length) {return o.color;}
o.color = _;
return legend;
};
legend.selectedItem = function(context, _) {
var returnArray;
if (typeof _ === 'undefined' ) {
returnArray = context.nodes()
.map(function (node) {return selectedItem.get(node);});
return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
}
context.each(function() {selectedItem.set(this, _);});
return legend;
};
legend.on = function(evt, callback) {
events.on(evt, callback);
return legend;
};
legend.highlight = function(selection, d) {
selection.call(highlight, d, 'mouseenter');
return legend;
};
legend.unhighlight = function(selection, d) {
selection.call(highlight, d, 'mouseleave');
return legend;
};
return legend;
};
if(typeof APP === 'undefined') {APP = {};}
APP.generateData = function(splice) {
'use strict';
var labels = ['travel', 'electricity', 'phone', 'shopping', 'food'],
colors = d3.scaleOrdinal(d3.schemeCategory10),
arr = [],
i;
for (i = 1; i <= 5; i++) {
arr.push({
id: i,
value: 5 + Math.random() * 15,
color: colors(i),
label: labels[i - 1]
});
}
if (splice) {
arr.sort(function() {return 0.5 - Math.random();})
.splice(0, Math.random() * 5);
}
return arr;
};
if(typeof APP === 'undefined') {APP = {};}
APP.pieSelectionRotation = function() {
'use strict';
var local = {
angle: d3.local(),
selectedSegment: d3.local(),
selectedKey: d3.local()
};
var o = {
key: null,
alignmentAngle: 0
};
function rotation(group) {
group.each(function() {
var selectedData = getSelectedData(this);
local.angle.set(this, local.angle.get(this) || 0);
local.selectedSegment.set(this, selectedData);
if (selectedData) {
local.angle.set(this, newAngle(local.angle.get(this), meanAngle(selectedData)));
}
});
}
function newAngle(offsetAngle, currentAngle) {
var radiansToTurn = degreesToRadians(o.alignmentAngle) - currentAngle - offsetAngle;
return shorterRotation(radiansToTurn) + offsetAngle;
}
function meanAngle(data) {
return d3.mean([data.startAngle, data.endAngle]);
}
function degreesToRadians(degrees) {
return degrees * Math.PI * 2 / 360;
}
function shorterRotation(offset) {
var tau = Math.PI * 2;
offset = offset % tau;
return (Math.abs(offset) > tau / 2) ? offset + tau * Math.sign(-offset) : offset;
}
function getSelectedData(node) {
return d3.select(node)
.datum()
.filter(function(d) {return o.key(d.data) === local.selectedKey.get(node)})[0];
}
rotation.selectedSegment = function(selection, d) {
var returnArray;
function nodeMap(node) {
return (local.selectedSegment.get(node) || {}).data;
}
if (typeof d === 'undefined' ) {
returnArray = selection.nodes().map(nodeMap);
return selection._groups[0] instanceof NodeList ? returnArray : returnArray[0];
}
selection.each(function() {
local.selectedKey.set(this, o.key(d));
});
return rotation;
};
rotation.getAngle = function(selection) {
var returnArray = selection.nodes()
.map(function(node) {return local.angle.get(node) || 0;});
return selection._groups[0] instanceof NodeList ? returnArray : returnArray[0];
};
rotation.key = function(_) {
if (!arguments.length) {return o.key;}
o.key = _;
return rotation;
};
rotation.alignmentAngle = function(_) {
if (!arguments.length) {return o.alignmentAngle;}
o.alignmentAngle = _;
return rotation;
};
return rotation;
};
if(typeof APP === 'undefined') {APP = {};}
APP.pieTransition = function() {
'use strict';
var allNodes,
firstPreviousNode,
firstCurrentNode,
enteringSegments,
transitioningSegments;
var previousSegmentData = d3.local();
var o = {
arc: null,
sort: null,
offset: 0
};
var methods = {
enter: function(transition) {
transition
.each(setEnterAngle)
.call(render);
},
transition: render,
exit: function(transition) {
transition
.each(setExitAngle)
.call(render);
}
};
function previousAdjacentAngle(node) {
var index = allNodes.indexOf(node);
if (index) {
return previousSegmentData.get(allNodes[index - 1]).endAngle;
} else if (firstPreviousNode) {
return previousSegmentData.get(firstPreviousNode).startAngle;
} else {
return nodeData(node).startAngle;
}
}
function currentAdjacentAngle(node) {
var index = allNodes.indexOf(node);
if (index) {
return nodeData(allNodes[index - 1]).endAngle;
} else {
return nodeData(firstCurrentNode).startAngle;
}
}
function updateNodes() {
if (!transitioningSegments || !enteringSegments) {return;}
allNodes = transitioningSegments.nodes()
.concat(transitioningSegments.exit().nodes())
.concat(enteringSegments.nodes())
.sort(sortNodes);
firstPreviousNode = transitioningSegments.nodes()
.concat(transitioningSegments.exit().nodes())
.sort(sortNodes)[0];
firstCurrentNode = transitioningSegments.nodes()
.concat(enteringSegments.nodes())
.sort(sortNodes)[0];
function sortNodes(a, b) {
return o.sort(nodeData(a).data, nodeData(b).data);
}
}
function nodeData(node) {
return d3.select(node).datum();
}
function setEnterAngle() {
var enterAngle = previousAdjacentAngle(this);
previousSegmentData.set(this, {
startAngle: enterAngle,
endAngle: enterAngle,
innerRadius: o.arc.innerRadius()(),
outerRadius: o.arc.outerRadius()()
});
}
function setExitAngle(d) {
var exitAngle = currentAdjacentAngle(this);
d.startAngle = exitAngle;
d.endAngle = exitAngle;
}
function render(transition) {
transition.attrTween('d', arcTween);
}
function arcTween() {
var i = interpolate(this);
previousSegmentData.set(this, i(0));
return function(t) {
var interation = i(t);
o.arc
.innerRadius(interation.innerRadius)
.outerRadius(interation.outerRadius);
return o.arc(interation);
};
}
function interpolate(segment) {
var d = d3.select(segment).datum();
var newData = {
startAngle: d.startAngle + o.offset,
endAngle: d.endAngle + o.offset,
innerRadius: o.arc.innerRadius()(),
outerRadius: o.arc.outerRadius()()
};
return d3.interpolate(previousSegmentData.get(segment), newData);
}
methods.enteringSegments = function (_) {
enteringSegments = _;
updateNodes();
return methods;
};
methods.transitioningSegments = function (_) {
transitioningSegments = _;
updateNodes();
return methods;
};
methods.arc = function(_) {
if (!arguments.length) {return o.arc;}
o.arc = _;
return methods;
};
methods.sort = function(_) {
if (!arguments.length) {return o.sort;}
o.sort = _;
return methods;
};
methods.offset = function(_) {
if (!arguments.length) {return o.offset;}
o.offset = _;
return methods;
};
return methods;
};
if(typeof APP === 'undefined') {APP = {};}
APP.rotatingDonut = function() {
'use strict';
var o,
events,
local,
rotation;
o = {
animationDuration: 600,
thickness: 0.4,
value: null,
color: null,
key: null,
sort: null
};
events = d3.dispatch('mouseenter', 'mouseleave', 'click');
local = {
label: d3.local(),
animate: d3.local(),
dimensions: d3.local()
};
rotation = APP.pieSelectionRotation()
.key(function(d) {return o.key(d);});
function donut(group) {
group.each(function(data) {
render.call(this, data, group);
});
}
function render(data, group) {
var context,
t,
dim,
pie,
arc,
pieTransition,
segments,
segmentEnter;
if (!data) {return;}
context = d3.select(this);
if (group instanceof d3.transition) {
t = d3.transition(group);
} else {
t = d3.transition().duration(o.animationDuration);
}
dim = getDimensions(context);
pie = d3.pie()
.value(o.value)
.sort(null);
arc = d3.arc()
.outerRadius(dim.outerRadius)
.innerRadius(dim.innerRadius);
pieTransition = local.animate.get(this) || local.animate.set(this, APP.pieTransition());
context.selectAll('svg')
.data([pie(data.sort(o.sort))])
.call(rotation)
.enter()
.append('svg')
.append('g')
.attr('class', 'group')
.append('text')
.attr('class', 'donut-label')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle');
context.selectAll('svg')
.transition(t)
.attr('width', dim.width)
.attr('height', dim.height)
.selectAll('g.group')
.attr('transform', 'translate(' + dim.width / 2 + ',' + dim.height / 2 + ')');
context.select('text.donut-label')
.text(local.label.get(context.node()));
segments = context.selectAll('svg')
.select('g.group')
.selectAll('path.segment')
.data(Object, dataAccess('key'));
segmentEnter = segments.enter()
.append('path')
.attr('class', 'segment')
.attr('fill', dataAccess('color'))
.on('mouseenter mouseleave click', onPathEvent(context));
pieTransition
.arc(arc)
.sort(o.sort)
.enteringSegments(segmentEnter)
.transitioningSegments(segments)
.offset(rotation.getAngle(context.select('svg')));
segmentEnter
.transition(t)
.call(pieTransition.enter);
segments
.transition(t)
.call(pieTransition.transition);
segments.exit()
.transition(t)
.call(pieTransition.exit)
.remove();
}
function onPathEvent(context) {
return function(d) {
if (d3.event.type === 'click') {
rotation.selectedSegment(context.select('svg'), d.data);
context.call(donut);
}
events.call(d3.event.type, context.node(), d.data);
};
}
function dataAccess(key) {
return function(d) {
return o[key](d.data);
};
}
function getDimensions(context) {
var thisDimensions = local.dimensions.get(context.node()) || {},
width = thisDimensions.width || context.node().getBoundingClientRect().width,
height = thisDimensions.height || context.node().getBoundingClientRect().height,
outerRadius = Math.min(width, height) / 2,
innerRadius = outerRadius * (1 - o.thickness);
return {
width: width,
height: height,
outerRadius: outerRadius,
innerRadius: innerRadius
};
}
donut.selectedSegment = function(context, d) {
if (typeof d === 'undefined' ) {return rotation.selectedSegment(context.select('svg'));}
rotation.selectedSegment(context.select('svg'), d);
return donut;
};
donut.alignmentAngle = function(_) {
if (typeof _ === 'undefined' ) {return rotation.alignmentAngle();}
rotation.alignmentAngle(_);
return donut;
};
donut.animationDuration = function(_) {
if (!arguments.length) {return o.animationDuration;}
o.animationDuration = _;
return donut;
};
donut.thickness = function(_) {
if (!arguments.length) {return o.thickness;}
o.thickness = _;
return donut;
};
donut.value = function(_) {
if (!arguments.length) {return o.value;}
o.value = _;
return donut;
};
donut.color = function(_) {
if (!arguments.length) {return o.color;}
o.color = _;
return donut;
};
donut.key = function(_) {
if (!arguments.length) {return o.key;}
o.key = _;
return donut;
};
donut.sort = function(_) {
if (!arguments.length) {return o.sort;}
o.sort = _;
return donut;
};
donut.dimensions = function(context, _) {
var returnArray;
if (typeof _ === 'undefined' ) {
returnArray = context.nodes()
.map(function (node) {return local.dimensions.get(node);});
return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
}
context.each(function() {local.dimensions.set(this, _);});
return donut;
};
donut.label = function(context, _) {
var returnArray;
if (typeof _ === 'undefined' ) {
returnArray = context.nodes()
.map(function (node) {return local.label.get(node);});
return context._groups[0] instanceof NodeList ? returnArray : returnArray[0];
}
context.each(function() {local.label.set(this, _);});
return donut;
};
donut.on = function(evt, callback) {
events.on(evt, callback);
return donut;
};
return donut;
};