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%
1/* ============================================================================2 * HFChart — application : catalogue de charts HF, sélection, personnalisation3 * Données : API HF Market Data (https://www.hfmarketdata.io)4 * Author : Simon-Pierre Boucher — contact@spboucher.ai5 * ==========================================================================*/6(function () {7 'use strict';8 const HF = window.HF;9 const $ = s => document.querySelector(s);10 const $$ = s => Array.from(document.querySelectorAll(s));1112 /* ------------------------------------------------------------ catalogue */13 const CHART_TYPES = [14 { id: 'candles', label: 'Chandelles', desc: 'OHLC classique, corps plein', draw: 'candles', vis: 220 },15 { id: 'hollow', label: 'Chandelles creuses', desc: 'Hausse creuse / baisse pleine — lisible sans couleur', draw: 'hollow', vis: 220 },16 { id: 'ohlc', label: 'Barres OHLC', desc: 'Barres à ticks open/close', draw: 'ohlc', vis: 260 },17 { id: 'line', label: 'Ligne', desc: 'Close en trait 2px', draw: 'line', vis: 600 },18 { id: 'step', label: 'Escalier', desc: 'Close en marches', draw: 'step', vis: 400 },19 { id: 'area', label: 'Aire', desc: 'Ligne + dégradé vers la base', draw: 'area', vis: 600 },20 { id: 'baseline', label: 'Baseline', desc: 'Au-dessus / en-dessous de l’ancre', draw: 'baseline', vis: 600 },21 { id: 'heikin', label: 'Heikin-Ashi', desc: 'Chandelles lissées de tendance', draw: 'candles', transform: 'heikinAshi', vis: 220 },22 { id: 'renko', label: 'Renko', desc: 'Briques à taille fixe (auto-ATR)', draw: 'bricks', transform: 'renko', vis: 160 },23 { id: 'linebreak', label: 'Three-Line Break', desc: 'Nouvelle barre sur cassure des 3 dernières', draw: 'bricks', transform: 'lineBreak', vis: 160 },24 { id: 'rangebars', label: 'Range Bars', desc: 'Chaque barre couvre un range fixe', draw: 'candles', transform: 'rangeBars', vis: 200 },25 { id: 'kagi', label: 'Kagi', desc: 'Yang épais / yin fin, renversement fixe', draw: 'kagi', transform: 'kagi', vis: 140, noVol: true },26 { id: 'pnf', label: 'Point & Figure', desc: 'Colonnes de X et O', draw: 'pnf', transform: 'pnf', vis: 90, noVol: true },27 { id: 'compare', label: 'Comparaison %', desc: 'Jusqu’à 4 symboles, base commune → variation %', draw: 'line', compare: true, vis: 600, noVol: true },28 ];2930 const OVERLAYS = [31 { id: 'sma20', label: 'SMA 20', slot: 3, make: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma20 || 20) }) },32 { id: 'sma50', label: 'SMA 50', slot: 1, make: (bars, p) => ({ vals: HF.ind.sma(HF.ind.closes(bars), p.sma50 || 50) }) },33 { id: 'ema20', label: 'EMA 20', slot: 2, make: (bars, p) => ({ vals: HF.ind.ema(HF.ind.closes(bars), p.ema20 || 20) }) },34 { id: 'boll', label: 'Bollinger 20·2', slot: 6, make: (bars, p) => { const b = HF.ind.bollinger(bars, p.bollN || 20, p.bollK || 2); return { band: b }; } },35 { id: 'vwap', label: 'VWAP session ±2σ', slot: 4, needVol: true, intraday: true,36 make: bars => { const v = HF.ind.vwapSession(bars); return { band: { up: v.up2, lo: v.dn2, mid: v.v } }; } },37 ];38 const PANES = [39 { id: 'rsi', label: 'RSI', kind: 'rsi', make: (bars, p) => HF.ind.rsi(bars, p.rsiN || 14) },40 { id: 'macd', label: 'MACD', kind: 'macd', make: (bars, p) => HF.ind.macd(bars, p.macdF || 12, p.macdS || 26, p.macdSig || 9) },41 { id: 'atr', label: 'ATR', kind: 'atr', make: (bars, p) => HF.ind.atr(bars, p.atrN || 14) },42 ];43 const TFS = [44 { id: '1', label: '1m' }, { id: '5', label: '5m' }, { id: '15', label: '15m' },45 { id: '30', label: '30m' }, { id: '60', label: '1h' }, { id: '240', label: '4h' },46 { id: 'D', label: '1J' }, { id: 'W', label: '1S' },47 ];48 const TF_WANT = { 1: 5000, 5: 5000, 15: 5000, 30: 5000, 60: 5000, 240: 3000, D: 12000, W: 12000 };49 const DEFAULT_SYMBOL = { index: 'SPX', etf: 'SPY', stock: 'AAPL', futures: 'ES', crypto: 'BTC', fx: 'EURUSD' };5051 /* ------------------------------------------------------------ état */52 const state = Object.assign({53 group: 'index', symbol: 'SPX', tf: '60', type: 'candles',54 theme: 'light', scheme: 'classic', log: false,55 inds: [], compare: [], params: {},56 }, load(), urlState());57 state.params = state.params || {};5859 /* Surcharges via l'URL (partage de vues) : ?symbol=SPY&group=etf&tf=60&type=renko60 * &inds=rsi,vwap&compare=QQQ,IWM&theme=dark&scheme=cvd&log=1 */61 function urlState() {62 const q = new URLSearchParams(location.search);63 const out = {};64 for (const k of ['group', 'symbol', 'tf', 'type', 'theme', 'scheme']) {65 if (q.get(k)) out[k] = q.get(k);66 }67 if (q.get('inds') != null) out.inds = q.get('inds').split(',').filter(Boolean);68 if (q.get('compare') != null) out.compare = q.get('compare').split(',').filter(Boolean);69 if (q.get('log') != null) out.log = q.get('log') === '1';70 return out;71 }7273 function load() {74 try { return JSON.parse(localStorage.getItem('hfchart-state') || '{}'); } catch (e) { return {}; }75 }76 function save() {77 localStorage.setItem('hfchart-state', JSON.stringify({78 group: state.group, symbol: state.symbol, tf: state.tf, type: state.type,79 theme: state.theme, scheme: state.scheme, log: state.log,80 inds: state.inds, compare: state.compare, params: state.params,81 }));82 }8384 let chart = null;85 let raw = null; // {parsed, hasVolume, decimals, srcLimitReached}86 let compareRaw = {}; // symbole → réponse87 let tfAvail = {}; // tf UI → dispo pour le groupe courant88 let loading = false;89 let curWant = 0;9091 function toast(msg, isErr) {92 const el = $('#toast');93 el.textContent = msg;94 el.className = 'toast show' + (isErr ? ' err' : '');95 clearTimeout(toast._t);96 toast._t = setTimeout(() => { el.className = 'toast'; }, 4000);97 }9899 /* ------------------------------------------------------------ spec */100 function typeDef() { return CHART_TYPES.find(t => t.id === state.type) || CHART_TYPES[0]; }101102 function buildSpec(keepView) {103 if (!raw) return;104 const def = typeDef();105 const bars = raw.parsed;106 let items = bars;107 const p = state.params;108 if (def.transform) {109 const opts = {};110 if (def.id === 'renko' && p.brick > 0) opts.brick = p.brick;111 if (def.id === 'rangebars' && p.range > 0) opts.range = p.range;112 if (def.id === 'kagi' && p.reversal > 0) opts.reversal = p.reversal;113 if (def.id === 'pnf') { if (p.box > 0) opts.box = p.box; if (p.pnfRev > 0) opts.reversal = p.pnfRev; }114 if (def.id === 'linebreak' && p.lines > 0) opts.lines = p.lines;115 items = HF.transforms[def.transform](bars, opts);116 if (!items.length) { toast('Pas assez de données pour ce type sur ce timeframe', true); items = bars; }117 }118119 const spec = {120 items,121 draw: def.draw,122 decimals: raw.decimals,123 logScale: state.log && !def.compare,124 defaultVisible: def.vis,125 hasVolume: raw.hasVolume,126 showVolume: raw.hasVolume && !def.noVol && !def.compare,127 overlays: [],128 panes: [],129 anchor: def.id === 'baseline' && items.length ? items[0].c : null,130 volumeProfile: state.inds.includes('vprofile') && raw.hasVolume && !def.transform && !def.compare,131 meta: { symbol: state.symbol, tfLabel: (TFS.find(t => t.id === state.tf) || {}).label || state.tf },132 };133134 if (!def.compare) {135 for (const ov of OVERLAYS) {136 if (!state.inds.includes(ov.id)) continue;137 if (ov.needVol && !raw.hasVolume) continue;138 if (ov.intraday && (state.tf === 'D' || state.tf === 'W')) continue;139 const made = ov.make(items, p);140 spec.overlays.push({ ...made, color: schemeSeries()[ov.slot], label: ov.label });141 }142 for (const pn of PANES) {143 if (!state.inds.includes(pn.id)) continue;144 spec.panes.push({ id: pn.id, kind: pn.kind, label: pn.label, data: pn.make(items, p) });145 }146 } else {147 buildCompare(spec);148 }149 chart.setSeries(spec, keepView);150 if (spec._absMain) { // comparaison : base % = première barre visible151 rebaseCompare(spec, chart.visibleRange()[0]);152 chart.render();153 }154 renderLegendStatic(spec);155 }156157 function schemeSeries() { return chart ? chart.th.series : []; }158159 /* Comparaison : tout est ré-exprimé en % depuis le début de la fenêtre.160 * Slots catégoriels FIXES (jamais recyclés) : le symbole principal garde161 * le slot 1, chaque comparé garde le slot de sa position. */162 function buildCompare(spec) {163 const main = raw.parsed;164 if (!main.length) return;165 spec.decimals = 2;166 spec.percent = true;167 spec.draw = 'none';168 spec.overlays = [];169 spec.anchor = null;170 const colors = schemeSeries();171 // séries absolues alignées sur la timeline du symbole principal172 // (report de la dernière valeur connue dans les trous de séance)173 spec._absMain = main;174 spec._absComp = state.compare.slice(0, 3).map((sym, k) => {175 const cr = compareRaw[sym];176 if (!cr) return null;177 const map = new Map(cr.parsed.map(b => [b.t, b.c]));178 let lastC = null;179 const closes = main.map(b => {180 const c = map.get(b.t);181 if (c != null) lastC = c;182 return lastC;183 });184 return { label: sym, color: colors[k + 1], closes };185 }).filter(Boolean);186 spec.items = main.map(b => ({ t: b.t, o: 0, h: 0, l: 0, c: 0, v: b.v }));187 spec.compare = [{ label: state.symbol, color: colors[0], vals: new Array(main.length).fill(null) }];188 for (const s of spec._absComp) spec.compare.push({ label: s.label, color: s.color, vals: new Array(main.length).fill(null) });189 rebaseCompare(spec, 0);190 }191192 /* Rebasage % sur la première barre visible — recalculé au pan/zoom,193 * comme une vraie échelle "percent". */194 let lastRebaseI0 = -1;195 function rebaseCompare(spec, i0) {196 const main = spec._absMain;197 if (!main) return;198 i0 = Math.max(0, Math.min(main.length - 1, i0));199 const baseM = main[i0].c;200 for (let i = 0; i < main.length; i++) {201 const b = main[i], it = spec.items[i];202 it.o = ((b.o / baseM) - 1) * 100;203 it.h = ((b.h / baseM) - 1) * 100;204 it.l = ((b.l / baseM) - 1) * 100;205 it.c = ((b.c / baseM) - 1) * 100;206 spec.compare[0].vals[i] = it.c;207 }208 spec._absComp.forEach((s, k) => {209 let base = null;210 for (let i = i0; i < s.closes.length; i++) { if (s.closes[i] != null) { base = s.closes[i]; break; } }211 const vals = spec.compare[k + 1].vals;212 for (let i = 0; i < s.closes.length; i++) {213 vals[i] = (base && s.closes[i] != null) ? ((s.closes[i] / base) - 1) * 100 : null;214 }215 });216 lastRebaseI0 = i0;217 }218219 /* ------------------------------------------------------------ chargement */220 async function loadMain(keepView) {221 const def = typeDef();222 loading = true;223 $('#chart-wrap').classList.add('loading');224 try {225 tfAvail = HF.data.tfAvailability(state.group);226 if (!tfAvail[state.tf]) {227 state.tf = tfAvail['60'] ? '60' : 'D';228 syncToolbar();229 }230 curWant = curWant || TF_WANT[state.tf] || 5000;231 raw = await HF.data.bars(state.group, state.symbol, state.tf, curWant);232 if (def.compare) {233 await Promise.all(state.compare.slice(0, 3).map(async s => {234 compareRaw[s] = await HF.data.bars(state.group, s, state.tf, curWant);235 }));236 }237 buildSpec(keepView);238 } catch (e) {239 toast('Erreur : ' + e.message, true);240 } finally {241 loading = false;242 $('#chart-wrap').classList.remove('loading');243 syncTfButtons();244 }245 }246247 function reload(resetLimit) {248 if (resetLimit) curWant = 0;249 loadMain(false);250 save();251 syncToolbar();252 }253254 /* pan/zoom : rebase la comparaison sur la 1re barre visible,255 * puis étend l'historique quand on bute sur le bord gauche */256 function maybeExtend(view) {257 if (chart.spec && chart.spec._absMain) {258 const i0 = Math.max(0, Math.floor(view.first));259 if (i0 !== lastRebaseI0) {260 rebaseCompare(chart.spec, i0);261 chart.render();262 }263 }264 if (loading || !raw) return;265 if (view.first > 40) return;266 if (raw.srcLimitReached) return;267 if (raw.parsed.length < curWant * 0.9) return; // le lac n'a pas plus d'historique268 curWant = Math.min(HF.data.MAX_API_LIMIT, curWant * 2);269 loadMain(true);270 }271272 /* ------------------------------------------------------------ légende OHLCV */273 function renderLegendStatic(spec) {274 const el = $('#legend');275 const def = typeDef();276 const tfLabel = (TFS.find(t => t.id === state.tf) || {}).label || state.tf;277 let html = `<span class="sym">${state.symbol}</span><span class="dim">· ${tfLabel} · ${def.label}</span><span id="ohlcv"></span>`;278 if (def.compare) {279 const colors = schemeSeries();280 const syms = [state.symbol, ...state.compare.slice(0, 3)];281 html = `<span class="dim">Comparaison % · ${tfLabel}</span>` + syms.map((s, k) =>282 `<span class="chip-sym"><i style="background:${colors[k]}"></i>${s}</span>`).join('');283 }284 el.innerHTML = html;285 updateOHLCV(null, null);286 }287288 function updateOHLCV(i, item) {289 const el = $('#ohlcv');290 if (!el || !raw || !chart.spec) return;291 const spec = chart.spec;292 const it = item || spec.items[spec.items.length - 1];293 if (!it) { el.textContent = ''; return; }294 const idx = i != null ? i : spec.items.length - 1;295 const prev = spec.items[idx - 1];296 const ref = prev ? prev.c : it.o;297 const chg = ref ? ((it.c / ref) - 1) * 100 : 0;298 const cls = it.c >= ref ? 'up' : 'dn';299 const f = v => HF.fmt.price(v, spec.decimals);300 el.innerHTML =301 `<span class="k">O</span>${f(it.o)} <span class="k">H</span>${f(it.h)} ` +302 `<span class="k">L</span>${f(it.l)} <span class="k">C</span><b class="${cls}">${f(it.c)}</b> ` +303 `<b class="${cls}">${chg >= 0 ? '+' : ''}${chg.toFixed(2)} %</b>` +304 (spec.hasVolume && it.v ? ` <span class="k">Vol</span>${HF.fmt.vol(it.v)}` : '');305 }306307 /* ------------------------------------------------------------ sidebar */308 function renderGroupTabs() {309 const tabs = $('#group-tabs');310 tabs.innerHTML = '';311 for (const g of HF.data.GROUPS) {312 const b = document.createElement('button');313 b.textContent = g.label;314 b.className = g.key === state.group ? 'active' : '';315 b.onclick = async () => {316 state.group = g.key;317 state.compare = [];318 state.symbol = DEFAULT_SYMBOL[g.key] || '';319 renderGroupTabs();320 await renderSymbolList(true);321 reload(true);322 };323 tabs.appendChild(b);324 }325 }326327 async function renderSymbolList(validate) {328 const ul = $('#symbols');329 ul.innerHTML = '<li class="dim">Chargement…</li>';330 let list;331 try {332 list = await HF.data.tickers(state.group);333 } catch (e) {334 ul.innerHTML = '';335 toast('Tickers : ' + e.message, true);336 return;337 }338 if (validate && !list.includes(state.symbol)) state.symbol = list[0] || '';339 const q = ($('#search').value || '').toUpperCase();340 ul.innerHTML = '';341 const frag = document.createDocumentFragment();342 let shown = 0;343 for (const s of list) {344 if (q && !s.toUpperCase().includes(q)) continue;345 if (++shown > 800) break; // le champ recherche affine au-delà346 const li = document.createElement('li');347 li.textContent = s;348 if (s === state.symbol) li.className = 'active';349 if (state.compare.includes(s)) li.className += ' compared';350 li.onclick = () => {351 if (compareMode() && s !== state.symbol) { toggleCompareSymbol(s); renderSymbolList(); return; }352 state.symbol = s;353 renderSymbolList();354 document.body.classList.remove('drawer-open');355 reload(true);356 };357 frag.appendChild(li);358 }359 ul.appendChild(frag);360 $('#sym-count').textContent = `${list.length} symboles`;361 }362363 function compareMode() { return typeDef().compare; }364365 function toggleCompareSymbol(sym) {366 const i = state.compare.indexOf(sym);367 if (i >= 0) state.compare.splice(i, 1);368 else {369 if (state.compare.length >= 3) { toast('Maximum 4 séries (lisibilité) — retirez-en une d’abord'); return; }370 state.compare.push(sym);371 }372 reload(false);373 }374375 /* ------------------------------------------------------------ toolbar */376 function syncTfButtons() {377 $$('#tf-row button').forEach(b => {378 b.classList.toggle('active', b.dataset.tf === state.tf);379 b.disabled = tfAvail[b.dataset.tf] === false;380 b.title = b.disabled ? 'Pas encore dans le lac de données' : '';381 });382 }383384 function syncToolbar() {385 syncTfButtons();386 $('#type-select').value = state.type;387 $('#btn-log').classList.toggle('active', state.log);388 $('#btn-theme').textContent = state.theme === 'dark' ? '☀︎' : '☾';389 $('#scheme-select').value = state.scheme;390 document.documentElement.dataset.theme = state.theme;391 document.documentElement.dataset.scheme = state.scheme;392 $$('#ind-menu input').forEach(cb => { cb.checked = state.inds.includes(cb.value); });393 renderParams();394 if (chart) { chart.readTheme(); chart.render(); }395 }396397 function buildToolbar() {398 const tfRow = $('#tf-row');399 for (const tf of TFS) {400 const b = document.createElement('button');401 b.textContent = tf.label;402 b.dataset.tf = tf.id;403 b.onclick = () => { if (!b.disabled) { state.tf = tf.id; reload(true); } };404 tfRow.appendChild(b);405 }406 const sel = $('#type-select');407 for (const t of CHART_TYPES) {408 const o = document.createElement('option');409 o.value = t.id; o.textContent = t.label;410 sel.appendChild(o);411 }412 sel.onchange = () => { state.type = sel.value; reload(false); updateCompareHint(); };413414 const menu = $('#ind-menu');415 const mk = (id, label, note) => {416 const l = document.createElement('label');417 l.innerHTML = `<input type="checkbox" value="${id}"> ${label}${note ? ` <em>${note}</em>` : ''}`;418 l.querySelector('input').onchange = e => {419 if (e.target.checked) state.inds.push(id);420 else state.inds = state.inds.filter(x => x !== id);421 reload(false);422 };423 menu.appendChild(l);424 };425 for (const ov of OVERLAYS) mk(ov.id, ov.label, ov.needVol ? 'volume requis' : '');426 mk('vprofile', 'Profil de volume', 'volume requis');427 for (const pn of PANES) mk(pn.id, pn.label + (pn.id === 'rsi' ? ' 14' : pn.id === 'atr' ? ' 14' : ' 12·26·9'), '');428429 $('#btn-log').onclick = () => { state.log = !state.log; buildSpec(true); save(); syncToolbar(); };430 $('#btn-theme').onclick = () => { state.theme = state.theme === 'dark' ? 'light' : 'dark'; save(); syncToolbar(); };431 $('#scheme-select').onchange = e => { state.scheme = e.target.value; save(); syncToolbar(); };432 $('#btn-gallery').onclick = openGallery;433 $('#btn-table').onclick = openTable;434 $('#btn-export').onclick = openExport;435 $('#search').oninput = () => renderSymbolList();436 // drawer mobile437 $('#btn-symbols').onclick = () => document.body.classList.toggle('drawer-open');438 $('#drawer-backdrop').onclick = () => document.body.classList.remove('drawer-open');439 document.addEventListener('click', e => {440 for (const dd of $$('.dropdown')) {441 if (!dd.contains(e.target)) dd.classList.remove('open');442 }443 });444 for (const dd of $$('.dropdown > button')) {445 dd.onclick = e => { e.stopPropagation(); dd.parentElement.classList.toggle('open'); };446 }447 for (const ov of $$('.overlay-panel')) {448 ov.addEventListener('click', e => { if (e.target === ov) ov.classList.remove('open'); });449 }450 }451452 function updateCompareHint() {453 $('#compare-hint').style.display = compareMode() ? 'block' : 'none';454 }455456 /* Réglages du type actif (brique, renversement, boîtes…) + périodes. */457 function renderParams() {458 const host = $('#params');459 const def = typeDef();460 host.innerHTML = '';461 const fields = [];462 if (def.id === 'renko') fields.push(['brick', 'Taille de brique (vide = auto ATR)']);463 if (def.id === 'rangebars') fields.push(['range', 'Range par barre (vide = auto)']);464 if (def.id === 'kagi') fields.push(['reversal', 'Renversement (vide = auto)']);465 if (def.id === 'pnf') { fields.push(['box', 'Taille de boîte (vide = auto)']); fields.push(['pnfRev', 'Renversement (boîtes, défaut 3)']); }466 if (def.id === 'linebreak') fields.push(['lines', 'Lignes à casser (défaut 3)']);467 if (state.inds.includes('sma20')) fields.push(['sma20', 'Période SMA 20']);468 if (state.inds.includes('sma50')) fields.push(['sma50', 'Période SMA 50']);469 if (state.inds.includes('ema20')) fields.push(['ema20', 'Période EMA 20']);470 if (state.inds.includes('boll')) { fields.push(['bollN', 'Bollinger : période']); fields.push(['bollK', 'Bollinger : écart-type']); }471 if (state.inds.includes('rsi')) fields.push(['rsiN', 'Période RSI']);472 if (state.inds.includes('atr')) fields.push(['atrN', 'Période ATR']);473 if (!fields.length) {474 host.innerHTML = '<p class="dim">Aucun réglage pour cette configuration.</p>';475 return;476 }477 for (const [key, label] of fields) {478 const row = document.createElement('label');479 row.className = 'param-row';480 row.innerHTML = `<span>${label}</span><input type="number" step="any" value="${state.params[key] ?? ''}">`;481 row.querySelector('input').onchange = e => {482 const v = parseFloat(e.target.value);483 if (isFinite(v) && v > 0) state.params[key] = v; else delete state.params[key];484 buildSpec(true);485 save();486 };487 host.appendChild(row);488 }489 }490491 /* ------------------------------------------------------------ galerie */492 async function openGallery() {493 const overlay = $('#gallery');494 overlay.classList.add('open');495 const grid = $('#gallery-grid');496 grid.innerHTML = '';497 let daily;498 try {499 daily = (await HF.data.bars(state.group, state.symbol, 'D', 400)).parsed.slice(-260);500 } catch (e) { toast('Erreur galerie : ' + e.message, true); return; }501 for (const t of CHART_TYPES) {502 const card = document.createElement('button');503 card.className = 'g-card' + (t.id === state.type ? ' active' : '');504 card.innerHTML = `<canvas></canvas><strong>${t.label}</strong><span>${t.desc}</span>`;505 card.onclick = () => { state.type = t.id; overlay.classList.remove('open'); reload(false); updateCompareHint(); };506 grid.appendChild(card);507 let items = daily;508 if (t.transform) items = HF.transforms[t.transform](daily, {});509 const draw = t.compare ? 'line' : t.draw;510 // les types à corps ont besoin de barres larges pour être reconnaissables511 const n = ['candles', 'hollow', 'ohlc', 'bricks'].includes(draw) ? 55 : 120;512 requestAnimationFrame(() => HF.mini(card.querySelector('canvas'), draw, items.slice(-n), chart.th));513 }514 $('#gallery-close').onclick = () => overlay.classList.remove('open');515 }516517 /* ------------------------------------------------------------ export snippet */518 const MAX_EXPORT_BARS = 3000;519520 /* Barres brutes couvrant la fenêtre visible (les transforms — Renko, Kagi… —521 * sont recalculées à l'identique dans l'embed à partir de ces barres). */522 function exportBars() {523 const def = typeDef();524 const [i0, i1] = chart.visibleRange();525 let bars;526 if (def.transform) {527 const t0 = chart.spec.items[i0] ? chart.spec.items[i0].t : raw.parsed[0].t;528 bars = raw.parsed.filter(b => b.t >= t0);529 } else {530 bars = raw.parsed.slice(i0, i1 + 1);531 }532 return bars.slice(-MAX_EXPORT_BARS);533 }534535 function buildEmbedSnippet() {536 const def = typeDef();537 const bars = exportBars();538 const d = raw.decimals;539 const r = v => +v.toFixed(d);540 const tfLabel = (TFS.find(t => t.id === state.tf) || {}).label || state.tf;541 const inds = state.inds.filter(x => x !== 'vprofile');542 const cfg = {543 symbol: state.symbol, tf: tfLabel, type: state.type, typeLabel: def.label,544 theme: state.theme, scheme: state.scheme,545 inds, params: state.params, decimals: d,546 hasVolume: raw.hasVolume && !def.noVol,547 visible: Math.min(bars.length, Math.round(chart.view.count)),548 bars: bars.map(b => [b.t, r(b.o), r(b.h), r(b.l), r(b.c), Math.round(b.v)]),549 };550 const id = 'hfchart-' + Math.random().toString(36).slice(2, 8);551 const base = location.origin.startsWith('http') ? location.origin : 'https://www.hfchart.io';552 return {553 count: bars.length,554 code:555`<!-- HFChart — ${state.symbol} · ${tfLabel} · ${def.label} — ${bars.length} barres intégrées556 Graphe interactif autonome (zoom, pan, crosshair), aucune requête réseau pour les données.557 https://www.hfchart.io — Simon-Pierre Boucher — contact@spboucher.ai -->558<div id="${id}" style="width:100%;height:420px"></div>559<script>560(window.HFChartEmbedQueue = window.HFChartEmbedQueue || []).push({561 el: "#${id}",562 config: ${JSON.stringify(cfg)}563});564</script>565<script async src="${base}/embed.js"></script>`,566 };567 }568569 function buildIframeSnippet() {570 const base = location.origin.startsWith('http') ? location.origin : 'https://www.hfchart.io';571 const q = new URLSearchParams({572 group: state.group, symbol: state.symbol, tf: state.tf, type: state.type,573 theme: state.theme, scheme: state.scheme, embed: '1',574 });575 if (state.inds.length) q.set('inds', state.inds.join(','));576 if (state.compare.length && compareMode()) q.set('compare', state.compare.join(','));577 if (state.log) q.set('log', '1');578 return {579 code:580`<!-- HFChart live — ${state.symbol} — https://www.hfchart.io -->581<iframe src="${base}/?${q.toString()}"582 style="width:100%;height:480px;border:0;border-radius:8px"583 loading="lazy" title="HFChart — ${state.symbol}"></iframe>`,584 };585 }586587 let exportTab = 'html';588 function renderExport() {589 const noteEl = $('#export-note');590 const codeEl = $('#export-code');591 $$('#export-tabs button').forEach(b => b.classList.toggle('active', b.dataset.tab === exportTab));592 if (exportTab === 'html') {593 if (compareMode()) {594 noteEl.textContent = 'Le mode Comparaison % s’exporte en iframe live (onglet suivant) — le snippet HTML autonome couvre les 13 autres types.';595 codeEl.value = '';596 return;597 }598 const s = buildEmbedSnippet();599 const kb = Math.round(s.code.length / 1024);600 noteEl.textContent =601 `Graphe 100 % autonome : ${s.count} barres visibles intégrées dans la page (~${kb} Ko), ` +602 `interactif (zoom, pan, crosshair), indicateurs et réglages inclus. Collez tel quel dans n’importe quelle page HTML.`;603 codeEl.value = s.code;604 } else {605 noteEl.textContent = 'Version live : le graphe embarqué charge la plateforme (données à jour via hfmarketdata.io).';606 codeEl.value = buildIframeSnippet().code;607 }608 }609610 function openExport() {611 if (!raw || !chart.spec) return;612 $('#export-view').classList.add('open');613 renderExport();614 $('#export-copied').textContent = '';615 $('#export-close').onclick = () => $('#export-view').classList.remove('open');616 $$('#export-tabs button').forEach(b => {617 b.onclick = () => { exportTab = b.dataset.tab; renderExport(); };618 });619 $('#export-copy').onclick = async () => {620 const code = $('#export-code').value;621 if (!code) return;622 try {623 await navigator.clipboard.writeText(code);624 } catch (e) {625 $('#export-code').select();626 document.execCommand('copy');627 }628 $('#export-copied').textContent = '✓ copié dans le presse-papiers';629 setTimeout(() => { $('#export-copied').textContent = ''; }, 2500);630 };631 }632633 /* ------------------------------------------------------------ vue table (accessibilité) */634 function openTable() {635 if (!chart || !chart.spec) return;636 const overlay = $('#table-view');637 overlay.classList.add('open');638 const [i0, i1] = chart.visibleRange();639 const items = chart.spec.items.slice(i0, i1 + 1).slice(-500);640 const f = v => HF.fmt.price(v, chart.spec.decimals);641 const hasV = chart.spec.hasVolume;642 let html = `<tr><th>Temps</th><th>Ouverture</th><th>Haut</th><th>Bas</th><th>Clôture</th>${hasV ? '<th>Volume</th>' : ''}</tr>`;643 for (let k = items.length - 1; k >= 0; k--) {644 const b = items[k];645 html += `<tr><td>${HF.fmt.time(b.t)}</td><td>${f(b.o)}</td><td>${f(b.h)}</td><td>${f(b.l)}</td><td>${f(b.c)}</td>${hasV ? `<td>${HF.fmt.vol(b.v)}</td>` : ''}</tr>`;646 }647 $('#table-view table').innerHTML = html;648 $('#table-title').textContent = `${state.symbol} — ${items.length} barres visibles (max 500)`;649 $('#table-close').onclick = () => overlay.classList.remove('open');650 }651652 /* ------------------------------------------------------------ boot */653 async function boot() {654 document.documentElement.dataset.theme = state.theme;655 document.documentElement.dataset.scheme = state.scheme;656 if (new URLSearchParams(location.search).get('embed') === '1') {657 document.body.classList.add('embed'); // mode iframe : chart seul658 }659 chart = new HF.HFChart($('#chart-wrap'), {660 onCrosshair: updateOHLCV,661 onViewChange: maybeExtend,662 });663 buildToolbar();664 syncToolbar();665 updateCompareHint();666 try {667 await HF.data.loadStatus();668 } catch (e) {669 toast('API hfmarketdata.io injoignable : ' + e.message, true);670 return;671 }672 renderGroupTabs();673 await renderSymbolList(true);674 reload(true);675 const panel = new URLSearchParams(location.search).get('panel');676 if (panel === 'gallery') openGallery();677 if (panel === 'table') setTimeout(openTable, 1500);678 if (new URLSearchParams(location.search).get('debug') === '1') {679 setTimeout(() => {680 $('#legend').textContent =681 `iw:${innerWidth} body:${document.body.clientWidth} wrap:${$('#chart-wrap').clientWidth}` +682 ` area:${$('#chart-area').clientWidth} layout:${$('#layout').clientWidth} chartW:${chart.w}|axis:${chart.axisW}`;683 }, 3000);684 }685 }686687 boot();688})();689