<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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
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
| Event | Detail | Description |
|---|---|---|
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
| Method | Signature | Description |
|---|---|---|
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
| Property | Description |
|---|---|
--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. |