v3.0.0

<bmx-trading-chart>

A financial price chart. Candles and the other bar types a trader reads (hollow candles, OHLC bars, line, area, baseline, Heikin-Ashi, Renko, line break) over a time axis that leaves out the hours a market is shut, with volume under them; fifty-nine indicators over the price or in panes of their own, each with its settings; drawings (trend lines, rays, levels, boxes, channels, Fibonacci retracements, measurements and notes) anchored to times and prices; other instruments compared in per cent; bar replay; an order book beside the chart, as a ladder or market depth; a crosshair with the bar's prices and every indicator's values; zoom and scroll by wheel, drag, pinch or keys; linear, logarithmic and percentage scales; bars rolled up to longer intervals; live ticks taken into the last bar. Everything is drawn in the page from the bars it is given, and the engine is also a Node module (dist/node/bmx-trading-chart.mjs).

20 properties · 7 events · 35 methods · 0 parts

Example

Two years of daily bars for an invented share, with a 50-day moving average, Bollinger Bands, volume, RSI, a trend line and a Fibonacci retracement. Scroll or pinch to zoom, drag to move back in time, add indicators, draw, or replay the bars one at a time. "Go live" sends a price every half second into a one-minute chart with an order book beside it, as a market feed would.

Show markup
<bmx-trading-chart id="ex-tc" label="Share price" symbol="NWRL" interval="1D" file-name="nwrl" locale="en-GB" time-zone="Europe/London"></bmx-trading-chart>
<p class="note" id="ex-tc-out" role="status">Two years of daily bars for an invented share, with a 50-day moving average, Bollinger Bands, volume, RSI, a trend line and a Fibonacci retracement. Scroll or pinch to zoom, drag to move back in time, add indicators, draw, or replay the bars one at a time. "Go live" sends a price every half second into a one-minute chart with an order book beside it, as a market feed would.</p>

<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
  <button type="button" id="ex-tc-live">Go live</button>
  <button type="button" id="ex-tc-compare">Compare with an index</button>
  <button type="button" id="ex-tc-daily">Back to daily bars</button>
</div>

<script type="module">
  await customElements.whenDefined('bmx-trading-chart');
  const el = document.getElementById('ex-tc');
  const out = document.getElementById('ex-tc-out');

  /** Invented daily prices: a random walk with drift and calmer and busier spells, weekdays only. */
  function dailyBars(count = 520, seed = 11) {
    let s = seed;
    const rnd = () => ((s = (s * 16807) % 2147483647) / 2147483647);
    const gauss = () => Math.sqrt(-2 * Math.log(rnd())) * Math.cos(2 * Math.PI * rnd());
    const bars = [];
    let t = Date.UTC(2024, 6, 1);
    let price = 142;
    let vol = 0.014;
    while (bars.length < count) {
      const day = new Date(t).getUTCDay();
      if (day !== 0 && day !== 6) {
        vol = Math.min(0.035, Math.max(0.007, vol * (1 + 0.12 * gauss())));
        const open = price * (1 + gauss() * vol * 0.3);
        const close = open * (1 + gauss() * vol + 0.0006);
        const high = Math.max(open, close) * (1 + Math.abs(gauss()) * vol * 0.5);
        const low = Math.min(open, close) * (1 - Math.abs(gauss()) * vol * 0.5);
        const r = x => Math.round(x * 100) / 100;
        bars.push({ time: t, open: r(open), high: r(high), low: r(low), close: r(close), volume: Math.round(2e6 * (0.5 + rnd()) * (1 + Math.abs(close / open - 1) * 40)) });
        price = close;
      }
      t += 86400000;
    }
    return bars;
  }

  const daily = dailyBars();
  function showDaily() {
    el.interval = '1D';
    el.data = daily;
    el.indicators = [
      { type: 'sma', params: { length: 50 } },
      { type: 'bollinger', params: { length: 20, mult: 2 } },
      { type: 'rsi', params: { length: 14 } },
    ];
    // A trend line along recent lows and a retracement of the last big move, anchored to times and prices.
    const n = daily.length;
    const lowest = (from, to) => daily.slice(from, to).reduce((a, b) => (b.low < a.low ? b : a));
    const highest = (from, to) => daily.slice(from, to).reduce((a, b) => (b.high > a.high ? b : a));
    const l1 = lowest(n - 120, n - 70);
    const l2 = lowest(n - 40, n - 5);
    const top = highest(n - 120, n);
    const bottom = lowest(n - 120, n);
    el.drawings = [
      { type: 'trend', points: [{ time: l1.time, price: l1.low }, { time: l2.time, price: l2.low }] },
      { type: 'fibonacci', points: top.time < bottom.time ? [{ time: top.time, price: top.high }, { time: bottom.time, price: bottom.low }] : [{ time: bottom.time, price: bottom.low }, { time: top.time, price: top.high }] },
    ];
    el.setOrderBook(null);
  }
  showDaily();

  let timer = 0;
  document.getElementById('ex-tc-live').addEventListener('click', () => {
    clearInterval(timer);
    // An hour of one-minute bars, then a price every half second.
    const now = Date.now();
    const start = now - (now % 60000) - 60 * 60000;
    let price = daily[daily.length - 1].close;
    const bars = [];
    for (let t = start; t < now - (now % 60000); t += 60000) {
      const open = price;
      const close = open * (1 + (Math.random() - 0.5) * 0.004);
      const r = x => Math.round(x * 100) / 100;
      bars.push({ time: t, open: r(open), high: r(Math.max(open, close) * 1.0008), low: r(Math.min(open, close) * 0.9992), close: r(close), volume: Math.round(20000 + Math.random() * 30000) });
      price = close;
    }
    el.interval = '1m';
    el.data = bars;
    el.indicators = [{ type: 'ema', params: { length: 9 } }, { type: 'vwap' }, { type: 'macd' }];
    el.drawings = [];
    // An order book around the price: sizes growing away from it, changed a little with every tick.
    const book = () => {
      const bids = [];
      const asks = [];
      const best = Math.floor(price * 20) / 20;
      for (let i = 0; i < 40; i++) {
        bids.push([+(best - i * 0.05).toFixed(2), Math.round((80 + i * 25) * (0.5 + Math.random()) * (Math.random() < 0.06 ? 5 : 1))]);
        asks.push([+(best + (i + 1) * 0.05).toFixed(2), Math.round((80 + i * 25) * (0.5 + Math.random()) * (Math.random() < 0.06 ? 5 : 1))]);
      }
      return { bids, asks };
    };
    el.setOrderBook(book());
    el.showOrderBook = true;
    timer = setInterval(() => {
      price *= 1 + (Math.random() - 0.495) * 0.0012;
      el.pushTick({ time: Date.now(), price: Math.round(price * 100) / 100, volume: Math.round(Math.random() * 900) });
      el.setOrderBook(book());
    }, 500);
    out.textContent = 'Live: a price every half second goes into the last one-minute bar; the countdown under the price is the time left in the bar.';
  });
  document.getElementById('ex-tc-compare').addEventListener('click', async () => {
    for (const c of await el.getCompares()) await el.removeCompare(c.id);
    // An invented index over the same days, calmer than the share.
    const index = dailyBars(daily.length, 29).map((b, i) => ({ time: daily[i].time, close: Math.round((4000 + (b.close - 142) * 9) * 100) / 100 }));
    await el.addCompare('Index', index);
    out.textContent = 'The index is drawn over the share; the scale shows the change of each in per cent from the first bar in view.';
  });
  el.addEventListener('bmxDrawingChange', e => {
    out.textContent = `${e.detail.drawings.length} drawing${e.detail.drawings.length === 1 ? '' : 's'} on the chart (${e.detail.reason}).`;
  });
  el.addEventListener('bmxBookClick', e => {
    out.textContent = `${e.detail.side === 'bid' ? 'Sell' : 'Buy'} at ${e.detail.price.toFixed(2)}? (${e.detail.size} resting there.)`;
  });
  el.addEventListener('bmxReplay', e => {
    if (e.detail.state === 'start') out.textContent = 'Replay: play, or step a bar at a time - the indicators only see the bars shown.';
  });
  document.getElementById('ex-tc-daily').addEventListener('click', () => {
    clearInterval(timer);
    el.getCompares().then(list => list.forEach(c => el.removeCompare(c.id)));
    showDaily();
    out.textContent = 'Daily bars again.';
  });

  el.addEventListener('bmxChange', e => {
    if (e.detail.reason === 'indicators') out.textContent = `Indicators: ${e.detail.state.indicators.map(i => i.type.toUpperCase()).join(', ') || 'none'}.`;
  });
  el.addEventListener('bmxExport', e => {
    out.textContent = `Saved ${e.detail.fileName} (${Math.round(e.detail.bytes.length / 1024)} KB).`;
  });
</script>
<bmx-trading-chart symbol="ACME" interval="1D"></bmx-trading-chart>
<script>chart.data = bars; chart.addIndicator('rsi', { length: 14 });</script>

Properties

PropertyAttributeTypeDefaultDescription
bookLevels book-levels number 14 Price levels shown each side of the order book's ladder.
bookOnChart book-on-chart boolean true The order book's sizes drawn against the price axis too.
boxSize box-size number 0 For Renko: the box size. 0: from the average true range.
chartType chart-type BmxTradingChartType 'candles' How the price is drawn.
data data BmxTradeBar[] | unknown[] | string — The bars: objects (time, open, high, low, close, volume), arrays, JSON or CSV text.
drawings drawings Array<Partial<BmxTradeDrawing> & { type: BmxTradeDrawingType; points: BmxTradePoint[] }> | string — Drawings to start with (or their JSON), as getDrawings gives them.
fileName file-name string 'chart' The name saved files get, without the extension.
indicators indicators Array<Partial<BmxTradingChartIndicator> & { type: string }> | string — Indicators to start with: [{ type: 'sma', params: { length: 50 } }] (or its JSON).
interval interval string '' The interval shown (1m, 5m, 1h, 1D, 1W ...): the bars are rolled up to it. Unset: the bars' own.
intervals intervals string[] | string ['1m', '5m', '15m', '30m', '1h', '4h', '1D', '1W', '1M'] The intervals offered (longer than the bars' own only).
label label string — The element's accessible name.
locale locale string — The locale for numbers and dates. Unset: the page's lang.
precision precision number — Decimal places of prices. Unset: as many as the data has.
scale scale BmxTradingChartScale 'linear' The price scale.
showOrderBook show-order-book boolean true The order book beside the chart (it shows once a book is given with setOrderBook).
showVolume show-volume boolean true Volume under the price.
strings strings Partial<BmxTradingChartStrings> | string — Replacements for any of the wording (an object, or its JSON).
symbol symbol string '' The instrument's name, shown in the legend.
timeZone time-zone string — The zone times are shown in and days begin in. Unset: the reader's.
wheelZoom wheel-zoom boolean true Zoom with the mouse wheel. Off: the wheel scrolls the page, and Ctrl with the wheel zooms.

Events

EventDetailDescription
bmxBookClick BmxTradingChartBookClickDetail Fired when a price in the order book is chosen.
bmxChange BmxTradingChartChangeDetail Fired when the interval, chart type, scale, volume, indicators or bars change.
bmxCrosshairMove BmxTradingChartCrosshairDetail | null Fired as the crosshair moves to another bar (null when it leaves).
bmxDrawingChange BmxTradingChartDrawingDetail Fired when a drawing is added, moved, changed or removed.
bmxExport BmxTradingChartExportDetail Fired with the bytes before a picture or CSV is downloaded. Cancelable.
bmxRangeChange BmxTradingChartRangeDetail Fired when the bars in view change (scrolling, zooming, new bars).
bmxReplay BmxTradingChartReplayDetail Fired as a bar replay starts, steps, plays, pauses and ends.

Methods

MethodSignatureDescription
addCompare addCompare(symbol: string, data: BmxTradeBar[] | unknown[] | string, options?: { color?: string; }) => Promise<string> Draws another instrument's prices over the chart for comparison (the scale turns to percentages). Resolves with its id.
addDrawing addDrawing(drawing: Partial<BmxTradeDrawing> & { type: BmxTradeDrawingType; points: BmxTradePoint[]; }) => Promise<string | null> Adds a drawing (anchored to times and prices). Resolves with its id, or null when it is not a drawing.
addIndicator addIndicator(type: string, params?: Record<string, number | string>) => Promise<string | null> Adds an indicator (by type, with any parameters left at their defaults). Resolves with its id, or null for an unknown type.
clearDrawings clearDrawings() => Promise<void> Removes every drawing.
fitContent fitContent() => Promise<void> Shows every bar.
getBars getBars() => Promise<BmxTradeBar[]> The bars as shown: rolled up to the interval and drawn as the chart type draws them (Heikin-Ashi, Renko ...).
getCompares getCompares() => Promise<BmxTradingChartCompare[]> The instruments compared.
getDrawings getDrawings() => Promise<BmxTradeDrawing[]> The drawings, as plain JSON to store.
getIndicatorValues getIndicatorValues(id: string) => Promise<Record<string, (number | null)[]> | null> An indicator's values, one array per line (null where it is not defined), in step with getBars.
getIndicators getIndicators() => Promise<BmxTradingChartIndicator[]> The indicators on the chart.
getOrderBook getOrderBook() => Promise<BmxTradeBook | null> The order book as shown (before grouping).
getState getState() => Promise<BmxTradingChartState> What the chart shows, to store and give back to setState.
getVisibleRange getVisibleRange() => Promise<BmxTradingChartRangeDetail | null> The bars in view.
pushTick pushTick(tick: BmxTradeTick) => Promise<void> Takes a live trade or quote into the last bar, or starts a new bar when its period has begun.
removeCompare removeCompare(id: string) => Promise<void> Takes a compared instrument off the chart.
removeDrawing removeDrawing(id: string) => Promise<void> Removes a drawing.
removeIndicator removeIndicator(id: string) => Promise<void> Removes an indicator.
replayPause replayPause() => Promise<void> Pauses a replay.
replayPlay replayPlay(speed?: number) => Promise<void> Plays a replay, a bar a second times speed.
replayStep replayStep(count?: number) => Promise<void> Shows the next bar (or the next count) of a replay.
saveCsv saveCsv() => Promise<void> Downloads the bars as shown, as CSV.
savePicture savePicture() => Promise<void> Downloads the chart as a PNG picture.
scrollToLatest scrollToLatest() => Promise<void> Scrolls to the latest bar.
setData setData(bars: BmxTradeBar[] | unknown[] | string) => Promise<void> Replaces the bars.
setDrawingTool setDrawingTool(tool: BmxTradeDrawingType | null) => Promise<void> Starts drawing with a tool (the reader then places its points), or stops with null.
setOrderBook setOrderBook(book: unknown) => Promise<void> Shows an order book: { bids, asks } of [price, size] pairs (or { price, size }), in any order; null takes it away.
setState setState(state: Partial<BmxTradingChartState>) => Promise<void> Brings back what getState gave.
setVisibleRange setVisibleRange(from: number, to: number) => Promise<void> Shows the bars between two times.
startReplay startReplay(from?: number) => Promise<void> Starts a bar replay from a time (unset: the reader chooses the bar), hiding the bars after it.
stopReplay stopReplay() => Promise<void> Ends a replay and shows every bar again.
toPng toPng() => Promise<Blob | null> The chart as a PNG picture, at the screen's resolution.
updateBar updateBar(bar: BmxTradeBar) => Promise<void> Adds a bar, or replaces the bar with the same time.
updateDrawing updateDrawing(id: string, change: Partial<Omit<BmxTradeDrawing, "id" | "type">>) => Promise<void> Changes a drawing: its points, text, colour, or whether it is locked or hidden.
updateIndicator updateIndicator(id: string, change: { params?: Record<string, number | string>; hidden?: boolean; }) => Promise<void> Changes an indicator's parameters, or shows or hides it.
updateOrderBook updateOrderBook(update: { bids?: unknown; asks?: unknown; time?: number; }) => Promise<void> Changes the order book by a feed's update: each level given replaces the size at its price; size 0 removes it.

CSS custom properties

PropertyDescription
--bmx-trading-chart-background Behind the bars.
--bmx-trading-chart-color-0 The first indicator line (and the line and area chart). Colours 0 to 9 are used by the indicators in turn.
--bmx-trading-chart-color-1 The second indicator colour.
--bmx-trading-chart-color-2 The third indicator colour.
--bmx-trading-chart-color-3 The fourth indicator colour.
--bmx-trading-chart-color-4 The fifth indicator colour.
--bmx-trading-chart-color-5 The sixth indicator colour.
--bmx-trading-chart-color-6 The seventh indicator colour.
--bmx-trading-chart-color-7 The eighth indicator colour.
--bmx-trading-chart-color-8 The ninth indicator colour.
--bmx-trading-chart-color-9 The tenth indicator colour.
--bmx-trading-chart-crosshair The crosshair's lines.
--bmx-trading-chart-crosshair-label Behind the crosshair's labels on the axes.
--bmx-trading-chart-crosshair-text The crosshair's labels.
--bmx-trading-chart-down A falling bar, and volume under it.
--bmx-trading-chart-grid Grid lines.
--bmx-trading-chart-height Height of the chart (all panes and the time axis).
--bmx-trading-chart-radius Corner radius of the frame.
--bmx-trading-chart-up A rising bar, and volume under it.