Creates a Angular Polar Signals Intelligence Dashboard using SciChart.js, with the following features: DataLabels, Rounded corners, Gradient-palette fill, startup animations.
drawExample.ts
angular.ts
theme.ts
1import {
2 PolarMouseWheelZoomModifier,
3 PolarZoomExtentsModifier,
4 PolarPanModifier,
5 PolarNumericAxis,
6 SciChartPolarSurface,
7 EPolarAxisMode,
8 NumberRange,
9 EAxisAlignment,
10 EPolarLabelMode,
11 HeatmapColorMap,
12 UniformHeatmapDataSeries,
13 PolarUniformHeatmapRenderableSeries,
14 HeatmapLegend,
15 Thickness,
16} from "scichart";
17import { appTheme } from "../../../theme";
18
19const COLOR_MAP = new HeatmapColorMap({
20 minimum: 0,
21 maximum: 1,
22 gradientStops: [
23 { offset: 0, color: appTheme.VividPink },
24 { offset: 0.2, color: appTheme.VividOrange },
25 { offset: 0.4, color: appTheme.MutedRed },
26 { offset: 0.6, color: appTheme.VividSkyBlue },
27 { offset: 0.8, color: appTheme.Indigo },
28 { offset: 1, color: appTheme.DarkIndigo },
29 ],
30});
31
32/******************************************************************************
33 * 1) Simple seeded RNG (LCG) + Perlin Noise implementation
34 ******************************************************************************/
35class LCG {
36 private state: number;
37 constructor(seed: number) { this.state = seed & 0xffffffff; }
38 next() {
39 this.state = (1664525 * this.state + 1013904223) & 0xffffffff;
40 return this.state / 0x100000000;
41 }
42}
43
44class Perlin2D {
45 private perm: Uint8Array;
46 constructor(seed: number) {
47 const rng = new LCG(seed);
48 // build and shuffle perm[0..255]
49 this.perm = new Uint8Array(512);
50 const p = new Uint8Array(256);
51 for (let i = 0; i < 256; i++) {
52 p[i] = i;
53 }
54 for (let i = 255; i > 0; i--) {
55 const j = Math.floor(rng.next() * (i + 1));
56 [p[i], p[j]] = [p[j], p[i]];
57 }
58 // duplicate
59 for (let i = 0; i < 512; i++) {
60 this.perm[i] = p[i & 255];
61 }
62 }
63 private fade(t: number) {
64 return t * t * t * (t * (t * 6 - 15) + 10);
65 }
66 private lerp(a: number, b: number, t: number) {
67 return a + t * (b - a);
68 }
69 private grad(hash: number, x: number, y: number) {
70 // 8 possible gradients
71 const h = hash & 7;
72 const u = h < 4 ? x : y;
73 const v = h < 4 ? y : x;
74 return ((h & 1) ? -u : u) + ((h & 2) ? -2 * v : 2 * v);
75 }
76 public noise(x: number, y: number) {
77 const X = Math.floor(x) & 255;
78 const Y = Math.floor(y) & 255;
79 const xf = x - Math.floor(x);
80 const yf = y - Math.floor(y);
81 const u = this.fade(xf);
82 const v = this.fade(yf);
83
84 const aa = this.perm[X + this.perm[Y]]
85 const ab = this.perm[X + this.perm[Y+1]]
86 const ba = this.perm[X +1 + this.perm[Y]]
87 const bb = this.perm[X +1 + this.perm[Y+1]]
88
89 const x1 = this.lerp(
90 this.grad(aa, xf, yf),
91 this.grad(ba, xf - 1, yf),
92 u
93 );
94 const x2 = this.lerp(
95 this.grad(ab, xf, yf - 1),
96 this.grad(bb, xf - 1, yf - 1),
97 u
98 );
99 // normalize to [0, 1]
100 return (this.lerp(x1, x2, v) + 1) * 0.5;
101 }
102}
103
104const W = 355;
105const H = 100;
106const perlin = new Perlin2D(1999); // seed
107
108const heatmap: Array<number[]> = new Array(H);
109for (let i = 0; i < H; i++) {
110 heatmap[i] = new Array(W);
111}
112
113// instead of x*0.02 … use circular coords:
114const ANGULAR_SCALE = 0.3; // tweak to stretch/shrink features around the circle
115const DEPTH_SCALE = 0.02; // your existing “forward” scale
116const TWO_PI = Math.PI * 2;
117
118function fillInitial() {
119 for (let y = 0; y < H; y++) {
120 const depth = y * DEPTH_SCALE;
121 for (let i = 0; i < W; i++) {
122 const theta = (i / W) * TWO_PI;
123 const nx = Math.cos(theta) * ANGULAR_SCALE;
124 const ny = Math.sin(theta) * ANGULAR_SCALE;
125 heatmap[y][i] = perlin.noise(nx + depth, ny + depth);
126 }
127 }
128}
129
130export const drawExample = async (rootElement: string | HTMLDivElement) => {
131 const { sciChartSurface, wasmContext } = await SciChartPolarSurface.create(rootElement, {
132 theme: appTheme.SciChartJsTheme,
133 padding: new Thickness(0, 60, 0, 0),
134 });
135
136 const radialAxis = new PolarNumericAxis(wasmContext, {
137 polarAxisMode: EPolarAxisMode.Radial,
138 axisAlignment: EAxisAlignment.Right,
139 visibleRangeLimit: new NumberRange(0, H),
140 drawMajorTickLines: false,
141 drawMinorTickLines: false,
142 labelPrecision: 0,
143 labelStyle: {
144 color: "white"
145 },
146 startAngle: Math.PI / 2, // start at 12 o'clock
147 });
148 sciChartSurface.yAxes.add(radialAxis);
149
150 const angularAxis = new PolarNumericAxis(wasmContext, {
151 polarAxisMode: EPolarAxisMode.Angular,
152 axisAlignment: EAxisAlignment.Top,
153 flippedCoordinates: true,
154 drawMajorTickLines: false,
155 drawMinorTickLines: false,
156 labelPrecision: 0,
157 labelStyle: {
158 color: "white"
159 },
160 totalAngle: Math.PI * 2, // full circle
161 startAngle: Math.PI / 2, // start at 12 o'clock
162 });
163 sciChartSurface.xAxes.add(angularAxis);
164
165 // 3.3 Prepare & define dataSeries
166 fillInitial();
167 const dataSeries = new UniformHeatmapDataSeries(wasmContext, {
168 xStart: 0,
169 xStep: 1,
170 yStart: 0,
171 yStep: 1,
172 zValues: heatmap,
173 });
174
175 const heatmapSeries = new PolarUniformHeatmapRenderableSeries(wasmContext, {
176 dataSeries,
177 colorMap: COLOR_MAP,
178 stroke: "white",
179 strokeThickness: 2,
180 });
181 sciChartSurface.renderableSeries.add(heatmapSeries);
182
183 sciChartSurface.chartModifiers.add(
184 new PolarPanModifier(),
185 new PolarZoomExtentsModifier(),
186 new PolarMouseWheelZoomModifier()
187 );
188
189 // 3.4 Animate
190 let vY = H; // virtual y‐coordinate of next row
191 const speed = 1; // rows per frame
192
193 const addNewRow = () => {
194 const row = new Array<number>(W);
195 const depth = vY * DEPTH_SCALE;
196
197 for (let i = 0; i < W; i++) {
198 const theta = (i / W) * TWO_PI;
199 const nx = Math.cos(theta) * ANGULAR_SCALE;
200 const ny = Math.sin(theta) * ANGULAR_SCALE;
201 row[i]= perlin.noise(nx + depth, ny + depth);
202 }
203 vY += speed;
204
205 heatmap.pop();
206 heatmap.unshift(row);
207 dataSeries.setZValues(heatmap);
208 }
209
210 const changeHeatmapFully = () => {
211 for (let y = 0; y < H; y++) {
212 const depth = y * DEPTH_SCALE;
213 for (let i = 0; i < W; i++) {
214 const theta = (i / W) * TWO_PI;
215 const nx = Math.cos(theta) * ANGULAR_SCALE;
216 const ny = Math.sin(theta) * ANGULAR_SCALE;
217 heatmap[y][i] = perlin.noise(nx + depth, ny + depth + Math.random() * 0.1);
218 }
219 }
220 dataSeries.setZValues(heatmap);
221 }
222
223 const animate = () => {
224 addNewRow();
225 // changeHeatmapFully();
226
227 requestAnimationFrame(animate);
228 };
229
230 requestAnimationFrame(animate);
231
232 return { sciChartSurface, wasmContext };
233};
234
235export const drawHeatmapLegend = async (rootElement: string | HTMLDivElement) => {
236 const { heatmapLegend } = await HeatmapLegend.create(rootElement, {
237 theme: {
238 ...appTheme.SciChartJsTheme,
239 sciChartBackground: appTheme.DarkIndigo + "BB",
240 loadingAnimationBackground: appTheme.DarkIndigo + "BB",
241 },
242 yAxisOptions: {
243 isInnerAxis: true,
244 labelStyle: { fontSize: 14, color: appTheme.ForegroundColor },
245 axisBorder: { borderRight: 2, color: appTheme.ForegroundColor },
246 majorTickLineStyle: {
247 color: appTheme.ForegroundColor,
248 tickSize: 8,
249 strokeThickness: 2,
250 },
251 minorTickLineStyle: {
252 color: appTheme.ForegroundColor,
253 tickSize: 4,
254 strokeThickness: 1,
255 },
256 },
257 colorMap: COLOR_MAP
258 });
259
260 return { sciChartSurface: heatmapLegend.innerSciChartSurface.sciChartSurface };
261};