Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 125 additions & 30 deletions plots/arc-basic/implementations/letsplot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
""" pyplots.ai
arc-basic: Basic Arc Diagram
Library: letsplot 4.8.2 | Python 3.13.11
Quality: 92/100 | Created: 2025-12-23
Library: letsplot 4.8.2 | Python 3.14.3
Quality: 89/100 | Updated: 2026-02-23
"""

import numpy as np
Expand All @@ -10,13 +10,18 @@
LetsPlot,
aes,
element_blank,
element_rect,
element_text,
geom_path,
geom_point,
geom_segment,
geom_text,
ggplot,
ggsize,
labs,
layer_tooltips,
scale_alpha_identity,
scale_color_identity,
scale_size_identity,
theme,
xlim,
Expand All @@ -28,14 +33,12 @@
LetsPlot.setup_html()

# Data: Character interactions in a story chapter
np.random.seed(42)

nodes = ["Alice", "Bob", "Carol", "David", "Eve", "Frank", "Grace", "Henry", "Iris", "Jack"]
n_nodes = len(nodes)

# Edges: pairs of connected nodes with weights (source, target, weight)
edges = [
(0, 1, 3), # Alice-Bob (strong connection)
(0, 1, 3), # Alice-Bob (strong)
(0, 3, 2), # Alice-David
(1, 2, 2), # Bob-Carol
(2, 4, 1), # Carol-Eve
Expand All @@ -52,9 +55,20 @@
(8, 9, 2), # Iris-Jack
]

# Node positions along x-axis
x_positions = np.linspace(0, 1, n_nodes)
y_baseline = 0.1
# Node positions along x-axis — wider spacing for label readability
x_positions = np.linspace(0, 1.3, n_nodes)
y_baseline = 0.06

# Count connections per node for visual hierarchy
connections = [0] * n_nodes
for s, t, w in edges:
connections[s] += w
connections[t] += w

# Arc color intensity by weight
weight_colors = {1: "#6A9BB5", 2: "#306998", 3: "#1A3A5C"}
weight_alphas = {1: 0.7, 2: 0.75, 3: 0.9}
weight_labels = {1: "Weak", 2: "Moderate", 3: "Strong"}

# Create arc data for geom_path
arc_data = []
Expand All @@ -66,57 +80,138 @@
distance = abs(end - start)
height = 0.08 * distance

# Generate points along the arc (semi-circle)
# Generate points along the arc
n_points = 50
t = np.linspace(0, np.pi, n_points)
arc_x = x_start + (x_end - x_start) * (1 - np.cos(t)) / 2
arc_y = y_baseline + height * np.sin(t)

# Line width based on weight
line_size = 1.5 + weight * 1.0
line_size = 1.0 + weight * 1.2
color = weight_colors[weight]
alpha = weight_alphas[weight]
tooltip_text = f"{nodes[start]} \u2194 {nodes[end]}"
strength = weight_labels[weight]

for i in range(n_points):
arc_data.append({"x": arc_x[i], "y": arc_y[i], "edge_id": edge_id, "weight": weight, "size": line_size})
arc_data.append(
{
"x": arc_x[i],
"y": arc_y[i],
"edge_id": edge_id,
"size": line_size,
"color": color,
"alpha": alpha,
"connection": tooltip_text,
"strength": strength,
}
)

arc_df = pd.DataFrame(arc_data)

# Node data
node_df = pd.DataFrame({"x": x_positions, "y": [y_baseline] * n_nodes, "name": nodes})
# Node data with size based on total connection weight (higher floor for peripheral nodes)
max_conn = max(connections)
node_sizes = [9 + 7 * (c / max_conn) for c in connections]
node_df = pd.DataFrame(
{"x": x_positions, "y": [y_baseline] * n_nodes, "name": nodes, "node_size": node_sizes, "connections": connections}
)

# Baseline segment data
baseline_df = pd.DataFrame({"x": [x_positions[0]], "xend": [x_positions[-1]], "y": [y_baseline], "yend": [y_baseline]})

# Label data (positioned below nodes)
label_df = pd.DataFrame({"x": x_positions, "y": [y_baseline - 0.05] * n_nodes, "name": nodes})
label_df = pd.DataFrame({"x": x_positions, "y": [y_baseline - 0.04] * n_nodes, "name": nodes})

# Legend data: small line segments showing weight encoding
legend_x = 1.1
legend_y_start = 0.72
legend_spacing = 0.06
legend_line_len = 0.07
legend_lines = pd.DataFrame(
{
"x": [legend_x] * 3,
"xend": [legend_x + legend_line_len] * 3,
"y": [legend_y_start - i * legend_spacing for i in range(3)],
"yend": [legend_y_start - i * legend_spacing for i in range(3)],
"color": [weight_colors[3], weight_colors[2], weight_colors[1]],
"size": [1.0 + 3 * 1.2, 1.0 + 2 * 1.2, 1.0 + 1 * 1.2],
"alpha": [weight_alphas[3], weight_alphas[2], weight_alphas[1]],
}
)
legend_text_df = pd.DataFrame(
{
"x": [legend_x + legend_line_len + 0.015] * 3,
"y": [legend_y_start - i * legend_spacing for i in range(3)],
"label": ["Strong (3)", "Moderate (2)", "Weak (1)"],
}
)
legend_title_df = pd.DataFrame({"x": [legend_x], "y": [legend_y_start + 0.05], "label": ["Connection Strength"]})

# Create plot
# Plot
plot = (
ggplot()
# Draw arcs with semi-transparency for overlapping connections
+ geom_path(data=arc_df, mapping=aes(x="x", y="y", group="edge_id", size="size"), color="#306998", alpha=0.6)
# Subtle baseline
+ geom_segment(data=baseline_df, mapping=aes(x="x", y="y", xend="xend", yend="yend"), color="#D0D8E0", size=0.8)
# Arcs with weight-based color, transparency, and tooltips
+ geom_path(
data=arc_df,
mapping=aes(x="x", y="y", group="edge_id", size="size", color="color", alpha="alpha"),
tooltips=layer_tooltips().title("@connection").line("Strength|@strength"),
)
+ scale_size_identity()
# Draw nodes
+ geom_point(data=node_df, mapping=aes(x="x", y="y"), size=10, color="#FFD43B", fill="#FFD43B", stroke=2, shape=21)
# Add node labels
+ scale_color_identity()
+ scale_alpha_identity()
# Nodes sized by connection weight with tooltips
+ geom_point(
data=node_df,
mapping=aes(x="x", y="y", size="node_size"),
color="#1A3A5C",
fill="#FFD43B",
stroke=1.5,
shape=21,
tooltips=layer_tooltips().title("@name").line("Total weight|@connections"),
)
+ scale_size_identity()
# Node labels
+ geom_text(
data=label_df, mapping=aes(x="x", y="y", label="name"), size=14, color="#306998", fontface="bold", vjust=1
data=label_df, mapping=aes(x="x", y="y", label="name"), size=20, color="#1A3A5C", fontface="bold", vjust=1
)
# Weight legend
+ geom_segment(
data=legend_lines,
mapping=aes(x="x", y="y", xend="xend", yend="yend", color="color", size="size", alpha="alpha"),
tooltips="none",
)
+ geom_text(data=legend_text_df, mapping=aes(x="x", y="y", label="label"), size=16, color="#1A3A5C", hjust=0)
+ geom_text(
data=legend_title_df,
mapping=aes(x="x", y="y", label="label"),
size=16,
color="#1A3A5C",
fontface="bold",
hjust=0,
)
# Styling
+ xlim(-0.05, 1.05)
+ ylim(-0.15, 0.85)
+ labs(title="Character Interactions · arc-basic · letsplot · pyplots.ai")
+ xlim(-0.05, 1.48)
+ ylim(-0.01, 0.82)
+ labs(
title="arc-basic \u00b7 letsplot \u00b7 pyplots.ai",
subtitle="Character interactions in a story chapter \u2014 node size reflects connection strength",
)
+ theme(
axis_title=element_blank(),
axis_text=element_blank(),
axis_ticks=element_blank(),
axis_line=element_blank(),
panel_grid=element_blank(),
panel_background=element_blank(),
plot_title=element_text(size=24, face="bold"),
panel_background=element_rect(fill="white", color="white"),
plot_background=element_rect(fill="white", color="white"),
plot_title=element_text(size=24, face="bold", color="#1A3A5C"),
plot_subtitle=element_text(size=16, color="#4A6B82"),
legend_position="none",
)
+ ggsize(1600, 900)
)

# Save as PNG (scale 3x to get 4800 x 2700 px)
# Save
ggsave(plot, "plot.png", path=".", scale=3)

# Save as HTML for interactive viewing
ggsave(plot, "plot.html", path=".")
Loading