Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
489 changes: 471 additions & 18 deletions apps/playground/index.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/chart/assets/icons/order-book.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/chart/assets/icons/range-profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions packages/chart/bench/webgl.bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* CPU submission time of the Canvas2D vs WebGL2 backends on recording
* contexts (MockContext2D / MockContextWebGL2), NOT GPU raster time or
* display FPS. The WebGL2 numbers cover quad packing and buffer staging; in a
* browser the rasterization the recording canvas skips here moves to the GPU.
*/
import { bench, candles, load } from './harness.mjs';
const { createChart } = await load('index.js');
const { MockDocument, MockGLDocument } = await load('dom.js');

const data = candles(100_000);
console.log('\nBackend benchmark (CPU submission; recording 2D context vs mock GL):');

function depthBook(timeMs) {
const bids = [];
const asks = [];
for (let j = 0; j < 50; j++) {
bids.push([103.5 - j * 0.25, (j + 1) * 3]);
asks.push([104.5 + j * 0.25, (j + 1) * 2]);
}
return { bids, asks, time: timeMs };
}

for (const visible of [200, 5000, 20000]) {
// The minimum spacing is 0.5px/bar; size the canvas for the requested range.
const width = Math.max(1264, Math.ceil(visible * 0.5) + 64);
for (const [label, makeDoc, renderer] of [
['canvas2d', () => new MockDocument(), undefined],
['webgl2', () => new MockGLDocument(), 'webgl2'],
]) {
const doc = makeDoc();
const chart = createChart({ document: doc, ...(renderer !== undefined ? { renderer } : {}),
config: { width, height: 900, data, wasm: false } });
chart.scale.zoomToRange(data.length - visible, data.length - 1);
const ctx = doc.created[0].context;
if (renderer !== undefined && chart.renderBackend !== 'webgl2') throw new Error('expected the webgl2 backend');
bench(`${visible} bars ${label}: full redraw`, () => { ctx.calls.length = 0; chart.render(); });
chart.destroy();
}
}

console.log('\nBackend benchmark with heatmap (500 buckets x 2 x 50 levels visible):');
for (const [label, makeDoc, renderer] of [
['canvas2d', () => new MockDocument(), undefined],
['webgl2', () => new MockGLDocument(), 'webgl2'],
]) {
const doc = makeDoc();
const chart = createChart({ document: doc, ...(renderer !== undefined ? { renderer } : {}),
config: { width: 2564, height: 900, data, wasm: false, heatmap: { enabled: true } } });
chart.scale.zoomToRange(data.length - 500, data.length - 1);
const end = data[data.length - 1].time * 1000;
for (let k = 500; k >= 1; k--) chart.setDepth(depthBook(end - k * 1000));
const ctx = doc.created[0].context;
bench(`heatmap ${label}: full redraw`, () => { ctx.calls.length = 0; chart.render(); });
chart.destroy();
}
2 changes: 1 addition & 1 deletion packages/chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"test": "npm run build && npm run build:ts-test && node --test \"dist-test/**/*.test.js\"",
"coverage": "npm run build && npm run build:ts-test && node scripts/check-coverage.mjs",
"coverage:lcov": "npm run build && npm run build:ts-test && mkdir -p coverage && node --enable-source-maps --test --experimental-test-coverage --test-coverage-exclude='**/kernels.base64.*' --test-coverage-exclude='dist-test/**' --test-coverage-exclude='test/**' --test-reporter=dot --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=coverage/lcov.info \"dist-test/**/*.test.js\" && node scripts/lcov-sanitize.mjs",
"bench": "npm run build && node bench/indicators.bench.mjs && node bench/rolling.bench.mjs && node bench/render.bench.mjs && node bench/workloads.bench.mjs",
"bench": "npm run build && node bench/indicators.bench.mjs && node bench/rolling.bench.mjs && node bench/render.bench.mjs && node bench/workloads.bench.mjs && node bench/webgl.bench.mjs",
"prepublishOnly": "npm test",
"prepack": "npm run build",
"bench:browser": "npm run build && node bench/run-browser.mjs"
Expand Down
118 changes: 117 additions & 1 deletion packages/chart/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,32 @@
*/

import type { Candle } from './core/data.js';
import {
DEFAULT_HEATMAP_BUCKET_MS,
DEFAULT_HEATMAP_COLOR_HIGH,
DEFAULT_HEATMAP_COLOR_LOW,
DEFAULT_HEATMAP_GAMMA,
DEFAULT_HEATMAP_MAX_BUCKETS,
DEFAULT_HEATMAP_MAX_LEVELS,
DEFAULT_HEATMAP_MIN_OPACITY,
DEFAULT_HEATMAP_OPACITY,
type HeatmapStop,
} from './core/heatmap.js';
import {
DEFAULT_INLINE_BOOK_ASK_COLOR,
DEFAULT_INLINE_BOOK_BID_COLOR,
DEFAULT_INLINE_BOOK_MAX_LEVELS,
DEFAULT_INLINE_BOOK_WIDTH,
} from './core/inline-book.js';
import type { CanvasImageSourceLike } from './dom.js';

/** Built-in main-series renderers. */
export type SeriesType = 'candlestick' | 'line' | 'area' | 'bar' | 'histogram' | 'heikin-ashi' | 'hollow-candlestick';

/** Main series appearance. Colors accept hex, rgb(a), and `color(display-p3 ...)`. */
export interface SeriesConfig {
type: SeriesType;
/** A built-in {@link SeriesType}, or a type registered via {@link import('./core/chart.js').Chart.registerSeries}. */
type: SeriesType | (string & {});
colorByPreviousClose: boolean;
bodyVisible: boolean;
borderVisible: boolean;
Expand Down Expand Up @@ -229,6 +247,76 @@ export interface VolumeConfig {
height: number;
}

/**
* Inline order book: a live bid/ask depth ladder docked beside the price
* axis, fed by `Chart.setDepth` (the datafeed's depth channel). The chart
* attaches the pane while `enabled` holds and drops it when it clears.
*/
export interface InlineBookConfig {
/** Dock the inline book pane, sharing the main price scale. */
enabled: boolean;
/** Levels shown per side. */
maxLevels: number;
/** Bid bar color. */
bidColor: string;
/** Ask bar color. */
askColor: string;
/** Shade cumulative depth behind the level bars. */
cumulative: boolean;
/** Size label per row when the row is tall enough. */
showLabels: boolean;
/**
* `'dock'` gives the book its own strip beside the price axis, shrinking the
* plot; `'overlay'` paints it behind the series along the plot's right edge,
* so the plot keeps the full width.
*/
placement: 'dock' | 'overlay';
/**
* `'ladder'` grows every level from the price-axis side; `'mirrored'` grows
* bids left and asks right from a central column of size labels.
*/
layout: 'ladder' | 'mirrored';
/** Book width in CSS pixels: the dock strip, or the overlay band. */
width: number;
/** Overlay only: gap in CSS pixels between the band and the price axis. */
inset: number;
}

/**
* Liquidity heatmap (Bookmap-style): resting order-book size over time as a
* color field behind the main series, fed by `Chart.setDepth` (the datafeed's
* depth channel). While `enabled` holds, the chart retains a bounded window
* of time-bucketed book snapshots (`maxBuckets` × 2 × `maxLevels` level
* pairs) and drops it when `enabled` clears.
*/
export interface HeatmapConfig {
/** Paint the heatmap behind the candles. */
enabled: boolean;
/** Wall-clock bucket width in ms; at most one retained snapshot per bucket. */
bucketMs: number;
/** Retained buckets (the history window's capacity). */
maxBuckets: number;
/** Levels kept per side per snapshot. */
maxLevels: number;
/** Ramp color at zero intensity (used when `stops` is empty). */
colorLow: string;
/** Ramp color at full intensity (used when `stops` is empty). */
colorHigh: string;
/**
* A custom gradient from zero to full intensity, replacing `colorLow` →
* `colorHigh` when it has at least one usable color. Bare colors spread
* evenly; `{ color, at }` pins a stop to an intensity (0-1). A stop's own
* alpha scales the ramp alpha there. Empty (the default) keeps the two-color ramp.
*/
stops: HeatmapStop[];
/** 0-1; peak cell alpha at full intensity. */
opacity: number;
/** 0-1; cell alpha at zero intensity, so thin liquidity can stay visible. Default 0. */
minOpacity: number;
/** Intensity curve exponent; below 1 lifts small sizes. */
gamma: number;
}

/** Label formatting hooks. */
export interface FormattersConfig {
price: (value: number) => string;
Expand Down Expand Up @@ -259,6 +347,10 @@ export interface ChartConfig {
theme: ThemeConfig;
watermark: WatermarkConfig;
volume: VolumeConfig;
/** Inline order book docked beside the price axis. */
inlineBook: InlineBookConfig;
/** Liquidity heatmap behind the main series. */
heatmap: HeatmapConfig;
formatters: FormattersConfig;
/** Relative height of each indicator sub-pane (main pane weighs 3). */
indicatorPaneWeight: number;
Expand Down Expand Up @@ -345,6 +437,30 @@ export const DEFAULT_CONFIG: ChartConfig = {
fontFamily: '',
},
volume: { overlay: false, upColor: 'up', downColor: 'down', opacity: 0.5, height: 0.2 },
inlineBook: {
enabled: false,
maxLevels: DEFAULT_INLINE_BOOK_MAX_LEVELS,
bidColor: DEFAULT_INLINE_BOOK_BID_COLOR,
askColor: DEFAULT_INLINE_BOOK_ASK_COLOR,
cumulative: true,
showLabels: true,
placement: 'dock',
layout: 'ladder',
width: DEFAULT_INLINE_BOOK_WIDTH,
inset: 0,
},
heatmap: {
enabled: false,
bucketMs: DEFAULT_HEATMAP_BUCKET_MS,
maxBuckets: DEFAULT_HEATMAP_MAX_BUCKETS,
maxLevels: DEFAULT_HEATMAP_MAX_LEVELS,
colorLow: DEFAULT_HEATMAP_COLOR_LOW,
colorHigh: DEFAULT_HEATMAP_COLOR_HIGH,
stops: [],
opacity: DEFAULT_HEATMAP_OPACITY,
minOpacity: DEFAULT_HEATMAP_MIN_OPACITY,
gamma: DEFAULT_HEATMAP_GAMMA,
},
formatters: { price: defaultPriceFormatter, time: defaultTimeFormatter },
indicatorPaneWeight: 1,
};
Expand Down
Loading
Loading