Hi,
would it be possible to add functionality to update an already existing network, without re-rendering it?
For instance, in leaflet you can modify an already existing map via a proxy object, see here.
I've already implemented it, as in a previous version of visNetwork this feature was missing (to my knowledge). I'm not very familiar with your code but I implemented my own binding of vis.js as follows:
Assume you generate a network via a render call in server under the id: mynetwork and let's say I want to change the physics properties and set the focus of the network to a given node in the network, in which the viewer zooms in a puts that node in the center of the page. However, we don't want to re-render the network each time, we just want to update an existing network.
So I want to do something like this:
- Create a network via an appropriate render function called in server.r
- Setup input controls that allow you to change the physics properties of the network via some input controls defined in ui.r
- Change the focus of the network to some node id controlled via some input controller defined in ui.r.
- Update the network when you change the input controls, however, without re-rendering the whole network.
- send custom messages from Shiny to update the network (via ''sendCustomMessage').
The idea is to store the network in a jQuery variable called state inside the div in which the network is rendered.
Auxiliary functions other than the main render network function in server.r can get to this data via the id and can then retrieve the network to make changes to it.
Here are some code snippets:
Create a render function to generate an initial network:
output$mynetwork <- renderNetwork({
# get some data
nodes = ...
edges = ...
# prep data
data = list(nodes=nodes,edges=edges)
# chart options
options = list()
# combine into list
L <- list(data = data, options = options)
# send list to javascript
return(L)
})
renderNetwork is just a simple vanilla output binding to vis.js in which I pass a data object holding node and edge info and an options list, containing network options I want to set initially.
Next setup some observers in server which listen to the input controller which manipulate the network:
server.r code to change the physics properties
observe({
data <- list(
id = "mynetwork1",
gravitationalConstant = input$gravitationalConstant,
centralGravity = input$centralGravity,
springLength = input$springLength,
springConstant = input$springConstant,
damping = input$damping,
avoidOverlap = input$avoidOverlap
)
session$sendCustomMessage("physicsOptions",data)
})
server code to change the focus
observe({
data <- list(id = "mynetwork", focusId = input$FocusId)
session$sendCustomMessage("Focus",data)
})
I implemented the binding as an 'old school' custom output binding, instead of your newer HTMLWidget style. For initial rendering of the network you would have a 'renderValue'. In the code snippet el is the element passed from Shiny in which we want to render the network and L is the data object, which holds the node/edge data and the options for the network.
binding.renderValue = function(el,L) {
// get element
var $el = $(el);
// store the result on $el as a data value called "state".
if (!$el.data("state")){
// create an array with nodes
var nodes = new vis.DataSet(L.data.nodes);
// create an array with edges
var edges = new vis.DataSet(L.data.edges);
// create data object
var data = {
nodes: nodes,
edges: edges
};
// determine chart id send by Shiny
var ChartID = el.id;
// get container element to hold chart
var container = document.getElementById(ChartID);
// some code here to setup the network e.g via the options
...
...
...
// create a network
var network = new vis.Network(container, data, options);
// now we have a chart
// store the chart object on el so we can get it next time
$el.data("state", {
network: network,
nodes: nodes,
edges: edges
});
}
// get previously constucted network
var network = $el.data("state").chart;
// code here to change the network after you rendered it
// but still change it via the main render network function
...
...
...
};
Next, outside of the main render function I added functions that via a data object passed from Shiny, (which contain an id and some data you need to change the network), can get access to the already rendered network, fetch it and manipulate it:
// Custom handler to update physicsOptions
Shiny.addCustomMessageHandler('physicsOptions', function(data){
// get container id
var el = $("#"+data.id);
// get nodes object
var network = el.data("state").chart;
// these are all options in full.
var options = {
physics:{
enabled: true,
barnesHut: {
gravitationalConstant: data.gravitationalConstant,
centralGravity: data.centralGravity,
springLength: data.springLength,
springConstant: data.springConstant,
damping: data.damping,
avoidOverlap: data.avoidOverlap
},
solver: 'barnesHut'
}
};
network.setOptions(options);
});
// focus on a node in the network
Shiny.addCustomMessageHandler('Focus', function(data){
// get container id
var el = $("#"+data.id);
// get nodes object
var network = el.data("state").chart;
var options = {scale: 2,animation: {duration: 1500, easingFunction: "easeInOutQuad"} };
network.focus(data.focusId,options);
});
With this type of code you can manipulate the network very easily after it was constructed, without re-rendering the network each time (which moves nodes to different places etc.).
Perhaps the current binding of visNetwork can already do this? Obviously my code doesn't you well with your code base, so there will be many differences implementing it. Anyway, I thought I share this idea with you.
kind regards, Herman
ps: this type of functionality comes in handy when you have a large graph/network and you want to explore it, instead of changing the underlying network data.