mirror of
https://github.com/gsi-upm/soil
synced 2025-09-14 04:02:21 +00:00
Squashed 'soil/web/' content from commit 4dcd0fc
git-subtree-dir: soil/web
git-subtree-split: 4dcd0fcb3d
This commit is contained in:
460
templates/js/socket.js
Executable file
460
templates/js/socket.js
Executable file
@@ -0,0 +1,460 @@
|
||||
|
||||
// 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':
|
||||
reset_trials();
|
||||
set_trials(msg['data']);
|
||||
// $('#load').removeClass('loader');
|
||||
break;
|
||||
|
||||
case 'get_trial':
|
||||
console.log(msg['data']);
|
||||
|
||||
self.GraphVisualization.import(convertJSON(msg['data']), function() {
|
||||
reset_configuration();
|
||||
set_configuration();
|
||||
// $('#home_menu').click(function() {
|
||||
// setTimeout(function() {
|
||||
// reset_timeline();
|
||||
// set_timeline(msg['data']);
|
||||
// }, 1000);
|
||||
// });
|
||||
reset_timeline();
|
||||
set_timeline(msg['data']);
|
||||
$('#load').hide();
|
||||
});
|
||||
$('#charts .chart').removeClass('no-data');
|
||||
set_chart_nodes(msg['data'], chart_nodes)
|
||||
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
|
||||
$('.config-item #properties').change(function() {
|
||||
chart_attrs.destroy();
|
||||
chart_attrs = create_chart(width_chart, height_chart, 'Time', 'Attributes', '#chart_attrs');
|
||||
set_chart_attrs(msg['data'], chart_attrs, $('.config-item #properties').val())
|
||||
});
|
||||
break;
|
||||
|
||||
case 'settings':
|
||||
$('#wrapper-settings').empty().removeClass('none');
|
||||
initGUI(msg['data']);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error(msg['error']);
|
||||
_socket.error(msg['error']);
|
||||
$('#load').removeClass('loader');
|
||||
break;
|
||||
|
||||
case 'log':
|
||||
$('.console').append('$ ' + msg['logger'] + ': ' + msg['logging'] + '<br/>');
|
||||
$('.console').animate({ scrollTop: $('.console')[0].scrollHeight }, 'fast');
|
||||
break;
|
||||
|
||||
case 'visualization_params':
|
||||
console.log(msg['data']);
|
||||
self.GraphVisualization.set_params(msg['data']['shape_property'], msg['data']['shapes'], msg['data']['colors']);
|
||||
|
||||
if ( msg['data']['background_image'] ) {
|
||||
// $('svg#graph').css('background-image', 'linear-gradient(to bottom, rgba(0,0,0,0.4) 0%,rgba(0,0,0,0.4) 100%), url(img/background/' + msg['data']['background_image'])
|
||||
// .css('background-size', '130%').css('background-position', '5% 30%').css('background-repeat', 'no-repeat');
|
||||
$('<style>').text('svg line.link { stroke: white !important; stroke-width: 1.5px !important; }').appendTo($('html > head'));
|
||||
$('<style>').text('svg circle.node { stroke-width: 2.5px !important; }').appendTo($('html > head'));
|
||||
self.GraphVisualization.set_background('img/background/' + msg['data']['background_image'], msg['data']['background_opacity'], msg['data']['background_filter_color']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'download_gexf':
|
||||
var xml_declaration = '<?xml version="1.0" encoding="utf-8"?>';
|
||||
download(msg['filename'] + '.gexf', 'xml', xml_declaration + msg['data']);
|
||||
break;
|
||||
|
||||
case 'download_json':
|
||||
download(msg['filename'] + '.json', 'json', JSON.stringify(msg['data'], null, 4));
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('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();
|
||||
},
|
||||
current_trial: undefined
|
||||
};
|
||||
|
||||
var set_trials = function(trials) {
|
||||
for ( i in trials ) {
|
||||
var list_item = $('<li>').appendTo('.dropdown#trials .dropdown-menu');
|
||||
$('<a>').val(i).text(trials[i]).appendTo(list_item);
|
||||
}
|
||||
// Select 'trials'
|
||||
$('.dropdown#trials li a').click(function() {
|
||||
var a = $('.dropdown-toggle .caret');
|
||||
$('.dropdown-toggle').text($(this).text() + ' ').append(a);
|
||||
_socket.send($(this).val(), 'get_trial');
|
||||
_socket.current_trial = $(this).val();
|
||||
});
|
||||
// Request first trial as default
|
||||
_socket.send(0, 'get_trial')
|
||||
_socket.current_trial = 0
|
||||
};
|
||||
|
||||
var reset_trials = function() {
|
||||
// 'Trials' selector
|
||||
$('.dropdown-menu').empty();
|
||||
var a = $('.dropdown-toggle .caret');
|
||||
$('.dropdown-toggle').text('Trials ').append(a);
|
||||
}
|
||||
|
||||
var convertJSON = function(json) {
|
||||
// For NetworkX Geometric Graphs get positions
|
||||
json.nodes.forEach(function(node) {
|
||||
var scx = d3.scale.linear().domain([0, 1]).range([0, width]);
|
||||
var scy = d3.scale.linear().domain([0, 1]).range([width, 0]);
|
||||
if ( node.pos ) {
|
||||
node.scx = scx(node.pos[0]);
|
||||
node.scy = scy(node.pos[1]);
|
||||
}
|
||||
delete node.pos;
|
||||
});
|
||||
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($('.config-item #properties').val(), 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);
|
||||
});
|
||||
|
||||
// Enable 'Run configuration' button
|
||||
$('#run_simulation').attr('data-toggle', 'modal').attr('data-target', '#simulation_modal');
|
||||
|
||||
// Enable 'Download' buttons
|
||||
$('#download_modal .btn-success').prop('disabled', false);
|
||||
$('#download_gexf').on('click', function() {
|
||||
_socket.send(_socket.current_trial, 'download_gexf')
|
||||
});
|
||||
$('#download_json').on('click', function() {
|
||||
_socket.send(_socket.current_trial, 'download_json')
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 'Download' buttons
|
||||
$('#download_gexf').off();
|
||||
$('#download_json').off();
|
||||
}
|
||||
|
||||
var set_timeline = function(graph) {
|
||||
// 'Timeline' slider
|
||||
var [min, max] = get_limits(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();
|
||||
setTimeout(function() {
|
||||
self.GraphVisualization.fit();
|
||||
if ( $('svg #root > image').length !== 0 ) {
|
||||
$('svg #root > image').attr('height', d3.select('#root').node().getBBox().height * 1.2);
|
||||
var dx = d3.select('#graph-wrapper').node().getBBox().width - d3.select('svg #root > image').node().getBBox().width;
|
||||
var dy = d3.select('#graph-wrapper').node().getBBox().height - d3.select('svg #root > image').node().getBBox().height;
|
||||
$('svg #root > image').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')');
|
||||
$('svg #root > rect').attr('transform', 'translate(' + (dx / 2) + ',' + (dy / 2) + ')')
|
||||
.attr('width', d3.select('svg #root > image').node().getBBox().width)
|
||||
.attr('height', d3.select('svg #root > image').node().getBBox().height);
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// '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_limits = 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];
|
||||
}
|
||||
|
||||
var set_chart_nodes = function(graph, chart) {
|
||||
var [min, max] = get_limits(graph);
|
||||
var data = ['nodes']
|
||||
for (var i = min; i <= max; i++) {
|
||||
data.push(this.GraphVisualization.get_nodes(i));
|
||||
}
|
||||
chart.load({
|
||||
unload: true,
|
||||
columns: [data]
|
||||
});
|
||||
}
|
||||
|
||||
var set_chart_attrs = function(graph, chart, property) {
|
||||
var [min, max] = get_limits(graph);
|
||||
var data_tmp = {}
|
||||
for (var i = min; i <= max; i++) {
|
||||
this.GraphVisualization.get_attributes(property, i, function(object) {
|
||||
for (var value in object) {
|
||||
if (!data_tmp[value]) {
|
||||
var time = 0
|
||||
for (var done in data_tmp)
|
||||
time = (data_tmp[done].length > time) ? data_tmp[done].length - 1 : time
|
||||
data_tmp[value] = Array(time).fill(0);
|
||||
}
|
||||
data_tmp[value].push(object[value]);
|
||||
}
|
||||
});
|
||||
}
|
||||
var data = $.map(data_tmp, function(value, index) {
|
||||
value.splice(0,0,index);
|
||||
return [value];
|
||||
});
|
||||
chart.load({
|
||||
unload: true,
|
||||
columns: data
|
||||
});
|
||||
chart.axis.labels({y: property});
|
||||
}
|
||||
|
||||
var create_chart = function(width, height, label_x, label_y, bind_to) {
|
||||
return c3.generate({
|
||||
size: {
|
||||
width: width,
|
||||
height: height
|
||||
},
|
||||
data: {
|
||||
columns: [],
|
||||
type: 'area-spline'
|
||||
},
|
||||
axis: {
|
||||
x: { label: label_x },
|
||||
y: { label: label_y }
|
||||
},
|
||||
point: { show: false },
|
||||
bindto: bind_to
|
||||
});
|
||||
}
|
||||
|
||||
var run_simulation = function() {
|
||||
var environment_variables = {}
|
||||
$('#wrapper-settings input').each(function() {
|
||||
switch(this.type) {
|
||||
case 'text':
|
||||
environment_variables[this.id] = Number(this.value);
|
||||
break;
|
||||
case 'checkbox':
|
||||
environment_variables[this.id] = ($(this).is(':checked')) ? true : false;
|
||||
break;
|
||||
case 'number':
|
||||
environment_variables[this.id] = Number(this.value);
|
||||
break;
|
||||
default:
|
||||
console.warn(this.id + ' not defined when running simulation!');
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
return environment_variables;
|
||||
}
|
||||
|
||||
var download = function(filename, filetype, content) {
|
||||
var file = document.createElement('a');
|
||||
file.setAttribute('href', 'data:text/' + filetype + ';charset=utf-8,' + encodeURIComponent(content));
|
||||
file.setAttribute('download', filename);
|
||||
file.click();
|
||||
delete file;
|
||||
}
|
99
templates/js/template.js
Normal file
99
templates/js/template.js
Normal file
@@ -0,0 +1,99 @@
|
||||
// Add model parameters that can be edited prior to a model run
|
||||
var initGUI = function(model_params) {
|
||||
|
||||
var addBooleanInput = function(name, value) {
|
||||
var checked = (value) ? 'checked' : 'value';
|
||||
|
||||
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
|
||||
var input_group = $('<div>').attr('class', 'input-group').appendTo(wrapper);
|
||||
var label = $('<label>').attr('for', name).attr('class', 'checkbox').appendTo(input_group);
|
||||
var input = $('<input>') .attr('class', 'form-check-input').attr('id', name).attr('type', 'checkbox').attr(checked, checked).appendTo(label);
|
||||
|
||||
input.after(name);
|
||||
$('#wrapper-settings').append(wrapper);
|
||||
};
|
||||
|
||||
var addSliderInput = function(name, value) {
|
||||
|
||||
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
|
||||
var label = $('<div>').width('100%').text(name).css('text-align', 'center').css('font-weight', 'bolder').appendTo(wrapper);
|
||||
var input = $('<input>').attr('id', name).attr('type', 'text').attr('data-slider-min', '0.001').attr('data-slider-max', '1').attr('data-slider-step', '0.001').attr('data-slider-value', value).attr('data-slider-tooltip', 'hide').css('padding', '0 10px').appendTo(wrapper);
|
||||
|
||||
var span = $('<div>').attr('id', name + '_value').text('Current value: ').width('100%').css('padding-top', '10px').appendTo(wrapper);
|
||||
var current_value = $('<span>').attr('id', name + '_number').text(value).appendTo(span);
|
||||
|
||||
var button_group = $('<div>').attr('class', 'btn-group').attr('role', 'group').css('position', 'absolute').css('right', '15px').appendTo(span);
|
||||
var button_down = $('<button>').attr('type', 'button').attr('class', 'btn btn-default btn-default-down').appendTo(button_group);
|
||||
var button_up = $('<button>').attr('type', 'button').attr('class', 'btn btn-default btn-default-down').appendTo(button_group);
|
||||
|
||||
$('<span>').attr('class', 'glyphicon glyphicon-chevron-down').attr('aria-hidden', 'true').appendTo(button_down);
|
||||
$('<span>').attr('class', 'glyphicon glyphicon-chevron-up').attr('aria-hidden', 'true').appendTo(button_up);
|
||||
|
||||
$('#wrapper-settings').append(wrapper);
|
||||
input.slider().on('change', function(slideEvt) {
|
||||
current_value.text(slideEvt.value.newValue);
|
||||
});
|
||||
var timeout, interval;
|
||||
button_down.on('mousedown', function() {
|
||||
input.slider('setValue', input.slider('getValue') - 0.001);
|
||||
current_value.text(input.slider('getValue'));
|
||||
timeout = setTimeout(function() {
|
||||
interval = setInterval(function() {
|
||||
input.slider('setValue', input.slider('getValue') - 0.001);
|
||||
current_value.text(input.slider('getValue'));
|
||||
}, 30);
|
||||
}, 500);
|
||||
});
|
||||
button_down.on('mouseup', function() {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
});
|
||||
button_up.on('mousedown', function() {
|
||||
input.slider('setValue', input.slider('getValue') + 0.001);
|
||||
current_value.text(input.slider('getValue'));
|
||||
timeout = setTimeout(function() {
|
||||
interval = setInterval(function() {
|
||||
input.slider('setValue', input.slider('getValue') + 0.001);
|
||||
current_value.text(input.slider('getValue'));
|
||||
}, 30);
|
||||
}, 500);
|
||||
});
|
||||
button_up.on('mouseup', function() {
|
||||
clearTimeout(timeout);
|
||||
clearInterval(interval);
|
||||
});
|
||||
};
|
||||
|
||||
var addNumberInput = function(name, value) {
|
||||
var wrapper = $('<div>').attr('class', 'col-sm-6').height('110px');
|
||||
var label = $('<div>').width('100%').text(name).css('text-align', 'center').css('font-weight', 'bolder').appendTo(wrapper);
|
||||
var input = $('<input>').attr('id', name).attr('type', 'number').attr('class', 'form-control').attr('value', value).attr('min', 0).css('margin-top', '18px').appendTo(wrapper);
|
||||
$('#wrapper-settings').append(wrapper);
|
||||
}
|
||||
|
||||
var addTextBox = function(param, obj) {
|
||||
var well = $('<div class="well">' + obj.value + '</div>')[0];
|
||||
sidebar.append(well);
|
||||
};
|
||||
|
||||
for (var option in model_params) {
|
||||
|
||||
var type = typeof(model_params[option]);
|
||||
var param_str = String(option);
|
||||
|
||||
switch (model_params[option]['type']) {
|
||||
case 'boolean':
|
||||
addBooleanInput(model_params[option]['label'], model_params[option]['value']);
|
||||
break;
|
||||
case 'number':
|
||||
addSliderInput(model_params[option]['label'], model_params[option]['value']);
|
||||
break;
|
||||
case 'great_number':
|
||||
addNumberInput(model_params[option]['label'], model_params[option]['value']);
|
||||
break;
|
||||
default:
|
||||
console.warn(model_params[option]['label'] + ' not defined!');
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
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;
|
||||
|
||||
}
|
||||
}));
|
686
templates/js/visualization.js
Normal file
686
templates/js/visualization.js
Normal file
@@ -0,0 +1,686 @@
|
||||
|
||||
;(function(undefined) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Graph Visualization
|
||||
* ===================
|
||||
*
|
||||
* Author: Tasio Méndez (tasiomendez)
|
||||
* URL: https://github.com/tasiomendez/
|
||||
* Version: 0.1
|
||||
*/
|
||||
|
||||
// Private constants
|
||||
var focus_opacity = 0.1,
|
||||
radius = 8,
|
||||
shape_size = 16,
|
||||
required_node = ['id', 'index', 'label', 'px', 'py', 'spells', 'weight', 'x', 'y', 'pos', 'scx', 'scy'];
|
||||
|
||||
// Private variables
|
||||
var width,
|
||||
height,
|
||||
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
|
||||
graph_wrapper,
|
||||
glinks,
|
||||
gnodes,
|
||||
background_image,
|
||||
background_opacity,
|
||||
background_filter_color,
|
||||
data_node, // Actual node data for the graph
|
||||
data_link, // Actual link data for the graph
|
||||
|
||||
link, // Line svgs
|
||||
node, // Circles for the nodes
|
||||
shape_property, // Property to whom the shape will be applied
|
||||
shapes, // Dictionary of shapes for nodes
|
||||
colors, // Dictionary of colors for nodes
|
||||
background; // Background of the graph
|
||||
|
||||
Number.prototype.between = function(min, max) {
|
||||
var min = (min || min === 0) ? min : Math.max(),
|
||||
max = (max || max === 0) ? max : Math.min();
|
||||
|
||||
return ( this > min && this <= max ) || ( min === 0 && this === 0 );
|
||||
};
|
||||
|
||||
Number.prototype.is_type = function() {
|
||||
if ( typeof(this) === 'number' )
|
||||
return ( Number.isInteger(this) ) ? 'int' : 'float';
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
String.prototype.is_type = function() {
|
||||
return "string";
|
||||
}
|
||||
|
||||
var lastFocusNode;
|
||||
var _helpers = {
|
||||
set_node: function(node, property, time) {
|
||||
// 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]) ) {
|
||||
var color_node = _helpers.set_color(property, d[property][0][0]);
|
||||
d[property].forEach(function(p) {
|
||||
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
|
||||
});
|
||||
return color_node;
|
||||
} else {
|
||||
return _helpers.set_color(property, d[property]);
|
||||
}
|
||||
})
|
||||
.style('stroke', function(d) {
|
||||
if (_helpers.set_shape(d[shape_property]) !== (-1))
|
||||
if ( Array.isArray(d[property]) ) {
|
||||
var color_node = _helpers.set_color(property, d[property][0][0]);
|
||||
d[property].forEach(function(p) {
|
||||
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
|
||||
});
|
||||
return color_node;
|
||||
} else {
|
||||
return _helpers.set_color(property, d[property]);
|
||||
}
|
||||
else
|
||||
return '#ffffff';
|
||||
})
|
||||
// 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 (_helpers.set_shape(d[shape_property]) !== (-1)) {
|
||||
return 'url(#' + _helpers.set_shape(d[shape_property]) + ')';
|
||||
}
|
||||
if ( Array.isArray(d[property]) ) {
|
||||
var color_node = _helpers.set_color(property, d[property][0][0]);
|
||||
d[property].forEach(function(p) {
|
||||
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
|
||||
});
|
||||
return color_node;
|
||||
} else {
|
||||
return _helpers.set_color(property, d[property]);
|
||||
}
|
||||
})
|
||||
.style('stroke', function(d) {
|
||||
if (_helpers.set_shape(d[shape_property]) !== (-1))
|
||||
if ( Array.isArray(d[property]) ) {
|
||||
var color_node = _helpers.set_color(property, d[property][0][0]);
|
||||
d[property].forEach(function(p) {
|
||||
if ( time.between(p[1], p[2]) ) color_node = _helpers.set_color(property, p[0]);
|
||||
});
|
||||
return color_node;
|
||||
} else {
|
||||
return _helpers.set_color(property, d[property]);
|
||||
}
|
||||
else
|
||||
return '#ffffff';
|
||||
})
|
||||
.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);
|
||||
}
|
||||
});
|
||||
},
|
||||
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;
|
||||
});
|
||||
},
|
||||
push_once: function(array, item, key) {
|
||||
for (var i in array) {
|
||||
if ( array[i][key] == item[key] ) return false;
|
||||
}
|
||||
array.push(item);
|
||||
return true;
|
||||
},
|
||||
set_color: function(property, value) {
|
||||
if ( colors instanceof Array ) {
|
||||
for ( var c in colors ) {
|
||||
if ( colors[c][property] == value ) { return colors[c]['color']; }
|
||||
}
|
||||
return color(value);
|
||||
} else {
|
||||
return color(value);
|
||||
}
|
||||
},
|
||||
set_shape: function(value) {
|
||||
if ( shapes instanceof Object && shape_property ) {
|
||||
for ( var s in shapes ) {
|
||||
var str_value = (value.includes('class')) ? value.split('.').pop().split('\'')[0] : value;
|
||||
if ( str_value == s ) return shapes[s];
|
||||
}
|
||||
return (-1);
|
||||
} else {
|
||||
return (-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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');
|
||||
|
||||
// Set background
|
||||
if ( background !== undefined ) {
|
||||
var rect = groot.append('rect').attr('fill', background_filter_color);
|
||||
background_image = groot.append('image').attr('href', background).style('opacity', background_opacity);
|
||||
graph_wrapper = groot.append('g') .attr('id', 'graph-wrapper');
|
||||
glinks = graph_wrapper.append('g') .attr('id', 'links');
|
||||
gnodes = graph_wrapper.append('g') .attr('id', 'nodes');
|
||||
} else {
|
||||
glinks = groot.append('g') .attr('id', 'links');
|
||||
gnodes = groot.append('g') .attr('id', 'nodes');
|
||||
}
|
||||
|
||||
// Add patterns for shapes
|
||||
var defs = [];
|
||||
for ( var i in shapes )
|
||||
if (!defs.includes(shapes[i])) defs.push(shapes[i])
|
||||
|
||||
svg.append('defs')
|
||||
.selectAll('pattern')
|
||||
.data(defs)
|
||||
.enter()
|
||||
.append('pattern')
|
||||
.attr('id', function(d, i) {
|
||||
return d;
|
||||
})
|
||||
.attr('patternUnits', 'objectBoundingBox')
|
||||
.attr('width', 1)
|
||||
.attr('height', 1)
|
||||
.append('image')
|
||||
.attr('href', function(d) {
|
||||
return window.location.protocol + '//' + window.location.host + '/img/svg/' + d + '.svg';
|
||||
})
|
||||
.attr('width', shape_size)
|
||||
.attr('height', shape_size);
|
||||
|
||||
// Zoom
|
||||
zoom = d3.behavior
|
||||
.zoom()
|
||||
.scaleExtent([1/5, 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) {
|
||||
if ( d.source.scx ) return d.source.scx;
|
||||
else return d.source.x;
|
||||
}).attr('y1', function (d) {
|
||||
if ( d.source.scy ) return d.source.scy;
|
||||
else return d.source.y;
|
||||
}).attr('x2', function (d) {
|
||||
if ( d.target.scx ) return d.target.scx;
|
||||
else return d.target.x;
|
||||
}).attr('y2', function (d) {
|
||||
if ( d.target.scy ) return d.target.scy;
|
||||
else return d.target.y;
|
||||
});
|
||||
|
||||
node.attr('transform', function translate(d) {
|
||||
if ( d.scx || d.scy ) return 'translate(' + d.scx + ',' + d.scy + ')';
|
||||
else 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, time);
|
||||
|
||||
// Node Attributes
|
||||
var statistics = {}
|
||||
self.GraphVisualization.statistics = {};
|
||||
data_node.forEach(function(n) {
|
||||
// Count node properties
|
||||
if ( Array.isArray(n[property]) ) {
|
||||
n[property].forEach(function(p) {
|
||||
if ( time.between(p[1], p[2]) ) statistics[p[0]] = (!statistics[p[0]]) ? 1 : statistics[p[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
|
||||
}
|
||||
|
||||
function get_models(graph) {
|
||||
|
||||
var models = { 'dynamic': [], 'static': [] }
|
||||
|
||||
graph['nodes'].forEach(function(node) {
|
||||
for ( var att in node ) {
|
||||
if (!required_node.includes(att)) {
|
||||
if ( Array.isArray(node[att]) ) _helpers.push_once(models['dynamic'], { 'title': att, 'type': node[att][0][0].is_type() }, 'title');
|
||||
else _helpers.push_once(models['static'], { 'title': att, 'type': typeof(node[att]) }, 'title');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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, n_height, n_width, callback) {
|
||||
name = id;
|
||||
svg = d3.select('svg#' + name)
|
||||
.attr('width', n_width)
|
||||
.attr('height', n_height)
|
||||
.style('background-color', 'rgba(128,128,128,0.1)');
|
||||
|
||||
height = n_height;
|
||||
width = n_width
|
||||
|
||||
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} callback A function called at the end.
|
||||
*/
|
||||
function importJSON(json, callback) {
|
||||
reset()
|
||||
graph = json;
|
||||
|
||||
// Create the graph itself
|
||||
Graph();
|
||||
|
||||
self.GraphVisualization.nodes = graph.nodes.length;
|
||||
self.GraphVisualization.links = graph.links.length;
|
||||
self.GraphVisualization.model = get_models(json);
|
||||
|
||||
// Draw graph with default property and time for the first time
|
||||
update_data(self.GraphVisualization.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);
|
||||
|
||||
var s = d3.scale.linear().domain([30, 1000]).range([16, 48]);
|
||||
if ( shapes instanceof Object && shape_property ) {
|
||||
svg.selectAll('pattern image').attr('width', s(distance)).attr('height', s(distance));
|
||||
}
|
||||
|
||||
if (callback) { callback(radius); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set background image.
|
||||
* A function that set a background image.
|
||||
*
|
||||
* @param {object} image Path to image.
|
||||
*/
|
||||
function set_background(image, set_opacity, set_color) {
|
||||
background = image;
|
||||
background_opacity = set_opacity || 0.8;
|
||||
background_filter_color = set_color || 'white';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set shapes and color of graph.
|
||||
* A function that set the shapes and colors of the nodes depending on their status.
|
||||
*
|
||||
* @param {object} set_shapes Shapes for nodes.
|
||||
* @param {object} set_colors Colors for nodes.
|
||||
* @param {object} callback A function called at the end.
|
||||
*/
|
||||
function set_params(set_shape_property, set_shapes, set_colors, callback) {
|
||||
shape_property = set_shape_property;
|
||||
shapes = set_shapes;
|
||||
colors = set_colors;
|
||||
|
||||
self.GraphVisualization.shapes = shapes;
|
||||
self.GraphVisualization.colors = colors;
|
||||
|
||||
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(property, value) {
|
||||
if (graph) {
|
||||
return _helpers.set_color(property, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes at one moment given.
|
||||
* A function that get the attributes of all nodes at a specific time.
|
||||
*
|
||||
* @param {object} time Instant of time.
|
||||
* @param {object} callback A function called at the end.
|
||||
* @return {object} object An object with the number of nodes.
|
||||
*/
|
||||
function get_attributes(property, time, callback) {
|
||||
var attrs = {}
|
||||
|
||||
graph.nodes.forEach(function(node) {
|
||||
|
||||
if (Array.isArray(node.spells)) {
|
||||
node.spells.forEach( function(d) {
|
||||
if ( time.between(d[0], d[1]) ) {
|
||||
|
||||
if (Array.isArray(node[property])) {
|
||||
node[property].forEach( function(p) {
|
||||
if ( time.between(p[1], p[2]) ) attrs[p[0]] = (!attrs[p[0]]) ? 1 : attrs[p[0]] + 1;
|
||||
});
|
||||
} else { attrs[node[property]] = (!attrs[node[property]]) ? 1 : attrs[node[property]] + 1; }
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
if (Array.isArray(node[property])) {
|
||||
node[property].forEach( function(p) {
|
||||
if ( time.between(p[1], p[2]) ) attrs[p[0]] = (!attrs[p[0]]) ? 1 : attrs[p[0]] + 1;
|
||||
});
|
||||
} else { attrs[node[property]] = (!attrs[node[property]]) ? 1 : attrs[node[property]] + 1; }
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (callback) { callback(attrs); }
|
||||
else { return attrs }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nodes at one moment given.
|
||||
* A function that get the number of nodes at a specific time.
|
||||
*
|
||||
* @param {object} time Instant of time.
|
||||
* @param {object} callback A function called at the end.
|
||||
* @return {object} number The number of nodes.
|
||||
*/
|
||||
function get_nodes(time, callback) {
|
||||
var total_nodes = 0;
|
||||
graph.nodes.forEach(function(node) {
|
||||
if (Array.isArray(node.spells)) {
|
||||
node.spells.forEach( function(d) {
|
||||
if ( time.between(d[0], d[1]) ) { total_nodes++; }
|
||||
});
|
||||
} else {
|
||||
total_nodes++;
|
||||
}
|
||||
});
|
||||
|
||||
if (callback) { callback(total_nodes); }
|
||||
else { return total_nodes }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Exporting
|
||||
* ---------
|
||||
*/
|
||||
this.GraphVisualization = {
|
||||
|
||||
// Functions
|
||||
create: create,
|
||||
import: importJSON,
|
||||
update_graph: update_graph,
|
||||
set_params: set_params,
|
||||
set_link_distance: set_link_distance,
|
||||
set_background: set_background,
|
||||
fit: zoom_to_fit,
|
||||
reset: reset,
|
||||
|
||||
// Attributes
|
||||
model: {},
|
||||
nodes: undefined,
|
||||
links: undefined,
|
||||
|
||||
// Getters
|
||||
color: color,
|
||||
shapes: shapes,
|
||||
colors: colors,
|
||||
get_attributes: get_attributes,
|
||||
get_nodes: get_nodes,
|
||||
|
||||
// Statistics
|
||||
statistics: {},
|
||||
|
||||
// Version
|
||||
version: '0.1'
|
||||
};
|
||||
|
||||
}).call(this);
|
Reference in New Issue
Block a user