JavaScript Heatmap Chart

Creates a JavaScript Heatmap Chart using SciChart.js, by leveraging the FastRectangleRenderableSeries, and its customTextureOptions property to have a custom tiling texture fill.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

vanilla.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    NumericAxis,
3    ZoomPanModifier,
4    ZoomExtentsModifier,
5    MouseWheelZoomModifier,
6    SciChartSurface,
7    FastRectangleRenderableSeries,
8    EColumnYMode,
9    EColumnMode,
10    IPointMetadata,
11    DefaultPaletteProvider,
12    TSciChart,
13    parseColorToUIntArgb,
14    TCursorTooltipDataTemplate,
15    SeriesInfo,
16    CursorModifier,
17    zeroArray2D,
18    XyzDataSeries,
19} from "scichart";
20import { appTheme } from "../../../theme";
21
22export const drawExample = async (rootElement: string | HTMLDivElement) => {
23    // This function generates data for the heatmap with contours series example
24    function generateExampleData(index: number, heatmapWidth: number, heatmapHeight: number, colorPaletteMax: number) {
25        const zValues = zeroArray2D([heatmapHeight, heatmapWidth]);
26
27        const angle = (Math.PI * 2 * index) / 30;
28        let smallValue = 0;
29        for (let x = 0; x < heatmapWidth; x++) {
30            for (let y = 0; y < heatmapHeight; y++) {
31                const v =
32                    (1 + Math.sin(x * 0.04 + angle)) * 50 +
33                    (1 + Math.sin(y * 0.1 + angle)) * 50 * (1 + Math.sin(angle * 2));
34                const cx = heatmapWidth / 2;
35                const cy = heatmapHeight / 2;
36                const r = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
37                const exp = Math.max(0, 1 - r * 0.008);
38                const zValue = v * exp;
39                zValues[y][x] = zValue > colorPaletteMax ? colorPaletteMax : zValue;
40                zValues[y][x] += smallValue;
41            }
42
43            smallValue += 0.001;
44        }
45
46        return zValues;
47    }
48
49    function noGradientColor(
50        value: number,
51        min: number,
52        max: number,
53        gradientSteps: { offset: number; color: string }[]
54    ) {
55        // Normalize the value to a 0-1 range
56        const normalizedValue = Math.max(0, Math.min(1, (value - min) / (max - min)));
57
58        // Find the appropriate color segment
59        for (let i = 0; i < gradientSteps.length - 1; i++) {
60            const currentStep = gradientSteps[i];
61            const nextStep = gradientSteps[i + 1];
62
63            if (normalizedValue >= currentStep.offset && normalizedValue <= nextStep.offset) {
64                // If the value falls exactly on a step, return that color
65                if (normalizedValue === currentStep.offset) {
66                    return currentStep.color;
67                }
68                if (normalizedValue === nextStep.offset) {
69                    return nextStep.color;
70                }
71
72                // For interpolation between colors, you could return the nearest step
73                // or implement color interpolation if your colors support it
74                const segmentProgress = (normalizedValue - currentStep.offset) / (nextStep.offset - currentStep.offset);
75
76                // Return the color closer to the normalized value
77                return segmentProgress < 0.5 ? currentStep.color : nextStep.color;
78            }
79        }
80
81        // Fallback: return the last color if value is at maximum
82        return gradientSteps[gradientSteps.length - 1].color;
83    }
84
85    function getGradientColor(
86        value: number,
87        min: number,
88        max: number,
89        gradientSteps: { offset: number; color: string }[]
90    ) {
91        const normalizedValue = Math.max(0, Math.min(1, (value - min) / (max - min)));
92
93        // Find the segment
94        for (let i = 0; i < gradientSteps.length - 1; i++) {
95            const currentStep = gradientSteps[i];
96            const nextStep = gradientSteps[i + 1];
97
98            if (normalizedValue >= currentStep.offset && normalizedValue <= nextStep.offset) {
99                const segmentProgress = (normalizedValue - currentStep.offset) / (nextStep.offset - currentStep.offset);
100
101                // Return interpolated color (implementation depends on your color system)
102                return interpolateColors(currentStep.color, nextStep.color, segmentProgress);
103            }
104        }
105
106        return gradientSteps[gradientSteps.length - 1].color;
107    }
108
109    // Helper function for color interpolation (returns hex color string)
110    function interpolateColors(color1: string, color2: string, factor: number): string {
111        // Parse color1 and color2 as hex strings (e.g. "#RRGGBB" or "#AARRGGBB")
112        const parseHex = (hex: string) => {
113            // Remove '#' if present
114            hex = hex.replace(/^#/, "");
115            // Support short hex
116            if (hex.length === 3) {
117                hex = hex
118                    .split("")
119                    .map((c) => c + c)
120                    .join("");
121            }
122            // Support ARGB or RGB
123            let r = 0,
124                g = 0,
125                b = 0;
126            if (hex.length === 6) {
127                r = parseInt(hex.substring(0, 2), 16);
128                g = parseInt(hex.substring(2, 4), 16);
129                b = parseInt(hex.substring(4, 6), 16);
130            } else if (hex.length === 8) {
131                // ignore alpha for now
132                r = parseInt(hex.substring(2, 4), 16);
133                g = parseInt(hex.substring(4, 6), 16);
134                b = parseInt(hex.substring(6, 8), 16);
135            }
136            return { r, g, b };
137        };
138        const c1 = parseHex(color1);
139        const c2 = parseHex(color2);
140        const r = Math.round(c1.r + (c2.r - c1.r) * factor);
141        const g = Math.round(c1.g + (c2.g - c1.g) * factor);
142        const b = Math.round(c1.b + (c2.b - c1.b) * factor);
143        // Return as hex string
144        return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
145    }
146
147    // Create a SciChartSurface
148    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
149        theme: appTheme.SciChartJsTheme,
150    });
151
152    class HeatmapPaletteProvider extends DefaultPaletteProvider {
153        private min;
154        private max;
155        private gradientSteps;
156        private gradient;
157
158        private _isRangeIndependant?: boolean = true;
159        public get isRangeIndependant(): boolean {
160            return this._isRangeIndependant;
161        }
162        public set isRangeIndependant(value: boolean) {
163            this._isRangeIndependant = value;
164        }
165
166        public shouldUpdatePalette(): boolean {
167            return false;
168        }
169
170        constructor(
171            wasmContext: TSciChart,
172            minimum: number,
173            maximum: number,
174            gradientSteps: { offset: number; color: string }[],
175            gradient: boolean
176        ) {
177            super();
178            this.min = minimum;
179            this.max = maximum;
180            this.gradientSteps = gradientSteps;
181            this.gradient = gradient;
182        }
183
184        public overrideFillArgb(
185            xValue: number,
186            yValue: number,
187            index: number,
188            opacity?: number,
189            metadata?: IPointMetadata
190        ): number | undefined {
191            const value = dataSeries.getNativeZValues().get(index);
192
193            const gradinetCalc = this.gradient
194                ? getGradientColor(value, this.min, this.max, this.gradientSteps)
195                : noGradientColor(value, this.min, this.max, this.gradientSteps);
196
197            return parseColorToUIntArgb(gradinetCalc);
198        }
199    }
200
201    // Add X-axis
202    const xAxis = new NumericAxis(wasmContext, {
203        isVisible: false,
204    });
205
206    sciChartSurface.xAxes.add(xAxis);
207
208    // Add Y-axis
209    const yAxis = new NumericAxis(wasmContext, {
210        isVisible: false,
211    });
212    sciChartSurface.yAxes.add(yAxis);
213
214    const heatmapWidth = 300;
215    const heatmapHeight = 200;
216    const colorPaletteMax = 150;
217
218    const initialZValues = generateExampleData(3, heatmapWidth, heatmapHeight, colorPaletteMax);
219
220    function transformData(data: number[][]) {
221        const xValues = [];
222        const yValues = [];
223
224        // Iterate through each row
225        for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
226            const row = data[rowIndex];
227
228            // For each element in the row, add corresponding x and y values
229            for (let colIndex = 0; colIndex < row.length; colIndex++) {
230                xValues.push(colIndex + 1); // Column index (1-based)
231                yValues.push(data.length - rowIndex); // Row index from bottom (1-based)
232            }
233        }
234
235        return { xValues, yValues };
236    }
237
238    const { xValues, yValues } = transformData(initialZValues);
239
240    const flatValues = initialZValues.flat();
241
242    const min = 0;
243    const max = colorPaletteMax;
244    const zValues = flatValues;
245
246    const dataSeries = new XyzDataSeries(wasmContext, {
247        xValues,
248        yValues,
249        zValues,
250    });
251
252    const setChart = (gradient: boolean) => {
253        sciChartSurface.renderableSeries.clear(false);
254
255        const rectangleSeries = new FastRectangleRenderableSeries(wasmContext, {
256            dataSeries,
257            columnXMode: EColumnMode.Start,
258            columnYMode: EColumnYMode.TopHeight,
259            paletteProvider: new HeatmapPaletteProvider(
260                wasmContext,
261                min,
262                max,
263                [
264                    { offset: 0, color: appTheme.DarkIndigo },
265                    { offset: 0.2, color: appTheme.Indigo },
266                    { offset: 0.3, color: appTheme.VividSkyBlue },
267                    { offset: 0.5, color: appTheme.VividGreen },
268                    { offset: 0.7, color: appTheme.MutedRed },
269                    { offset: 0.9, color: appTheme.VividOrange },
270                    { offset: 1, color: appTheme.VividPink },
271                ],
272                gradient
273            ),
274            dataPointWidth: 1,
275            defaultY1: 1,
276            strokeThickness: 0,
277        });
278        sciChartSurface.renderableSeries.add(rectangleSeries);
279    };
280
281    // true = gradient, false = solid
282    setChart(true);
283
284    const tooltipDataTemplate: TCursorTooltipDataTemplate = (seriesInfos: SeriesInfo[]) => {
285        const valuesWithLabels: string[] = [];
286
287        seriesInfos.forEach((si) => {
288            console.log(si);
289            const xyzSI = si;
290            if (xyzSI.isWithinDataBounds) {
291                if (!isNaN(xyzSI.yValue) && xyzSI.isHit) {
292                    const value = dataSeries.getNativeZValues().get(xyzSI.dataSeriesIndex);
293                    valuesWithLabels.push(`X: ${xyzSI.xValue}, Y: ${xyzSI.yValue}, Value: ${value.toFixed(2)}`);
294                }
295            }
296        });
297        return valuesWithLabels;
298    };
299
300    // Add interactivity modifiers
301    sciChartSurface.chartModifiers.add(
302        new ZoomPanModifier({ enableZoom: true }),
303        new ZoomExtentsModifier(),
304        new MouseWheelZoomModifier(),
305        new CursorModifier({
306            showTooltip: true,
307            tooltipDataTemplate,
308            showXLine: false,
309            showYLine: false,
310            tooltipContainerBackground: appTheme.MutedPurple + 55,
311        })
312    );
313
314    return { sciChartSurface, wasmContext, setChart };
315};
316

Heatmap Rectangle Chart - JavaScript

Overview

This example demonstrates how to create a heatmap-style visualization using rectangular series in SciChart.js. It replicates heatmap functionality by leveraging the FastRectangleRenderableSeries with a custom palette provider for color mapping.

Technical Implementation

The implementation uses an XyzDataSeries to store 3D data points, where Z-values represent heatmap intensity. A custom HeatmapPaletteProvider extends DefaultPaletteProvider to map values to colors using either gradient or discrete steps. The chart is initialized asynchronously with hidden axes, focusing purely on the heatmap visualization.

Features and Capabilities

The example showcases dynamic data generation with generateExampleData(), supporting real-time updates. Interactive features include ZoomPanModifier, ZoomExtentsModifier, and a custom tooltip via CursorModifier. The color mapping system supports both gradient and discrete modes through the noGradientColor and getGradientColor functions.

Integration and Best Practices

The implementation follows JavaScript best practices with async initialization and proper resource cleanup. The heatmap data is efficiently transformed and flattened for optimal performance with large datasets. Developers can adjust the heatmapWidth and heatmapHeight parameters to control resolution.

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.