React FastRectangle Heatmap Chart

Creates a React 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.tsx

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

Overview

This React example creates a heatmap visualization using SciChart's FastRectangleRenderableSeries within a React component. The implementation leverages the <SciChartReact/> wrapper for seamless integration.

Technical Implementation

The chart is initialized via the initChart prop, which points to the drawExample function. This function creates a SciChartSurface with hidden axes and configures the rectangular series with column modes set to EColumnMode.Start and EColumnYMode.TopHeight. The custom HeatmapPaletteProvider handles color mapping, supporting both gradient and discrete modes.

Features and Capabilities

The component demonstrates React-friendly heatmap rendering with interactive features including zooming, panning, and value inspection via tooltips. The setChart function allows dynamic toggling between gradient and discrete color modes. The implementation uses React's component lifecycle for efficient resource management.

Integration and Best Practices

The example follows React best practices by encapsulating chart logic in a separate function and using the SciChartReact component for integration. The className prop applies responsive styling through CSS modules. Developers can extend this pattern for state-controlled heatmap updates.

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