/* ============================================================================ * HFChart Embed — bootstrapper des graphes intégrés (données inline) * Author : Simon-Pierre Boucher — contact@spboucher.ai * ---------------------------------------------------------------------------- * Servi concaténé après transforms/indicators/renderers/engine dans /embed.js. * Usage (snippet généré par le bouton « Exporter » de www.hfchart.io) : *
* * * Le graphe est pleinement interactif (zoom, pan, crosshair) et 100 % autonome : * les données sont dans la page, aucune requête réseau. * ==========================================================================*/ (function () { 'use strict'; if (window.__HFEmbedLoaded) return; window.__HFEmbedLoaded = true; const HF = window.HF; const TYPES = { candles: { draw: 'candles' }, hollow: { draw: 'hollow' }, ohlc: { draw: 'ohlc' }, line: { draw: 'line' }, step: { draw: 'step' }, area: { draw: 'area' }, baseline: { draw: 'baseline' }, heikin: { draw: 'candles', transform: 'heikinAshi' }, renko: { draw: 'bricks', transform: 'renko' }, linebreak: { draw: 'bricks', transform: 'lineBreak' }, rangebars: { draw: 'candles', transform: 'rangeBars' }, kagi: { draw: 'kagi', transform: 'kagi', noVol: true }, pnf: { draw: 'pnf', transform: 'pnf', noVol: true }, }; /* Tokens des deux thèmes (mêmes valeurs que style.css de la plateforme). */ const TOKENS = { light: { '--surface': '#fcfcfb', '--ink': '#0b0b0b', '--ink2': '#52514e', '--muted': '#898781', '--grid': '#e1e0d9', '--axis': '#c3c2b7', '--chip': '#0b0b0b', '--chip-text': '#ffffff', '--series-1': '#2a78d6', '--series-2': '#eb6834', '--series-3': '#1baf7a', '--series-4': '#eda100', '--series-5': '#e87ba4', '--series-6': '#008300', '--series-7': '#4a3aa7', '--series-8': '#e34948', }, dark: { '--surface': '#1a1a19', '--ink': '#ffffff', '--ink2': '#c3c2b7', '--muted': '#898781', '--grid': '#2c2c2a', '--axis': '#383835', '--chip': '#ffffff', '--chip-text': '#0b0b0b', '--series-1': '#3987e5', '--series-2': '#d95926', '--series-3': '#199e70', '--series-4': '#c98500', '--series-5': '#d55181', '--series-6': '#008300', '--series-7': '#9085e9', '--series-8': '#e66767', }, }; const UPDN = { classic: { '--up': '#0ca30c', '--dn': '#d03b3b' }, cvd_light: { '--up': '#2a78d6', '--dn': '#e34948' }, cvd_dark: { '--up': '#3987e5', '--dn': '#e66767' }, }; const OVERLAYS = { sma20: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma20 || 20), slot: 3 }), sma50: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma50 || 50), slot: 1 }), ema20: (bars, p) => ({ vals: HF.ind.ema(HF.ind.closes(bars), p.ema20 || 20), slot: 2 }), boll: (bars, p) => ({ band: HF.ind.bollinger(bars, p.bollN || 20, p.bollK || 2), slot: 6 }), vwap: bars => { const v = HF.ind.vwapSession(bars); return { band: { up: v.up2, lo: v.dn2, mid: v.v }, slot: 4, needVol: true }; }, }; const PANES = { rsi: (bars, p) => ({ kind: 'rsi', data: HF.ind.rsi(bars, p.rsiN || 14) }), macd: (bars, p) => ({ kind: 'macd', data: HF.ind.macd(bars, p.macdF || 12, p.macdS || 26, p.macdSig || 9) }), atr: (bars, p) => ({ kind: 'atr', data: HF.ind.atr(bars, p.atrN || 14) }), }; function render(item) { const el = typeof item.el === 'string' ? document.querySelector(item.el) : item.el; const cfg = item.config || {}; if (!el || !cfg.bars || !cfg.bars.length) return; const theme = cfg.theme === 'dark' ? 'dark' : 'light'; const scheme = cfg.scheme === 'cvd' ? (theme === 'dark' ? 'cvd_dark' : 'cvd_light') : 'classic'; el.innerHTML = ''; el.style.position = el.style.position || 'relative'; if (!el.style.height) el.style.height = '420px'; for (const [k, v] of Object.entries(TOKENS[theme])) el.style.setProperty(k, v); for (const [k, v] of Object.entries(UPDN[scheme])) el.style.setProperty(k, v); el.style.background = TOKENS[theme]['--surface']; el.style.borderRadius = el.style.borderRadius || '8px'; el.style.overflow = 'hidden'; el.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", sans-serif'; const chartDiv = document.createElement('div'); chartDiv.style.cssText = 'position:absolute;inset:0 0 20px 0;'; el.appendChild(chartDiv); // barre de crédit (attribution + identité de la vue) const credit = document.createElement('div'); credit.style.cssText = `position:absolute;left:0;right:0;bottom:0;height:20px;display:flex;align-items:center;` + `justify-content:space-between;padding:0 8px;font-size:10.5px;box-sizing:border-box;` + `color:${TOKENS[theme]['--muted']};border-top:1px solid ${TOKENS[theme]['--grid']};`; const label = [cfg.symbol, cfg.tf, cfg.typeLabel || cfg.type].filter(Boolean).join(' · '); credit.innerHTML = `${label}` + `HFChart · données hfmarketdata.io`; el.appendChild(credit); const bars = cfg.bars.map(b => ({ t: b[0], o: b[1], h: b[2], l: b[3], c: b[4], v: b[5] || 0 })); const def = TYPES[cfg.type] || TYPES.candles; const p = cfg.params || {}; let items = bars; if (def.transform) { const opts = {}; if (cfg.type === 'renko' && p.brick > 0) opts.brick = p.brick; if (cfg.type === 'rangebars' && p.range > 0) opts.range = p.range; if (cfg.type === 'kagi' && p.reversal > 0) opts.reversal = p.reversal; if (cfg.type === 'pnf') { if (p.box > 0) opts.box = p.box; if (p.pnfRev > 0) opts.reversal = p.pnfRev; } if (cfg.type === 'linebreak' && p.lines > 0) opts.lines = p.lines; items = HF.transforms[def.transform](bars, opts); if (!items.length) items = bars; } const chart = new HF.HFChart(chartDiv, {}); const spec = { items, draw: def.draw, decimals: cfg.decimals != null ? cfg.decimals : 2, defaultVisible: Math.min(items.length, cfg.visible || 320), hasVolume: !!cfg.hasVolume, showVolume: !!cfg.hasVolume && !def.noVol, overlays: [], panes: [], anchor: cfg.type === 'baseline' && items.length ? items[0].c : null, }; for (const id of (cfg.inds || [])) { if (OVERLAYS[id]) { const made = OVERLAYS[id](items, p); if (made.needVol && !cfg.hasVolume) continue; spec.overlays.push({ ...made, color: chart.th.series[made.slot] }); } else if (PANES[id]) { spec.panes.push({ id, label: id.toUpperCase(), ...PANES[id](items, p) }); } } chart.setSeries(spec); } const q = window.HFChartEmbedQueue = window.HFChartEmbedQueue || []; const boot = () => { q.splice(0).forEach(render); q.push = it => { render(it); return 0; }; }; if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); })();