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