Demonstrates how to create an Angular Phasor Diagram chart in SciChart.js, combining a Cartesian surface containing fifo Mountains, with a Polar subsurface with annotations depicting vectors.
drawExample.ts
angular.ts
theme.ts
1import {
2 PolarNumericAxis,
3 SciChartSurface,
4 NumberRange,
5 EDraggingGripPoint,
6 EAxisAlignment,
7 EPolarAxisMode,
8 LineArrowAnnotation,
9 XyDataSeries,
10 NumericAxis,
11 ESciChartSurfaceType,
12 LineAnnotation,
13 Rect,
14 SciChartPolarSubSurface,
15 RadianLabelProvider,
16 SplineMountainRenderableSeries,
17 EAutoRange,
18 PolarArcAnnotation,
19 Thickness,
20 Logger,
21} from "scichart";
22import { appTheme } from "../../../theme";
23
24function calculateDiameterProjection(x: number, y: number) {
25 return {
26 x2: x % Math.PI < Math.PI ? Math.PI / 2 : (Math.PI * 3) / 2,
27 y2: y * Math.sin(x),
28 };
29}
30
31export async function drawExample(rootElement: string | HTMLDivElement) {
32 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
33 theme: appTheme.SciChartJsTheme,
34 });
35 // Add master axes
36 const xAxis = new NumericAxis(wasmContext, {
37 labelStyle: {
38 color: "white",
39 padding: new Thickness(0, 0, 8, 0), // add a bit of bottom padding to labels
40 },
41 growBy: new NumberRange(0, 0.34), // start the mountain drawing at around 0.25 of the chart (from polar center)
42 isInnerAxis: true, // to not push chart upwards and overcomplicate centering calculations
43 autoRange: EAutoRange.Always, // allow change of xAxis range on fifo changes
44 flippedCoordinates: true, // increment from right to left
45
46 autoTicks: false,
47 majorDelta: Math.PI / 4,
48 labelProvider: new RadianLabelProvider(),
49 });
50 sciChartSurface.xAxes.add(xAxis);
51
52 const yAxis = new NumericAxis(wasmContext, {
53 axisAlignment: EAxisAlignment.Right,
54 autoTicks: false,
55 majorDelta: 0.5,
56 labelStyle: { color: "#FFFFFF" },
57 });
58 sciChartSurface.yAxes.add(yAxis);
59
60 // calculate yAxis range based on the aspect ratio of the chart (to sync the yRange with the polar chart diameter)
61 const aspectRatio = sciChartSurface.viewRect.height / sciChartSurface.viewRect.width; // likely a value like 0.66
62 const padding = aspectRatio * 2;
63
64 yAxis.visibleRange = new NumberRange(
65 -2 * padding, // -2 is the min value that `sumVector` can reach
66 2 * padding // same here, but positive
67 );
68
69 // add the 3 cartesian mountain projections of the polar vectors
70 const vector1Mountain = new SplineMountainRenderableSeries(wasmContext, {
71 dataSeries: new XyDataSeries(wasmContext, {
72 xValues: [],
73 yValues: [],
74 fifoCapacity: 300, // allow 300 points at once until sweeping happens
75 }),
76 fill: appTheme.VividRed + "22",
77 stroke: appTheme.VividRed,
78 strokeThickness: 3,
79 });
80
81 const vector2Mountain = new SplineMountainRenderableSeries(wasmContext, {
82 dataSeries: new XyDataSeries(wasmContext, {
83 xValues: [],
84 yValues: [],
85 fifoCapacity: 300,
86 }),
87 fill: appTheme.VividGreen + "22",
88 stroke: appTheme.VividGreen,
89 strokeThickness: 3,
90 });
91
92 const vectorSumMountain = new SplineMountainRenderableSeries(wasmContext, {
93 dataSeries: new XyDataSeries(wasmContext, {
94 xValues: [],
95 yValues: [],
96 fifoCapacity: 300,
97 }),
98 fill: "#DDDDDD22",
99 stroke: "#AAAAAA",
100 strokeThickness: 3,
101 });
102
103 sciChartSurface.renderableSeries.add(vectorSumMountain, vector1Mountain, vector2Mountain);
104
105 // Add polar subchart on the left half of the screen
106 const polarSub = SciChartPolarSubSurface.createSubSurface(sciChartSurface, {
107 surfaceType: ESciChartSurfaceType.Polar2D,
108 position: new Rect(0, 0, 0.5, 1), // left half of the screen
109 });
110
111 // add angular x / y axes
112 const angularAxis = new PolarNumericAxis(wasmContext, {
113 polarAxisMode: EPolarAxisMode.Angular,
114 axisAlignment: EAxisAlignment.Top,
115 isInnerAxis: true,
116 visibleRange: new NumberRange(0, Math.PI * 2), // full circle
117 labelStyle: { color: "#FFFFFF" },
118
119 majorGridLineStyle: {
120 color: "#FFFFFF44",
121 strokeThickness: 1,
122 },
123 drawMajorTickLines: false,
124 drawMinorTickLines: false,
125 drawMinorGridLines: false,
126
127 autoTicks: false,
128 majorDelta: Math.PI / 2,
129 labelProvider: new RadianLabelProvider(),
130 });
131 polarSub.xAxes.add(angularAxis);
132
133 const radialAxis = new PolarNumericAxis(wasmContext, {
134 polarAxisMode: EPolarAxisMode.Radial,
135 axisAlignment: EAxisAlignment.Left,
136 labelPrecision: 0,
137 visibleRange: new NumberRange(0, 2), // max value of `vectorSum`, since our 2 vectors are normalized to [0, 1]
138
139 drawMinorGridLines: false,
140 autoTicks: false,
141 majorDelta: 1,
142 startAngle: Math.PI / 2, // Start at the top (12 o'clock)
143 });
144 polarSub.yAxes.add(radialAxis);
145
146 // Now we add pointer annotations (vectors, projections, sum vector, angle arc)
147
148 // 1. Vector1 (red one)
149 const vector1 = new LineArrowAnnotation({
150 x1: 0,
151 y1: 0,
152 x2: 2.8,
153 y2: 1,
154 strokeThickness: 3,
155 stroke: appTheme.VividRed,
156 arrowStyle: {
157 headLength: 10,
158 headWidth: 10,
159 headDepth: 0.6,
160 },
161 isEditable: true,
162 dragPoints: [EDraggingGripPoint.x2y2],
163 selectionBoxStroke: appTheme.VividRed + "44",
164 });
165 const vector1ToDiameter = new LineAnnotation({
166 // (optional) - projection of vector1 to the diameter line
167 stroke: appTheme.VividRed,
168 strokeThickness: 3,
169 });
170
171 // 2. Vector2 (green one)
172 const vector2 = new LineArrowAnnotation({
173 x1: 0,
174 y1: 0,
175 x2: 1,
176 y2: 0.8,
177 strokeThickness: 3,
178 stroke: appTheme.VividGreen,
179 arrowStyle: {
180 headLength: 10,
181 headWidth: 10,
182 headDepth: 0.6,
183 },
184 isEditable: true,
185 dragPoints: [EDraggingGripPoint.x2y2],
186 selectionBoxStroke: appTheme.VividGreen + "44",
187 });
188 const vector2ToDiameter = new LineAnnotation({
189 // (optional)
190 stroke: appTheme.VividGreen,
191 strokeThickness: 3,
192 });
193
194 // 3. VectorSum (white one)
195 const vectorSum = new LineArrowAnnotation({
196 x1: 0,
197 y1: 0,
198 strokeThickness: 3,
199 stroke: "white",
200 arrowStyle: {
201 headLength: 10,
202 headWidth: 10,
203 headDepth: 0.6,
204 },
205 isEditable: false,
206 });
207 const vectorSumToDiameter = new LineAnnotation({
208 // (optional)
209 stroke: "#AAAAAA",
210 strokeThickness: 3,
211 });
212
213 // (optional) draw lines from x2y2 of `vector1` and `vector2` to the x2y2 of `vectorSum`
214 const vector1ToSumLine = new LineAnnotation({
215 stroke: "gray",
216 strokeThickness: 2,
217 });
218 const vector2ToSumLine = new LineAnnotation({
219 stroke: "gray",
220 strokeThickness: 2,
221 });
222
223 // 4. Arc annotation acting as a degree indicator between `vector1` and `vector2`
224 const vectorArc = new PolarArcAnnotation({
225 y2: 0, // startRadius is always 0, the rest (y1, x2, x1) will be calculated in `updatePolarAnnotations()`
226 stroke: "#FFFFFF",
227 fill: "#FFFFFF22",
228 });
229
230 // the appending order of the annotation is important, as the `vectorArc` will be rendered first,
231 // and so below all the other annotations, and so on
232 polarSub.annotations.add(
233 vectorArc,
234 vector1ToSumLine,
235 vector2ToSumLine,
236 vector1ToDiameter,
237 vector2ToDiameter,
238 vectorSumToDiameter,
239 vector1,
240 vector2,
241 vectorSum
242 );
243
244 function updatePolarAnnotations() {
245 if (!vector1 || !vector2) {
246 return;
247 }
248
249 // Normalize to (0, 1)
250 vector1.y2 = Math.max(0, Math.min(vector1.y2, 1));
251 vector2.y2 = Math.max(0, Math.min(vector2.y2, 1));
252
253 const v1x = vector1.y2 * Math.cos(vector1.x2); // x component of vector1
254 const v1y = vector1.y2 * Math.sin(vector1.x2); // y component of vector1
255
256 const v2x = vector2.y2 * Math.cos(vector2.x2); // x component of vector2
257 const v2y = vector2.y2 * Math.sin(vector2.x2); // y component of vector2
258
259 const sumX = v1x + v2x;
260 const sumY = v1y + v2y;
261
262 // Convert back to polar coordinates
263 vectorSum.x2 = Math.atan2(sumY, sumX); // angle
264 vectorSum.y2 = Math.sqrt(sumX * sumX + sumY * sumY); // magnitude
265
266 // Update perpendicular projections
267 // vector1ToDiameter
268 vector1ToDiameter.x1 = vector1.x2;
269 vector1ToDiameter.y1 = vector1.y2;
270
271 const v1Projection = calculateDiameterProjection(vector1.x2, vector1.y2);
272 vector1ToDiameter.x2 = v1Projection.x2;
273 vector1ToDiameter.y2 = v1Projection.y2;
274
275 // vector2ToDiameter
276 vector2ToDiameter.x1 = vector2.x2;
277 vector2ToDiameter.y1 = vector2.y2;
278
279 const v2Projection = calculateDiameterProjection(vector2.x2, vector2.y2);
280 vector2ToDiameter.x2 = v2Projection.x2;
281 vector2ToDiameter.y2 = v2Projection.y2;
282
283 // vectorSumToDiameter
284 vectorSumToDiameter.x1 = vectorSum.x2;
285 vectorSumToDiameter.y1 = vectorSum.y2;
286 const sumProjection = calculateDiameterProjection(vectorSum.x2, vectorSum.y2);
287 vectorSumToDiameter.x2 = sumProjection.x2;
288 vectorSumToDiameter.y2 = sumProjection.y2;
289
290 // Update lines to sum vector
291 vector1ToSumLine.x1 = vector1.x2;
292 vector1ToSumLine.y1 = vector1.y2;
293 vector1ToSumLine.x2 = vectorSum.x2;
294 vector1ToSumLine.y2 = vectorSum.y2;
295
296 vector2ToSumLine.x1 = vector2.x2;
297 vector2ToSumLine.y1 = vector2.y2;
298 vector2ToSumLine.x2 = vectorSum.x2;
299 vector2ToSumLine.y2 = vectorSum.y2;
300
301 // Update the arc annotation from vector1 to vector2, ensuring the angle is within [0, PI]
302 // We always want the shortest arc, so we calculate the angle difference and adjust accordingly
303 const angle1 = vector1.x2 % (2 * Math.PI);
304 const angle2 = vector2.x2 % (2 * Math.PI);
305
306 let delta = (angle2 - angle1 + 2 * Math.PI) % (2 * Math.PI);
307
308 if (delta > Math.PI) {
309 // The arc goes the long way around with the angle > 180 degrees, so we reverse direction
310 vectorArc.x1 = angle2;
311 vectorArc.x2 = angle1;
312 } else {
313 vectorArc.x1 = angle1;
314 vectorArc.x2 = angle2;
315 }
316
317 // optionally keep height of the angle arc smaller than smallest vector
318 vectorArc.y1 = Math.min(vector1.y2, vector2.y2) / 2;
319
320 // optionally add value to the mountains if the animation is not running
321 if (!isAnimating) {
322 updateCartesianProjection();
323 }
324 }
325 updatePolarAnnotations(); // call once to init sum, projections and arc
326
327 // On change of any of the 2 vectors, update the annotations
328 vector1.dragDelta.subscribe((relativeCoords) => updatePolarAnnotations());
329 vector2.dragDelta.subscribe((relativeCoords) => updatePolarAnnotations());
330
331 function updateCartesianProjection() {
332 // calculate with the height and angle, the projection of the vectors from 0-2
333 const vector1Projection = calculateDiameterProjection(vector1.x2, vector1.y2);
334 const vector2Projection = calculateDiameterProjection(vector2.x2, vector2.y2);
335 const vectorSumProjection = calculateDiameterProjection(vectorSum.x2, vectorSum.y2);
336
337 if (!vector1Mountain.dataSeries || !vector2Mountain.dataSeries || !vectorSumMountain.dataSeries) {
338 return; // Ensure dataSeries are initialized
339 }
340 const lastXIndex = vector1Mountain.dataSeries.count() - 1;
341 const pastX = vector1Mountain.dataSeries.getNativeXValues().get(lastXIndex) ?? 0;
342 const newX = pastX + Math.PI / 180; // Increment by 1 degree in radians
343
344 // Update the data series of the mountains
345 (vector1Mountain.dataSeries as XyDataSeries).append(newX, vector1Projection.y2);
346 (vector2Mountain.dataSeries as XyDataSeries).append(newX, vector2Projection.y2);
347 (vectorSumMountain.dataSeries as XyDataSeries).append(newX, vectorSumProjection.y2);
348 }
349
350 // Rotate vectors by a small angle
351 function rotateVectors() {
352 const angleIncrement = Math.PI / 180; // 1 degree in radians
353 vector1.x2 += angleIncrement;
354 vector2.x2 += angleIncrement;
355 vector1.dragDelta.raiseEvent(); // trigger annotation update (see lines 305-306)
356 }
357
358 // Animation controls
359 let isAnimating = true;
360
361 function animate() {
362 if (!isAnimating) return;
363
364 updateCartesianProjection();
365 rotateVectors();
366
367 requestAnimationFrame(animate);
368 }
369
370 animate(); // call once to start the animation
371
372 return {
373 sciChartSurface,
374 wasmContext,
375 controls: {
376 startAnimation: () => {
377 if (isAnimating) return; // Prevent multiple animations (speeding things up uncontrollably)
378 isAnimating = true;
379 animate();
380 },
381 stopAnimation: () => {
382 isAnimating = false;
383 },
384 },
385 };
386}
387This Angular example showcases a Phasor Diagram Chart using SciChart.js, demonstrating vector visualization through polar coordinates and cartesian projections. The implementation uses the scichart-angular package for Angular integration.
The chart is initialized via drawExample() in the component template using <scichart-angular>. The polar chart is created with SciChartPolarSubSurface, while vector interactions are handled through LineArrowAnnotation with EDraggingGripPoint configurations.
The example features real-time vector operations with polar-to-cartesian projections, animated vector rotation, and interactive editing. The PolarArcAnnotation visualizes angles between vectors, while SplineMountainRenderableSeries shows cartesian projections.
The implementation follows Angular best practices with standalone components and proper resource cleanup. Performance is optimized through WebAssembly rendering and efficient data updates using Angular's change detection strategy.

In this example we are simulating four channels of data showing that SciChart.js can be used to draw real-time ECG/EKG charts and graphs to monitor heart reate, body temperature, blood pressure, pulse rate, SPO2 blood oxygen, volumetric flow and more.

Demonstrates Logarithmic Axis on a Angular Chart using SciChart.js. SciChart supports logarithmic axis with scientific or engineering notation and positive and negative values

Demonstrating the capability of SciChart.js to create JavaScript 3D Point Cloud charts and visualize LiDAR data from the UK Defra Survey.

Demonstrates Vertically Stacked Axes on a Angular Chart using SciChart.js, allowing data to overlap

See the frequency of recordings with the Angular audio spectrum analyzer example from SciChart. This real-time audio visualizer demo uses a Fourier Transform.

Demonstrating the capability of SciChart.js to create a JavaScript Audio Analyzer Bars and visualize the Fourier-Transform of an audio waveform in realtime.

Demonstrates how to create a Waterfall chart in SciChart.js, showing chromotragraphy data with interactive selection of points.

Create Angular Correlation Plot with high performance SciChart.js. Easily render pre-defined point types. Supports custom shapes. Get your free trial now.
Angular **Semiconductors Dashboard** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and its `customTextureOptions` property to have a custom tiling texture fill.

Angular **Wafer Analysis Chart** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and crossfilter to enable live filtering.