mirror of
https://github.com/gsi-upm/soil
synced 2025-08-24 12:02:20 +00:00
Python Server
This commit is contained in:
269
templates/js/socket.js
Executable file
269
templates/js/socket.js
Executable file
@@ -0,0 +1,269 @@
|
||||
|
||||
// Open the websocket connection
|
||||
var ws = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/ws');
|
||||
|
||||
// Open conection with Socket
|
||||
ws.onopen = function() {
|
||||
console.log('Connection opened!');
|
||||
};
|
||||
|
||||
// Receive data from server
|
||||
ws.onmessage = function(message) {
|
||||
console.log('Message received!');
|
||||
|
||||
var msg = JSON.parse(message.data);
|
||||
|
||||
switch(msg['type']) {
|
||||
case 'trials':
|
||||
$('#load').removeClass('loader');
|
||||
set_trials(msg['data']);
|
||||
break;
|
||||
|
||||
case 'get_trial':
|
||||
console.log(msg['data']);
|
||||
GraphVisualization.import(convertJSON(msg['data']['graph']), msg['data']['models'], function() {
|
||||
$('#load').hide();
|
||||
reset_configuration();
|
||||
set_configuration();
|
||||
reset_timeline();
|
||||
set_timeline(msg['data']['graph']);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.log(msg['error']);
|
||||
_socket.error(msg['error']);
|
||||
$('#load').removeClass('loader');
|
||||
|
||||
default:
|
||||
console.log('Unexpected message!')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var _socket = {
|
||||
send: function(message, type) {
|
||||
var json = {}
|
||||
json['type'] = type
|
||||
json['data'] = message
|
||||
ws.send(JSON.stringify(json))
|
||||
},
|
||||
error: function(message) {
|
||||
$('#error-message').text(message);
|
||||
$('.alert.alert-danger').show();
|
||||
}
|
||||
};
|
||||
|
||||
var set_trials = function(trials) {
|
||||
for ( i in trials ) {
|
||||
$('<option>').val(i).text(trials[i]).appendTo('select#trials');
|
||||
}
|
||||
// Request first trial as default
|
||||
_socket.send(0, 'get_trial')
|
||||
};
|
||||
|
||||
var convertJSON = function(json) {
|
||||
json.links.forEach(function(link) {
|
||||
link.source = json.nodes[link.source]
|
||||
link.target = json.nodes[link.target]
|
||||
});
|
||||
// Fix spells for nodes
|
||||
json.nodes.forEach(function(node) {
|
||||
for (i in node.spells) {
|
||||
if (node.spells[i][0] > node.spells[i][1]) {
|
||||
aux = node.spells[i][0];
|
||||
node.spells[i][0] = node.spells[i][1];
|
||||
node.spells[i][1] = aux;
|
||||
}
|
||||
}
|
||||
});
|
||||
return json;
|
||||
}
|
||||
|
||||
var update_statistics_table = function() {
|
||||
|
||||
$('#percentTable tbody').empty()
|
||||
|
||||
var statisticsSorted = Object.keys(self.GraphVisualization.statistics).sort(function(a,b) {
|
||||
return self.GraphVisualization.statistics[b] - self.GraphVisualization.statistics[a];
|
||||
});
|
||||
|
||||
for ( var i in statisticsSorted ) {
|
||||
if ( i <= 5 ) {
|
||||
// Draw table
|
||||
var appendTo = '#percentTable > tbody tr:nth-child(' + Number(parseInt(i) + 1) + ')';
|
||||
var propertyName = (statisticsSorted[i].includes('class')) ?
|
||||
statisticsSorted[i].split('.').pop().split('\'')[0] : statisticsSorted[i];
|
||||
|
||||
$('<tr>').addClass('col-sm-12').appendTo('#percentTable > tbody');
|
||||
$('<td>').css('background-color', self.GraphVisualization.color(statisticsSorted[i])).addClass('col-sm-1').appendTo(appendTo);
|
||||
$('<td>').addClass('text-left col-sm-4').text(self.GraphVisualization.statistics[statisticsSorted[i]] + ' %').appendTo(appendTo);
|
||||
$('<td>').addClass('text-right col-sm-6 property-name').text(propertyName).appendTo(appendTo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var set_configuration = function() {
|
||||
// Number of nodes and links info table
|
||||
$('<tr>').appendTo('#info-graph > tbody');
|
||||
$('<th>').text('Nodes:').appendTo('#info-graph > tbody tr:nth-child(1)');
|
||||
$('<th>').text(self.GraphVisualization.nodes).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(1)');
|
||||
|
||||
$('<tr>').appendTo('#info-graph > tbody');
|
||||
$('<th>').text('Links:').appendTo('#info-graph > tbody tr:nth-child(2)');
|
||||
$('<th>').text(self.GraphVisualization.links).addClass('text-right').appendTo('#info-graph > tbody tr:nth-child(2)');
|
||||
|
||||
// Options of 'Select'
|
||||
for ( var i in self.GraphVisualization.model['dynamic'] ) {
|
||||
$('<option>').val(self.GraphVisualization.model['dynamic'][i].title)
|
||||
.text(self.GraphVisualization.model['dynamic'][i].title).appendTo('#properties-dynamic');
|
||||
}
|
||||
for ( var i in self.GraphVisualization.model['static'] ) {
|
||||
$('<option>').val(self.GraphVisualization.model['static'][i].title)
|
||||
.text(self.GraphVisualization.model['static'][i].title).appendTo('#properties-static');
|
||||
}
|
||||
|
||||
// Hide optgroups if they are empty
|
||||
if ( $('#properties-dynamic').children().length === 0 ) $('#properties-dynamic').hide();
|
||||
if ( $('#properties-static').children().length === 0 ) $('#properties-static').hide();
|
||||
|
||||
update_statistics_table();
|
||||
|
||||
// Enable 'Link Distance' slider
|
||||
$('#link-distance-slider').slider('enable').on('change', function(value) {
|
||||
self.GraphVisualization.set_link_distance(value.value.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
var reset_configuration = function() {
|
||||
// Information table about the graph
|
||||
$('#info-graph > tbody').empty();
|
||||
|
||||
// 'Select' for properties
|
||||
$('#properties-dynamic').empty().show();
|
||||
$('#properties-static').empty().show();
|
||||
|
||||
// 'Link Distance' slider
|
||||
$('#link-distance-slider').slider('disable').slider('setValue', 30);
|
||||
|
||||
}
|
||||
|
||||
var set_timeline = function(graph) {
|
||||
// 'Timeline' slider
|
||||
var [min, max] = get_min_and_max(graph);
|
||||
|
||||
var stepUnix = (max - min) / 200;
|
||||
var minUnix = (min !== Math.min()) ? min : 0;
|
||||
var maxUnix = (max !== Math.max()) ? max : minUnix + 20;
|
||||
|
||||
slider = d3.slider();
|
||||
d3.select('#slider3').attr('width', width).call(
|
||||
slider.axis(true).min(minUnix).max(maxUnix).step(stepUnix).value(maxUnix)
|
||||
.on('slide', function(evt, value) {
|
||||
self.GraphVisualization.update_graph($('.config-item #properties').val(), value, function() {
|
||||
update_statistics_table();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Draw graph for the first time
|
||||
self.GraphVisualization.update_graph($('.config-item #properties').val(), maxUnix, function() {
|
||||
update_statistics_table();
|
||||
});
|
||||
|
||||
// 'Speed' slider
|
||||
$('#speed-slider').slider('enable').on('change', function(value) {
|
||||
speed = value.value.newValue;
|
||||
});
|
||||
|
||||
// Button 'Play'
|
||||
$('button#button_play').on('click', function() {
|
||||
|
||||
$('button#button_play').addClass('pressed').prop("disabled", true);
|
||||
$('#speed-slider').slider('disable');
|
||||
slider.step( 1 / speed );
|
||||
|
||||
if (slider.value() >= maxUnix) {
|
||||
slider.value(minUnix);
|
||||
self.GraphVisualization.update_graph($('.config-item #properties').val(), slider.value(), function() {
|
||||
update_statistics_table();
|
||||
});
|
||||
setTimeout(player, 1000);
|
||||
} else {
|
||||
player();
|
||||
}
|
||||
|
||||
var i = slider.value();
|
||||
function player() {
|
||||
clearInterval(play);
|
||||
play = setInterval(function() {
|
||||
self.GraphVisualization.update_graph($('.config-item #properties').val(), slider.value(), function() {
|
||||
update_statistics_table();
|
||||
});
|
||||
|
||||
if (slider.value() + slider.step() >= maxUnix) {
|
||||
slider.value(maxUnix);
|
||||
slider.step(stepUnix);
|
||||
clearInterval(play);
|
||||
$('button#button_play').removeClass('pressed').prop("disabled", false);
|
||||
$('#speed-slider').slider('enable');
|
||||
} else {
|
||||
slider.value(i);
|
||||
i += slider.step();
|
||||
}
|
||||
|
||||
}, 5);
|
||||
}
|
||||
});
|
||||
|
||||
// Button 'Pause'
|
||||
$('button#button_pause').on('click', function() {
|
||||
clearInterval(play);
|
||||
slider.step(stepUnix);
|
||||
$('button#button_play').removeClass('pressed').prop("disabled", false);
|
||||
$('#speed-slider').slider('enable');
|
||||
});
|
||||
|
||||
// Button 'Zoom to Fit'
|
||||
$('button#button_zoomFit').click(function() { self.GraphVisualization.fit(); });
|
||||
}
|
||||
|
||||
var reset_timeline = function() {
|
||||
// 'Timeline' slider
|
||||
$('#slider3').html('');
|
||||
|
||||
// 'Speed' slider
|
||||
$('#speed-slider').slider('disable').slider('setValue', 1000);
|
||||
|
||||
// Buttons
|
||||
clearInterval(play);
|
||||
$('button#button_play').off().removeClass('pressed').prop("disabled", false);
|
||||
$('button#button_pause').off();
|
||||
$('button#button_zoomFit').off();
|
||||
}
|
||||
|
||||
var get_min_and_max = function(graph) {
|
||||
var max = Math.max();
|
||||
var min = Math.min()
|
||||
graph.links.forEach(function(link) {
|
||||
if (link.end > max) max = link.end
|
||||
if (link.start > max) max = link.start
|
||||
if (link.end < min) min = link.end
|
||||
if (link.start < min) min = link.start
|
||||
});
|
||||
graph.nodes.forEach(function(node) {
|
||||
for (property in node) {
|
||||
if ( Array.isArray(node[property]) ) {
|
||||
|
||||
for (i in node[property]) {
|
||||
for (j in node[property][i]) {
|
||||
if (node[property][i][j] > max) max = node[property][i][j];
|
||||
if (node[property][i][j] < min) min = node[property][i][j];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
return [min, max];
|
||||
}
|
429
templates/js/timeline.js
Normal file
429
templates/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;
|
||||
|
||||
}
|
||||
}));
|
426
templates/js/visualization.js
Normal file
426
templates/js/visualization.js
Normal file
@@ -0,0 +1,426 @@
|
||||
|
||||
;(function(undefined) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Graph Visualization
|
||||
* ===================
|
||||
*
|
||||
* Author: Tasio Méndez (tasiomendez)
|
||||
* URL: https://github.com/tasiomendez/
|
||||
* Version: 0.1
|
||||
*/
|
||||
|
||||
// Private constants
|
||||
var width = window.innerWidth * 0.75,
|
||||
height = window.innerHeight * 4 / 5,
|
||||
focus_opacity = 0.1,
|
||||
radius = 8;
|
||||
|
||||
// Private variables
|
||||
var graph, // JSON data for the graph
|
||||
model, // Definition of the attributes of the nodes
|
||||
linkedByIndex, // Nodes linked by index
|
||||
name, // Name of the graph (id for svg item)
|
||||
svg, // Svg item
|
||||
force, // Set up the force layout
|
||||
color, // Color for nodes
|
||||
zoom, // Zoom
|
||||
|
||||
groot, // Append sections to svg to have nodes and edges separately
|
||||
glinks,
|
||||
gnodes,
|
||||
data_node, // Actual node data for the graph
|
||||
data_link, // Actual link data for the graph
|
||||
|
||||
link, // Line svgs
|
||||
node; // Circles for the nodes
|
||||
|
||||
Number.prototype.between = function(min, max) {
|
||||
var min = (min) ? min : Math.max(),
|
||||
max = (max) ? max : Math.min();
|
||||
|
||||
return this > min && this <= max;
|
||||
};
|
||||
|
||||
var lastFocusNode;
|
||||
var _helpers = {
|
||||
set_node: function(node, property) {
|
||||
// Add nodes if data has more nodes than before
|
||||
node.enter().append('circle')
|
||||
.attr('class', 'node')
|
||||
.attr('r', radius)
|
||||
.style('fill', function (d) {
|
||||
if ( Array.isArray(d[property]) ) {
|
||||
return color(d[property][0][0]);
|
||||
} else {
|
||||
return color(d[property]);
|
||||
}
|
||||
})
|
||||
// Cancel zoom movement so you can move the node
|
||||
.on('mousedown', function(d) {
|
||||
d3.event.stopPropagation();
|
||||
})
|
||||
// Double-click to focus neighbours
|
||||
.on('dblclick', function(d) {
|
||||
d3.event.stopPropagation();
|
||||
if (d === lastFocusNode) {
|
||||
lastFocusNode = undefined;
|
||||
node.style('opacity', 1);
|
||||
link.style('opacity', 1);
|
||||
} else {
|
||||
lastFocusNode = d;
|
||||
_helpers.set_focus(d);
|
||||
}
|
||||
}).call(force.drag);
|
||||
|
||||
// Remove nodes if data has less nodes than before
|
||||
node.exit().remove();
|
||||
|
||||
// Update existing nodes
|
||||
node.attr('class', 'node')
|
||||
.attr('r', radius)
|
||||
.style('fill', function (d) {
|
||||
if ( Array.isArray(d[property]) ) {
|
||||
return color(d[property][0][0]);
|
||||
} else {
|
||||
return color(d[property]);
|
||||
}
|
||||
});
|
||||
},
|
||||
set_link: function(link) {
|
||||
// Remove links if data has more links than before
|
||||
link.enter().append('line')
|
||||
.attr('class', 'link')
|
||||
.style('stroke-width', function (d) {
|
||||
return Math.sqrt(d.value);
|
||||
});
|
||||
|
||||
// Remove links if data has less links than before
|
||||
link.exit().remove();
|
||||
},
|
||||
isConnected: function(source, neighbour) {
|
||||
return linkedByIndex[source.id + ',' + neighbour.id] ||
|
||||
linkedByIndex[neighbour.id + ',' + source.id];
|
||||
},
|
||||
set_focus: function(d) {
|
||||
node.style('opacity', function(o) {
|
||||
return _helpers.isConnected(d,o) || d.index == o.index ? 1 : focus_opacity;
|
||||
});
|
||||
link.style('opacity', function(o) {
|
||||
return o.source.index == d.index || o.target.index == d.index ? 1 : focus_opacity;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Graph Visualization Core Functions
|
||||
* ----------------------------------
|
||||
*
|
||||
* The graph visualization functions themselves.
|
||||
*/
|
||||
|
||||
function Graph() {
|
||||
// Color
|
||||
color = d3.scale.category20();
|
||||
|
||||
// Set up the force layout
|
||||
force = d3.layout.force()
|
||||
.charge(-500)
|
||||
.linkDistance(30)
|
||||
.size([width, height]);
|
||||
|
||||
// Append sections to svg to have nodes and edges separately
|
||||
groot = svg.append('g') .attr('id', 'root');
|
||||
glinks = groot.append('g') .attr('id', 'links');
|
||||
gnodes = groot.append('g') .attr('id', 'nodes');
|
||||
|
||||
// Zoom
|
||||
zoom = d3.behavior
|
||||
.zoom()
|
||||
.scaleExtent([1/10, 10])
|
||||
.on('zoom', function () {
|
||||
//console.trace("zoom", d3.event.translate, d3.event.scale);
|
||||
groot.attr('transform',
|
||||
'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');
|
||||
});
|
||||
|
||||
// Activate zoom for the svg item
|
||||
svg.style('background-color', 'rgb(255,255,255)')
|
||||
.call(zoom);
|
||||
|
||||
// Update linkedByIndex
|
||||
linkedByIndex = {};
|
||||
graph.links.forEach(function(d) {
|
||||
linkedByIndex[d.source.id + ',' + d.target.id] = true;
|
||||
});
|
||||
|
||||
// Creates the graph data structure out of the json data
|
||||
force.nodes(graph.nodes)
|
||||
.links(graph.links)
|
||||
.start();
|
||||
|
||||
// Now we are giving the SVGs coordinates - the force layout is generating the coordinates
|
||||
// which this code is using to update the attributes of the SVG elements
|
||||
force.on('tick', function () {
|
||||
|
||||
link.attr('x1', function (d) {
|
||||
return d.source.x;
|
||||
}).attr('y1', function (d) {
|
||||
return d.source.y;
|
||||
}).attr('x2', function (d) {
|
||||
return d.target.x;
|
||||
}).attr('y2', function (d) {
|
||||
return d.target.y;
|
||||
});
|
||||
|
||||
node.attr('transform', function translate(d) {
|
||||
return 'translate(' + d.x + ',' + d.y + ')';
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function update_data(property, time) {
|
||||
|
||||
// Reset data
|
||||
var delete_links = true;
|
||||
data_node = [];
|
||||
data_link = graph.links.slice();
|
||||
|
||||
// Nodes
|
||||
graph.nodes.forEach(function(node) {
|
||||
if (Array.isArray(node.spells)) {
|
||||
node.spells.forEach( function(d) {
|
||||
if ( time.between(d[0], d[1]) ) {
|
||||
data_node.push(node);
|
||||
} else {
|
||||
graph.links.forEach(function(link) {
|
||||
if (link.source === node || link.target === node)
|
||||
data_link.splice(data_link.indexOf(link), 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
data_node.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// Links
|
||||
graph.links.forEach(function(link) {
|
||||
if ( !(time.between(link.start, link.end)) && data_link.includes(link) )
|
||||
data_link.splice(data_link.indexOf(link), 1);
|
||||
});
|
||||
|
||||
// Reset force
|
||||
force.stop()
|
||||
.nodes(data_node)
|
||||
.links(data_link)
|
||||
.start();
|
||||
|
||||
// Create all the line svgs but without locations
|
||||
link = glinks.selectAll('.link').data(data_link);
|
||||
_helpers.set_link(link);
|
||||
|
||||
// Do the same with the circles for the nodes - no
|
||||
node = gnodes.selectAll('.node').data(data_node);
|
||||
_helpers.set_node(node, property);
|
||||
|
||||
// Node Attributes
|
||||
var statistics = {}
|
||||
self.GraphVisualization.statistics = {};
|
||||
data_node.forEach(function(n) {
|
||||
// Count node properties
|
||||
if ( Array.isArray(n[property]) ) {
|
||||
statistics[n[property][0][0]] = (!statistics[n[property][0][0]]) ? 1 : statistics[n[property][0][0]] + 1;
|
||||
} else {
|
||||
statistics[n[property]] = (!statistics[n[property]]) ? 1 : statistics[n[property]] + 1;
|
||||
}
|
||||
});
|
||||
for ( i in statistics ) {
|
||||
statistics[i] = (statistics[i] / data_node.length * 100).toFixed(2);
|
||||
}
|
||||
self.GraphVisualization.statistics = statistics
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Public API
|
||||
* -----------
|
||||
*
|
||||
* User-accessible functions.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create the space where the graph will we drawn.
|
||||
* A function that identifies the svg item.
|
||||
*
|
||||
* @param {object} id The id of the svg item.
|
||||
* @return {object} This class.
|
||||
*/
|
||||
function create(id, callback) {
|
||||
name = id;
|
||||
svg = d3.select('svg#' + name)
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('background-color', 'rgba(128,128,128,0.1)');
|
||||
|
||||
if (callback) { callback(this.GraphVisualization); }
|
||||
else { return this.GraphVisualization }
|
||||
}
|
||||
|
||||
/**
|
||||
* Import JSON and attributes.
|
||||
* A function that imports the graph and the attributes of all the nodes.
|
||||
*
|
||||
* @param {object} json The json structure of the graph.
|
||||
* @param {object} attributes Definition of the attributes of the nodes
|
||||
* (statics and dynamics).
|
||||
* @param {object} callback A function called at the end.
|
||||
*/
|
||||
function importJSON(json, attributes, callback) {
|
||||
reset()
|
||||
graph = json;
|
||||
model = attributes
|
||||
|
||||
// Create the graph itself
|
||||
Graph();
|
||||
|
||||
self.GraphVisualization.nodes = graph.nodes.length;
|
||||
self.GraphVisualization.links = graph.links.length;
|
||||
self.GraphVisualization.model = model
|
||||
|
||||
// Draw graph with default property and time for the first time
|
||||
update_data(model.dynamic[0].title, 0)
|
||||
|
||||
if (callback) { callback(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Set link distance.
|
||||
* A function that set the link distance. If it is not called, it uses 30 as default
|
||||
*
|
||||
* @param {object} distance Distance.
|
||||
* @param {object} callback A function called at the end.
|
||||
*/
|
||||
function set_link_distance(distance, callback) {
|
||||
if (graph) {
|
||||
force.stop().linkDistance(distance).start();
|
||||
|
||||
// Update radius of the nodes to see them better
|
||||
var r = d3.scale.linear().domain([30, 1000]).range([8, 24]);
|
||||
radius = r(distance);
|
||||
node.attr('r', radius);
|
||||
|
||||
if (callback) { callback(radius); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set property and instant of time.
|
||||
* A function that draws the graph depends on the property and instant of time selected.
|
||||
*
|
||||
* @param {object} property Property to show.
|
||||
* @param {object} time Instant of time.
|
||||
* @param {object} callback A function called at the end.
|
||||
*/
|
||||
function update_graph(property, time, callback) {
|
||||
if (graph) {
|
||||
update_data(property, time);
|
||||
|
||||
if (callback) { callback(); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the graph to the whole area.
|
||||
* A function that adjust the graph to the svg item.
|
||||
*
|
||||
* @param {object} padding Space from the graph to the border.
|
||||
* 85% by default.
|
||||
* @param {object} transition Duration of the zoom action.
|
||||
* 750 milliseconds by default.
|
||||
* @param {object} callback A function called at the end.
|
||||
*/
|
||||
function zoom_to_fit(padding, transition, callback) {
|
||||
|
||||
var bounds = groot.node().getBBox();
|
||||
var parent = groot.node().parentElement;
|
||||
var fullWidth = parent.clientWidth,
|
||||
fullHeight = parent.clientHeight;
|
||||
var widthBounds = bounds.width,
|
||||
heightBounds = bounds.height;
|
||||
var midX = bounds.x + widthBounds / 2,
|
||||
midY = bounds.y + heightBounds / 2;
|
||||
if (widthBounds == 0 || heightBounds == 0) return; // nothing to fit
|
||||
var scale = (padding || 0.85) / Math.max(widthBounds / fullWidth, heightBounds / fullHeight);
|
||||
var translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
|
||||
|
||||
//console.trace("zoomFit", translate, scale);
|
||||
groot
|
||||
.transition()
|
||||
.duration(transition || 750) // milliseconds
|
||||
.call(zoom.translate(translate).scale(scale).event);
|
||||
|
||||
if (callback) { callback(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the whole graph.
|
||||
* A function that reset the svg item.
|
||||
*
|
||||
*/
|
||||
function reset() {
|
||||
d3.select('svg#' + name)
|
||||
.html('')
|
||||
.attr('width', width)
|
||||
.attr('height', height)
|
||||
.style('background-color', 'rgba(128,128,128,0.1)');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for a value.
|
||||
* A function that get the color of a node or a group of nodes.
|
||||
*
|
||||
* @param {object} value Value.
|
||||
* @return {object} color The color in hexadecimal.
|
||||
*/
|
||||
function color(value) {
|
||||
if (graph) {
|
||||
return color(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exporting
|
||||
* ---------
|
||||
*/
|
||||
this.GraphVisualization = {
|
||||
|
||||
// Functions
|
||||
create: create,
|
||||
import: importJSON,
|
||||
update_graph: update_graph,
|
||||
set_link_distance: set_link_distance,
|
||||
fit: zoom_to_fit,
|
||||
reset: reset,
|
||||
|
||||
// Attributes
|
||||
model: {},
|
||||
nodes: undefined,
|
||||
links: undefined,
|
||||
|
||||
// Getters
|
||||
color: color,
|
||||
|
||||
// Statistics
|
||||
statistics: {},
|
||||
|
||||
// Version
|
||||
version: '0.1'
|
||||
};
|
||||
|
||||
}).call(this);
|
Reference in New Issue
Block a user