mirror of
https://github.com/gsi-upm/soil
synced 2024-11-14 15:32:29 +00:00
429 lines
12 KiB
JavaScript
429 lines
12 KiB
JavaScript
/*
|
|
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%"),
|
|
tickPadding = 5,
|
|
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)
|
|
.tickPadding(tickPadding)
|
|
.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 - 16 + "px");
|
|
|
|
svg.attr({
|
|
width: sliderLength + margin * 2,
|
|
height: margin + 30
|
|
});
|
|
|
|
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;
|
|
|
|
}
|
|
})); |