Skip to content

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 unsupported once, 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-extra first, 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.