WeakNode and Nested WeakNode (Interactive)
This notebook shows how to build Document → Section → Page hierarchies with WeakNode, how to inspect the graph state after each operation, and how to delete nodes with different strategies.
It includes an interactive visual graph editor powered by Cytoscape.js: the graph updates in real time as you add or delete nodes, with color-coded node types and a hierarchical layout.
[ ]:
import importlib.util
# Check if the package is installed
package_to_check = 'cvcdocdb'
spec = importlib.util.find_spec(package_to_check)
if spec is None:
print(f'⚠️ {package_to_check} is not installed. Installing...')
%pip install -q --upgrade cvcdocdb-tools
print("✅ Installation complete. Kernel may need a restart.")
else:
print(f'✅ {package_to_check} is already installed. Skipping installation.')
Overview
The notebook is organised as a step-by-step walkthrough followed by an interactive visual editor:
Control panel — add documents, sections, pages, or delete selected nodes
Visual graph — real-time Cytoscape.js rendering with color-coded node types
State inspector — detailed text output after each operation
Node colors: 🟢 Document (strong) · 🔵 Section (weak) · 🟠 Page (nested weak)
Interactions: Scroll to zoom · Drag to pan · Drag nodes to rearrange
[ ]:
import os
import tempfile
import uuid
from IPython.display import display, clear_output, Javascript, HTML
import ipywidgets as widgets
try:
from cvcdocdb import NetworkXGraph, Node, WeakNode
except ImportError:
from cvcdocdb import NetworkXGraph
from cvcdocdb.base import Node, WeakNode
persistence_path = os.path.join(tempfile.gettempdir(), f"cvcdocdb_tutorial_weaknodes_{uuid.uuid4().hex}.pkl")
graph = NetworkXGraph(persistence_path=persistence_path)
registry = {}
# ── Color palette for node types ──────────────────────────────────
NODE_COLORS = {
"Document": "#4CAF50",
"Section": "#2196F3",
"Page": "#FF9800",
}
NODE_BORDERS = {
"Document": "#2E7D32",
"Section": "#1565C0",
"Page": "#E65100",
}
[ ]:
# ── Core graph operations ─────────────────────────────────────────
def key_for_document(doc_id):
return f"doc:{doc_id}"
def key_for_section(doc_id, section_id):
return f"section:{doc_id}:{section_id}"
def key_for_page(doc_id, section_id, page_id):
return f"page:{doc_id}:{section_id}:{page_id}"
def show_state(title="State"):
print(f"\n=== {title} ===")
print("nodes:", graph.get_nodes())
print("edges:", graph.get_edges())
debug = graph.debug()
print("debug nodes:", debug.get("nodes", []))
print("debug edges:", debug.get("edges", []))
def add_document(doc_id):
k = key_for_document(doc_id)
node = Node(pk={"doc": doc_id}, main_label="Document")
graph.insertNode(node, replace=True)
registry[k] = node
return k
def add_section(doc_id, section_id):
parent_key = key_for_document(doc_id)
if parent_key not in registry:
add_document(doc_id)
parent = registry[parent_key]
k = key_for_section(doc_id, section_id)
node = WeakNode(parent=parent, pk={"section": section_id}, main_label="Section")
graph.insertNode(node, insert_parent=True, replace=True)
registry[k] = node
return k
def add_page(doc_id, section_id, page_id):
section_key = key_for_section(doc_id, section_id)
if section_key not in registry:
add_section(doc_id, section_id)
parent = registry[section_key]
k = key_for_page(doc_id, section_id, page_id)
node = WeakNode(parent=parent, pk={"page": page_id}, main_label="Page")
graph.insertNode(node, insert_parent=True, replace=True)
registry[k] = node
return k
def delete_key(k, detach=True, propagation=False, on_delete="cascade"):
node = registry.get(k)
if node is None:
raise KeyError(f"Unknown key: {k}")
graph.deleteNode(node, detach=detach, propagation=propagation, on_delete=on_delete)
registry.pop(k, None)
[ ]:
# ── Graph → Cytoscape conversion helpers ───────────────────────────
def _node_label(key, node):
"""Generate a human-readable label for display."""
base = node.main_label.split(':')[0] if ':' in node.main_label else node.main_label
pk_parts = []
if hasattr(node, '_pk') and node._pk:
for k, v in sorted(node._pk.items()):
pk_parts.append(f"{k}={v}")
return f"{base}({', '.join(pk_parts)})" if pk_parts else base
def _node_color(main_label):
"""Map node main label to a color."""
base = main_label.split(':')[0] if ':' in main_label else main_label
return NODE_COLORS.get(base, '#888888')
def _node_border(main_label):
"""Map node main label to a border color."""
base = main_label.split(':')[0] if ':' in main_label else main_label
return NODE_BORDERS.get(base, '#555555')
def _is_weak_node(node):
"""Check if a node is a WeakNode."""
return isinstance(node, WeakNode)
def update_graph_visualization():
"""Extract nodes and edges from the graph and push to the Cytoscape.js visualization."""
nodes = []
edges = []
# Collect nodes from registry
for key, node in registry.items():
nodes.append({
"id": key,
"label": _node_label(key, node),
"color": _node_color(node.main_label),
"border": _node_border(node.main_label),
"weak": _is_weak_node(node),
})
# Collect edges from the graph
try:
all_edges = graph.get_edges()
for edge in all_edges:
src = getattr(edge, 'source', None)
tgt = getattr(edge, 'target', None)
if src and tgt:
src_key = getattr(src, '_key', None) or str(src)
tgt_key = getattr(tgt, '_key', None) or str(tgt)
is_weak_edge = False
if hasattr(edge, '_metadata') and edge._metadata:
is_weak_edge = edge._metadata.get('_propagate', False)
edges.append({
"id": f"e_{src_key}_{tgt_key}",
"source": src_key,
"target": tgt_key,
"label": "cascade" if is_weak_edge else "",
"weak": is_weak_edge,
})
except Exception:
pass
# Push to Cytoscape.js
js_code = f"updateGraph({nodes}, {edges});"
display(Javascript(js_code))
print("Graph initialized at:", persistence_path)
Step-by-step walkthrough
Build a hierarchy and watch the visual graph update below:
[ ]:
add_document("DOC-001")
update_graph_visualization()
show_state("After adding Document")
add_section("DOC-001", 1)
update_graph_visualization()
show_state("After adding Section as WeakNode")
add_page("DOC-001", 1, 1)
update_graph_visualization()
show_state("After adding Page as WeakNode of WeakNode")
[ ]:
delete_key("section:DOC-001:1", detach=True, propagation=False, on_delete="cascade")
update_graph_visualization()
show_state("After deleting Section with ON DELETE CASCADE")
Interactive visual editor
Below is the interactive graph visualization (Cytoscape.js). Use the control panel to add or delete nodes, and watch the graph update in real time.
How to interact:
Scroll to zoom in/out
Drag the background to pan
Drag individual nodes to rearrange
Hover over nodes to see their details
Dashed blue edges indicate
WeakRelationlinks (cascade delete)
[ ]:
# ── Cytoscape.js visualization ────────────────────────────────────
CYTOSCAPE_HTML = """
<div id="cvcdocdb-graph" style="
width: 100%;
height: 520px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fafafa;
overflow: hidden;
"></div>
<div id="graph-info" style="
margin-top: 10px;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
color: #555;
"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.30.3/cytoscape.min.js"></script>
<script>
var drmCy = null;
function initGraph() {
if (drmCy) { drmCy.destroy(); }
drmCy = cytoscape({
container: document.getElementById('cvcdocdb-graph'),
elements: [],
style: [
// Strong nodes (Documents) — rounded rectangles
{
selector: 'node.strong',
style: {
'label': 'data(label)',
'width': 110,
'height': 48,
'shape': 'round-rectangle',
'background-color': 'data(color)',
'border-width': 3,
'border-color': 'data(border)',
'color': '#fff',
'text-valign': 'center',
'text-halign': 'center',
'text-wrap': 'wrap',
'text-max-width': 100,
'font-size': 12,
'font-weight': 'bold',
}
},
// Weak nodes (Sections, Pages) — rectangles
{
selector: 'node.weak',
style: {
'label': 'data(label)',
'width': 100,
'height': 40,
'shape': 'rectangle',
'background-color': 'data(color)',
'border-width': 3,
'border-color': 'data(border)',
'color': '#fff',
'text-valign': 'center',
'text-halign': 'center',
'text-wrap': 'wrap',
'text-max-width': 90,
'font-size': 11,
'font-weight': 'bold',
}
},
// Hover effect
{
selector: ':hover',
style: {
'border-width': 5,
'shadow-blur': 10,
'shadow-color': '#ccc',
'shadow-opacity': 0.5,
}
},
// Strong edges (parent-child)
{
selector: 'edge.strong',
style: {
'width': 2.5,
'line-color': '#bbb',
'target-arrow-color': '#bbb',
'target-arrow-shape': 'triangle',
'arrow-scale': 1.3,
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': 9,
'color': '#999',
'text-rotation': 'autorotate',
'text-margin-y': -12,
}
},
// Weak edges (WeakRelation with cascade) — dashed blue
{
selector: 'edge.weak',
style: {
'width': 3.5,
'line-style': 'dashed',
'line-color': '#64B5F6',
'target-arrow-color': '#64B5F6',
'target-arrow-shape': 'triangle',
'arrow-scale': 1.4,
'curve-style': 'bezier',
'label': 'data(label)',
'font-size': 10,
'color': '#1976D2',
'font-weight': 'bold',
'text-rotation': 'autorotate',
'text-margin-y': -14,
}
},
],
layout: { name: 'breadthfirst', directed: true, padding: 50, nodeSeparation: 30 },
userZoomable: true,
userPannable: true,
userPannableUnpanned: true,
selectionType: 'single',
autounselectify: true,
minZoom: 0.3,
maxZoom: 3,
});
}
function updateGraph(nodes, edges) {
if (!drmCy) { initGraph(); }
var elements = [];
nodes.forEach(function(n) {
elements.push({
group: 'nodes',
data: {
id: n.id,
label: n.label,
color: n.color || '#888888',
border: n.border || '#555555',
},
classes: n.weak ? 'weak' : 'strong',
});
});
edges.forEach(function(e) {
elements.push({
group: 'edges',
data: {
id: e.id,
source: e.source,
target: e.target,
label: e.label || '',
},
classes: e.weak ? 'weak' : 'strong',
});
});
drmCy.elements().remove();
drmCy.add(elements);
var layout = drmCy.layout({
name: 'breadthfirst',
directed: true,
padding: 50,
nodeSeparation: 40,
animate: true,
animationDuration: 300,
});
layout.run();
var info = document.getElementById('graph-info');
if (info) {
var typeCounts = {};
nodes.forEach(function(n) {
var base = n.label.split('(')[0].trim();
typeCounts[base] = (typeCounts[base] || 0) + 1;
});
var summary = Object.entries(typeCounts)
.map(function(e) { return e[0] + ': ' + e[1]; })
.join(' · ');
info.innerHTML = nodes.length + ' node(s), ' + edges.length + ' edge(s) — ' + summary;
}
}
// Auto-initialize
initGraph();
updateGraph([], []);
</script>
"""
display(HTML(CYTOSCAPE_HTML))
[ ]:
# ── Control panel ─────────────────────────────────────────────────
doc_w = widgets.Text(value="DOC-002", description="doc:", layout=widgets.Layout(width='150px'))
section_w = widgets.IntText(value=1, description="sec:", layout=widgets.Layout(width='70px'))
page_w = widgets.IntText(value=1, description="page:", layout=widgets.Layout(width='70px'))
delete_dropdown = widgets.Dropdown(options=[], description="delete:", layout=widgets.Layout(width='250px'))
detach_w = widgets.Checkbox(value=True, description="detach", layout=widgets.Layout(width='90px'))
prop_w = widgets.Checkbox(value=False, description="propagate", layout=widgets.Layout(width='100px'))
on_delete_w = widgets.Dropdown(
options=["cascade", "restrict", "set_null"],
value="cascade",
description="on_delete:",
layout=widgets.Layout(width='150px')
)
out = widgets.Output(layout=widgets.Layout(max_height='180px', overflow='auto'))
def refresh_delete_options(*_):
opts = sorted(registry.keys())
delete_dropdown.options = opts if opts else ["(none)"]
def act_add_doc(_):
with out:
clear_output(wait=True)
k = add_document(doc_w.value)
print(f"✓ Added: {k}")
refresh_delete_options()
update_graph_visualization()
def act_add_section(_):
with out:
clear_output(wait=True)
k = add_section(doc_w.value, section_w.value)
print(f"✓ Added: {k}")
refresh_delete_options()
update_graph_visualization()
def act_add_page(_):
with out:
clear_output(wait=True)
k = add_page(doc_w.value, section_w.value, page_w.value)
print(f"✓ Added: {k}")
refresh_delete_options()
update_graph_visualization()
def act_delete(_):
with out:
clear_output(wait=True)
k = delete_dropdown.value
if not k or k == "(none)":
print("No node selected for deletion")
return
try:
delete_key(k, detach=detach_w.value, propagation=prop_w.value, on_delete=on_delete_w.value)
print(f"✓ Deleted: {k}")
except Exception as err:
print(f"✗ Delete error: {err}")
refresh_delete_options()
update_graph_visualization()
btn_doc = widgets.Button(description="+ Document", button_style="success", tooltip="Add a new Document")
btn_sec = widgets.Button(description="+ Section", button_style="info", tooltip="Add a Section under the current doc")
btn_page = widgets.Button(description="+ Page", button_style="warning", tooltip="Add a Page under the current section")
btn_del = widgets.Button(description="Delete", button_style="danger", tooltip="Delete the selected node")
btn_doc.on_click(act_add_doc)
btn_sec.on_click(act_add_section)
btn_page.on_click(act_add_page)
btn_del.on_click(act_delete)
refresh_delete_options()
display(widgets.VBox([
widgets.HBox([doc_w, section_w, page_w]),
widgets.HBox([btn_doc, btn_sec, btn_page]),
widgets.HBox([delete_dropdown, detach_w, prop_w, on_delete_w, btn_del]),
out,
]))
Cleanup
Close the graph and remove temporary files:
[ ]:
graph.close()
try:
os.remove(persistence_path)
except FileNotFoundError:
pass
print("Closed graph and removed temp persistence file")