|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | indicator-ema: Exponential Moving Average (EMA) Indicator Chart |
3 | | -Library: pygal 3.1.0 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-11 |
| 3 | +Library: pygal 3.1.0 | Python 3.13.13 |
| 4 | +Quality: 86/100 | Updated: 2026-05-19 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | +import sys |
| 9 | + |
| 10 | + |
| 11 | +# Prevent the local pygal.py from shadowing the installed pygal package |
| 12 | +_here = os.path.dirname(os.path.abspath(__file__)) |
| 13 | +sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _here] |
| 14 | + |
7 | 15 | import numpy as np |
8 | 16 | import pandas as pd |
9 | 17 | import pygal |
10 | 18 | from pygal.style import Style |
11 | 19 |
|
12 | 20 |
|
13 | | -# Data - Generate synthetic stock price data with EMA indicators |
14 | | -np.random.seed(42) |
| 21 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 22 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 23 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 24 | +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
15 | 25 |
|
16 | | -# Create 120 trading days |
17 | | -dates = pd.date_range(start="2024-01-02", periods=120, freq="B") # Business days |
| 26 | +OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442") |
18 | 27 |
|
19 | | -# Generate realistic stock price movement (random walk with drift) |
| 28 | +# Data |
| 29 | +np.random.seed(42) |
| 30 | +dates = pd.date_range(start="2024-01-02", periods=120, freq="B") |
20 | 31 | initial_price = 150.0 |
21 | | -returns = np.random.normal(0.0008, 0.018, 120) # Daily returns with slight upward drift |
| 32 | +returns = np.random.normal(0.0008, 0.018, 120) |
22 | 33 | prices = initial_price * np.cumprod(1 + returns) |
23 | 34 |
|
24 | | - |
25 | | -# Calculate EMAs (exponentially weighted moving average) |
26 | 35 | close = prices |
27 | 36 | ema_12 = pd.Series(close).ewm(span=12, adjust=False).mean().values |
28 | 37 | ema_26 = pd.Series(close).ewm(span=26, adjust=False).mean().values |
29 | 38 |
|
30 | | -# Custom style for large canvas |
| 39 | +# Detect EMA crossover points |
| 40 | +crossover_vals = [None] * len(close) |
| 41 | +for i in range(1, len(ema_12)): |
| 42 | + prev_diff = ema_12[i - 1] - ema_26[i - 1] |
| 43 | + curr_diff = ema_12[i] - ema_26[i] |
| 44 | + if prev_diff * curr_diff < 0: |
| 45 | + crossover_vals[i] = float(close[i]) |
| 46 | + |
| 47 | +# Style |
31 | 48 | custom_style = Style( |
32 | | - background="white", |
33 | | - plot_background="white", |
34 | | - foreground="#333333", |
35 | | - foreground_strong="#333333", |
36 | | - foreground_subtle="#666666", |
37 | | - colors=("#306998", "#FFD43B", "#E74C3C"), # Price in blue, EMA12 in yellow, EMA26 in red |
| 49 | + background=PAGE_BG, |
| 50 | + plot_background=PAGE_BG, |
| 51 | + foreground=INK, |
| 52 | + foreground_strong=INK, |
| 53 | + foreground_subtle=INK_MUTED, |
| 54 | + colors=OKABE_ITO, |
38 | 55 | title_font_size=72, |
39 | 56 | label_font_size=48, |
40 | 57 | major_label_font_size=42, |
|
46 | 63 | opacity_hover=1.0, |
47 | 64 | ) |
48 | 65 |
|
49 | | -# Create line chart |
| 66 | +# Chart |
50 | 67 | chart = pygal.Line( |
51 | 68 | width=4800, |
52 | 69 | height=2700, |
53 | 70 | style=custom_style, |
54 | | - title="indicator-ema · pygal · pyplots.ai", |
| 71 | + title="indicator-ema · python · pygal · anyplot.ai", |
55 | 72 | x_title="Date", |
56 | 73 | y_title="Price ($)", |
57 | | - show_dots=False, # Cleaner line appearance |
58 | | - show_x_guides=False, |
| 74 | + show_dots=False, |
| 75 | + show_x_guides=True, |
59 | 76 | show_y_guides=True, |
60 | 77 | stroke_style={"width": 6}, |
61 | 78 | legend_at_bottom=True, |
62 | | - legend_at_bottom_columns=3, |
| 79 | + legend_at_bottom_columns=4, |
63 | 80 | x_label_rotation=45, |
64 | 81 | truncate_label=10, |
65 | 82 | show_minor_x_labels=False, |
| 83 | + dots_size=10, |
66 | 84 | ) |
67 | 85 |
|
68 | | -# Format date labels (show every 10th date) |
69 | 86 | chart.x_labels = [d.strftime("%Y-%m-%d") for d in dates] |
70 | 87 | chart.x_labels_major = [dates[i].strftime("%Y-%m-%d") for i in range(0, len(dates), 20)] |
71 | 88 |
|
72 | | -# Add data series - Price line should be most prominent |
73 | 89 | chart.add("Close Price", close.tolist(), stroke_style={"width": 8}) |
74 | 90 | chart.add("EMA 12-day", ema_12.tolist(), stroke_style={"width": 5, "dasharray": "10,5"}) |
75 | 91 | chart.add("EMA 26-day", ema_26.tolist(), stroke_style={"width": 5, "dasharray": "5,5"}) |
| 92 | +chart.add("Crossovers", crossover_vals, show_dots=True, stroke=False) |
76 | 93 |
|
77 | | -# Save as PNG and HTML |
78 | | -chart.render_to_png("plot.png") |
79 | | -chart.render_to_file("plot.html") |
| 94 | +# Save |
| 95 | +chart.render_to_png(f"plot-{THEME}.png") |
| 96 | +with open(f"plot-{THEME}.html", "wb") as f: |
| 97 | + f.write(chart.render()) |
0 commit comments