DocumentationThe Widget Tier

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.

live
Rendering live chart…
Pick a tool from the rail, open Indicators, change the chart type, right-click the chart, press ? for the shortcut list. No feed is wired here, so the bars go straight on widget.series.
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 under src/ except src/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 shake bundles an entry that imports only createChart and 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 a Widget terminal row measures what one createWidget call 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-charts

The 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.

OptionTypeWhat it does
feedDataFeedWhere 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.
loadingDataLoadingOptionsShared-controller request timeout, page size/window, retained bar limit, optional polling and clock.
symbolstringThe instrument shown at start and in the top bar. Upper-cased.
exchangestringPassed to the feed with the symbol. Default ''.
intervalstringAn 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'.
intervalsstring[]The interval pills. Default: DEFAULT_INTERVALS (1m 5m 15m 1h 1d 1w) plus every other registered code.
chartTypestringThe primary series type, a registered chart type id. Default 'candlestick'.
theme'dark' | 'light' | ChartThemeA 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).
railboolean | RailOptionsThe 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.
topbarbooleanThe symbol, interval, chart type, indicators, capture, settings and theme controls.
statuslinebooleanThe 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.
indicatorsbooleanThe Indicators button and picker. Turn it off for a host that manages indicators itself.
persistboolean | stringtrue 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.
storageStorageLike | nullThe store behind persist. Default: the page’s localStorage.
localestringA 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.
lookbackBarsnumberBars per load. Default 500.
now() => numberThe clock for the load window and the capture filename. Default Date.now.
onOrder(order: OrderRequest) => voidOrder entry from the right-click menu ({ side, type, price, paneIndex }). Without it the menu draws no trade rows.
styleNoncestringThe 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 changed

on 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 groupNames (each prefixed --oac-)
Surfacesbg, panel, panel-2, elev, elev-2, elev-3, scrim, shadow
Bordersbd, bd-soft, bd-hover
Texttx, tx-strong, mut, faint
Accent and stateacc, acc-2, on-bg, on-bd, ring, ring-soft, buy, sell, amber, danger
Scrollbarssb-thumb, sb-thumb-hover
Type and metricsfont, 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:

RowFilesBudgetMeasured
Widget tieropenalgo-charts.widget.mjs43 KB42.41 KB
Widget terminalbase + draw + indicators + widget183 KB182.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.