commit 0f920d3883ed1b8420df0916e9e37fead20c999a
parent 7a6c7c0397c6ea6ffa5ef8f2aa3d7194b93999ab
Author: Andy Khramtsov <>
Date: Fri, 29 May 2026 23:21:11 +0300
feat: improve the looks
Diffstat:
1 file changed, 167 insertions(+), 80 deletions(-)
diff --git a/src/deps/pages/home.py b/src/deps/pages/home.py
@@ -1,6 +1,7 @@
import base64
import io
import tomllib
+from dataclasses import dataclass
import dash
import polars as pl
@@ -37,6 +38,44 @@ class ids:
cache_store: str = "cache-store"
+class colors:
+ background: str = "#FAFAFA"
+ outline: str = "#FFFFFF"
+ outline_highlight: str = "#808080"
+ node: str = "#B0D0F0"
+ node_main: str = "#F080A0"
+ node_faint: str = "#E0E0E0"
+ line: str = "#B0B0B0"
+ line_highlight: str = "#808080"
+ line_faint: str = "#E0E0E0"
+ text: str = "#202020"
+ text_faint: str = "#B0B0B0"
+
+
+@dataclass
+class DfGraph:
+ nodes: pl.DataFrame
+ arcs: pl.DataFrame
+
+
+@dataclass
+class EdgeFigData:
+ x: list[int | float | None]
+ y: list[int | float | None]
+
+
+@dataclass
+class NodeFigData:
+ x: list[int | float | None]
+ y: list[int | float | None]
+ color: list[str]
+ label: list[str]
+ label_color: list[str]
+ hover_text: list[str]
+ outline_color: list[str]
+ custom_data: list[str]
+
+
def layout():
return html.Div(
className="padded-box vertical-content vertical-content_large-gap",
@@ -140,8 +179,8 @@ def parse_cargo_lock(contents: str) -> pl.DataFrame:
return pl.DataFrame(toml.get("package", []))
-def files_into_graph(cargo_toml: pl.DataFrame, cargo_lock: pl.DataFrame) -> dict[str, pl.DataFrame]:
- graph_arcs = (
+def files_into_graph(cargo_toml: pl.DataFrame, cargo_lock: pl.DataFrame) -> DfGraph:
+ arcs = (
(
cargo_lock.select(
(pl.col("name") + " " + pl.col("version")).alias("source"),
@@ -187,7 +226,7 @@ def files_into_graph(cargo_toml: pl.DataFrame, cargo_lock: pl.DataFrame) -> dict
)
)
- graph_nodes = cargo_lock.join(
+ nodes = cargo_lock.join(
cargo_toml.select(
pl.col("name"),
pl.lit(True).alias("explicit"),
@@ -207,18 +246,18 @@ def files_into_graph(cargo_toml: pl.DataFrame, cargo_lock: pl.DataFrame) -> dict
).alias("full_details"),
)
- return dict(
- nodes=graph_nodes,
- arcs=graph_arcs,
+ return DfGraph(
+ nodes=nodes,
+ arcs=arcs,
)
-def compute_node_positions(df_graph: dict[str, pl.DataFrame]) -> dict[str, tuple[float, float]]:
- def compute_graph(df_graph: dict[str, pl.DataFrame]) -> pydot.Dot:
+def compute_node_positions(df_graph: DfGraph) -> dict[str, tuple[int | float, int | float]]:
+ def compute_graph(df_graph: DfGraph) -> pydot.Dot:
graph = pydot.Dot(graph_type="digraph", rankdir="LR")
- for row in df_graph["nodes"].iter_rows(named=True):
+ for row in df_graph.nodes.iter_rows(named=True):
graph.add_node(pydot.Node(name=str(row["node_id"])))
- for row in df_graph["arcs"].iter_rows(named=True):
+ for row in df_graph.arcs.iter_rows(named=True):
if row["target"] is None:
continue
graph.add_edge(pydot.Edge(str(row["source"]), str(row["target"])))
@@ -243,102 +282,150 @@ def compute_node_positions(df_graph: dict[str, pl.DataFrame]) -> dict[str, tuple
return extract_positions(compute_graph(df_graph))
-def draw_graph_figure(
- df_graph: dict[str, pl.DataFrame],
- positions: dict[str, tuple[float, float]],
- selected_node=None,
-) -> go.Figure:
- adjacency_list = {node: set() for node in df_graph["nodes"]["node_id"]}
+def compute_adjacency_list(df_graph: DfGraph) -> dict[str, set[str]]:
+ adjacency_list = {node: set() for node in df_graph.nodes["node_id"]}
- for row in df_graph["arcs"].iter_rows(named=True):
+ for row in df_graph.arcs.iter_rows(named=True):
if not row["target"]:
continue
source, target = str(row["source"]), str(row["target"])
adjacency_list[source].add(target)
adjacency_list[target].add(source)
+ return adjacency_list
+
+
+def draw_graph_figure(
+ df_graph: DfGraph,
+ positions: dict[str, tuple[float, float]],
+ selected_node=None,
+) -> go.Figure:
+ adjacency_list = compute_adjacency_list(df_graph)
+
highlight_nodes = set()
if selected_node and selected_node in adjacency_list:
highlight_nodes.add(selected_node)
highlight_nodes.update(adjacency_list[selected_node])
- edge_x = []
- edge_y = []
- edge_x_highlight = []
- edge_y_highlight = []
- for row in df_graph["arcs"].iter_rows(named=True):
+ edges = EdgeFigData(
+ x=[],
+ y=[],
+ )
+
+ edges_highlight = EdgeFigData(
+ x=[],
+ y=[],
+ )
+
+ for row in df_graph.arcs.iter_rows(named=True):
if not row["target"]:
continue
source, target = str(row["source"]), str(row["target"])
if source in positions and target in positions:
- x0, y0 = positions[source]
- x1, y1 = positions[target]
+ source_x, source_y = positions[source]
+ target_x, target_y = positions[target]
if (
source in highlight_nodes
and target in highlight_nodes
and (target == selected_node or source == selected_node)
):
- edge_x_highlight.extend([x0, x1, None])
- edge_y_highlight.extend([y0, y1, None])
+ edges_highlight.x.extend([source_x, target_x, None])
+ edges_highlight.y.extend([source_y, target_y, None])
else:
- edge_x.extend([x0, x1, None])
- edge_y.extend([y0, y1, None])
-
- edge_trace = (
- go.Scatter(x=edge_x, y=edge_y, line=dict(width=1.0, color="#A0A0A0"), hoverinfo="skip", mode="lines")
- if not selected_node
- else go.Scatter(x=edge_x, y=edge_y, line=dict(width=1.0, color="#D0D0D0"), hoverinfo="skip", mode="lines")
+ edges.x.extend([source_x, target_x, None])
+ edges.y.extend([source_y, target_y, None])
+
+ def build_line_scatter(edge: EdgeFigData, width: float, color: str) -> go.Scatter:
+ return go.Scatter(x=edge.x, y=edge.y, line=dict(width=width, color=color), hoverinfo="skip", mode="lines")
+
+ edge_trace = build_line_scatter(edges, width=1.0, color=colors.line_faint if selected_node else colors.line)
+ edge_trace_highlight = build_line_scatter(edges_highlight, width=1.5, color=colors.line_highlight)
+
+ nodes = NodeFigData(
+ x=[],
+ y=[],
+ color=[],
+ label=[],
+ label_color=[],
+ hover_text=[],
+ outline_color=[],
+ custom_data=[],
)
- edge_trace_highlight = go.Scatter(
- x=edge_x_highlight, y=edge_y_highlight, line=dict(width=1.5, color="#8080F0"), hoverinfo="skip", mode="lines"
+
+ nodes_highlight = NodeFigData(
+ x=[],
+ y=[],
+ color=[],
+ label=[],
+ label_color=[],
+ hover_text=[],
+ outline_color=[],
+ custom_data=[],
)
- node_x = []
- node_y = []
- short_labels = []
- hover_details = []
- marker_colors = []
- text_colors = []
- custom_data = []
- for row in df_graph["nodes"].iter_rows(named=True):
+ for row in df_graph.nodes.iter_rows(named=True):
node_id = row["node_id"]
- if node_id in positions:
- x, y = positions[node_id]
- node_x.append(x)
- node_y.append(y)
- short_labels.append(row["short_label"])
- hover_details.append(parse_node_details(row["full_details"]))
- custom_data.append(node_id)
-
- base_color = "#F0A0A0" if row["full_details"]["explicit"] else "#A0A0F0"
- if not selected_node:
- marker_colors.append(base_color)
- text_colors.append("#303030")
- elif node_id in highlight_nodes:
- marker_colors.append(base_color if node_id != selected_node else "#A0F0A0")
- text_colors.append("#000000")
- else:
- marker_colors.append("#E0E0E0")
- text_colors.append("#A0A0A0")
-
- node_trace = go.Scatter(
- x=node_x,
- y=node_y,
- mode="markers+text",
- text=short_labels,
- textposition="middle left",
- hovertext=hover_details,
- hoverinfo="text",
- customdata=custom_data,
- marker=dict(showscale=False, color=marker_colors, size=16, line=dict(width=1, color="white")),
- textfont=dict(size=10, color=text_colors),
- )
+
+ if node_id not in positions:
+ continue
+
+ nodes_appending = nodes if (not selected_node) or (node_id not in highlight_nodes) else nodes_highlight
+
+ x, y = positions[node_id]
+ nodes_appending.x.append(x)
+ nodes_appending.y.append(y)
+ nodes_appending.label.append(row["short_label"])
+ nodes_appending.hover_text.append(parse_node_details(row["full_details"]))
+ nodes_appending.custom_data.append(node_id)
+
+ if (not selected_node) or (node_id in highlight_nodes):
+ nodes_appending.color.append(colors.node_main if row["full_details"]["explicit"] else colors.node)
+ nodes_appending.label_color.append(colors.text)
+ nodes_appending.outline_color.append(
+ colors.outline_highlight if node_id == selected_node else colors.outline
+ )
+ else:
+ nodes_appending.color.append(colors.node_faint)
+ nodes_appending.label_color.append(colors.text_faint)
+ nodes_appending.outline_color.append(colors.outline)
+
+ def build_node_scatter(node: NodeFigData) -> go.Scatter:
+ return go.Scatter(
+ x=node.x,
+ y=node.y,
+ text=node.label,
+ hovertext=node.hover_text,
+ customdata=node.custom_data,
+ marker=dict(
+ showscale=False,
+ color=node.color,
+ size=16,
+ line=dict(width=1, color=node.outline_color),
+ ),
+ textfont=dict(
+ size=10,
+ color=node.label_color,
+ shadow=(
+ f"-1px -1px {colors.outline},"
+ f"1px -1px {colors.outline},"
+ f"-1px 1px {colors.outline},"
+ f"1px 1px {colors.outline}"
+ ),
+ ),
+ mode="markers+text",
+ textposition="middle left",
+ hoverinfo="text",
+ )
+
+ node_trace = build_node_scatter(nodes)
+ node_trace_highlight = build_node_scatter(nodes_highlight)
fig = go.Figure(
data=[
edge_trace,
- edge_trace_highlight,
node_trace,
+ edge_trace_highlight,
+ node_trace_highlight,
],
layout=go.Layout(
showlegend=False,
@@ -346,7 +433,7 @@ def draw_graph_figure(
margin=dict(b=20, l=20, r=20, t=20),
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
- plot_bgcolor="#FAFAFA",
+ plot_bgcolor=colors.background,
),
)
@@ -495,12 +582,12 @@ def visualize(inputs, state):
positions = compute_node_positions(df_graph)
cache = {
"positions": positions,
- "df_graph_nodes": df_graph["nodes"].serialize(format="json"),
- "df_graph_arcs": df_graph["arcs"].serialize(format="json"),
+ "df_graph_nodes": df_graph.nodes.serialize(format="json"),
+ "df_graph_arcs": df_graph.arcs.serialize(format="json"),
}
else:
positions = state["cache"]["positions"]
- df_graph = dict(
+ df_graph = DfGraph(
nodes=pl.DataFrame.deserialize(io.StringIO(state["cache"]["df_graph_nodes"]), format="json"),
arcs=pl.DataFrame.deserialize(io.StringIO(state["cache"]["df_graph_arcs"]), format="json"),
)
@@ -509,7 +596,7 @@ def visualize(inputs, state):
details = [
item
for part in (
- parse_node_details(df_graph["nodes"].filter(pl.col("node_id") == selected).to_dicts()[0]["full_details"])
+ parse_node_details(df_graph.nodes.filter(pl.col("node_id") == selected).to_dicts()[0]["full_details"])
if selected
else ""
).split("<br>")