|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | stem-basic: Basic Stem Plot |
3 | | -Library: altair 6.0.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: altair 6.1.0 | Python 3.13.13 |
| 4 | +Quality: 89/100 | Updated: 2026-04-30 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import altair as alt |
8 | 10 | import numpy as np |
9 | 11 | import pandas as pd |
10 | 12 |
|
11 | 13 |
|
12 | | -# Data - Discrete signal samples (simulating a damped oscillation) |
| 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 | +BRAND = "#009E73" # Okabe-Ito position 1 |
| 21 | + |
| 22 | +# Data — acoustic impulse response samples |
13 | 23 | np.random.seed(42) |
14 | | -x = np.arange(0, 30) |
15 | | -y = np.exp(-x / 10) * np.cos(x * 0.8) + np.random.randn(30) * 0.05 |
| 24 | +n_samples = 30 |
| 25 | +sample_index = np.arange(n_samples) |
| 26 | +amplitude = np.exp(-sample_index / 8) * np.cos(sample_index * 0.9) + np.random.randn(n_samples) * 0.03 |
| 27 | + |
| 28 | +df = pd.DataFrame({"n": sample_index, "amplitude": amplitude, "baseline": 0.0}) |
| 29 | + |
| 30 | +# Decay envelope — highlights the exponential decay story |
| 31 | +n_env = np.linspace(0, n_samples - 1, 200) |
| 32 | +env_df = pd.DataFrame({"n": n_env, "upper": np.exp(-n_env / 8), "lower": -np.exp(-n_env / 8)}) |
16 | 33 |
|
17 | | -df = pd.DataFrame({"x": x, "y": y, "y0": 0}) |
| 34 | +# Shaded decay region (subtle background emphasis) |
| 35 | +envelope_area = ( |
| 36 | + alt.Chart(env_df) |
| 37 | + .mark_area(color=BRAND, opacity=0.07) |
| 38 | + .encode(x=alt.X("n:Q"), y=alt.Y("upper:Q"), y2=alt.Y2("lower:Q")) |
| 39 | +) |
| 40 | + |
| 41 | +# Dashed bounds of the decay envelope |
| 42 | +envelope_upper = ( |
| 43 | + alt.Chart(env_df) |
| 44 | + .mark_line(color=INK_SOFT, strokeWidth=1.5, strokeDash=[5, 4], opacity=0.45) |
| 45 | + .encode(x=alt.X("n:Q"), y=alt.Y("upper:Q")) |
| 46 | +) |
18 | 47 |
|
19 | | -# Create stems (vertical lines from baseline y=0 to each data point) |
| 48 | +envelope_lower = ( |
| 49 | + alt.Chart(env_df) |
| 50 | + .mark_line(color=INK_SOFT, strokeWidth=1.5, strokeDash=[5, 4], opacity=0.45) |
| 51 | + .encode(x=alt.X("n:Q"), y=alt.Y("lower:Q")) |
| 52 | +) |
| 53 | + |
| 54 | +# Baseline rule at y=0 |
| 55 | +baseline_rule = alt.Chart(pd.DataFrame({"y": [0]})).mark_rule(color=INK_SOFT, strokeWidth=1.5).encode(y=alt.Y("y:Q")) |
| 56 | + |
| 57 | +# Stems: vertical rules from baseline to each data point |
20 | 58 | stems = ( |
21 | 59 | alt.Chart(df) |
22 | | - .mark_rule(color="#306998", strokeWidth=2.5, opacity=0.8) |
| 60 | + .mark_rule(color=BRAND, strokeWidth=2.5, opacity=0.85) |
23 | 61 | .encode( |
24 | | - x=alt.X("x:Q", title="Sample Index", axis=alt.Axis(labelFontSize=18, titleFontSize=22)), |
25 | | - y=alt.Y("y0:Q"), |
26 | | - y2=alt.Y2("y:Q"), |
| 62 | + x=alt.X("n:Q", title="Sample Index (n)", axis=alt.Axis(labelFontSize=18, titleFontSize=22)), |
| 63 | + y=alt.Y("baseline:Q"), |
| 64 | + y2=alt.Y2("amplitude:Q"), |
27 | 65 | ) |
28 | 66 | ) |
29 | 67 |
|
30 | | -# Create markers at the top of each stem |
| 68 | +# Markers at the tip of each stem |
31 | 69 | markers = ( |
32 | 70 | alt.Chart(df) |
33 | | - .mark_circle(color="#306998", size=300, stroke="white", strokeWidth=2) |
| 71 | + .mark_circle(color=BRAND, size=300, stroke=PAGE_BG, strokeWidth=2) |
34 | 72 | .encode( |
35 | | - x=alt.X("x:Q"), |
36 | | - y=alt.Y("y:Q", title="Amplitude", axis=alt.Axis(labelFontSize=18, titleFontSize=22)), |
37 | | - tooltip=[alt.Tooltip("x:Q", title="Sample"), alt.Tooltip("y:Q", title="Amplitude", format=".3f")], |
| 73 | + x=alt.X("n:Q"), |
| 74 | + y=alt.Y("amplitude:Q", title="Amplitude (a.u.)", axis=alt.Axis(labelFontSize=18, titleFontSize=22)), |
| 75 | + tooltip=[ |
| 76 | + alt.Tooltip("n:Q", title="Sample (n)"), |
| 77 | + alt.Tooltip("amplitude:Q", title="Amplitude (a.u.)", format=".3f"), |
| 78 | + ], |
38 | 79 | ) |
39 | 80 | ) |
40 | 81 |
|
41 | | -# Create baseline at y=0 |
42 | | -baseline = alt.Chart(pd.DataFrame({"y": [0]})).mark_rule(color="#333333", strokeWidth=2).encode(y=alt.Y("y:Q")) |
43 | | - |
44 | | -# Combine stems, markers, and baseline |
| 82 | +# Compose and apply theme-adaptive chrome |
45 | 83 | chart = ( |
46 | | - (baseline + stems + markers) |
| 84 | + (envelope_area + envelope_upper + envelope_lower + baseline_rule + stems + markers) |
47 | 85 | .properties( |
48 | | - width=1600, height=900, title=alt.Title("stem-basic · altair · pyplots.ai", fontSize=28, anchor="middle") |
| 86 | + width=1600, |
| 87 | + height=900, |
| 88 | + background=PAGE_BG, |
| 89 | + title=alt.Title("stem-basic · altair · anyplot.ai", fontSize=28, anchor="middle", color=INK), |
| 90 | + ) |
| 91 | + .configure_view(fill=PAGE_BG, stroke=None) |
| 92 | + .configure_axis( |
| 93 | + domainColor=INK_SOFT, |
| 94 | + tickColor=INK_SOFT, |
| 95 | + gridColor=INK, |
| 96 | + gridOpacity=0.10, |
| 97 | + labelColor=INK_SOFT, |
| 98 | + titleColor=INK, |
| 99 | + labelFontSize=18, |
| 100 | + titleFontSize=22, |
49 | 101 | ) |
50 | | - .configure_axis(labelFontSize=18, titleFontSize=22, grid=True, gridOpacity=0.3, gridDash=[4, 4]) |
51 | | - .configure_view(strokeWidth=0) |
| 102 | + .configure_axisX(grid=False) |
| 103 | + .configure_title(color=INK, fontSize=28) |
52 | 104 | ) |
53 | 105 |
|
54 | | -# Save as PNG (scale_factor=3 to get 4800x2700) |
55 | | -chart.save("plot.png", scale_factor=3.0) |
56 | | - |
57 | | -# Save as HTML for interactive version |
58 | | -chart.save("plot.html") |
| 106 | +# Save |
| 107 | +chart.save(f"plot-{THEME}.png", scale_factor=3.0) |
| 108 | +chart.save(f"plot-{THEME}.html") |
0 commit comments