{"spec_id":"candlestick-basic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncandlestick-basic: Basic Candlestick Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\nimport re\nimport sys\nfrom datetime import datetime, timedelta\n\n\n# This file is named 'pygal.py'; remove its own directory from sys.path so\n# 'import pygal' resolves to the installed package rather than this script itself.\n_self_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _self_dir]\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# --- Theme tokens (Imprint palette + theme-adaptive chrome) ---\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Finance semantic: bullish=green (Imprint pos 1), bearish=matte-red (Imprint pos 5)\nBULL = \"#009E73\"\nBEAR = \"#AE3030\"\nPEAK_CLR = \"#C475FD\"  # Imprint pos 2\nLOW_CLR = \"#4467A3\"  # Imprint pos 3\nMA_CLR = INK_MUTED  # theme-adaptive reference line\n\n# --- Data: 30 trading days of OHLC stock prices ---\nnp.random.seed(42)\nn_days = 30\n\nstart_date = datetime(2024, 1, 2)\ndates = []\ncur = start_date\nfor _ in range(n_days):\n    while cur.weekday() >= 5:\n        cur += timedelta(days=1)\n    dates.append(cur)\n    cur += timedelta(days=1)\n\nbase_price = 150.0\nreturns = np.random.randn(n_days) * 2.5\nprice_series = base_price * np.cumprod(1 + returns / 100)\n\nohlc_data = []\nfor i, close in enumerate(price_series):\n    volatility = np.abs(np.random.randn()) * 2 + 0.5\n    intraday_range = close * volatility / 100\n    open_price = base_price if i == 0 else ohlc_data[-1][\"close\"]\n    high = max(open_price, close) + np.random.rand() * intraday_range\n    low = min(open_price, close) - np.random.rand() * intraday_range\n    ohlc_data.append({\"day\": i + 1, \"open\": open_price, \"high\": high, \"low\": low, \"close\": close})\n\n# 5-day moving average for trend context\ncloses = [d[\"close\"] for d in ohlc_data]\nma_points = [(i + 1, float(np.mean(closes[i - 4 : i + 1]))) for i in range(4, n_days)]\n\n# Price extremes for storytelling markers\npeak = max(ohlc_data, key=lambda d: d[\"high\"])\ntrough = min(ohlc_data, key=lambda d: d[\"low\"])\n\n# --- Group candlestick segments by direction (None = line break) ---\nbull_wicks, bear_wicks = [], []\nbull_bodies, bear_bodies = [], []\n\nfor candle in ohlc_data:\n    x = candle[\"day\"]\n    wick = [(x, candle[\"low\"]), (x, candle[\"high\"]), None]\n    body = [(x, candle[\"open\"]), (x, candle[\"close\"]), None]\n    if candle[\"close\"] >= candle[\"open\"]:\n        bull_wicks.extend(wick)\n        bull_bodies.extend(body)\n    else:\n        bear_wicks.extend(wick)\n        bear_bodies.extend(body)\n\n# CVD redundant encoding: positional markers at wick extremes signal direction\n# (top-of-wick dot = bullish / bottom-of-wick dot = bearish, independent of color)\nbull_direction_pts = [(c[\"day\"], c[\"high\"]) for c in ohlc_data if c[\"close\"] >= c[\"open\"]]\nbear_direction_pts = [(c[\"day\"], c[\"low\"]) for c in ohlc_data if c[\"close\"] < c[\"open\"]]\n\ndate_map = {i + 1: dates[i] for i in range(n_days)}\n\n# --- Style: Imprint palette + theme-adaptive chrome ---\n# 9 colors indexed by series add order (0–8 incl. hidden CVD marker series 7–8)\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=(BULL, BEAR, MA_CLR, BULL, BEAR, PEAK_CLR, LOW_CLR, BULL, BEAR),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n)\n\n# --- Chart: 3200×1800 landscape canvas ---\nWICK_W, BODY_W, MA_W = 13, 51, 4\n\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=\"candlestick-basic · python · pygal · anyplot.ai\",\n    x_title=\"Date\",\n    y_title=\"Price ($)\",\n    show_dots=False,\n    show_x_guides=False,\n    show_y_guides=True,\n    allow_interruptions=True,\n    range=(min(d[\"low\"] for d in ohlc_data) - 2, max(d[\"high\"] for d in ohlc_data) + 3),\n    xrange=(0, n_days + 1),\n    legend_box_size=22,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=5,\n    margin=36,\n    spacing=20,\n    tooltip_border_radius=8,\n    truncate_legend=-1,\n    value_formatter=lambda x: f\"${x:.2f}\",\n)\n\nchart.x_labels = [1, 5, 10, 15, 20, 25, 30]\nchart.x_value_formatter = lambda x: date_map[int(round(x))].strftime(\"%b %d\") if int(round(x)) in date_map else \"\"\n\n# Series 0: bull wicks (hidden from legend)\nchart.add(None, bull_wicks, stroke=True, show_dots=False, stroke_style={\"width\": WICK_W, \"linecap\": \"butt\"})\n# Series 1: bear wicks (hidden from legend)\nchart.add(None, bear_wicks, stroke=True, show_dots=False, stroke_style={\"width\": WICK_W, \"linecap\": \"butt\"})\n# Series 2: moving average trend line\nchart.add(\"5-Day MA\", ma_points, stroke=True, show_dots=False, stroke_style={\"width\": MA_W, \"linecap\": \"round\"})\n# Series 3: bullish candle bodies\nchart.add(\"Bullish (Up)\", bull_bodies, stroke=True, show_dots=False, stroke_style={\"width\": BODY_W, \"linecap\": \"butt\"})\n# Series 4: bearish candle bodies\nchart.add(\n    \"Bearish (Down)\", bear_bodies, stroke=True, show_dots=False, stroke_style={\"width\": BODY_W, \"linecap\": \"butt\"}\n)\n# Series 5: peak accent marker\nchart.add(f\"Peak ${peak['high']:.2f}\", [(peak[\"day\"], peak[\"high\"])], stroke=False, show_dots=True, dots_size=12)\n# Series 6: trough accent marker\nchart.add(f\"Low ${trough['low']:.2f}\", [(trough[\"day\"], trough[\"low\"])], stroke=False, show_dots=True, dots_size=12)\n# Series 7: CVD direction markers — top of bullish wicks (hidden from legend)\nchart.add(None, bull_direction_pts, stroke=False, show_dots=True, dots_size=7)\n# Series 8: CVD direction markers — bottom of bearish wicks (hidden from legend)\nchart.add(None, bear_direction_pts, stroke=False, show_dots=True, dots_size=7)\n\n# --- Render: SVG post-processing for cairosvg and visual refinement ---\n# cairosvg ignores CSS class-based stroke properties; inline them directly on each series group\nsvg = chart.render(is_unicode=True)\n\n# 1. Inline stroke widths for cairosvg compatibility\nseries_strokes = {\n    0: (WICK_W, \"butt\"),\n    1: (WICK_W, \"butt\"),\n    2: (MA_W, \"round\"),\n    3: (BODY_W, \"butt\"),\n    4: (BODY_W, \"butt\"),\n}\nfor sid, (width, cap) in series_strokes.items():\n    style_attr = f' style=\"stroke-width:{width};stroke-linecap:{cap}\"'\n    svg = re.sub(\n        rf'(class=\"series serie-{sid} color-{sid}\"[^>]*>.*?</g>)',\n        lambda m, s=style_attr: m.group(0).replace('class=\"line reactive nofill\"', 'class=\"line reactive nofill\"' + s),\n        svg,\n        count=1,\n        flags=re.DOTALL,\n    )\n\n# 2. Grid lines: override dashed default with solid lines\nsvg = re.sub(r\"stroke-dasharray\\s*:\\s*[\\d.,\\s]+\", \"stroke-dasharray:none\", svg)\n\n# 3. L-spine border: replace full-frame rect with left + bottom lines only\nm = re.search(r'<g\\b[^>]*class=\"plot\"[^>]*>\\s*(<rect\\b[^>]*/?>)', svg)\nif m:\n    rect_tag = m.group(1)\n    rx = re.search(r'\\bx=\"([^\"]+)\"', rect_tag)\n    ry = re.search(r'\\by=\"([^\"]+)\"', rect_tag)\n    rw = re.search(r'\\bwidth=\"([^\"]+)\"', rect_tag)\n    rh = re.search(r'\\bheight=\"([^\"]+)\"', rect_tag)\n    if rx and ry and rw and rh:\n        x, y, w, h = float(rx.group(1)), float(ry.group(1)), float(rw.group(1)), float(rh.group(1))\n        no_stroke = re.sub(r'\\bstroke=\"[^\"]*\"', 'stroke=\"none\"', rect_tag)\n        if \"stroke=\" not in rect_tag:\n            no_stroke = rect_tag[:-2] + ' stroke=\"none\"/>' if rect_tag.endswith(\"/>\") else rect_tag\n        l_spine = (\n            f'<line x1=\"{x:.1f}\" y1=\"{y:.1f}\" x2=\"{x:.1f}\" y2=\"{y + h:.1f}\" '\n            f'stroke=\"{INK_MUTED}\" stroke-width=\"1.5\"/>'\n            f'<line x1=\"{x:.1f}\" y1=\"{y + h:.1f}\" x2=\"{x + w:.1f}\" y2=\"{y + h:.1f}\" '\n            f'stroke=\"{INK_MUTED}\" stroke-width=\"1.5\"/>'\n        )\n        svg = svg.replace(rect_tag, no_stroke + l_spine, 1)\n\ncairosvg.svg2png(bytestring=svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(chart.render(is_unicode=True))\n"}