Skip to content

Commit db8d4e8

Browse files
feat(altair): implement quiver-basic (#5559)
## Implementation: `quiver-basic` - python/altair Implements the **python/altair** version of `quiver-basic`. **File:** `plots/quiver-basic/implementations/python/altair.py` **Parent Issue:** #1014 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/25137092714)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 9fd330b commit db8d4e8

2 files changed

Lines changed: 238 additions & 198 deletions

File tree

Lines changed: 66 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
quiver-basic: Basic Quiver Plot
3-
Library: altair 6.0.0 | Python 3.13.11
4-
Quality: 91/100 | Created: 2025-12-23
3+
Library: altair 6.1.0 | Python 3.13.13
4+
Quality: 83/100 | Updated: 2026-04-29
55
"""
66

7+
import os
8+
79
import altair as alt
810
import numpy as np
911
import pandas as pd
1012

1113

12-
# Data - Create a 15x15 grid with circular rotation pattern (u = -y, v = x)
14+
# Theme tokens
15+
THEME = os.getenv("ANYPLOT_THEME", "light")
16+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
18+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
19+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
20+
21+
# Data - 15x15 grid with circular rotation field (u = -y, v = x)
1322
np.random.seed(42)
1423
grid_size = 15
1524
x_range = np.linspace(-2, 2, grid_size)
@@ -22,76 +31,52 @@
2231
U = -y_flat
2332
V = x_flat
2433

25-
# Calculate magnitude for color encoding
34+
# Magnitude for color encoding
2635
magnitude = np.sqrt(U**2 + V**2)
2736

28-
# Normalize vectors for consistent arrow length, then scale
37+
# Scale vectors proportionally to magnitude (arrow length encodes magnitude)
2938
scale = 0.12
30-
mag_safe = np.where(magnitude > 0, magnitude, 1)
31-
U_norm = np.where(magnitude > 0, U / mag_safe * scale, 0)
32-
V_norm = np.where(magnitude > 0, V / mag_safe * scale, 0)
39+
U_scaled = U * scale
40+
V_scaled = V * scale
41+
42+
# Arrow tip positions
43+
x2 = x_flat + U_scaled
44+
y2 = y_flat + V_scaled
3345

34-
# Create dataframe with arrow start and end points
35-
df = pd.DataFrame({"x": x_flat, "y": y_flat, "x2": x_flat + U_norm, "y2": y_flat + V_norm, "magnitude": magnitude})
46+
# Arrowhead geometry — size proportional to arrow length for visual coherence
47+
angle = np.arctan2(V_scaled, U_scaled)
48+
arrow_length = magnitude * scale
49+
head_size = np.maximum(arrow_length * 0.30, 0.005)
3650

37-
# Create arrowhead geometry (small triangle at the end of each arrow)
38-
arrow_head_size = 0.04
39-
angle = np.arctan2(V_norm, U_norm)
51+
n = len(x_flat)
52+
ids = np.arange(n)
4053

41-
# Left and right points of arrowhead
42-
df_heads = pd.DataFrame(
54+
# Vectorized construction of shaft + two arrowhead lines per arrow
55+
shaft = pd.DataFrame({"x": x_flat, "y": y_flat, "x2": x2, "y2": y2, "magnitude": magnitude, "arrow_id": ids})
56+
left = pd.DataFrame(
4357
{
44-
"x": df["x2"],
45-
"y": df["y2"],
46-
"x_left": df["x2"] - arrow_head_size * np.cos(angle - 0.4),
47-
"y_left": df["y2"] - arrow_head_size * np.sin(angle - 0.4),
48-
"x_right": df["x2"] - arrow_head_size * np.cos(angle + 0.4),
49-
"y_right": df["y2"] - arrow_head_size * np.sin(angle + 0.4),
58+
"x": x2,
59+
"y": y2,
60+
"x2": x2 - head_size * np.cos(angle - 0.4),
61+
"y2": y2 - head_size * np.sin(angle - 0.4),
5062
"magnitude": magnitude,
63+
"arrow_id": ids,
64+
}
65+
)
66+
right = pd.DataFrame(
67+
{
68+
"x": x2,
69+
"y": y2,
70+
"x2": x2 - head_size * np.cos(angle + 0.4),
71+
"y2": y2 - head_size * np.sin(angle + 0.4),
72+
"magnitude": magnitude,
73+
"arrow_id": ids,
5174
}
5275
)
5376

54-
# Build arrow data for line marks (shaft + two head lines per arrow)
55-
arrow_data = []
56-
for i in range(len(df)):
57-
mag = df.iloc[i]["magnitude"]
58-
# Arrow shaft
59-
arrow_data.append(
60-
{
61-
"x": df.iloc[i]["x"],
62-
"y": df.iloc[i]["y"],
63-
"x2": df.iloc[i]["x2"],
64-
"y2": df.iloc[i]["y2"],
65-
"magnitude": mag,
66-
"arrow_id": i,
67-
}
68-
)
69-
# Left head line
70-
arrow_data.append(
71-
{
72-
"x": df_heads.iloc[i]["x"],
73-
"y": df_heads.iloc[i]["y"],
74-
"x2": df_heads.iloc[i]["x_left"],
75-
"y2": df_heads.iloc[i]["y_left"],
76-
"magnitude": mag,
77-
"arrow_id": i,
78-
}
79-
)
80-
# Right head line
81-
arrow_data.append(
82-
{
83-
"x": df_heads.iloc[i]["x"],
84-
"y": df_heads.iloc[i]["y"],
85-
"x2": df_heads.iloc[i]["x_right"],
86-
"y2": df_heads.iloc[i]["y_right"],
87-
"magnitude": mag,
88-
"arrow_id": i,
89-
}
90-
)
91-
92-
arrow_df = pd.DataFrame(arrow_data)
77+
arrow_df = pd.concat([shaft, left, right], ignore_index=True)
9378

94-
# Create the chart using rule marks for arrows with magnitude-based coloring
79+
# Chart — rule marks encode vectors; viridis colormap encodes magnitude
9580
chart = (
9681
alt.Chart(arrow_df)
9782
.mark_rule(strokeWidth=2.5)
@@ -108,12 +93,25 @@
10893
),
10994
)
11095
.properties(
111-
width=1600, height=900, title=alt.Title("quiver-basic · altair · pyplots.ai", fontSize=28, anchor="middle")
96+
width=1600,
97+
height=900,
98+
background=PAGE_BG,
99+
title=alt.Title("quiver-basic · altair · anyplot.ai", fontSize=28, anchor="middle"),
100+
)
101+
.configure_view(fill=PAGE_BG, stroke=INK_SOFT)
102+
.configure_axis(
103+
labelFontSize=18,
104+
titleFontSize=22,
105+
labelColor=INK_SOFT,
106+
titleColor=INK,
107+
domainColor=INK_SOFT,
108+
tickColor=INK_SOFT,
109+
gridColor=INK,
110+
gridOpacity=0.10,
112111
)
113-
.configure_axis(labelFontSize=18, titleFontSize=22, gridOpacity=0.3)
114-
.configure_view(strokeWidth=0)
112+
.configure_title(color=INK)
113+
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
115114
)
116115

117-
# Save as PNG and HTML
118-
chart.save("plot.png", scale_factor=3.0)
119-
chart.save("plot.html")
116+
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
117+
chart.save(f"plot-{THEME}.html")

0 commit comments

Comments
 (0)