rsdeps

Cargo.lock visualizer (mirror)
Log | Files | Refs | README | LICENSE

commit 0f920d3883ed1b8420df0916e9e37fead20c999a
parent 7a6c7c0397c6ea6ffa5ef8f2aa3d7194b93999ab
Author: Andy Khramtsov <>
Date:   Fri, 29 May 2026 23:21:11 +0300

feat: improve the looks

Diffstat:
Msrc/deps/pages/home.py | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
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>")