Commercial
Moving from Highcharts
One real Highcharts Stock chart ported end to end: what the translator does and what is left by hand.
Read this page as Markdown, or take the whole documentation in one file.
The Chart Every Research Product Has
This page ports one real Highcharts Stock chart, the one nearly every screener and research
product has: candles, volume on a second y-axis, two moving averages as indicator series,
earnings flags, a target line, a range selector, a navigator and the exporting module, with a
page-wide theme from Highcharts.setOptions and live ticks through addPoint. The options
object below is the one the port starts from; the API mapping is the
concept-by-concept table to keep open beside it.
const options: Highcharts.Options = {
chart: { height: 520 },
title: { text: "IBM · NYSE" },
rangeSelector: { selected: 4, buttons: [{ type: "month", count: 1, text: "1m" }, { type: "year", count: 1, text: "1y" }, { type: "all", text: "All" }] },
navigator: { enabled: true },
exporting: { enabled: true },
yAxis: [
{ title: { text: "Price" }, height: "65%", opposite: true, plotLines: [{ value: 250, color: "#ff9800", dashStyle: "ShortDash", label: { text: "Target 250" } }] },
{ title: { text: "Volume" }, top: "70%", height: "30%", offset: 0, opposite: true },
],
plotOptions: { candlestick: { color: "#ef5350", upColor: "#26a69a" } },
series: [
{ type: "candlestick", id: "ibm", name: "IBM", data: ohlc }, // [x, o, h, l, c]
{ type: "column", id: "vol", name: "Volume", data: volume, yAxis: 1 }, // [x, v]
{ type: "sma", linkedTo: "ibm", name: "SMA 20", params: { period: 20 }, color: "#ffb74d" },
{ type: "ema", linkedTo: "ibm", name: "EMA 50", params: { period: 50 }, color: "#4fc3f7" },
{ type: "flags", onSeries: "ibm", name: "Earnings", shape: "circlepin", data: flags }, // { x, title, text }
],
};
Highcharts.stockChart("chart", options);
Timestamps need no conversion: Highcharts and ORTEX Charts both use milliseconds since the Unix epoch.
Step One: The Translator
npm install @ortex-charts/migrate
import { fromHighcharts } from "@ortex-charts/migrate";
const { chart, series, indicators, markers, unsupported } = fromHighcharts(el, options, { theme: "dark" });
console.info("no counterpart:", unsupported);
options is the typed Highcharts.Options from above, passed as it is; the translator's
input types accept it without a cast. One call produces:
| In the options | On the chart |
|---|---|
candlestick with [x, o, h, l, c] rows |
The main series, on the right scale of the first pane. |
yAxis[1] with top: "70%" and height: "30%" |
A second pane, 30/65 the height of the first. |
The column on yAxis: 1 |
The volume, in that pane, colored by the candle's direction and formatted as volume. |
sma and ema with params.period |
addIndicator("sma", { length: 20 }) and addIndicator("ema", { length: 50 }), in the series' colors. |
flags with { x, title, text } |
Markers above the bar: the title as the text, the text as the tooltip, circlepin as a circle. |
plotLines on yAxis[0] |
One price line at 250, dashed, labelled, on the main series. |
title.text |
The watermark. |
chart.height |
The chart height. |
And unsupported reads:
exporting (the shell offers the equivalent)
navigator (the shell offers the equivalent)
rangeSelector (the shell offers the equivalent)
Nothing else is lost, and nothing is emitted for what was: a series the translator cannot draw leaves no empty legend row behind.
translateHighcharts(options) returns the same translation without creating anything:
chart, panes, series, indicators, markers and unsupported, for a test or for a
port that builds the chart itself, as the next step does.
Step Two: What Is Left by Hand
Three items in unsupported, and two that never appear in a Highcharts options object.
The range selector and the navigator. These are the shell's period buttons.
createChartShell takes everything createFinancialChart takes, so the translation can be
poured into it. The buttons default to a day, a week, a month, a quarter, a year, five years
and ten years; ranges sets your own. See the toolbar.
Exporting. There is no export server. The shell's screenshot button and CSV download
cover the two things the module was used for; downloadPNG and rowsToCSV in
@ortex-charts/viz are the same from code. A PDF report is your renderer's job.
The theme. Highcharts.setOptions({ colors, chart: { backgroundColor } }) configured the
page. Here every chart carries a theme object: start from darkTheme, set background,
grid, text, fontFamily and palette, and pass it as theme. Indicators take the
palette in order, which is what the colors array did. See
colors, fonts and formatting.
Live ticks. series.addPoint(point, false) on the candle, again on the volume, then
chart.redraw() becomes one call: fc.update(bar). A bar whose time equals the last bar
replaces it; a later time appends. The chart schedules its own frame.
"All" is not fitContent. Highcharts drew ten years in one view by grouping the data
into weekly points. timeScale.fitContent() stops at minBarSpacing, half a pixel per bar
by default, so 2,500 daily bars do not fit in 1,100 pixels and the view opens on the most
recent bars that do. Lower timeScale.minBarSpacing, or open on the last year with
setVisibleRange, which is what most readers wanted from "All" anyway.
Step Three: The Finished Port
The translation plus the shell, in about 30 lines. This is the code the research-product row of moving in is budgeting for.
import { createChartShell } from "@ortex-charts/ui";
import { darkTheme } from "@ortex-charts/financial";
import { translateHighcharts } from "@ortex-charts/migrate";
import type { Bar } from "@ortex-charts/math";
const t = translateHighcharts(options);
const candle = t.series.find((s) => s.type === "candlestick")!;
const bars = candle.data as Bar[];
// The volume came through as its own series; the financial chart draws volume from Bar.volume.
const volume = t.series.find((s) => s.volume);
if (volume) {
const byTime = new Map((volume.data as Array<{ time: number; value: number }>).map((p) => [p.time, p.value]));
for (const b of bars) b.volume = byTime.get(b.time) ?? 0;
}
const shell = createChartShell(el, {
...t.chart, // height, legend, watermark
theme: { ...darkTheme, name: "brand", background: "#131722", grid: "#1f2535", palette: ["#4fc3f7", "#ffb74d", "#ba68c8"] },
timeZone: "America/New_York",
resolution: "1D",
symbol: "IBM",
data: bars,
volume: { heightRatio: t.panes[0]?.heightRatio ?? 0.3 },
toolbar: { symbol: false, resolution: false }, // the page has its own header
extrasCollapsed: true,
});
const fc = shell.chart;
for (const l of candle.priceLines) fc.chart.addPriceLine(fc.main.id, { ...l, lineWidth: 1, axisLabelVisible: true, title: l.title ?? "" });
for (const ind of t.indicators) fc.addIndicator(ind.id, ind.inputs, ind.color ? { colors: { [ind.id]: ind.color } } : {});
fc.setMarkers(t.markers);
feed.onBar((bar) => fc.update(bar)); // was addPoint + redraw
fromHighcharts is this without the shell: the same panes, indicators, markers and price
lines on a core chart, for a page that has its own period buttons and export.
Column and Pie Charts
The other Highcharts charts on the page move in one call each, with nothing left by hand.
import { fromHighchartsCategory, fromHighchartsPie } from "@ortex-charts/migrate";
const { chart: column, unsupported } = fromHighchartsCategory(el, columnOptions); // xAxis.categories, stacking: "normal"
const pie = fromHighchartsPie(el, pieOptions); // innerSize: "60%" is a donut
Percent stacking is the one thing reported: normalise the data first. See treemaps and heatmaps for what the category and pie charts can do that Highcharts' cannot.
Before You Ship
- Check
unsupportedonce, then delete the log line. - A comparison axis becomes a second series on a percentage or indexed scale:
rightPriceScale: { mode: "percentage" }, or"indexedTo100". - A Highcharts indicator with no counterpart (Ichimoku, VWAP and the rest of the long
tail) is reported by type. Look in
@ortex-charts/indicators-extrafirst, then register your own. - Indicator defaults differ. A period Highcharts left unset takes the ORTEX Charts default, which is 20 for a moving average where Highcharts used 14. Set it explicitly if the number matters.
- Many small charts in a results table should be sparklines, not charts.
- The licence key on the deployed origin, not just on localhost.