mirror of
https://github.com/gsi-upm/soil
synced 2025-09-15 20:52:22 +00:00
merge visualization branch
The web server is included as a submodule. The dependencies for the web (tornado) are not installed by default, but they can be installed as an extra: ``` pip install soil[web] ``` Once installed, the soil web can be used like this: ``` soil-web OR python -m soil.web ``` There are other minor changes: * History re-connects to the sqlite database if it is used from a different thread. * Environment accepts additional parameters (so it can run simulations with `visualization_params` or any other in the future). * The simulator class is no longer necessary * Logging is done in the same thread, and the simulation is run in a separate one. This had to be done because it was creating some problems with tornado not being able to find the current thread during logs, which caused hundreds of repeated lines in the web "console". * The player is slightly modified in this version. I noticed that when the visualization was playing, if you clicked somewhere it would change for a second, and then go back to the previous place. The code for the playback seemed too complex, especially speed control, so I rewrote some parts. I might've introduced new bugs.
This commit is contained in:
429
soil/web/static/js/timeline.js
Normal file
429
soil/web/static/js/timeline.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
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;
|
||||
|
||||
}
|
||||
}));
|
Reference in New Issue
Block a user