Creates a JavaScript Heatmap Chart using SciChart.js, by leveraging the FastRectangleRenderableSeries, and its customTextureOptions property to have a custom tiling texture fill.
drawExample.ts
index.html
vanilla.ts
theme.ts
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};
316This 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.
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.
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.
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.