SPB Git

spb/hfchart Public

HFChart — la référence des charts haute fréquence : 14 types rendus canvas from scratch (zéro dépendance), données HF Market Data — www.hfchart.io

JavaScript 82.1% CSS 10% HTML 5.8% Python 2.1%
7.3 KB · 152 lines javascript
Raw Blame History
1/* ============================================================================2 * HFChart Embed — bootstrapper des graphes intégrés (données inline)3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ----------------------------------------------------------------------------5 * Servi concaténé après transforms/indicators/renderers/engine dans /embed.js.6 * Usage (snippet généré par le bouton « Exporter » de www.hfchart.io) :7 *   <div id="hfchart-xxxx" style="width:100%;height:420px"></div>8 *   <script>(window.HFChartEmbedQueue=window.HFChartEmbedQueue||[])9 *     .push({el:'#hfchart-xxxx',config:{symbol,tf,type,theme,scheme,inds,10 *            params,decimals,hasVolume,bars:[[t,o,h,l,c,v],…]}});</script>11 *   <script async src="https://www.hfchart.io/embed.js"></script>12 * Le graphe est pleinement interactif (zoom, pan, crosshair) et 100 % autonome :13 * les données sont dans la page, aucune requête réseau.14 * ==========================================================================*/15(function () {16  'use strict';17  if (window.__HFEmbedLoaded) return;18  window.__HFEmbedLoaded = true;19  const HF = window.HF;2021  const TYPES = {22    candles: { draw: 'candles' },23    hollow: { draw: 'hollow' },24    ohlc: { draw: 'ohlc' },25    line: { draw: 'line' },26    step: { draw: 'step' },27    area: { draw: 'area' },28    baseline: { draw: 'baseline' },29    heikin: { draw: 'candles', transform: 'heikinAshi' },30    renko: { draw: 'bricks', transform: 'renko' },31    linebreak: { draw: 'bricks', transform: 'lineBreak' },32    rangebars: { draw: 'candles', transform: 'rangeBars' },33    kagi: { draw: 'kagi', transform: 'kagi', noVol: true },34    pnf: { draw: 'pnf', transform: 'pnf', noVol: true },35  };3637  /* Tokens des deux thèmes (mêmes valeurs que style.css de la plateforme). */38  const TOKENS = {39    light: {40      '--surface': '#fcfcfb', '--ink': '#0b0b0b', '--ink2': '#52514e',41      '--muted': '#898781', '--grid': '#e1e0d9', '--axis': '#c3c2b7',42      '--chip': '#0b0b0b', '--chip-text': '#ffffff',43      '--series-1': '#2a78d6', '--series-2': '#eb6834', '--series-3': '#1baf7a',44      '--series-4': '#eda100', '--series-5': '#e87ba4', '--series-6': '#008300',45      '--series-7': '#4a3aa7', '--series-8': '#e34948',46    },47    dark: {48      '--surface': '#1a1a19', '--ink': '#ffffff', '--ink2': '#c3c2b7',49      '--muted': '#898781', '--grid': '#2c2c2a', '--axis': '#383835',50      '--chip': '#ffffff', '--chip-text': '#0b0b0b',51      '--series-1': '#3987e5', '--series-2': '#d95926', '--series-3': '#199e70',52      '--series-4': '#c98500', '--series-5': '#d55181', '--series-6': '#008300',53      '--series-7': '#9085e9', '--series-8': '#e66767',54    },55  };56  const UPDN = {57    classic: { '--up': '#0ca30c', '--dn': '#d03b3b' },58    cvd_light: { '--up': '#2a78d6', '--dn': '#e34948' },59    cvd_dark: { '--up': '#3987e5', '--dn': '#e66767' },60  };6162  const OVERLAYS = {63    sma20: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma20 || 20), slot: 3 }),64    sma50: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma50 || 50), slot: 1 }),65    ema20: (bars, p) => ({ vals: HF.ind.ema(HF.ind.closes(bars), p.ema20 || 20), slot: 2 }),66    boll: (bars, p) => ({ band: HF.ind.bollinger(bars, p.bollN || 20, p.bollK || 2), slot: 6 }),67    vwap: bars => { const v = HF.ind.vwapSession(bars); return { band: { up: v.up2, lo: v.dn2, mid: v.v }, slot: 4, needVol: true }; },68  };69  const PANES = {70    rsi: (bars, p) => ({ kind: 'rsi', data: HF.ind.rsi(bars, p.rsiN || 14) }),71    macd: (bars, p) => ({ kind: 'macd', data: HF.ind.macd(bars, p.macdF || 12, p.macdS || 26, p.macdSig || 9) }),72    atr: (bars, p) => ({ kind: 'atr', data: HF.ind.atr(bars, p.atrN || 14) }),73  };7475  function render(item) {76    const el = typeof item.el === 'string' ? document.querySelector(item.el) : item.el;77    const cfg = item.config || {};78    if (!el || !cfg.bars || !cfg.bars.length) return;79    const theme = cfg.theme === 'dark' ? 'dark' : 'light';80    const scheme = cfg.scheme === 'cvd' ? (theme === 'dark' ? 'cvd_dark' : 'cvd_light') : 'classic';81    el.innerHTML = '';82    el.style.position = el.style.position || 'relative';83    if (!el.style.height) el.style.height = '420px';84    for (const [k, v] of Object.entries(TOKENS[theme])) el.style.setProperty(k, v);85    for (const [k, v] of Object.entries(UPDN[scheme])) el.style.setProperty(k, v);86    el.style.background = TOKENS[theme]['--surface'];87    el.style.borderRadius = el.style.borderRadius || '8px';88    el.style.overflow = 'hidden';89    el.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", sans-serif';9091    const chartDiv = document.createElement('div');92    chartDiv.style.cssText = 'position:absolute;inset:0 0 20px 0;';93    el.appendChild(chartDiv);9495    // barre de crédit (attribution + identité de la vue)96    const credit = document.createElement('div');97    credit.style.cssText =98      `position:absolute;left:0;right:0;bottom:0;height:20px;display:flex;align-items:center;` +99      `justify-content:space-between;padding:0 8px;font-size:10.5px;box-sizing:border-box;` +100      `color:${TOKENS[theme]['--muted']};border-top:1px solid ${TOKENS[theme]['--grid']};`;101    const label = [cfg.symbol, cfg.tf, cfg.typeLabel || cfg.type].filter(Boolean).join(' · ');102    credit.innerHTML =103      `<span style="font-weight:600;color:${TOKENS[theme]['--ink2']}">${label}</span>` +104      `<a href="https://www.hfchart.io" target="_blank" rel="noopener" ` +105      `style="color:${TOKENS[theme]['--muted']};text-decoration:none">HFChart · données hfmarketdata.io</a>`;106    el.appendChild(credit);107108    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 }));109    const def = TYPES[cfg.type] || TYPES.candles;110    const p = cfg.params || {};111    let items = bars;112    if (def.transform) {113      const opts = {};114      if (cfg.type === 'renko' && p.brick > 0) opts.brick = p.brick;115      if (cfg.type === 'rangebars' && p.range > 0) opts.range = p.range;116      if (cfg.type === 'kagi' && p.reversal > 0) opts.reversal = p.reversal;117      if (cfg.type === 'pnf') { if (p.box > 0) opts.box = p.box; if (p.pnfRev > 0) opts.reversal = p.pnfRev; }118      if (cfg.type === 'linebreak' && p.lines > 0) opts.lines = p.lines;119      items = HF.transforms[def.transform](bars, opts);120      if (!items.length) items = bars;121    }122123    const chart = new HF.HFChart(chartDiv, {});124    const spec = {125      items,126      draw: def.draw,127      decimals: cfg.decimals != null ? cfg.decimals : 2,128      defaultVisible: Math.min(items.length, cfg.visible || 320),129      hasVolume: !!cfg.hasVolume,130      showVolume: !!cfg.hasVolume && !def.noVol,131      overlays: [],132      panes: [],133      anchor: cfg.type === 'baseline' && items.length ? items[0].c : null,134    };135    for (const id of (cfg.inds || [])) {136      if (OVERLAYS[id]) {137        const made = OVERLAYS[id](items, p);138        if (made.needVol && !cfg.hasVolume) continue;139        spec.overlays.push({ ...made, color: chart.th.series[made.slot] });140      } else if (PANES[id]) {141        spec.panes.push({ id, label: id.toUpperCase(), ...PANES[id](items, p) });142      }143    }144    chart.setSeries(spec);145  }146147  const q = window.HFChartEmbedQueue = window.HFChartEmbedQueue || [];148  const boot = () => { q.splice(0).forEach(render); q.push = it => { render(it); return 0; }; };149  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);150  else boot();151})();152