block by almccon 7aa05d3ae0afc0f42ae5792c276620c4

galaxy

Full Screen

Built with blockbuilder.org

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="d3.v3.min.js"></script>
  <script src="d3.slider.js"></script>
  <link rel="stylesheet" href="d3.slider.css" />
  <style>
    body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0;background:black;color:white;font: 10px sans-serif; }
    path#spiral1fixed,
    path#spiral2fixed,
    path#spiral1,
    path#spiral2 {
      fill: none;
      stroke: #222;
      //stroke: none;
      stroke-width: 3px;
    }
    path#spiral1fixed,
    path#spiral2fixed {
      display: none;
    }
    svg {
      float: left;
    }
  </style>
</head>

<body>
  <script>
    var width = 500,
        height = 300
        start = 0   // for the spiral
        end = 2;    // for the spiral

    var center = { x: width/2, y: height/2};
    var dotWidth = d3.min([width,height])/50; // size of dots relative to view
    var step = dotWidth * 3;  // spacing relative to dot size
    var offset = 42;  //initial step
    var numDots = 7;
    var numDotsMax = 50;
    var decay = 1; // to make the dots smaller after each step

    var centralRatio = 2; // how big the centralDot is relative to other dots

    var tightness = 1; // how tight the spiral is

    var rotate = 30; // Initial rotation

    // for rotating the view in "3d" space
    var spinAngle = 50; // between -180 and 180
    var tiltAngle = 40;//45; // between 0 and 90

    var params = {
      numDots: {
        value: 7,
        min: 0,
        max: numDotsMax,
        step: 1
      },
      dotWidth: {   // width of dots
        value: 6,
        min: 0,
        max: 100,
        step: null
      },
      step: {       // spacing of dots along arms
        value: 18,
        min: 0,
        max: 100,
        step: 1
      },
      offset: {     // initial step away from the galactic center
        value: 42,
        min: 0,
        max: 300,
        step: 1
      },
      rotate: {     // initial rotation of the galaxy on its axis
        value: 30,
        min: 0,
        max: 1800,
        step: 1
      },
      spinAngle: {  // for rotating the view in "3d" space
        value: 50,
        min: 0,
        max: 90,
        step: 1
      },
      tiltAngle: {  // for rotating the view in "3d" space
        value: 40,
        min: 0,
        max: 90,
        step: 1
      },
      centralRatio: {// how big the centralDot is relative to other dots
        value: 2,
        min: 0,
        max: 5,
        step: null
      },
      tightness: {  // how tight the spiral is
        value: 1,
        min: 0,
        max: 5,
        step: null
      },
      decay: {      // to make the dots smaller after each step
        value: 1,
        min: 0,
        max: 5,
        step: null
      }
    };

    // create sliders
    d3.keys(params).forEach(function(key) {
      params[key].slider = d3.slider()
        .axis(d3.svg.axis().ticks(0))
        .min(params[key].min)
        .max(params[key].max)
        .value(params[key].value)
        .on("slide", function(evt, value) {
          console.log("slide",key,value);
          params[key].value = value;
          redraw();
        });
      if (params[key].step)
        params[key].slider.step(params[key].step);
    });

    var numDotsSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(30).step(1).value(numDots);
    var dotWidthSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(100).step(1).value(dotWidth);
    var stepSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(100).step(1).value(step);
    var offsetSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(300).step(1).value(offset);
    var rotateSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(1800).step(1).value(rotate);
    var spinSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(90).step(1).value(spinAngle);
    var tiltSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(90).step(1).value(tiltAngle);
    var centralSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(5).step(null).value(centralRatio);
    var tightnessSlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(5).step(null).value(tightness);
    var decaySlider = d3.slider().axis(d3.svg.axis().ticks(0)).min(0).max(2).step(null).value(decay);

    var theta = function(r) {
      return -2*Math.PI*r*params.tightness.value;
    };
    var radius = d3.scale.linear()
      .domain([start, end])
      .range([0, d3.min([width,height])]);
    var spiralLine1 = d3.svg.line()
      .x(function(d) {
        return getGalacticX(d)
          *(90-spinAngle)/90 // should include cos or sin
          +(getGalacticY(d)*(tiltAngle)/90); // should include cos or sin
      })
      .y(function(d) {
        return getGalacticY(d)
          *(90-tiltAngle)/90; // should include cos or sin
      })
    var spiralLine2 = d3.svg.line()
      .x(function(d) {
        return getGalacticX(d, 'left')
          *(90-spinAngle)/90 // should include cos or sin
          +(getGalacticY(d, 'left')*(tiltAngle)/90); // should include cos or sin
      })
      .y(function(d) {
        return getGalacticY(d, 'left')
          *(90-tiltAngle)/90; // should include cos or sin
      })

    var spiralLineFixed1 = d3.svg.line()
      .x(function(d) { return getGalacticX(d); })
      .y(function(d) { return getGalacticY(d); })
    var spiralLineFixed2 = d3.svg.line()
      .x(function(d) { return getGalacticX(d, 'left'); })
      .y(function(d) { return getGalacticY(d, 'left'); })


    // GalacticX and GalacticY are relative to a 0,0 at the center of the galaxy
    getGalacticX = function(d, arm = 'right') {
      var extraRotation = (arm == 'left') ? 0 : Math.PI;
      return radius(d)*Math.cos((rotate/180)+extraRotation+theta(d));
    }
    getGalacticY = function(d, arm = 'right') {
      var extraRotation = (arm == 'left') ? 0 : Math.PI;
      return radius(d)*Math.sin((rotate/180)+extraRotation+theta(d));
    }

    var values = [...Array(numDotsMax).keys()];

    var pieces = d3.range(start, end+0.001, (end-start)/1000);

    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    var spiral1 = svg.append("g").selectAll("#spiral1")
        .data([pieces])
      .enter().append("path")
        .attr("id", "spiral1")
        .attr("d", spiralLine1)
        .attr("transform",
          "translate(" + center.x + "," + center.y +") "
        );

    var spiral2 = svg.append("g").selectAll("#spiral2")
        .data([pieces])
      .enter().append("path")
        .attr("id", "spiral2")
        .attr("d", spiralLine2)
        .attr("transform",
          "translate(" + center.x + "," + center.y +") "
        );

    var spiral1fixed = svg.append("g").selectAll("#spiral1fixed")
        .data([pieces])
      .enter().append("path")
        .attr("id", "spiral1fixed")
        .attr("d", spiralLineFixed1)
        .attr("transform",
          "translate(" + center.x + "," + center.y +") "
        );

    var spiral2fixed = svg.append("g").selectAll("#spiral2")
        .data([pieces])
      .enter().append("path")
        .attr("id", "spiral2fixed")
        .attr("d", spiralLineFixed2)
        .attr("transform",
          "translate(" + center.x + "," + center.y +") "
        );

    // Center dot:
    var centralDot = svg.append("g")
      .append("circle")
      .attr("fill","#ccc")
      //.attr("fill","none")
      //.attr("stroke","white")
      .attr("cx", center.x)
      .attr("cy", center.y)
      .attr("r", centralRatio*dotWidth);

/*
    var spiral1Circles = svg.append("g")
      .selectAll("circle")
      .attr("class", "circles1")
      .data(values)
      .enter()
      .append("circle")
      .attr("fill","none")
      .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
      .attr("cx", function(d,i) {return spiral1.node().getPointAtLength(offset+step*i).x;})
      .attr("cy", function(d,i) {return spiral1.node().getPointAtLength(offset+step*i).y;})
      .attr("r", function(d,i) {
        var radius = dotWidth - i*decay;
        return radius > 0 ? radius : 0;
      })
      .attr("transform",
        "translate(" + center.x + "," + center.y +") "
      );

    var spiral2Circles = svg.append("g")
      .selectAll("circle")
      .data(values)
      .enter()
      .append("circle")
      .attr("class", "circles2")
      .attr("fill","none")
      .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
      .attr("cx", function(d,i) {return spiral2.node().getPointAtLength(offset+step*i).x;})
      .attr("cy", function(d,i) {return spiral2.node().getPointAtLength(offset+step*i).y;})
      .attr("r", function(d,i) {
        var radius = dotWidth - i*decay;
        return radius > 0 ? radius : 0;
      })
      .attr("transform",
        "translate(" + center.x + "," + center.y +") "
      );
*/

    var spiral1CirclesFixed = svg.append("g")
      .selectAll("circle")
      .attr("class", "circles1")
      .data(values)
      .enter()
      .append("circle")
      .attr("fill","none")
      .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
      .attr("cx", function(d,i) {
        return spiral1fixed.node().getPointAtLength(offset+step*i).x
          *(90-spinAngle)/90
          +(spiral1fixed.node().getPointAtLength(offset+step*i).y*(tiltAngle)/90);
      })
      .attr("cy", function(d,i) {
        return spiral1fixed.node().getPointAtLength(offset+step*i).y
          *(90-tiltAngle)/90;
      })
      .attr("r", function(d,i) {
        var radius = dotWidth - i*decay;
        return radius > 0 ? radius : 0;
      })
      .attr("transform",
        "translate(" + center.x + "," + center.y +") "
      );

    var spiral2CirclesFixed = svg.append("g")
      .selectAll("circle")
      .data(values)
      .enter()
      .append("circle")
      .attr("class", "circles2")
      .attr("fill","none")
      .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
      .attr("cx", function(d,i) {
        return spiral2fixed.node().getPointAtLength(offset+step*i).x
          *(90-spinAngle)/90
          +(spiral2fixed.node().getPointAtLength(offset+step*i).y*(tiltAngle)/90);
      })
      .attr("cy", function(d,i) {
        return spiral2fixed.node().getPointAtLength(offset+step*i).y
          *(90-tiltAngle)/90;
      })
      .attr("r", function(d,i) {
        var radius = dotWidth - i*decay;
        return radius > 0 ? radius : 0;
      })
      .attr("transform",
        "translate(" + center.x + "," + center.y +") "
      );

    function redraw() {
      centralDot.attr("r", centralRatio*dotWidth);
      spiral1.attr("d", spiralLine1);
      spiral2.attr("d", spiralLine2);
      spiral1fixed.attr("d", spiralLineFixed1);
      spiral2fixed.attr("d", spiralLineFixed2);
/*
      spiral1Circles
        .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
        .attr("r", function(d,i) {
          var radius = dotWidth - i*decay;
          return radius > 0 ? radius : 0;
        })
        .attr("cx", function(d,i) {return spiral1.node().getPointAtLength(offset+step*i).x;})
        .attr("cy", function(d,i) {return spiral1.node().getPointAtLength(offset+step*i).y;});
      spiral2Circles
        .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
        .attr("r", function(d,i) {
          var radius = dotWidth - i*decay;
          return radius > 0 ? radius : 0;
        })
        .attr("cx", function(d,i) {return spiral2.node().getPointAtLength(offset+step*i).x;})
        .attr("cy", function(d,i) {return spiral2.node().getPointAtLength(offset+step*i).y;});
*/
      spiral1CirclesFixed
        .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
        .attr("cx", function(d,i) {
          return spiral1fixed.node().getPointAtLength(offset+step*i).x
            *(90-spinAngle)/90
            +(spiral1fixed.node().getPointAtLength(offset+step*i).y*(tiltAngle)/90);
        })
        .attr("cy", function(d,i) {
          return spiral1fixed.node().getPointAtLength(offset+step*i).y
            *(90-tiltAngle)/90;
        })
        .attr("r", function(d,i) {
          var radius = dotWidth - i*decay;
          return radius > 0 ? radius : 0;
        });
      spiral2CirclesFixed
        .attr("stroke", function(d,i) { return i < numDots ? "white" : "none"; })
        .attr("cx", function(d,i) {
          return spiral2fixed.node().getPointAtLength(offset+step*i).x
            *(90-spinAngle)/90
            +(spiral2fixed.node().getPointAtLength(offset+step*i).y*(tiltAngle)/90);
        })
        .attr("cy", function(d,i) {
          return spiral2fixed.node().getPointAtLength(offset+step*i).y
            *(90-tiltAngle)/90;
        })
        .attr("r", function(d,i) {
          var radius = dotWidth - i*decay;
          return radius > 0 ? radius : 0;
        });
    }

    numDotsSlider.on("slide", function(evt, value) { numDots = value; redraw(); });
    dotWidthSlider.on("slide", function(evt, value) { dotWidth = value; redraw(); });
    stepSlider.on("slide", function(evt, value) { step = value; redraw(); });
    offsetSlider.on("slide", function(evt, value) { offset = value; redraw(); });
    rotateSlider.on("slide", function(evt, value) { rotate = value; redraw(); });
    spinSlider.on("slide", function(evt, value) { spinAngle = value; redraw(); });
    tiltSlider.on("slide", function(evt, value) { tiltAngle = value; redraw(); });
    centralSlider.on("slide", function(evt, value) { centralRatio = value; redraw(); });
    tightnessSlider.on("slide", function(evt, value) { tightness = value; redraw(); });
    decaySlider.on("slide", function(evt, value) { decay = value; redraw(); });

    d3.select("body").append("text").text("numDots").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(numDotsSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("dotWidth").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(dotWidthSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("centerRatio").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(centralSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("step").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(stepSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("decay").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(decaySlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("offset").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(offsetSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("tightness").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(tightnessSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("rotate").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(rotateSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("spin").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(spinSlider).style("width","50%").style("float","left");
    d3.select("body").append("text").text("tilt").style("width","10%").style("float","left").style("clear","both");
    d3.select("body").append("div").call(tiltSlider).style("width","50%").style("float","left");

//When I finish with the parameterization:

/*
    d3.keys(params).forEach(function(key) {
      d3.select("body").append("text").text(key).style("width","10%").style("float","left").style("clear","both");
      d3.select("body").append("div").call(params[key].slider).style("width","50%").style("float","left");
    });
*/


  </script>
</body>

d3.slider.css

.d3-slider {
    position: relative;
    font-family: Verdana,Arial,sans-serif;
    font-size: 1.1em;
    border: 1px solid #aaaaaa;
    z-index: 2;
}

.d3-slider-horizontal {
    height: .8em;
}  

.d3-slider-range {
  background:#2980b9;
  left:0px;
  right:0px;
  height: 0.8em;
  position: absolute;
}

.d3-slider-range-vertical {
  background:#2980b9;
  left:0px;
  right:0px;
  position: absolute;
  top:0;
}

.d3-slider-vertical {
    width: .8em;
    height: 100px;
}      

.d3-slider-handle {
    position: absolute;
    width: 1.2em;
    height: 1.2em;
    border: 1px solid #d3d3d3;
    border-radius: 4px;
    background: #eee;
    background: linear-gradient(to bottom, #eee 0%, #ddd 100%);
    z-index: 3;
}

.d3-slider-handle:hover {
    border: 1px solid #999999;
}

.d3-slider-horizontal .d3-slider-handle {
    top: -.3em;
    margin-left: -.6em;
}

.d3-slider-axis {
    position: relative;
    z-index: 1;    
}

.d3-slider-axis-bottom {
    top: .8em;
}

.d3-slider-axis-right {
    left: .8em;
}

.d3-slider-axis path {
    stroke-width: 0;
    fill: none;
}

.d3-slider-axis line {
    fill: none;
    stroke: #aaa;
    shape-rendering: crispEdges;
}

.d3-slider-axis text {
    font-size: 11px;
}

.d3-slider-vertical .d3-slider-handle {
    left: -.25em;
    margin-left: 0;
    margin-bottom: -.6em;      
}

d3.slider.js

/*
    D3.js Slider
    Inspired by jQuery UI Slider
    Copyright (c) 2013, Bjorn Sandvik - http://blog.thematicmapping.org
    BSD license: http://opensource.org/licenses/BSD-3-Clause
*/
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['d3'], factory);
  } else if (typeof exports === 'object') {
    if (process.browser) {
      // Browserify. Import css too using cssify.
      require('./d3.slider.css');
    }
    // Node. Does not work with strict CommonJS, but
    // only CommonJS-like environments that support module.exports,
    // like Node.
    module.exports = factory(require('d3'));
  } else {
    // Browser globals (root is window)
    root.d3.slider = factory(root.d3);
  }
}(this, function (d3) {
return function module() {

  "use strict";

  // Public variables width default settings
  var min = 0,
      max = 100,
      step = 0.01,
      animate = true,
      orientation = "horizontal",
      axis = false,
      margin = 50,
      value,
      active = 1,
      snap = false,
      scale;

  // Private variables
  var axisScale,
      dispatch = d3.dispatch("slide", "slideend"),
      formatPercent = d3.format(".2%"),
      tickFormat = d3.format(".0"),
      handle1,
      handle2 = null,
      divRange,
      sliderLength;

  function slider(selection) {
    selection.each(function() {

      // Create scale if not defined by user
      if (!scale) {
        scale = d3.scale.linear().domain([min, max]);
      }

      // Start value
      value = value || scale.domain()[0];

      // DIV container
      var div = d3.select(this).classed("d3-slider d3-slider-" + orientation, true);
      
      var drag = d3.behavior.drag();
      drag.on('dragend', function () {
        dispatch.slideend(d3.event, value);
      })

      // Slider handle
      //if range slider, create two
      // var divRange;

      if (toType(value) == "array" && value.length == 2) {
        handle1 = div.append("a")
          .classed("d3-slider-handle", true)
          .attr("xlink:href", "#")
          .attr('id', "handle-one")
          .on("click", stopPropagation)
          .call(drag);
        handle2 = div.append("a")
          .classed("d3-slider-handle", true)
          .attr('id', "handle-two")
          .attr("xlink:href", "#")
          .on("click", stopPropagation)
          .call(drag);
      } else {
        handle1 = div.append("a")
          .classed("d3-slider-handle", true)
          .attr("xlink:href", "#")
          .attr('id', "handle-one")
          .on("click", stopPropagation)
          .call(drag);
      }
      
      // Horizontal slider
      if (orientation === "horizontal") {

        div.on("click", onClickHorizontal);
        
        if (toType(value) == "array" && value.length == 2) {
          divRange = d3.select(this).append('div').classed("d3-slider-range", true);

          handle1.style("left", formatPercent(scale(value[ 0 ])));
          divRange.style("left", formatPercent(scale(value[ 0 ])));
          drag.on("drag", onDragHorizontal);

          var width = 100 - parseFloat(formatPercent(scale(value[ 1 ])));
          handle2.style("left", formatPercent(scale(value[ 1 ])));
          divRange.style("right", width+"%");
          drag.on("drag", onDragHorizontal);

        } else {
          handle1.style("left", formatPercent(scale(value)));
          drag.on("drag", onDragHorizontal);
        }
        
        sliderLength = parseInt(div.style("width"), 10);

      } else { // Vertical

        div.on("click", onClickVertical);
        drag.on("drag", onDragVertical);
        if (toType(value) == "array" && value.length == 2) {
          divRange = d3.select(this).append('div').classed("d3-slider-range-vertical", true);

          handle1.style("bottom", formatPercent(scale(value[ 0 ])));
          divRange.style("bottom", formatPercent(scale(value[ 0 ])));
          drag.on("drag", onDragVertical);

          var top = 100 - parseFloat(formatPercent(scale(value[ 1 ])));
          handle2.style("bottom", formatPercent(scale(value[ 1 ])));
          divRange.style("top", top+"%");
          drag.on("drag", onDragVertical);

        } else {
          handle1.style("bottom", formatPercent(scale(value)));
          drag.on("drag", onDragVertical);
        }
        
        sliderLength = parseInt(div.style("height"), 10);

      }
      
      if (axis) {
        createAxis(div);
      }


      function createAxis(dom) {

        // Create axis if not defined by user
        if (typeof axis === "boolean") {

          axis = d3.svg.axis()
              .ticks(Math.round(sliderLength / 100))
              .tickFormat(tickFormat)
              .orient((orientation === "horizontal") ? "bottom" :  "right");

        }

        // Copy slider scale to move from percentages to pixels
        axisScale = scale.ticks ? scale.copy().range([0, sliderLength]) : scale.copy().rangePoints([0, sliderLength], 0.5);
          axis.scale(axisScale);

          // Create SVG axis container
        var svg = dom.append("svg")
            .classed("d3-slider-axis d3-slider-axis-" + axis.orient(), true)
            .on("click", stopPropagation);

        var g = svg.append("g");

        // Horizontal axis
        if (orientation === "horizontal") {

          svg.style("margin-left", -margin + "px");

          svg.attr({
            width: sliderLength + margin * 2,
            height: margin
          });

          if (axis.orient() === "top") {
            svg.style("top", -margin + "px");
            g.attr("transform", "translate(" + margin + "," + margin + ")");
          } else { // bottom
            g.attr("transform", "translate(" + margin + ",0)");
          }

        } else { // Vertical

          svg.style("top", -margin + "px");

          svg.attr({
            width: margin,
            height: sliderLength + margin * 2
          });

          if (axis.orient() === "left") {
            svg.style("left", -margin + "px");
            g.attr("transform", "translate(" + margin + "," + margin + ")");
          } else { // right          
            g.attr("transform", "translate(" + 0 + "," + margin + ")");
          }

        }

        g.call(axis);

      }

      function onClickHorizontal() {
        if (toType(value) != "array") {
          var pos = Math.max(0, Math.min(sliderLength, d3.event.offsetX || d3.event.layerX));
          moveHandle(scale.invert ? 
                      stepValue(scale.invert(pos / sliderLength))
                    : nearestTick(pos / sliderLength));
        }
      }

      function onClickVertical() {
        if (toType(value) != "array") {
          var pos = sliderLength - Math.max(0, Math.min(sliderLength, d3.event.offsetY || d3.event.layerY));
          moveHandle(scale.invert ? 
                      stepValue(scale.invert(pos / sliderLength))
                    : nearestTick(pos / sliderLength));
        }
      }

      function onDragHorizontal() {
        if ( d3.event.sourceEvent.target.id === "handle-one") {
          active = 1;
        } else if ( d3.event.sourceEvent.target.id == "handle-two" ) {
          active = 2;
        }
        var pos = Math.max(0, Math.min(sliderLength, d3.event.x));
        moveHandle(scale.invert ? 
                    stepValue(scale.invert(pos / sliderLength))
                  : nearestTick(pos / sliderLength));
      }

      function onDragVertical() {
        if ( d3.event.sourceEvent.target.id === "handle-one") {
          active = 1;
        } else if ( d3.event.sourceEvent.target.id == "handle-two" ) {
          active = 2;
        }
        var pos = sliderLength - Math.max(0, Math.min(sliderLength, d3.event.y))
        moveHandle(scale.invert ? 
                    stepValue(scale.invert(pos / sliderLength))
                  : nearestTick(pos / sliderLength));
      }

      function stopPropagation() {
        d3.event.stopPropagation();
      }

    });

  }

  // Move slider handle on click/drag
  function moveHandle(newValue) {
    var currentValue = toType(value) == "array"  && value.length == 2 ? value[active - 1]: value,
        oldPos = formatPercent(scale(stepValue(currentValue))),
        newPos = formatPercent(scale(stepValue(newValue))),
        position = (orientation === "horizontal") ? "left" : "bottom";
    if (oldPos !== newPos) {

      if (toType(value) == "array" && value.length == 2) {
        value[ active - 1 ] = newValue;
        if (d3.event) {
          dispatch.slide(d3.event, value );
        };
      } else {
        if (d3.event) {
          dispatch.slide(d3.event.sourceEvent || d3.event, value = newValue);
        };
      }

      if ( value[ 0 ] >= value[ 1 ] ) return;
      if ( active === 1 ) {
        if (toType(value) == "array" && value.length == 2) {
          (position === "left") ? divRange.style("left", newPos) : divRange.style("bottom", newPos);
        }

        if (animate) {
          handle1.transition()
              .styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
              .duration((typeof animate === "number") ? animate : 250);
        } else {
          handle1.style(position, newPos);
        }
      } else {
        
        var width = 100 - parseFloat(newPos);
        var top = 100 - parseFloat(newPos);

        (position === "left") ? divRange.style("right", width + "%") : divRange.style("top", top + "%");
        
        if (animate) {
          handle2.transition()
              .styleTween(position, function() { return d3.interpolate(oldPos, newPos); })
              .duration((typeof animate === "number") ? animate : 250);
        } else {
          handle2.style(position, newPos);
        }
      }
    }
  }

  // Calculate nearest step value
  function stepValue(val) {

    if (val === scale.domain()[0] || val === scale.domain()[1]) {
      return val;
    }

    var alignValue = val;
    if (snap) {
      alignValue = nearestTick(scale(val));
    } else{
      var valModStep = (val - scale.domain()[0]) % step;
      alignValue = val - valModStep;

      if (Math.abs(valModStep) * 2 >= step) {
        alignValue += (valModStep > 0) ? step : -step;
      }
    };

    return alignValue;

  }

  // Find the nearest tick
  function nearestTick(pos) {
    var ticks = scale.ticks ? scale.ticks() : scale.domain();
    var dist = ticks.map(function(d) {return pos - scale(d);});
    var i = -1,
        index = 0,
        r = scale.ticks ? scale.range()[1] : scale.rangeExtent()[1];
    do {
        i++;
        if (Math.abs(dist[i]) < r) {
          r = Math.abs(dist[i]);
          index = i;
        };
    } while (dist[i] > 0 && i < dist.length - 1);

    return ticks[index];
  };

  // Return the type of an object
  function toType(v) {
    return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
  };

  // Getter/setter functions
  slider.min = function(_) {
    if (!arguments.length) return min;
    min = _;
    return slider;
  };

  slider.max = function(_) {
    if (!arguments.length) return max;
    max = _;
    return slider;
  };

  slider.step = function(_) {
    if (!arguments.length) return step;
    step = _;
    return slider;
  };

  slider.animate = function(_) {
    if (!arguments.length) return animate;
    animate = _;
    return slider;
  };

  slider.orientation = function(_) {
    if (!arguments.length) return orientation;
    orientation = _;
    return slider;
  };

  slider.axis = function(_) {
    if (!arguments.length) return axis;
    axis = _;
    return slider;
  };

  slider.margin = function(_) {
    if (!arguments.length) return margin;
    margin = _;
    return slider;
  };

  slider.value = function(_) {
    if (!arguments.length) return value;
    if (value) {
      moveHandle(stepValue(_));
    };
    value = _;
    return slider;
  };

  slider.snap = function(_) {
    if (!arguments.length) return snap;
    snap = _;
    return slider;
  };

  slider.scale = function(_) {
    if (!arguments.length) return scale;
    scale = _;
    return slider;
  };

  d3.rebind(slider, dispatch, "on");

  return slider;

}
}));