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