# ORTEX Charts, complete reference > ORTEX Charts is a commercial JavaScript charting library for financial applications, built from the ground up with no third-party code or dependencies: 83.2 KB gzip for a full chart with a toolbar, indicators, drawings and order flow, 30 KB for a line chart, and a first paint in 36 ms at 100,000 bars. ## What ORTEX Charts is ORTEX Charts is a commercial JavaScript charting library for financial applications, made by ORTEX Technologies Ltd, the company behind the ORTEX trading and market-data platform. It draws candlesticks, bars, line, area, baseline, Heikin-Ashi, hollow candles, Renko, Kagi, Point and Figure and Line Break, with 51 built-in indicators, 32 drawing tools, order flow (footprint bars, volume profile, market profile, delta, cumulative delta, relative volume), bar replay, draggable price alerts, linked charts, a toolbar and shell, sparklines, treemaps, heatmaps and React bindings. It is Canvas 2D rather than WebGL. It is not the ORTEX subscription. ORTEX is a market-data and short-interest platform sold to investors at ortex.com, priced per user per month. ORTEX Charts is a library sold to developers at ortexcharts.com, priced per developer or per product. Buying one does not include the other, and using ORTEX Charts does not require an ORTEX data subscription. It is not built on TradingView, Lightweight Charts, Highcharts or ChartIQ. It is an independent rendering engine built from the ground up in TypeScript, with no third-party runtime dependencies and no code, icons or styles from any other library: every package installs with an empty dependency tree, and every line of it is ORTEX's own. ## What it costs ### Community: Free Evaluate every package free, with the ORTEX mark shown. - Every package, including order flow and the shell. - Personal, educational and non-commercial projects, indefinitely. - Commercial evaluation and prototypes, for 90 days. - One domain, and localhost always. - Community support through the issue tracker. ### Team: $59 per developer, per month Commercial use in your own products, including software as a service. $590 per developer per year, two months free. - Commercial license for paid, private and internal applications. - The private npm registry with type definitions. - Every release published during the term. - Email support, with a named contact above five seats. - Perpetual fallback: the last release of the term keeps working. - The ORTEX mark stays visible. ### White-label: $299 per product, per month The same library with the ORTEX mark removed, or replaced with yours. - The whitelabel feature on the license key. - As many developers as you like. - Up to 25 domains for the product. - Priority support. - Perpetual fallback: the last release of the term keeps working. ### OEM: As agreed annual contract Redistribution, source access and a roadmap conversation. - Rights to redistribute the library inside your product. - A read-only mirror of the source repository. - A named engineer and a service-level agreement. - Roadmap input, and features built to contract. Every plan keeps working after the term: the last release published during the term stays licensed in perpetuity, on the domains it covered. The library makes no network calls of its own and no license check phones home. ## How it compares, measured Payload, gzip, measured 2026-09-05 with one method for every library: - ORTEX Charts, a line chart: 29.5 KB in one file. engine, scales, crosshair, one series. - ORTEX Charts, candles and volume: 55.8 KB in one file. createFinancialChart, plus legend, price lines and crosshair readout. - ORTEX Charts, candles with indicators and drawings: 64.1 KB in one file. what a stock page loads. - ORTEX Charts, the full shell: 83.2 KB in one file. toolbar, menus, indicators, drawings, order flow. - ORTEX Charts, a sparkline: 3.2 KB in one file. the lite package. - TradingView Lightweight Charts 5.2.1: 60.7 KB in one file. standalone production build; series only. - Highcharts Stock 13, chart only: 132.6 KB in one file. highstock.js alone. - Highcharts Stock 13, with its stock tools: 215.6 KB across 9 files. highstock, indicators-all, stock-tools, annotations-advanced, drag-panes, price-indicator, full-screen, heikinashi, hollowcandlestick. - TradingView Advanced Charts: 4,757.5 KB across 154 files. the licensed drop, uncompressed on disk. - ChartIQ: 4.4 MB across 65 files. transferred by their public demo. Rendering, headless Chromium, the same chart in both libraries: ORTEX Charts first paints 100,000 bars in 36 ms against 447 ms for Lightweight Charts 5, and 10,000 bars in 42 ms against 75 ms. On a live tick, measured to the frame that paints it in both, the two are level at 10,000 bars and ORTEX Charts stays inside one frame at 33 ms against 82 ms at 100,000. TradingView Advanced Charts and ChartIQ have no installable build to drive, so only their load cost is comparable. ### ORTEX Charts against TradingView Lightweight Charts TradingView Lightweight Charts is a free, Apache 2.0 series renderer of 60.7 KB gzip. ORTEX Charts is a commercial financial charting library whose comparable line-chart build is 29.5 KB, whose full shell with a toolbar, indicators, drawings and order flow is 83.2 KB in one file, and which first paints 100,000 bars in 36 ms against 447 ms. Lightweight Charts costs nothing and ships no toolbar, no drawings, no indicators and no order flow; ORTEX Charts costs $590 per developer per year and ships all four. Where ORTEX Charts wins: Payload, like for like. A chart with one series is 29.5 KB in ORTEX Charts against 60.7 KB in Lightweight Charts 5, measured the same way on the same day. Start-up at size. Create to first paint at 100,000 bars is 36 ms against 447 ms, and 42 ms against 75 ms at 10,000 bars. Everything above the renderer: toolbar, drawings, indicators, order flow, replay, alerts, linked charts and session-anchored intraday bars are shipped rather than scoped. A live tick at 100,000 bars, measured to the frame that paints it in both libraries, stays inside one frame at 33 ms against 82 ms, and 124 ms at the 95th percentile. Where TradingView Lightweight Charts wins: Price. Lightweight Charts is free and ORTEX Charts is not. License freedom. Apache 2.0 asks nothing of you; a commercial license is a purchase order and a key. Community size. Lightweight Charts is used by thousands of projects, so the answer to a common question is often already on Stack Overflow. Here the answer comes from us, by email, usually the same day. Which to choose: Choose Lightweight Charts when the requirement really is a series renderer and the chrome around it is your product. Choose ORTEX Charts when the toolbar, the drawings, the indicators and the order flow would otherwise be a quarter of engineering time, and when the chart has to be smaller and faster than the free option rather than larger and slower. Full comparison: https://ortexcharts.com/compare/tradingview-lightweight-charts ### ORTEX Charts against TradingView Advanced Charts TradingView Advanced Charts is free of charge under a signed agreement whose free tier requires the TradingView mark to stay visible and the service to be offered to the public free of charge, which a subscription product and an internal desk tool are not; anything else is priced by conversation. The licensed drop is 154 JavaScript files totalling 4,757.5 KB uncompressed. ORTEX Charts is 83.2 KB gzip in one file for the full shell, is licensed for paid, private and internal applications from $590 per developer per year, and removes its own mark for $299 per product per month. Where ORTEX Charts wins: Licensing fit. ORTEX Charts is licensed for exactly the paid, private and internal cases the free TradingView tier excludes, and a White-label license removes the ORTEX mark entirely. Payload. The full ORTEX shell is 83.2 KB gzip in one file, against 154 files and 4,757.5 KB uncompressed. Order flow. Footprint bars with imbalance marking, exact volume profile and market profile, delta, cumulative delta and relative volume, computed from aggressor volumes. Bar replay with higher-timeframe followers, which is one of the most requested missing features on TradingView. Indicators in plain JavaScript rather than a proprietary language, with your own study appearing in the menu, the legend and the saved layout like a built-in one. Host-owned layouts and drawing storage: JSON in and out, with no save-and-load server contract to implement. A 3.2 KB sparkline build, treemaps and heatmaps on the same engine, and colour-blind safe themes one option away. Where TradingView Advanced Charts wins: Catalog breadth. Advanced Charts has years of accumulated studies and drawing tools behind it. Native mobile. The honest ORTEX answer is the web build inside a WebView. End-user scripting. There is no Pine Script equivalent and there is no plan for one. Price, where the free tier's conditions happen to fit your product. Which to choose: Choose Advanced Charts when the product is offered to the public free of charge, the TradingView mark is acceptable, and catalog breadth matters more than payload. Choose ORTEX Charts when the chart sits inside a paid product, on an internal desk tool, or anywhere the free conditions do not reach, when the mark has to go, or when order flow and replay are the features being bought. Full comparison: https://ortexcharts.com/compare/tradingview-advanced-charts ### ORTEX Charts against Highcharts Stock Highcharts Stock 13 is 132.6 KB gzip for the chart alone and 215.6 KB across nine files with the stock tools and indicators its own demo loads. It ships the strongest accessibility story in the category, and is licensed per developer per year with Internal, SaaS and OEM classes at about $732 per seat and a reported median spend of $4,350 per year. ORTEX Charts is 83.2 KB gzip in one file for a full shell with a toolbar, indicators, drawings and order flow, and costs $590 per developer per year, or $299 per product per month with the ORTEX mark removed. Where ORTEX Charts wins: Payload. The comparison that matters is a chart with a financial toolbar: 215.6 KB across nine files against 83.2 KB in one. Order flow, which Highcharts Stock does not attempt at all. Replay, linked charts with a shared crosshair, and draggable alerts. Licensing simplicity. One price per developer, or one price per product to remove the mark, with no license class to match to a deployment. Cost at the small-team end: $590 per developer per year, against about $732 per seat and a reported median spend of $4,350. Where Highcharts Stock wins: Accessibility. Highcharts has the strongest accessibility story in the category and ORTEX Charts does not match it; the screen-reader description and the data table are open work. The navigator strip and range presets, which are not built in. WebGL. Highcharts has a boost path; ORTEX Charts is Canvas 2D. Breadth outside finance. Highcharts is a general charting company with a financial product; ORTEX Charts is the other way round. Which to choose: Choose Highcharts Stock when accessibility conformance is a procurement requirement, when a navigator strip is expected, or when one vendor has to cover charting across a whole application. Choose ORTEX Charts when the page is a financial page, when order flow and replay are the features being bought, and when 215.6 KB across nine files is more than the page can afford. Full comparison: https://ortexcharts.com/compare/highcharts-stock ### ORTEX Charts against ChartIQ ChartIQ is an enterprise charting product sold on an annual contract whose price is not published and is generally in the tens of thousands of dollars. Its public demo transfers 4.4 MB of JavaScript across 65 files before a chart appears. ORTEX Charts is 83.2 KB gzip in one file for a full shell with a toolbar, indicators, drawings and order flow, is bought with a card at $590 per developer per year or $299 per product per month unbranded, and first paints 100,000 bars in 32 ms. Where ORTEX Charts wins: Payload, by a wide margin: 83.2 KB gzip in one file against 4.4 MB transferred across 65 files. Order flow computed from aggressor volumes rather than inferred, including footprint bars, exact volume profile and market profile. Bar replay with higher-timeframe followers. Buying. A card payment through Stripe and a key by email, against an annual enterprise contract. A published price, which ChartIQ does not have. A 3.2 KB sparkline build for tables and cards, and treemaps and heatmaps on the same engine. Where ChartIQ wins: Catalog breadth. ChartIQ has years of accumulated indicator and drawing coverage behind it. Native mobile SDKs. Institutional procurement fit: professional services, long-term contracts and the assurances that come with them. Which to choose: Choose ChartIQ when an institution requires an enterprise vendor with professional services and a native mobile SDK, and the payload is somebody else's problem. Choose ORTEX Charts when the page has a performance budget, when order flow is the reason for the chart, or when the evaluation has to finish this month rather than after procurement. Full comparison: https://ortexcharts.com/compare/chartiq ### ORTEX Charts against SciChart JS SciChart JS is a WebGL charting library sold perpetually from $1,349 per developer with a year of updates, and it is the library to beat for very large point clouds rendered all at once. ORTEX Charts is Canvas 2D, is 83.2 KB gzip in one file for a full financial shell, first paints 100,000 bars in 32 ms, holds 60 Hz while panning and zooming at that size, and costs $590 per developer per year. SciChart wins on raw point count and cross-platform reach; ORTEX Charts wins on the financial furniture around the chart, on payload and on price. Where ORTEX Charts wins: Financial completeness: candles, panes, indicators, drawings, markers, order flow, replay, linked charts and draggable alerts in one library. Payload. A full shell in 83.2 KB gzip in one file. Price. $590 per developer per year, or $299 per product per month with the mark removed. Purchasing. A card payment and a key by email rather than a seat negotiation. Where SciChart JS wins: WebGL. ORTEX Charts is Canvas 2D and does not render millions of points all on screen at once without decimation. If that is the workload, SciChart is the right choice today. Cross-platform reach outside the browser. Scientific chart types and the breadth of a general-purpose visualization suite. Which to choose: Choose SciChart JS when the workload is millions of points at 60 Hz, or when the same charts have to run outside the browser. Choose ORTEX Charts when the workload is financial: bars, indicators, drawings and order flow at the sizes a market data page actually carries, where Canvas 2D holds 60 Hz and the payload is two orders of magnitude smaller than a licensed drop. Full comparison: https://ortexcharts.com/compare/scichart The six-way comparison, including the cases where the answer is not ORTEX Charts: https://ortexcharts.com/compare/best-javascript-charting-library ## What is below All 24 documentation pages, in the order the site presents them. Individual pages are served as Markdown at https://ortexcharts.com/docs/.md, and the index is at https://ortexcharts.com/llms.txt. The 19 live examples at https://ortexcharts.com/examples run in a browser and are not included below. --- # Quickstart > Install the package, draw a chart, add an indicator, in about five minutes. Section: Start here. Source: https://ortexcharts.com/docs/quickstart ## What You Will Have in Five Minutes A candlestick chart of your own bars, with a volume pane, a moving average, a crosshair readout and live updates, in about 30 lines of code. There are two paths through this page: one for an application with a bundler (Vite, webpack, Next.js, Rollup) and one for a plain HTML page with no build step at all. Both end at the same library. _Live example: [stock-chart](https://ortexcharts.com/examples/stock-chart)_ ## Path One: A Project With a Bundler ### Install the Package The packages are published to a private registry, so a one-line `.npmrc` has to come first. [Installation and delivery](https://ortexcharts.com/docs/install) explains where the token comes from and what the other delivery modes are. ```bash # .npmrc, next to package.json # @ortex-charts:registry=https://gitlab.com/api/v4/projects/86057283/packages/npm/ # //gitlab.com/api/v4/projects/86057283/packages/npm/:_authToken=${ORTEX_NPM_TOKEN} npm install @ortex-charts/financial ``` `@ortex-charts/financial` re-exports everything in `@ortex-charts/core`, so one dependency is enough for a price chart. Data types such as `Bar` and `Tick` come from `@ortex-charts/math`, which arrives as a transitive dependency. Add `@ortex-charts/ui` when you want the toolbar, `@ortex-charts/react` for components, `@ortex-charts/lite` for sparklines and `@ortex-charts/viz` for treemaps and heatmaps. ### Set the License Key Call `setLicenseKey` once, as early in the application as you can, before any chart is created. It returns a promise that resolves when the signature check finishes, but you do not have to await it: the check is asynchronous and the chart never blocks on it. ```ts import { setLicenseKey } from "@ortex-charts/financial"; void setLicenseKey(process.env.NEXT_PUBLIC_ORTEX_CHARTS_KEY!); ``` Without a key the library still works completely. The only difference is that the ORTEX mark stays on the first pane and one warning is written to the console. See [the ORTEX mark](https://ortexcharts.com/docs/branding) and [license keys](https://ortexcharts.com/docs/licensing). ### Draw the Chart The container needs a height. The chart follows its container with a `ResizeObserver`, so a `height` in CSS is enough and no resize handling is required. ```ts import { createFinancialChart } from "@ortex-charts/financial"; import type { Bar } from "@ortex-charts/math"; const bars: Bar[] = [ { time: 1735689600000, open: 219.4, high: 221.8, low: 218.9, close: 221.2, volume: 41200000 }, { time: 1735776000000, open: 221.3, high: 224.0, low: 220.6, close: 223.7, volume: 38900000 }, // … ]; const el = document.getElementById("chart") as HTMLDivElement; const chart = createFinancialChart(el, { theme: "dark", // "dark", "light", or your own token object timeZone: "America/New_York", resolution: "1D", // datafeed notation: 1, 5, 15, 60, 1D, 1W, 1M data: bars, }); ``` `time` is milliseconds since the Unix epoch, in UTC. Every timestamp in the library is. The time zone controls where bar boundaries fall and how axis labels read, not the timestamps themselves. Bars must be sorted ascending; duplicates and gaps are a question for your feed, not for the chart. `createFinancialChart` returns a small handle, not the engine itself. `chart.chart` is the core `Chart` and `chart.main` is the candlestick series, and everything the handle does not cover is reachable through them. ### Add an Indicator ```ts chart.addIndicator("ema", { length: 21 }); chart.addIndicator("sma", { length: 200 }); chart.addIndicator("rsi"); // lands in a pane of its own ``` There are 51 built-in indicators, listed in [Indicators](https://ortexcharts.com/docs/indicators). Registering your own takes about 20 lines and it then behaves like a built-in one, including in the toolbar menu and in saved layouts. ### Read Values Under the Crosshair ```ts chart.chart.subscribeCrosshairMove((e) => { if (e.index === null) return; // pointer left the chart const v = e.seriesValues.get(chart.main.id); if (v && "close" in v) { console.log(v.open, v.high, v.low, v.close, v.volume); } }); ``` `seriesValues` is a `Map` keyed by series id. Bar series give OHLCV, value series give `{ value }`. ### Push Live Updates The simplest form is a single call per update. An incoming bar whose `time` equals the last bar replaces it in place; a later `time` appends. ```ts chart.update({ time: 1735862400000, open: 223.7, high: 226.1, low: 223.0, close: 225.8, volume: 12400000 }); ``` For a stream of trades rather than bars, hand the chart a source and let it fold ticks into bars of the chart resolution: ```ts import { websocketSource, type LiveItem } from "@ortex-charts/financial"; const source = websocketSource({ url: "wss://feed.example.com/trades", onOpen: (send) => send(JSON.stringify({ subscribe: "ACME" })), parse: (raw) => { const m = JSON.parse(String(raw)) as { t: number; p: number; s: number }; return { time: m.t, price: m.p, size: m.s }; // a Tick }, }); const stop = chart.live(source); // call stop() to unsubscribe ``` The socket opens on the first subscriber, reconnects with backoff, and closes when the last subscriber leaves. [Your own data](https://ortexcharts.com/docs/data) covers datafeeds, adapters and every input shape. _Live example: [live-stream](https://ortexcharts.com/examples/live-stream)_ ### Clean Up ```ts chart.remove(); ``` In a component, call this in the teardown of the effect that created the chart. ## Path Two: A Plain HTML Page Customers on the hosted kit get a single script tag with their key already baked in. There is nothing to install and nothing to configure. ```html
``` The kit is an immediately-invoked bundle of `@ortex-charts/financial` and `@ortex-charts/ui` that puts everything on one global, `OrtexCharts`, and installs the license key for you. `OrtexCharts.createChartShell` is there too, so the whole toolbar is one more call: ```html ``` The CDN serves a kit only to the domains registered on it, so the same script tag on another site returns a 403. Evaluating without a kit works too: load the packages from your own build and leave the key unset. ## The Whole Thing, Once Everything above, together, as one module. This is the shape of nearly every integration. ```ts import { createFinancialChart, setLicenseKey } from "@ortex-charts/financial"; import type { Bar } from "@ortex-charts/math"; void setLicenseKey(import.meta.env.VITE_ORTEX_CHARTS_KEY); export function mountChart(el: HTMLDivElement, bars: Bar[], onHover: (text: string) => void) { const chart = createFinancialChart(el, { theme: "dark", timeZone: "America/New_York", resolution: "1D", watermark: { text: "ACME", visible: true, fontSize: 44 }, data: bars, }); chart.addIndicator("ema", { length: 21 }); chart.addIndicator("sma", { length: 200 }); chart.setMarkers([ { time: bars[120].time, position: "belowBar", shape: "arrowUp", text: "E", tooltip: "Earnings" }, ]); chart.chart.subscribeCrosshairMove((e) => { const v = e.index === null ? null : e.seriesValues.get(chart.main.id); onHover(v && "close" in v ? `O ${v.open} H ${v.high} L ${v.low} C ${v.close}` : ""); }); // Open on the last year rather than on the whole history. chart.chart.timeScale.setVisibleRange(bars.length - 260, bars.length - 1); return () => chart.remove(); } ``` ## Common First Problems - **Nothing is drawn and the container is zero pixels high.** The chart follows its container, so the container needs a height from CSS or from `options.height`. - **Every bar is at the wrong hour.** Timestamps are milliseconds, not seconds; multiply a Unix second timestamp by 1,000 before passing it in. - **Daily bars land on the wrong day.** Set `timeZone` to the exchange time zone rather than leaving it at the default of `UTC`; see [Time, sessions and resolutions](https://ortexcharts.com/docs/time). - **The ORTEX mark is on a licensed chart.** Only a license that grants the `whitelabel` feature may hide it, and the console says which of the two cases applies. - **The chart works in development and is blank after a server-side render.** The library is browser-only; create it inside an effect, never during a render on the server. ## Where to Go Next | If you want to | Read | |---|---| | Understand panes, scales, series and the render loop | [Concepts](https://ortexcharts.com/docs/concepts) | | Connect a real feed rather than a static array | [Your own data](https://ortexcharts.com/docs/data) | | Change every color, font and number format | [Colors, fonts and formatting](https://ortexcharts.com/docs/customization) | | Get the toolbar, symbol search and saved layouts | [The shell and layouts](https://ortexcharts.com/docs/shell) | | Write React instead of imperative calls | [React](https://ortexcharts.com/docs/react) | | Know what the library does not do | [Migrating](https://ortexcharts.com/docs/migration) and [Questions we are asked](https://ortexcharts.com/docs/faq) | --- # Installation and delivery > The hosted kit, the private npm registry and the source drop, and which one you want. Section: Start here. Source: https://ortexcharts.com/docs/install ## Three Ways to Get the Library ORTEX Charts is a commercial library, so the code does not come from public npm. There are three delivery modes. They share one license-key system and one set of builds; they differ only in what leaves ORTEX and how much of the plumbing you own. | Mode | What you do | What you get | Who it suits | |---|---|---|---| | Hosted kit | Put one ` ``` Everything exported by the two packages is on the `OrtexCharts` global. There is no module system involved, so `OrtexCharts.createFinancialChart`, `OrtexCharts.createChartShell`, `OrtexCharts.registerIndicator` and the rest are all reachable directly. Two properties are worth knowing about: - `window.__ORTEX_CHARTS_KIT__` carries the kit id, key id, plan, library version and build time, which is what you quote in a support request. - The CDN checks the request `Origin` or `Referer` against the domains registered on the kit before the bytes leave the edge, so a copied script tag on another host returns a 403 rather than a working chart. Kits auto-update within their major version. A new patch or minor release is picked up the next time the file is fetched; a major version is a new URL, so nothing breaks under you. ## The Private Registry Packages live in the GitLab package registry of the `ortex/ortex-charts` project. Two lines in `.npmrc` point the `@ortex-charts` scope at it and supply a token. ```bash # .npmrc — commit this file; the token comes from the environment @ortex-charts:registry=https://gitlab.com/api/v4/projects/86057283/packages/npm/ //gitlab.com/api/v4/projects/86057283/packages/npm/:_authToken=${ORTEX_NPM_TOKEN} ``` The token is issued with the license, is scoped to reading this one package registry, and expires with the license term. Never commit a real token. Export it in your shell for local work and set it as a masked variable in continuous integration: ```bash export ORTEX_NPM_TOKEN="the token from your license email" npm install @ortex-charts/financial @ortex-charts/ui ``` npm, pnpm and yarn all read `.npmrc` in this form. Yarn Berry users who have moved to `.yarnrc.yml` need the equivalent `npmScopes` entry pointing at the same URL. ### Which Packages to Install | Package | Install it when | Depends on | |---|---|---| | `@ortex-charts/math` | You want the scales, sessions, resolutions or rolling statistics without a chart | Nothing | | `@ortex-charts/core` | You are building a chart type of your own on the engine | `math` | | `@ortex-charts/financial` | You want a price chart; re-exports all of `core` | `core`, `math` | | `@ortex-charts/ui` | You want the toolbar, drawing rail and layouts | `financial` | | `@ortex-charts/lite` | You want sparklines in a table | `math` only | | `@ortex-charts/viz` | You want treemaps, heatmaps or category charts | `core`, `math` | | `@ortex-charts/react` | You prefer components and hooks | All of the above | Installing `@ortex-charts/ui` pulls in `financial`, `core` and `math` transitively; you do not have to list them, though listing the ones you import from is good practice. Every package is published at the same version, 0.5.0 — see [Release notes](https://ortexcharts.com/docs/changelog) — ships ECMAScript modules with declaration files, is marked side-effect free, and has a single entry point. There are no CommonJS builds and no source maps outside the OEM mode. ## Source Access Under OEM An OEM or enterprise agreement includes read access to a mirror of the repository: the TypeScript source of every package, the unit and visual test suites, and the playground application. Building it is `pnpm install && pnpm build`, which writes `packages/*/dist`. The contract, not the code, is what makes this mode different, and it is the only mode in which the source and the tests leave ORTEX. ## Bundler Notes The packages are plain ECMAScript modules with `"sideEffects": false`, which is all a modern bundler needs. A few specifics: - **Tree shaking works, and it matters.** A chart built with `createFinancialChart` and no toolbar is about 56 KB gzipped; adding `@ortex-charts/ui` brings the full chart to about 83.2 KB gzipped. Importing `@ortex-charts/viz` when you only wanted a sparkline is the usual way to pay for something you are not using. - **Vite, Rollup, esbuild, webpack 5 and Next.js need no configuration.** There is no `require`, no Node built-in, and no import of CSS from a package other than `@ortex-charts/ui`, which injects its own stylesheet at runtime rather than shipping a `.css` file to import. - **Webpack 4 and other bundlers without ECMAScript module support will not work**, because no CommonJS build is published. - **There is nothing else to install.** No `@ortex-charts/*` package has a third-party runtime dependency, so an install adds the packages you asked for and nothing behind them. The maths that used to come from D3 is ours; where an algorithm was ported rather than rewritten, the file carries the ISC notice and the third-party notices list it. ## TypeScript Declarations ship with every package, so no `@types` package is needed. The library is written against `"module": "NodeNext"` semantics with explicit `.js` specifiers internally, which means your `tsconfig.json` should use a modern module resolution mode: ```json { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "Bundler", "lib": ["ES2020", "DOM", "DOM.Iterable"], "strict": true } } ``` `"moduleResolution": "Bundler"` (or `"NodeNext"`) is required; the legacy `"Node"` mode does not read the `exports` field and will not find the types. TypeScript 5.0 or later is expected. One import path is worth memorizing: **the data types live in `@ortex-charts/math`**. `Bar`, `Tick`, `BarSeries`, `ValueSeries`, `Resolution` and `Session` are exported from there, while everything about drawing a chart comes from `@ortex-charts/core` (re-exported by `@ortex-charts/financial`). Installing `financial` puts `math` in `node_modules` transitively, but listing it in `package.json` is worth it if you import the types. ```ts import { createFinancialChart } from "@ortex-charts/financial"; import type { Bar, Tick } from "@ortex-charts/math"; ``` Two further typing habits make the API pleasant. Options objects are `DeepPartial`, so you only write the keys you are changing. Series options are keyed by series type, so `chart.addSeries("candlestick", { … })` narrows to `CandlestickSeriesOptions` and `chart.addSeries("histogram", { colorMode: "sign" })` type-checks without a cast. ## Server-Side Rendering The library is browser-only. It creates canvases, reads `devicePixelRatio`, attaches pointer listeners and observes element sizes, none of which exist on a server. There is no server-side rendering mode and none is planned; chart images for emails and social cards are a separate problem, better solved by a rendering service. In practice this means one rule: **create the chart inside an effect, never during render.** ```tsx "use client"; import { useEffect, useRef } from "react"; import { createFinancialChart, type Bar } from "@ortex-charts/financial"; export function PriceChart({ bars }: { bars: Bar[] }) { const ref = useRef(null); useEffect(() => { if (!ref.current) return; const chart = createFinancialChart(ref.current, { data: bars, resolution: "1D" }); return () => chart.remove(); }, [bars]); return
; } ``` The components in [`@ortex-charts/react`](https://ortexcharts.com/docs/react) already do this: they create the chart in a layout effect guarded by a `typeof window` check, so they render an empty container on the server and fill it on hydration. In Next.js the file still needs the `"use client"` directive, because the effect only runs in a client component. ## Content Security Policy The library needs no `unsafe-eval` and makes no network requests of its own. Two directives are worth checking: - `img-src data:` is needed if the ORTEX mark is shown, because the wordmark is drawn from an inline data URI. It is also needed for symbol logos supplied as data URIs. - `connect-src` must allow your own feed hosts when you use `websocketSource` or a datafeed; the library uses the browser `fetch` and `WebSocket` you would have used yourself. Hosted-kit users additionally need `script-src https://cdn.ortexcharts.com`. ## Verifying the Installation The fastest check that everything resolved, the key is valid and the browser is happy: ```ts import { createFinancialChart, licenseStatus, setLicenseKey } from "@ortex-charts/financial"; await setLicenseKey(KEY); console.log(licenseStatus()); // { state: "valid", verified: true, message: "Licensed", payload: { customer, tier, features, domains, exp } } const chart = createFinancialChart(document.body.appendChild(document.createElement("div")), { height: 300, data: [{ time: Date.now(), open: 1, high: 2, low: 0.5, close: 1.5, volume: 10 }], }); ``` A `state` of `unlicensed`, `expired`, `domain` or `invalid` is explained in [license keys](https://ortexcharts.com/docs/licensing). None of them stop the chart from working. --- # Concepts > Charts, panes, series, scales, primitives and the render loop, in one page. Section: Start here. Source: https://ortexcharts.com/docs/concepts ## The Shape of the Library Seven packages sit in four layers, and dependencies only ever point downwards. ```text react components and hooks financial lite viz ui product packages core canvas engine: panes, scales, series, interaction math pure functions, no DOM ``` Nothing sits below `math`. There is no third-party runtime dependency anywhere in the library and no third-party code inside it: what you install is our code, written from the ground up, and your lockfile gains one entry per package you actually import. `@ortex-charts/math` has no access to the DOM and no canvas; it runs in Node, in a worker or on a server. `@ortex-charts/core` owns the canvas and the DOM. The product packages register series types, indicators, tools and chrome with the core. `@ortex-charts/react` wraps the product packages and has no logic of its own. The practical consequence is that you can drop a layer at any point. `createChartShell` gives you a whole application; `createFinancialChart` gives you a price chart without chrome; `createChart` gives you an engine with nothing on it. Each returns a handle that exposes the layer below it, so you never have to choose the top layer and then discover a wall. ## Chart, Pane, Price Scale, Series A **chart** is a vertical stack of **panes** that share one time axis. A pane has its own canvases, its own legend and any number of **price scales**. A **series** belongs to one pane and one price scale. ```ts const chart = createChart(el, { theme: "dark", timeZone: "America/New_York" }); const price = chart.addSeries("candlestick", { title: "AAPL" }); price.setData(bars); const rsiPane = chart.addPane({ heightRatio: 0.25 }); const rsi = chart.addSeries("line", { title: "RSI", pane: rsiPane.id, color: "#A78BFA" }); rsi.setData(rsiPoints); ``` Pane heights are relative weights, not pixels. The first pane has a weight of one, so a pane created with `heightRatio: 0.25` takes a fifth of the height. Users can drag the separator between panes, collapse a pane to a legend strip, or maximize one; the core methods for that are `setPaneCollapsed`, `maximizePane` and `restorePanes`. Price scales are addressed by id. `"right"` and `"left"` are drawn on the corresponding edge; any other id is an overlay scale that is not drawn at all, which is how a series gets its own autoscaling without taking up axis width. ```ts chart.addSeries("line", { title: "Volume ratio", priceScaleId: "left" }); chart.addSeries("line", { title: "Score", priceScaleId: "score-only" }); // invisible scale chart.priceScale("left").applyOptions({ mode: "percentage", scaleMargins: { top: 0.2, bottom: 0.05 } }); ``` Each scale carries its own mode: `normal`, `logarithmic`, `percentage` or `indexedTo100`. Autoscale considers only the visible index range and only the visible series on that scale, which is why zooming into a quiet stretch expands the price axis instead of leaving a flat line at the bottom. ## The Index-Based Time Axis The time axis is indexed by **bar position**, not by clock time. Bar 0 is the first bar of the main series, bar 1 the second, and the pixel position of a bar is a linear function of its index. Times in between are interpolated onto fractional indices. This is what makes nights, weekends and holidays disappear without special cases: they are simply absent from the index. It also means a chart can be scrolled past the last bar, since indices extrapolate beyond the data, and that panning cost does not depend on how much history is loaded. The consequences worth internalizing: - Ranges are expressed in indices for precision (`setVisibleRange(from, to)`) and in times for convenience (`setVisibleTimeRange(fromTime, toTime)`); the second converts to the first. - The **main series** owns the timeline. It is the first bar series added, or whatever `chart.setMainSeries(id)` names. - Every other series is **aligned** onto that timeline before it is drawn, which is the next concept. ```ts chart.timeScale.setVisibleRange(bars.length - 260, bars.length - 1); chart.timeScale.fitContent(); chart.timeScale.scrollToRealtime(); chart.timeScale.subscribeVisibleRangeChange((r) => console.log(r.fromTime, r.toTime, r.barSpacing)); ``` ## Alignment: Series That Do Not Share a Timeline Short interest arrives daily while the price chart shows five-minute bars. A benchmark trades on a different calendar. A signal fires on 30 dates in two years. All of these go on the same chart, and none of them share the main timeline. Every series therefore declares an `align` mode, and the engine produces one value per bar of the main timeline before drawing: | Mode | Behavior | Typical use | |---|---|---| | `exact` | A value appears only where the timestamps match exactly. | Indicators computed from the bars themselves. | | `forwardFill` | The last known value is carried forward until the next one. | Slower-moving fundamentals: short interest, cost to borrow, ratings. | | `nearest` | The closest value within `maxGapMs` is used. | Feeds whose timestamps are close but not identical. | | `bucket` | Values are collected into the bar that contains them. | Event counts and other things that are summed per bar. | `forwardFill` is the default for a new series, because it is what a host adding its own data almost always wants. Indicators set `exact` for themselves. Alignment is incremental. When a live tick extends the main timeline in place, only the tail is re-aligned, so an overlay costs work proportional to the new bars rather than to all of the history. _Live example: [short-interest](https://ortexcharts.com/examples/short-interest)_ ## Data Representation Series data is stored as a struct of arrays, not an array of objects. A bar series is `{ length, time, open, high, low, close, volume }` where every column is a `Float64Array`; a value series is `{ length, time, value }`. `NaN` marks a gap. You do not have to build those arrays. `setData` accepts whichever form you have: ```ts series.setData(barRows); // Bar[] series.setData([[time, value], [time, value]]); // [time, value] pairs series.setData([{ time, value }, { time, value }]); // point objects series.setData({ length, time, value }); // columns you already own ``` Passing columns you already own is the cheapest path and the one to prefer for large data sets, because nothing is copied. The trade-off is the invariant that goes with it: aligned columns handed to renderers are **views** into growable buffers, so never hold a reference to `series.columns` across a data change. Timestamps are always milliseconds since the Unix epoch, in UTC. ## The Render Loop Drawing is scheduled on an animation frame and split into layers, so the common interactions do not repaint everything. - Each pane has a **main canvas** (background, grid, series, primitives) and an **overlay canvas** (crosshair, active drawings, tooltips). Moving the crosshair repaints only the overlay canvases and the legend. - The time-axis label has a canvas of its own for the same reason. - Panning and zooming repaint the main canvases, but only for the visible index range, and dense lines are decimated to one minimum and one maximum per pixel column. - Legend DOM is rebuilt only when its content actually changes, so a 60 Hz pan does not churn the DOM. - Live updates append into a capacity-doubling backing store, so a tick does not copy the columns, and indicators recompute a window rather than the whole history. `chart.requestDraw()` schedules a frame, `chart.requestDraw("overlay")` schedules an overlay frame, and `chart.flush()` draws synchronously, which is what a test or a screenshot wants. ## Primitives: Anything That Is Not a Series A **primitive** is an object attached to a pane that can draw in three passes, take part in autoscale, be hit-tested, and consume pointer events before the chart does. Markers, drawings, events, footprints, volume profiles, alerts and indicator band fills are all primitives, and so is anything you write. ```ts import type { PanePrimitive, PaneView } from "@ortex-charts/core"; const sessionShading: PanePrimitive = { id: "session-shading", zIndex: -1, draw(ctx, view: PaneView, layer) { if (layer !== "belowSeries") return; ctx.fillStyle = "rgba(120, 140, 180, 0.08)"; for (let i = view.from; i <= view.to; i++) { if (!isPreMarket(view.indexToTime(i))) continue; ctx.fillRect(view.x(i) - view.timeScale.barSpacing / 2, view.plot.y, view.timeScale.barSpacing, view.plot.height); } }, }; chart.addPrimitive(sessionShading); ``` The `PaneView` a primitive receives carries everything needed to place pixels: `x(index)`, `y(value)`, `valueAt(px)`, `indexAt(px)`, the visible range, the master timeline, the theme, the device pixel ratio and the current hover position. ## Options, Themes and Events Options are deeply partial everywhere. `chart.applyOptions({ crosshair: { mode: "free" } })` changes one nested key and leaves the rest alone, and the same is true of `series.applyOptions`. A **theme** is a flat token object. Switching themes is a repaint, never a rebuild: series, indicators, drawings and the visible range all survive `chart.setTheme("light")`. See [Colors, fonts and formatting](https://ortexcharts.com/docs/customization) for the token list. Events are subscriptions that return their own unsubscribe function, which is the shape the whole library uses: ```ts const off = chart.subscribeCrosshairMove((e) => { /* … */ }); off(); ``` The subscriptions on the core chart are `subscribeCrosshairMove`, `subscribeClick`, `subscribeDblClick`, `subscribeDataChange`, `subscribePaneChange`, `subscribeLegendMenu` and `chart.timeScale.subscribeVisibleRangeChange`. ## State Belongs to the Host The chart never writes to storage and never talks to a server. It does not fetch bars, it does not save layouts, and it has no account system. What it does instead is hand you plain JSON at the boundary. A [layout](https://ortexcharts.com/docs/shell) is a serializable object you store per user. [Drawings](https://ortexcharts.com/docs/drawings) serialize to an array you store per symbol. [Alerts](https://ortexcharts.com/docs/replay) are a list you persist and re-apply. Where the data comes from is a [datafeed or an adapter](https://ortexcharts.com/docs/data) that you supply. This is a deliberate constraint. It keeps ORTEX out of your customers' data, it makes the library work identically behind a login and on a public page, and it means there is no ORTEX service that has to be up for your chart to work. ## Vocabulary | Term | Meaning | |---|---| | Bar | One OHLCV row: `{ time, open, high, low, close, volume }`, optionally with `buyVolume` and `sellVolume`. | | Tick | One trade: `{ time, price, size }`, optionally with an aggressor `side`. | | Resolution | A bar size in datafeed notation: `1`, `5`, `60`, `1D`, `1W`, `1M`. | | Session | Trading hours as `0930-1600`, possibly several segments, or `24x7`. | | Main series | The bar series that owns the timeline. | | Alignment | Placing another series' values onto the main timeline. | | Pane | A horizontal band with its own canvases and scales. | | Primitive | A drawable, hit-testable object on a pane that is not a series. | | Overlay | A host-defined data series the user can switch on from the toolbar. | | Layout | The user's chart configuration as JSON, stored by the host. | --- # Series types > Candlesticks, bars, line, area, baseline, histogram, Heikin-Ashi and your own. Section: Charts. Source: https://ortexcharts.com/docs/series ## The Six Built-In Types Every series is created through `chart.addSeries(type, options)` and returns a `SeriesModel`. Three of the types hold OHLCV bars and three hold single values; the difference matters, because a value series cannot be redrawn as candles. | Type | Kind | Data | Draws | |---|---|---|---| | `candlestick` | bars | `Bar[]` | Body between open and close, wicks to high and low. Hollow and Heikin-Ashi are options on this type, not separate types. | | `bar` | bars | `Bar[]` | A vertical high-low line with an open tick left and a close tick right. | | `line` | values | points | A polyline, optionally curved, optionally with point markers. | | `area` | values | points | A line with a vertical gradient fill beneath it. | | `baseline` | values | points | A line whose halves above and below a base value are colored and filled separately. | | `histogram` | values | points | Columns from a base value, colored fixed, by sign, or by the main series direction. | ```ts const price = chart.addSeries("candlestick", { title: "AAPL" }); price.setData(bars); const ma = chart.addSeries("line", { title: "EMA 21", color: "#F5A524", lineWidth: 1.5 }); ma.setData(emaPoints); ``` _Live example: [stock-chart](https://ortexcharts.com/examples/stock-chart)_ ## Options Every Series Has These come from `SeriesOptionsBase` and apply to all six types and to any type you register yourself. | Option | Type | Default | What it does | |---|---|---|---| | `id` | `string` | generated | Stable identity, useful when you restore a saved chart. | | `title` | `string` | `""` | Name in the legend and in the layout. Placement is keyed by it. | | `pane` | `string` | first pane | Which pane the series lives on. | | `priceScaleId` | `string` | `"right"` | `right`, `left`, or any other id for an undrawn overlay scale. | | `visible` | `boolean` | `true` | Hides the series without removing it; it keeps updating. | | `color` | `string` | next palette color | Line, area and histogram color. | | `lineWidth` | `number` | `2` | Stroke width in CSS pixels. | | `lineStyle` | `"solid" \| "dashed" \| "dotted"` | `"solid"` | Stroke pattern. | | `priceFormat` | `PriceFormat` | price, 2 decimals | Number formatting for the axis, legend and labels. | | `lastValueVisible` | `boolean` | `true` | Value label on the price axis. | | `priceLineVisible` | `boolean` | `false` | Dashed horizontal line at the last value. The `bar` and `candlestick` types default it to `true`. | | `align` | `AlignMode` | `"forwardFill"` | How timestamps map onto the main timeline. | | `zIndex` | `number` | `0` | Draw order within a pane. | | `legendVisible` | `boolean` | `true` | Whether the series appears in the legend. | | `crosshairMarkerVisible` | `boolean` | `true` | Dot on the series at the crosshair index. | `color` left unset takes the next color from the theme palette, so several overlays added in a row are distinguishable without any color management on your side. ## Candlesticks ```ts chart.addSeries("candlestick", { title: "AAPL", upColor: "#22C08A", downColor: "#EF4E5A", wickUpColor: "#22C08A", wickDownColor: "#EF4E5A", borderVisible: true, hollowUp: false, heikinAshi: false, bodyWidthRatio: 0.7, }); ``` `hollowUp` draws up candles with a transparent body, which is the classic hollow-candle style. `bodyWidthRatio` is the fraction of the bar spacing a body occupies; bodies never grow wider than the gap allows and never shrink below one pixel, so a chart at maximum zoom out still reads as candles rather than as mush. Colors left unset fall back to the theme `up`, `down`, `upWick` and `downWick` tokens, which is what makes a theme switch repaint candles correctly without touching the series. ### Heikin-Ashi Heikin-Ashi is an option rather than a type, so it can be toggled without recreating the series or losing the indicators computed from it. ```ts price.applyOptions({ heikinAshi: true }); ``` The transform runs over the raw bars whenever the data or the flag changes. Note the limitation: on a live chart the transform currently re-runs over all bars on every tick, which is measurable on very long intraday histories. Indicators still read the raw columns, so an EMA over Heikin-Ashi candles is an EMA of the real closes. ## OHLC Bars ```ts chart.addSeries("bar", { title: "AAPL", thin: false, openVisible: true }); ``` `thin` drops the open and close ticks and draws only the high-low line, which is what most people want below about four pixels of bar spacing. `openVisible: false` keeps the close tick and drops the open one. ## Line ```ts chart.addSeries("line", { title: "Cost to borrow", curve: "linear", // "linear" | "step" | "monotone" | "smooth" pointMarkers: false, pointRadius: 2.5, align: "forwardFill", }); ``` `curve: "step"` is the right choice for anything that holds a value until it changes, such as a rating, a threshold or a daily statistic drawn on an intraday chart. `monotone` is a shape-preserving spline; `smooth` is a basis spline that does not pass through its points, so avoid it where the exact values matter. Line series decimate when they are denser than the pixels available: one minimum and one maximum per pixel column. The visual result is identical and the cost stops growing with the data. ## Area ```ts chart.addSeries("area", { title: "Estimated Short Interest", color: "#7C5CE6", topColor: "rgba(124, 92, 230, 0.30)", bottomColor: "rgba(124, 92, 230, 0.02)", baseValue: 0, }); ``` `topColor` and `bottomColor` are the ends of a vertical gradient; unset, they come from the theme `areaTop` and `areaBottom` tokens. `baseValue` fills down to a value instead of to the bottom of the pane, which is what you want for a series that can go negative. ## Baseline ```ts chart.addSeries("baseline", { title: "Relative performance", baseValue: 0, // or "first" for the first visible value topLineColor: "#22C08A", bottomLineColor: "#EF4E5A", topFillColor: "rgba(34, 192, 138, 0.18)", bottomFillColor: "rgba(239, 78, 90, 0.18)", }); ``` `baseValue: "first"` re-bases on every pan, which turns the series into "performance since the left edge of the view" without any recomputation on your side. ## Histogram ```ts chart.addSeries("histogram", { title: "Volume", colorMode: "mainDirection", // "fixed" | "sign" | "mainDirection" base: 0, widthRatio: 0.7, upColor: "rgba(34, 192, 138, 0.45)", downColor: "rgba(239, 78, 90, 0.45)", priceFormat: { type: "volume", precision: 0, minMove: 1 }, }); ``` The three color modes cover the three things histograms are used for. `fixed` is one color for a count. `sign` colors above and below `base` differently, which is what a MACD histogram or a delta pane wants. `mainDirection` colors each column by whether the main series bar at the same index closed up or down, which is what a volume pane wants — and it is why `createFinancialChart` does not need you to precompute colored volume rows. ## Setting and Updating Data `setData` replaces everything. `update` handles one row and is the path live data takes. ```ts series.setData(rows); const appended = series.update({ time, open, high, low, close, volume }); // true when a new bar was appended, false when the last bar was replaced in place ``` The rules `update` follows are worth knowing, because the fast paths depend on them: - A `time` equal to the last bar replaces that bar in place. - A later `time` appends into a capacity-doubling backing store, so no column is copied. - An earlier `time` takes a slower rebuild path; feeds that emit out of order pay for it. `series.holdUpdates()` and `series.releaseUpdates()` queue updates and apply them in order later. [Bar replay](https://ortexcharts.com/docs/replay) uses this so a live feed can keep running while the user replays history. ## Switching Type Without Losing State ```ts price.setType("line", { lineWidth: 2 }); ``` `setType` keeps the data, the id, the pane, the scale, the title, the alignment and every base option, and swaps only the renderer and its type-specific defaults. Indicators, markers and drawings attached to the chart are untouched. This is what the toolbar's chart-type buttons call, and it is why switching from candles to a line does not flash. A value series cannot become a bar series, because it has no open, high or low. The call throws with a message that says exactly that. ## Reading Values ```ts series.valueAt(index); // { open, high, low, close, volume } or { value } or null series.primaryValue(index); // the number the axis label and price line use series.lastIndex(); // index of the last finite value, or -1 series.rawLength; // rows as given, before alignment series.raw; // the struct-of-arrays form, or null before setData series.columns; // aligned columns — a view, never hold it across a data change ``` ## Price Lines A price line is a horizontal line at a fixed value on the series' scale, with an axis label. It belongs to the series, so it moves with the scale and disappears with it. ```ts const id = chart.addPriceLine(series.id, { price: 182.5, title: "Entry", color: "#F5A524", lineStyle: "dashed", lineWidth: 1, axisLabelVisible: true, }); chart.removePriceLine(series.id, id); ``` For lines the user can drag, and for crossing notifications, use [alerts](https://ortexcharts.com/docs/replay) instead. ## Writing Your Own Series Type A series type is four functions and a defaults object. Register it once and `chart.addSeries("your-type", …)` works, including in saved layouts and the legend. ```ts import { registerSeriesType, pixelColumns, type SeriesOptionsBase, type SeriesTypeDef } from "@ortex-charts/core"; import { valueExtent } from "@ortex-charts/math"; interface DotOptions extends SeriesOptionsBase { radius: number; } const dotSeries: SeriesTypeDef = { kind: "values", defaults: { radius: 3 }, draw(ctx, view) { const { xs, ys } = pixelColumns(view, "value"); ctx.fillStyle = view.options.color; for (let i = 0; i < xs.length; i++) { if (ys[i] !== ys[i]) continue; // NaN is a gap ctx.beginPath(); ctx.arc(xs[i], ys[i], view.options.radius, 0, Math.PI * 2); ctx.fill(); } }, extent: (columns, from, to) => valueExtent(columns.value, from, to), valueAt: (columns, i) => (columns.value[i] === columns.value[i] ? { value: columns.value[i] } : null), primaryValue: (columns, i) => columns.value[i], }; registerSeriesType("dot", dotSeries); chart.addSeries("dot", { title: "Signals", color: "#F472B6", radius: 4 }).setData(points); ``` The `SeriesView` handed to `draw` carries aligned `columns`, the visible index bounds `from` and `to`, the mappings `x(index)` and `y(value)`, the bar spacing, the resolved theme, the device pixel ratio, the plot rectangle, the hovered index and the main series columns. Draw only between `from` and `to`; the engine has already clipped to the pane. Four rules keep a custom type well behaved: - Return `null` from `extent` when the series should not affect autoscale. - Return `null` from `valueAt` where there is no data, so the legend shows nothing rather than `NaN`. - Treat `NaN` as a gap in every column rather than as a zero. - Add an optional `colorAt` when the series is drawn in more than one color, so the last-value label matches what is on screen. ## What Is Not Here Renko, Kagi, Point and Figure, line break, step-line bars, scatter and HLC area are not built in. They are transforms plus a renderer on the same interfaces shown above, and they are on the roadmap, but today writing one is your work rather than a configuration flag. --- # Indicators > The built-in library, their inputs, and how to register your own in 20 lines. Section: Charts. Source: https://ortexcharts.com/docs/indicators ## Adding One ```ts const ema = chart.addIndicator("ema", { length: 21 }); const rsi = chart.addIndicator("rsi"); // its own pane const macd = chart.addIndicator("macd", { fast: 12, slow: 26, signal: 9 }); ``` An indicator declares its own inputs, outputs, colors and pane, so the second argument is optional and any input you leave out takes its default. Outputs become ordinary series, so everything on [Series types](https://ortexcharts.com/docs/series) applies to them: they appear in the legend, they autoscale, they can be moved to another scale or pane, and they are hidden and shown like any other series. The instance you get back is the handle for changing and removing it: ```ts ema.setInputs({ length: 50 }); // recomputes and relabels ema.setVisible(false); ema.remove(); ema.id; // instance id ema.def; // the definition: name, inputs, outputs, pane, levels ema.paneId; // where it was placed ema.series; // Map ema.inputs; // resolved inputs, defaults filled in ``` ## The Built-In Library There are 51 as of version 0.2.0: 18 that draw on the price pane and 33 that take a pane of their own. That is comparable to Highcharts Stock, which ships about 50, and short of TradingView Advanced Charts, which ships roughly 110, where the long tail is mostly further variations on what is here. Anything missing is a `registerIndicator` call, and the engine — inputs, multiple outputs, bands, levels, own panes, incremental recompute — has no privileged path for the built-ins. The tables below are generated from the registry. `listIndicators()` is the same list at runtime, and it is the one to read if you build your own menu. ### Overlays on the Price Pane | Id | Name | Inputs | Outputs | |---|---|---|---| | `sma` | Moving Average | `length` (20), `source` | `sma` | | `ema` | Exponential Moving Average | `length` (20), `source` | `ema` | | `wma` | Weighted Moving Average | `length` (20), `source` | `wma` | | `vwma` | Volume Weighted Moving Average | `length` (20), `source` | `vwma` | | `hma` | Hull Moving Average | `length` (9), `source` | `hma` | | `dema` | Double Exponential Moving Average | `length` (20), `source` | `dema` | | `tema` | Triple Exponential Moving Average | `length` (20), `source` | `tema` | | `bb` | Bollinger Bands | `length` (20), `mult` (2), `source` | `upper`, `basis`, `lower` | | `keltner` | Keltner Channels | `length` (20), `mult` (2), `atrLength` (10), `source` | `upper`, `basis`, `lower` | | `donchian` | Donchian Channels | `length` (20) | `upper`, `basis`, `lower` | | `envelope` | Envelope | `length` (20), `percent` (2.5), `source` | `upper`, `basis`, `lower` | | `atrbands` | ATR Bands | `length` (14), `mult` (2), `source` | `upper`, `lower` | | `linreg` | Linear Regression Channel | `length` (100), `mult` (2), `source` | `upper`, `basis`, `lower` | | `ichimoku` | Ichimoku Cloud | `conversion` (9), `base` (26), `spanB` (52), `displacement` (26) | `conversion`, `base`, `spanA`, `spanB`, `lagging` | | `psar` | Parabolic SAR | `step` (0.02), `max` (0.2) | `sar` | | `supertrend` | Supertrend | `length` (10), `mult` (3) | `supertrend` | | `pivots` | Pivot Points | `period` (`day`, `week`, `month`), `method` (`classic`, `fibonacci`) | `r3`, `r2`, `r1`, `p`, `s1`, `s2`, `s3` | | `vwap` | VWAP | `anchor` (`session`, `week`, `month`, `all`) | `vwap` | ### Indicators in Their Own Pane | Id | Name | Inputs | Outputs | Levels | |---|---|---|---|---| | `rsi` | Relative Strength Index | `length` (14), `source` | `rsi` | 30, 70 | | `stochrsi` | Stochastic RSI | `rsiLength` (14), `stochLength` (14), `k` (3), `d` (3), `source` | `k`, `d` | 20, 80 | | `macd` | MACD | `fast` (12), `slow` (26), `signal` (9), `source` | `hist`, `macd`, `signal` | 0 | | `ppo` | Percentage Price Oscillator | `fast` (12), `slow` (26), `signal` (9), `source` | `hist`, `ppo`, `signal` | 0 | | `stoch` | Stochastic | `k` (14), `smooth` (3), `d` (3) | `k`, `d` | 20, 80 | | `willr` | Williams %R | `length` (14) | `r` | −80, −20 | | `cci` | Commodity Channel Index | `length` (20) | `cci` | −100, 100 | | `cmo` | Chande Momentum Oscillator | `length` (9), `source` | `cmo` | −50, 50 | | `roc` | Rate of Change | `length` (9), `source` | `roc` | 0 | | `mom` | Momentum | `length` (10), `source` | `mom` | 0 | | `trix` | TRIX | `length` (15), `source` | `trix` | 0 | | `dpo` | Detrended Price Oscillator | `length` (21), `source` | `dpo` | 0 | | `ao` | Awesome Oscillator | `fast` (5), `slow` (34) | `ao` | 0 | | `uo` | Ultimate Oscillator | `fast` (7), `middle` (14), `slow` (28) | `uo` | 30, 70 | | `adx` | Average Directional Index | `length` (14), `smooth` (14) | `adx`, `plus`, `minus` | 25 | | `aroon` | Aroon | `length` (14) | `up`, `down` | 30, 70 | | `vortex` | Vortex Indicator | `length` (14) | `plus`, `minus` | 1 | | `volume` | Volume | `ma` (20) | `volume`, `ma` | — | | `rvol` | Relative Volume | `length` (50) | `rvol` | 1 | | `cumvol` | Cumulative Volume | `length` (20) | `sum` | — | | `obv` | On Balance Volume | — | `obv` | — | | `ad` | Accumulation/Distribution | — | `ad` | — | | `pvt` | Price Volume Trend | — | `pvt` | — | | `mfi` | Money Flow Index | `length` (14) | `mfi` | 20, 80 | | `cmf` | Chaikin Money Flow | `length` (20) | `cmf` | 0 | | `chaikinosc` | Chaikin Oscillator | `fast` (3), `slow` (10) | `osc` | 0 | | `eom` | Ease of Movement | `length` (14), `divisor` (10000) | `eom` | 0 | | `atr` | Average True Range | `length` (14) | `atr` | — | | `stdev` | Standard Deviation | `length` (20), `source` | `stdev` | — | | `hv` | Historical Volatility | `length` (10), `annual` (365) | `hv` | — | | `bbw` | Bollinger Bandwidth | `length` (20), `mult` (2), `source` | `bbw` | — | | `delta` | Volume Delta | `method` (`auto`, `aggressor`, `estimate`) | `delta` | 0 | | `cvd` | Cumulative Volume Delta | `method` (`auto`, `aggressor`, `estimate`) | `cvd` | — | `delta`, `cvd` and `rvol` are covered in detail on [Order flow](https://ortexcharts.com/docs/order-flow); they read the aggressor volumes on the bars when the feed supplies them and say so in the tooltip when they had to estimate instead. The `source` input accepts `close`, `open`, `high`, `low`, `hl2`, `hlc3` and `ohlc4`. Enumerate the registry at runtime rather than hard-coding the list: ```ts import { listIndicators, getIndicator } from "@ortex-charts/financial"; for (const def of listIndicators()) { console.log(def.id, def.name, def.pane, def.inputs.map((i) => i.key)); } getIndicator("rsi").levels; // [30, 70] ``` _Live example: [custom-indicator](https://ortexcharts.com/examples/custom-indicator)_ ## Placement and Color ```ts chart.addIndicator("rsi", { length: 14 }, { pane: "separate", // "overlay", "separate", or an existing pane id paneHeightRatio: 0.25, colors: { rsi: "#A78BFA" }, seriesOptions: { lineWidth: 1, lastValueVisible: false }, sourceSeriesId: comparisonSeries.id, }); ``` - `pane` overrides where the definition wanted to go. Passing an existing pane id puts two indicators in one pane, which is how RSI and Stochastic end up together. - `colors` overrides output colors by output key; unset, each output takes the color its definition derives from the theme, so a theme switch recolors it. - `seriesOptions` is applied to every output series. - `sourceSeriesId` computes the indicator from another bar series instead of the main one, which is how you put an RSI of a comparison symbol on the chart. ## How Live Recomputation Works Indicators recompute whenever the series they read from changes. On a live tick, a full pass over ten years of minute bars for every indicator would be the dominant cost, so an indicator may declare a `lookback`: the number of bars its last value depends on, to the precision that matters on screen. When `lookback` is set, a tick recomputes only the last `max(500, lookback)` bars and patches the final value. This is verified against a full recompute to within 1e-6 in the test suite. Indicators that carry state from the beginning of the data — `obv`, `cvd`, `vwap`, `cumvol` — leave `lookback` unset and recompute fully, which is correct and slower. The practical consequences: - A moving average, RSI or MACD on 100,000 bars costs the same per tick as on 1,000. - Cumulative indicators are the ones to watch on very long histories with a fast feed. - Changing an input forces a full recompute, which is what you want. ## Writing Your Own An indicator is one object. `compute` receives the aligned main-series columns and returns one `Float64Array` per output key, and everything else — the legend label, the settings dialog, the pane, the level lines, the band fill, the layout entry — follows from the declaration. ```ts import { registerIndicator, sourceColumn, type IndicatorDef } from "@ortex-charts/financial"; import { rollingMean, rollingStd } from "@ortex-charts/math"; const zscore: IndicatorDef = { id: "zscore", name: "Z-Score", short: "Z", pane: "separate", levels: [-2, 0, 2], lookback: (inputs) => 10 * Number(inputs.length), inputs: [ { key: "length", name: "Length", type: "number", default: 50, min: 2, max: 2000, step: 1 }, { key: "source", name: "Source", type: "select", default: "close", options: ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"] }, ], outputs: [{ key: "z", title: "Z", plot: "line", color: (theme) => theme.palette[2] }], compute({ columns, inputs }) { const src = sourceColumn(columns, inputs.source); const length = Number(inputs.length); const mean = rollingMean(src, length); const sd = rollingStd(src, length); const z = new Float64Array(src.length); for (let i = 0; i < src.length; i++) z[i] = sd[i] > 0 ? (src[i] - mean[i]) / sd[i] : NaN; return { z }; }, }; registerIndicator(zscore); chart.addIndicator("zscore", { length: 100 }); ``` Registering it also puts it in the toolbar's indicator menu and lets it survive in a saved [layout](https://ortexcharts.com/docs/shell), because both read the same registry. ### The Definition Fields | Field | Meaning | |---|---| | `id` | Registry key, and what a layout stores. | | `name` | Full name in the menu and the settings dialog. | | `short` | Legend label prefix, for example `EMA`. Inputs are appended automatically. | | `pane` | `"overlay"` for the price pane, `"separate"` for its own. | | `levels` | Constant value lines drawn in a separate pane, such as 30 and 70 for RSI. | | `lookback` | Bars the last value depends on, a number or a function of the inputs. Unset means full recompute. | | `priceFormat` | `{ type, precision }` for the axis and legend of the outputs. | | `inputs` | Declarations the settings dialog is generated from. | | `outputs` | One per returned column. | | `compute` | The calculation. | ### Input Declarations ```ts { key: "length", name: "Length", type: "number", default: 20, min: 1, max: 2000, step: 1 } { key: "source", name: "Source", type: "select", default: "close", options: ["close", "hl2"] } { key: "showBands", name: "Show bands", type: "boolean", default: true } ``` The four types are `number`, `select`, `boolean` and `source`. The shell builds the settings dialog from these declarations, so an indicator you register gets the same dialog the built-ins have. ### Output Declarations ```ts { key: "hist", title: "Histogram", plot: "histogram", signColors: true } { key: "upper", title: "Upper", plot: "band", bandWith: "lower", color: (t) => t.palette[5], lineWidth: 1 } { key: "vol", title: "Volume", plot: "histogram", mainDirectionColors: true } { key: "sum", title: "Sum", plot: "area", color: (t) => t.palette[4] } ``` `plot` is `line`, `histogram`, `area` or `band`. A `band` output pairs with another output named in `bandWith` and the space between them is filled, which is how Bollinger Bands and Donchian Channels get their shading from two ordinary line series. `signColors` colors a histogram by sign; `mainDirectionColors` colors it by the main series candle direction. `color` is a function of the theme rather than a string, so the indicator recolors itself when the theme changes rather than being repainted in a color that no longer fits. ### The Compute Context ```ts compute(ctx) { ctx.columns; // aligned main-series columns: time, open, high, low, close, volume, // plus buyVolume and sellVolume when the feed supplied them ctx.length; // number of bars ctx.inputs; // resolved inputs ctx.timeZone; // the chart time zone, for session-anchored calculations such as VWAP return { key: Float64Array }; } ``` Return arrays of `ctx.length`. Use `NaN` for bars where the indicator has no value, which is normally the warm-up period; the renderer treats `NaN` as a gap and the legend shows nothing rather than a number. `@ortex-charts/math` carries the primitives most indicators need: `rollingSum`, `rollingMean`, `rollingStd`, `rollingMin`, `rollingMax`, `ema`, `wilderSmooth`, `pctChange`, `logReturns`, `cumulativeReturn` and `diff`. They are all typed-array in, typed-array out. ## What Is Not Here - **There is no scripting language.** No Pine Script, no formula editor, no sandbox. Indicators are TypeScript or JavaScript, which is more capable and less safe: an indicator is code you ship, not code your users write. If letting end users author indicators matters to your product, this library does not solve it for you. - **The library is 51 indicators deep, not 110.** Ichimoku, Supertrend, ADX, Parabolic SAR, Keltner Channels, Money Flow Index, Williams %R, Aroon and TRIX all arrived in version 0.2.0, but the far end of the Advanced Charts list is not here, and some of it never will be. - **Indicators of indicators are not declarative.** Computing an RSI of a moving average means doing both calculations inside one `compute`. --- # Drawing tools > Trend lines, Fibonacci levels, shapes and notes, with magnet, undo and JSON. Section: Charts. Source: https://ortexcharts.com/docs/drawings ## Attaching the Tools Drawing tools are a primitive on a pane. Create them once and the returned object is the whole API: starting a tool, adding drawings from code, selecting, deleting, undo, redo and serialization. ```ts import { createDrawingTools, createFinancialChart } from "@ortex-charts/financial"; const chart = createFinancialChart(el, { data: bars, resolution: "1D" }); const tools = createDrawingTools(chart.chart, { magnetPx: 8, onChange: (change) => { if (change.type !== "selected" && change.type !== "toolChanged") { saveDrawings(tools.getDrawings()); } }, }); ``` Using [the shell](https://ortexcharts.com/docs/shell) instead gives you the same primitive with a tool rail already wired to it, reachable as `shell.drawings`. ## The 32 Tools Every kind below is placed by clicking, moved by dragging, edited by its handles, hit-tested for tooltips and serialized to the same JSON shape. `POINTS_REQUIRED` is exported, so a custom tool rail can show progress while a drawing is being placed. ### Lines and Freehand | Tool | `kind` | Points | Notes | |---|---|---|---| | Trend line | `trendline` | 2 | Extends left, right, both or neither through `style.extend`. | | Ray | `ray` | 2 | A trend line that always extends right. | | Horizontal line | `horizontalLine` | 1 | Price line across the pane with a value label. | | Horizontal ray | `horizontalRay` | 1 | A price line from its anchor rightward. | | Vertical line | `verticalLine` | 1 | Time line across the pane. | | Arrow | `arrow` | 2 | A line with a head at the second point, labeled from `style.text`. | | Curved arrow | `curvedArrow` | 3 | The third point bends the curve. | | Polyline | `polyline` | Open ended | A click per vertex, ended by a double-click or Enter. | | Brush | `brush` | Open ended | Samples the pointer while the button is held, smoothed. | ### Shapes and Channels | Tool | `kind` | Points | Notes | |---|---|---|---| | Rectangle | `rectangle` | 2 | Filled at `style.fillOpacity`, optional label from `style.text`. | | Ellipse | `ellipse` | 2 | The two points are opposite corners of its bounding box. | | Triangle | `triangle` | 3 | Filled at `style.fillOpacity`. | | Parallel channel | `parallelChannel` | 3 | Two points define the base line, the third the width. | | Regression trend channel | `regressionChannel` | 2 | A least-squares fit between the two times, with a band at `style.deviation` standard deviations either side. | ### Fibonacci, Gann and Pitchfork | Tool | `kind` | Points | Default levels | |---|---|---|---| | Fibonacci retracement | `fibRetracement` | 2 | 0, 0.236, 0.382, 0.5, 0.618, 0.786, 1 | | Fibonacci extension | `fibExtension` | 3 | 0, 0.618, 1, 1.618, 2.618, 3.618, 4.236 | | Fibonacci fan | `fibFan` | 2 | 0.236, 0.382, 0.5, 0.618, 0.786 | | Fibonacci arcs | `fibArcs` | 2 | 0.382, 0.5, 0.618, 1 | | Fibonacci time zones | `fibTimeZone` | 2 | 0, 1, 2, 3, 5, 8, 13, 21, 34, 55, counted in bars | | Fibonacci channel | `fibChannel` | 3 | 0, 0.236, 0.382, 0.5, 0.618, 1, 1.618 | | Gann fan | `gannFan` | 2 | 1/8, 1/4, 1/3, 1/2, 1, 2, 3, 4, 8, as price per bar | | Pitchfork | `pitchfork` | 3 | 1, the median line and its two tines; add 0.5 for the half lines | All eight read `style.levels`, so replacing the array changes which lines are drawn. `LEVELS_BY_KIND` holds the sets that differ from the retracement default, and `levelsFor` returns the one a kind starts with. ### Measurement and Position | Tool | `kind` | Points | Notes | |---|---|---|---| | Measure | `measure` | 2 | Price change, percent change and bar count between two points. | | Price range | `priceRange` | 2 | The vertical move alone, colored up or down. | | Date range | `dateRange` | 2 | The horizontal span alone, in bars and in elapsed time. | | Date and price range | `datePriceRange` | 2 | Both readouts in one box. | | Long position | `longPosition` | 3 | Entry, target and stop, shaded profit and loss, with the reward-to-risk ratio. | | Short position | `shortPosition` | 3 | The same, inverted. | ### Annotation | Tool | `kind` | Points | Notes | |---|---|---|---| | Text | `text` | 1 | A text note anchored to a bar and a price. | | Callout | `callout` | 2 | Text in a rounded box with a tail to the anchor. | | Note with a leader line | `note` | 2 | A dot at the anchor, an underlined note at the second point. | | Price label | `priceLabel` | 1 | The price at a bar as a filled tag, prefixed by `style.text`. | `TEXT_KINDS` names the three that ask the host for their text as they are placed, through `requestText`. ### What the Rail Shows The shell's tool rail shows nine of these by default, because 32 icons crowd a rail: trend line, ray, horizontal line, vertical line, rectangle, parallel channel, Fibonacci retracement, measure and text. Its `drawingTools` option names the set you want instead, in the order you want it. See [The toolbar](https://ortexcharts.com/docs/toolbar). TradingView ships roughly 90 drawing tools, counting the variations within each family. The 32 here are the ones a chart reaches for: the whole Fibonacci set, the channels, the ranges, the position tools and the annotations. Pattern tools, Elliott wave and XABCD among them, are the family that is not here. ## Drawing From the User Interface ```ts tools.startTool("trendline"); // next clicks place the points tools.startTool("fibRetracement", { color: "#F5A524", extend: "right" }); tools.cancelTool(); ``` While a tool is active, each click places a point and the drawing completes when it has the points it needs. Escape cancels. Once a drawing exists, clicking selects it, dragging moves it, and dragging a handle moves one point. Keyboard behavior on a focused chart: | Key | Effect | |---|---| | Escape | Cancel the active tool, or clear the selection. | | Delete or Backspace | Remove the selected drawing. | | Ctrl or Cmd + Z | Undo. | | Ctrl or Cmd + Shift + Z, Ctrl + Y | Redo. | ### Magnet `magnetPx` snaps a placed or dragged price to the nearest open, high, low or close of the bar under the cursor, when one is within that many pixels. It defaults to eight. Setting it to zero disables snapping entirely, which is what a chart of a continuous value rather than of bars wants. ## Drawing From Code ```ts tools.add({ kind: "trendline", points: [ { time: bars[120].time, price: bars[120].low }, { time: bars[300].time, price: bars[300].high }, ], style: { color: "#22C08A", extend: "right", text: "Support" }, meta: { source: "auto-detected", confidence: 0.8 }, }); ``` Only `kind` and `points` are required; the id, style, `locked` and `visible` are filled in. `meta` is carried through serialization untouched, which is where you keep the identifier that links a drawing back to whatever produced it. ```ts tools.updateDrawing(id, { style: { color: "#EF4E5A" }, locked: true }); tools.remove(id); tools.clear(); tools.select(id); // or null to clear the selection tools.selected; // a copy of the selected drawing, or null tools.undo(); tools.redo(); ``` A locked drawing is drawn and hit-tested but cannot be moved, which is the right state for levels your application computed rather than the user drew. ## The Drawing Shape Drawings are plain JSON. Points are anchored to data, not to pixels, so they survive zooming, panning, a resolution change and a window resize. ```ts interface Drawing { id: string; kind: DrawingKind; points: Array<{ time: number; price: number }>; style: DrawingStyle; locked: boolean; visible: boolean; meta?: unknown; } interface DrawingStyle { color: string; // empty string means the theme line color lineWidth: number; // 1.5 lineStyle: "solid" | "dashed" | "dotted"; fillOpacity: number; // 0.12 extend: "none" | "right" | "left" | "both"; text?: string; fontSize: number; // 12 levels?: number[]; // Fibonacci ratios deviation: number; // 2, the regression channel half-width in standard deviations showLabels: boolean; // true } ``` The Fibonacci defaults are `[0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]`. Replace the array to change which levels are drawn, including adding extension levels above one: ```ts tools.add({ kind: "fibRetracement", points: [low, high], style: { levels: [0, 0.382, 0.5, 0.618, 1, 1.618, 2.618] }, }); ``` `DRAWING_STYLE_DEFAULTS` is exported if you want to start from the defaults and change one field. ## Saving and Restoring ```ts const json = JSON.stringify(tools.getDrawings()); tools.setDrawings(JSON.parse(json)); ``` That is the whole persistence story. The chart never writes to storage. Where drawings live is your decision, and the two common answers are different: - **Per symbol.** A trend line drawn on AAPL should be there on every chart of AAPL. This is the most-requested behavior in the category and it is a storage question, so [the shell](https://ortexcharts.com/docs/shell) takes a `drawingsStore` with `load(symbol)` and `save(symbol, drawings)` and does the rest. - **Per layout.** A drawing that belongs to one saved view. Pass `layoutIncludesDrawings: true` to the shell and drawings ride along inside the [layout](https://ortexcharts.com/docs/shell) JSON. Use one or the other, not both. ## Change Events ```ts createDrawingTools(chart.chart, { onChange: (c) => { switch (c.type) { case "created": console.log("added", c.drawing.kind); break; case "updated": console.log("moved", c.drawing.id); break; case "deleted": console.log("removed", c.id); break; case "selected": setSelected(c.id); break; case "toolChanged": setActiveTool(c.kind); break; } }, }); ``` `selected` and `toolChanged` fire for interface state and do not change the data, so persistence should ignore them, which is why the example at the top of this page filters them out. ## Other Hooks ```ts createDrawingTools(chart.chart, { paneId: rsiPane.id, // tools on a pane other than the first priceScaleId: "left", // anchor prices to another scale defaultStyle: { color: "#4C8DFF", lineWidth: 2 }, requestText: async () => window.prompt("Note") ?? null, onEdit: (drawing) => openMyStyleDialog(drawing), priceFormatter: (price) => `$${price.toFixed(2)}`, }); ``` - `requestText` is called when a text drawing is placed. It defaults to `window.prompt`; replace it with your own modal to keep the interface consistent. - `onEdit` fires on a double-click, which is where a style dialog belongs. - `priceFormatter` controls the labels on measures, Fibonacci levels and horizontal lines; it defaults to the main series price format. - One instance covers one pane. Drawing on two panes means two instances. ## Hit Testing and Tooltips Drawings take part in the chart hit test, so a hovered drawing reports through `chart.subscribeCrosshairMove` in the `hit` field of the event, and a click reports through `chart.subscribeClick`. ```ts chart.chart.subscribeClick((e) => { if (!e.hit) return; const payload = e.hit.data as { drawing: Drawing; handle: number } | undefined; if (payload) openDetailPanel(payload.drawing.id, payload.drawing.meta); }); ``` `hit.id` is the drawing id, `hit.data.drawing` is a copy of the drawing including your `meta`, and `hit.data.handle` is the index of the handle under the pointer or −1 for the body. That is enough to open your own context menu or detail panel. --- # Markers and events > Earnings, dividends, filings and news on the chart, as badges, lanes, callouts and ranges. Section: Charts. Source: https://ortexcharts.com/docs/markers ## Two Layers, Different Jobs There are two ways to put something on the chart at a point in time, and they are not interchangeable. - **Markers** (`setMarkers`) are trade-style marks anchored to a bar and a price: arrows, flags, circles, labels. They stack outward on a shared bar and collapse into a count when the zoom gets tight. This is the layer for entries, exits and signals. - **Events** (`setEvents`) are annotations anchored to time, in five presentation styles: lane badges above the time axis, badges next to the bar, callouts with a leader line, vertical lines and shaded ranges. This is the layer for earnings, dividends, filings, lock-ups and news. Both are primitives, both carry a tooltip and an arbitrary payload, and both surface through `chart.subscribeClick`. Use markers for things that belong to a price and events for things that belong to a moment. ## Markers ```ts chart.setMarkers([ { time: bars[120].time, position: "belowBar", shape: "arrowUp", color: "#22C08A", text: "B", tooltip: ["Bought 400 at 182.40", "Order 88213"], data: { orderId: 88213 }, }, { time: bars[164].time, position: "aboveBar", shape: "arrowDown", color: "#EF4E5A", text: "S" }, ]); ``` `setMarkers` replaces the whole list and returns the primitive, so calling it again with a new array is the normal way to update. The primitive is created on the first call and reused after that. | Field | Type | Notes | |---|---|---| | `id` | `string` | Generated when omitted. | | `time` | `number` | Milliseconds since the epoch; snapped to the nearest bar. | | `position` | `"aboveBar" \| "belowBar" \| "inBar" \| "top" \| "bottom"` | Relative to the bar, or pinned to the top or bottom of the pane. | | `shape` | `"arrowUp" \| "arrowDown" \| "circle" \| "square" \| "diamond" \| "flag" \| "label"` | `label` draws `text` in a rounded box. | | `color` | `string` | Falls back to the theme text color. | | `text` | `string` | One or two characters inside or beside the mark. | | `tooltip` | `string \| string[]` | Shown on hover; an array becomes lines. | | `size` | `number` | Overrides the size derived from the bar spacing. | | `data` | `unknown` | Returned with click and hover events. | Behavior worth knowing: - Markers on the same bar stack outward, so a bar with an entry and an exit shows both. - Markers that share a pixel column at low zoom collapse into a count badge, so a decade of trades does not become a solid stripe. - Markers past the last bar on the timeline are not drawn, so [bar replay](https://ortexcharts.com/docs/replay) does not leak the future. ```ts const markers = chart.setMarkers(list); markers.getMarkers(); // the normalized list, ids filled in ``` _Live example: [stock-chart](https://ortexcharts.com/examples/stock-chart)_ ## Events ```ts chart.setEvents([ { time: earnings, style: "lane", group: "Earnings", text: "E", tooltip: ["Q3 earnings", "EPS 2.18 vs 2.04 est"] }, { time: dividend, style: "lane", group: "Dividends", text: "D", tooltip: "Ex-dividend 0.25" }, { time: fomc, style: "line", text: "FOMC", lineStyle: "dashed", color: "#60A5FA" }, { time: newsAt, style: "callout", text: "Guidance raised", position: "aboveBar" }, { time: lockupStart, endTime: lockupEnd, style: "range", text: "Lock-up", color: "#A78BFA" }, ]); ``` ### The Five Styles | Style | Looks like | Used for | |---|---|---| | `lane` | A letter badge in a strip above the time axis, one row per `group`. | Earnings, dividends, splits — the convention traders already read. | | `badge` | A letter in a circle, square or pin next to the bar. | Signals, alerts, pinned news. | | `callout` | A text box on the chart with a leader line to the bar. | Analyst calls, headlines, trade notes. | | `line` | A vertical line across the pane with a label tag. | Macro events, index rebalances, product launches. | | `range` | A shaded band between `time` and `endTime` with a label. | Lock-ups, blackout periods, trading halts. | Lanes never overlap the price, which is why they are the right default for a recurring corporate-action feed. Badges and lane items collapse into a count at low zoom; callouts truncate and stack so they do not cover each other. ### The Event Shape | Field | Type | Notes | |---|---|---| | `id` | `string` | Generated when omitted. | | `time` | `number` | Anchor, in milliseconds. | | `endTime` | `number` | Range end; only for `range`. | | `style` | `EventStyle` | One of the five above. | | `text` | `string` | One or two letters for badges and lanes; the label for the rest. | | `tooltip` | `string \| string[]` | Hover text. | | `color` | `string` | Falls back to the theme. | | `position` | `"aboveBar" \| "belowBar" \| "top" \| "bottom"` | For badges and callouts. | | `price` | `number` | Callout anchor price; defaults to the bar high or low. | | `shape` | `"circle" \| "square" \| "pin"` | Badge shape. | | `group` | `string` | Lane row key; badges of one group share a row. | | `lineStyle` | `"solid" \| "dashed" \| "dotted"` | For `line` events. | | `data` | `unknown` | Returned with click and hover events. | ### Layout Options ```ts import { addEvents } from "@ortex-charts/financial"; const events = addEvents(chart.chart, chart.main, list, { laneHeight: 18, badgeSize: 16, rangeOpacity: 0.12, }); ``` `chart.setEvents(list)` is the same thing with the defaults. Use `addEvents` directly when you want the options or when the events belong to a series other than the main one. ## Reacting to Clicks and Hovers Both layers report through the chart hit test, so one subscription covers everything on the chart, including [drawings](https://ortexcharts.com/docs/drawings) and [alerts](https://ortexcharts.com/docs/replay). ```ts chart.chart.subscribeClick((e) => { if (!e.hit) return; const payload = e.hit.data as { orderId?: number } | undefined; if (payload?.orderId) openOrder(payload.orderId); }); chart.chart.subscribeCrosshairMove((e) => { setHovered(e.hit?.id ?? null); }); ``` The tooltip in `HitResult.tooltip` is rendered by the chart itself, so a marker or event with a `tooltip` needs no work from you. Subscribe only when you want behavior beyond the tooltip. ## A Worked Example: A Corporate-Action Feed The usual integration is a list of dated records from an API turned into lanes, one row per family, with the raw record kept for the detail panel. ```ts type Action = { date: string; kind: "earnings" | "dividend" | "split"; headline: string; detail: string }; const LETTER = { earnings: "E", dividend: "D", split: "S" } as const; const GROUP = { earnings: "Earnings", dividend: "Dividends", split: "Splits" } as const; chart.setEvents( actions.map((a) => ({ time: Date.parse(`${a.date}T00:00:00Z`), style: "lane" as const, group: GROUP[a.kind], text: LETTER[a.kind], tooltip: [a.headline, a.detail], data: a, })), ); chart.chart.subscribeClick((e) => { const action = e.hit?.data as Action | undefined; if (action?.date) showActionDetail(action); }); ``` Events do not need to be sorted; the primitive sorts and normalizes the list. Re-calling `setEvents` on a symbol change replaces everything, which is the intended pattern when the chart changes symbol. --- # Order flow > Footprint bars, volume profile, market profile, delta, cumulative delta and relative volume. Section: Charts. Source: https://ortexcharts.com/docs/order-flow ## The Position This Library Takes Most charting libraries estimate directional volume. They do not know which side was the aggressor, so they infer it from where the price moved inside the bar, which is the tick rule from the 1990s. The delta, cumulative delta and footprint numbers that come out of it disagree with the platforms that read exchange aggressor flags, and traders notice. ORTEX Charts takes the other route. **The host supplies the aggressor data and the library renders it exactly.** Where the data is missing it estimates, marks the result as estimated in the tooltip, and offers a strict mode that shows a gap rather than a guess. That is a real requirement, not a preference: footprint bars, exact volume profiles and true delta need per-price bid and ask volume from your feed. Without it you get the same estimates everyone else produces, honestly labeled. ## The Data Model Three optional shapes carry order-flow information into the library. ```ts // On a bar: volume that lifted the offer and volume that hit the bid. interface Bar { time: number; open: number; high: number; low: number; close: number; volume: number; buyVolume?: number; sellVolume?: number; } // Per bar, per price level: a footprint. interface FootprintBar { time: number; levels: Array<{ price: number; bid: number; ask: number }>; // ascending by price } // On a tick: the aggressor side when the feed reports it. interface Tick { time: number; price: number; size: number; side?: "buy" | "sell" } ``` `bid` is volume that traded at the bid, meaning aggressive sellers. `ask` is volume that lifted the offer, meaning aggressive buyers. `buyVolume` and `sellVolume` travel through every path in the engine: row conversion, bar aggregation, the realtime append buffers, `update`, and the aligned columns indicators read. Two consequences follow from how the columns are allocated: - A series created **without** aggressor volumes ignores them on later bars. History cannot be retro-fitted, so set the first `setData` with the columns you intend to use. - A series created **with** them writes `NaN` for a bar that lacks them, which is what `method: "aggressor"` shows as a gap. ## Footprint Bars ```ts const fp = chart.setFootprint(flow, { tickSize: 0.01, layout: "bidAsk", imbalanceRatio: 3, }); fp.updateBar(liveFootprintBar); // realtime ``` For every bar wider than `minBarWidth` the primitive draws one cell per price level: the bid half shaded in the down color and the ask half in the up color by share of the busiest level in that bar, the numbers when the cell is at least nine pixels high, diagonal imbalances outlined, the bar point of control boxed, and delta plus total volume under the bar. When bars are narrower than `minBarWidth` the cells are unreadable, so the primitive draws a thin per-bar delta strip along the bottom of the pane instead. The information stays visible at every zoom level rather than disappearing. Hovering a cell shows its bid, ask, delta and total. | Option | Default | What it does | |---|---|---| | `minBarWidth` | `28` | Below this bar width in pixels, the delta strip replaces the cells. | | `cluster` | `1` | Merge this many consecutive price levels into one cell. | | `tickSize` | `0` | Level height in price units; zero derives it from the smallest gap between levels. | | `layout` | `"bidAsk"` | `bidAsk` prints bid and ask per cell, `delta` prints ask − bid, `volume` prints the total. | | `imbalanceRatio` | `3` | Outline a level whose ask is at least this many times the bid one level below, and the mirror. Zero disables it. | | `heat` | `true` | Shade cells by volume relative to the busiest level of the bar. | | `showTotals` | `true` | Print delta and total under each bar. | | `showPoc` | `true` | Outline the bar point of control. | | `buyColor` | `null` | Overrides the theme up color. | | `sellColor` | `null` | Overrides the theme down color. | | `stripHeight` | `4` | Height of the fallback delta strip in pixels. | `setFootprint` does two extra things worth knowing. It gives the main bars their aggressor volumes if they arrived without them, so Delta and CVD become exact; and it feeds every volume profile added with `addVolumeProfile`, so those become exact too. _Live example: [footprint](https://ortexcharts.com/examples/footprint)_ ### Building Footprints From Ticks If your feed gives sided ticks rather than pre-aggregated levels: ```ts import { footprintFromTicks, mergeFootprint } from "@ortex-charts/financial"; import { parseResolution } from "@ortex-charts/math"; const flow = footprintFromTicks(ticks, parseResolution("1"), 0.01, "America/New_York"); const barsWithFlow = mergeFootprint(bars, flow); chart.setData(barsWithFlow); chart.setFootprint(flow, { tickSize: 0.01 }); ``` Ticks without a `side` fall back to the tick rule, which is the estimate every platform without aggressor data uses. Pass sided ticks for exact results. `footprintTotals(bar)` gives `{ buy, sell, total, delta, poc }` for one footprint bar, which is what a host-drawn summary row wants. ## Volume Profile and Market Profile ```ts const vp = chart.addVolumeProfile({ mode: "visible", tickSize: 0.01, splitDelta: true }); vp.setRange(fromTime, toTime); // switch to a fixed range vp.setOptions({ mode: "session" }); // one profile per trading day vp.setOptions({ kind: "tpo", tpoPeriodMs: 30 * 60_000 }); // market profile vp.profiles(); // the computed rows, POC and value area ``` ### Modes - `visible` recomputes for the bars currently on screen, cached per range and data version. - `fixed` uses `range`, which is what you set from a date-range drawing or a preset button. - `session` draws one profile per zoned trading day, anchored at the first bar of the day. ### Kinds - `volume` counts volume at price: a volume profile. - `tpo` counts one letter per `tpoPeriodMs` period per row: a market profile. Letters are drawn when rows are at least nine pixels high and a histogram replaces them below that. ### Accuracy With footprint levels attached, volume lands in the row that contains its price and the buy-and-sell split is real. The computed profile reports `exact: true`. Without them, each bar volume is spread evenly over the rows its range covers and split by where the close sits in that range. The profile reports `exact: false` and the tooltip says "estimated". This is the same approximation other libraries make; the difference is that it is labeled. ### Point of Control and Value Area The point of control is the row with the most volume, or the most letters for a market profile. The value area grows outward from it, taking the larger neighbor at each step, until it holds `valueArea` of the total, which defaults to 70 percent. ### Options | Option | Default | What it does | |---|---|---| | `kind` | `"volume"` | `volume` or `tpo`. | | `mode` | `"visible"` | `visible`, `fixed` or `session`. | | `range` | `null` | `[fromTime, toTime]` for `fixed`. | | `rows` | `24` | Number of price rows when `tickSize` is zero. | | `tickSize` | `0` | Row height in price units; capped at 400 rows. | | `width` | `0.3` | Widest bar as a fraction of the plot width, or of the session width. | | `position` | `"right"` | Which edge the profile grows from. | | `valueArea` | `0.7` | Share of volume inside the value area. | | `showPoc` | `true` | Draw the point-of-control line. | | `showValueArea` | `true` | Draw the value area more opaquely. | | `splitDelta` | `false` | Draw buy and sell volume as two stacked segments. | | `showLabels` | `true` | Print the row total when there is room. | | `color`, `buyColor`, `sellColor` | `null` | Override the theme. | | `opacity` | — | Fill opacity of the profile. | | `tpoPeriodMs` | — | Market profile period; one letter per period. | Profiles draw below the series, so they never cover the candles. Hovering a row shows its price range, volume, buy and sell split, and delta. _Live example: [volume-profile](https://ortexcharts.com/examples/volume-profile)_ ### Computing a Profile Without Drawing One `computeProfile` is the pure function behind the primitive and is what the tests cover. Use it when you want the numbers for a table, an export or a rule. It takes bar columns, an inclusive bar-index range, options, and an optional map of footprint bars keyed by bar time. ```ts import { computeProfile, type FootprintBar } from "@ortex-charts/financial"; const raw = chart.main.raw; if (!raw || !("high" in raw)) throw new Error("no bars yet"); const byTime = new Map(flow.map((f) => [f.time, f])); const profile = computeProfile( { time: raw.time, high: raw.high, low: raw.low, close: raw.close, volume: raw.volume }, 0, raw.length - 1, { rows: 40, valueArea: 0.7 }, byTime, ); console.log(profile.rows[profile.poc], profile.vaLow, profile.vaHigh, profile.exact); ``` The primitive already keeps its own computed results, so `vp.profiles()` is the cheaper route when a profile is on screen anyway. ## The Three Indicators | Id | Pane | What it shows | |---|---|---| | `delta` | Separate, histogram colored by sign, level line at zero | Per-bar buy minus sell volume. | | `cvd` | Separate, line | Cumulative delta. | | `rvol` | Separate, histogram, level line at one | Volume over the mean of the previous `length` bars, the current bar excluded. | `delta` and `cvd` share a `method` input: - `auto` uses the real aggressor columns when they exist and estimates otherwise. - `aggressor` uses only real data and writes `NaN` where it is missing, so a gap in the histogram is a gap in the feed rather than a zero. - `estimate` always uses the tick-rule estimate, which is useful when you want the number other platforms would show. ```ts chart.addIndicator("delta", { method: "aggressor" }); chart.addIndicator("cvd", { method: "auto" }); chart.addIndicator("rvol", { length: 50 }); ``` `indicators.barDelta(columns, method)` is exported for hosts that want the numbers without a pane. _Live example: [delta-cvd](https://ortexcharts.com/examples/delta-cvd)_ ## Putting It Together ```ts import { createFinancialChart, mergeFootprint } from "@ortex-charts/financial"; const chart = createFinancialChart(el, { timeZone: "America/New_York", resolution: "1", session: "0930-1600", data: mergeFootprint(bars, flow), seriesOptions: { priceFormat: { type: "price", precision: 2, minMove: 0.01 } }, }); chart.setFootprint(flow, { tickSize: 0.01, layout: "bidAsk", imbalanceRatio: 3 }); chart.addVolumeProfile({ mode: "session", tickSize: 0.01, splitDelta: true }); chart.addIndicator("delta", { method: "aggressor" }); chart.addIndicator("cvd"); ``` ## What Is Not Here - Footprint is a primitive drawn over whichever main series is shown, not a series type of its own, so there is no delta-colored candle style yet. - Composite profiles across several symbols are not supported. - Profiles do not animate as developing profiles during [bar replay](https://ortexcharts.com/docs/replay). - The toolbar has no built-in profile toggle. Adding one is a [`ToolbarAction`](https://ortexcharts.com/docs/toolbar) that calls `addVolumeProfile`. --- # Replay, linking and alerts > Bar replay with higher-timeframe followers, linked charts, and draggable price alerts. Section: Charts. Source: https://ortexcharts.com/docs/replay ## Three Features, One Principle Bar replay, linked charts and price alerts are the three most-requested additions to a charting library after indicators and drawings. All three are implemented the same way here: no server, no account, no ORTEX service. The host keeps whatever state survives a reload, and the library does the rest in the browser. ## Bar Replay Replay steps through history as if it were arriving live. The controller keeps the full data, shows a prefix of it, and appends each next bar **through the realtime path** rather than by redrawing a slice. That detail is what makes it useful: indicators take their incremental fast paths, the volume pane mirrors each bar, and drawings and alerts see exactly the sequence they would see on a live feed. ```ts const replay = chart.replay({ speed: 4 }); // created once, reused after that replay.start({ time: someTime }); // or start(barIndex) replay.step(1); replay.step(-1); replay.play(10); // 10 bars per second replay.pause(); replay.stop(); // put the full history back replay.subscribe((s) => { if (!s.active) return; setPosition(`${s.index + 1} / ${s.total}`, new Date(s.time), s.playing, s.speed); }); ``` `ReplayState` is `{ active, playing, index, total, time, speed }`. `index` is the position in the full history and is −1 while replay is inactive. | Method | What it does | |---|---| | `start(at)` | Begin at a bar index, or at `{ time }`. Defaults to the beginning. | | `step(count)` | Move forward or backward by whole bars. | | `seek(index)` | Jump to a bar. | | `play(speed?)` | Advance automatically at `speed` bars per second. | | `pause()` | Stop advancing, stay in replay. | | `stop()` | Leave replay and restore the full history. | | `link(target, resolution?)` | Make another chart follow the clock. | | `history()` | The full bar array the controller snapshotted. | | `subscribe(cb)` | State changes; returns an unsubscribe function. | Options are `speed` (bars per second, default two) and `follow` (keep the current bar in view while stepping, default true). ### Live Data During Replay A datafeed or WebSocket can keep running while the user replays. Updates that arrive are **held on the series** and applied in order when replay stops, so nothing is lost and nothing jumps into the middle of the replayed sequence. Under the hood this is `SeriesModel.holdUpdates()` and `releaseUpdates()`, which you can use directly for any other reason you might want to freeze a series. Markers and events past the last replayed bar are not drawn, so the future stays hidden rather than spoiling what the replay is for. ### Backward Steps Forward steps append one bar. Backward steps and seeks reset the prefix, which is a rebuild rather than an append. That asymmetry is deliberate: stepping forward is the common case and is cheap, and stepping backward is rare and costs a full recompute. _Live example: [replay](https://ortexcharts.com/examples/replay)_ ### Multi-Timeframe Replay ```ts const unlink = replay.link(fiveMinuteChart); // a FinancialChart, or { chart, main } ``` A linked chart follows the clock. It shows its own bars that closed before the current period, plus the current period **aggregated from the master bars so far**, so a five-minute candle forms in front of the viewer as the one-minute replay advances. This is the behavior traders mean by multi-timeframe replay, and it is the reason the higher timeframe cannot simply be a second replay. Link charts at the master resolution or coarser; linking a finer chart has nothing to aggregate from. Pair it with `linkCharts` when you also want a shared crosshair. ### In the Shell The toolbar's Replay button is off by default because most charts do not want it. ```ts createChartShell(el, { symbol: "AAPL", datafeed, toolbar: { replay: true } }); ``` Clicking it asks for a start bar: the next click on the chart begins the replay there, and a control strip appears under the toolbar with step back, play and pause, step forward, a speed selector from 1× to 30×, the position and an exit button. From code, `shell.replay()` returns the same `ReplayController`, and `shell.startReplay(index?)` and `shell.stopReplay()` drive it without the click. ## Linked Charts ```ts import { linkCharts } from "@ortex-charts/financial"; const unlink = linkCharts([daily.chart, hourly.chart, minute.chart], { timeRange: true, crosshair: true, }); ``` Panning or zooming any chart sets the same **time** span on the others, and the crosshair mirrors by time. Because the link is by time rather than by bar index, it works across resolutions: a daily chart and a one-minute chart stay on the same window and the bar spacing adapts on each. Echoes are suppressed, so there are no feedback loops. Both options default to true; set either to `false` to link only the other. The returned function unlinks everything. There is no grid container in the library, so a two-by-two layout is your own CSS. The link works with any arrangement of containers. ## Price Alerts Alerts are draggable horizontal lines that report crossings to the host. Nothing here talks to a server, sends an email or schedules anything: the library detects the crossing and tells you, and what happens next is your product's decision. ```ts const alerts = chart.alerts({ draggable: true }); alerts.add({ price: 182.5, label: "Breakout", color: "#22C08A" }); alerts.add({ price: 178, label: "Support", repeat: true }); alerts.subscribe((e) => { switch (e.type) { case "triggered": notify(`${e.alert.label} crossed ${e.direction} at ${e.price}`); break; case "added": case "removed": case "moved": saveAlerts(alerts.list()); break; } }); alerts.set(savedAlerts); // on load ``` ### Behavior - The line is dashed with a labeled tag on the right, drawn on the main series scale. - Dragging it moves the price and re-arms the alert, and emits a `moved` event carrying the previous price in `from`. - Every data update — live, replayed or set in bulk — is checked for a crossing of the main series last value, in either direction. - An alert is one-shot by default: it fires once, `triggered` is set, and it does not fire again until it is moved or patched. `repeat: true` fires on every crossing. - `hideTriggered: true` hides the line after a one-shot alert fires. ### The Alert Shape and the API ```ts interface Alert { id: string; price: number; label?: string; color?: string; repeat?: boolean; triggered?: boolean; data?: unknown; // anything you want back with the events } ``` | Method | What it does | |---|---| | `add(alert)` | Add one; `price` is required, the id is generated. | | `remove(id)` | Remove one. | | `patch(id, patch)` | Change price, label, color, repeat or triggered. | | `list()` | Plain JSON for your store. | | `set(list)` | Replace everything, for example after loading. | | `subscribe(cb)` | Alert events; returns an unsubscribe function. | `list()` and `set()` are the whole persistence contract. Store the array against a user and a symbol, restore it when the chart loads, and re-save on `added`, `removed` and `moved`. Alerts have no toolbar chrome in the shell. `shell.chart.alerts()` is the API and the host decides how to present adding one — a button, a context menu, a right-click on the price scale. _Live example: [alerts](https://ortexcharts.com/examples/alerts)_ ## Putting the Three Together A common arrangement: a master chart the user replays, a higher-timeframe chart following the clock, both linked for the crosshair, and alerts that fire during the replay so a strategy can be checked against them. ```ts import { createFinancialChart, linkCharts } from "@ortex-charts/financial"; const minute = createFinancialChart(topEl, { resolution: "1", timeZone: "America/New_York", data: minuteBars }); const fiveMin = createFinancialChart(bottomEl, { resolution: "5", timeZone: "America/New_York", data: fiveMinuteBars }); const unlinkCharts = linkCharts([minute.chart, fiveMin.chart], { crosshair: true, timeRange: false }); const replay = minute.replay({ speed: 8 }); const unlinkReplay = replay.link(fiveMin); const alerts = minute.alerts(); alerts.set(savedAlerts); alerts.subscribe((e) => e.type === "triggered" && toast(`${e.alert.label ?? "Alert"} at ${e.price}`)); replay.start({ time: sessionOpen }); replay.play(); ``` Note that `timeRange` is switched off in this arrangement: the follower chart is driven by the replay clock, so a second synchronization of the visible range would fight it. Keep the crosshair link, drop the range link. --- # Time, sessions and resolutions > Time zones, trading sessions, session-anchored intraday bars and aggregation. Section: Data. Source: https://ortexcharts.com/docs/time ## Timestamps Are Milliseconds, UTC Every timestamp in the library — bars, ticks, events, drawings, alerts, ranges — is milliseconds since the Unix epoch in UTC. There is no other convention anywhere and no per-series exception. A time zone never changes a timestamp. It decides two things: where bar boundaries fall when the library builds or aggregates bars itself, and how the axis, crosshair and legend labels read. ```ts const chart = createFinancialChart(el, { timeZone: "America/New_York", // an IANA zone name locale: "en-GB", // formatting locale for dates and numbers resolution: "1D", }); ``` Zone handling goes through `Intl`, so any IANA zone the browser knows works, daylight saving transitions are handled by the platform rather than by an offset table, and there is no time zone database in the bundle. If your feed sends seconds, multiply by 1,000 before handing the rows over. If it sends `YYYY-MM-DD` for daily bars, the [adapter layer](https://ortexcharts.com/docs/data) has a `date` time unit that parses it as midnight in a named zone, which is almost always what you want for daily data. ## Resolutions A resolution is written in datafeed notation: a number of minutes, or a number with a `D`, `W` or `M` suffix. | Text | Meaning | |---|---| | `1`, `5`, `15`, `30` | Minutes. | | `60`, `240` | Minutes that happen to be whole hours; parsed as hours. | | `1H`, `4H` | Hours, normalized to the minute form. | | `1D`, `3D` | Days. | | `1W` | Weeks. | | `1M`, `3M` | Months. | ```ts import { parseResolution, floorTimeToBar, nextBarTime, sameBar } from "@ortex-charts/math"; const res = parseResolution("15"); // { text: "15", unit: "minute", multiple: 15, isIntraday: true, durationMs: 900000 } floorTimeToBar(Date.now(), res, "America/New_York"); // start of the current bar nextBarTime(Date.now(), res, "America/New_York"); // start of the next one sameBar(a, b, res, "America/New_York"); // do two times share a bar ``` `durationMs` is nominal: calendar units use one day, seven days and 30 days respectively, because a month is not a fixed length. Bar boundaries themselves are computed with calendar arithmetic in the chart time zone, not by dividing by `durationMs`. The chart resolution matters in three places: it is what live ticks are folded into, it is what the axis labels are chosen for, and it is what a datafeed binding requests. Set it in options, or let the [datafeed binding](https://ortexcharts.com/docs/data) set it when the user changes it. ## Sessions A session string describes trading hours as local wall-clock minutes, with commas between segments and an optional day list after a colon. ```text 0930-1600 regular US equity hours 0400-0930,0930-1600,1600-2000 with pre-market and after-hours 24x7 continuous, for crypto 0930-1600:12345 Monday to Friday only (the default anyway) 1700-1600 an overnight session that wraps midnight ``` ```ts import { parseSession, isInSession, sessionBoundsForDay, sessionMinuteAt } from "@ortex-charts/math"; const session = parseSession("0400-0930,0930-1600,1600-2000"); isInSession(Date.now(), session, "America/New_York"); // boolean sessionBoundsForDay(Date.now(), session, "America/New_York"); // [[start, end], …] in ms sessionMinuteAt(Date.now(), session, "America/New_York"); // minutes elapsed in the session, NaN when closed ``` Days are 0 for Sunday through 6 for Saturday, and the default is Monday to Friday. Adjacent segments are merged when parsed, so `0930-1200,1200-1600` becomes one segment. The chart takes the session as an option, and the [datafeed binding](https://ortexcharts.com/docs/data) sets it for you from `SymbolInfo.session` when a symbol resolves. ```ts createFinancialChart(el, { timeZone: "America/New_York", resolution: "60", session: "0930-1600" }); ``` ## Shading Extended Hours An intraday chart of a stock with pre-market and after-hours data draws 04:00 exactly like 10:00, and a reader cannot tell thin extended-hours prints from the real session. Two options fix that, and both are off by default: ```ts createFinancialChart(el, { timeZone: "America/New_York", resolution: "60", session: "0400-0930,0930-1600,1600-2000", sessions: { shadeExtendedHours: true, dayBreaks: true, // Regular hours. Without this the first segment is used, which for the // session above is the pre-market, so a symbol with extended hours wants // it stated. regular: "0930-1600", }, data: bars, }); ``` | Option | Effect | |---|---| | `sessions.shadeExtendedHours` | Fills the columns outside `regular` with the theme's `sessionShade` | | `sessions.dayBreaks` | Draws a line at the first column of each trading day, in the theme's `sessionBreak` | | `sessions.regular` | Regular hours, as a session string. Defaults to the first segment of `session` | The day boundary is the **exchange's**, not the reader's: two bars an hour apart across UTC midnight are one New York day and two London days, and the break is drawn where the exchange says the day turned. Both colours are theme tokens, so a custom theme sets them like any other. Neither option costs anything on a chart that does not use it: a daily chart, or a market with no extended hours, produces no bands. ## Session-Anchored Intraday Bars This is the part that is different from most libraries, and it fixes a long-standing complaint about hourly candles. If hourly bars are floored to the hour, a US equity session produces a first candle covering only 09:30 to 10:00 and then a run of clean hours. The half-hour stub is an artifact of the arithmetic, not of the market. When the chart knows the session, intraday resolutions are **phased from the session open** instead. With `0930-1600`, 60-minute bars run 09:30–10:30, 10:30–11:30 and so on, and pre-market bars keep the same phase going backwards, so 08:30–09:30 lines up with the rest. ```ts import { anchoredResolution, parseResolution } from "@ortex-charts/math"; const res = anchoredResolution(parseResolution("60"), "0930-1600"); // { …, anchorMinute: 570 } // 09:30 expressed in minutes from midnight ``` Four things worth being precise about: - The anchor applies to **everything the library builds or aggregates itself**: ticks folded into bars by `streamTo`, `aggregateBars`, and the higher-timeframe followers in [bar replay](https://ortexcharts.com/docs/replay). - Bars delivered ready-made by a feed are shown exactly as given. The library does not re-cut somebody else's candles. - Daily and longer resolutions, and `24x7` sessions, are unaffected. - With several segments, the regular session is the one that starts latest before noon, so `0400-0930,0930-1600,1600-2000` anchors at 09:30 rather than at 04:00. _Live example: [intraday-session](https://ortexcharts.com/examples/intraday-session)_ ## The Time Axis The axis is indexed by bar position, so closed periods do not exist on it. There is no gap to hide, no `breaks` configuration and no special case for holidays: a day the market did not trade simply has no bars. Tick labels are chosen by weight rather than by a fixed interval, so a year label beats a month label, a month beats a day and a day beats an hour. When there is not room for everything, the more significant labels survive, which is why a two-year daily chart reads as years and months rather than as a row of collided dates. ```ts chart.applyOptions({ timeScale: { barSpacing: 8, // pixels per bar minBarSpacing: 0.5, maxBarSpacing: 60, rightOffsetPx: 60, // empty space right of the last bar fixLeftEdge: false, // stop scrolling past the first bar fixRightEdge: false, followRealtime: true, // follow new bars while the view is at the right edge borderVisible: true, visible: true, }, timeLabelGap: 72, // minimum pixels between time labels }); ``` `followRealtime` is what makes a live chart scroll with new bars only while the user is already at the right edge. Scrolling back into history stops the follow until `scrollToRealtime()` is called or the user pans back. ## Working With Ranges ```ts chart.timeScale.setVisibleRange(fromIndex, toIndex); chart.timeScale.setVisibleTimeRange(fromTime, toTime); chart.timeScale.fitContent(); chart.timeScale.scrollToRealtime(); chart.timeScale.setBarSpacing(12); chart.timeScale.timeToCoordinate(time); chart.timeScale.coordinateToTime(px); chart.timeScale.timeToIndex(time); chart.timeScale.indexToTime(index); const off = chart.timeScale.subscribeVisibleRangeChange((r) => { console.log(r.fromIndex, r.toIndex, r.fromTime, r.toTime, r.barSpacing); }); ``` Index ranges are exact and time ranges are converted to them, so `setVisibleRange` is the right call when you know how many bars you want on screen and `setVisibleTimeRange` is the right call when you are synchronizing with something that thinks in dates. Range presets — 1D, 5D, 1M, 3M, 6M, YTD, 1Y, 5Y, All — are not built into the toolbar. They are a few lines on top of `setVisibleTimeRange`, and adding them as [toolbar actions](https://ortexcharts.com/docs/toolbar) is the usual approach: ```ts const RANGES = { "1M": 30, "3M": 91, "6M": 182, "1Y": 365, "5Y": 1826 }; function applyRange(days: number) { const to = chart.main.rawTimes[chart.main.rawLength - 1]; chart.chart.timeScale.setVisibleTimeRange(to - days * 86_400_000, to); } ``` ## Aggregating Bars Yourself When you have fine bars and want coarse ones without another request: ```ts import { aggregateBars, anchoredResolution, barSeriesFromRows, barSeriesToRows, parseResolution } from "@ortex-charts/math"; const minute = barSeriesFromRows(minuteBars); const hourly = aggregateBars(minute, anchoredResolution(parseResolution("60"), "0930-1600"), "America/New_York"); chart.setData(barSeriesToRows(hourly)); ``` Aggregation carries `buyVolume` and `sellVolume` through when the source has them, so an [order-flow](https://ortexcharts.com/docs/order-flow) chart keeps exact delta after aggregation. ## Formatting Time Yourself ```ts import { formatBarTime, formatClock, formatDate } from "@ortex-charts/math"; formatBarTime(time, { timeZone: "America/New_York", resolution: parseResolution("15"), locale: "en-US" }); formatClock(time, { timeZone: "America/New_York" }); formatDate(time, { timeZone: "America/New_York" }); ``` `formatBarTime` picks the shape from the resolution, which is what the crosshair label uses: a date for daily bars, a date and a time for intraday ones. All three go through `Intl`, so `locale` controls the ordering and the month names. There is no string table for the rest of the interface. The toolbar, menus and dialogs in `@ortex-charts/ui` are English only today; dates and numbers localize, labels do not. --- # Your own data > Static arrays, a datafeed, a WebSocket, or an adapter that maps your existing JSON. Section: Data. Source: https://ortexcharts.com/docs/data ## The Library Never Fetches ORTEX Charts consumes data; it does not go and get it. There is no built-in HTTP client, no symbol database, no cache service and no ORTEX endpoint your chart depends on. You supply rows, or you supply an object that knows how to produce rows, and the chart draws them. That leaves four ways in, in increasing order of how much the library does for you: 1. **A static array.** You already have the bars. Call `setData`. 2. **A datafeed.** An object with `getBars` and optionally `subscribeBars`; the chart handles the initial load, lazy history when the user scrolls left, symbol switching and the realtime subscription. 3. **A live source.** Anything that pushes items over time — a WebSocket, a poller, a handle you push into — folded into bars at the chart resolution. 4. **An adapter spec.** A small JSON description of where history, live data and symbol metadata live in *your* messages, from which the library builds the datafeed and the sources for you. No code per feed. They compose. The common production shape is an adapter spec for the price feed and host-supplied series for everything else. ## Static Bars ```ts import { createFinancialChart } from "@ortex-charts/financial"; import type { Bar } from "@ortex-charts/math"; const bars: Bar[] = rows.map((r) => ({ time: r.t * 1000, // milliseconds, UTC open: r.o, high: r.h, low: r.l, close: r.c, volume: r.v, })); const chart = createFinancialChart(el, { timeZone: "America/New_York", resolution: "1D", data: bars }); chart.setData(nextBars); // replaces everything ``` Bars must be sorted ascending by time. `volume` is required by the type; use zero when your instrument has none. ### Every Accepted Input Shape `setData` takes whichever of these you already have, so there is normally no conversion step. | Shape | Example | Use for | |---|---|---| | `Bar[]` | `[{ time, open, high, low, close, volume }]` | OHLCV series. | | `BarSeries` | `{ length, time, open, high, low, close, volume }` with `Float64Array` columns | Large OHLCV data you already hold columnar. | | `[time, value][]` | `[[1735689600000, 12.4], …]` | The shape most value endpoints return. | | `{ time, value }[]` | `[{ time: 1735689600000, value: 12.4 }]` | Readable value series. | | `ValueSeries` | `{ length, time, value }` with `Float64Array` columns | Large value data held columnar. | Columnar input is the cheapest, because nothing is copied. Helpers convert between the forms when you need them: ```ts import { barSeriesFromRows, barSeriesToRows, valueSeriesFromPoints, valueSeriesFromColumns } from "@ortex-charts/math"; const series = barSeriesFromRows(bars); const rows = barSeriesToRows(series); const values = valueSeriesFromPoints([[t1, v1], [t2, v2]]); const columns = valueSeriesFromColumns(timeArray, valueArray); ``` ### Optional Aggressor Volumes A bar may carry `buyVolume` and `sellVolume`: the volume that lifted the offer and the volume that hit the bid. They make delta, cumulative delta and split volume profiles exact rather than estimated. See [Order flow](https://ortexcharts.com/docs/order-flow). The one rule to remember is that a series created **without** those columns ignores them on later bars, because history cannot be retro-fitted into an already-allocated column. Decide at the first `setData`. ## A Datafeed A datafeed is one object with one required method. Give it to the chart with `bind` and the chart handles the initial load, paging older history when the user scrolls near the left edge, realtime subscription, and the teardown and reload on a symbol or resolution change. ```ts import type { Datafeed } from "@ortex-charts/financial"; const datafeed: Datafeed = { async resolveSymbol(symbol) { const info = await fetch(`/api/symbols/${symbol}`).then((r) => r.json()); return { symbol, name: info.name, exchange: info.exchange, timeZone: info.tz, // applied to the chart session: info.hours, // applied to the chart, anchors intraday bars currency: info.ccy, priceFormat: { precision: info.decimals, minMove: 10 ** -info.decimals }, logo: info.logoUrl, // shown in the toolbar and the legend }; }, async getBars({ symbol, resolution, from, to, countBack, firstRequest }) { const url = `/api/bars?s=${symbol}&res=${resolution}&from=${from}&to=${to}`; const rows: Array<[number, number, number, number, number, number]> = await fetch(url).then((r) => r.json()); return { bars: rows.map(([t, o, h, l, c, v]) => ({ time: t, open: o, high: h, low: l, close: c, volume: v })), noMoreData: rows.length === 0, }; }, subscribeBars(symbol, resolution, onBar) { const socket = openMyFeed(symbol, resolution, onBar); return () => socket.close(); }, async searchSymbols(query) { return fetch(`/api/search?q=${encodeURIComponent(query)}`).then((r) => r.json()); }, }; const binding = chart.bind(datafeed, { symbol: "AAPL", resolution: "1D" }); ``` ### The Interface ```ts interface Datafeed { resolveSymbol?(symbol: string): Promise; getBars(request: BarsRequest): Promise; subscribeBars?(symbol: string, resolution: string, onBar: (bar: Bar) => void): () => void; searchSymbols?(query: string): Promise; } interface BarsRequest { symbol: string; resolution: string; from: number; // inclusive start, milliseconds to: number; // exclusive end, milliseconds countBack: number; // the caller wants at least this many bars before `to` firstRequest: boolean; } interface BarsResponse { bars: Bar[]; noMoreData?: boolean; // set when the feed knows there is nothing earlier } interface SymbolInfo { symbol: string; name?: string; exchange?: string; timeZone?: string; session?: string; // "0930-1600" priceFormat?: Partial; currency?: string; logo?: string; // URL or data URI } ``` Returning a bare `Bar[]` from `getBars` is allowed; it is treated as `{ bars }` with no `noMoreData`. ### What the Binding Does For You - Resolves the symbol, applies `timeZone`, `session` and `priceFormat` to the chart and the series, and reports the info through `onSymbolInfo`. - Requests an initial window sized from `initialBars` (500 by default) and the resolution, sorts the rows, sets them and scrolls to the right edge. - Watches the visible range and requests older history when fewer than `loadMoreThresholdBars` (60 by default) remain to the left of the view, stopping when the feed returns `noMoreData` or nothing. - Subscribes to realtime bars and applies them through `series.update`, so a bar with the same time replaces the current one and a later time appends. - Cancels in-flight work on a symbol or resolution change with a generation counter, so a slow response for the old symbol cannot land on the new chart. ```ts const binding = chart.bind(datafeed, { symbol: "AAPL", resolution: "1D", initialBars: 800, loadMoreThresholdBars: 100, onSymbolInfo: (info) => setHeader(info.name, info.currency), onLoading: (loading) => setSpinner(loading), onError: (e) => reportError(e), }); await binding.setSymbol("MSFT"); await binding.setResolution("60"); await binding.reload(); binding.dispose(); ``` ## Live Sources A live source is anything with a `subscribe(listener)` that returns an unsubscribe function. `chart.live(source)` folds whatever it emits into the main series. ```ts type LiveItem = | Tick // { time, price, size, side? } — folded into bars | Bar // replaces or appends, finer bars fold into coarser | { time: number; value: number } // for a value series | readonly [number, number]; // [time, value] ``` ### WebSocket ```ts import { websocketSource, type LiveItem } from "@ortex-charts/financial"; const source = websocketSource({ url: "wss://feed.example.com/stream", protocols: ["v2"], onOpen: (send) => send(JSON.stringify({ action: "subscribe", symbols: ["AAPL"] })), parse: (raw) => { const m = JSON.parse(String(raw)) as { ev: string; t: number; p: number; s: number }; return m.ev === "T" ? { time: m.t, price: m.p, size: m.s } : null; // null ignores the message }, onStatus: (s) => setConnectionState(s), // "connecting" | "open" | "closed" | "reconnecting" onError: (e) => console.warn(e), reconnect: true, minReconnectDelayMs: 500, maxReconnectDelayMs: 15000, heartbeat: JSON.stringify({ action: "ping" }), heartbeatMs: 15000, }); const stop = chart.live(source, { resolution: "1", onUpdate: (item, appended) => countTicks(appended) }); ``` The socket opens on the first subscriber and closes when the last one leaves, so subscribing the chart is enough to manage the connection. Reconnection is exponential with jitter. `parse` may return one item, an array of items, or `null` to ignore the message. `chart.connect(options)` is the same thing in one call when the chart is the only consumer; it returns a function that both unsubscribes and closes the socket. ### Polling and Manual Sources ```ts import { pollingSource, ManualSource, type LiveItem } from "@ortex-charts/financial"; const polled = pollingSource(async () => { const q = await fetch("/api/quote/AAPL").then((r) => r.json()); return { time: q.t, price: q.last, size: q.size }; }, 2000); const manual = new ManualSource(); chart.live(manual); manual.push({ time: Date.now(), price: 182.4, size: 100 }); ``` `ManualSource` is the escape hatch for transports the library does not know about: server-sent events, a shared worker, a message bus, a test fixture. _Live example: [live-stream](https://ortexcharts.com/examples/live-stream)_ ### How Folding Works `streamTo` — which is what `chart.live` calls — decides what to do from the shape of the item and the kind of the series: - A **tick** on a bar series extends the current bar or starts a new one at the chart resolution, in the chart time zone, honoring the [session anchor](https://ortexcharts.com/docs/time). - A **bar** on a bar series merges into the current bar of the chart resolution: the open stays, high and low extend, the close follows, and volume accumulates when the incoming bar is a new sub-bar rather than a restatement of the same one. This is what makes one-minute candles from a feed drive a 15-minute chart correctly. - A **value point** on a value series replaces or appends. - A **tick or bar** on a value series contributes its price or close. ## Adapter Specs Writing the datafeed above is 40 lines of glue for every feed. An **adapter spec** replaces it with data: a description of where history, live updates and symbol metadata live in your messages. `createAdapter` turns it into a `Datafeed` and live sources. Because a spec is plain data, it can live in a JSON file, be validated, be versioned, and be tested against a captured sample without running a chart. ```ts import { createAdapter, createFinancialChart } from "@ortex-charts/financial"; const spec = { name: "my-feed", variables: { token: process.env.FEED_TOKEN! }, symbol: { static: { timeZone: "America/New_York", session: "0930-1600", precision: 2, minMove: 0.01 } }, history: { url: "https://api.example.com/v1/{symbol}/bars?res={resolution}&from={from}&to={to}&key={token}", dates: { unit: "date" }, resolutions: { "1D": "daily", "1": "1min", "60": "hourly" }, records: { path: "bars", kind: "bar", time: { path: "d", unit: "date" }, fields: { open: "o", high: "h", low: "l", close: "c", volume: "vol" }, }, }, live: { url: "wss://stream.example.com?key={token}", subscribe: { action: "subscribe", symbols: ["{symbol}"] }, messages: [ { match: [{ path: "ev", equals: "T" }], records: { kind: "tick", time: { path: "t", unit: "ms" }, fields: { price: "p", size: "s" }, filter: [{ path: "sym", equals: "{symbol}" }], }, }, ], }, } as const; const adapter = createAdapter(spec); const chart = createFinancialChart(el, { timeZone: "America/New_York" }); chart.bind(adapter.datafeed, { symbol: "ACME", resolution: "1D" }); chart.live(adapter.live("ACME")); ``` ### The Vocabulary | Concept | What it is | |---|---| | Field path | `a.b[0].c` for objects, a column index such as `3` for array rows, `$` for the record itself. | | Field reference | A path plus `scale`, `offset`, `default` and `map`, for pence-to-pounds conversions and code lookups. | | Time unit | `ms`, `s`, `us`, `ns`, `iso`, `date`, `datetime`, `epochDays`, with a `timeZone` for the date units and an optional `floor` to a bar boundary. | | Records | An array at `path`, a single object, or parallel column arrays with `layout: "columns"`. | | Filter | Conditions applied to each record; `{symbol}` placeholders split a multiplexed feed. | | Message spec | Conditions on a whole message plus a records spec; a feed lists one per message type. | | Templates | `{symbol}`, `{exchange}`, `{ticker}`, `{resolution}`, `{from}`, `{to}`, `{countBack}`, your `variables`, and `{info.}` from symbol resolution. | Records come in three kinds: `bar` (needs `close`), `tick` (needs `price`) and `value` (needs `value`). A `value` record is how a non-price series arrives through the same machinery. ### Field References ```ts // A plain path { close: "c" } // A column index, for array rows like [t, o, h, l, c, v] { time: { path: 0, unit: "ms" }, open: 1, high: 2, low: 3, close: 4, volume: 5 } // Scaling: a feed that quotes UK stocks in pence { close: { path: "px", scale: 0.01 } } // A default when the field is missing { volume: { path: "v", default: 0 } } // A lookup table { value: { path: "rating", map: { BUY: 1, HOLD: 0, SELL: -1 } } } ``` ### Time Fields ```ts { time: { path: "t", unit: "ms" } } { time: { path: "d", unit: "date", timeZone: "America/New_York" } } { time: { path: "ts", unit: "iso" } } { time: { path: "t", unit: "s", floor: "1" } } // candles stamped with the trade time ``` `floor` is the fix for feeds that stamp a one-minute candle with the time of the last trade in it rather than with the start of the bar. ### Columnar Responses ```ts records: { layout: "columns", kind: "bar", time: { path: "t", unit: "s" }, fields: { open: "o", high: "h", low: "l", close: "c", volume: "v" }, } ``` A response of `{ t: [...], o: [...], h: [...], … }` needs nothing more than `layout`. ### Testing a Spec Without a Chart ```ts import { validateAdapterSpec, createAdapter, explainMapping } from "@ortex-charts/financial"; const problems = validateAdapterSpec(spec); // string[] with actionable messages if (problems.length) throw new Error(problems.join("; ")); const adapter = createAdapter(spec); console.log(adapter.explainHistory(capturedResponse)); // the records the chart would see console.log(adapter.explainLive(capturedMessage, "ACME")); // the same for a live message ``` `createAdapter` validates and throws an `AdapterError` listing every problem, so a broken spec fails at construction rather than as an empty chart. When a sample produces nothing, `explainMapping` says why. ### What the Adapter Handles - A reconnecting WebSocket with backoff and heartbeat, one socket per symbol, or one shared socket for a multiplexed feed, opened on the first subscriber and closed on the last. - History paging: more is requested when the user scrolls near the left edge, and the requests stop when the feed says there is nothing earlier. - Folding: ticks become bars at the chart resolution, finer bars fold into coarser ones, and candles stamped with trade times are floored. - Sorting and de-duplication of history rows. ### The Escape Hatches Some feeds need logic no declaration can express. When the spec is authored in TypeScript rather than JSON, `url`, `subscribe`, `unsubscribe` and `body` may be functions of the request context, `symbol.transform` may post-process the resolved info, and any field reference may carry a `transform`. A JSON spec cannot express these; that is the trade-off for the spec being data. ```ts const spec: AdapterSpec = { name: "venue-aware", live: { url: "wss://stream.example.com", subscribe: (ctx) => (ctx.exchange === "LSE" ? [{ sub: `${ctx.isin}GBP` }, { sub: `${ctx.isin}GBX` }] : { sub: ctx.ticker }), messages: [/* … */], }, }; ``` ### The ORTEX Preset `ortexAdapterSpec({ apiBase, wsBase })` returns a spec for ORTEX's own API, so the library is a drop-in on app.ortex.com. It is also the largest worked example of a spec, covering symbol resolution, daily history over HTTP, a live WebSocket with a per-venue subscribe handshake, and symbol search. ```ts import { createAdapter, ortexAdapterSpec } from "@ortex-charts/financial"; const adapter = createAdapter(ortexAdapterSpec({ apiBase: "https://app.ortex.com", wsBase: "wss://ws.ortex.com" })); ``` ## Your Own Series on Their Own Panes This is the case that sells the library, and it needs no adapter at all. A host series is just points, a pane and a scale. ```ts const chart = createFinancialChart(el, { timeZone: "America/New_York", resolution: "1D", volume: false, data: bars }); // Estimated short interest, on a pane of its own, as a percentage. const siPane = chart.chart.addPane({ heightRatio: 0.22 }); chart.chart .addSeries("area", { pane: siPane.id, title: "Estimated Short Interest, % of free float", color: "#7C5CE6", priceFormat: { type: "percent", precision: 2, minMove: 0.01 }, align: "forwardFill", }) .setData(siPoints); // Days to cover, another pane. const dtcPane = chart.chart.addPane({ heightRatio: 0.18 }); chart.chart .addSeries("line", { pane: dtcPane.id, title: "Days to Cover", color: "#F5A524", lineWidth: 1.5 }) .setData(dtcPoints); ``` _Live example: [short-interest](https://ortexcharts.com/examples/short-interest)_ Three details make this work with data that does not share the price timeline: - **`align: "forwardFill"`** carries the last known value forward, which is what a daily statistic on an intraday chart needs. `exact`, `nearest` and `bucket` are the other modes; see [Concepts](https://ortexcharts.com/docs/concepts). - **`priceFormat`** is per series, so a percentage pane and a price pane format independently. - **`priceScaleId`** puts a series on the left scale, or on an id of its own that is never drawn, which gives it independent autoscaling without spending axis width. To overlay rather than separate, drop the pane and choose a scale: ```ts chart.chart.addSeries("line", { title: "Utilization", priceScaleId: "left", color: "#34D399", }).setData(utilizationPoints); chart.chart.priceScale("left").applyOptions({ visible: true, mode: "normal" }); ``` ### Registering Them in the Toolbar When you are using [the shell](https://ortexcharts.com/docs/shell), wrap each of these in an `OverlayDef` and the user gets them in the Data menu, with the checked state and the layout handled for you: ```ts const overlays: OverlayDef[] = [ { id: "si", label: "Short interest", group: "Securities lending", defaultOn: true, add: async ({ chart, symbol }) => { const rows = await api.shortInterest(symbol); const s = chart.addSeries("line", { title: "Short interest", priceScaleId: "left", align: "forwardFill" }); s.setData(rows); return () => chart.removeSeries(s.id); }, }, ]; ``` `add` may be async, receives the symbol to fetch for, and returns the function that removes the overlay again. It is called again after every symbol or resolution change, with the previous disposer run first, so the host never has to watch the chart itself. ## Choosing an Approach | Your situation | Use | |---|---| | The data is already in the page. | `setData`. | | One symbol, no history paging, a push feed. | `setData` plus `chart.live(source)`. | | Symbol switching, resolution switching, lazy history. | A `Datafeed` and `chart.bind`. | | The same as above, but you would rather write JSON than glue. | An adapter spec. | | Non-price series alongside the price. | Host series on their own panes, or `OverlayDef` in the shell. | | A transport the library does not know. | `ManualSource` and push into it. | --- # Colors, fonts and formatting > Theme tokens, per-series colors, number and date formatting, and localization. Section: Customization. Source: https://ortexcharts.com/docs/customization ## Three Levels of Color Colors are resolved in one direction, from general to specific, and knowing the order saves a lot of guessing. 1. **The theme** is a flat token object. Every color the engine paints, when nothing more specific is set, comes from here. 2. **Series options** override the theme for one series: `color`, `upColor`, `topColor` and so on. 3. **Per-value coloring** overrides both, where the type supports it: a histogram in `colorMode: "sign"`, candles in up and down colors, a baseline series above and below its base. The rule to internalize is that **a series left with unset colors follows the theme**. That is what makes `chart.setTheme("light")` repaint an entire chart, indicators included, without touching a single series. Setting explicit colors opts that series out. _Live example: [theme-tokens](https://ortexcharts.com/examples/theme-tokens)_ Throughout this page `chart` is the core `Chart` — `setTheme`, `applyOptions`, `priceScale` and `theme` live there. If you started from `createFinancialChart` or `createChartShell`, reach it as `fc.chart` or `shell.chart.chart`. ## Theme Tokens `darkTheme` and `lightTheme` are exported objects, and a theme is just such an object. Every token is required, so start from one of the built-ins rather than from an empty object. | Token | Type | What it colors | |---|---|---| | `name` | `string` | An identifier for your own bookkeeping. | | `background` | color | The plot area of every pane. | | `axisBackground` | color | The price and time axis strips. | | `axisBorder` | color | The line between the axes and the plot. | | `grid` | color | Grid lines. | | `paneSeparator` | color | The divider between panes. | | `paneSeparatorHover` | color | The same divider while it is being hovered or dragged. | | `text` | color | Axis labels and general text. | | `textMuted` | color | Secondary text. | | `fontFamily` | CSS font stack | Every string the canvas draws. | | `fontSize` | number | Base size in pixels; labels derive from it. | | `crosshair` | color | The crosshair lines. | | `crosshairLabelBackground` | color | The axis tags at the crosshair. | | `crosshairLabelText` | color | Text inside those tags. | | `up` | color | Rising candle bodies and bars. | | `down` | color | Falling candle bodies and bars. | | `upWick` | color | Rising candle wicks. | | `downWick` | color | Falling candle wicks. | | `volumeUp` | color | Volume columns on a rising bar. | | `volumeDown` | color | Volume columns on a falling bar. | | `line` | color | A line or area series with no color of its own. | | `areaTop` | color | Top of the area gradient. | | `areaBottom` | color | Bottom of the area gradient. | | `palette` | `string[]` | Handed to successive overlays and indicator outputs in order. | | `watermark` | color | The watermark text. | | `lastValueText` | color | Text inside the last-value axis label. | | `legendBackground` | color | Behind the legend; transparent in both built-in themes. | | `legendText` | color | Legend text. | | `selection` | color | Selected drawings and selection rectangles. | | `tooltipBackground` | color | Hover tooltips. | | `tooltipText` | color | Tooltip text. | | `tooltipBorder` | color | Tooltip border. | Colors accept `#rgb`, `#rrggbb`, `#rrggbbaa`, `rgb()` and `rgba()`. ### The Palette `palette` is the one token that is not a single color, and it does more work than it looks. Every series added without an explicit `color` takes the next entry, cycling when it runs out. So does every indicator output whose definition asks for `theme.palette[n]`. Setting a brand palette therefore recolors indicators and host overlays at once: ```ts import { darkTheme } from "@ortex-charts/financial"; chart.setTheme({ ...darkTheme, name: "brand-dark", palette: ["#FF7A00", "#7C5CE6", "#00B8D9", "#36B37E", "#FFAB00", "#FF5630"], }); ``` ### Colour-Blind Safe Themes About one man in twelve cannot separate the red and green a candle chart has used since the 1980s. Two themes ship for that, and the toolbar has a switch: ```ts import { createFinancialChart } from "@ortex-charts/financial"; createFinancialChart(el, { theme: "dark-colorblind", data: bars }); ``` | Theme | Up | Down | |---|---|---| | `dark-colorblind` | `#56B4E9` | `#D55E00` | | `light-colorblind` | `#0072B2` | `#E69F00` | Both pairs come from the Okabe-Ito palette, which is chosen to stay apart under deuteranopia, protanopia and tritanopia, and both differ in **luminance** as well as hue, so direction survives a greyscale print, a monochrome display and total colour blindness. Overlay series use the rest of that palette, exported as `OKABE_ITO`. Only the colours that carry meaning change: direction, volume, the overlay palette and the single-series line. The background, grid and text keep the theme they came from, so a colour-blind chart still looks like the rest of your application. Your own theme can have the same treatment: ```ts import { colorBlindSafe } from "@ortex-charts/core"; const acmeSafe = colorBlindSafe(acmeTheme); // name becomes "acme-colorblind" ``` In [the shell](https://ortexcharts.com/docs/shell), the `colorBlind` toolbar item is a switch beside the light and dark one, and the two are independent: turning the palette on keeps you in dark, and switching to light keeps the palette. ### A Complete Custom Theme ```ts import { darkTheme, type Theme } from "@ortex-charts/financial"; export const brandDark: Theme = { ...darkTheme, name: "brand-dark", background: "#0B0E14", axisBackground: "#0B0E14", axisBorder: "#1E2531", grid: "#141A23", text: "#8A96A6", textMuted: "#5A6675", fontFamily: '"Inter", -apple-system, "Segoe UI", Roboto, sans-serif', fontSize: 11, up: "#00C08B", down: "#FF4D5E", upWick: "#00C08B", downWick: "#FF4D5E", volumeUp: "rgba(0, 192, 139, 0.4)", volumeDown: "rgba(255, 77, 94, 0.4)", line: "#4C8DFF", areaTop: "rgba(76, 141, 255, 0.28)", areaBottom: "rgba(76, 141, 255, 0.02)", palette: ["#FFAB00", "#A78BFA", "#F472B6", "#34D399", "#FB923C", "#60A5FA"], }; createFinancialChart(el, { theme: brandDark, data: bars }); ``` ### Switching at Runtime ```ts chart.setTheme("light"); chart.setTheme(brandDark); chart.theme(); // the resolved Theme object ``` A theme change is a repaint, never a rebuild. Series, indicators, drawings, primitives, the visible range and the crosshair position all survive it. Indicator colors are functions of the theme, so they follow too. ### Color Helpers ```ts import { parseColor, withAlpha, contrastText } from "@ortex-charts/financial"; parseColor("#22C08A"); // [34, 192, 138, 1] withAlpha(theme.up, 0.15); // "rgba(34, 192, 138, 0.15)" contrastText("#22C08A"); // "#111418" or "#FFFFFF", whichever reads ``` `contrastText` is what the engine uses for text on colored badges, and it is the right tool for a host drawing its own labels on a theme color. ## Chart Chrome Everything below is a chart option, applied at creation or with `applyOptions`, which is deeply partial so you write only the keys you are changing. ### Grid, Crosshair and Watermark ```ts chart.applyOptions({ grid: { vertical: true, horizontal: true }, crosshair: { mode: "magnet", // "magnet" | "free" | "hidden" vertLine: { visible: true, width: 1, style: "dashed", labelVisible: true, color: "#6F7D91" }, horzLine: { visible: true, width: 1, style: "dashed", labelVisible: true }, }, watermark: { text: "AAPL", visible: true, fontSize: 44 }, }); ``` `magnet` snaps the crosshair to the nearest bar value; `free` follows the pointer exactly; `hidden` removes it while leaving hover events intact. ### The Legend ```ts chart.applyOptions({ legend: { visible: true, showValues: true, // OHLC or value at the crosshair, not just titles logo: "https://logos.example.com/aapl.png", paneControls: true, // the collapse toggle on pane legends }, }); ``` `logo` is drawn before the first series title and is set automatically by the [datafeed binding](https://ortexcharts.com/docs/data) from `SymbolInfo.logo`. A missing image falls back to the color dot silently. ### Price Scales Each scale is configured independently, per pane. ```ts chart.priceScale("right").applyOptions({ mode: "logarithmic", // "normal" | "logarithmic" | "percentage" | "indexedTo100" autoScale: true, visible: true, scaleMargins: { top: 0.1, bottom: 0.1 }, borderVisible: true, ticksVisible: true, minimumWidth: 64, invert: false, }); chart.priceScale("right", volumePane.id).applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } }); ``` `scaleMargins` are fractions of the pane height left empty above and below the data, which is how a volume pane keeps its columns in the lower fifth. `invert` flips the axis, which is occasionally right for a yield or a rate. ### The Time Scale Covered on [Time, sessions and resolutions](https://ortexcharts.com/docs/time); the options are `barSpacing`, `minBarSpacing`, `maxBarSpacing`, `rightOffsetPx`, `fixLeftEdge`, `fixRightEdge`, `followRealtime`, `borderVisible` and `visible`. ### Interaction and Layout ```ts chart.applyOptions({ handleScroll: true, handleScale: true, kineticScroll: true, // momentum after a fast drag or flick autoSize: true, // follow the container with a ResizeObserver width: 900, // only when autoSize is false height: 480, timeAxisHeight: 26, priceLabelGap: 36, // minimum pixels between price labels timeLabelGap: 72, // minimum pixels between time labels }); ``` Raising `priceLabelGap` and `timeLabelGap` thins the labels, which is the fix for a small chart whose axes look crowded. Lowering them packs more in. ## Number Formatting Formatting is per series, through `priceFormat`. ```ts series.applyOptions({ priceFormat: { type: "price", precision: 2, minMove: 0.01 } }); series.applyOptions({ priceFormat: { type: "volume", precision: 0, minMove: 1 } }); series.applyOptions({ priceFormat: { type: "percent", precision: 2, minMove: 0.01 } }); series.applyOptions({ priceFormat: { type: "custom", precision: 2, minMove: 0.01, formatter: (v) => `${v >= 0 ? "+" : ""}${v.toFixed(2)} bp`, }, }); ``` | `type` | Renders | Used for | |---|---|---| | `price` | `1,234.56` with `precision` decimals | Prices and most values. | | `volume` | `1.23M`, `4.56B` | Volume and share counts. | | `percent` | `12.34%` | Ratios already expressed in percent. | | `custom` | Whatever `formatter` returns | Anything else: basis points, currencies, ratings. | `minMove` is the smallest increment the instrument trades in. It matters because the axis adds decimals when the visible range is narrow, and `minMove` is the floor it will not go below. A `minMove` of `0.01` on a stock and `0.0001` on a currency pair is the usual pairing with `precision` 2 and 4. `formatter` wins over everything else, and it is used for the axis labels, the crosshair tags, the last-value label and the legend, so one function covers them all. The formatting helpers are exported for host-drawn labels: ```ts import { formatPrice, formatPercent, formatCompact, formatVolume, precisionFromMinMove } from "@ortex-charts/math"; import { formatValue } from "@ortex-charts/financial"; formatPrice(1234.5678, { precision: 2 }); // "1,234.57" formatPercent(0.1234, { precision: 2 }); // percent formatting formatCompact(1234567, { precision: 2 }); // "1.23M" formatVolume(1234567); // volume shorthand precisionFromMinMove(0.0001); // 4 formatValue(1234.5, series.options.priceFormat); // exactly what the chart would print ``` ## Dates, Locales and Language Dates and numbers go through `Intl`, so the chart's `locale` option controls month names, date ordering and grouping separators. ```ts createFinancialChart(el, { locale: "de-DE", timeZone: "Europe/Berlin" }); ``` There is a limit worth stating plainly: **the interface strings are English only.** The toolbar labels, menu entries, dialog titles and tooltips in `@ortex-charts/ui` are not translated and there is no string table to supply. Dates and numbers localize; words do not. A host that needs another language today replaces the chrome with its own controls and drives the chart through the shell methods, which are all public. ## Per-Series Overrides at a Glance | Series type | The options that override the theme | |---|---| | `candlestick` | `upColor`, `downColor`, `wickUpColor`, `wickDownColor`, `borderVisible`, `hollowUp`, `bodyWidthRatio` | | `bar` | `upColor`, `downColor`, `thin`, `openVisible` | | `line` | `color`, `lineWidth`, `lineStyle`, `curve`, `pointMarkers`, `pointRadius` | | `area` | `color`, `topColor`, `bottomColor`, `baseValue`, `curve` | | `baseline` | `baseValue`, `topLineColor`, `bottomLineColor`, `topFillColor`, `bottomFillColor` | | `histogram` | `color`, `upColor`, `downColor`, `colorMode`, `base`, `widthRatio` | See [Series types](https://ortexcharts.com/docs/series) for what each one does. ## Styling the Chrome The toolbar and drawing rail in `@ortex-charts/ui` are ordinary DOM. They are styled with CSS custom properties derived from the chart theme, injected once per document, so a theme switch restyles them along with the canvas. ```ts import { applyThemeVars, UI_CSS, ensureStyles } from "@ortex-charts/ui"; ``` `ensureStyles(document)` injects the stylesheet if it is not already there, and `applyThemeVars(element, theme)` writes the custom properties onto an element. The shell does both for you; the exports exist so a host can style its own controls to match, or override the properties in its own stylesheet. Class names are prefixed `oc-ui-`, which makes them stable targets for a host stylesheet without fighting a CSS-in-JS runtime. ## A Practical Recipe: Two Themes and a Toggle ```ts import { createFinancialChart, darkTheme, lightTheme, type Theme } from "@ortex-charts/financial"; const brand = (base: Theme, name: string): Theme => ({ ...base, name, fontFamily: '"Inter", system-ui, sans-serif', palette: ["#FF7A00", "#7C5CE6", "#00B8D9", "#36B37E", "#FFAB00", "#FF5630"], }); const themes = { dark: brand(darkTheme, "brand-dark"), light: brand(lightTheme, "brand-light") }; const chart = createFinancialChart(el, { theme: themes.dark, data: bars }); matchMedia("(prefers-color-scheme: light)").addEventListener("change", (e) => { chart.chart.setTheme(e.matches ? themes.light : themes.dark); }); ``` Deriving both themes from one function keeps the pair consistent, and the switch is a repaint, so it can be wired to a media query without any care about timing. --- # The toolbar and your buttons > Hide what you do not want, add your own buttons and menus, and keep it accessible. Section: Customization. Source: https://ortexcharts.com/docs/toolbar ## The Toolbar Is a Set of Switches `createChartShell` draws a toolbar above the chart and a drawing rail beside it. Every item is independently switchable, and everything an item does is also a method on the returned shell, so a host can hide the whole toolbar and still drive the chart from its own controls. ```ts import { createChartShell } from "@ortex-charts/ui"; const shell = createChartShell(el, { symbol: "AAPL", datafeed, toolbar: { theme: false, screenshot: false, download: true }, resolutions: ["5", "15", "60", "1D", "1W"], indicators: ["sma", "ema", "rsi", "macd", "bb"], drawingTools: ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"], }); ``` _Live example: [custom-toolbar](https://ortexcharts.com/examples/custom-toolbar)_ ## The Built-In Items `ToolbarItem` is the union of the switch names. Everything is on by default except `download`. | Item | What it is | Shell method | |---|---|---| | `symbol` | Symbol button opening a search popover; Enter loads typed text | `setSymbol` | | `resolution` | Segmented buttons labeled `1m 5m 15m 1h D W M` | `setResolution` | | `seriesType` | Candles, bars, line, area, baseline, plus Heikin-Ashi and hollow toggles | `setSeriesType` | | `indicators` | Searchable list of the registry, plus the active list with settings, hide and remove | `addIndicator`, `indicators` | | `overlays` | Host data overlays, as a menu or the "Add data to chart" modal | `setOverlay`, `overlays`, `openDataPicker` | | `scale` | Auto, logarithmic and percent scale modes | `setScaleMode` | | `fit` | Fit the whole history into view | — | | `realtime` | Scroll to the latest bar | — | | `replay` | Bar replay; **off by default** | `replay`, `startReplay`, `stopReplay` | | `screenshot` | PNG download, or `onScreenshot` | — | | `download` | CSV of the visible series; **off by default** | `exportCsv` | | `theme` | Light and dark toggle | `setTheme` | | `colorBlind` | Colour-blind safe palette, independent of light and dark | `setTheme` | | `fullscreen` | Full screen and back | `toggleFullscreen` | ```ts toolbar: { replay: true, download: true, theme: false } ``` Switching everything off leaves a bare chart that is still fully driven by the shell methods, which is the supported way to build a completely custom interface without giving up symbol binding, layouts and overlays. `drawings: false` removes the drawing rail entirely. `indicators: false` removes the indicator menu. `indicators: ["sma", "ema"]` limits which ones the menu offers without affecting what `addIndicator` can do from code. ## Configuration as Data Every switch is plain JSON, so a company's chart configuration can live in a checked-in file with only the callbacks attached in code. ```json { "resolutions": ["5", "15", "60", "1D", "1W"], "toolbar": { "theme": true, "colorBlind": true, "screenshot": false, "download": true }, "indicators": ["sma", "ema", "rsi", "macd", "bb"], "drawingTools": ["trendline", "ray", "horizontalLine", "rectangle", "fibRetracement"], "dataPicker": "modal", "compare": true } ``` ```ts import config from "./chart-config.json"; createChartShell(el, { ...config, datafeed, overlays, onLayoutChange: (layout) => api.saveChartLayout("stock-page", layout), }); ``` ## Your Own Buttons `ToolbarAction` adds a button or a dropdown. It is data plus callbacks, so it fits the same configuration-file pattern. ```ts interface ToolbarAction { id: string; label?: string; // icon-only when omitted icon?: IconName | string; // a built-in name, or your own markup title?: string; // tooltip and accessible name; defaults to label placement?: "start" | "middle" | "end"; onClick?: (shell: ChartShell, event: MouseEvent) => void; menu?: Array<{ label: string; icon?: IconName; hint?: string; onSelect: (shell: ChartShell) => void }>; active?: boolean; // render as pressed disabled?: boolean; } ``` ```ts createChartShell(el, { symbol: "AAPL", datafeed, actions: [ { id: "api", label: "Download with API", icon: "api", placement: "end", onClick: (shell) => openApiDialog(shell.symbol(), shell.resolution()), }, { id: "share", icon: "share", title: "Share", menu: [ { label: "Copy link", onSelect: (s) => navigator.clipboard.writeText(linkFor(s.getLayout())) }, { label: "Post to Slack", icon: "camera", onSelect: (s) => slack(s.chart.chart.takeScreenshot()) }, ], }, ], }); ``` `placement` decides where the button lands: `start` puts it with the symbol and resolutions, `middle` with the chart-type menus, and `end` at the right, which is the default. At runtime, `shell.addAction(action)` adds one and returns a function that removes it again, which is how a button appears only in a particular application state. ```ts const removeAlertButton = shell.addAction({ id: "add-alert", icon: "plus", title: "Add alert at the last close", onClick: (s) => { const last = s.chart.main.lastIndex(); const v = s.chart.main.valueAt(last); if (v && "close" in v) s.chart.alerts().add({ price: v.close, label: "New alert" }); }, }); removeAlertButton(); ``` ## Icons `ICONS` is a record of 52 inline SVG path fragments and `icon(name)` builds an `` element from one. They are 20-by-20, single stroke, and use `currentColor`, so they inherit the button color and follow the theme. ```ts import { ICONS, icon } from "@ortex-charts/ui"; Object.keys(ICONS); // every available IconName const el = icon("magnet"); ``` The names, grouped by what they are for: - **Series types:** `candles`, `bars`, `line`, `area`, `baseline`, `heikinAshi`, `hollow`. - **Tools:** `cursor`, `crosshair`, `trendline`, `ray`, `horizontalLine`, `verticalLine`, `rectangle`, `parallelChannel`, `fibRetracement`, `measure`, `text`, `magnet`. - **Editing:** `undo`, `redo`, `trash`, `clear`, `eye`, `eyeOff`, `settings`, `lock`, `plus`, `minus`, `check`, `close`, `chevron`. - **Navigation and scale:** `fit`, `realtime`, `log`, `percent`, `auto`, `search`. - **Replay:** `replay`, `play`, `pause`, `stepForward`, `stepBack`, `stop`. - **Actions:** `camera`, `download`, `share`, `link`, `api`, `indicator`, `sun`, `moon`. Passing your own markup instead of a name works and is the right answer for a brand icon: ```ts { id: "brand", icon: '' } ``` Use a 20-by-20 or 24-by-24 coordinate space and `currentColor` strokes so the icon matches the built-ins at every size and in both themes. ## Menus and Dialogs The same primitives the built-in toolbar uses are exported, so a host action can open a menu or a form that looks exactly like the rest of the chrome rather than like a bolted-on `window.prompt`. ```ts import { openMenu, openDialog, openSymbolSearch } from "@ortex-charts/ui"; openMenu(shell.root, anchorButton, (menu) => { menu.head("Export"); menu.item({ label: "CSV", icon: "download", onSelect: () => save(shell.exportCsv()) }); menu.item({ label: "PNG", icon: "camera", onSelect: () => savePng(shell.chart.chart.takeScreenshot()) }); menu.sep(); menu.item({ label: "Include hidden series", checked: includeHidden, onSelect: () => { includeHidden = !includeHidden; return false; } }); }); ``` Returning `false` from `onSelect` keeps the menu open, which is what a toggle wants. ```ts openDialog(shell.root, { title: "Alert settings", fields: [ { key: "price", label: "Price", type: "number", value: 182.5, step: 0.01 }, { key: "repeat", label: "Repeat", type: "boolean", value: false }, { key: "channel", label: "Notify by", type: "select", value: "email", options: ["email", "push", "none"] }, ], submitLabel: "Create", onSubmit: (values) => createAlert(values), }); ``` `openDialog` returns a function that closes it. Fields are `number`, `select` or `boolean`; this is the same generator the indicator settings dialog uses, which is why an indicator you [register yourself](https://ortexcharts.com/docs/indicators) gets a proper form without writing one. ## Keyboard and Accessibility What is implemented: - **The chart is focusable.** Arrow keys scroll, with Shift for a larger step; `+` and `-` zoom about the center; Home resets the time scale. - **Drawings respond to keys** on a focused chart: Escape cancels a tool or clears the selection, Delete and Backspace remove the selected drawing, and Ctrl or Cmd with Z and Y undo and redo. See [Drawing tools](https://ortexcharts.com/docs/drawings). - **Toolbar buttons carry `aria-pressed`** so a screen reader announces the active chart type, scale mode and tool. - **Menus are `role="menu"`** with `aria-haspopup` and `aria-expanded` on the button that opens them, arrow-key navigation between items, `aria-checked` on toggles, Escape to close, and focus returned to the anchor. - **Dialogs are `role="dialog"` with `aria-modal`**, an accessible name from the title, a focus trap, Enter to submit and Escape to cancel. - **Icon-only buttons carry a `title` and an accessible name**, which is why `ToolbarAction` falls back from `title` to `label`. - **Reduced motion** is respected in the chrome CSS. What is not implemented, stated plainly: - **There is no screen-reader description of the data.** The canvas is not annotated, and there is no textual summary of the series, the visible range or the values at the crosshair. - **There is no focusable data table** as an alternative representation, which is the approach the Highcharts accessibility module takes. - **There is no high-contrast theme** shipped, though one is a [theme object](https://ortexcharts.com/docs/customization) away. - **Keyboard support does not extend to creating or editing drawings**, only to canceling and deleting them. If accessibility conformance is a procurement requirement for you, these are the gaps to weigh, and the CSV export plus your own table is the honest interim answer. ## Mobile Below 640 pixels the toolbar drops its labels, the resolution strip scrolls horizontally, and the drawing rail becomes a horizontal bar under the chart. Coarse pointers get 32-pixel targets. Pinch zoom, drag panning and long-press crosshair are in the engine rather than in the chrome, so they work on a bare `createFinancialChart` too. Full screen uses the Fullscreen API on the shell root, and falls back to a fixed-position layer where the API is missing, which is the case on iOS Safari. Escape leaves either. ## Driving the Chart Without the Toolbar Every method behind the buttons is public, so switching the toolbar off is not a loss of functionality. ```ts const BARE = { symbol: false, resolution: false, seriesType: false, indicators: false, overlays: false, scale: false, fit: false, realtime: false, screenshot: false, theme: false, fullscreen: false, } as const; const shell = createChartShell(el, { symbol: "AAPL", datafeed, toolbar: BARE, drawings: false }); await shell.setSymbol("MSFT"); await shell.setResolution("60"); shell.setSeriesType("line"); shell.setScaleMode("logarithmic"); shell.addIndicator("rsi", { length: 21 }); await shell.setOverlay("si", true); shell.setTheme("light"); await shell.toggleFullscreen(); const csv = shell.exportCsv(); const layout = shell.getLayout(); ``` See [the shell and layouts](https://ortexcharts.com/docs/shell) for the full surface, including overlays, layouts and per-series placement. --- # The shell and layouts > The complete chart application: symbol search, panes, layouts and stored drawings. Section: Customization. Source: https://ortexcharts.com/docs/shell ## One Call for the Whole Application `createChartShell` wraps a financial chart in the chrome a user expects: a toolbar, a drawing rail, symbol search, an indicator menu, a data picker, scale controls, screenshot, theme and full screen. It is plain DOM with no framework dependency, styled through CSS custom properties derived from the chart theme. ```ts import { createChartShell } from "@ortex-charts/ui"; const shell = createChartShell(container, { symbol: "AAPL", datafeed, // any Datafeed; search comes from searchSymbols overlays: myOverlays, // host-defined data series layout: savedLayout, // what the user chose last time, or null onLayoutChange: (layout) => api.saveChartLayout("stock-page", layout), drawingsStore: myDrawingsStore, // drawings that follow the symbol }); ``` In React it is `` from [`@ortex-charts/react`](https://ortexcharts.com/docs/react). _Live example: [chart-shell](https://ortexcharts.com/examples/chart-shell)_ `ChartShellOptions` extends `FinancialChartOptions`, so every chart option — theme, time zone, resolution, session, watermark, crosshair, branding, `data` — is accepted in the same object. ## The Shell Handle ```ts interface ChartShell { readonly root: HTMLElement; readonly chart: FinancialChart; // .chart.chart is the core Chart readonly drawings: DrawingsPrimitive | null; readonly binding: DatafeedBinding | null; symbol(): string; setSymbol(symbol: string): Promise; resolution(): string; setResolution(resolution: string): Promise; setSeriesType(type: MainSeriesType): void; setScaleMode(mode: ScaleMode): void; addIndicator(id: string, inputs?: Record): IndicatorInstance; indicators(): readonly IndicatorInstance[]; overlays(): Array<{ id: string; symbol: string; key: string; label: string; on: boolean }>; setOverlay(id: string, on: boolean, symbol?: string): Promise; openDataPicker(): void; placeSeries(seriesId: string, placement: Partial): void; placementOf(seriesId: string): SeriesPlacement | null; addAction(action: ToolbarAction): () => void; exportCsv(): string; getLayout(): ChartLayout; setLayout(layout: ChartLayout | null): Promise; setTheme(theme: Theme | "dark" | "light"): void; toggleFullscreen(): Promise; isFullscreen(): boolean; replay(): ReplayController; startReplay(index?: number): void; stopReplay(): void; remove(): void; } ``` Nothing is hidden behind the toolbar. `shell.chart` is the `FinancialChart`, `shell.chart.chart` is the core `Chart`, and `shell.drawings` is the drawing primitive, so anything on [Series types](https://ortexcharts.com/docs/series), [Indicators](https://ortexcharts.com/docs/indicators), [Drawing tools](https://ortexcharts.com/docs/drawings) or [Order flow](https://ortexcharts.com/docs/order-flow) applies unchanged. ## Layouts: The Chart Never Persists, the Host Does A **layout** is the user's choice of *how* the chart looks, independent of *which* symbol is shown. It is plain JSON with a version. ```ts interface ChartLayout { version: 1; seriesType: "candlestick" | "bar" | "line" | "area" | "baseline"; scaleMode: "normal" | "logarithmic" | "percentage" | "indexedTo100"; resolution?: string; indicators: Array<{ id: string; inputs?: Record; visible?: boolean }>; overlays: string[]; // ids that are on; `id@SYMBOL` for a compared symbol scales?: Record; drawings?: Drawing[]; // only with layoutIncludesDrawings } ``` ```ts const layout = shell.getLayout(); await shell.setLayout(layout); // null resets to the shell defaults, including defaultOn overlays ``` `onLayoutChange` fires at most once per frame after any user action that changes the layout: chart type, scale mode, resolution, indicator added, removed, reconfigured or hidden, overlay toggled, series moved to another scale or pane, and drawings when they are included. The shell never touches storage. Where the layout lives — per user, per page kind, per symbol — is a host decision. The usual arrangement is one row per user and page kind: ```ts const saved = await api.get(`/chart-layout/stock/`); const shell = createChartShell(el, { symbol, datafeed, overlays, layout: saved, onLayoutChange: (l) => api.put(`/chart-layout/stock/`, l), }); ``` Unknown overlay and indicator ids in a stored layout are ignored rather than throwing, so renaming or retiring an overlay does not break every saved layout in your database. ## Overlays Are Host-Defined An overlay is a named thing your application knows how to put on the chart. The shell shows the label and group in the Data menu, keeps the check marks in sync, stores only the id in the layout, and calls your disposer when it is switched off. ```ts import type { OverlayDef } from "@ortex-charts/ui"; const overlays: OverlayDef[] = [ { id: "si", label: "Short interest", group: "Securities lending", description: "Estimated short interest as a percentage of free float", defaultOn: true, add: async ({ chart, symbol, resolution }) => { const rows = await api.shortInterest(symbol, resolution); const s = chart.addSeries("line", { title: "Short interest", priceScaleId: "left", align: "forwardFill", priceFormat: { type: "percent", precision: 2, minMove: 0.01 }, }); s.setData(rows); return () => chart.removeSeries(s.id); }, }, ]; ``` `add` receives `{ chart, fc, shell, symbol, isCurrentSymbol, resolution }`, may be asynchronous, and returns the function that removes the overlay again. It is called again after **every symbol or resolution change**, with the previous disposer run first, so the host fetches for `ctx.symbol` inside `add` and never has to watch the chart itself. `otherSymbolOnly: true` marks an overlay that only makes sense for a compared symbol and hides it for the chart's own. ## Comparing Symbols When a datafeed is bound, the shell adds a built-in **Price** overlay for other symbols: the closes of another instrument as a line, with the scale switched to percent so the two are comparable. Comparing AAPL with MSFT therefore needs no host code beyond `searchSymbols`. Overlays for another symbol are stored as `id@SYMBOL` in the layout, and `overlayKey` and `parseOverlayKey` are exported for hosts that need to read or build those keys. ```ts await shell.setOverlay("price", true, "MSFT"); shell.overlays(); // [{ id, symbol, key, label, on }, …] including compared symbols ``` Set `compare: false` to remove the feature. ## The "Add Data to Chart" Modal With more than eight overlays, or whenever symbol search is available, the Data button opens a modal instead of a dropdown. `dataPicker: "menu" | "modal" | "auto"` overrides the decision. The modal has a series search, a category list with counts on the left, grouped rows that toggle on click, and a symbol search that points the picker at another instrument so its series can be compared. `shell.openDataPicker()` opens it from your own control, and `openDataPicker` is exported from `@ortex-charts/ui` for hosts that want the same component somewhere else. ## Moving a Series to Another Scale or Pane Every legend row carries a `⋯` button. For an overlay or an indicator output it offers **Right scale**, **Left scale**, **Own scale** (a hidden scale, so the series autoscales alone), **Main pane**, **New pane**, **Hide** and **Remove**, plus **Collapse pane**, **Expand pane**, **Maximise pane** and **Restore panes** for the pane the series sits on. The choice is stored in `layout.scales`, keyed by series title, so it survives an overlay being re-added after a symbol change and comes back with the user's saved layout. ```ts shell.placeSeries(series.id, { scale: "own", pane: "separate" }); shell.placementOf(series.id); // { scale, pane, visible } or null ``` The underlying core methods are `chart.setPaneCollapsed`, `chart.isPaneCollapsed`, `chart.maximizePane`, `chart.restorePanes`, `chart.maximizedPane` and `chart.subscribePaneChange`, and `chart.subscribeLegendMenu` is the hook a host uses to build its own menu on that button. A collapsed pane is a 24-pixel legend strip. Its series stay attached and keep updating, so collapsing is a display state rather than a teardown. ## Drawings That Follow the Symbol The most-requested behavior in this category — a trend line drawn on AAPL being there on every chart of AAPL — is a storage question, and the host owns storage. Give the shell a `drawingsStore` and it does the rest. ```ts import { localStorageDrawingsStore } from "@ortex-charts/ui"; createChartShell(el, { symbol: "AAPL", datafeed, drawingsStore: localStorageDrawingsStore() }); // or your own, server-backed: createChartShell(el, { symbol: "AAPL", datafeed, drawingsStore: { load: (symbol) => api.get(`/drawings/${symbol}`), save: (symbol, drawings) => api.put(`/drawings/${symbol}`, drawings), }, }); ``` Both methods may be asynchronous. Drawings are saved whenever they change and on every symbol switch, and loaded for the new symbol. With a store in use, leave `layoutIncludesDrawings` off: a layout then describes the chart and drawings describe the symbol, which is the separation users expect. Turn `layoutIncludesDrawings` on instead only when the page is inherently single-symbol and you want one blob rather than two. ## Symbol Logos `SymbolInfo.logo` — a URL or a data URI — is shown in the symbol button and before the main series in the legend. A missing or failing image falls back to the color dot silently. The [datafeed](https://ortexcharts.com/docs/data) supplies it from `resolveSymbol` or `searchSymbols`, and an [adapter spec](https://ortexcharts.com/docs/data) maps it with `symbol.logo`, which accepts a template such as `https://logos.example.com/{symbol}.png` or `{info.logo_url}`. ## CSV Export The `download` toolbar item is off by default. Turning it on exports every visible series on the main timeline as CSV. ```ts createChartShell(el, { symbol: "AAPL", datafeed, toolbar: { download: true }, download: { fileName: "aapl-daily.csv", onDownload: (csv, shell) => api.saveExport(shell.symbol(), csv), // intercept it }, }); const csv = shell.exportCsv(); // the same text, on demand ``` With `onDownload` set, the browser does not save a file; the text goes to you instead. ## Screenshots ```ts createChartShell(el, { screenshotFileName: "chart.png", onScreenshot: (canvas) => canvas.toBlob((blob) => uploadToSlack(blob!)), }); const canvas = shell.chart.chart.takeScreenshot(); // from code, at any time ``` `takeScreenshot` composes every pane canvas into one and is on the core chart, so it works without the shell. ## Full Screen `toggleFullscreen()` uses the Fullscreen API on the shell root and falls back to a fixed-position layer where the API is unavailable, which is the case on iOS Safari. Escape leaves either. `isFullscreen()` reports the current state, and the toolbar button reflects it. ## Events ```ts createChartShell(el, { onLayoutChange: (layout) => save(layout), onSymbolChange: (symbol, info) => setPageTitle(symbol, info?.name), onResolutionChange: (resolution) => track("resolution", resolution), onThemeChange: (theme) => persistThemePreference(theme), onScreenshot: (canvas) => upload(canvas), onError: (error) => report(error), }); ``` `onError` catches datafeed and overlay failures that would otherwise be silent; wiring it to your error reporter is worth the two lines. ## What the Host Has to Provide The chart side is finished; the persistence side is yours. For a complete integration you need four things, none of which the library can decide for you: - **A per-user layout store keyed by page kind**, one JSON body per user and page, read on mount and written from `onLayoutChange`. - **An overlay registry**, one `OverlayDef[]` per page kind, wrapping your own series endpoints. - **A drawings store per symbol**, if you want drawings to follow the symbol. - **Defaults for anonymous users**, which is simply a layout object in your bundle. Nothing in the chart depends on those existing. Without any of them the shell still works; it just forgets everything on reload. --- # The ORTEX mark > What the mark is, when it shows, and how a white-label license removes it. Section: Commercial. Source: https://ortexcharts.com/docs/branding ## What the Mark Is Every chart drawn by ORTEX Charts carries a small mark on the first pane: the caption "Charts provided by" above the ORTEX wordmark, in the theme text color at 60 percent opacity, twelve pixels high, in the bottom-left corner by default. Clicking it opens ortex.com in a new tab. That is the entire commercial enforcement mechanism in the code. There is nothing else. _Live example: [branding](https://ortexcharts.com/examples/branding)_ ## What the Mark Is Not Stating this plainly matters more than describing the mark, because the assumption in this category is usually the opposite. - **The chart never breaks.** There is no state — missing key, invalid key, expired key, wrong domain — in which the library refuses to draw, degrades the data, throws, or disables a feature. A customer whose key expires on a Friday night has a working chart on Saturday morning. - **The chart never nags.** There is no overlay, no modal, no banner, no watermark across the plot, and no repeated warning. One `console.warn` is written once per page load, for the developer, and nothing is written to the user's screen beyond the mark itself. - **The mark does not say "Unlicensed".** A visitor to your site cannot tell from the chart whether you have paid. This was decided deliberately: shaming a customer's users is not a sales tactic ORTEX is willing to use. - **The chart never phones home.** No request is made to any ORTEX host. The key is verified locally with WebCrypto. See [Questions we are asked](https://ortexcharts.com/docs/faq) for the complete telemetry answer. ## When the Mark Shows There are three states, and only one of them removes the mark. | State | Mark | Console | |---|---|---| | No key, or an invalid, expired or wrong-domain key | Shown, with the caption | One warning naming the reason | | A valid key **without** the `whitelabel` feature | Shown, exactly the same mark | One warning if you asked to hide it | | A valid key **with** the `whitelabel` feature | Hidden when `branding.visible` is `false` | Nothing | The Community and Team plans grant the `financial`, `viz` and `drawings` features. Neither grants `whitelabel`. The White-label plan grants `*`, which includes it. OEM agreements are issued with whatever the contract says. So the mark is not the difference between paying and not paying. It is the difference between the White-label plan and everything below it, and it is the feature that plan is mostly bought for. See [/pricing](https://ortexcharts.com/pricing) for the plans and the numbers. ## Removing It ```ts import { setLicenseKey, createFinancialChart } from "@ortex-charts/financial"; await setLicenseKey(WHITELABEL_KEY); const chart = createFinancialChart(el, { branding: { visible: false }, data: bars, }); ``` `branding.visible = false` is a request, not a command. It is honored only when the current license grants `whitelabel`. Setting it without that feature does exactly one thing beyond being ignored: it writes one line to the console. ```text [ORTEX Charts] This license does not include white-labeling; the ORTEX mark stays on. ``` With no key at all, the message is different and names the underlying problem: ```text [ORTEX Charts] No license key set. The ORTEX mark stays on until a valid key is set with setLicenseKey(). ``` Check what you have before assuming a key is the problem: ```ts import { hasFeature, licenseStatus } from "@ortex-charts/financial"; licenseStatus(); // { state, payload, message, verified } hasFeature("whitelabel"); // true only on a valid or pending license that grants it ``` A `state` of `pending` means the payload passed the synchronous checks and the signature is still being verified; features are granted during that window, because blocking a paying customer's chart on an asynchronous check is worse than the risk it removes. ## Replacing It With Your Own A `whitelabel` license may also keep a mark and make it yours, which is what a white-labeled product usually wants rather than a bare corner. ```ts createFinancialChart(el, { branding: { visible: true, logo: "https://cdn.example.com/brand/mark.svg", // a URL, a data URI, or "text" caption: "", // empty removes the caption line position: "bottomRight", height: 14, opacity: 0.5, href: "https://example.com", }, }); ``` Setting `logo: "text"` draws the `text` option in a bold face at `height` pixels instead of an image, which avoids an extra request for a wordmark that is just letters. Keeping the ORTEX mark on a licensed chart is also fine, and some customers do; the option simply stays at its default. ## Every Branding Option | Option | Type | Default | What it does | |---|---|---|---| | `visible` | `boolean` | `true` | Show the mark. Setting it to `false` requires the `whitelabel` feature. | | `position` | `"bottomLeft" \| "bottomRight" \| "topLeft" \| "topRight"` | `"bottomLeft"` | Corner of the first pane. | | `height` | `number` | `12` | Logo height in CSS pixels. | | `opacity` | `number` | `0.6` | Opacity of the whole mark. | | `logo` | `"ortex" \| "text" \| string` | `"ortex"` | The ORTEX wordmark, the `text` option, or an image URL or data URI. | | `text` | `string` | `"ORTEX"` | Drawn when `logo` is `"text"`. | | `caption` | `string` | `"Charts provided by"` | Small line above the mark; an empty string removes it. | | `href` | `string` | `"https://ortex.com"` | Opened in a new tab when the mark is clicked. | `position`, `height`, `opacity` and `caption` are honored regardless of the license, so an unlicensed evaluation can move the mark out of the way of a legend without anything being enforced against it. What requires the feature is removing the mark, and replacing it with another brand. `BRANDING_DEFAULTS` is exported if you want to start from the defaults and change one field. ## Why It Works This Way Anything that runs in a browser ships to the browser. Minified JavaScript is source code with the comments removed, and a determined person can read it, copy it and patch out a check in an afternoon. That is true of every library in this category and every one of them is a functioning business, so the interesting question is not how to prevent copying but what makes the legitimate path the easier one. ORTEX's answer, in order: - **A contract with terms a company can be held to.** The customers who can afford a chart library do not risk a breach-of-contract suit to save a subscription. - **Updates and support**, which stop when the subscription does. - **Distribution through a per-customer channel** — a hosted kit locked to your domains, or a registry token — rather than through public npm. - **A signed, domain-bound key with one visible consequence**, so the unlicensed state is obvious in a screenshot but harmless to the end user. - **No obfuscation**, because it costs performance and debuggability and buys about a day of a copier's time. Removing the check by editing the bundle is possible. It is also a deliberate act, named as a breach in the agreement, with the evidence sitting in the customer's own deployment, and the key id in a leaked build identifies which customer it came from. That is the honest description of the protection, and it is the same one every vendor in the category could give. ## Related - [License keys](https://ortexcharts.com/docs/licensing) — issuing, domains, expiry and the states. - [Questions we are asked](https://ortexcharts.com/docs/faq) — telemetry, expiry, support. - [/pricing](https://ortexcharts.com/pricing) — which plan grants `whitelabel`. --- # Sparklines > The 3.2 KB build for tables and cards: line, area, column and win-loss. Section: Packages. Source: https://ortexcharts.com/docs/sparklines ## The 5.4 KB Build `@ortex-charts/lite` is a separate package with one job: a tiny canvas chart for a table cell or a card. It is 3.2 KB gzipped, depends only on `@ortex-charts/math`, and has no axes, no crosshair, no legend and no interaction. A table with 300 sparklines schedules one animation frame, not 300. ```bash npm install @ortex-charts/lite ``` ```ts import { createSparkline } from "@ortex-charts/lite"; const spark = createSparkline(cell, { kind: "area", colorByTrend: true, width: 120, height: 28 }); spark.setData(closes); ``` _Live example: [sparkline-table](https://ortexcharts.com/examples/sparkline-table)_ ## Four Kinds | Kind | Draws | Used for | |---|---|---| | `line` | A polyline, optionally curved, with an endpoint dot. | Price and value history. | | `area` | The same line with a fill beneath it. | The same, with more weight in a dense table. | | `column` | One column per value, colored by sign around `baseline`. | Returns, flows, changes. | | `winloss` | Equal-height marks up or down by sign. | Win and loss streaks, hit rates. | ```ts createSparkline(el, { kind: "line" }); createSparkline(el, { kind: "column", baseline: 0 }); createSparkline(el, { kind: "winloss", baseline: 0 }); ``` ## Data ```ts spark.setData([12.4, 12.9, 12.1, 13.4]); // numbers spark.setData([{ value: 12.4 }, { value: 12.9 }]); // rows spark.setData([[t1, 12.4], [t2, 12.9]]); // [time, value] pairs ``` Time is accepted but not used for spacing: points are evenly spaced. A sparkline is a shape, not a chart, and a gap in the dates does not become a gap in the line. ```ts spark.push(13.1); // append one value spark.push(13.1, 60); // append, keeping at most 60 points ``` `push` is the live path. It is cheap enough to call on every tick of a streaming table. ## Options | Option | Default | What it does | |---|---|---| | `kind` | `"line"` | One of the four above. | | `width`, `height` | container size, else 100 by 28 | Canvas size in CSS pixels. | | `color` | `"#4C8DFF"` | Line or column color. | | `positiveColor` | `"#22C08A"` | Used by `colorByTrend` and by sign coloring. | | `negativeColor` | `"#EF4E5A"` | The same, downward. | | `colorByTrend` | `false` | Color the whole sparkline by whether the last value is above the first. | | `lineWidth` | `1.5` | Stroke width. | | `curve` | `"linear"` | `linear`, `step`, `monotone` or `smooth`. | | `fillOpacity` | `0.18` | Area fill opacity. | | `endpoint` | `true` | Dot at the last point. | | `endpointRadius` | `2` | Its radius. | | `baseline` | `null` | Draw a faint line at this value and color columns by sign around it. | | `baselineColor` | a faint gray | Color of that line. | | `min`, `max` | data extent | Fixed value range, for comparability across rows. | | `padding` | `2` | Inset in CSS pixels. | | `extremes` | `false` | Highlight the minimum and maximum points. | | `background` | `"transparent"` | Canvas background. | `SPARKLINE_DEFAULTS` is exported if you want to derive from the defaults. The two options that matter most in a table are `colorByTrend` and `min`/`max`. `colorByTrend` is what makes a column of sparklines readable at a glance without a legend. Fixing `min` and `max` makes rows comparable to each other; leaving them unset makes each row use its full height, which reads better per row and worse down the column. Choose deliberately. ## The Handle ```ts const spark = createSparkline(el, options); spark.canvas; // the element, appended to the container spark.setData(values); spark.push(value, maxPoints?); spark.applyOptions({ kind: "column", color: "#F5A524" }); spark.resize(160, 32); spark.invalidate(); // schedule a redraw spark.flush(); // draw now, synchronously spark.remove(); // detach and stop ``` Every mutator returns `this`, so calls chain. `flush()` is the one to call before taking a screenshot or asserting in a test, because drawing is otherwise deferred to the next frame. ## The Shared Render Loop Every sparkline on the page schedules its redraw on one `RenderScheduler`, exported as `renderScheduler`. A table that updates 300 cells in a loop produces one animation frame with 300 draws, not 300 frames. This is the reason the package exists separately from `@ortex-charts/core`, and it is why the cost of a sparkline table is proportional to the data rather than to the number of cells. You do not have to do anything to get this. It applies automatically. ## In a Table ```ts import { createSparkline, type Sparkline } from "@ortex-charts/lite"; const sparks = new Map(); for (const row of rows) { const cell = document.querySelector(`#spark-${row.id}`)!; const spark = createSparkline(cell, { kind: "area", colorByTrend: true, width: 120, height: 26, endpoint: true, }); spark.setData(row.closes); sparks.set(row.id, spark); } // Live: one push per row, one frame for all of them. feed.subscribe((tick) => sparks.get(tick.id)?.push(tick.price, 120)); // Teardown, when the table unmounts. for (const s of sparks.values()) s.remove(); ``` ## Beside a Line of Text A sparkline sized to the type next to it is the densest useful form, and it needs no chrome at all. ```ts createSparkline(span, { kind: "line", width: 56, height: 14, lineWidth: 1, endpoint: false, padding: 1, colorByTrend: true, }); ``` Keep the container `display: inline-block` with `line-height: 0` so the canvas sits on the text baseline instead of adding a descender's worth of space. _Live example: [spark-cells](https://ortexcharts.com/examples/spark-cells)_ ## In React ```tsx import { Sparkline } from "@ortex-charts/react"; ``` The component creates the sparkline on mount, applies data and option changes in place, and removes it on unmount. The instance is available through `ref` when you want `push`. See [React](https://ortexcharts.com/docs/react). ## When to Use the Full Chart Instead `@ortex-charts/lite` is deliberately small, and the things it does not do are the reason it is small. - **No axes, no ticks, no labels.** If you need a scale, use `@ortex-charts/financial` or `@ortex-charts/viz`. - **No crosshair, no tooltip, no hit testing.** Hovering does nothing. A tooltip on a sparkline is a `title` attribute or your own overlay on the container. - **No time axis.** Points are evenly spaced regardless of their timestamps. - **No theme object.** Colors are options on the sparkline, not tokens, because pulling in the theme would pull in the engine. - **No multiple series.** One array of numbers per sparkline. The crossover point in practice is a tooltip. As soon as a small chart needs to tell the user what a point is worth, it wants [`createFinancialChart`](https://ortexcharts.com/docs/quickstart) with most of the chrome switched off, which is about 56 KB gzipped instead of 3.2 KB. --- # Treemaps and heatmaps > The general-purpose charts in the family, on the same engine. Section: Packages. Source: https://ortexcharts.com/docs/viz ## What Is in the Package `@ortex-charts/viz` holds the general-purpose charts a financial data product needs alongside its price charts: a category chart for bars and columns, a treemap, a heatmap, and export helpers for PNG and CSV. They run on the same canvas layer, follow the same [theme tokens](https://ortexcharts.com/docs/customization) and share one animation frame with everything else on the page. ```bash npm install @ortex-charts/viz ``` Each component follows its container, is device-pixel-ratio aware, has an optional built-in tooltip, and is created and disposed the same way: ```ts import { createTreemap, createHeatmap, createCategoryChart } from "@ortex-charts/viz"; const chart = createTreemap(el, { theme: "dark", data }); chart.applyOptions({ colorMode: "change" }); chart.setTheme("light"); chart.resize(900, 500); chart.flush(); // draw synchronously chart.remove(); ``` _Live example: [viz-treemap](https://ortexcharts.com/examples/viz-treemap)_ ## Treemap A treemap sizes rectangles by value and colors them by change, which is the standard way to show a universe of instruments at once. ```ts import { createTreemap, type TreemapNode } from "@ortex-charts/viz"; const data: TreemapNode = { name: "S&P 500", children: [ { name: "Technology", children: [ { name: "AAPL", value: 3_100_000, change: 1.42 }, { name: "MSFT", value: 2_900_000, change: -0.63 }, ], }, { name: "Financials", children: [{ name: "JPM", value: 620_000, change: 0.28 }], }, ], }; const treemap = createTreemap(el, { theme: "dark", data, colorMode: "change", changeDomain: "auto", valueFormat: { type: "compact", precision: 1 }, changeFormat: { type: "percent", precision: 2 }, headers: true, breadcrumb: true, drillDown: true, onClick: (hit) => openInstrument(hit.data.name), }); ``` A node has `name`, an optional `value` for leaves, an optional `change` that drives the diverging color scale, an optional explicit `color` inherited by descendants, and `children`. Parents are sized by the sum of their leaves, so only the leaves need values. | Option | Default | What it does | |---|---|---| | `colorMode` | `"auto"` | `auto` uses an explicit color, else the change scale, else the group palette color. `explicit`, `change` and `group` force one of them. | | `changeDomain` | `"auto"` | Domain of the diverging scale; `auto` is symmetric about zero over the visible leaves. | | `changeColors` | theme | `[low, neutral, high]`; defaults to `theme.down`, `theme.grid`, `theme.up`. | | `valueFormat`, `changeFormat` | — | Number formatting for the labels and the tooltip. | | `paddingInner`, `paddingOuter` | — | Gaps between and around rectangles. | | `headers`, `headerHeight` | `true` | Header strips above groups. | | `breadcrumb` | `true` | Path strip above the map while zoomed in. | | `labels`, `minFontSize`, `maxFontSize` | `true` | Names and values inside leaves when they fit. | | `drillDown` | `true` | Click a group to zoom in; click the breadcrumb to zoom out. | ```ts treemap.setData(nextRoot); treemap.zoomTo(node); treemap.zoomOut(1); treemap.currentRoot(); treemap.getPath(); // TreemapNode[] from the full root to the current one treemap.getLayout(); // the laid-out tree in CSS pixels ``` `layoutTreemap`, `treemapLeaves` and `findPath` are exported for hosts that want the layout without the canvas — for a server-rendered image, a test, or an accessible table alternative. ## Heatmap A grid of rows by columns with a sequential or diverging color scale, which is what a returns calendar, a correlation matrix or a factor exposure grid wants. ```ts import { createHeatmap } from "@ortex-charts/viz"; const heatmap = createHeatmap(el, { theme: "light", rows: ["2021", "2022", "2023", "2024", "2025"], columns: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], values: [ [1.2, -0.4, 2.8, 0.1, -1.9, 3.2], [-2.1, 0.8, 1.1, NaN, 0.4, -0.7], // … ], scale: { type: "diverging", domain: "auto", center: 0 }, format: { type: "percent", precision: 1 }, cellLabels: true, legend: true, onClick: (hit) => drillInto(hit.rowLabel, hit.columnLabel), }); ``` `values` is row-major: `values[row][column]`. `NaN` is an empty cell and is drawn in `emptyColor` rather than at the bottom of the scale, which matters for a calendar with missing months. | Option | Default | What it does | |---|---|---| | `scale.type` | `"diverging"` | `sequential` or `diverging`. | | `scale.domain` | `"auto"` | `[min, max]`, or derived from the finite values; symmetric about `center` when diverging. | | `scale.center` | `0` | Center of a diverging scale. | | `scale.colors` | theme | Two colors for sequential, three for diverging. | | `format` | percent, 1 decimal | Cell and tooltip formatting. | | `cellLabels` | `true` | Print the value in the cell when it fits. | | `cellGap` | `1` | Pixels between cells. | | `rowLabels`, `columnLabels` | `true` | Axis labels. | | `columnLabelPosition` | `"top"` | `top` or `bottom`. | | `maxLabelWidth` | `140` | Cap on a label footprint in pixels. | | `legend` | `true` | Color bar under the grid. | | `emptyColor` | a faint tint | Fill for `NaN` cells. | ```ts heatmap.setData({ rows, columns, values }); heatmap.colorFor(1.4); // the color a value would get, for a host-drawn legend ``` `resolveHeatmapDomain` and `createColorScale` are exported for the same reason: a host that wants to color a table cell to match the heatmap can use exactly the same scale. ## Category Chart Bars and columns over a categorical axis, stacked or grouped, horizontal or vertical. ```ts import { createCategoryChart } from "@ortex-charts/viz"; const chart = createCategoryChart(el, { theme: "dark", orientation: "horizontal", categories: ["Energy", "Financials", "Healthcare", "Technology", "Utilities"], series: [ { key: "long", title: "Long", color: "#22C08A", data: [12, 34, 21, 55, 8] }, { key: "short", title: "Short", color: "#EF4E5A", data: [-4, -12, -6, -19, -2] }, ], stacked: true, baseline: 0, colorBySign: false, valueLabels: true, legend: true, sortBy: "desc", valueAxis: { position: "bottom", format: { type: "compact", precision: 1 }, gridlines: true, visible: true }, onClick: (hit) => filterBy(hit.category), }); chart.setData({ categories, series }); ``` `orientation: "vertical"` draws columns and `"horizontal"` draws bars. `colorBySign` colors each bar by which side of `baseline` it falls on, using the theme up and down colors, which is the fastest way to a readable contribution chart. `sortBy` orders categories by their total across series without changing the underlying arrays, and `CategoryHit.categoryIndex` is the index into your original `categories`, not into the displayed order. ## Export ```ts import { canvasToPNG, downloadPNG, downloadCSV, rowsToCSV, composeCanvases } from "@ortex-charts/viz"; await downloadPNG(chartCanvas, "sectors.png", { scale: 2, background: "#0E1218" }); const csv = rowsToCSV(rows, ["symbol", { key: "change", label: "1D %" }]); downloadCSV(rows, "universe.csv", ["symbol", "value", "change"]); const combined = composeCanvases([topCanvas, bottomCanvas], "#0E1218"); ``` `canvasToPNG` returns a `Blob` for hosts that want to upload rather than download, and `composeCanvases` stacks several canvases into one image, which is how a multi-pane screenshot is assembled. These helpers are in `viz` rather than in `core` because they are DOM download plumbing rather than chart engine. A financial chart's own screenshot is `chart.takeScreenshot()`, which returns a canvas you can pass straight to `canvasToPNG`. ## Shared Options and Methods Every component takes `theme`, `width`, `height`, `tooltip` and `padding`, and exposes: ```ts component.root; // the container div it created component.getTheme(); component.getOptions(); // readonly component.setTheme(theme); // "dark", "light", or a Theme object component.applyOptions(patch); // deeply partial component.resize(width, height); component.invalidate(); // schedule a redraw component.flush(); // draw now component.remove(); ``` Leaving `width` and `height` unset makes the component follow its container, which is the usual choice; setting them fixes the size, which is what an export path wants. ## What Is Not Here `@ortex-charts/viz` is three chart types, not a general charting library. There is no scatter or bubble chart, no pie or donut, no radar, no sankey, no gauge, no geographic map and no 3D. Apache ECharts has 20 or more chart types and is a reasonable thing to use alongside this package if you need them. What `viz` is for is the small set of general charts that have to look like the price chart, share its theme, and cost nothing extra in the bundle because the engine is already there. --- # React > Declarative components and hooks over the same imperative API. Section: Packages. Source: https://ortexcharts.com/docs/react ## What the Package Is `@ortex-charts/react` is a thin binding layer with no logic of its own. Every component creates the corresponding imperative object in a layout effect, applies prop changes with `applyOptions` or `setData`, and disposes it on unmount. The underlying handle is always available through `ref`, so nothing is lost by using the components. It is tested against React 17, 18 and 19, and declares them as peer dependencies with `>=17`. ```bash npm install @ortex-charts/react ``` _Live example: [react-chart](https://ortexcharts.com/examples/react-chart)_ ## The Components | Component | Wraps | Handle through `ref` | |---|---|---| | `` | `createChart` | `Chart` | | `` | `chart.addPane` | — (provides its id through context) | | `` | `chart.addSeries` | `SeriesModel` | | `` | `createFinancialChart` | `FinancialChart` | | `` | `createChartShell` | `ChartShell` | | `` | `createSparkline` | `Sparkline` | ## The Financial Chart The common case is one component with data, indicators and markers as props. ```tsx import { FinancialChart, type IndicatorSpec } from "@ortex-charts/react"; import type { Bar, SeriesMarker } from "@ortex-charts/react"; import { useMemo, useState } from "react"; const OPTIONS = { theme: "dark", timeZone: "America/New_York", resolution: "1D" } as const; export function PriceChart({ bars, earnings }: { bars: Bar[]; earnings: SeriesMarker[] }) { const [hover, setHover] = useState(""); const indicators = useMemo( () => [ { id: "ema", inputs: { length: 21 } }, { id: "sma", inputs: { length: 200 } }, { id: "rsi" }, ], [], ); return ( <> setHover(e.time ? new Date(e.time).toDateString() : "")} />

{hover}

); } ``` `indicators` is diffed by identity: new entries are added, missing ones removed, and the rest are left alone rather than being torn down and recreated. The identity of an entry is the JSON of `{ id, inputs, options }`, or an explicit `key` when you supply one, so two moving averages with different lengths are two distinct entries and an unchanged array causes no work at all. `data` and `markers` are applied whenever the **array identity** changes, so a new array on every render means `setData` on every render. Memoize them or hold them in state. `options` is compared shallowly at the top level and the changed keys go to `applyOptions`. Two keys — `seriesType` and `volume` — take effect only at creation, because they decide what the chart is made of. ## The Core Chart With Children For anything that is not a standard price chart, compose it. ```tsx import { Chart, Pane, Series } from "@ortex-charts/react"; ``` `` renders nothing into the DOM; it manages a series on the chart from context. `` provides its id to nested series, and `options.pane` on a series overrides the enclosing pane if you need to place one explicitly. Changing `type` on a `` recreates it. Changing `heightRatio` or `minHeight` on a `` applies live. ## The Full Shell ```tsx import { ChartShell } from "@ortex-charts/react"; router.push(`/chart/${s}`), }} style={{ height: 640 }} /> ``` `symbol`, `resolution` and `theme` are controlled props: the shell follows them when they change, so the chart and your application state stay in step in either direction. `layout` is re-applied whenever its identity changes, which is what you want after loading a user's saved layout asynchronously. `options` is read at creation. Callbacks inside it are always invoked through their latest value, so inline arrow functions are fine and do not need memoizing. Structural options — `datafeed`, `overlays`, `toolbar` — need a remount to change, and the idiomatic way to force one is a `key`: ```tsx ``` ## Hooks ### `useChart` and `usePaneId` Inside a `` or ``, `useChart()` returns the core `Chart` or `null` until it exists after the first commit. `usePaneId()` returns the id of the enclosing ``. Both are how you write your own child components. ```tsx import { useChart, usePaneId } from "@ortex-charts/react"; import { useEffect } from "react"; function SessionShading() { const chart = useChart(); const paneId = usePaneId(); useEffect(() => { if (!chart) return; const primitive = makeSessionShading(); chart.addPrimitive(primitive, paneId); return () => chart.removePrimitive(primitive.id); }, [chart, paneId]); return null; } ``` ### `useChartEvents` The four event props — `onCrosshairMove`, `onClick`, `onDblClick`, `onVisibleRangeChange` — are accepted directly by `` and ``. The hook behind them is exported for a chart you hold yourself: ```tsx useChartEvents(chart, { onCrosshairMove: setHover, onVisibleRangeChange: setRange }); ``` Subscriptions are made once per chart instance and always call the latest handler, so handlers never need memoizing. ### `useLiveSource` A `LiveSource` must survive re-renders and be closed exactly once. `useMemo` does not guarantee either. `useLiveSource` does: it keeps the value for the lifetime of its dependencies, closes the previous one when they change, and closes the last one on unmount. ```tsx import { FinancialChart, useLiveSource } from "@ortex-charts/react"; import { websocketSource, type LiveItem } from "@ortex-charts/financial"; function LiveChart({ symbol, bars }: { symbol: string; bars: Bar[] }) { const source = useLiveSource( () => websocketSource({ url: `wss://feed.example.com/${symbol}`, parse: (raw) => JSON.parse(String(raw)) as LiveItem, onOpen: (send) => send(JSON.stringify({ subscribe: symbol })), }), [symbol], ); return ; } ``` Anything with a `close()` method is closed, so the hook works for your own sources too. ## Sparklines ```tsx import { Sparkline } from "@ortex-charts/react"; ``` The container is an inline-block `span` with zero line height, so it sits on the text baseline. Hold a `ref` to call `push` for live values. See [Sparklines](https://ortexcharts.com/docs/sparklines). ## Referential Stability This is the one thing to get right, and it applies to every component in the package. `options` and `data` props are compared by identity, not by value. A new object literal on every render is a new identity, so the effect runs every render. ```tsx // Wrong: a new object every render, so applyOptions runs every render. // Right: stable identities. const OPTIONS = { theme: "dark", resolution: "1D" } as const; // module constant const data = useMemo(() => rows.map(toBar), [rows]); // memoized ``` Callbacks are the exception. Every event prop and every shell callback is routed through a ref, so inline functions are correct and cost nothing. ## Getting the Imperative Handle ```tsx import { useRef } from "react"; import { FinancialChart, type FinancialChartHandle } from "@ortex-charts/react"; const ref = useRef(null); c.chart.timeScale.fitContent()} />; // Later: ref.current?.chart.timeScale.setVisibleTimeRange(from, to); ref.current?.alerts().add({ price: 182.5, label: "Breakout" }); ``` `onReady` fires once with the handle after creation, which is the right place for anything that has to happen before the user sees the first frame. The `ref` is the right place for anything that happens later. For realtime updates, prefer the handle over the `data` prop: `ref.current?.update(bar)` is one row, while a new `data` array is a full `setData`. ## Server-Side Rendering The components check for `window` before creating anything, so they render an empty container on the server and fill it on hydration. In Next.js the file still needs `"use client"`, because the effect only runs in a client component. There is no server-rendered chart image; see [Installation and delivery](https://ortexcharts.com/docs/install). ## Re-Exported Types To avoid importing from four packages, the React package re-exports the types callers usually need alongside the components: ```ts import type { ChartHandle, ChartOptions, DeepPartial, SeriesModel, SeriesDataInput, SeriesOptionsBase, SeriesTypeMap, CrosshairEvent, VisibleRangeEvent, FinancialChartHandle, FinancialChartOptions, SeriesMarker, LiveSource, LiveItem, StreamOptions, AddIndicatorOptions, IndicatorInstance, SparklineHandle, SparklineOptions, SparklineData, ChartShellHandle, ChartShellOptions, ChartLayout, OverlayDef, Bar, } from "@ortex-charts/react"; ``` ## What the Package Does Not Do - **There is no React renderer for the canvas.** Children of `` are effects that call the imperative API; they do not reconcile drawing operations. - **There are no components for drawings, events, order flow, replay or alerts.** Those are primitives on the chart, reached through the handle. Wrapping one in a component of your own is about ten lines, as in the `SessionShading` example above. - **`` does not manage realtime updates.** Use the `SeriesModel` from `ref` and call `update`, or hand the chart a `live` source. --- # Performance > What the numbers are, how they are measured, and how to keep them. Section: Packages. Source: https://ortexcharts.com/docs/performance ## The Numbers Measured on 5 September 2026 by `scripts/bench/bundle-size.mjs` in the library repository. Our entry points are bundled with esbuild and gzipped; the other vendors' own published production files are gzipped by the same code, file by file. Sizes are per entry point rather than per library, because tree-shaking makes "the library is N kilobytes" meaningless: a page that draws a line pays for a line. ### Payload | What a page imports | gzip | Files | |---|---|---| | ORTEX Charts, a line chart (`core`) | **29.5 KB** | 1 | | ORTEX Charts, candles and volume (`createFinancialChart`) | **55.8 KB** | 1 | | ORTEX Charts, candles with indicators and drawings | **64.1 KB** | 1 | | ORTEX Charts, the full shell (`financial` + `ui`) | **83.2 KB** | 1 | | ORTEX Charts, a sparkline (`lite`) | **3.2 KB** | 1 | | TradingView Lightweight Charts 5, standalone production build | 60.7 KB | 1 | | Highcharts Stock 13, `highstock.js` alone | 132.6 KB | 1 | | Highcharts Stock 13, with the stock tools and indicators its own demo loads | 215.6 KB | 9 | | TradingView Advanced Charts, the licensed drop | 4,757.5 KB uncompressed | 154 | | ChartIQ, its public demo | 4.4 MB transferred | 65 | Three comparisons are worth drawing out. - **Like for like, ORTEX Charts is smaller than Lightweight Charts.** A chart with one series costs 29.5 KB here against 60.7 KB there, a little over half for the same job, and none of the difference is features it has and we do not. Adding candles, a volume pane, a legend, price lines and a crosshair readout takes ORTEX Charts to 55.8 KB, which is still under the 60.7 KB of a renderer that draws a series and stops. The 45 KB figure quoted in most comparisons is the vendor's own claim rather than a measurement of the file they publish. - **Against a chart with a financial toolbar, the gap is wide.** Highcharts Stock does have one, and loading it the way their own Stock Tools demo does costs 215.6 KB across nine files. The ORTEX equivalent is 83.2 KB in one. - **Against the licensed drops it is not close.** Advanced Charts is 154 files and 4.8 MB on disk; ChartIQ transfers 4.4 MB across 65 files before a chart appears. ### Rendering Measured on 2026-09-04 in headless Chromium at 1400 by 900, driven by Playwright, with the same chart in both libraries: candles, a volume pane and two overlay lines. Four invocations of three repeats each, every repeat on a fresh page, and the cell is the median of the four invocation medians. **Create to first paint**, timed from before the create call to the frame that paints the data. | Bars | ORTEX Charts | Lightweight Charts 5 | |---|---|---| | 10,000 | **42 ms** | 75 ms | | 100,000 | **36 ms** | 447 ms | **A live tick, to the frame that paints it.** Call the library's update, then wait for the frame that carries the change to the compositor. Both libraries coalesce drawing onto their own animation frame, so both are measured through that path: 60 ticks, one per frame, one in ten opening a new bar and the rest refining the forming one, as a feed does. | Bars | ORTEX Charts | Lightweight Charts 5 | |---|---|---| | 10,000 | **33 ms** | 33 ms | | 100,000 | **33 ms** | 82 ms, and 124 ms at the 95th percentile | 33 ms is the floor of this measurement: two frames of the 60 Hz cadence headless Chromium paces rendering at. A library sitting on the floor absorbed the tick inside one frame budget and dropped nothing. At 10,000 bars both do, and there is no difference a user could see. At 100,000 bars ORTEX Charts is still on the floor and Lightweight Charts is two to five frames late. An earlier version of this page compared an ORTEX redraw against a Lightweight Charts update call that only schedules one. That is unlike work: it flattered us at 100,000 bars and flattered them at 10,000, and it has been withdrawn. Where the same call exists on both sides, the processor cost is a tie: an update at 100,000 bars is 3.0 ms here and 3.4 ms there. Both libraries pan and zoom at the display's 60 Hz with 100,000 bars. Advanced Charts and ChartIQ cannot be driven from code in this harness — there is no installable build — so only their load cost is compared. _Live example: [benchmark](https://ortexcharts.com/examples/benchmark)_ The example above runs the same measurement in your browser: it creates a chart, sets 100,000 bars, waits for the first paint and prints the number your machine produced. Expect it to be slower than the headless figure on a laptop on battery, and faster than you expect on a desktop. ### Why 100,000 Bars Starts Faster Than 10,000 The 36 ms at 100,000 bars against 42 ms at 10,000 is not a typographical error, and it is worth understanding because it explains the engine's design. Start-up cost is dominated by the work proportional to the **visible** range, not to the data. With 100,000 bars the initial view is a smaller share of the data, so fewer bars are drawn on the first frame, and the per-bar setup is a typed-array copy rather than object allocation. The remaining variance between the two runs is measurement noise on a small absolute number. ## How the Numbers Are Produced Everything above is reproducible rather than asserted. - **Payload** is measured from a production build of the packages with `bundle-size`, which bundles the public entry points and gzips the result. The competitor figures are the transferred bytes their own demo pages fetch, read from the network panel. - **Rendering** is a Playwright script against the benchmark page in the repository, headless Chromium, fixed viewport, the same generated bars given to both libraries, timed around chart creation, `setData` and the first paint. It is opt-in, so it does not run in every continuous-integration job. Both are in the repository and are re-run when the numbers change. ## What Makes It Fast - **Typed-array columns.** Series are a struct of arrays, so a data set is a handful of allocations rather than one object per bar, and slicing a visible window is free. - **Visible-range work only.** Renderers receive `from` and `to` and draw between them. Autoscale considers only the visible range. Nothing iterates the whole history on a frame. - **Decimation.** A line denser than the pixels available is reduced to one minimum and one maximum per pixel column, so drawing cost stops growing with the data. - **Layered canvases.** Each pane has a main canvas and an overlay canvas, and the time-axis label has its own. Moving the crosshair repaints the overlays only, so a pointer move never redraws a series. - **Legend DOM only on change.** The legend is rebuilt when its content key changes, so a 60 Hz pan does not churn the DOM. - **Amortized appends.** Series keep a capacity-doubling backing store and hand out subarray views, so a live tick does not copy the columns. - **Incremental alignment.** When the main timeline grows in place, overlays re-align only their tail: O(tail) per tick rather than O(n). - **Windowed indicator recompute.** An indicator that declares its `lookback` has only that window recomputed on a tick, verified equal to a full recompute to within 1e-6. - **Candles batched by color.** Up candles and down candles are two fill passes, not one per bar. The single largest improvement in the library's history came from the realtime path: the processor cost of a live tick on 100,000 bars with two overlays, an EMA and an RSI fell by roughly four fifths when alignment and indicator recompute became incremental. ## Practical Guidance ### Hand Over Columns, Not Objects ```ts // Fine for a few thousand rows. series.setData(rows.map((r) => ({ time: r.t * 1000, value: r.v }))); // Better for a large set: build the columns once and hand them over. const time = new Float64Array(n); const value = new Float64Array(n); for (let i = 0; i < n; i++) { time[i] = rows[i].t * 1000; value[i] = rows[i].v; } series.setData({ length: n, time, value }); ``` Columnar input is not copied. For 100,000 rows this is the difference between one allocation and 100,000. ### Use `update` for Live Data, Never `setData` ```ts series.update(bar); // O(1) amortized, keeps every fast path series.setData(allBars); // full rebuild, full re-align, full indicator recompute ``` Calling `setData` on every tick is the single most common performance mistake, and it undoes every incremental path in the engine at once. The rules `update` relies on: - A time equal to the last bar replaces it in place. - A later time appends into the backing store. - An **earlier** time takes the slow rebuild path, so a feed that emits out of order pays for it on every tick. Buffer and sort before handing rows over. ### Do Not Hold On to `columns` Aligned columns are views into growable buffers. They are valid until the next data change and not after it. Read what you need and let go. ### Declare `lookback` on Custom Indicators An indicator without a `lookback` recomputes over all history on every tick. That is correct and, on a long history with a fast feed, the dominant cost. Declaring the window it actually depends on turns it into a constant. ```ts lookback: (inputs) => 10 * Number(inputs.length) ``` Leave it unset only for genuinely cumulative indicators, where the last value depends on the first bar. ### Watch the Cumulative Indicators `obv`, `cvd`, `vwap` and `cumvol` recompute fully on every tick by construction. On 100,000 bars with several of them and a tick every 200 ms, they are what will show up in a profile. ### Heikin-Ashi Is Not Incremental Yet The Heikin-Ashi transform re-runs over all bars whenever the data changes, including on every live tick. On a long intraday history with a live feed this is measurable. It is a known gap rather than a design decision. ### Prefer One Chart With Panes Over Several Charts Panes share one time scale, one interaction layer and one animation frame. Three linked charts are three of everything. ### Give the Container a Size Before Creating the Chart A chart created into a zero-height container does its layout work twice: once at zero and once when the `ResizeObserver` fires. Setting the height in CSS avoids the second pass and the visible reflow. ## Where the Ceiling Is Rendering is **Canvas 2D, not WebGL**. That is a real limit and it is worth being precise about where it bites. - A few hundred thousand visible points hold 60 Hz with decimation. A decade of daily bars is 2,500 rows; a decade of minute bars is about a million rows, of which only the visible window is drawn. - What Canvas 2D does not do is millions of points **all on screen at once** at 60 Hz with no decimation — the case SciChart's WebGL renderer is built for. If your product plots raw tick data for a whole session with every point visible, this library is the wrong choice today. - A WebGL path for line and candle series behind the same API is on the roadmap. It is not implemented. The other honest limits: there is no worker offload, so computation happens on the main thread; and there is no progressive rendering, so a very large `setData` is one synchronous block rather than a series of frames. ## Measuring Your Own Chart ```ts performance.mark("start"); const chart = createFinancialChart(el, { data: bars }); chart.chart.flush(); // force a synchronous draw performance.mark("painted"); performance.measure("first-paint", "start", "painted"); console.log(performance.getEntriesByName("first-paint")[0].duration); ``` `flush()` is the important call: drawing is otherwise deferred to the next animation frame, so a naive measurement times the setup and misses the paint. For the realtime path, measure a tick the same way around `series.update` followed by `chart.flush()`, and compare it against your tick rate. Anything under about 4 ms leaves headroom at 60 Hz. --- # License keys > Issuing keys, domain binding, expiry, and what happens when a key is missing. Section: Commercial. Source: https://ortexcharts.com/docs/licensing ## What a Key Is A license key is a string in three dot-separated parts: ```text OC1.. ``` The payload is JSON. The signature is Ed25519 over the payload string, made with ORTEX's private signing key, whose public half is embedded in `@ortex-charts/core`. Keys cannot be forged without that private key, which never leaves ORTEX's secret store. ```ts interface LicensePayload { id: string; // unique key id, for support and revocation customer: string; product: string; // always "ortex-charts" tier: string; // informational: "team", "whitelabel", "oem" features: string[]; // "whitelabel", "financial", "viz", "drawings", or "*" domains: string[]; // hostnames; "*.example.com" matches subdomains; "*" matches anything iat: number; // issued at, Unix seconds exp: number; // expiry, Unix seconds } ``` ## Installing One Call `setLicenseKey` once, as early as you can, before any chart is created. It is asynchronous because the signature check goes through WebCrypto, but you do not have to await it: the synchronous checks — format, product, expiry, domain — run first and the state is `pending` while the signature is verified, and features are granted during that window. ```ts import { setLicenseKey } from "@ortex-charts/financial"; const status = await setLicenseKey(process.env.NEXT_PUBLIC_ORTEX_CHARTS_KEY!); console.log(status.state, status.verified, status.message); ``` On the [hosted kit](https://ortexcharts.com/docs/install) this is already done: the kit file ends with a `setLicenseKey` call carrying your key, so nothing in your page has to know about it. ### Where to Put the Key The key is a public artifact. It ships to the browser, it is bound to your domains, and anyone can read it out of your bundle. Treat it like a publishable API key rather than a secret: an environment variable at build time is convenient and correct, and a secret store is unnecessary. ```ts void setLicenseKey(import.meta.env.VITE_ORTEX_CHARTS_KEY); // Vite void setLicenseKey(process.env.NEXT_PUBLIC_ORTEX_CHARTS_KEY!); // Next.js ``` What is secret is the **registry token** in your `.npmrc`, which grants access to the package downloads. That one belongs in continuous-integration secrets and never in a commit. ## The States ```ts import { licenseStatus, onLicenseChange, hasFeature, clearLicense } from "@ortex-charts/financial"; licenseStatus(); // { state, payload, message, verified } hasFeature("whitelabel"); // true only on a valid or pending license granting it onLicenseChange((s) => console.log(s.state)); clearLicense(); // mostly for tests ``` | `state` | Meaning | Effect | |---|---|---| | `unlicensed` | No key has been set. | The [ORTEX mark](https://ortexcharts.com/docs/branding) shows; one console warning. | | `pending` | Payload accepted, signature still being checked. | Features are granted; the chart draws normally. | | `valid` | Everything checked out. | Features per the payload. | | `invalid` | Malformed, wrong product, or a signature that failed. | Treated as unlicensed. | | `expired` | `exp` is in the past. | Treated as unlicensed. | | `domain` | The current hostname is not covered. | Treated as unlicensed. | `verified` is a separate boolean from `state`. It is `true` only once the Ed25519 signature has actually been checked. A browser whose WebCrypto lacks Ed25519 accepts the payload with `state: "valid"` and `verified: false`, because breaking a paying customer's chart over a missing platform primitive is worse than a missed check. **In every one of these states the chart works.** Nothing is disabled, no data is degraded, and nothing appears on the user's screen except the mark. See [the ORTEX mark](https://ortexcharts.com/docs/branding). ## Domains `domains` is a list of hostnames the key covers. - An exact hostname matches itself: `app.example.com`. - A wildcard matches the base and every subdomain: `*.example.com` covers `example.com`, `app.example.com` and `a.b.example.com`. - `*` matches anything, and is what an OEM key usually carries. - **`localhost` is always allowed**, along with `127.0.0.1`, `::1` and any `*.localhost` host, on every key. Development never fails a domain check. ```ts import { domainAllowed } from "@ortex-charts/financial"; domainAllowed("app.example.com", ["*.example.com"]); // true domainAllowed("example.net", ["*.example.com"]); // false ``` The check runs against `location.hostname` by default. Override it for tests: ```ts await setLicenseKey(key, { hostname: "app.example.com", now: Date.parse("2027-01-01") }); ``` Preview deployments are the case that catches people out. A branch preview on `pr-482.vercel.app` is not covered by `*.example.com`, so add the preview host as a wildcard domain when the key is issued, or accept the mark on previews. ### Two Layers of Domain Enforcement Hosted-kit customers are checked twice. The CloudFront function in front of the CDN compares the request `Origin` or `Referer` against the domains registered on the kit and refuses to serve the file to an unlisted host, and the key inside the file checks the hostname again in the browser. Registry customers get the second check only. ## Expiry and Renewal Keys are issued for the subscription term with a **14-day grace period** added on top, so a renewal that is a few days late never flips a customer's chart to the mark. A renewal re-signs onto the **same key id and the same kit id**, so your script tag and your key id do not change from year to year — only the key string does, and hosted-kit customers do not even see that. When a key does expire, the state becomes `expired` and the mark comes back. Nothing else changes. There is no countdown, no degradation and no interruption. Under the White-label plan the last version released during the term keeps working with its key indefinitely, which is the perpetual fallback customers expect from this kind of agreement. ## Features | Feature | What it grants | |---|---| | `whitelabel` | `branding.visible = false` is honored, and the mark may be replaced with your own. | | `financial` | Informational; the financial package is not gated. | | `viz` | Informational; the viz package is not gated. | | `drawings` | Informational; drawing tools are not gated. | | `*` | Everything, including `whitelabel`. | Being precise about this: **only `whitelabel` changes behavior today.** The others are recorded on the key so that the plan a customer is on is visible in the payload, and so that a future gate has a name to use, but no feature check in the library disables a package. A Community key and a White-label key run the same code with the same capabilities; the mark is the difference. ## Getting a Key - **Community, Team and White-label** keys are issued automatically. A purchase on ortexcharts.com signs a key, builds a hosted kit, registers your domains, issues a registry token and emails all of it within seconds of payment. Community follows the same path without the payment step. - **OEM and enterprise** keys are issued by hand from the same signer after a conversation. Changing the domains on a key means re-issuing it, which happens automatically when the domains on the subscription change; the kit is rebuilt at the same URL. ## Revocation Every payload carries an `id`. A key that has to be killed before its expiry is handled by publishing a revocation list in a release rather than by a runtime call to ORTEX, which would mean every customer page depending on ORTEX uptime. The practical implication for a customer is that revocation takes effect on the next library update — and for a hosted kit, that the kit simply stops being served. ## Testing Without a Key The library is fully functional unlicensed, so evaluation, unit tests and continuous integration need nothing. `localhost` is always allowed, so local development with a real key never fails a domain check either. For tests that need a specific license state: ```ts import { clearLicense, setLicenseKey, licenseStatus } from "@ortex-charts/financial"; beforeEach(() => clearLicense()); it("hides the mark under a white-label key", async () => { await setLicenseKey(TEST_KEY, { publicKey: TEST_PUBLIC_KEY, hostname: "example.com", now: FIXED_TIME }); expect(licenseStatus().state).toBe("valid"); }); ``` `publicKey` overrides the embedded key so a test can sign with its own pair, and `now` and `hostname` make expiry and domain behavior deterministic. ## Related - [The ORTEX mark](https://ortexcharts.com/docs/branding) — what showing and hiding it actually means. - [Installation and delivery](https://ortexcharts.com/docs/install) — kits, registry tokens and source access. - [Questions we are asked](https://ortexcharts.com/docs/faq) — expiry, telemetry and support. --- # Migrating > From Lightweight Charts, Highcharts Stock, TradingView Advanced Charts or ChartIQ. Section: Commercial. Source: https://ortexcharts.com/docs/migration ## How to Read This Page Four libraries, four honest comparisons. Each section maps the concepts that line up, then states plainly what that library does which ORTEX Charts does not. The second half is the part that matters when you are deciding, so it is not buried. Competitor APIs change between major versions. The mappings below are written against Lightweight Charts 5, Highcharts Stock 13, TradingView Advanced Charts 20-era and ChartIQ 8; check the exact names against the version you are on. One thing is common to all four migrations: **timestamps in ORTEX Charts are milliseconds since the Unix epoch, in UTC, everywhere.** Not seconds, not business-day objects, not local dates. That single conversion is most of the work in a mechanical port. ## From TradingView Lightweight Charts This is the easiest migration of the four. The mental models are close: one chart, series you add to it, data you set and update, a time scale you drive. ### API Mapping | Lightweight Charts | ORTEX Charts | |---|---| | `createChart(el, options)` | `createChart(el, options)`, or `createFinancialChart(el, options)` for a chart with a volume pane | | `chart.addSeries(CandlestickSeries, o)` | `chart.addSeries("candlestick", o)` | | `chart.addSeries(LineSeries, o)` | `chart.addSeries("line", o)` | | `chart.addSeries(AreaSeries, o)` | `chart.addSeries("area", o)` | | `chart.addSeries(BaselineSeries, o)` | `chart.addSeries("baseline", o)` | | `chart.addSeries(HistogramSeries, o)` | `chart.addSeries("histogram", o)` | | `chart.addSeries(BarSeries, o)` | `chart.addSeries("bar", o)` | | `series.setData(data)` | `series.setData(data)` | | `series.update(bar)` | `series.update(bar)` | | `series.applyOptions(o)` | `series.applyOptions(o)` | | `chart.removeSeries(series)` | `chart.removeSeries(series.id)` | | `chart.applyOptions(o)` | `chart.applyOptions(o)` | | `chart.timeScale().fitContent()` | `chart.timeScale.fitContent()` | | `chart.timeScale().setVisibleLogicalRange({from, to})` | `chart.timeScale.setVisibleRange(from, to)` | | `chart.timeScale().scrollToRealTime()` | `chart.timeScale.scrollToRealtime()` | | `chart.subscribeCrosshairMove(cb)` | `chart.subscribeCrosshairMove(cb)` | | `chart.subscribeClick(cb)` | `chart.subscribeClick(cb)` | | `chart.addPane()` | `chart.addPane({ heightRatio })` | | `createSeriesMarkers(series, markers)` | `addSeriesMarkers(chart, series, markers)`, or `fc.setMarkers(markers)` | | `series.createPriceLine(o)` | `chart.addPriceLine(series.id, o)` | | `chart.remove()` | `chart.remove()` | | Custom series plugin | `registerSeriesType(name, def)` | | Pane primitive plugin | `chart.addPrimitive(primitive, paneId)` | ### The Differences That Bite - **`timeScale` is a property, not a method.** `chart.timeScale.fitContent()`. - **Times are milliseconds.** Lightweight Charts takes Unix **seconds** or a business-day object. Multiply by 1,000; convert `{ year, month, day }` to a UTC midnight timestamp. - **Series are removed by id**, not by reference: `chart.removeSeries(series.id)`. - **Series types are strings**, not imported constructors. - **Colors default from the theme.** Where you set `upColor` and `downColor` on every series, you can set them once in the theme instead. - **A series declares how it aligns** to the main timeline. Lightweight Charts requires every series to share a time axis; here a daily series on an intraday chart is `align: "forwardFill"` rather than a data-preparation problem. ### What Lightweight Charts Has That ORTEX Charts Does Not - **It is open source, Apache 2.0.** No license key, no mark, no contract, no cost. If that is the deciding factor, it decides. - It is about 5 KB smaller for the subset it covers. - It has a longer public track record and a larger body of community examples. ### What You Gain A toolbar, symbol search, 32 drawing tools, 51 indicators, event markers, order flow, bar replay, alerts, host-managed layouts, an adapter layer, and a faster first paint at large data sizes — for about 5 KB more. ## From Highcharts Stock The mental model is further away. Highcharts is declarative: you describe a whole chart as one options object and call `update` to change it. ORTEX Charts is imperative: you create objects and call methods on them. ### API Mapping | Highcharts Stock | ORTEX Charts | |---|---| | `Highcharts.stockChart(el, options)` | `createFinancialChart(el, options)` | | `series: [{ type: "candlestick", data }]` | `chart.addSeries("candlestick").setData(data)` | | `data: [[x, o, h, l, c]]` | `[{ time, open, high, low, close, volume }]` | | `series: [{ type: "line", data: [[x, y]] }]` | `chart.addSeries("line").setData([[time, value]])` | | `yAxis: [{ …}, { … }]` with `height` and `top` | `chart.addPane({ heightRatio })` plus `priceScaleId` | | `yAxis.type: "logarithmic"` | `chart.priceScale("right").applyOptions({ mode: "logarithmic" })` | | `series[i].addPoint(point, redraw, shift)` | `series.update(point)` | | `series[i].setData(data)` | `series.setData(data)` | | `chart.update(options)` | `chart.applyOptions(options)` | | `series[i].update({ color })` | `series.applyOptions({ color })` | | `series: [{ type: "sma", linkedTo: "main", params: { period } }]` | `chart.addIndicator("sma", { length })` | | `series: [{ type: "flags", data }]` | `chart.setEvents(events)` or `chart.setMarkers(markers)` | | `chart.setSize(w, h)` | `chart.resize(w, h)`, or leave `autoSize` on | | `Highcharts.setOptions({ colors })` | `chart.setTheme({ ...darkTheme, palette })` | | `chart.destroy()` | `chart.remove()` | | `exporting` module | `chart.takeScreenshot()`, `shell.exportCsv()`, `downloadPNG`, `downloadCSV` | ### The Differences That Bite - **There is no global options object.** Nothing is configured for the page; every chart carries its own options and its own theme. - **Panes are not y-axes with pixel offsets.** A pane is a real container with a relative height weight, and a scale lives inside it. - **Indicators are not linked series.** They are a separate concept with their own registration, inputs and outputs. - **Rendering is Canvas, not SVG.** There is no DOM node per point to style with CSS or select with a query, and no SVG export. Styling is options and theme tokens. - **Redrawing is automatic.** There is no `redraw` flag on mutators; the chart schedules a frame. - **Times are milliseconds** here as well, which Highcharts also uses, so this is the one migration where timestamps need no conversion. ### What Highcharts Stock Has That ORTEX Charts Does Not - **A navigator strip and range-selector buttons** (1m, 3m, YTD, 1y, All) as built-in components. Range presets are a few lines on `setVisibleTimeRange` here; a navigator mini-chart is not built in. - **An accessibility module**: screen-reader descriptions of the data, a focusable data table, and keyboard navigation of points. See [The toolbar](https://ortexcharts.com/docs/toolbar) for what is and is not implemented here. - **A server-side export server** producing PNG, PDF and SVG from a running service. - **SVG rendering**, if per-point DOM matters to you for styling or testing. - **A boost module** for very large point counts. ### What You Gain About a fifth of the payload, a financial toolbar and drawing tools that Highcharts Stock does not include, an index-based time axis that removes closed periods without configuration, order flow, bar replay, and per-product rather than per-developer licensing. ## From TradingView Advanced Charts This is the migration with the largest gap in both directions, because Advanced Charts is a complete application delivered as a widget rather than a library you compose. ### API Mapping | Advanced Charts | ORTEX Charts | |---|---| | `new TradingView.widget({ … })` | `createChartShell(el, { … })` | | `datafeed` with `onReady`, `resolveSymbol`, `getBars`, `subscribeBars`, `searchSymbols` | `Datafeed` with `resolveSymbol`, `getBars`, `subscribeBars`, `searchSymbols`; no `onReady` | | `LibrarySymbolInfo` | `SymbolInfo` | | `resolution` as `"1"`, `"60"`, `"1D"` | The same notation | | `widget.setSymbol(symbol, interval, cb)` | `shell.setSymbol(s)` and `shell.setResolution(r)` | | `widget.activeChart().createStudy(name, …)` | `shell.addIndicator(id, inputs)` | | `widget.activeChart().createShape(point, o)` | `tools.add({ kind, points, style })` | | `widget.activeChart().createMultipointShape(points, o)` | The same | | `widget.activeChart().setChartType(n)` | `shell.setSeriesType("candlestick")` | | `widget.activeChart().setVisibleRange({from, to})` | `chart.timeScale.setVisibleTimeRange(from, to)` | | `widget.save(cb)` and `widget.load(state)` | `shell.getLayout()` and `shell.setLayout(layout)` | | `disabled_features` and `enabled_features` | `toolbar`, `drawings`, `indicators`, `drawingTools` switches | | `overrides` | Theme tokens and series options | | `widget.onChartReady(cb)` | The shell is ready when `createChartShell` returns | | Timescale marks | `chart.setEvents(events)` with `style: "lane"` | | Compare study | The built-in `price` overlay for another symbol | Datafeed shapes are deliberately close, so a port is usually mechanical: drop `onReady`, return `{ bars, noMoreData }` instead of calling `onHistoryCallback`, and return bars with millisecond `time` rather than second `time`. ### The Differences That Bite - **Callbacks become promises.** `getBars` returns a promise instead of calling `onHistoryCallback` and `onErrorCallback`. - **Times are milliseconds**, not seconds. - **There is no iframe.** The chart is in your DOM, so your CSS, your fonts and your developer tools all apply — and so do your layout bugs. - **State is yours.** There is no `save`/`load` server contract and no chart storage service; `getLayout` gives you JSON and you decide where it lives. - **Feature flags become option switches.** The `disabled_features` string list is replaced by typed options on the shell. ### What Advanced Charts Has That ORTEX Charts Does Not - **Around 110 built-in studies** against 51. The long tail there is mostly variations on what is here. - **The TradingView platform behind it**: the interface users already recognize, and the Pine Script ecosystem that comes with the platform rather than with the library. Custom studies in Advanced Charts itself are JavaScript, as they are here. - **Around 90 drawing tools** against 32. - **Multi-chart layouts** (two-by-two and the rest) as a built-in container, with symbol and interval synchronization. `linkCharts` synchronizes time and crosshair here, but the grid container is your CSS. - **Around 30 interface languages.** The chrome here is English only. - **A broker and order-management integration** — order tickets, positions, depth of market, an account panel — in the Trading Platform edition. - **The brand recognition.** Users know the interface. ### What You Gain 83.2 KB in one file instead of 4.8 MB across 154. A chart you can style, extend and debug. No attribution requirement on a licensed build, and no restriction to services offered free of charge: the free Advanced Charts agreement requires the TradingView mark **and** a service the public can use without paying, which rules out a subscription product and an internal desk tool outright. ## From ChartIQ ChartIQ is the closest competitor on feature depth and the furthest away on API style. It is also the most expensive of the four, sold as an annual enterprise contract. ### API Mapping | ChartIQ | ORTEX Charts | |---|---| | `new CIQ.ChartEngine({ container })` | `createFinancialChart(el, options)` | | `stxx.loadChart(symbol, { periodicity, masterData })` | `chart.bind(datafeed, { symbol, resolution })` | | `quotefeed` with `fetchInitialData`, `fetchPaginationData`, `fetchUpdateData` | `Datafeed` with `getBars` and `subscribeBars` | | `stxx.setPeriodicity({ period, interval })` | `binding.setResolution("15")` | | `stxx.setChartType("candle")` | `fc.setSeriesType("candlestick")` | | `stxx.setChartScale("log")` | `chart.priceScale("right").applyOptions({ mode: "logarithmic" })` | | `CIQ.Studies.addStudy(stxx, "rsi", inputs, outputs)` | `fc.addIndicator("rsi", inputs, options)` | | `stxx.addSeries(symbol, params)` | `chart.addSeries(type, options).setData(data)` | | `stxx.createDrawing(kind, params)` | `tools.add({ kind, points, style })` | | `stxx.exportDrawings()` and `importDrawings()` | `tools.getDrawings()` and `setDrawings()` | | `stxx.append`/`updateChartData` | `series.update(bar)` | | Themes as CSS | Theme tokens on the chart | | `stxx.destroy()` | `chart.remove()` | ### The Differences That Bite - **Styling is options, not CSS.** ChartIQ is styled with stylesheets over its own class names; here every color is a theme token or a series option. - **There is no periodicity object.** A resolution is a string. - **There is no `masterData` concept**; series own their own data and alignment. ### What ChartIQ Has That ORTEX Charts Does Not - **A much deeper indicator and study library**, with study editing, comparison studies and a study browser. - **Term structure, options chains, cross-section and time-span event charts** as first-class chart types. - **A trading and order-management integration**, and a market-depth component. - **Multi-chart layouts and workspace management** as products in their own right. - **A managed data service and a signature integration ecosystem.** - **An accessibility story**, including screen-reader support. ### What You Gain The demo transfers 4.4 MB and reaches DOMContentLoaded at 3.4 seconds. The full ORTEX chart is 83.2 KB in one file. Order flow — footprint bars, exact volume and market profiles, real delta from aggressor data — is included rather than a platform tier. And the price is public. ## A Migration Checklist Whichever library you are coming from: 1. **Convert timestamps to milliseconds, UTC.** Multiply seconds by 1,000, convert business-day objects to a UTC midnight, and drop local-date parsing. 2. **Set `timeZone` to the exchange zone**, not to the browser zone, or daily bars will land on the wrong day for a third of your users. 3. **Set `session`** if you draw intraday bars, so hourly candles start at the open. See [Time, sessions and resolutions](https://ortexcharts.com/docs/time). 4. **Move colors into a theme** rather than onto every series, so the theme switch works. 5. **Replace per-series time alignment work with `align`.** Data that does not share the price timeline is a series option here, not a pre-processing step. 6. **Replace `setData`-per-tick with `update`.** This is the single biggest performance difference between a good port and a bad one. 7. **Decide where layouts and drawings live**, because the library will not decide for you. See [The shell and layouts](https://ortexcharts.com/docs/shell). 8. **Audit your indicator list against the 51 built-ins** and budget for the ones you have to write. This is the most common source of unplanned work in a migration. ## The Honest Summary | If you need | Choose | |---|---| | Open source with no license at all | Lightweight Charts | | 110 studies, 90 drawing tools, 30 interface languages | TradingView Advanced Charts | | A navigator, an accessibility module, a server-side export service, SVG | Highcharts Stock | | The deepest study library, term structure, order management, an enterprise contract | ChartIQ | | Millions of points on screen at 60 Hz | SciChart | | A small, fast, extensible financial chart with a toolbar, drawings, order flow and replay, that you can white-label, priced per product | ORTEX Charts | --- # API reference > Every export of every package, grouped by what it is for. Section: Packages. Source: https://ortexcharts.com/docs/api ## How This Page Is Organized Every runtime export of every package, at version 0.5.0, grouped by package and then by purpose. Type-only exports are listed where they matter and omitted where they are only the options object of the function above them. `@ortex-charts/financial` re-exports all of `@ortex-charts/core`, so anything in the core tables is importable from `financial` too. Nothing re-exports `@ortex-charts/math`: data types such as `Bar` and `Tick` are imported from there directly. ## The Entry Points The five functions that create something. Everything else configures or extends what they return. ```ts function createChart(container: HTMLElement, options?: DeepPartial): Chart; function createFinancialChart(container: HTMLElement, options?: FinancialChartOptions): FinancialChart; function createChartShell(container: HTMLElement, options?: ChartShellOptions): ChartShell; function createSparkline(container: HTMLElement, options?: Partial): Sparkline; function createAdapter(spec: AdapterSpec, deps?: AdapterDeps): Adapter; ``` Plus the three in `@ortex-charts/viz`: `createTreemap`, `createHeatmap` and `createCategoryChart`, each `(container, options) => Component`. ## `@ortex-charts/math` Pure functions and small classes. No DOM, no canvas. Runs in Node and in a worker. ### Data Types and Conversion | Export | Description | |---|---| | `Bar` | One OHLCV row, with optional `buyVolume` and `sellVolume`. | | `Tick` | One trade: `time`, `price`, `size`, optional aggressor `side`. | | `BarSeries` | OHLCV in struct-of-arrays form with `Float64Array` columns. | | `ValueSeries` | Single-value series in the same form. | | `TimeValuePoint` | A `[time, value]` pair. | | `Range` | A numeric `[from, to]` pair. | | `barSeriesFromRows(rows)` | Rows to columns; allocates the aggressor columns only when a row carries them. | | `barSeriesToRows(series)` | Columns back to rows. | | `valueSeriesFromPoints(points)` | `[time, value]` pairs to columns. | | `valueSeriesFromColumns(time, value)` | Two arrays to a `ValueSeries`; throws when the lengths differ. | | `EMPTY_BARS` | A shareable empty `BarSeries`. | | `clamp(v, lo, hi)` | Clamp a number into a range. | ### Scales | Export | Description | |---|---| | `PriceScale` | Value-to-pixel mapping for one price axis, with autoscale and margins. | | `PriceScaleMode` | `"normal" \| "logarithmic" \| "percentage" \| "indexedTo100"`. | | `PriceScaleOptions` | Options for `PriceScale` (distinct from the chart's own price-scale options). | | `TimeIndexScale` | The index-based time axis: bar index to pixel, time to fractional index, zoom and scroll, extrapolation past the data. | | `TimeIndexScaleOptions`, `LogicalRange` | Its options and visible-range type. | | `symLog10(v)` | Symmetric base-10 log, linear near zero and defined for negatives. | | `symPow10(t)` | Its inverse. | ### Ticks | Export | Description | |---|---| | `priceTicks(...)` | Nice tick values for a price scale given pixel height and a minimum label gap. | | `PriceTick`, `PriceTickOptions` | Its result and options. | | `timeTicks(...)` | Ticks for the index axis with weighted labels so labels never collide. | | `TimeTick`, `TimeTickOptions` | Its result and options. | | `TickWeight` | The label significance ladder: year beats month beats day beats hour. | | `minimumWeight(barSpacing, minLabelGap, resolution)` | The finest weight worth labeling at a given zoom. | | `labelFor(parts, weight, locale)` | Label text for a tick: the coarsest unit that changed. | ### Formatting | Export | Description | |---|---| | `formatPrice(v, options?)` | Grouped fixed-precision number with an ASCII minus. | | `formatPercent(v, options?)` | Percentage formatting. | | `formatCompact(v, options?)` | `1.23M`, `4.56B`. | | `formatVolume(v)` | Volume shorthand. | | `precisionFromMinMove(minMove)` | Decimals implied by a minimum move. | | `precisionForRange(lo, hi, tickCount, minPrecision?)` | Decimals needed so neighboring tick labels differ. | | `NumberFormatOptions` | `precision`, `grouping`, `sign`. | | `formatBarTime(time, options)` | Bar label appropriate to the resolution. | | `formatClock(time, options)` | Time of day in a zone. | | `formatDate(time, options)` | Date in a zone. | | `TimeFormatOptions` | `timeZone`, `resolution`, `locale`. | ### Time Zones | Export | Description | |---|---| | `zonedParts(time, zone)` | Wall-clock parts of a timestamp in a zone. | | `ZonedParts` | Their shape. | | `tzOffsetMinutes(time, zone)` | Offset from UTC at that instant. | | `zonedTimeToUtc(parts, zone)` | Wall-clock parts back to a timestamp. | | `localWallToUtc(localMs, zone)` | A local wall-clock millisecond value back to UTC. | | `startOfZonedDay(time, zone)` | Midnight of that day in the zone. | | `minuteOfZonedDay(time, zone)` | Minutes since midnight in the zone. | ### Resolutions | Export | Description | |---|---| | `parseResolution(text)` | `"1"`, `"15"`, `"1D"`, `"1W"`, `"1M"` to a `Resolution`; throws on nonsense. | | `Resolution`, `ResolutionUnit` | The parsed form and its unit. | | `floorTimeToBar(time, res, zone?)` | Start of the bar containing a time, honoring `anchorMinute`. | | `nextBarTime(time, res, zone?)` | Start of the following bar. | | `sameBar(a, b, res, zone?)` | Whether two times share a bar. | | `anchoredResolution(res, session)` | A resolution phased from the session open. | | `intervalForUnit(unit)` | The `UtcInterval` a resolution's unit steps by. | ### Sessions | Export | Description | |---|---| | `parseSession(text)` | `"0930-1600"`, `"0400-0930,0930-1600"`, `"24x7"`, with an optional `:days` suffix. | | `Session`, `SessionSegment` | The parsed form. | | `mergeSegments(segments)` | Sort and merge touching or overlapping segments. | | `sessionMinutes(session)` | Total minutes in a session. | | `isInSession(time, session, zone)` | Membership, including overnight wraps. | | `sessionBoundsForDay(time, session, zone)` | `[start, end]` pairs for that day. | | `sessionMinuteAt(time, session, zone)` | Minutes elapsed in the session, `NaN` when closed. | ### Bars and Alignment | Export | Description | |---|---| | `ticksToBars(ticks, res, zone?)` | Trades to bars. | | `applyTick(last, tick, res, zone?)` | Fold one tick into the current bar; returns the bar and whether it appended. | | `aggregateBars(series, res, zone?)` | Coarser bars from finer ones, carrying aggressor volumes through. | | `heikinAshi(series)` | The Heikin-Ashi transform. | | `finiteRange(bars)` | Bar-index range whose close is finite. | | `priceExtent(bars, from, to)` | Min low and max high over an inclusive index range. | | `valueExtent(values, from, to)` | Min and max of a value column over an inclusive index range. | | `BarBuffer` | Growable OHLCV storage with amortized appends and zero-copy views. | | `alignToTimeline(masterTimes, series, options?)` | One value per master time, `NaN` where the rules give nothing. | | `alignTail(masterTimes, from, series, options?)` | The same for the tail only, which is the live path. | | `timesToIndices(masterTimes, times, barDurationMs)` | Fractional master indices for arbitrary times. | | `unionTimes(...columns)` | One sorted, de-duplicated timeline from several. | | `AlignMode`, `AlignOptions` | `"exact" \| "forwardFill" \| "nearest" \| "bucket"`, and `maxGapMs`. | ### Statistics and Search | Export | Description | |---|---| | `rollingSum`, `rollingMean`, `rollingStd`, `rollingMin`, `rollingMax` | Windowed statistics over a `Float64Array`. | | `ema(values, length)` | Exponential moving average. | | `wilderSmooth(values, length)` | Wilder's smoothing, for RSI and ATR. | | `pctChange`, `logReturns`, `cumulativeReturn`, `diff` | Return series. | | `lowerBound`, `upperBound` | Bisect on a sorted array. | | `nearestIndex(sorted, t)` | Closest index; ties go earlier. | | `indexRange(sorted, t0, t1)` | Inclusive index range within a value range. | ### Geometry | Export | Description | |---|---| | `traceLine(ctx, xs, ys, options?)` | Trace a polyline; `NaN` breaks the line into gaps. | | `traceArea(...)` | Trace a closed area between a series and a baseline. | | `crisp(px, lineWidth?, dpr?)` | Snap a coordinate so a one-pixel line is not blurred. | | `PathContext`, `CurveKind`, `PathOptions` | The minimal canvas path surface, `"linear" \| "step" \| "monotone" \| "smooth"`, and options. | ## `@ortex-charts/core` The canvas engine. ### Chart | Export | Description | |---|---| | `createChart(el, options?)` | Create a chart. | | `Chart` | The engine class; see the method groups below. | | `TimeScaleApi` | `chart.timeScale`: ranges, conversions and the range subscription. | | `CHART_DEFAULTS` | The full default `ChartOptions`. | | `ChartOptions` and the option interfaces | `PriceScaleOptions`, `TimeScaleOptions`, `CrosshairOptions`, `CrosshairLineOptions`, `CrosshairMode`, `GridOptions`, `LegendOptions`, `WatermarkOptions`. | `Chart` methods, grouped: - **Options and theme:** `options`, `applyOptions`, `theme`, `setTheme`, `resolution`. - **Panes:** `addPane`, `removePane`, `pane`, `panesList`, `resizePanes`, `setPaneCollapsed`, `isPaneCollapsed`, `maximizePane`, `restorePanes`, `maximizedPane`, `subscribePaneChange`. - **Scales:** `priceScale`, `zoomPriceScale`, `panPriceScale`, `resetPriceScale`, `valueAtPixel`. - **Series:** `addSeries`, `removeSeries`, `series`, `getSeries`, `mainSeries`, `setMainSeries`, `times`, `nextPaletteColor`, `subscribeDataChange`. - **Price lines:** `addPriceLine`, `removePriceLine`. - **Primitives:** `addPrimitive`, `removePrimitive`, `primitive`, `paneView`. - **Interaction and events:** `subscribeCrosshairMove`, `subscribeClick`, `subscribeDblClick`, `subscribeLegendMenu`, `setCrosshairPosition`, `clearCrosshairPosition`, `crosshairState`, `setCursor`. - **Geometry:** `paneRect`, `plotRect`, `zoneAt`. - **Lifecycle:** `resize`, `size`, `requestDraw`, `flush`, `takeScreenshot`, `remove`. `TimeScaleApi` methods: `fitContent`, `scrollToRealtime`, `scrollByPixels`, `zoom`, `setVisibleRange`, `setVisibleTimeRange`, `visibleRange`, `setBarSpacing`, `barSpacing`, `timeToCoordinate`, `coordinateToTime`, `indexToCoordinate`, `coordinateToIndex`, `timeToIndex`, `indexToTime`, `subscribeVisibleRangeChange`. ### Panes and Scales | Export | Description | |---|---| | `PaneModel` | A pane: canvases, legend element, scales and primitives. | | `PaneOptions` | `heightRatio`, `minHeight`, `collapsed`. | | `PriceScaleState` | One price scale in a pane; `applyOptions`, `zoomAt`, `panBy`, `resetAutoScale`, `currentPlotDomain`. | | `PRICE_SCALE_DEFAULTS` | Default price-scale options. | ### Series | Export | Description | |---|---| | `SeriesModel` | A series: data, options, alignment, price lines. | | `SeriesHost` | The interface a chart satisfies for a series. | | `registerSeriesType(name, def)` | Register a renderer so `addSeries(name, …)` works. | | `getSeriesType(name)` | Look one up; throws with the registered names. | | `SeriesTypeDef`, `SeriesView`, `SeriesKind` | The renderer contract. | | `SeriesTypeMap`, `BuiltInSeriesType` | Type-level map from a built-in name to its options. | | `SeriesOptionsBase`, `SeriesDataInput`, `SeriesValue` | Common options, accepted inputs, and the crosshair value shape. | | `LineSeriesOptions`, `AreaSeriesOptions`, `BaselineSeriesOptions`, `HistogramSeriesOptions`, `CandlestickSeriesOptions`, `BarSeriesOptions` | Per-type options. | | `pixelColumns(view, key)` | Pixel columns for the visible range, with one extra bar each side. | | `drawDecimated(...)` | Draw a dense line as one minimum and one maximum per pixel column. | | `bodyWidth(barSpacing, ratio)` | Candle body width that never exceeds the gap. | | `ohlcValueAt(columns, index)` | OHLCV at an index, or null. | `SeriesModel` methods: `setData`, `update`, `updateNow`, `applyOptions`, `setType`, `valueAt`, `primaryValue`, `lastIndex`, `holdUpdates`, `releaseUpdates`; properties `id`, `type`, `kind`, `options`, `columns`, `raw`, `rawTimes`, `rawLength`, `holding`, `priceLines`, `optionsChanged`. ### Primitives, Drawing and Layout | Export | Description | |---|---| | `PanePrimitive`, `PaneView`, `PrimitiveLayer`, `PrimitivePointerEvent` | The primitive contract. | | `PriceLineOptions` | A horizontal line at a value with an axis label. | | `CanvasLayer` | A device-pixel-ratio aware canvas. | | `FrameScheduler` | Coalesces draw requests onto one animation frame. | | `applyLineStyle`, `hline`, `vline`, `roundRect`, `measureText`, `drawLabel`, `drawText` | Canvas helpers a primitive can reuse. | | `LabelStyle` | Options for `drawLabel`. | | `inRect(rect, x, y)`, `Point`, `Rect` | Geometry helpers. | ### Theme and Formatting | Export | Description | |---|---| | `darkTheme`, `lightTheme` | The two built-in token sets. | | `Theme` | The token interface. | | `resolveTheme(theme)` | `"dark"`, `"light"` or an object to an object. | | `parseColor(color)` | Any accepted color string to `[r, g, b, a]`. | | `withAlpha(color, alpha)` | The same color at a new alpha. | | `contrastText(background)` | Black or white, whichever reads. | | `formatValue(value, priceFormat)` | Exactly what the chart would print for a series value. | | `formatAxisValue(value, format, lo, hi, tickCount)` | The same for an axis tick, adding decimals on a narrow range. | | `PriceFormat` | `type`, `precision`, `minMove`, optional `formatter`. | ### Events, Utilities and Licensing | Export | Description | |---|---| | `Emitter` | The minimal event emitter the library uses. | | `Listener`, `DeepPartial` | Its listener type, and the recursive partial used by every options argument. | | `merge(base, patch)` | Deep merge used by `applyOptions`. | | `uid(prefix)` | Unique id generator. | | `CrosshairEvent`, `VisibleRangeEvent`, `LegendMenuEvent`, `HitResult` | Event payloads. | | `LineStyle`, `PriceScalePosition` | `"solid" \| "dashed" \| "dotted"`, `"left" \| "right" \| "overlay"`. | | `setLicenseKey(key, options?)` | Install a license key; resolves with the final status. | | `licenseStatus()` | The current `LicenseStatus`. | | `hasFeature(name)` | Whether the current license grants a feature. | | `onLicenseChange(cb)` | Subscribe to status changes. | | `clearLicense()` | Reset to unlicensed, mostly for tests. | | `domainAllowed(hostname, domains)` | The domain-matching rule, including the localhost exemption. | | `ORTEX_CHARTS_PUBLIC_KEY` | The embedded Ed25519 verification key. | | `LicenseState`, `LicensePayload`, `LicenseStatus`, `SetLicenseOptions` | Licensing types. | | `Branding`, `BRANDING_DEFAULTS`, `BrandingOptions`, `BrandingPosition` | The mark and its options. | | `ortexLogoSvg`, `ortexLogoDataUri`, `ORTEX_LOGO_ASPECT` | The wordmark as markup, as a data URI, and its aspect ratio. | ## `@ortex-charts/financial` Everything in `core`, plus the following. ### The Financial Chart ```ts function createFinancialChart(container: HTMLElement, options?: FinancialChartOptions): FinancialChart; ``` | Member | Description | |---|---| | `chart`, `main`, `volume` | The core chart, the main series, and the volume series or null. | | `setData(bars)`, `update(bar)` | Data in. | | `setSeriesType(type, options?)`, `seriesType()` | Switch the main series style without losing state. | | `addIndicator(id, inputs?, options?)` | Add an indicator. | | `setMarkers(markers)` | Trade-style marks; returns the primitive. | | `setEvents(events)` | Time-anchored annotations; returns the primitive. | | `setFootprint(flow, options?)` | Order-flow cells, and exact aggressor volumes on the bars. | | `addVolumeProfile(options?)` | A volume or market profile on the main pane. | | `replay(options?)` | The bar-replay controller, created on first use. | | `alerts(options?)` | The price-alerts primitive, created on first use. | | `live(source, options?)` | Stream a live source into the main series. | | `connect(wsOptions, streamOptions?)` | Open a WebSocket and stream it in. | | `bind(feed, options)` | Bind a datafeed. | | `remove()` | Dispose. | `MainSeriesType` is `"candlestick" \| "bar" \| "line" \| "area" \| "baseline"`. ### Data | Export | Description | |---|---| | `bindDatafeed(chart, series, feed, options)` | Initial load, lazy history, realtime subscription, symbol and resolution switching. | | `Datafeed`, `BarsRequest`, `BarsResponse`, `SymbolInfo`, `BindOptions`, `DatafeedBinding` | The datafeed contract. | | `WebSocketSource`, `websocketSource(options)` | A reconnecting WebSocket that parses messages into items. | | `ManualSource` | A source you push into. | | `pollingSource(fetchNext, intervalMs)` | Poll an async function. | | `LiveSource`, `SocketStatus`, `WebSocketSourceOptions` | Source types. | | `streamTo(series, source, options?)` | Fold live items into a series; what `live` calls. | | `StreamOptions`, `LiveItem` | Its options and the accepted item shapes. | ### Adapters | Export | Description | |---|---| | `createAdapter(spec, deps?)` | Build a datafeed and live sources from a spec; throws on an invalid one. | | `validateAdapterSpec(spec)` | Structural checks with actionable messages. | | `Adapter`, `AdapterDeps` | The result and its injectable dependencies. | | `AdapterError` | Thrown with the list of problems. | | `ortexAdapterSpec(options)`, `OrtexAdapterOptions` | The preset for the ORTEX API. | | `AdapterSpec`, `HistorySpec`, `LiveSpec`, `SymbolSpec`, `SearchSpec`, `MessageSpec`, `RecordSpec`, `RecordsSpec`, `RecordKind`, `FieldSpec`, `FieldRef`, `TimeFieldSpec`, `TimeUnit`, `Condition`, `JsonTemplate`, `DateParamSpec` | The spec vocabulary. | | `getPath`, `readField`, `readNumber`, `parseTime`, `formatTimeParam` | Field and time readers, exported so a host can reuse them. | | `fillTemplate`, `fillJsonTemplate`, `matches` | Template filling and condition matching. | | `mapRecord`, `mapRecords`, `explainMapping` | Mapping, and why a sample produced nothing. | | `MappedRecord`, `TemplateContext` | Their types. | ### Indicators | Export | Description | |---|---| | `addIndicator(chart, id, inputs?, options?)` | Add an indicator to any chart. | | `registerIndicator(def)` | Register your own. | | `getIndicator(id)` | Look one up; throws with the registered ids. | | `listIndicators()` | Every registered definition. | | `indicators` | The built-in library namespace, including `BUILT_IN_INDICATORS` and `barDelta`. | | `sourceColumn(columns, source)` | Resolve `close`, `hl2`, `hlc3`, `ohlc4` and the rest. | | `IndicatorDef`, `IndicatorInput`, `IndicatorOutput`, `IndicatorContext`, `IndicatorInputType`, `AddIndicatorOptions`, `IndicatorInstance` | The indicator contract. | ### Markers, Events, Drawings | Export | Description | |---|---| | `addSeriesMarkers(chart, series, markers)`, `SeriesMarkersPrimitive` | Trade-style marks. | | `SeriesMarker`, `MarkerShape`, `MarkerPosition` | Their types. | | `addEvents(chart, series, events, options?)`, `EventsPrimitive` | Lanes, badges, callouts, lines and ranges. | | `ChartEvent`, `EventStyle`, `EventsOptions` | Their types. | | `createDrawingTools(chart, options?)`, `DrawingsPrimitive` | Drawing tools on a pane. | | `Drawing`, `DrawingKind`, `DrawingPoint`, `DrawingStyle`, `DrawingChange`, `DrawingToolsOptions` | Their types. | | `DRAWING_STYLE_DEFAULTS`, `POINTS_REQUIRED` | Default style, and points needed per tool. | ### Order Flow | Export | Description | |---|---| | `addFootprint(chart, series, flow?, options?)`, `FootprintPrimitive` | Footprint cells and the fallback delta strip. | | `footprintFromTicks(ticks, res, tickSize, zone?)` | Build footprints from sided ticks. | | `mergeFootprint(bars, flow)` | Copy aggressor totals onto bars. | | `footprintTotals(bar)` | Buy, sell, total, delta and the bar point of control. | | `footprintIndex(times, bar)` | The bar index a footprint belongs to. | | `addVolumeProfile(chart, series, options?)`, `VolumeProfilePrimitive` | Volume and market profiles. | | `computeProfile(cols, from, to, options?, flow?)` | The pure function behind them. | | `FootprintBar`, `FootprintLevel`, `FootprintTotals`, `FootprintOptions` | Footprint types. | | `Profile`, `ProfileRow`, `ProfileKind`, `ProfileMode`, `ProfileColumns`, `VolumeProfileOptions` | Profile types. | ### Replay, Linking, Alerts | Export | Description | |---|---| | `createReplay(target, options?)`, `ReplayController` | Bar replay with higher-timeframe followers. | | `ReplayState`, `ReplayOptions`, `ReplayTarget` | Its types. | | `linkCharts(charts, options?)`, `LinkOptions` | Synchronize time range and crosshair across charts. | | `addAlerts(chart, series, options?)`, `AlertsPrimitive` | Draggable price alerts with crossing events. | | `Alert`, `AlertEvent`, `AlertsOptions` | Their types. | ## `@ortex-charts/lite` | Export | Description | |---|---| | `createSparkline(el, options?)`, `Sparkline` | The 3.2 KB table-cell chart. | | `SPARKLINE_DEFAULTS` | Default options. | | `SparklineOptions`, `SparklineKind`, `SparklineData` | Its types. | | `renderScheduler` | The animation frame shared by every sparkline on the page. | ## `@ortex-charts/ui` | Export | Description | |---|---| | `createChartShell(el, options?)` | Toolbar, drawing rail and chart in one call. | | `ChartShell`, `ChartShellOptions` | The handle and its options. | | `ChartLayout`, `LayoutIndicator`, `SeriesPlacement`, `ScaleMode` | The layout JSON a host stores. | | `OverlayDef`, `OverlayContext` | Host-defined data overlays. | | `overlayKey(id, symbol, current)`, `parseOverlayKey(key, current)` | The `id@SYMBOL` convention for compared symbols. | | `ToolbarItem`, `ToolbarAction` | The switches, and host buttons and menus. | | `DEFAULT_RESOLUTIONS` | `["1", "5", "15", "60", "1D", "1W", "1M"]`. | | `resolutionLabel(res)` | `"60"` to `"1h"`, `"1D"` to `"D"`. | | `DrawingsStore`, `localStorageDrawingsStore(prefix?)` | Drawings that follow the symbol. | | `openDataPicker(options)`, `DataPickerOptions`, `PickerItem` | The "Add data to chart" modal. | | `openMenu(root, anchor, build, onClose?)`, `Menu`, `MenuItemSpec` | Keyboard-navigable dropdowns. | | `openDialog(root, options)`, `DialogOptions`, `FieldSpec`, `FieldValue` | Modal forms with a focus trap. | | `openSymbolSearch(options)`, `SymbolSearchOptions` | The debounced symbol search popover. | | `ICONS`, `icon(name)`, `IconName` | 52 inline SVG icons using `currentColor`. | | `UI_CSS`, `ensureStyles(doc)`, `applyThemeVars(el, theme)` | The chrome stylesheet and its theme variables. | | `h(doc, tag, attrs?)`, `button(spec)`, `ButtonSpec` | DOM helpers used by the toolbar. | | `downloadCanvas(canvas, name)`, `downloadText(text, name)` | Browser download helpers. | ## `@ortex-charts/viz` | Export | Description | |---|---| | `createTreemap(el, options?)`, `Treemap`, `TREEMAP_DEFAULTS` | Sized-and-colored rectangles with drill-down. | | `TreemapNode`, `TreemapLayoutNode`, `TreemapLayoutOptions`, `TreemapOptions`, `TreemapHit` | Its types. | | `layoutTreemap(...)`, `treemapLeaves(node)`, `findPath(root, node)` | The layout without the canvas. | | `createHeatmap(el, options?)`, `Heatmap`, `HEATMAP_DEFAULTS` | A row-by-column grid with a color scale. | | `HeatmapOptions`, `HeatmapScaleOptions`, `HeatmapHit` | Its types. | | `resolveHeatmapDomain(...)`, `createColorScale(...)` | The scale, for host-drawn legends and table cells. | | `createCategoryChart(el, options?)`, `CategoryChart`, `CATEGORY_CHART_DEFAULTS` | Bars and columns, stacked or grouped. | | `CategoryChartOptions`, `CategorySeries`, `CategoryValueAxisOptions`, `CategoryAxisOptions`, `CategoryHit` | Its types. | | `sortedCategoryOrder(...)`, `categoryExtent(...)` | Ordering and extent helpers. | | `VizComponent`, `VizBaseOptions`, `ThemeInput` | The shared component base. | | `Tooltip`, `TooltipContent`, `TooltipRow` | The DOM tooltip the components use. | | `formatValue(value, format)`, `ValueFormat` | Viz number formatting. | | `niceTicks(...)`, `niceDomain(...)`, `thinLabels(...)`, `thinnedIndices(...)`, `fitText(...)`, `LabelThinning` | Axis and label helpers. | | `blend(a, b, t)` | Color interpolation. | | `canvasToPNG(canvas, options?)`, `downloadPNG(...)`, `downloadBlob(...)` | Image export. | | `rowsToCSV(rows, columns?, options?)`, `downloadCSV(...)`, `CSVColumn`, `CSVOptions` | CSV export. | | `composeCanvases(canvases, background?, options?)`, `ComposeOptions` | Stack canvases into one image. | | `renderScheduler` | The shared animation frame. | | `VIZ_VERSION` | The package version string. | ## `@ortex-charts/react` | Export | Description | |---|---| | `Chart`, `ChartProps` | The core chart in a div; children attach to it. | | `Pane`, `PaneProps` | An extra pane; provides its id to nested series. | | `Series`, `SeriesProps`, `SeriesOptionsFor` | A series on the enclosing chart and pane. | | `FinancialChart`, `FinancialChartProps`, `IndicatorSpec` | The financial chart with data, indicators, markers and a live source as props. | | `ChartShell`, `ChartShellProps` | The full shell, with controlled symbol, resolution, theme and layout. | | `Sparkline`, `SparklineProps` | A sparkline in an inline-block span. | | `useChart()`, `usePaneId()` | Context accessors for your own children. | | `ChartContext`, `PaneContext` | The contexts themselves. | | `useChartEvents(chart, props)`, `ChartEventProps` | Subscribe a chart to the four event props. | | `useLiveSource(factory, deps)` | A `LiveSource` that survives re-renders and is closed once. | The package also re-exports the types callers need alongside the components: `ChartHandle`, `ChartOptions`, `DeepPartial`, `SeriesModel`, `SeriesDataInput`, `SeriesOptionsBase`, `SeriesTypeMap`, `CrosshairEvent`, `VisibleRangeEvent`, `FinancialChartHandle`, `FinancialChartOptions`, `SeriesMarker`, `LiveSource`, `LiveItem`, `StreamOptions`, `AddIndicatorOptions`, `IndicatorInstance`, `SparklineHandle`, `SparklineOptions`, `SparklineData`, `ChartShellHandle`, `ChartShellOptions`, `ChartLayout`, `OverlayDef` and `Bar`. ## Registries You Can Extend Three registries decide what the library can draw, and all three are open. ```ts registerSeriesType(name, def); // core — a new series renderer registerIndicator(def); // financial — a new indicator chart.addPrimitive(primitive); // core — anything else drawn on a pane ``` Everything built in goes through these same three, so a series type, indicator or primitive you register is not a second-class citizen: it appears in the legend, the toolbar menus and saved layouts exactly as the built-ins do. See [Series types](https://ortexcharts.com/docs/series), [Indicators](https://ortexcharts.com/docs/indicators) and [Concepts](https://ortexcharts.com/docs/concepts). --- # Questions we are asked > Support, browsers, mobile, accessibility, data licensing and the awkward ones. Section: Commercial. Source: https://ortexcharts.com/docs/faq ## What This Is ### Is ORTEX Charts the Same Thing as ORTEX? No. ORTEX is a market-data and short-interest platform sold to investors at ortex.com, priced per user per month. ORTEX Charts is a JavaScript charting library sold to developers at ortexcharts.com, priced per developer or per product. The same company makes both. They are separate products with separate prices, separate licenses and separate customers: buying one does not include the other, and using ORTEX Charts does not require an ORTEX data subscription. ### Is It Built on TradingView, Lightweight Charts or Highcharts? No. ORTEX Charts is an independent rendering engine built from the ground up in TypeScript. It has no third-party runtime dependencies, and no line of it is copied, ported or adapted from any other library: no code, no icons, no styles. The algorithms it implements come from the published literature and are cited where they are used. It shares nothing with TradingView Lightweight Charts, TradingView Advanced Charts, Highcharts or ChartIQ, and is compared with all four on this site because those are the products a buyer is choosing between, not because it is built on any of them. ## Browsers and Platforms ### Which Browsers Are Supported? Current versions of Chrome, Edge, Safari and Firefox, on desktop and mobile. The requirements are Canvas 2D, `ResizeObserver`, Pointer Events, `Intl` and ECMAScript 2020 syntax, all of which have been available in every major browser since 2020. There is no Internet Explorer support and none is possible without a compilation step you would have to add yourself. One caveat worth stating: the automated test suite exercises **Chromium only** — the desktop pane, headless, and phone emulation. Safari and Firefox are expected to work and have not been put through the visual suite. If you find a rendering difference on either, report it; it is a bug rather than a limitation. ### Does It Work on Mobile and With Touch? Yes, and touch handling is in the engine rather than in the chrome, so it works on a bare `createFinancialChart` as well as in the shell. Drag pans, pinch zooms, a long press raises the crosshair, and a fast flick has momentum. The shell adapts below 640 pixels: the toolbar drops its labels, the resolution strip scrolls horizontally, the drawing rail becomes a horizontal bar under the chart, and coarse pointers get 32-pixel targets. Full screen uses the Fullscreen API where it exists and a fixed-position layer where it does not, which is the case on iOS Safari. ### Is There a React Native or Native Mobile SDK? No, and there is not going to be one. The honest answer for a mobile application is the web build inside a WebView — Capacitor, React Native WebView or the platform equivalent — which is the same code, the same license key and the same behavior. ### Does It Work in Vue, Svelte or Angular? Yes. The engine has no framework dependency at all; it takes an element and returns an object. The only framework-specific package is `@ortex-charts/react`, and it exists because ORTEX needed it, not because the core requires anything. A binding in any framework is the same three steps: create the chart in a mount effect, call `applyOptions` or `setData` when props change, and call `remove` on unmount. Reading [the React source](https://ortexcharts.com/docs/react) is a reasonable template. ## Rendering and Size ### How Big Is It? 83.2 KB gzipped for the full chart with the toolbar, drawings, indicators and order flow; 56 KB for candles and volume, 30 KB for a line chart, and 3.2 KB for the sparkline build. All in one file each, with no chunk loading at runtime. See [Performance](https://ortexcharts.com/docs/performance) for how those are measured and how they compare. ### Is It Canvas or WebGL? Canvas 2D. That is a deliberate choice and it has a real ceiling. Canvas 2D with typed-array columns, visible-range-only work and decimation holds 60 Hz to a few hundred thousand visible points, which covers every normal financial size: a decade of daily bars is 2,500 rows, and a decade of minute bars is about a million rows of which only the visible window is drawn. What it does not do is millions of points **all on screen at once** without decimation. If that is your product, SciChart's WebGL renderer is the right tool today. A WebGL path behind the same API is on the roadmap and is not implemented. ### Why Is My Chart Blank? Almost always one of three things: the container has no height, the timestamps are in seconds rather than milliseconds, or the chart was created during a server-side render. See the common problems in [Quickstart](https://ortexcharts.com/docs/quickstart). ## Data and Privacy ### Is Any Data Sent Anywhere? No. There is no analytics call, no usage beacon, no license check over the network and no hidden call-home in the library. The license key is verified locally with WebCrypto against a public key embedded in the bundle. Your bars, your symbols, your users and your layouts never leave the browser through anything the library does. Two things are disclosed rather than hidden, and both are in the contract: - **The hosted kit's CDN logs.** If you load the library from `cdn.ortexcharts.com`, that CDN sees the requests, which means which domain loaded which kit and when. It is a standard web server log with no user data in it. Registry customers who bundle the packages themselves do not touch ORTEX infrastructure at all. - **An optional license check-in.** The agreement describes a documented check-in carrying `{ keyId, hostname, version }` once per session, which enterprise customers may switch off. It is disclosed because a hidden one would be blocked by ad blockers and content security policies, would be a privacy liability, and would be the first thing a copier deleted. Nothing about it is covert. There is no third position. If a privacy review asks "does this library phone home", the answer is no, and the two items above are the complete disclosure. ### Do I Need a Data Subscription From ORTEX? No. The library consumes data; it does not supply it. There is no bundled feed and no requirement to buy one. The [ORTEX adapter preset](https://ortexcharts.com/docs/data) exists because ORTEX is a customer of its own library, not because you have to be. ### Can It Fetch Data Itself? Only through what you give it. A [datafeed](https://ortexcharts.com/docs/data) is your code. An [adapter spec](https://ortexcharts.com/docs/data) tells the library which URLs to call and how to read the responses, using the browser `fetch` and `WebSocket` you would have used yourself. Nothing is requested that your spec did not name. ### Where Are Layouts and Drawings Stored? Wherever you put them. `getLayout()` returns JSON, `getDrawings()` returns an array, and `list()` on the alerts primitive returns a list. There is no ORTEX storage service and no save-and-load server contract. See [The shell and layouts](https://ortexcharts.com/docs/shell). ## Licensing ### What Happens When a License Expires? The [ORTEX mark](https://ortexcharts.com/docs/branding) comes back and one warning is written to the console. Nothing else changes: the chart draws, the data updates, every feature works. Keys carry a 14-day grace period beyond the subscription period end, and a renewal re-signs onto the same key id and the same kit URL, so nothing in your deployment changes from year to year. ### Does the Chart Ever Stop Working? No. There is no state — no key, invalid key, expired key, wrong domain, revoked key — in which the library refuses to draw, degrades the data or disables a feature. A chart that broke over a licensing problem on a Friday night would be a support ticket for both of us; a branded one is a sales lead. ### Is the License Key a Secret? No. It ships to the browser, it is bound to your domains, and anyone can read it out of your bundle. Treat it like a publishable API key. What **is** secret is the npm registry token in your `.npmrc`, which grants package downloads and belongs in your continuous-integration secrets. ### Do I Need a License to Evaluate It? No. The library is fully functional unlicensed; the only difference is the mark. `localhost` is always allowed on every key, so local development never fails a domain check either. ### Can I Remove the ORTEX Mark? Only with a license that grants the `whitelabel` feature, which is the White-label plan and above. Setting `branding.visible = false` without it is ignored and logs one line. See [The ORTEX mark](https://ortexcharts.com/docs/branding) and [/pricing](https://ortexcharts.com/pricing). ### Is It Open Source? No. It is a commercial library. Source access exists under OEM and enterprise agreements; see [Installation and delivery](https://ortexcharts.com/docs/install). It has no third-party runtime dependencies and contains no third-party code: every package installs with an empty dependency tree, and the source drop is entirely ORTEX's own. ### Can I Redistribute It Inside My Own Product? Embedding it in an application you sell is what the Team and White-label plans are for. Redistributing the library itself — shipping it as a component your customers then build with — is an OEM matter, because it changes who the licensee is. Ask. ## Features and Limitations ### How Many Indicators Are There? 51, as of version 0.2.0. That is comparable to Highcharts Stock, which ships about 50, and short of TradingView Advanced Charts, which ships about 110 — the long tail there is mostly variations on what is here. Registering your own is about 20 lines and it then behaves exactly like a built-in one. See [Indicators](https://ortexcharts.com/docs/indicators). ### Is There Pine Script or Another Indicator Language? No, and there is no plan for one. A scripting language is a decade of work and a permanent support burden. Indicators here are TypeScript, which is strictly more capable — and strictly less safe for end-user authoring, which is the trade-off. If letting your users write their own indicators is a product requirement, this library does not solve it. ### How Many Drawing Tools Are There? 32, as of version 0.2.0: trend lines and rays, arrows, a polyline and a freehand brush, rectangles, ellipses and triangles, parallel and regression channels, six Fibonacci tools, a Gann fan, a pitchfork, measure and range tools, long and short position tools with a reward-to-risk readout, and four annotation tools. Every one of them has magnet snapping, handle editing, undo, redo and JSON serialization. The shell's tool rail shows nine of them until you ask for more, because the full set crowds a rail, and `drawingTools` names the ones you want. See [Drawing tools](https://ortexcharts.com/docs/drawings). ### Can I Show Several Charts in a Grid? Yes, but the grid is your CSS. `linkCharts` synchronizes the visible time range and the crosshair across any number of charts, across resolutions, and a bar replay can drive higher-timeframe followers. There is no built-in two-by-two layout container. See [Replay, linking and alerts](https://ortexcharts.com/docs/replay). ### Is There a Navigator Strip or Range Buttons? No. Range presets are a few lines on `setVisibleTimeRange` and fit naturally as [toolbar actions](https://ortexcharts.com/docs/toolbar); there is an example on [Time, sessions and resolutions](https://ortexcharts.com/docs/time). A navigator mini-chart is not built in. ### Can the Chart Be Rendered on a Server? No. The library is browser-only: it creates canvases, reads `devicePixelRatio` and observes element sizes. There is no headless rendering mode. Chart images for emails and social cards are better solved by a separate rendering service. In React this means creating the chart inside an effect, which the components already do; the file still needs `"use client"` in Next.js. See [Installation and delivery](https://ortexcharts.com/docs/install). ### Is It Accessible? Partially, and the gaps are worth knowing before you commit. Implemented: the chart is focusable and responds to arrow keys, `+`, `-` and Home; toolbar buttons carry `aria-pressed`; menus are `role="menu"` with arrow-key navigation, `aria-checked` on toggles and focus return; dialogs are `role="dialog"` with `aria-modal` and a focus trap; icon-only buttons have accessible names; reduced motion is respected. Not implemented: there is no screen-reader description of the chart data, no focusable data table as an alternative representation, and no high-contrast theme shipped. If conformance is a procurement requirement, weigh those; Highcharts Stock ships an accessibility module and this library does not. The CSV export plus a table of your own is the honest interim answer. ### Is the Interface Available in Other Languages? Dates and numbers localize through `Intl` and follow the chart's `locale` option. The interface strings — toolbar labels, menu entries, dialog titles — are English only, and there is no string table to supply. A host that needs another language today switches the toolbar off and drives the chart from its own controls, which are all public methods. ### Does It Do Order Entry, Positions or Depth of Market? No. Price lines and [draggable alerts](https://ortexcharts.com/docs/replay) are as close as it gets. Order and position lines with drag handles are a small primitive on a documented interface and would be built if a customer needed them; a broker integration is deliberately out of scope. ## Support and Process ### How Does Support Work? By email, at [charts@ortex.com](mailto:charts@ortex.com). Team plans include email support; White-label includes priority support; OEM agreements name an engineer and carry a service-level agreement. Commercial questions go to [sales@ortex.com](mailto:sales@ortex.com). When reporting a rendering problem, `window.__ORTEX_CHARTS_KIT__` on a hosted kit carries the kit id, key id, plan and library version, which is the fastest thing to quote. ### How Often Is It Released? Within a major version, hosted kits pick up new releases automatically the next time the file is fetched; registry customers upgrade on their own schedule. A subscription buys the versions released while it runs, and under the White-label plan the last version released during the term keeps working with its key indefinitely. ### Can I Get the Source? Under an OEM or enterprise agreement, yes: read access to a repository mirror with the TypeScript source, the tests and the playground. Outside that, published packages are minified with no source maps. ### Something Is Missing That I Need. What Now? Ask. The extension points — series types, indicators, primitives — are the same ones the built-ins use, so a lot of what looks missing is a day of work rather than an architectural change. Where the answer is genuinely no — Pine Script, native mobile, a broker integration — this documentation says so rather than implying a date. --- # Release notes > Every release, newest first, with the one upgrade note there has been so far. Section: Commercial. Source: https://ortexcharts.com/docs/changelog ## How Versions Work Every release of ORTEX Charts is listed below, newest first. Versions are published together: all eight packages share one number, so `@ortex-charts/ui@0.5.0` always works with `@ortex-charts/core@0.5.0` and you never have to reason about a matrix. The version comes from a signed tag in the library repository, which is also what triggers the publish, so the registry and the history cannot disagree. Anything that would change the behavior of code you have already written is called out under **Upgrading**. There has been one such change so far, in 0.3.2. ## 0.5.1 — 5 September 2026 **No third-party code of any kind.** 0.5.0 removed the last dependency but three files were still ports of D3 code under the ISC license. They are now independent implementations written from the published algorithms — Heckbert's nice numbers for axis ticks, Steffen's monotone interpolation and the uniform cubic B-spline for the curves, and Bruls, Huizing and van Wijk's squarified treemap — held to exactly the output the ports produced. Nothing in any package is copied, ported or adapted from another library. They are also faster. Filling an area through 100,000 points takes a quarter to a third less time across all four curves, the monotone line a fifth less, and the treemap layout no longer allocates per row. ## 0.5.0 — 5 September 2026 **No third-party runtime dependencies.** Every package now installs with an empty dependency tree. The D3 modules the library was built on were replaced by local ones: the tick algorithm and binary search that came from `d3-array`, the four curve interpolators from `d3-shape`, the UTC intervals from `d3-time`, the squarified treemap from `d3-hierarchy` and the color ramp from `d3-scale`. Each replacement was checked against the module it replaced while both were installed, and D3's own output is kept in the repository as a golden fixture, so the parity is asserted on every test run. The browser snapshot suite moved zero pixels. | Build | 0.4.0 | 0.5.0 | |---|---|---| | Line chart | 31.3 KB | **29.5 KB** | | Basic, the free build | 31.8 KB | **30.0 KB** | | Candles and volume | 58.2 KB | **55.8 KB** | | Full shell | 85.7 KB | **83.2 KB** | | Treemap and heatmap | 18.5 KB | **10.0 KB** | Drawing got faster as well. The D3 line and area generators wanted an array to iterate and an accessor function per column, so every redraw of every line built an index array and made two calls per point; the local curves read the typed arrays directly. Tracing 100,000 points takes 0.83 ms rather than 1.35 ms for a line, and 1.94 ms rather than 2.80 ms for an area. ## 0.4.0 — 5 September 2026 **Extended-hours shading and day breaks.** An intraday chart can now shade the columns outside regular trading hours and draw a line where each trading day begins, both off by default and both taking the exchange's day rather than the reader's. See [Time, sessions and resolutions](https://ortexcharts.com/docs/time). Number and date formatting moved onto the platform's own `Intl`, which removed another dependency and made locale-aware formatting the default rather than an option. ## 0.3.2 — 4 September 2026 **The price transforms register on demand.** Renko, Kagi, Point and Figure and Line Break used to register themselves when `@ortex-charts/core` was imported, which meant every build carried all four renderers whether or not anything drew them. They now register when `@ortex-charts/financial` is imported, which takes about 4 KB gzip off any build that does not use them, and keeps four paid renderers out of the free build. **Upgrading.** If you import only `@ortex-charts/core` and call `addSeries("renko")`, call `registerPriceTransforms()` first: ```ts import { registerPriceTransforms } from "@ortex-charts/core"; registerPriceTransforms(); ``` Nothing else changes. `@ortex-charts/financial`, `@ortex-charts/ui` and `@ortex-charts/react` do it for you, which covers almost every application. ## 0.3.1 — 4 September 2026 Publishes `@ortex-charts/basic`, which 0.3.0 built and did not send to the registry. ## 0.3.0 — 4 September 2026 **`@ortex-charts/basic`, the free build.** A candlestick chart with a volume pane, a legend and a crosshair, with no license key and a larger ORTEX mark. See [the ORTEX mark](https://ortexcharts.com/docs/branding). **Color-blind safe themes.** A dark and a light theme built on the Okabe-Ito palette, where up and down are told apart by hue and luminance rather than by red against green, plus a toolbar switch that turns them on. See [Colors, fonts and formatting](https://ortexcharts.com/docs/customization). **License key revocation and signing-key rotation.** Keys carry the identifier of the key that signed them, the runtime holds a revocation list, and a signing key can be retired without invalidating every key ever issued. See [License keys](https://ortexcharts.com/docs/licensing). ## 0.2.0 — 4 September 2026 **51 built-in indicators and 32 drawing tools**, up from 22 and nine, and the four price transforms: Renko, Kagi, Point and Figure and Line Break. See [Indicators](https://ortexcharts.com/docs/indicators) and [Drawing tools](https://ortexcharts.com/docs/drawings). ## 0.1.0 The founding release: the engine, the financial chart, the shell, the sparkline build, the general-purpose charts and the React components.