The widget tier
openalgo-charts/widget turns the engine into a working terminal in one call: a chart
with a top bar (symbol, interval, chart type, theme, settings, indicators, objects), a drawing rail
down the left, a status line, the dialogs behind each of those buttons, keyboard shortcuts
with a ? panel that lists them, and optional persistence of the layout. It is the eighth
loadable tier (41.91 KB Brotli), and it is the only one that builds DOM.
View example code
const widget = lib.createWidget(el, {
symbol: 'DEMO',
exchange: 'NSE',
interval: '1h',
intervals: ['5m', '15m', '1h', '1d'],
});
// Without a feed the host puts bars on the primary series itself.
const bars = lib.generateBars(1700000000, 240, 3600);
widget.series.setData(bars);
widget.chart.timeScale.fitContent(160);
return widget;The engine still ships no DOM
openalgo-charts and the six tiers beneath the widget contain no toolbar, no dialog, no
menu and no stylesheet. They touch the document only to own their canvases. The widget is
a host, packaged: everything it draws in HTML it drives through the same public API a host
of your own would use (createChart, DrawingController, chartSettingsSchema, the
contextmenu event, the indicator registry).
That promise is enforced, not stated:
- The tier ACL (
eslint.config.js). Nothing undersrc/exceptsrc/widget/may import the widget, and the widget may reach the engine and the draw tier only through their package specifiers, so it can never be inlined into another bundle. npm run shakebundles an entry that imports onlycreateChartand asserts the widget’s CSS scope (oac-widget) is absent from the result, on every build.- The size budgets (
.size-limit.json). The base engine row did not move when the widget arrived; the widget has its own row, and aWidget terminalrow measures what onecreateWidgetcall actually loads.
Importing the module touches no DOM either; only createWidget does. The module can
therefore be imported by code that also runs on a server, and called once a container
exists.
Install
npm install openalgo-chartsThe widget entry imports openalgo-charts and openalgo-charts/draw itself. The
indicator picker offers whatever the indicator registry holds, so import
openalgo-charts/indicators alongside it for the 102 built-ins; without that import the
picker offers only what you registered yourself.
One call
The widget inherits the engine’s navigation preferences. Set
navigation: { mousePan: 'both', defaultVisibleBars: 100 } to show the
latest 100 bars on load and reset. Axes settings save these choices in the
widget layout. The count changes the visible window without discarding history;
0 fits all loaded bars. Mouse and pen pan time and price by default.
Choose mousePan: 'horizontal' for time-only panning. Saved preferences are retained.
Wheel input is proportional across pixel, line and page delta modes. Horizontal and
Shift-wheel input pan time, wheel input over a visible price axis scales that axis at
the pointer, and Ctrl-wheel or Meta-wheel pinch input zooms time at the pointer. See
Interactions.
import { createWidget } from 'openalgo-charts/widget';
import 'openalgo-charts/indicators';
import { OpenAlgoDataFeed } from 'openalgo-charts';
const widget = createWidget('#terminal', {
feed: new OpenAlgoDataFeed({ baseUrl: 'http://127.0.0.1:5000', apiKey: 'YOUR_KEY' }),
symbol: 'RELIANCE',
exchange: 'NSE',
interval: '5m',
theme: 'dark',
persist: true,
});The container is an element, a CSS selector or an element id, and it needs a non-zero
size before the call, the same rule createChart has. What comes back is a Widget:
widget.dataController; // DataLoadingController | null (null without a feed)
widget.chart; // the Chart underneath, every base API available
widget.draw; // the DrawingController the rail drives
widget.objects; // shared ChartObjects inventory and supported actions
widget.series; // the primary series; setChartType replaces it
widget.root; // the .oac-widget element
widget.context; // what every dialog was handed, for a panel of your own
widget.symbol(); widget.exchange(); widget.interval(); widget.chartType(); widget.theme();
widget.setSymbol('INFY', 'NSE');
widget.setInterval('15m'); // throws UnknownIntervalError for a code the registry lacks
widget.setChartType('area');
widget.setTheme('light');
widget.openSettings(); // false when no settings dialog is registered
widget.openIndicatorPicker();
widget.openObjects(); // searchable inventory, false after destruction
await widget.reload(); // fetch again for the current symbol and interval
const saved = widget.getState(); // { version, symbol, exchange, interval, chartType, theme, chart, rail }
widget.restoreState(saved); // { applied, reason?, chart? }
widget.destroy(); // saves if persisting, removes the chrome, destroys the chart
widget.isDestroyed;restoreState applies a saved viewport only when the state was captured on the same
symbol and interval; on any other dataset the indicators, drawings and panes still land
and the view is dropped, because a range of bar indices means nothing on different bars.
A Widget is a thin owner. Anything the chrome does not expose, do on widget.chart or
widget.draw directly; the chrome observes the chart and stays in step.
The Objects panel follows drawing selection and indicator changes,
offers only supported actions, and reuses the existing settings editors. The primary
price source is protected from removal. Register host-owned profiles explicitly on
widget.objects; arbitrary primitives and broker order lines are not inventory rows.
Dialogs respond to the actual widget container. At narrow widths the tab rail becomes a horizontal tab list, fields fit the available width and actions remain reachable while the body scrolls. This also applies to a small chart inside a wide desktop page.
Mobile widget controls
The mobile option defaults to 'auto': compact controls activate when the widget
container is at most 640 CSS px wide or its primary pointer is coarse. This keeps the
controls on a phone in landscape. Use 'always' for a permanently touch-oriented host
or 'never' to keep desktop chrome at every width.
const widget = createWidget('#terminal', {
mobile: 'auto',
rail: { tools: ['trend-line', 'horizontal-line', 'rectangle'] },
});The mobile header contains symbol entry and interval selection. Its bottom bar exposes
Draw, Studies, Objects and More according to the same topbar, rail and indicators
switches as desktop chrome. Drawing selection adds Properties, Lock or Unlock, and Delete.
The Drawing sheet includes Finish, Cancel, Undo, Magnet and Stay while a tool is active.
rail.tools restricts both layouts to the same allowed drawing ids.
Both layouts drive one DrawingController, ChartObjects inventory, overlay stack,
selection and undo history. A container resize changes the controls without replacing
the chart or its drawings. The widget also honors prefers-reduced-motion: reduce: when
the host omits animZoom or animAutoscale, their defaults become false. Explicit values
remain authoritative.
mountMobile(ctx, options) is exported for a host composing the widget pieces itself.
It returns a MobileHandle with el, active(), refresh() and destroy(). The widget
entry exports MobileMode, MobileOptions and MobileHandle as types. createWidget
is the usual entry because it supplies the shared context and teardown. See
Mobile and Touch and
Scales and Panes.
Options
Since 2.1.9, the widget inherits default corner branding
and an optional background watermark. branding: false disables the corner logo;
watermark: true enables symbol/interval text, which starts off otherwise. Use
Appearance in Chart settings for text, color, size and opacity. Symbol changes
update automatic text through the existing chart data context. See
Branding and Watermarks.
WidgetOptions is ChartOptions plus the fields below. Every ChartOptions key
(timezone, grid, priceScale, axisChrome, renderer, animZoom, animAutoscale and the rest)
passes through to createChart unchanged.
| Option | Type | What it does |
|---|---|---|
feed | DataFeed | Where bars come from. The widget calls getBars for the current symbol and interval (and subscribeBars when the feed has it), and again on every setSymbol / setInterval / reload; with no feed, put data on widget.series yourself. See Custom Data Feeds. |
loading | DataLoadingOptions | Shared-controller request timeout, page size/window, retained bar limit, optional polling and clock. |
symbol | string | The instrument shown at start and in the top bar. Upper-cased. |
exchange | string | Passed to the feed with the symbol. Default ''. |
interval | string | An interval code the interval registry knows ('1m', '5m', '1d', or one you registered with registerInterval). An unknown code throws UnknownIntervalError; a persisted code this build does not know falls back to '1d'. Default '1d'. |
intervals | string[] | The interval pills. Default: DEFAULT_INTERVALS (1m 5m 15m 1h 1d 1w) plus every other registered code. |
chartType | string | The primary series type, a registered chart type id. Default 'candlestick'. |
theme | 'dark' | 'light' | ChartTheme | A named palette or a full theme object. Drives both the canvas and the chrome tokens (see below). Default 'dark' (the engine’s own default is light). |
rail | boolean | RailOptions | The drawing rail. false hides it; tools restricts which registered tool ids appear (the order follows the rail’s own groups); favorites seeds the pins when nothing is stored. |
topbar | boolean | The symbol, interval, chart type, indicators, capture, settings and theme controls. |
statusline | boolean | The status line under the chart. |
mobile | 'auto' | 'always' | 'never' | Responsive widget controls. Default 'auto', active at a container width of 640 CSS px or less or when the primary pointer is coarse. |
indicators | boolean | The Indicators button and picker. Turn it off for a host that manages indicators itself. |
persist | boolean | string | true saves the state under the default namespace (oac-widget:default:state) and restores it on the next createWidget; a string names the namespace, for more than one widget per origin. |
storage | StorageLike | null | The store behind persist. Default: the page’s localStorage. |
locale | string | A BCP 47 tag the status line formats numbers with. |
symbolSearch | (query) => SymbolMatch[] | Promise<SymbolMatch[]> | Called as the user types in the symbol box; the results open as a menu under it. |
lookbackBars | number | Bars per load. Default 500. |
now | () => number | The clock for the load window and the capture filename. Default Date.now. |
onOrder | (order: OrderRequest) => void | Order entry from the right-click menu ({ side, type, price, paneIndex }). Without it the menu draws no trade rows. |
styleNonce | string | The host’s CSP nonce for the shared widget and dialog stylesheet. See Content Security Policy. |
The chrome switches (rail, topbar, statusline, indicators) default to on, so a
bare createWidget(el) is the full terminal. persist defaults to off: nothing is
written to storage until you ask.
A control exists only where the engine has something behind it. Every dialog is generated
from a schema the engine already ships: chart settings from chartSettingsSchema,
indicator settings from the descriptor, drawing properties from drawingSettingsSchema,
so a field appears only when the renderer reads it. The right-click menu draws its order
rows only when onOrder is given.
Live history and reconnect recovery
After loading history, the widget passes its last bar as seedFrom in the optional
third subscribeBars argument. A compatible live feed can continue the forming candle
and call onResync if a restored connection may have missed data.
On onResync, the widget pauses display updates, buffers incoming live bars, and requests
the current history window with noCache: true. It merges those observations into the
refreshed history, replaces the series, preserves the visible logical range, and seeds
the next subscription from the merged last bar. Monitoring continues during the fetch;
another reconnect starts a newer request and supersedes the earlier one.
withBarCache honors the bypass; custom cache wrappers should forward or honor it too.
Responses and live callbacks from a superseded symbol or interval are ignored.
At matching timestamps, the merge keeps the history open, combines high/low extremes, uses the latest buffered close, and takes the maximum volume instead of adding two snapshots. This repairs the available bars; it does not replay unseen trades. Bars without buffered updates retain the authoritative history values. The overlap merge is conservative: a buffered candle can retain a seed extreme that history has corrected. Exact snapshot/tick reconciliation requires a host-specific protocol.
If this automatic refresh fails or returns no bars, the previous chart remains visible,
the status line reports stale history, and a data event carries the error. Display
updates stay paused while live buffering and reconnect monitoring continue. Call
widget.reload() to retry; the cache bypass remains active until a load succeeds.
Manual reload keeps its usual fit-to-data behavior, subject to restoring a saved view;
viewport preservation is specific to automatic reconnect recovery.
Events
widget.on('symbol', (e) => console.log(e)); // { symbol, exchange }: picked in the top bar, or setSymbol
widget.on('interval', (e) => console.log(e)); // { interval }
widget.on('theme', (e) => console.log(e)); // { theme, chartTheme }
widget.on('layout', (e) => console.log(e)); // { reason, chartType? }: getState() would now return differently
widget.on('data', (e) => console.log(e)); // { symbol, interval, bars, error? }: a load finished or failed
widget.on('status', (e) => console.log(e)); // { text, kind }: the status line's message changedon returns the unsubscriber; off(event, cb?) is the same thing by name. The six
events are what a host needs to keep its own state (a URL, a workspace, a watchlist) in
step with the chrome. Everything the chart itself emits (crosshair:move, contextmenu,
trading:*, the drawing controller’s drawing:*) is still there on widget.chart and
widget.draw; see Events. The exact callback payloads are in
dist/widget/index.d.ts.
Theming tokens
The chrome never carries a colour of its own. widgetTokens(theme) derives every chrome
colour from the active ChartTheme: panels are the theme background stepped towards
white or black, borders come from axisLine and paneSeparator, text from axisText
lifted for legibility, the accent from lineColor, and the buy and sell colours from
upColor and downColor. Spacing, radius and font are added, and the set is written as
--oac- custom properties on the widget root, .oac-widget. One <style> element is
injected per page, scoped under that class, so nothing leaks into the host page and
nothing from the host page leaks in. Calling setTheme rewrites the tokens; every control
follows without a repaint of its own.
| Token group | Names (each prefixed --oac-) |
|---|---|
| Surfaces | bg, panel, panel-2, elev, elev-2, elev-3, scrim, shadow |
| Borders | bd, bd-soft, bd-hover |
| Text | tx, tx-strong, mut, faint |
| Accent and state | acc, acc-2, on-bg, on-bd, ring, ring-soft, buy, sell, amber, danger |
| Scrollbars | sb-thumb, sb-thumb-hover |
| Type and metrics | font, mono, fs, radius, rail-w, topbar-h, status-h, ctl-h |
The tokens are set as inline custom properties on the root (that is how setTheme can
swap them without touching the stylesheet), so a host override in a stylesheet has to
outrank an inline declaration:
#terminal .oac-widget {
--oac-font: "IBM Plex Sans", system-ui, sans-serif !important;
--oac-radius: 4px !important;
}Override tokens, not controls: a rule written against an internal class name is a rule against an implementation detail.
Content Security Policy
Pass the nonce generated by your server for this response through styleNonce:
createWidget('#terminal', { styleNonce: requestNonce });The widget assigns the style element’s .nonce before adding its CSS to the document.
The shell and dialogs share one stylesheet, including when multiple widgets use the
same document. The lower-level helper accepts the same value as
injectWidgetStyles(document, extraCSS, requestNonce).
A matching stylesheet policy can be paired with a separate style-attribute policy:
Content-Security-Policy: style-src-elem 'nonce-RESPONSE_NONCE'; style-src-attr 'unsafe-inline'RESPONSE_NONCE is a placeholder for the same fresh, unpredictable response nonce
passed to the widget. This example covers style directives; the host manages the rest
of its CSP. A stylesheet nonce does not authorize inline style attributes. The widget
uses dynamic inline styles for theme tokens and layout, so assess the attribute policy
separately.
For server-rendered markup, an empty <style id="oac-widget-css" nonce="..."> is filled
in place, retaining its nonce. Include the nonce in the HTML itself: an empty style
element without one can trigger a CSP report during parsing, before the widget runs.
A populated stylesheet with this id is left untouched, including its CSS and nonce;
a host supplying the full stylesheet must include both WIDGET_CSS and DIALOG_CSS.
Extending the rail with your own tools
The rail is a view of the draw tier’s tool registry. Register a tool the way
Drawing Tools documents, then name it in
rail.tools:
import { createWidget } from 'openalgo-charts/widget';
import { registerDrawingTool, LINE_FIELDS, type DrawingTool } from 'openalgo-charts/draw';
const midline: DrawingTool = {
id: 'midline',
name: 'Midline',
points: 2,
angleLock: true,
shortcut: 'Alt+M',
settings: LINE_FIELDS,
draw(c) {
// paint a horizontal line at the midpoint of the two anchors
},
distance(x, y, c) {
// media px from the cursor to the line, or null for a miss
return null;
},
};
registerDrawingTool(midline);
createWidget('#terminal', {
rail: { tools: ['trend-line', 'horizontal-line', 'midline', 'fib-retracement'], favorites: ['midline'] },
});Register before you call createWidget: the rail reads the registry once when it builds,
shows only ids it finds there, and drops a favourite it cannot resolve. The rail labels
every button with the tool’s name and draws a glyph when the draw tier’s icon registry
(DRAWING_TOOL_ICONS) has one for the id; the built-in 51 all do. A tool that declares
shortcut is bound by the widget’s keymap and listed in the ? panel, and a binding that
collides with an existing one is reported rather than silently overridden. settings
decides what the drawing properties dialog shows for the tool: a control appears only for
a field the tool’s draw reads.
Loading without a bundler
Every tier bundle imports its neighbours by sibling path (./openalgo-charts.mjs,
./openalgo-charts.draw.mjs), so dist/ served as-is is enough and no import map is
needed:
<div id="terminal" style="height: 600px"></div>
<script type="module">
import { createWidget } from '/dist/openalgo-charts.widget.mjs';
import '/dist/openalgo-charts.indicators.mjs';
createWidget(document.getElementById('terminal'), { symbol: 'RELIANCE', interval: '5m' });
</script>The standalone script (openalgo-charts.standalone.js) is base-only and cannot host the
widget: a tier loaded beside it would import its own second engine. See
Use from a CDN.
Size
Budgets from .size-limit.json, Brotli, enforced by npm run size:
| Row | Files | Budget | Measured |
|---|---|---|---|
| Widget tier | openalgo-charts.widget.mjs | 43 KB | 42.41 KB |
| Widget terminal | base + draw + indicators + widget | 183 KB | 182.37 KB |
The widget is a tier because of these rows. A host that never calls createWidget
downloads none of it, and the base engine’s own budget is unchanged.
In a framework
Create the widget in a mount effect, hold it in a ref, and destroy() it on cleanup, the
same lifecycle as a bare chart in Framework Integration. The widget
instance is never framework state: it owns DOM of its own and re-rendering around it is
wasted work.
Managed loading (2.1.6)
With a feed, widget.dataController owns history, streaming and older pages.
The widget shows compact accessible loading, empty, refreshing, stale, error and
Retry states, including failures in external studies. Retention limits are
reported separately from provider exhaustion. Controls work with topbar or
statusline hidden and inherit the widget theme and stylesheet nonce.
widget.reload() refreshes the same instrument while retaining its time anchor.
Symbol and interval changes clear the old primary data, set ChartDataContext,
and reject obsolete responses. Saved view restoration belongs to the instrument
on which it was captured. The history loader always releases its completion latch.
For a custom replay host, call dataController.setPaused(true) for the entire
replay lifetime, including paused replay; stop replay before resuming delivery.
See data loading for the interactive failure/retry example.