Skip to content

Packages

Treemaps and heatmaps

The general-purpose charts in the family, on the same engine.

Read this page as Markdown, or take the whole documentation in one file.

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, an XY chart over a numeric axis, a treemap, a heatmap, a pie or donut, a Sankey diagram, and export helpers for PNG and CSV. They run on the same canvas layer, follow the same theme tokens and share one animation frame with everything else on the page.

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:

import { createTreemap, createHeatmap, createCategoryChart, createXYChart, createPieChart, createSankeyChart } 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();
Treemap and heatmap@ortex-charts/viz
Loading treemap and heatmap…
The general-purpose charts in the family: a sector treemap and a returns heatmap on the same engine.

Open this example with its source

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.

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, an optional logo URL drawn in tiles with room for one, 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 and reaches the ninetieth percentile of the moves on the map, not the largest of them, so one extreme cannot flatten every ordinary move into the neutral colour; anything beyond clamps to the ends. Pass [-3, 3] to fix it.
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.
headerDepth 1 How deep those strips go. A map nested sector to industry to ticker draws three bands of small text above every block at 3; 1 names the top-level groups and lets the rest be tiles.
minHeaderWidth 56 A group narrower than this gets no strip, because the name would be an ellipsis.
logoMinSize 44 A tile at least this many pixels on its short side draws its logo.
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.
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.

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

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.

XY Chart

A chart over a numeric x axis rather than a timeline or a category list: an option strike ladder, a payoff diagram, a distribution, one measure against another. Lines, areas, columns, scatter and bubbles share the plot, with a second value axis when a series asks for it.

import { createXYChart } from "@ortex-charts/viz";

const chart = createXYChart(el, {
  theme: "dark",
  series: [
    { key: "gex", title: "Gamma", kind: "column", data: [[90, 10], [100, -4], [110, 7]], colorBySign: true },
    { key: "oi", title: "Open interest", kind: "line", data: [[90, 100], [100, 250]], axis: "right", markers: true },
    { key: "zone", title: "Profit zone", kind: "area", data: payoff, color: "#22C08A", fillOpacity: 0.16, lineWidth: 0 },
  ],
  referenceLines: [
    { value: 100, axis: "x", label: "Spot", lineStyle: "dashed" },
    { value: 0, axis: "left", lineStyle: "solid" },
  ],
  referenceBands: [{ from: 95, to: 105, axis: "x", color: "rgba(56,189,248,0.10)", label: "Expected move" }],
  xAxis: { format: { type: "price", precision: 0 }, title: "Strike" },
  yAxis: { title: "Profit ($)", gridlines: true },
  tooltipExtra: (x) => [{ label: "Δ", value: deltaAt(x).toFixed(3) }],
});

Points are [x, y], [x, y, size] or { x, y, size?, color?, label? }. A reference line is a marker on either axis with an optional label; a reference band is a shaded range, which is how a probability cone or an expected-move zone is drawn. Labels along the top of the plot are laid out so two markers close together do not print over each other.

lineWidth: 0 on an area is shading with no outline, which is what a zone under another series wants. A color that already carries transparency keeps it: fillOpacity scales the alpha rather than replacing it, so rgba(…, 0.2) with a fill opacity of 1 stays at 0.2.

The pointer snaps to the nearest x within snapDistance and reports every series at that x, so one tooltip covers the whole ladder.

Pie and Donut

import { createPieChart } from "@ortex-charts/viz";

createPieChart(el, {
  slices: [{ label: "Cash", value: 42 }, { label: "Equity", value: 58, color: "#7FA3F0" }],
  innerRadius: 0.6,             // 0 is a pie, 0.6 a donut
  labels: "percent",            // "none" | "percent" | "value" | "label"
  labelPlacement: "auto",       // inside where it fits, outside on a leader line where it does not
  minLabelAngle: 8,             // thinner slices carry no label inside
  minOutsideFraction: 0.015,    // and nothing under this share is named outside either
  centerText: "$1.2bn",
  centerSubtext: "Assets",
  sortBy: "desc",
  startAngle: 0,
});

Where the Labels Go

A pie is usually one large slice and a tail of small ones, and that tail is where most pies fail: text written inside a sliver either overflows it or is dropped.

labelPlacement: "auto" writes a label inside its slice only when it fits there, in a colour picked for contrast against the fill, and otherwise outside on a leader line. The outside labels are stacked down each side and pushed apart so none overlaps, long names wrap onto a second line, and each side keeps as many as its height can hold, largest slices first. "inside" and "outside" force the choice; under "inside", a label with no room is left off rather than printed over its neighbours.

A newline in a slice's label starts a second, quieter line, which is how a sector name and its move sit together:

slices: sectors.map((s) => ({ label: `${s.name}
${s.move.toFixed(2)}%`, value: s.marketCap, color: s.move >= 0 ? up : down }))

Sankey

Flow from one set of nodes to another: revenue into costs and profit, holders into positions, order flow into venues.

import { createSankeyChart } from "@ortex-charts/viz";

createSankeyChart(el, {
  nodes: [{ id: "revenue" }, { id: "gross", label: "Gross profit" }, { id: "cogs", label: "Cost of sales", color: "#EF4E5A" }],
  links: [
    { from: "revenue", to: "gross", value: 43_000 },
    { from: "revenue", to: "cogs", value: 71_000 },
  ],
  linkColor: "gradient",        // "source" | "target" | "gradient"
  linkOpacity: 0.45,
  nodeWidth: 14,
  nodePadding: 12,
  curve: 0.4,                   // how far the ribbon control points sit from the ends
  format: { type: "compact", precision: 1 },
  labelFormat: (node, value) => [node.label ?? node.id, fmt(value)],
});

A node's column is its longest path from a source unless you pin it with column. Links of zero or negative value are not drawn, so a statement with a missing line still renders. sankeyLayout is exported for hosts that want the geometry without the chart.

Export

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:

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 a handful of chart types, not a general charting library. There is no radar, 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.