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
169 changes: 97 additions & 72 deletions plots/scatter-connected-temporal/implementations/python/letsplot.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,58 @@
""" pyplots.ai
""" anyplot.ai
scatter-connected-temporal: Connected Scatter Plot with Temporal Path
Library: letsplot 4.9.0 | Python 3.14.3
Quality: 90/100 | Created: 2026-03-13
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 88/100 | Updated: 2026-06-09
"""

import os

import numpy as np
import pandas as pd
from lets_plot import * # noqa: F403
from lets_plot.export import ggsave as export_ggsave
from lets_plot import (
LetsPlot,
aes,
arrow,
element_blank,
element_line,
element_rect,
element_text,
geom_path,
geom_point,
geom_segment,
geom_text,
ggplot,
ggsave,
ggsize,
labs,
layer_tooltips,
scale_color_gradient,
scale_fill_gradient,
scale_x_continuous,
scale_y_continuous,
theme,
theme_minimal,
)


LetsPlot.setup_html()

LetsPlot.setup_html() # noqa: F405
# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
GRID = "rgba(26,26,23,0.12)" if THEME == "light" else "rgba(240,239,232,0.12)"

# Data — Unemployment rate vs inflation rate (Phillips curve), 1990-2023
# Imprint sequential colormap: brand green (early) → blue (recent)
SEQ_LOW = "#009E73" # Imprint position 1 — start of temporal path
SEQ_HIGH = "#4467A3" # Imprint position 3 — end of temporal path

# Data — Phillips curve dynamics, unemployment vs inflation 1990–2023
np.random.seed(42)
years = np.arange(1990, 2024)
n = len(years)

# Simulate realistic Phillips curve dynamics with regime shifts
unemployment = np.concatenate(
[
np.linspace(5.6, 4.0, 10) + np.random.randn(10) * 0.3, # 1990s decline
Expand Down Expand Up @@ -47,14 +82,10 @@
}
)

# Color gradient: map time index to a normalized value for color encoding
df["time_norm"] = df["time_idx"] / (n - 1)

# Label only key years — reduced set to avoid crowding in dense areas
# Annotate key economic turning points
key_years = {1990, 2000, 2007, 2009, 2020, 2023}
df_labels = df[df["year"].isin(key_years)].copy()

# Per-label offset to prevent overlap and clipping
nudge_map = {
1990: (0.35, 0.7),
2000: (0.35, -0.7),
Expand All @@ -64,96 +95,90 @@
2023: (0.35, 0.7),
}

# Highlight start and end points with larger markers
df_endpoints = df[df["year"].isin([1990, 2023])].copy()
df_labels["label_x"] = df_labels.apply(lambda r: r["unemployment"] + nudge_map.get(r["year"], (0, 0))[0], axis=1)
df_labels["label_y"] = df_labels.apply(lambda r: r["inflation"] + nudge_map.get(r["year"], (0, 0))[1], axis=1)

# Arrow segment at end of path to show time direction
# Direction arrow at terminal segment of the path
last = df.iloc[-1]
prev = df.iloc[-2]

arrow_df = pd.DataFrame(
{"x": [prev["unemployment"]], "y": [prev["inflation"]], "xend": [last["unemployment"]], "yend": [last["inflation"]]}
)

title = "scatter-connected-temporal · python · letsplot · anyplot.ai"

# Plot
plot = (
ggplot(df, aes(x="unemployment", y="inflation")) # noqa: F405
+ geom_path( # noqa: F405
aes(color="time_idx"), # noqa: F405
size=1.8,
alpha=0.7,
tooltips="none",
)
+ geom_segment( # noqa: F405
ggplot(df, aes(x="unemployment", y="inflation"))
+ geom_path(aes(color="time_idx"), size=1.5, alpha=0.75, tooltips="none")
+ geom_segment(
data=arrow_df,
mapping=aes(x="x", y="y", xend="xend", yend="yend"), # noqa: F405
color="#1a3a5c",
size=2.5,
arrow=arrow(angle=25, length=12, type="closed"), # noqa: F405
mapping=aes(x="x", y="y", xend="xend", yend="yend"),
color=SEQ_HIGH,
size=2.2,
arrow=arrow(angle=25, length=10, type="closed"),
)
+ geom_point( # noqa: F405
aes(fill="time_idx"), # noqa: F405
color="white",
size=7,
stroke=1.2,
+ geom_point(
aes(fill="time_idx"),
color=PAGE_BG,
size=3.5,
stroke=1.0,
shape=21,
alpha=0.85,
tooltips=layer_tooltips() # noqa: F405
alpha=0.9,
tooltips=layer_tooltips()
.line("Year|@year")
.line("Unemployment|@{unemployment}{.1f}%")
.line("Inflation|@{inflation}{.1f}%"),
)
+ geom_point( # noqa: F405
+ geom_point(
data=df_endpoints,
mapping=aes(x="unemployment", y="inflation", fill="time_idx"), # noqa: F405
color="#1a1a1a",
size=11,
stroke=2.0,
mapping=aes(x="unemployment", y="inflation", fill="time_idx"),
color=INK,
size=6.0,
stroke=1.8,
shape=21,
alpha=1.0,
)
+ geom_text( # noqa: F405
+ geom_text(
data=df_labels,
mapping=aes(x="label_x", y="label_y", label="year_label"), # noqa: F405
size=13,
color="#222222",
mapping=aes(x="label_x", y="label_y", label="year_label"),
size=6,
color=INK,
family="monospace",
fontface="bold",
)
+ scale_color_gradient( # noqa: F405
low="#a8d5e2", high="#1a3a5c", name="Year", breaks=[0, (n - 1) / 2, n - 1], labels=["1990", "2006", "2023"]
+ scale_color_gradient(
low=SEQ_LOW, high=SEQ_HIGH, name="Year", breaks=[0, (n - 1) / 2, n - 1], labels=["1990", "2006", "2023"]
)
+ scale_fill_gradient( # noqa: F405
low="#a8d5e2", high="#1a3a5c", guide="none"
)
+ scale_x_continuous(expand=[0.06, 0]) # noqa: F405
+ scale_y_continuous(expand=[0.08, 0]) # noqa: F405
+ labs( # noqa: F405
+ scale_fill_gradient(low=SEQ_LOW, high=SEQ_HIGH, guide="none")
+ scale_x_continuous(expand=[0.06, 0])
+ scale_y_continuous(expand=[0.08, 0])
+ labs(
x="Unemployment Rate (%)",
y="Inflation Rate (%)",
title="scatter-connected-temporal · letsplot · pyplots.ai",
subtitle="Phillips Curve Dynamics — US-style unemployment vs inflation, 1990-2023",
title=title,
subtitle="Phillips Curve: unemployment vs inflation, 19902023",
)
+ ggsize(1600, 900) # noqa: F405
+ theme_minimal() # noqa: F405
+ theme( # noqa: F405
axis_text=element_text(size=16, color="#555555"), # noqa: F405
axis_title=element_text(size=20, color="#333333"), # noqa: F405
plot_title=element_text(size=24, color="#1a1a1a", face="bold"), # noqa: F405
plot_subtitle=element_text(size=16, color="#555555"), # noqa: F405
legend_text=element_text(size=14), # noqa: F405
legend_title=element_text(size=16, face="bold"), # noqa: F405
panel_grid_major=element_line(color="#E0E0E0", size=0.3), # noqa: F405
panel_grid_minor=element_blank(), # noqa: F405
plot_background=element_rect(fill="#FAFBFC"), # noqa: F405
plot_margin=[60, 50, 20, 20],
+ ggsize(800, 450)
+ theme_minimal()
+ theme(
axis_text=element_text(size=10, color=INK_SOFT),
axis_title=element_text(size=12, color=INK),
plot_title=element_text(size=16, color=INK),
plot_subtitle=element_text(size=10, color=INK_SOFT),
legend_text=element_text(size=10, color=INK_SOFT),
legend_title=element_text(size=12, color=INK),
panel_grid_major=element_line(color=GRID, size=0.3),
panel_grid_minor=element_blank(),
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
panel_background=element_rect(fill=PAGE_BG),
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
panel_border=element_blank(),
plot_margin=[20, 20, 10, 10],
)
)

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

# Save HTML for interactive version
export_ggsave(plot, filename="plot.html", path=".")
# Save — PNG at 3200×1800 (800×450 × scale=4), plus interactive HTML
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")
Loading
Loading