Fix handling of set_options() function
Documentation says that Network.set_options() should get JSON options, generated by browser, and set it to the Network, but didn't work at all. Now it's fixed inside of options.py file and work perfectly, but with one small peculiarity. It works only after calling all other functions that related to Options class and not after it. Details of my solution you can find in commentary on this topic: https://github.com/WestHealth/pyvis/issues/81#issuecomment-2837830953
The root reason for the bug is that Network.set_options() is setting options variable to dict, but not Options class! That's why all functions from Options class became unavalieble after calling Network.set_options().
Also there were some smaller issues like:
- "Generate options" button generate some options with null-value. When these settings are passed to the function, they are reset and also lead to a breakdown in the visualization of some components. I fixed it with
del_nulls()subfunction. - "Generate options" button generate not all options that necessary for the execution of the program. Also, before fix
Network.set_options()rewrote the values of options that were seted before using functions (Network.set_edge_smooth(),Network.show_buttons()etc.) That's whydeep_merge(dict1, dict2)function needed. This merge is "deep", becouse of recursive representation of options in JSON form:
"edges": { "color": { "inherit": true } }
Therefore, I see two ways to solve this bug complitly, so that users could use set_options() few times and another functions from Options class in any order.
First way — is to construnt an Options class from JSON instead of reasigning it as dictionary. It's truly problematic becouse of JSON file with options consists not only of Options class' members, but of "nodes" and "edges" too, which, as I can see, not represented in python part of pyvis library. Those go to the input of other imported libraries and set defoult color to Nodes and so on(?) I mean — when Node's color is not clarified, it became the color from those options.
And the second way looks easier for me — to check if options is the dict in every function wich operate with it and choose suitable command. It does not require rewriting Options as a class, and similar exception check already exists in pyvis code. But only for one function inside network.py:
I'll try to implement the second solution in the next commit.
Now I pushed a second commit with changes in network.py. There, as I sugested a few hours before, a just hardcoded check if isinstance(self.options, dict) for every function. For some functions it was obvious, for some — I had to watch the corresponding keys in html-file. I think this commit might be important for algorithmic graph generation, when it is easier to add a few new settings than to rewrite the previous code.
Also, it's really ugly solution, and I just didn't find any better, but now it solving bug completely. And I have to draw attention to some particularly ugly place in my code: I totaly copied a function set() from the Options class into Network.set_options() in case of if isinstance(self.options, dict). Technically, set() from the Options now is not needed, and documenting of Network.set_options() should be rewriten, becouse now it's not allways "delegates to the options.Options.set routine" as before.
The reason why I done it — the necessary solution includes merging new options with current options, what is done in my first commit. But after reassigning JSON to options variable, options not an Options class anymore, it's dictionary. Therefore we can't use options.set() again after asigning options from JSON once. We also can't use Options.set(), because set() is merging self with a new options, but we need to merge a dictionary of options with another dictionary of options.
I could take the merge function out of the set() function and make it separate. But in this case it would be a totaly unexpected, undocumented and seemingly-unnecessary new function inside Options class. So for better code structure I repeated it and also left else-case, which could also be erased and included in the dictionary-case.
So... The code could be improved, but I don't know if it should be... The main thing is that it works now.
I checked my changes with the next script by commiting different commands in it, so I hope everything works allright:
from pyvis.pyvis.network import Network
import os
net = Network(directed=True, neighborhood_highlight=True, select_menu=False, filter_menu=True)
options = '''
const options = {
"nodes": {
"borderWidth": null,
"borderWidthSelected": null,
"opacity": null,
"size": null
},
"edges": {
"color": {
"inherit": true
},
"selfReferenceSize": null,
"selfReference": {
"angle": 0.7853981633974483
},
"smooth": {
"forceDirection": "none"
}
},
"interaction": {
"keyboard": {
"enabled": true
},
"multiselect": true
},
"manipulation": {
"enabled": true
},
"physics": {
"forceAtlas2Based": {
"springLength": 100
},
"minVelocity": 0.75,
"solver": "forceAtlas2Based"
}
}
'''
net.add_node(1, label="Node 1", group='red', castom_prop='what!')
net.add_node(2, label="2")
nodes = ["a", "b", "c", "d"]
net.add_nodes(nodes)
net.add_nodes("hello")
net.add_edge(2, 1)
net.add_edge(2, 1)
net.add_edge(1, 2)
net.barnes_hut()
net.repulsion()
net.hrepulsion()
net.force_atlas_2based()
net.set_edge_smooth('dynamic')
net.set_edge_smooth('continuous')
net.set_edge_smooth('discrete')
net.set_edge_smooth('diagonalCross')
net.set_edge_smooth('straightCross')
net.set_edge_smooth('horizontal')
net.set_edge_smooth('vertical')
net.set_edge_smooth('curvedCW')
net.set_edge_smooth('curvedCCW')
net.set_edge_smooth('cubicBezier')
net.toggle_hide_edges_on_drag(True)
net.toggle_hide_nodes_on_drag(True)
net.inherit_edge_colors(True)
net.show_buttons(filter_=['nodes'])
net.show_buttons()
net.toggle_physics(False)
net.toggle_drag_nodes(False)
net.toggle_stabilization(False)
net.set_options(options)
net.barnes_hut()
net.repulsion()
net.hrepulsion()
net.force_atlas_2based()
net.set_edge_smooth('dynamic')
net.set_edge_smooth('continuous')
net.set_edge_smooth('discrete')
net.set_edge_smooth('diagonalCross')
net.set_edge_smooth('straightCross')
net.set_edge_smooth('horizontal')
net.set_edge_smooth('vertical')
net.set_edge_smooth('curvedCW')
net.set_edge_smooth('curvedCCW')
net.set_edge_smooth('cubicBezier')
net.toggle_hide_edges_on_drag(True)
net.toggle_hide_nodes_on_drag(True)
net.inherit_edge_colors(True)
net.show_buttons(filter_=['nodes'])
net.show_buttons()
net.toggle_physics(False)
net.toggle_drag_nodes(False)
net.toggle_stabilization(False)
net.set_options(options)
net.add_node("aaa", group='red', castom_prop='what!')
net.add_node("bbb", group='red', castom_prop='what!', size = 50)
print(os.getcwd())
net.show(os.getcwd()+'/graph.html', notebook=False)
Related issues: https://github.com/WestHealth/pyvis/issues/243 https://github.com/WestHealth/pyvis/issues/140 https://github.com/WestHealth/pyvis/issues/81 https://github.com/WestHealth/pyvis/issues/290
Instead of mutating self.options into a dict and then wrapping every call with isinstance(self.options, dict), just create an OptionsWrapper class that transparently handles both dict and Options types behind the scenes. This way you write clean, centralized logic like:
Then inside Network:
Just wanted to share in case it's helpful! You've clearly put a lot of effort into untangling this
Thanks! That's definitely better. But it seems that library is not maintained at all, so... Probably people will use my workaround from here. And it's far from ideal, but helps to use set_options() at least somehow.
You've clearly put a lot of effort into untangling this
Not really, it took two weekends to find out the reasons of that bug. But thanks for suggestion! Maybe someone would implement it in some better version of pyvis.
UPD: Nice spider-project also!