Hello fellow Panel users,
I am building a data visualization app for work and I would like to represent certain parts of the app as a hierarchical tree. I’m familiar with JSTree and panel-jstree
but I found the latter insufficiently flexible for what I wanted to do, and the documentation is quite sparse. Long story short, I decided to give ReactiveHTML
a spin and implement the thing myself. Here’s what I came up with:
import panel as pn
import param
from panel.reactive import ReactiveHTML
pn.extension()
class TreeNode(ReactiveHTML):
node_id = param.String()
children = param.List()
label = param.String()
toggled = param.Boolean()
open = param.Boolean()
def _toggle(self, event):
self.toggled = not self.toggled
self.toggle_children(self.toggled)
def _label_click(self, event):
pass
def _expand_children(self, event=None):
for child in self.children:
child.open = True
child._expand_children()
_template = """
<li id="node-{{ node_id }}">
<details id="details-{{ node_id }}" open="${open}">
<summary id="summary-{{ node_id }}" onclick="${_expand_children}">
<input type="checkbox" id="checkbox" checked="${toggled}" onclick="${_toggle}"/>
{{label}}
</summary>
<ul class="tree-children" id="tree-children">
{% for child in children %}
<div id="child-container-{{ loop.index0 }}">
${child}
</div>
{% endfor %}
</ul>
</details>
</li>
"""
_dom_events = {'checkbox': ['change']}
def _checkbox_change(self, event):
pass
def find_node(self, target_id):
for child in self.children:
if child.node_id == target_id:
return child
elif (result := child.find_node(target_id)) is not None:
return result
return None
def insert_node(self, node, parent_id):
parent = self.find_node(parent_id)
print(parent_id, parent)
if parent is not None:
parent.children.append(node)
def toggle_children(self, value):
for child in self.children:
child.toggled = value
child.toggle_children(value)
class TreeRoot(TreeNode):
_template = """
<ul class="tree-root" id="tree-root">
{% for child in children %}
<div id="child-{{ loop.index0 }}">${child}</div>
{% endfor %}
</ul>
"""
I had to break up the tree into a special root node and general nodes, because the nodes themselves need to be mapped to <li>
elements but the overall outer wrapping starts with a <ul>
, so that breaks the recursivity. I’m sure there are other ways to do this using just divs and the appropriate JavaScript, but I’m not a JS expert so I haven’t bothered to figure out how to do that. Anyway, I hope this is useful to other people trying to build hierarchical representations in Panel. Happy to answer more questions about this.