Skip to content

Commit 1129e58

Browse files
feat(pygal): implement indicator-ema (#7350)
## Implementation: `indicator-ema` - python/pygal Implements the **python/pygal** version of `indicator-ema`. **File:** `plots/indicator-ema/implementations/python/pygal.py` **Parent Issue:** #3652 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26076885400)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 07bf11d commit 1129e58

3 files changed

Lines changed: 199 additions & 145 deletions

File tree

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,57 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

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+
715
import numpy as np
816
import pandas as pd
917
import pygal
1018
from pygal.style import Style
1119

1220

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"
1525

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")
1827

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")
2031
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)
2233
prices = initial_price * np.cumprod(1 + returns)
2334

24-
25-
# Calculate EMAs (exponentially weighted moving average)
2635
close = prices
2736
ema_12 = pd.Series(close).ewm(span=12, adjust=False).mean().values
2837
ema_26 = pd.Series(close).ewm(span=26, adjust=False).mean().values
2938

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
3148
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,
3855
title_font_size=72,
3956
label_font_size=48,
4057
major_label_font_size=42,
@@ -46,34 +63,35 @@
4663
opacity_hover=1.0,
4764
)
4865

49-
# Create line chart
66+
# Chart
5067
chart = pygal.Line(
5168
width=4800,
5269
height=2700,
5370
style=custom_style,
54-
title="indicator-ema · pygal · pyplots.ai",
71+
title="indicator-ema · python · pygal · anyplot.ai",
5572
x_title="Date",
5673
y_title="Price ($)",
57-
show_dots=False, # Cleaner line appearance
58-
show_x_guides=False,
74+
show_dots=False,
75+
show_x_guides=True,
5976
show_y_guides=True,
6077
stroke_style={"width": 6},
6178
legend_at_bottom=True,
62-
legend_at_bottom_columns=3,
79+
legend_at_bottom_columns=4,
6380
x_label_rotation=45,
6481
truncate_label=10,
6582
show_minor_x_labels=False,
83+
dots_size=10,
6684
)
6785

68-
# Format date labels (show every 10th date)
6986
chart.x_labels = [d.strftime("%Y-%m-%d") for d in dates]
7087
chart.x_labels_major = [dates[i].strftime("%Y-%m-%d") for i in range(0, len(dates), 20)]
7188

72-
# Add data series - Price line should be most prominent
7389
chart.add("Close Price", close.tolist(), stroke_style={"width": 8})
7490
chart.add("EMA 12-day", ema_12.tolist(), stroke_style={"width": 5, "dasharray": "10,5"})
7591
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)
7693

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

Comments
 (0)