Skip to content
126 changes: 84 additions & 42 deletions plots/quiver-basic/implementations/python/plotly.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,131 @@
""" anyplot.ai
quiver-basic: Basic Quiver Plot
Library: plotly 6.7.0 | Python 3.13.13
Quality: 80/100 | Updated: 2026-04-29
Library: plotly 6.9.0 | Python 3.13.14
Quality: 85/100 | Updated: 2026-07-24
"""

import numpy as np
import plotly.figure_factory as ff
import plotly.graph_objects as go
import os
import sys


# Data - create a 15x15 grid with circular rotation pattern (u = -y, v = x)
# Prevent this file from shadowing the installed plotly package
_this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir]

import numpy as np # noqa: E402
import plotly.figure_factory as ff # noqa: E402
import plotly.graph_objects as go # noqa: E402


THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette — brand green is always the first (and here, only) series color
BRAND = "#009E73"
# Continuous magnitude is single-polarity (0 -> max), so the sequential Imprint cmap applies
imprint_seq = [[0.0, "#009E73"], [1.0, "#4467A3"]]

# Data - cyclonic wind vortex over a 4x4 km coastal grid (u = -y, v = x, m/s)
np.random.seed(42)
x_grid = np.linspace(-2, 2, 15)
y_grid = np.linspace(-2, 2, 15)
x_grid = np.linspace(-2, 2, 13)
y_grid = np.linspace(-2, 2, 13)
X, Y = np.meshgrid(x_grid, y_grid)

# Flatten for plotly
x = X.flatten()
y = Y.flatten()
# Cyclone-strength wind speeds (Beaufort ~5-30 m/s range) via a physical multiplier
WIND_SPEED_SCALE = 10.0
u = -Y.flatten() * WIND_SPEED_SCALE
v = X.flatten() * WIND_SPEED_SCALE

# Circular rotation pattern: u = -y, v = x
u = -Y.flatten()
v = X.flatten()

# Calculate magnitude for coloring
# True wind speed magnitude (0 at the eddy's calm centre, growing outward)
magnitude = np.sqrt(u**2 + v**2)
magnitude[magnitude == 0] = 1 # Avoid division by zero at origin
max_magnitude = magnitude.max()
safe_magnitude = np.where(magnitude == 0, 1e-6, magnitude)

# Normalize vectors for consistent arrow length display
scale_factor = 0.2
u_norm = u / magnitude * scale_factor
v_norm = v / magnitude * scale_factor
# Arrow length scales with relative magnitude (proportional encoding), with a
# small floor so the calm-centre arrows stay visible instead of vanishing
scale_factor = 0.32
min_length_frac = 0.18
length_frac = min_length_frac + (1 - min_length_frac) * (magnitude / max_magnitude)
u_norm = (u / safe_magnitude) * length_frac * scale_factor
v_norm = (v / safe_magnitude) * length_frac * scale_factor

# Create quiver plot using figure_factory
fig = ff.create_quiver(x, y, u_norm, v_norm, scale=1, arrow_scale=0.3, line={"width": 2.5, "color": "#306998"})
fig = ff.create_quiver(x, y, u_norm, v_norm, scale=1, arrow_scale=0.3, line={"width": 2, "color": BRAND})

# Add scatter points at arrow bases for magnitude coloring
# Add scatter points at arrow bases for wind-speed coloring
fig.add_trace(
go.Scatter(
x=x,
y=y,
mode="markers",
marker={
"size": 10,
"size": 6,
"opacity": 0.65,
"color": magnitude,
"colorscale": "Viridis",
"colorscale": imprint_seq,
"colorbar": {
"title": {"text": "Magnitude", "font": {"size": 22}},
"tickfont": {"size": 18},
"thickness": 25,
"title": {"text": "Wind Speed (m/s)", "font": {"size": 12, "color": INK}},
"tickfont": {"size": 10, "color": INK_SOFT},
"thickness": 16,
"outlinewidth": 0,
"len": 0.8,
},
"showscale": True,
},
hovertemplate="x: %{x:.2f}<br>y: %{y:.2f}<br>magnitude: %{marker.color:.2f}<extra></extra>",
hovertemplate="x: %{x:.2f} km<br>y: %{y:.2f} km<br>speed: %{marker.color:.2f} m/s<extra></extra>",
)
)

# Layout for 4800x2700 px
# Title fontsize scales linearly with length off the 67-char / 16px baseline
title = "Cyclonic Wind Vortex · quiver-basic · python · plotly · anyplot.ai"
ratio = 67 / len(title) if len(title) > 67 else 1.0
title_fontsize = max(11, round(16 * ratio))

# Layout for 2400x2400 px (Step 0 canonical square canvas — the vortex is
# radially symmetric with no preferred horizontal axis, and a square canvas
# avoids the wasted lateral space a 1:1 aspect-locked field would leave in a
# 16:9 frame)
fig.update_layout(
title={"text": "Circular Flow Field · quiver-basic · plotly · pyplots.ai", "font": {"size": 32}, "x": 0.5},
autosize=False,
width=600,
height=600,
title={"text": title, "font": {"size": title_fontsize, "color": INK}, "x": 0.5},
xaxis={
"title": {"text": "X Position", "font": {"size": 24}},
"tickfont": {"size": 18},
"title": {"text": "X Position (km)", "font": {"size": 12, "color": INK}},
"tickfont": {"size": 10, "color": INK_SOFT},
"range": [-2.3, 2.3],
"showgrid": True,
"gridcolor": "rgba(0,0,0,0.1)",
"gridcolor": GRID,
"zeroline": True,
"zerolinecolor": "rgba(0,0,0,0.3)",
"zerolinecolor": INK_SOFT,
"linecolor": INK_SOFT,
},
yaxis={
"title": {"text": "Y Position", "font": {"size": 24}},
"tickfont": {"size": 18},
"title": {"text": "Y Position (km)", "font": {"size": 12, "color": INK}},
"tickfont": {"size": 10, "color": INK_SOFT},
"range": [-2.3, 2.3],
"showgrid": True,
"gridcolor": "rgba(0,0,0,0.1)",
"gridcolor": GRID,
"zeroline": True,
"zerolinecolor": "rgba(0,0,0,0.3)",
"zerolinecolor": INK_SOFT,
"linecolor": INK_SOFT,
"scaleanchor": "x",
"scaleratio": 1,
},
template="plotly_white",
paper_bgcolor=PAGE_BG,
plot_bgcolor=PAGE_BG,
font={"color": INK},
showlegend=False,
margin={"l": 80, "r": 130, "t": 100, "b": 80},
margin={"l": 80, "r": 70, "t": 80, "b": 60},
)

# Save as PNG (4800x2700 px)
fig.write_image("plot.png", width=1600, height=900, scale=3)
# Save as PNG (2400x2400 px)
fig.write_image(f"plot-{THEME}.png", width=600, height=600, scale=4)

# Save as HTML for interactivity
fig.write_html("plot.html")
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")
Loading
Loading