@razedotbot/charts
v0.6.0
Published
Dependency-free financial and dashboard charts with a documented TradingView-shaped compatibility subset, typed native grammar, and React adapter.
Maintainers
Readme
Raze Charts
Raze Charts is a dependency-free TypeScript toolkit for product-grade charts. It combines a realtime financial widget, a small renderer-neutral chart grammar, and a thin React adapter without locking product UI behind a proprietary shell.
The priorities are deliberate: a clear API, predictable lifecycle, excellent defaults, honest compatibility, and escape hatches that remain typed.
Raze Charts is not affiliated with TradingView. It is an independent, clean-room implementation of a documented subset of the TradingView widget and datafeed interfaces. TradingView is a trademark of TradingView, Inc.
Choose a surface
| Import | Use it for | Contract |
| --- | --- | --- |
| @razedotbot/charts | Realtime financial charts and low-level financial building blocks | TradingView-shaped compatibility subset plus Raze-native exports |
| @razedotbot/charts/chart | Framework-neutral product and dashboard charts | Native typed grammar, SVG string rendering, SVG/Canvas DOM mounting |
| @razedotbot/charts/react | React product charts or an incremental Recharts migration | Thin adapter over /chart; React is an optional peer dependency |
| @razedotbot/charts/studies | Indicator math and study definitions without the widget (servers, workers, screeners) | Pure kernels (sma, ema, rsi, macd, bollinger, vwap, …), StudyRegistry, BUILTIN_STUDIES, and the StudyDefinition contract |
Those are the only public package subpaths today. Imports from src/** or
dist/** are implementation details and are not a compatibility contract.
For a precise feature-by-feature view, see the capability matrix. For internals and extension points, see architecture.
Product principles
- Native first. The typed chart definition and data source own behavior; compatibility layers translate into those contracts.
- No silent compatibility. A supported component works; a known
unsupported Recharts prop such as
Line fillfails with guidance. - One explicit lifecycle. Mounted charts update in place and expose cleanup; stale async work cannot regain ownership after a target change or teardown.
- One semantic scene. SVG and Canvas consume the same compiled chart, with renderer parity tested rather than assumed.
- Measured quality. Accessibility, package formats, visual output, dense data, and bundle size have documented checks and honest boundaries.
Install
npm install @razedotbot/chartsNode 18 or newer is required for development and server-side SVG generation. React 17 or newer is required only when importing the React entrypoint. There are no runtime dependencies. The published declaration surface requires TypeScript 5.0 or newer.
Native charts: recommended for new product UI
Data keys are inferred from the datum type. A misspelled key is a TypeScript error, and accessors can be functions when a key is not enough.
import { defineChart, line, mountChart } from "@razedotbot/charts/chart";
type PricePoint = {
time: Date;
close: number;
};
const prices: PricePoint[] = [
{ time: new Date("2026-09-01T00:00:00Z"), close: 42.1 },
{ time: new Date("2026-09-02T00:00:00Z"), close: 44.8 },
];
const definition = defineChart({
marks: [
line(prices, {
x: "time",
y: "close",
name: "Close",
stroke: "#66d89e",
}),
],
scales: { x: { type: "time" } },
tooltip: true,
legend: true,
ariaLabel: "Daily close price",
ariaDescription: "Close price for 1 and 2 September 2026.",
});
const chart = mountChart(document.querySelector("#chart")!, definition, {
renderer: "svg", // or "canvas"
height: 320,
});
// Preserve the host and interaction lifecycle while data or options change.
chart.update(definition, { height: 360 });
chart.destroy();Wheel zoom, pan, and brush stay within interaction.zoom: { minSpan, maxSpan }
and interaction.panBounds ("data" by default); onTooltip/onSelect
payloads carry data-space x/y, datum, and index. See the
capability matrix for the defaults.
Built-in marks are line, area, bar, point, ruleY, pie, radar,
and heatmap. Scales are linear, band, time, and log. The compiler
produces a renderer-neutral scene that can be inspected with getScene() or
rendered with renderChartSvg, svgFromCompiled, and paintChartCanvas.
Omit scales.x to infer it from the data; once an X scale object is present,
its type is required so configuration cannot silently mean something else.
Built-in marks expose mark-specific option types, and dynamic JavaScript input
is checked against the same boundary with stable ChartCompileError codes.
Pie and heatmap are standalone compositions. Multiple radar layers may be
overlaid when they share the same axes; Cartesian scales cannot be mixed into
a polar chart.
Every mark is a series with a stable id (its id option, else mark-<index>).
Legend clicks and hiddenSeries use that id, and a hidden series stays in the
legend, dimmed, so it can be shown again. Series that share a name get numbered
legend rows (Revenue (2)) unless they also share a colour, which groups them
into one row. scales.y.tickFormat formats tooltips, value chips and rule
labels as well as ticks.
Dense line and area geometry is reduced by a pixel-aware extrema envelope by
default. Tune it with performance.maxRenderedPoints, opt out with
performance.decimation: "none", and inspect CompiledChart.diagnostics.
While decimation is enabled, the configured maximum is a hard per-series cap
on source-derived path samples (minimum 1; area closure vertices are extra).
The compiler still scans source rows, so this bounds render complexity without
pretending input processing is free.
Axes are measured at compile time: tick decimals follow the tick step,
large values switch to compact notation (1.2T), the value axis widens to fit
its labels and last-value chips, category labels thin or rotate instead of
overlapping (scales.x.labels: { rotate, maxWidth, interval }), and time
scales tick on calendar boundaries from seconds to decades. Heatmap values are
plain numbers unless the mark sets valueFormat ("percent", "signed",
"signed-percent", or a function).
Product-specific layers can use the typed defineMarkPlugin + customMark
extension point without forking the compiler. Plugins receive isolated,
read-only scale/theme snapshots and must return validated, discriminated scene
geometry. The plugin contract is documented in
architecture.
Financial widget
The root entrypoint is for candlesticks, live bars, financial studies, drawings, marks, and TradingView-style datafeeds.
import {
widget,
type IBasicDataFeed,
type ResolutionString,
} from "@razedotbot/charts";
declare const datafeed: IBasicDataFeed;
const financialChart = new widget({
container: document.getElementById("chart")!,
symbol: "MYTOKEN",
interval: "1" as ResolutionString,
datafeed,
autosize: true,
theme: "dark",
raze: {
chart_types: ["candles", "line"],
compact_breakpoint: 520,
aria_label: "MYTOKEN price chart",
aria_description: "One-minute candles quoted in USD.",
},
});
financialChart.onChartReady(() => {
void financialChart.activeChart().createStudy(
"EMA",
false,
false,
{ length: 21 },
);
});
// Required when the host unmounts.
financialChart.remove();Trading overlays
Orders and positions are first-class broker primitives rather than drawings.
They stay above the price series, participate in auto-scale, support mouse and
touch dragging, and can be nudged one minimum tick with Up/Down while selected.
The fluent order/position adapters mirror the TradingView integration style;
createBracketOrder links entry, stop-loss, and take-profit with live
risk/reward shading and callbacks.
financialChart.onChartReady(async () => {
const chart = financialChart.activeChart();
const bracket = await chart.createBracketOrder({
side: "buy",
entryPrice: 101.5,
stopLossPrice: 98,
takeProfitPrice: 108.5,
quantity: 2,
currency: "USD",
onChange: (snapshot, event) => {
// `moving` is a transient preview. Persist only the final price.
if (event.type === "moved") {
sendOrderAmendment(event.line.id, event.line.price);
}
updateRiskPreview(snapshot.riskRewardRatio);
},
onCancel: (snapshot) => cancelOrderGroup(snapshot.id),
});
bracket.stopLoss
?.setLineColor("#ef5350")
.onMove((line) => console.log("new stop", line.getPrice()));
const limit = await chart.createOrderLine({
side: "sell",
price: 112,
quantity: 1,
text: "Reduce",
});
limit.onCancel(() => cancelOrder(limit.id));
});trading_event subscriptions receive a detached line snapshot plus the event
type. Trading overlays are deliberately excluded from widget.save() because
broker state must be rehydrated from the execution backend, not a chart layout.
For a standalone browser bundle, dist/charting_library.standalone.js assigns
window.TradingView.widget:
<script src="/static/charting_library.standalone.js"></script>
<script>
const chart = new TradingView.widget({ /* widget options */ });
</script>For new integrations, a Promise-first data source avoids callback plumbing and adapts to the widget protocol:
import {
createDatafeed,
defineDataSource,
type ResolutionString,
} from "@razedotbot/charts";
const source = defineDataSource({
async resolveSymbol(symbol) {
return {
name: symbol,
ticker: symbol,
description: symbol,
type: "crypto",
session: "24x7",
timezone: "Etc/UTC",
exchange: "Raze",
listed_exchange: "Raze",
format: "price",
minmov: 1,
pricescale: 100,
has_intraday: true,
supported_resolutions: ["1", "5", "15"] as ResolutionString[],
};
},
async getBars({ symbol, resolution, from, to }) {
const query = new URLSearchParams({
symbol,
resolution: String(resolution),
from: String(from),
to: String(to),
});
const response = await fetch(`/api/bars?${query}`);
if (!response.ok) throw new Error(`History failed: ${response.status}`);
return response.json();
},
});
const datafeed = createDatafeed(source, {
supportedResolutions: ["1", "5", "15"] as ResolutionString[],
});subscribeBars additionally receives an AbortSignal and may resolve to a
cleanup function, making async realtime setup safe. Existing
IBasicDataFeed implementations remain supported directly.
The data path handles initial history, left-scroll pagination, live updates,
marks, symbol/interval changes, and stale async callbacks. TimeIndex maps real
timestamps to logical bars, so weekends and missing sessions do not create
phantom candles for drawings or marks. See
performance and data correctness.
Financial customization
Chrome is data-driven through favorites, feature flags, and raze options:
new widget({
...requiredWidgetOptions, // container, symbol, interval, datafeed
favorites: { intervals: ["1", "5", "15"] as ResolutionString[] },
disabled_features: ["scale_bar"],
raze: {
sidebar: [
"cursor",
"trend_line",
"horizontal_line",
"separator",
"indicators",
"fit",
"screenshot",
],
indicator_presets: [
{ name: "EMA", length: 9 },
{ name: "RSI", length: 14 },
],
custom_studies: [
{
name: "MOM",
pane: "pane",
defaults: { length: 10, color: "#8ecae6" },
levels: [{ value: 0, dashed: true, axisLabel: true }],
compute: (bars, { length }) =>
bars.map((bar, index) =>
index < length ? null : bar.close - bars[index - length]!.close,
),
},
],
},
});Custom studies use the public full-array compute contract. Built-in EMA,
SMA, and RSI update incrementally for an appended or replaced live bar;
backfills and custom studies recompute. This distinction matters for high-rate
feeds and is intentionally documented rather than hidden.
For typed plugins, defineIndicator() from @razedotbot/charts/studies adds a
validated input schema (int, source, select, …), plot/fill/level
descriptors, a read-only compute context (symbol info, resolution, timezone,
visible range) and incremental init()/update() steps that run once per live
tick. Invalid inputs reject createStudy() with a StudyInputError. See
docs/indicators.md.
The legend, objects tree and Indicators menu label a study from its definition:
MOM 10 above, EMA 9, BB 20 2, MACD 12 26 9 or VWAP for the built-ins.
Set shortTitle or formatLabel(inputs) on the definition to change it.
Price formatting
One formatter controls the price axis, last-price tag, OHLC legend, crosshair,
and shape price labels. A TradingView-shaped factory takes precedence; return
null to fall through to the Raze-native formatter and then the built-in
pricescale formatter.
declare function formatTokenPrice(value: number, pricescale?: number): string;
new widget({
...requiredWidgetOptions, // container, symbol, interval, datafeed
custom_formatters: {
priceFormatterFactory: (symbolInfo) =>
symbolInfo ? { format: (value) => formatTokenPrice(value) } : null,
},
raze: {
format_price: (value, pricescale) =>
formatTokenPrice(value, pricescale),
},
});Percent-scale ticks keep percent notation. Overlay studies may still own their
legend value through StudyDefinition.formatValue.
Time zones and time labels
timezone sets the zone of the time-axis labels, the crosshair time and the
session breaks: an IANA zone, a fixed offset such as "+05:30", "exchange"
for the symbol's timezone, or an id from custom_timezones. Labels are
DST-correct and calendar-aligned, and their density follows the pixels per
bar, so gapped equity sessions stay readable. Daily and coarser bars keep
their trading date in every zone.
const tzChart = new widget({
...requiredWidgetOptions, // container, symbol, interval, datafeed
timezone: "America/New_York",
custom_timezones: [{ id: "desk", alias: "Europe/London", title: "Trading desk" }],
custom_formatters: {
// `date` carries the local time in its UTC fields; null keeps the default label.
tickMarkFormatter: (date, type) =>
type === "Year" ? `'${String(date.getUTCFullYear()).slice(2)}` : null,
},
});
tzChart.onChartReady(() => {
const chart = tzChart.activeChart();
chart.onTimezoneChanged().subscribe(null, (zone) => console.log("timezone", zone));
chart.setTimezone("Asia/Tokyo"); // throws a RangeError for an unknown zone
});dateFormatter and timeFormatter replace the two halves of the crosshair
time label the same way. An unknown zone in the options or in
symbolInfo.timezone warns once and shows UTC.
The root also exports DataManager, TimeIndex, ChartEngine,
ChartRenderer, ShapeStore, TradingStore, StudyStore, indicator math, formatting and
resolution helpers, defineDataSource / createDatafeed, and the default UI
chrome. These pieces are useful for a custom financial shell, but currently
share the widget's mutable ChartContext; they are not separate package
subpaths. Build that context with createChartContext(), and change the
viewport, price scale and series style through its reason-tagged setters.
The exception is indicator math: import it from @razedotbot/charts/studies
to compute studies without bundling the widget.
import { ema, StudyRegistry, type StudyDefinition } from "@razedotbot/charts/studies";
const spread: StudyDefinition = {
name: "Spread",
pane: "pane",
compute: (bars) => bars.map((bar) => bar.high - bar.low),
};
const registry = new StudyRegistry(); // built-ins plus anything you register
registry.register(spread);
const fast = ema([10, 11, 12, 13], 3); // [null, null, 11, 12]The same StudyDefinition works in the widget through raze.custom_studies.
The /studies bundle is separate from the root widget, so registering on a
StudyRegistry created there does not change a mounted widget.
React
For the native API, <Chart> is a lifecycle adapter around a
ChartDefinition. It mounts once and forwards new definitions, dimensions,
renderer choices, and accessibility text through update().
For Recharts-shaped JSX, use the typed factory in new code:
import { createChartComponents } from "@razedotbot/charts/react";
type Sale = { month: string; revenue: number };
const { LineChart, Line, XAxis, CartesianGrid, Tooltip, Legend } =
createChartComponents<Sale>({ xKey: "month", valueKey: "revenue" });
export function RevenueChart({ data }: { data: Sale[] }) {
return (
<LineChart
data={data}
height={320}
ariaLabel="Monthly revenue"
ariaDescription="Revenue from January through March."
>
<CartesianGrid />
<XAxis dataKey="month" />
<Line dataKey="revenue" name="Revenue" stroke="#66d89e" />
<Tooltip />
<Legend />
</LineChart>
);
}Tooltip and Legend are functional configuration toggles consumed by the
parent chart. They are not customizable overlay components. Brush windows
the native viewport through startIndex/endIndex (or a time domain) and
throws on unknown Recharts brush props; it is never silently ignored. The
adapter is a migration convenience, not a drop-in implementation of the full
Recharts API. See the
Recharts migration matrix.
Each series component accepts only the options it implements, both in
TypeScript and at runtime. ResponsiveContainer measures its box and injects
numeric dimensions into its one child: a chart, your own wrapper component
that forwards width/height, or a render function
({ width, height }) => …. Set visual size through chart props or the
container, not Chart.style.width / height. onReady exposes detached,
deeply frozen scene snapshots plus setViewport/getViewport, while React
retains lifecycle and teardown ownership.
Re-rendering is cheap: inline callbacks, interaction={{ zoom: true }} and
viewport literals never recompile, and Recharts-shaped charts memoize on the
structure of their JSX children, so only a real data or prop change repaints
(React update flow). Charts with the
same syncId (or viewportGroup from createViewportGroup()) pan and zoom
together; they share the X window, not the tooltip position
(synchronized charts).
The React entry ships with a "use client" directive, so Next.js App Router
Server Components can render <LineChart> without a client wrapper file of
your own; pass serializable props from the server and keep callbacks in client
components (details).
Design and accessibility
Dark and light themes ship with cohesive pane, grid, axis, tooltip, status,
and series colors. Every native chart also accepts a partial theme, so a
product can own its visual language without replacing the renderer. Default
series palettes stay distinguishable under colour-vision deficiencies, and
COLORBLIND_CHART_THEME / COLORBLIND_LIGHT_CHART_THEME switch up/down
and heatmap colours from green/red to blue/orange.
Good visual defaults do not make every integration accessible automatically.
Supply a specific ariaLabel, add ariaDescription when the trend needs
context, preserve keyboard focus styles, and offer a table or textual summary
when users need exact values. The implemented behavior and integration
checklist live in the accessibility guide. Raze
Charts does not claim a blanket WCAG conformance certification.
On phones and narrow viewports the widget's menus open as bottom sheets with
touch-sized rows. The financial widget's chrome also works under a strict
Content Security Policy: its styles need no 'unsafe-inline', and library
markup goes only through a raze-charts Trusted Types policy. The /chart
SVG renderer still needs 'unsafe-inline' for style attributes. See
UI kit, CSP and localization.
Compatibility and migration
- Capability matrix — what each entrypoint supports.
- Migration guide — TradingView and Recharts mappings, differences, and unsupported surfaces.
- Architecture — data flow, lifecycle, renderers, and custom mark plugins.
- Performance — reproducible benchmarks, budgets, and large-data guidance.
- Accessibility — current semantics and host-app responsibilities.
Development
npm ci
npm run quality
npm run test:visualnpm run quality runs strict source/API type checks, builds the distributable
artifacts, runs every discovered tests/*.mjs suite, and exercises the widget, dashboard compiler and edge cases,
SVG/Canvas color parity, React 17/18/19 contracts, data races, time indexing,
studies, declaration watch mode, the packed ESM/CJS/NodeNext package contract,
documentation, bundle budgets for published artifacts and tree-shaken consumer
scenarios, and compiler performance. Visual tests use
Playwright Chromium snapshots and remain a separate platform-specific gate.
npm run check:docs type-checks every ts/tsx fence in this README and
docs/ against the built declarations, with both Bundler and NodeNext
resolution. A fragment can declare hidden setup with
<!-- prelude: financial --> (see scripts/check-doc-snippets.mjs) or opt
out with <!-- no-check: reason -->.
Use npm run typecheck as the faster type-only feedback loop while editing.
Open examples/index.html, examples/dashboard.html, or
examples/visual.html through a local HTTP server after building:
python -m http.server 8799Contributions are welcome. Start with CONTRIBUTING.md.
License
MIT — see LICENSE.
