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%
12.0 KB · 349 lines javascript
Raw Blame History
1/* ============================================================================2 * HFChart — renderers canvas, from scratch3 * Author : Simon-Pierre Boucher — contact@spboucher.ai4 * ==========================================================================*/5/* Chaque renderer reçoit un environnement :6 *   { ctx, items, i0, i1, X(i)→px centre, Y(prix)→px, rect, bw, th }7 * th = tokens du thème (up, dn, ink, grid, series[], surface, …).8 * Specs de marques : corps fins avec 2px d'écart entre barres adjacentes,9 * lignes 2px, extrémités arrondies discrètes. */10(function () {11  'use strict';12  const HF = (window.HF = window.HF || {});1314  function bodyWidth(bw) {15    // écart de 2px garanti entre corps adjacents dès que la place existe16    return Math.max(1, Math.min(bw - 2, Math.round(bw * 0.8)));17  }1819  function roundRect(ctx, x, y, w, h, r) {20    if (h < 0) { y += h; h = -h; }21    r = Math.min(r, w / 2, h / 2);22    if (r < 0.5) { ctx.rect(x, y, w, h); return; }23    ctx.moveTo(x + r, y);24    ctx.arcTo(x + w, y, x + w, y + h, r);25    ctx.arcTo(x + w, y + h, x, y + h, r);26    ctx.arcTo(x, y + h, x, y, r);27    ctx.arcTo(x, y, x + w, y, r);28    ctx.closePath();29  }3031  /* Fast path haute densité : 1 trait hi-lo par barre quand bw < 2px. */32  function denseHiLo(env) {33    const { ctx, items, i0, i1, X, Y, th } = env;34    ctx.lineWidth = 1;35    for (const up of [true, false]) {36      ctx.strokeStyle = up ? th.up : th.dn;37      ctx.beginPath();38      for (let i = i0; i <= i1; i++) {39        const b = items[i];40        if (!b || (b.c >= b.o) !== up) continue;41        const x = Math.round(X(i)) + 0.5;42        ctx.moveTo(x, Y(b.h));43        ctx.lineTo(x, Y(b.l));44      }45      ctx.stroke();46    }47  }4849  function candles(env, hollow) {50    const { ctx, items, i0, i1, X, Y, bw, th } = env;51    if (bw < 2) return denseHiLo(env);52    const w = bodyWidth(bw);53    const wick = bw >= 10 ? 1.5 : 1;54    const r = w >= 6 ? 1.5 : 0;55    for (const up of [true, false]) {56      const col = up ? th.up : th.dn;57      // mèches58      ctx.strokeStyle = col;59      ctx.lineWidth = wick;60      ctx.beginPath();61      for (let i = i0; i <= i1; i++) {62        const b = items[i];63        if (!b || (b.up !== undefined ? b.up : b.c >= b.o) !== up) continue;64        const x = Math.round(X(i)) + (wick === 1 ? 0.5 : 0);65        const top = Math.max(b.o, b.c), bot = Math.min(b.o, b.c);66        if (b.h > top) { ctx.moveTo(x, Y(b.h)); ctx.lineTo(x, Y(top)); }67        if (b.l < bot) { ctx.moveTo(x, Y(bot)); ctx.lineTo(x, Y(b.l)); }68      }69      ctx.stroke();70      // corps71      ctx.beginPath();72      for (let i = i0; i <= i1; i++) {73        const b = items[i];74        if (!b || (b.up !== undefined ? b.up : b.c >= b.o) !== up) continue;75        const x = Math.round(X(i) - w / 2);76        const y0 = Y(b.o), y1 = Y(b.c);77        const h = Math.abs(y1 - y0) < 1 ? 1 : y1 - y0;78        roundRect(ctx, x, y0, w, h, r);79      }80      if (hollow && up) {81        ctx.strokeStyle = col;82        ctx.lineWidth = Math.min(2, Math.max(1, w / 5));83        ctx.stroke();84      } else {85        ctx.fillStyle = col;86        ctx.fill();87      }88    }89  }9091  function ohlcBars(env) {92    const { ctx, items, i0, i1, X, Y, bw, th } = env;93    if (bw < 3) return denseHiLo(env);94    const tick = Math.max(2, Math.min(bw * 0.4, 8));95    ctx.lineWidth = bw >= 8 ? 1.5 : 1;96    for (const up of [true, false]) {97      ctx.strokeStyle = up ? th.up : th.dn;98      ctx.beginPath();99      for (let i = i0; i <= i1; i++) {100        const b = items[i];101        if (!b || (b.c >= b.o) !== up) continue;102        const x = Math.round(X(i)) + 0.5;103        ctx.moveTo(x, Y(b.h)); ctx.lineTo(x, Y(b.l));104        ctx.moveTo(x - tick, Y(b.o)); ctx.lineTo(x, Y(b.o));105        ctx.moveTo(x, Y(b.c)); ctx.lineTo(x + tick, Y(b.c));106      }107      ctx.stroke();108    }109  }110111  function linePath(env, color, width) {112    const { ctx, items, i0, i1, X, Y } = env;113    ctx.strokeStyle = color;114    ctx.lineWidth = width || 2;115    ctx.lineJoin = 'round';116    ctx.lineCap = 'round';117    ctx.beginPath();118    let started = false;119    for (let i = i0; i <= i1; i++) {120      const b = items[i];121      if (!b) continue;122      const x = X(i), y = Y(b.c);123      if (!started) { ctx.moveTo(x, y); started = true; } else ctx.lineTo(x, y);124    }125    ctx.stroke();126  }127128  function line(env) { linePath(env, env.th.accent, 2); }129130  function step(env) {131    const { ctx, items, i0, i1, X, Y, th } = env;132    ctx.strokeStyle = th.accent;133    ctx.lineWidth = 2;134    ctx.lineJoin = 'round';135    ctx.beginPath();136    let started = false, py = 0;137    for (let i = i0; i <= i1; i++) {138      const b = items[i];139      if (!b) continue;140      const x = X(i), y = Y(b.c);141      if (!started) { ctx.moveTo(x, y); started = true; }142      else { ctx.lineTo(x, py); ctx.lineTo(x, y); }143      py = y;144    }145    ctx.stroke();146  }147148  function area(env) {149    const { ctx, items, i0, i1, X, Y, rect, th } = env;150    const grad = ctx.createLinearGradient(0, rect.y, 0, rect.y + rect.h);151    grad.addColorStop(0, th.alpha(th.accent, 0.30));152    grad.addColorStop(0.55, th.alpha(th.accent, 0.10));153    grad.addColorStop(1, th.alpha(th.accent, 0));154    ctx.beginPath();155    let started = false, lastX = 0, firstX = 0;156    for (let i = i0; i <= i1; i++) {157      const b = items[i];158      if (!b) continue;159      const x = X(i), y = Y(b.c);160      if (!started) { ctx.moveTo(x, y); firstX = x; started = true; } else ctx.lineTo(x, y);161      lastX = x;162    }163    if (!started) return;164    ctx.lineTo(lastX, rect.y + rect.h);165    ctx.lineTo(firstX, rect.y + rect.h);166    ctx.closePath();167    ctx.fillStyle = grad;168    ctx.fill();169    linePath(env, th.accent, 2);170  }171172  /* Baseline : ancre = close de la première barre chargée (env.anchor).173   * Au-dessus → up, en dessous → dn (aires + segments de ligne). */174  function baseline(env) {175    const { ctx, items, i0, i1, X, Y, rect, th } = env;176    const anchor = env.anchor;177    const yA = Math.max(rect.y, Math.min(rect.y + rect.h, Y(anchor)));178    for (const side of ['up', 'dn']) {179      ctx.save();180      ctx.beginPath();181      if (side === 'up') ctx.rect(rect.x, rect.y, rect.w, yA - rect.y);182      else ctx.rect(rect.x, yA, rect.w, rect.y + rect.h - yA);183      ctx.clip();184      const col = side === 'up' ? th.up : th.dn;185      const grad = side === 'up'186        ? ctx.createLinearGradient(0, rect.y, 0, yA)187        : ctx.createLinearGradient(0, yA, 0, rect.y + rect.h);188      grad.addColorStop(side === 'up' ? 0 : 1, th.alpha(col, 0.28));189      grad.addColorStop(side === 'up' ? 1 : 0, th.alpha(col, 0.02));190      // aire fermée sur la baseline191      ctx.beginPath();192      let started = false, lastX = 0, firstX = 0;193      for (let i = i0; i <= i1; i++) {194        const b = items[i];195        if (!b) continue;196        const x = X(i), y = Y(b.c);197        if (!started) { ctx.moveTo(x, y); firstX = x; started = true; } else ctx.lineTo(x, y);198        lastX = x;199      }200      if (started) {201        ctx.lineTo(lastX, yA); ctx.lineTo(firstX, yA); ctx.closePath();202        ctx.fillStyle = grad; ctx.fill();203        linePath(env, col, 2);204      }205      ctx.restore();206    }207    // trait d'ancre208    ctx.strokeStyle = th.baselineAxis;209    ctx.setLineDash([4, 4]);210    ctx.lineWidth = 1;211    ctx.beginPath();212    ctx.moveTo(rect.x, Math.round(yA) + 0.5);213    ctx.lineTo(rect.x + rect.w, Math.round(yA) + 0.5);214    ctx.stroke();215    ctx.setLineDash([]);216  }217218  /* Briques pleines (Renko, Line Break, Range) — chandelles sans mèche. */219  function bricks(env) { candles(env, false); }220221  function kagiDraw(env) {222    const { ctx, items, i0, i1, X, Y, th } = env;223    for (const thick of [false, true]) {224      ctx.strokeStyle = thick ? th.up : th.dn;225      ctx.lineWidth = thick ? 2.5 : 1.25;226      ctx.lineJoin = 'miter';227      ctx.beginPath();228      for (let i = Math.max(1, i0); i <= i1; i++) {229        const b = items[i];230        if (!b || b.up !== thick) continue;231        const x = Math.round(X(i)) + 0.5;232        const xPrev = Math.round(X(i - 1)) + 0.5;233        ctx.moveTo(xPrev, Y(b.o));    // épaule horizontale depuis la ligne précédente234        ctx.lineTo(x, Y(b.o));235        ctx.lineTo(x, Y(b.c));        // ligne verticale236      }237      const first = items[i0];238      if (first && first.up === thick && i0 === 0) {239        ctx.moveTo(Math.round(X(0)) + 0.5, Y(first.o));240        ctx.lineTo(Math.round(X(0)) + 0.5, Y(first.c));241      }242      ctx.stroke();243    }244  }245246  function pnfDraw(env) {247    const { ctx, items, i0, i1, X, Y, bw, th } = env;248    if (bw < 3) return denseHiLo(env);249    const w = bodyWidth(bw);250    ctx.lineWidth = Math.max(1, Math.min(2, w / 6));251    for (let i = i0; i <= i1; i++) {252      const col = items[i];253      if (!col || !col.box) continue;254      const x = X(i);255      const glyphW = Math.min(w, Math.abs(Y(col.lo) - Y(col.lo + col.box)) * 0.9);256      ctx.strokeStyle = col.type === 'X' ? th.up : th.dn;257      ctx.beginPath();258      for (let k = 0; k < col.n; k++) {259        const p0 = col.lo + k * col.box;260        const yB = Y(p0), yT = Y(p0 + col.box);261        const half = Math.min(glyphW / 2, Math.abs(yB - yT) / 2 - 1);262        if (half < 1) { // trop dense : trait plein263          ctx.moveTo(x, yB); ctx.lineTo(x, yT);264          continue;265        }266        const cy = (yB + yT) / 2;267        if (col.type === 'X') {268          ctx.moveTo(x - half, cy + half); ctx.lineTo(x + half, cy - half);269          ctx.moveTo(x - half, cy - half); ctx.lineTo(x + half, cy + half);270        } else {271          ctx.moveTo(x + half, cy);272          ctx.arc(x, cy, half, 0, Math.PI * 2);273        }274      }275      ctx.stroke();276    }277  }278279  /* Colonnes de volume, colorées par direction de la barre (alpha pour rester280   * une couche secondaire), 2px d'écart. */281  function volumeColumns(env, maxV) {282    const { ctx, items, i0, i1, X, rect, bw, th } = env;283    const w = Math.max(1, Math.min(bw - 2, Math.round(bw * 0.8)));284    const y0 = rect.y + rect.h;285    for (const up of [true, false]) {286      // dégradé vertical : plus dense à la base, léger vers le haut du pane287      const col = up ? th.up : th.dn;288      const grad = ctx.createLinearGradient(0, rect.y, 0, y0);289      grad.addColorStop(0, th.alpha(col, 0.28));290      grad.addColorStop(1, th.alpha(col, 0.6));291      ctx.fillStyle = grad;292      ctx.beginPath();293      for (let i = i0; i <= i1; i++) {294        const b = items[i];295        if (!b || !(b.v > 0) || (b.up !== undefined ? b.up : b.c >= b.o) !== up) continue;296        const h = Math.max(1, (b.v / maxV) * (rect.h - 4));297        roundRect(ctx, Math.round(X(i) - w / 2), y0 - h, w, h, w >= 6 ? 2 : 0);298      }299      ctx.fill();300    }301  }302303  /* Ligne d'indicateur (overlay ou pane) sur un tableau de valeurs alignées. */304  function indicatorLine(env, vals, color, width, dash) {305    const { ctx, i0, i1, X, Y } = env;306    ctx.strokeStyle = color;307    ctx.lineWidth = width || 2;308    ctx.lineJoin = 'round';309    if (dash) ctx.setLineDash(dash);310    ctx.beginPath();311    let started = false;312    for (let i = i0; i <= i1; i++) {313      const v = vals[i];314      if (v == null) { continue; }315      const x = X(i), y = Y(v);316      if (!started) { ctx.moveTo(x, y); started = true; } else ctx.lineTo(x, y);317    }318    ctx.stroke();319    if (dash) ctx.setLineDash([]);320  }321322  /* Remplissage entre deux tableaux (bandes Bollinger / VWAP). */323  function bandFill(env, upVals, loVals, color) {324    const { ctx, i0, i1, X, Y } = env;325    ctx.fillStyle = color;326    ctx.beginPath();327    let started = false;328    const pts = [];329    for (let i = i0; i <= i1; i++) {330      if (upVals[i] == null || loVals[i] == null) continue;331      const x = X(i);332      if (!started) { ctx.moveTo(x, Y(upVals[i])); started = true; } else ctx.lineTo(x, Y(upVals[i]));333      pts.push([x, Y(loVals[i])]);334    }335    for (let k = pts.length - 1; k >= 0; k--) ctx.lineTo(pts[k][0], pts[k][1]);336    if (started) { ctx.closePath(); ctx.fill(); }337  }338339  HF.render = {340    candles: env => candles(env, false),341    hollow: env => candles(env, true),342    ohlc: ohlcBars,343    line, step, area, baseline, bricks,344    kagi: kagiDraw,345    pnf: pnfDraw,346    volumeColumns, indicatorLine, bandFill, linePath, roundRect, denseHiLo,347  };348})();349