Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate TradingView-style dark-theme candlestick charts with RSI, MACD, Bollinger Bands, and EMA/SMA using mplfinance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/build_chart.py
1#!/usr/bin/env python32"""3Chart builder v3: project-based chart generation.45Each chart project lives in output/chart-html/<project-name>/6index.html — the chart page7generate.py — the generation script (optional, for reproducibility)8README.md — title, description, data sources9data.json — raw data snapshot10screenshot.png — exported PNG (via Playwright or button)1112Usage:13from skills.chart.scripts.build_chart import (14create_project, build_chart, build_chart_custom, save_chart, screenshot_chart15)1617# Create project directory18project_dir = create_project('btc-gold-90d')1920# Build HTML21html = build_chart_custom(title='BTC vs Gold', ...)22save_chart(html, project_dir=project_dir)2324# Optional: screenshot for direct image delivery25screenshot_chart(project_dir)26"""2728import os29import json30from datetime import datetime31from pathlib import Path3233SKILL_DIR = os.path.join(os.path.dirname(__file__), '..')34SCRIPTS_DIR = os.path.join(SKILL_DIR, 'scripts')35TEMPLATES_DIR = os.path.join(SKILL_DIR, 'templates')36CHART_HTML_DIR = os.path.join('/data/workspace', 'output', 'chart-html')373839def _read(path):40with open(path, 'r') as f:41return f.read()424344def get_base_css():45return _read(os.path.join(SCRIPTS_DIR, 'base-styles.css'))464748def get_base_js():49return _read(os.path.join(SCRIPTS_DIR, 'base-export.js'))505152def create_project(name, description='', data_sources=None):53"""Create a new chart project directory.5455Args:56name: Project folder name (e.g. 'btc-gold-90d-20250701')57description: What this chart shows58data_sources: list of data source strings5960Returns:61Absolute path to the project directory62"""63project_dir = os.path.join(CHART_HTML_DIR, name)64os.makedirs(project_dir, exist_ok=True)6566# Write README.md67readme = f"# {name}\n\n"68readme += f"**Created:** {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"69if description:70readme += f"{description}\n\n"71if data_sources:72readme += "**Data Sources:**\n"73for src in data_sources:74readme += f"- {src}\n"7576readme_path = os.path.join(project_dir, 'README.md')77if not os.path.exists(readme_path):78with open(readme_path, 'w') as f:79f.write(readme)8081return project_dir828384def build_chart(template_name, title='Chart', subtitle='', replacements=None):85"""Build chart from a template file.8687Args:88template_name: template filename without .html extension (e.g. 'line', 'bar')89title: chart page title90subtitle: subtitle text91replacements: dict of additional {{KEY}} → value replacements9293Returns:94Complete HTML string95"""96tpl_path = os.path.join(TEMPLATES_DIR, f'{template_name}.html')97html = _read(tpl_path)9899css = get_base_css()100js = get_base_js()101102html = html.replace('{{BASE_STYLES}}', css)103html = html.replace('{{BASE_EXPORT_JS}}', js)104html = html.replace('{{TITLE}}', title)105html = html.replace('{{SUBTITLE}}', subtitle)106107if replacements:108for k, v in replacements.items():109html = html.replace(f'{{{{{k}}}}}', v)110111return html112113114def build_chart_custom(title='Chart', subtitle='', body_html='', chart_js='',115extra_css='', kpi_html='', layout='vertical'):116"""Build a fully custom chart page without using a template.117118IMPORTANT: chart_js MUST:1191. Set window.CHART_INSTANCES = []; at the start1202. Push each echarts instance: CHART_INSTANCES.push(chartVar);1213. Optionally set window.CHART_LAYOUT = 'grid'; for 2-column export layout122123Args:124title: page title125subtitle: subtitle text below title126body_html: HTML for chart containers (inside #chart-area)127chart_js: JavaScript for chart initialization (must register CHART_INSTANCES)128extra_css: additional CSS129kpi_html: optional KPI cards HTML (placed above chart-area, not exported)130layout: 'vertical' (default) or 'grid' — how charts are merged in export PNG131132Returns:133Complete HTML string134"""135css = get_base_css()136js = get_base_js()137layout_js = f"window.CHART_LAYOUT = '{layout}';" if layout != 'vertical' else ''138139return f'''<!DOCTYPE html>140<html lang="en">141<head>142<meta charset="UTF-8">143<meta name="viewport" content="width=device-width, initial-scale=1.0">144<title>{title}</title>145<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>146<style>147{css}148{extra_css}149</style>150</head>151<body>152<div class="toolbar">153<div>154<h1>{title}</h1>155<div class="subtitle">{subtitle}</div>156</div>157<div class="actions">158<button onclick="downloadPNG(this)">📥 Download PNG</button>159<button onclick="copyToClipboard(this)">📋 Copy Image</button>160<button onclick="saveToProject(this)">💾 Save Image</button>161</div>162</div>163{kpi_html}164<div id="chart-area">165{body_html}166</div>167<script>168{js}169{layout_js}170</script>171<script>172{chart_js}173</script>174</body>175</html>'''176177178def save_chart(html, filename='index.html', project_dir=None, output_dir=None):179"""Save HTML to project directory or a legacy output directory.180181Args:182html: HTML string183filename: filename (default 'index.html')184project_dir: project directory path (preferred)185output_dir: legacy fallback directory186187Returns:188The file path written189"""190if project_dir:191target_dir = project_dir192elif output_dir:193target_dir = output_dir194else:195target_dir = CHART_HTML_DIR196197os.makedirs(target_dir, exist_ok=True)198path = os.path.join(target_dir, filename)199with open(path, 'w') as f:200f.write(html)201return path202203204def save_data(data, project_dir, filename='data.json'):205"""Save data snapshot to project directory.206207Args:208data: dict or list to serialize as JSON209project_dir: project directory path210filename: filename (default 'data.json')211212Returns:213The file path written214"""215os.makedirs(project_dir, exist_ok=True)216path = os.path.join(project_dir, filename)217with open(path, 'w') as f:218json.dump(data, f, ensure_ascii=False, indent=2)219return path220221222def save_generate_script(script_content, project_dir):223"""Save the generation script for reproducibility.224225Args:226script_content: Python script as string227project_dir: project directory path228229Returns:230The file path written231"""232os.makedirs(project_dir, exist_ok=True)233path = os.path.join(project_dir, 'generate.py')234with open(path, 'w') as f:235f.write(script_content)236return path237238239def screenshot_chart(project_dir, filename='screenshot.png', width=1280, height=720):240"""Generate PNG via the same merge pipeline as the "Save Image" button.241242This ensures one-click screenshot output is visually equivalent to saveToProject()243in the browser (title included, merged multi-chart layout consistent).244245Args:246project_dir: project directory containing index.html247filename: output PNG filename248width: viewport width249height: viewport height250251Returns:252The screenshot file path, or None if failed253"""254try:255from playwright.sync_api import sync_playwright256except ImportError:257print("[chart] Playwright not available, skipping screenshot")258return None259260html_path = os.path.join(project_dir, 'index.html')261if not os.path.exists(html_path):262print(f"[chart] No index.html found in {project_dir}")263return None264265out_path = os.path.join(project_dir, filename)266file_url = f"file://{os.path.abspath(html_path)}"267268try:269with sync_playwright() as p:270browser = p.chromium.launch()271page = browser.new_page(viewport={'width': width, 'height': height})272page.goto(file_url)273page.wait_for_timeout(2200) # let ECharts render274275# Reuse front-end merge/export logic for fidelity with save button.276data_url = page.evaluate(277"""278async () => {279if (typeof mergeChartsToDataURL === 'function') {280return await mergeChartsToDataURL();281}282throw new Error('mergeChartsToDataURL is not available on page');283}284"""285)286287if not data_url or ',' not in data_url:288raise RuntimeError('Invalid data URL returned from page export pipeline')289290b64 = data_url.split(',', 1)[1]291import base64292png_bytes = base64.b64decode(b64)293with open(out_path, 'wb') as f:294f.write(png_bytes)295296browser.close()297298print(f"[chart] Screenshot saved: {out_path}")299return out_path300except Exception as e:301print(f"[chart] Screenshot failed: {e}")302return None303304305if __name__ == '__main__':306# Quick test307proj = create_project('test-chart')308html = build_chart('line', title='Test Chart', subtitle='Testing build v3')309path = save_chart(html, project_dir=proj)310print(f'Built: {path}')311