openalgo-heatmap
v0.2.0
Published
Finviz-style market heatmap for React: squarified treemap sized by value, colored by percent change. Includes an NSE Nifty 500 demo; pure layout engine ships separately at /core.
Maintainers
Readme
openalgo-heatmap

A Finviz-style market heatmap for React, from OpenAlgo. Tiles are sized by market cap with a squarified treemap and colored by percent change on a diverging red-to-green scale. Sectors and industries nest as labeled groups.
The layout and color math ships separately at openalgo-heatmap/core with
zero dependencies (no React, no DOM, no network), so you can reuse it in a
Canvas or WebGL renderer, run it in Node for server-side image generation, or
use it as a reference when porting to Go or Rust.
What is this?
Three things in one repo:
- A React component —
MarketHeatmap, the<MarketHeatmap data={...} />you drop into your own React app. This is the product. - A zero-dependency engine — the layout + colour math at
openalgo-heatmap/core(no React, no DOM, no network). Reuse it in a Canvas/WebGL renderer, in Node for server-side images, or as a port reference. - A runnable demo app — a full-screen NSE Nifty 500 heatmap in
demo/(the "OpenAlgo heatmap"). See Run the app.
So it is a library (a publishable npm package: component + engine), and the
repo also ships a demo app that showcases it with real NSE data. The demo is
a harness — you embed the component in your own app rather than shipping demo/.
Features
- Squarified treemap layout (near-square tiles, like Finviz)
- Nested sector and industry groups to any depth
- Diverging color scale with a configurable saturation cap
- Realtime updates that repaint colors without recomputing the layout
- Responsive SVG that scales to its container
- Pure, dependency-free engine on a separate import path
- TypeScript types included
Install
npm i openalgo-heatmapReact 18 or newer is a peer dependency, so it is never bundled.
Quick start
import { MarketHeatmap, type HeatmapNode } from "openalgo-heatmap";
const data: HeatmapNode[] = [
{
name: "SEMICONDUCTORS",
children: [
{ symbol: "NVDA", value: 3000, change: -4.99 },
{ symbol: "AMD", value: 250, change: -9.11 },
],
},
{
name: "SOFTWARE",
children: [{ symbol: "MSFT", value: 3100, change: -1.82 }],
},
];
export default function App() {
return <MarketHeatmap data={data} title="TECHNOLOGY" cap={3} />;
}value drives tile size, change drives color. Nest children to any depth.
Realtime data
The component does no fetching. It is transport-agnostic, so any source works:
REST polling, WebSocket, or SSE. For streaming, pass a quotes map (symbol to
latest percent change). Updating quotes repaints tile colors only and skips
the treemap recompute, so it stays smooth at high tick rates.
One rule: keep data referentially stable across renders and push ticks through
quotes. If you build a new data array on every tick, the layout recomputes
every tick. Define data once (or memoize it), then stream into quotes.
WebSocket example:
import { useEffect, useState } from "react";
import { MarketHeatmap } from "openalgo-heatmap";
function useLiveQuotes(symbols: string[]) {
const [quotes, setQuotes] = useState<Record<string, number>>({});
useEffect(() => {
const ws = new WebSocket("wss://your-feed.example/quotes");
ws.onopen = () => ws.send(JSON.stringify({ subscribe: symbols }));
ws.onmessage = (e) => {
const tick = JSON.parse(e.data); // { symbol, changePct }
setQuotes((q) => ({ ...q, [tick.symbol]: tick.changePct }));
};
return () => ws.close();
}, [symbols]);
return quotes;
}
// <MarketHeatmap data={structure} quotes={useLiveQuotes(symbols)} cap={1.5} />REST polling is the same idea with an interval:
useEffect(() => {
const id = setInterval(async () => {
const res = await fetch("/api/quotes");
setQuotes(await res.json()); // { NVDA: -4.2, AMD: 1.1, ... }
}, 1000);
return () => clearInterval(id);
}, []);In production, coalesce bursts of ticks (for example to one update per animation frame or every 250 ms) so the UI does not re-render faster than the eye can read.
Sizing and layout
The output is an SVG (not a <canvas>). It scales to fill its container width
while preserving aspect ratio, so you control the on-screen size from the parent:
<div style={{ maxWidth: 1000, margin: "0 auto" }}>
<MarketHeatmap data={data} />
</div>width and height set the internal coordinate space, which fixes the aspect
ratio and the label density, not the pixel size. Use a landscape ratio for a
dashboard panel (for example width={1280} height={720}) or the portrait
default for a tall column. You can also pass className and style, which are
merged onto the root element, to size or theme it with your own CSS.
For a very large universe (several thousand tiles), SVG gets heavy. The engine is
renderer-agnostic, so at that scale draw the same layoutTree output to a
<canvas> instead and keep everything else.
Props
| Prop | Default | Notes |
| ------------- | ----------- | ------------------------------------------------------------------ |
| data | required | Array of groups; leaves are { symbol, value, change }. Keep it stable. |
| quotes | none | Record<symbol, number> of live percent changes. Overrides color only. |
| cap | 3 | Percent move that saturates the scale. Use 1.5 to 2 for calmer markets. |
| colorScale | built-in | (change, cap) => cssColor to override the red/green ramp. |
| title | "" | Top-left label; omit to drop the title bar. |
| width | 920 | viewBox width (coordinate space and aspect ratio). |
| height | 1180 | viewBox height. |
| pad | 2 | Gap between groups. |
| headerHeight| 17 | Group label strip height. |
| background | "#0d0d0f" | Backdrop and tile gridline color. |
| fontFamily | Inter stack | Font for labels. |
| showLegend | true | Show the gradient legend below the map. |
| className | none | Passed to the root element. |
| style | none | Merged onto the root element (set width, height, maxWidth here). |
Engine only
No React required. Useful for a Canvas/WebGL renderer or server-side rendering.
import { layoutTree, changeColor, escapeXml, type LayoutItem } from "openalgo-heatmap/core";
const items: LayoutItem[] = [];
layoutTree(data, { x: 0, y: 0, w: 800, h: 600 }, 0, { headerHeight: 16, pad: 2 }, items);
// items is a flat list of positioned rects:
// { type: "group", rect, name, headerHeight, depth }
// { type: "leaf", rect, node, depth }
// Color leaves with changeColor(node.change, cap). If you emit SVG as a raw
// string, wrap any label with escapeXml() first.Security
This is a pure rendering component, which keeps the surface small:
- It renders every label as a React text node and sets colors as plain
attributes. It never uses
dangerouslySetInnerHTML,innerHTML, oreval, so ticker and sector names cannot inject script. It is XSS-safe by default. - It makes no network requests of its own, so there is no request forgery or data exfiltration path inside the library. You own the data fetch.
- Numeric inputs are handled defensively: non-finite or non-positive sizes are dropped, and a non-finite live change falls back to the baked-in value, so a malformed feed degrades gracefully instead of corrupting the layout.
- Secure the feed, not the renderer. Serve quotes over
https/wss, authenticate the socket with a short-lived token, and keep broker API keys on your server by proxying the feed. This is a browser component; never ship secrets in the client bundle. - For non-React rendering (server-side PNG, or a Go/Rust port) escape text and
attribute values. The core exports
escapeXmlfor that. - Content Security Policy: the component uses inline styles. Under a strict
style-srcallow inline styles, or pass your ownclassNameand move styling to a stylesheet.
Development
npm i
npm run build # tsup -> dist (ESM + CJS + .d.ts) for both entries
npm run typecheckRun the app
The demo/ folder is a full-screen, Finviz-style NSE Nifty 500 heatmap. Tiles are
sized by traded value (₹ Cr) and coloured by percent change, grouped by sector.
Hover a stock to spotlight its whole sector and open a card with that sector's
mini-treemap and a ranked peer list; scroll to zoom, drag to pan,
double-click to reset.
npm install
npm run demo # Vite dev server at http://localhost:5173That is the whole app. npm run build / npm run typecheck are for the library,
not the demo.
Custom data
Into the component (your own app)
MarketHeatmap takes a data prop: an array of groups whose leaves are
{ symbol, value, change }. value sizes each tile, change colours it. Nest
children to any depth to add sector → industry levels.
import { MarketHeatmap, type HeatmapNode } from "openalgo-heatmap";
const data: HeatmapNode[] = [
{ name: "BANKS", children: [
{ symbol: "HDFCBANK", value: 1200, change: -0.8 },
{ symbol: "ICICIBANK", value: 900, change: 1.4 },
]},
{ name: "IT", children: [{ symbol: "TCS", value: 1400, change: 0.3 }]},
];
<MarketHeatmap data={data} title="MY UNIVERSE" cap={3} />;Keep data referentially stable and stream live ticks through quotes
({ HDFCBANK: -0.8, ICICIBANK: 1.4, ... }), which repaints colours without
recomputing the layout — see Realtime data.
Into the demo (swap the NSE export)
The demo parses demo/nifty500.csv at runtime. Replace it with a fresh NSE Nifty
500 export and reload — no rebuild needed. The parser tolerates the BOM and the
multi-line quoted headers in NSE's file and reads these columns (the rest are
ignored):
| Column | Drives |
| ------------------ | ------------------------------- |
| SYMBOL | join key (matches the reference) |
| LTP | last price in the hover card |
| %CHNG | tile colour |
| VALUE (₹ Crores) | tile size |
Company names & sectors
These are not in the quote CSV, so they are joined in from
demo/nse-reference.ts (symbol -> { name, sector }). Sectors come from NSE's
official macro classification. To refresh them from an official constituents
file (Company Name, Industry, Symbol, ...):
node demo/build-reference.cjs # demo/nse500_official.csv -> demo/nse-reference.tsHand-edit demo/nse-reference.ts to override a name or sector. A symbol with no
reference entry falls back to sector OTHER with the symbol as its name.
To drive the demo from a non-NSE universe entirely, edit demo/nifty.ts: it
only has to produce Row[] (symbol, name, sector, ltp, change, value); the
component and hover card take it from there.
Publish
Published to npm as openalgo-heatmap.
Only dist/ ships (see files); the demo/ app is not part of the package.
npm login # one-time, with the account that owns the package
npm run build # or rely on the prepublishOnly hook
npm publish # runs prepublishOnly (build) first
# next releases:
npm version patch && npm publishLicense
MIT © Marketcalls — see LICENSE.
