Added first controls
This commit is contained in:
238
src/myfasthtml/core/network_utils.py
Normal file
238
src/myfasthtml/core/network_utils.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
ROOT_COLOR = "#ff9999"
|
||||
GHOST_COLOR = "#cccccc"
|
||||
|
||||
def from_nested_dict(trees: list[dict]) -> tuple[list, list]:
|
||||
"""
|
||||
Convert a list of nested dictionaries to vis.js nodes and edges format.
|
||||
|
||||
Args:
|
||||
trees: List of nested dictionaries where keys are node names and values are
|
||||
dictionaries of children (e.g., [{"root1": {"child1": {}}}, {"root2": {}}])
|
||||
|
||||
Returns:
|
||||
tuple: (nodes, edges) where:
|
||||
- nodes: list of dicts with auto-incremented numeric IDs
|
||||
- edges: list of dicts with 'from' and 'to' keys
|
||||
|
||||
Example:
|
||||
>>> trees = [{"root1": {"child1": {}}}, {"root2": {"child2": {}}}]
|
||||
>>> nodes, edges = from_nested_dict(trees)
|
||||
>>> # nodes = [{"id": 1, "label": "root1"}, {"id": 2, "label": "child1"}, ...]
|
||||
"""
|
||||
nodes = []
|
||||
edges = []
|
||||
node_id_counter = 1
|
||||
node_id_map = {} # maps node_label -> node_id
|
||||
|
||||
def traverse(subtree: dict, parent_id: int | None = None):
|
||||
nonlocal node_id_counter
|
||||
|
||||
for node_label, children in subtree.items():
|
||||
# Create node with auto-incremented ID
|
||||
current_id = node_id_counter
|
||||
node_id_map[node_label] = current_id
|
||||
nodes.append({
|
||||
"id": current_id,
|
||||
"label": node_label
|
||||
})
|
||||
node_id_counter += 1
|
||||
|
||||
# Create edge from parent to current node
|
||||
if parent_id is not None:
|
||||
edges.append({
|
||||
"from": parent_id,
|
||||
"to": current_id
|
||||
})
|
||||
|
||||
# Recursively process children
|
||||
if children:
|
||||
traverse(children, parent_id=current_id)
|
||||
|
||||
# Process each tree in the list
|
||||
for tree in trees:
|
||||
traverse(tree)
|
||||
|
||||
return nodes, edges
|
||||
|
||||
|
||||
def from_tree_with_metadata(
|
||||
trees: list[dict],
|
||||
id_getter: Callable = None,
|
||||
label_getter: Callable = None,
|
||||
children_getter: Callable = None
|
||||
) -> tuple[list, list]:
|
||||
"""
|
||||
Convert a list of trees with metadata to vis.js nodes and edges format.
|
||||
|
||||
Args:
|
||||
trees: List of dictionaries with 'id', 'label', and 'children' keys
|
||||
(e.g., [{"id": "root1", "label": "Root 1", "children": [...]}, ...])
|
||||
id_getter: Optional callback to extract node ID from dict
|
||||
Default: lambda n: n.get("id")
|
||||
label_getter: Optional callback to extract node label from dict
|
||||
Default: lambda n: n.get("label", "")
|
||||
children_getter: Optional callback to extract children list from dict
|
||||
Default: lambda n: n.get("children", [])
|
||||
|
||||
Returns:
|
||||
tuple: (nodes, edges) where:
|
||||
- nodes: list of dicts with IDs from tree or auto-incremented
|
||||
- edges: list of dicts with 'from' and 'to' keys
|
||||
|
||||
Example:
|
||||
>>> trees = [
|
||||
... {
|
||||
... "id": "root1",
|
||||
... "label": "Root Node 1",
|
||||
... "children": [
|
||||
... {"id": "child1", "label": "Child 1", "children": []}
|
||||
... ]
|
||||
... },
|
||||
... {"id": "root2", "label": "Root Node 2"}
|
||||
... ]
|
||||
>>> nodes, edges = from_tree_with_metadata(trees)
|
||||
"""
|
||||
# Default getters
|
||||
if id_getter is None:
|
||||
id_getter = lambda n: n.get("id")
|
||||
if label_getter is None:
|
||||
label_getter = lambda n: n.get("label", "")
|
||||
if children_getter is None:
|
||||
children_getter = lambda n: n.get("children", [])
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
node_id_counter = 1
|
||||
|
||||
def traverse(node_dict: dict, parent_id: int | str | None = None):
|
||||
nonlocal node_id_counter
|
||||
|
||||
# Extract ID (use provided or auto-increment)
|
||||
node_id = id_getter(node_dict)
|
||||
if node_id is None:
|
||||
node_id = node_id_counter
|
||||
node_id_counter += 1
|
||||
|
||||
# Extract label
|
||||
node_label = label_getter(node_dict)
|
||||
|
||||
# Create node
|
||||
nodes.append({
|
||||
"id": node_id,
|
||||
"label": node_label
|
||||
})
|
||||
|
||||
# Create edge from parent to current node
|
||||
if parent_id is not None:
|
||||
edges.append({
|
||||
"from": parent_id,
|
||||
"to": node_id
|
||||
})
|
||||
|
||||
# Recursively process children
|
||||
children = children_getter(node_dict)
|
||||
if children:
|
||||
for child in children:
|
||||
traverse(child, parent_id=node_id)
|
||||
|
||||
# Process each tree in the list
|
||||
for tree in trees:
|
||||
traverse(tree)
|
||||
|
||||
return nodes, edges
|
||||
|
||||
|
||||
def from_parent_child_list(
|
||||
items: list,
|
||||
id_getter: Callable = None,
|
||||
label_getter: Callable = None,
|
||||
parent_getter: Callable = None,
|
||||
ghost_color: str = GHOST_COLOR,
|
||||
root_color: str | None = ROOT_COLOR
|
||||
) -> tuple[list, list]:
|
||||
"""
|
||||
Convert a list of items with parent references to vis.js nodes and edges format.
|
||||
|
||||
Args:
|
||||
items: List of items (dicts or objects) with parent references
|
||||
id_getter: callback to extract node ID
|
||||
label_getter: callback to extract node label
|
||||
parent_getter: callback to extract parent ID
|
||||
ghost_color: color for ghost nodes (referenced parents)
|
||||
root_color: color for root nodes (nodes without parent)
|
||||
|
||||
Returns:
|
||||
tuple: (nodes, edges)
|
||||
"""
|
||||
|
||||
# Default getters
|
||||
if id_getter is None:
|
||||
id_getter = lambda item: item.get("id")
|
||||
|
||||
if label_getter is None:
|
||||
label_getter = lambda item: item.get("label", "")
|
||||
|
||||
if parent_getter is None:
|
||||
parent_getter = lambda item: item.get("parent")
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
|
||||
# Track all existing node IDs
|
||||
existing_ids = set()
|
||||
|
||||
# First pass: create nodes for all items
|
||||
for item in items:
|
||||
node_id = id_getter(item)
|
||||
node_label = label_getter(item)
|
||||
|
||||
existing_ids.add(node_id)
|
||||
nodes.append({
|
||||
"id": node_id,
|
||||
"label": node_label,
|
||||
# root color assigned later
|
||||
})
|
||||
|
||||
# Track ghost nodes
|
||||
ghost_nodes = set()
|
||||
|
||||
# Track which nodes have parents
|
||||
nodes_with_parent = set()
|
||||
|
||||
# Second pass: create edges and detect ghost nodes
|
||||
for item in items:
|
||||
node_id = id_getter(item)
|
||||
parent_id = parent_getter(item)
|
||||
|
||||
# Skip roots
|
||||
if parent_id is None or parent_id == "":
|
||||
continue
|
||||
|
||||
# Child has a parent
|
||||
nodes_with_parent.add(node_id)
|
||||
|
||||
# Create edge parent → child
|
||||
edges.append({
|
||||
"from": parent_id,
|
||||
"to": node_id
|
||||
})
|
||||
|
||||
# Create ghost node if parent not found
|
||||
if parent_id not in existing_ids and parent_id not in ghost_nodes:
|
||||
ghost_nodes.add(parent_id)
|
||||
nodes.append({
|
||||
"id": parent_id,
|
||||
"label": str(parent_id),
|
||||
"color": ghost_color
|
||||
})
|
||||
|
||||
# Final pass: assign color to root nodes
|
||||
if root_color is not None:
|
||||
for node in nodes:
|
||||
if node["id"] not in nodes_with_parent and node["id"] not in ghost_nodes:
|
||||
# Root node
|
||||
node["color"] = root_color
|
||||
|
||||
return nodes, edges
|
||||
Reference in New Issue
Block a user