<!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('&larr;');

    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});
  }
});