<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Mocha Tests</title>
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />
<script data-require="mocha@*" data-semver="1.13.0" src="//cdnjs.cloudflare.com/ajax/libs/mocha/1.13.0/mocha.js"></script>
<script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
<script data-require="mocha-chai@*" data-semver="1.9.0" src="//cdnjs.cloudflare.com/ajax/libs/chai/1.9.0/chai.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="pie_icons.js"></script>
<script src="basic_legend.js"></script>
<script src="description_with_arrow.js"></script>
<link data-require="mocha@*" data-semver="1.13.0" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/mocha/1.13.0/mocha.css" />
</head>
<body>
<div id="mocha"></div>
<script>mocha.setup('bdd')</script>
<script src="expectations.js"></script>
<script src="basic_legend_test.js"></script>
<script src="description_with_arrow_test.js"></script>
<script src="pie_icons_test.js"></script>
<script src="pie_selection_rotation_test.js"></script>
<script src="pie_transitions_test.js"></script>
<script src="rotating_donut_test.js"></script>
<script>
mocha.checkLeaks();
mocha.globals(['d3', 'APP']);
mocha.run();
</script>
</body>
</html>
if(typeof APP === 'undefined') {APP = {};}
APP.generateData = function(splice) {
'use strict';
// Icons from Freepik at http://www.flaticon.com/packs/miscellaneous-elements
var icons = ['car.svg', 'idea.svg', 'phone-call.svg', 'shopping-cart.svg', 'cutlery.svg'],
labels = ['travel', 'electricity', 'phone', 'shopping', 'food'],
descriptions = [
'Including car payments, fuel, tolls', 'Electric Bill',
'Cell phone, cell plan, land-line',
'Any non-food shopping items such as clothing, gifts, etc.',
'Groceries and restaurant expenses'
],
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),
icon: 'images/' + icons[i - 1],
label: labels[i - 1],
description: descriptions[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.rotatingDonut = function() {
'use strict';
var o,
events,
local,
rotation;
o = {
animationDuration: 600,
iconSize: 0.7,
thickness: 0.4,
value: null,
icon: null,
color: null,
key: null,
sort: null
};
events = d3.dispatch('mouseenter', 'mouseleave', 'click');
local = {
label: d3.local(),
animate: d3.local(),
icons: 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,
pieIcons,
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());
pieIcons = local.icons.get(this) || local.icons.set(this, APP.pieIcons());
pieIcons
.container(function() {return context.select('g.group');})
.iconPath(dataAccess('icon'))
.imageWidth(dim.outerRadius * o.thickness * o.iconSize)
.interpolate(pieTransition.interpolate);
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
.call(pieIcons)
.transition(t)
.call(pieTransition.enter)
.call(pieIcons.tween);
segments
.transition(t)
.call(pieTransition.transition)
.call(pieIcons.tween);
segments.exit()
.transition(t)
.call(pieTransition.exit)
.call(pieIcons.exitTween)
.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.iconSize = function(_) {
if (!arguments.length) {return o.iconSize;}
o.iconSize = _;
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.icon = function(_) {
if (!arguments.length) {return o.icon;}
o.icon = _;
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;
};
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.descriptionWithArrow = function() {
'use strict';
var o = {
label: null,
text: null
};
function description(group) {
group.each(render);
}
function render(data) {
var context = d3.select(this),
right;
context
.html('')
.classed('no-data', !data)
.append('div')
.attr('class', 'desc-left arrow')
.html('←');
right = context.append('div')
.attr('class', 'desc-right');
right.append('div')
.attr('class', 'label')
.text(o.label);
right.append('div')
.attr('class', 'text')
.text(o.text);
}
description.label = function(_) {
if (!arguments.length) {return o.label;}
o.label = _;
return description;
};
description.text = function(_) {
if (!arguments.length) {return o.text;}
o.text = _;
return description;
};
return description;
};
if(typeof APP === 'undefined') {APP = {};}
APP.pieIcons = function() {
'use strict';
var icon = d3.local();
var o = {
iconPath: null,
imageWidth: null,
interpolate: null,
container: function (selection) {return d3.select(selection._parents[0]);}
};
function icons(group) {
var container = o.container(group);
group.each(function(data) {
render.call(this, data, container);
});
}
function render(data, container) {
var thisIcon = container
.append('image')
.attr('class', 'icon')
.attr('xlink:href', o.iconPath.bind(null, data))
.attr('width', o.imageWidth)
.attr('height', o.imageWidth)
.style('opacity', 0);
icon.set(this, thisIcon);
}
function iconTranslate(i, t) {
var dimensions = this.getBoundingClientRect(),
coords = d3.arc().centroid(i(t)),
adjustedCoords = [
coords[0] - dimensions.width / 2,
coords[1] - dimensions.height / 2
];
return 'translate(' + adjustedCoords.join(',') + ')';
}
function removeIfParentIsGone(pieSegment) {
return function() {
if (!document.body.contains(pieSegment)) {
this.remove();
}
};
}
function iconTween(pieSegment) {
var i = o.interpolate(pieSegment);
return function () {
return iconTranslate.bind(this, i);
};
}
icons.tween = function (transition, isExiting) {
transition.selection().each(function () {
icon.get(this)
.transition(transition)
.duration(transition.duration())
.attr('width', o.imageWidth)
.attr('height', o.imageWidth)
.style('opacity', Number(!isExiting))
.attrTween('transform', iconTween(this))
.on('end', removeIfParentIsGone(this));
});
};
icons.exitTween = function(transition) {
icons.tween(transition, true);
};
icons.iconPath = function(_) {
if (!arguments.length) {return o.iconPath;}
o.iconPath = _;
return icons;
};
icons.imageWidth = function(_) {
if (!arguments.length) {return o.imageWidth;}
o.imageWidth = _;
return icons;
};
icons.interpolate = function(_) {
if (!arguments.length) {return o.interpolate;}
o.interpolate = _;
return icons;
};
icons.container = function(_) {
if (!arguments.length) {return o.container;}
o.container = _;
return icons;
};
return icons;
};
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);
},
interpolate: function (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);
}
};
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 = methods.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);
};
}
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;
};
describe('BASIC LEGEND', function() {
var t = chai.assert;
var initialData = [
{id: 1, color: 'red', label: 'item 1'},
{id: 2, color: 'blue', label: 'item 2'},
{id: 3, color: 'green', label: 'item 3'}
];
var updatedData = [
{id: 1, color: 'red', label: 'item 1'},
{id: 3, color: 'green', label: 'item 3'},
{id: 4, color: 'yellow', label: 'item 4'}
];
it('Allows Multiple Instances', function() {
var legend1 = APP.basicLegend().key(1);
var legend2 = APP.basicLegend().key(2);
t.notEqual(legend1.key(), legend2.key());
});
describe('Options', function() {
var defaults = {label: null, key: null, color: null},
custom = {label: 1, key: 2, color: 3},
legend = APP.basicLegend();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
t.equal(legend[option](), defaults[option]);
})
});
});
describe('Set Values', function() {
before(function() {
legend
.label(custom.label)
.color(custom.color)
.key(custom.key);
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(legend[option](), custom[option]);
});
});
});
});
describe('Adds to Dom', function() {
var node,
legend;
beforeEach(function() {
node = document.createElement('div');
legend = new Legend();
d3.select(node)
.datum(initialData)
.transition()
.duration(0)
.call(legend);
});
it('Initial Data Loads', function(done) {
setTimeout(function(){
t.equal(node.innerHTML, expected.legendInitial);
done()
}, 30)
});
it('Loads Updated Data', function(done) {
d3.select(node)
.datum(updatedData)
.transition()
.duration(0)
.on('end', function() {
setTimeout(function() {
t.equal(node.innerHTML, expected.legendUpdated);
done();
}, 30)
})
.call(legend);
})
});
describe('Events', function() {
var div,
legend;
beforeEach(function() {
div = document.createElement('div');
legend = new Legend();
d3.select(div)
.datum(initialData)
.transition()
.duration(0)
.call(legend);
});
['mouseenter', 'mouseleave', 'click'].forEach(function(evt) {
it(evt, function(done) {
legend.on(evt, function(d) {
t.deepEqual(d, initialData[1], 'incorrect data is emitted');
done();
});
d3.select(div)
.selectAll('li:nth-child(2)')
.dispatch(evt)
});
})
});
describe('Highlight', function() {
var div,
legend;
beforeEach(function() {
div = document.createElement('div');
legend = new Legend();
d3.select(div)
.datum(initialData)
.call(legend)
.call(legend.highlight, initialData[1]);
});
it('highlights correct item', function() {
var shouldBeHovered = d3.select(div)
.selectAll('li:nth-child(2)')
.classed('hovered');
t.ok(shouldBeHovered);
});
it("doesn't highlight incorrect item", function() {
var shouldNotBeHovered = d3.select(div)
.selectAll('li:nth-child(1)')
.classed('hovered');
t.notOk(shouldNotBeHovered);
})
});
describe('Selected Item', function() {
var div,
legend;
beforeEach(function() {
div = document.createElement('div');
legend = new Legend();
d3.select(div)
.datum(initialData)
.call(legend)
.call(legend.highlight, initialData[1]);
});
it('initially undefined', function() {
t.equal(typeof legend.selectedItem(d3.select(div)), 'undefined');
});
it('set to correct item', function() {
d3.select(div).call(legend.selectedItem, initialData[1]);
t.equal(legend.selectedItem(d3.select(div)), initialData[1]);
});
});
function Legend() {
return APP.basicLegend()
.label(function(d) {return d.label})
.color(function(d) {return d.color})
.key(function(d) {return d.id});
}
});
describe('DESCRIPTION WITH ARROW', function () {
var t = chai.assert,
initialData = {label: 'a', text: 'item 1'},
updatedData = {label: 'b', text: 'item 2'};
it('Allows Multiple Instances', function() {
var descriptionWithArrow1 = APP.descriptionWithArrow().text(1);
var descriptionWithArrow2 = APP.descriptionWithArrow().text(2);
t.notEqual(descriptionWithArrow1.text(), descriptionWithArrow2.text());
});
describe('Options', function() {
var defaults = {label: null, text: null},
custom = {label: 1, text: 2},
description = APP.descriptionWithArrow();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
t.equal(description[option](), defaults[option]);
})
});
});
describe('Set Values', function() {
before(function() {
description
.label(custom.label)
.text(custom.text);
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(description[option](), custom[option]);
});
});
});
});
describe('Adds to Dom', function() {
var node,
description;
beforeEach(function() {
node = document.createElement('div');
description = APP.descriptionWithArrow()
.label(function(d) {return d.label;})
.text(function(d) {return d.text;});
d3.select(node)
.datum(initialData)
.transition()
.duration(0)
.call(description)
});
it('Initial Data Loads', function(done) {
t.equal(node.innerHTML, expected.descriptionInitial);
done()
});
it('Loads Updated Data', function(done) {
d3.select(node)
.datum(updatedData)
.transition()
.duration(0)
.on('end', function() {
setTimeout(function() {
t.equal(node.innerHTML, expected.descriptionUpdated);
done();
}, 0)
})
.call(description);
})
});
});
var expected = {
legendInitial: (function() {
return '<ul class="legend">' +
'<li class="legend-label" data-id="1" style="left: 12px; opacity: 1; top: 0px;">' +
'<svg width="22" height="22">' +
'<rect fill="red" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 1</span>' +
'</li>' +
'<li class="legend-label" data-id="2" style="left: 12px; opacity: 1; top: 22px;">' +
'<svg width="22" height="22">' +
'<rect fill="blue" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 2</span>' +
'</li>' +
'<li class="legend-label" data-id="3" style="left: 12px; opacity: 1; top: 44px;">' +
'<svg width="22" height="22">' +
'<rect fill="green" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 3</span>' +
'</li>' +
'</ul>'
}()),
legendUpdated: (function() {
return '<ul class="legend">' +
'<li class="legend-label" data-id="1" style="left: 12px; opacity: 1; top: 0px;">' +
'<svg width="22" height="22">' +
'<rect fill="red" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 1</span></li>' +
'<li class="legend-label" data-id="3" style="left: 12px; opacity: 1; top: 22px;">' +
'<svg width="22" height="22">' +
'<rect fill="green" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 3</span></li>' +
'<li class="legend-label" data-id="4" style="left: 12px; opacity: 1; top: 44px;">' +
'<svg width="22" height="22">' +
'<rect fill="yellow" width="20" height="20" x="1" y="1"></rect>' +
'</svg>' +
'<span>item 4</span></li>' +
'</ul>'
}()),
descriptionInitial: (function() {
return '<div class="desc-left arrow">←</div>' +
'<div class="desc-right">' +
'<div class="label">a</div>' +
'<div class="text">item 1</div>' +
'</div>'
}()),
descriptionUpdated: (function() {
return '<div class="desc-left arrow">←</div>' +
'<div class="desc-right">' +
'<div class="label">b</div>' +
'<div class="text">item 2</div>' +
'</div>'
}()),
pieIconsInitial: (function() {
return '<svg>' +
'<g></g>' +
'<g></g>' +
'<g></g>' +
'<image class="icon" href="../images/car.svg" width="20" height="20" style="opacity: 0;"></image>' +
'<image class="icon" href="../images/cutlery.svg" width="20" height="20" style="opacity: 0;"></image>' +
'<image class="icon" href="../images/idea.svg" width="20" height="20" style="opacity: 0;"></image>' +
'</svg>'
}()),
pieIconsTweened: (function() {
return '<svg>' +
'<g></g>' +
'<g></g>' +
'<g></g>' +
'<image class="icon" href="../images/car.svg" width="20" height="20" transform="translate(28.90365984439606,21.408681136136956)" style="opacity: 1;"></image>' +
'<image class="icon" href="../images/cutlery.svg" width="20" height="20" transform="translate(28.90365984439606,21.408681136136956)" style="opacity: 1;"></image>' +
'<image class="icon" href="../images/idea.svg" width="20" height="20" transform="translate(28.90365984439606,21.408681136136956)" style="opacity: 1;"></image>' +
'</svg>'
}())
};
describe('PIE ICONS', function () {
var t = chai.assert;
var initialData = [
{iconPath: '../images/car.svg'},
{iconPath: '../images/cutlery.svg'},
{iconPath: '../images/idea.svg'}
];
it('Allows Multiple Instances', function() {
var descriptionWithArrow1 = APP.descriptionWithArrow().text(1);
var descriptionWithArrow2 = APP.descriptionWithArrow().text(2);
t.notEqual(descriptionWithArrow1.text(), descriptionWithArrow2.text());
});
describe('Options', function() {
var defaults = {
iconPath: null,
imageWidth: null,
interpolate: null,
container: function (selection) {return d3.select(selection._parents[0]);}
},
custom = {
iconPath: 1,
imageWidth: 2,
interpolate: 3,
container: 4
},
pieIcons = APP.pieIcons();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
if (typeof pieIcons[option]() === 'function') {
t.equal(pieIcons[option]().toString(), defaults[option].toString());
} else {
t.equal(pieIcons[option](), defaults[option]);
}
})
});
});
describe('Set Values', function() {
before(function() {
pieIcons
.iconPath(custom.iconPath)
.imageWidth(custom.imageWidth)
.interpolate(custom.interpolate)
.container(custom.container);
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(pieIcons[option](), custom[option]);
});
});
});
});
describe('Adds to DOM', function() {
var node,
iStart = {startAngle: 1, endAngle: 1.5, innerRadius: 30, outerRadius: 50},
iEnd = {startAngle: 2, endAngle: 2.5, innerRadius: 40, outerRadius: 60},
pieIcons,
trans,
dom;
beforeEach(function() {
node = document.createElement('div');
window.document.body.appendChild(node);
pieIcons = APP.pieIcons()
.iconPath(function(d) {return d.iconPath;})
.imageWidth(20)
.interpolate(function() {return d3.interpolate(iStart, iEnd)});
trans = d3.transition().duration(1);
dom = d3.select(node)
.append('svg')
.selectAll('g')
.data(initialData)
.enter()
.append('g')
.transition()
.duration(0)
.call(pieIcons);
});
afterEach(function() {
window.document.body.removeChild(node);
});
it('Initial Data Loads', function() {
var comparisonNode = document.createElement('div');
comparisonNode.innerHTML = expected.pieIconsInitial;
t.ok(equivElms(node, comparisonNode));
});
it('Loads Updated Data', function(done) {
var completedTransitions = 0;
dom.transition(trans)
.call(pieIcons.tween)
.on('end', function onEnd() {
var comparisonNode;
if (++completedTransitions >= initialData.length) {
comparisonNode = document.createElement('div');
comparisonNode.innerHTML = expected.pieIconsTweened;
t.ok(equivElms(node, comparisonNode));
done();
}
});
})
});
// https://stackoverflow.com/a/10679802
function equivElms(elm1, elm2) {
var attrs1, attrs2, name, node1, node2, index;
function getAttributeNames(node) {
var index, rv, attrs;
rv = [];
attrs = node.attributes;
for (index = 0; index < attrs.length; ++index) {
rv.push(attrs[index].nodeName);
}
rv.sort();
return rv;
}
// Compare attributes without order sensitivity
attrs1 = getAttributeNames(elm1);
attrs2 = getAttributeNames(elm2);
if (attrs1.join(",") !== attrs2.join(",")) {
console.log("Found nodes with different sets of attributes; not equiv");
return false;
}
// ...and values
// unless you want to compare DOM0 event handlers
// (onclick="...")
for (index = 0; index < attrs1.length; ++index) {
name = attrs1[index];
if (elm1.getAttribute(name) !== elm2.getAttribute(name)) {
console.log("Found nodes with mis-matched values for attribute '" + name + "'; not equiv");
return false;
}
}
// Walk the children
for (node1 = elm1.firstChild, node2 = elm2.firstChild;
node1 && node2;
node1 = node1.nextSibling, node2 = node2.nextSibling) {
if (node1.nodeType !== node2.nodeType) {
console.log("Found nodes of different types; not equiv");
return false;
}
if (node1.nodeType === 1) { // Element
if (!equivElms(node1, node2)) {
return false;
}
}
else if (node1.nodeValue !== node2.nodeValue) {
console.log("Found nodes with mis-matched nodeValues; not equiv");
return false;
}
}
if (node1 || node2) {
// One of the elements had more nodes than the other
console.log("Found more children of one element than the other; not equivalent");
return false;
}
// Seem the same
return true;
}
});
describe('PIE SELECTION ROTATION', function () {
var t = chai.assert;
it('Allows Multiple Instances', function() {
var pieSelectionRotation1 = APP.pieSelectionRotation().key(1);
var pieSelectionRotation2 = APP.pieSelectionRotation().key(2);
t.notEqual(pieSelectionRotation1.key(), pieSelectionRotation2.key());
});
describe('Options', function() {
var defaults = {
key: null,
alignmentAngle: 0
},
custom = {
key: 1,
alignmentAngle: 1
},
pieSelectionRotation = APP.pieSelectionRotation();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
t.equal((pieSelectionRotation[option]() || '').toString(), (defaults[option] || '').toString());
})
});
});
describe('Set Values', function() {
before(function() {
pieSelectionRotation
.key(custom.key)
.alignmentAngle(custom.alignmentAngle);
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(pieSelectionRotation[option](), custom[option]);
});
});
});
});
describe('selectedSegment', function() {
var data = [
{data:{id: 1}},
{data:{id: 99}}
],
node,
pieSelectionRotation;
beforeEach(function() {
node = document.createElement('div');
pieSelectionRotation = APP.pieSelectionRotation().key(function(d) {return d.id});
d3.select(node)
.datum(data)
.call(pieSelectionRotation);
});
it('initially undefined', function() {
t.typeOf(pieSelectionRotation.selectedSegment(d3.selectAll(node)), 'undefined');
});
it('sets and retrieves selected data', function() {
d3.select(node)
.call(pieSelectionRotation.selectedSegment, {id: 99})
.call(pieSelectionRotation);
t.equal(pieSelectionRotation.selectedSegment(d3.select(node)).id, 99);
});
});
describe('getAngle', function() {
var data = [
{data:{id: 1}, startAngle: 0.1, endAngle: 0.2},
{data:{id: 99}, startAngle: 0.3, endAngle: 0.4}
],
node,
pieSelectionRotation;
beforeEach(function() {
node = document.createElement('div');
pieSelectionRotation = APP.pieSelectionRotation().key(function(d) {return d.id});
d3.select(node)
.datum(data)
.call(pieSelectionRotation);
});
it('initially undefined', function() {
t.typeOf(pieSelectionRotation.getAngle(d3.selectAll(node)), 'undefined');
});
it('return correct angle', function() {
d3.select(node)
.call(pieSelectionRotation.selectedSegment, {id: 99})
.call(pieSelectionRotation);
t.equal(pieSelectionRotation.getAngle(d3.select(node)), -0.35);
});
it('applies correct offset', function() {
pieSelectionRotation.alignmentAngle(90);
d3.select(node)
.call(pieSelectionRotation.selectedSegment, {id: 99})
.call(pieSelectionRotation);
t.equal(pieSelectionRotation.getAngle(d3.select(node)), (Math.PI / 2) - 0.35);
});
})
});
describe('PIE TRANSITIONS', function () {
var t = chai.assert;
it('Allows Multiple Instances', function() {
var pieTransition1 = APP.pieTransition().offset(1);
var pieTransition2 = APP.pieTransition().offset(2);
t.notEqual(pieTransition1.offset(), pieTransition2.offset());
});
describe('Options', function() {
var defaults = {
arc: null,
sort: null,
offset: 0
},
custom = {
arc: 1,
sort: 2,
offset: 3
},
pieTransition = APP.pieTransition();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
t.equal((pieTransition[option]() || '').toString(), (defaults[option] || '').toString());
})
});
});
describe('Set Values', function() {
before(function() {
pieTransition
.arc(custom.arc)
.sort(custom.sort)
.offset(custom.offset);
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(pieTransition[option](), custom[option]);
});
});
});
});
});
describe('ROTATING DONUT', function() {
var t = chai.assert;
var initialData = [
{
"id": 1,
"value": 15.213097270116679,
"color": "#1f77b4",
"icon": "../images/car.svg"
}, {
"id": 2,
"value": 5.613515522616516,
"color": "#ff7f0e",
"icon": "../images/idea.svg"
}, {
"id": 3,
"value": 14.16732472832057,
"color": "#2ca02c",
"icon": "../images/phone-call.svg"
}, {
"id": 4,
"value": 17.71135749720593,
"color": "#d62728",
"icon": "../images/shopping-cart.svg"
}, {
"id": 5,
"value": 13.292696478959586,
"color": "#9467bd",
"icon": "../images/cutlery.svg"
}
];
var updatedData = [
{
"id": 1,
"value": 15.22440168193661,
"color": "#1f77b4",
"icon": "../images/car.svg"
}, {
"id": 2,
"value": 9.47758140497308,
"color": "#ff7f0e",
"icon": "../images/idea.svg"
}, {
"id": 3,
"value": 9.000185206567648,
"color": "#2ca02c",
"icon": "../images/phone-call.svg"
}, {
"id": 4,
"value": 19.675559607646996,
"color": "#d62728",
"icon": "../images/shopping-cart.svg"
}, {
"id": 5,
"value": 11.145987996393668,
"color": "#9467bd",
"icon": "../images/cutlery.svg"
}
];
it('Allows Multiple Instances', function() {
var rotatingDonut1 = APP.rotatingDonut().key(1);
var rotatingDonut2 = APP.rotatingDonut().key(2);
t.notEqual(rotatingDonut1.key(), rotatingDonut2.key());
});
describe('Options', function() {
var defaults = {
animationDuration: 600,
iconSize: 0.7,
thickness: 0.4,
value: null,
icon: null,
color: null,
key: null,
sort: null
},
custom = {
animationDuration: 1,
iconSize: 2,
thickness: 3,
value: 4,
icon: 5,
color: 6,
key: 7,
sort: 8
},
donut = APP.rotatingDonut();
describe('Default Values', function() {
Object.keys(defaults).forEach(function(option) {
it(option, function() {
t.equal(donut[option](), defaults[option]);
})
});
});
describe('Set Values', function() {
before(function() {
Object.keys(defaults).forEach(function(option) {
donut[option](custom[option])
});
});
Object.keys(custom).forEach(function(option) {
it(option, function() {
t.equal(donut[option](), custom[option]);
});
});
});
});
describe('Adds to Dom', function() {
var node,
donut;
var path = d3.arc()
.outerRadius(50)
.innerRadius(25);
var pie = d3.pie()
.value(function(d) {return d.value})
.sort(null);
beforeEach(function() {
node = document.createElement('div');
donut = new Donut();
d3.select(node)
.datum(initialData)
.call(donut.label, 'The label')
.call(donut.dimensions, {width: 200, height: 100})
.transition()
.duration(0)
.call(donut);
});
it('Initial Data Loads', function(done) {
var comparisonPaths = pie(initialData)
.map(function(d) {return path(d);});
d3.select(node)
.datum(initialData)
.transition()
.duration(0)
.call(donut);
setTimeout(function() {
t.equal(d3.select(node).select('.donut-label').text(), 'The label');
t.deepEqual(getPathsD(node), comparisonPaths);
done();
}, 10)
});
it('Updated Data Loads', function(done) {
var comparisonPaths = pie(updatedData)
.map(function(d) {return path(d);});
d3.select(node)
.datum(updatedData)
.call(donut.label, 'The new label')
.transition()
.duration(0)
.call(donut)
.on('end', function onEnd() {
setTimeout(function() {
t.equal(d3.select(node).select('.donut-label').text(), 'The new label');
t.deepEqual(getPathsD(node), comparisonPaths);
done();
}, 10)
});
});
it('Rotates to Selected', function(done) {
var index = 2,
pieData = pie(updatedData),
comparisonAngle = d3.mean([pieData[index].startAngle, pieData[index].endAngle]);
var offsetData = pieData.map(function(d) {
return {
startAngle: d.startAngle - comparisonAngle,
endAngle: d.endAngle - comparisonAngle
};
});
var comparisonPaths = offsetData
.map(function(d) {return path(d);});
d3.select(node)
.datum(updatedData)
.call(donut.selectedSegment, updatedData[index])
.transition()
.duration(0)
.call(donut)
.on('end', function onEnd() {
t.deepEqual(getPathsD(node), comparisonPaths);
done();
});
});
});
describe('Events', function() {
var index = 2,
node = document.createElement('div'),
donut = new Donut();
d3.select(node)
.datum(initialData)
.transition()
.duration(0)
.call(donut);
it('click', function(done) {
donut.on('click', function(d) {
t.deepEqual(d, initialData[index], 'incorrect data is emitted');
done();
});
d3.select(node)
.selectAll('path')
.filter(function(d, i) {return i === index})
.dispatch('click')
});
});
function getPathsD(node) {
return d3.select(node)
.selectAll('path')
.nodes()
.map(function(path) {return path.getAttribute('d')});
}
function Donut() {
return APP.rotatingDonut()
.alignmentAngle(0)
.iconSize(0.5)
.thickness(0.5)
.value(function(d) {return d.value})
.icon(function(d) {return d.icon})
.color(function(d) {return d.color})
.key(function(d) {return d.id})
.sort(function(a, b) {return a.id - b.id});
}
});