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();@ortex-charts/vizTreemap
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.