Demonstrates rich interactivity with custom modifiers using SciChart.js, High Performance JavaScript Charts
drawExample.ts
angular.ts
theme.ts
AddIOModifier.ts
DiscreteAxisMarker.ts
PointDragModifier.ts
1import { appTheme } from "../../../theme";
2import { AddIOModifier } from "./AddIOModifier";
3import { DiscreteAxisMarker } from "./DiscreteAxisMarker";
4import { PointDragModifier } from "./PointDragModifier";
5
6import {
7 AnnotationClickEventArgs,
8 AxisMarkerAnnotation,
9 BasePaletteProvider,
10 BoxAnnotation,
11 CustomAnnotation,
12 ECoordinateMode,
13 EDataChangeType,
14 EHorizontalAnchorPoint,
15 EStrokePaletteMode,
16 ESeriesType,
17 EAxisAlignment,
18 ELabelPlacement,
19 EExecuteOn,
20 EDraggingGripPoint,
21 EWrapTo,
22 FastLineRenderableSeries,
23 HeatmapColorMap,
24 HorizontalLineAnnotation,
25 IStrokePaletteProvider,
26 IRenderableSeries,
27 MouseWheelZoomModifier,
28 NativeTextAnnotation,
29 NumericAxis,
30 NumberRange,
31 PaletteFactory,
32 RolloverModifier,
33 RubberBandXyZoomModifier,
34 SciChartSurface,
35 SplineLineRenderableSeries,
36 TGradientStop,
37 TSciChart,
38 UniformHeatmapDataSeries,
39 UniformHeatmapRenderableSeries,
40 VerticalLineAnnotation,
41 XyDataSeries,
42 XyScatterRenderableSeries,
43 YAxisDragModifier,
44 ZoomExtentsModifier,
45 ZoomPanModifier,
46 EVerticalAnchorPoint,
47 GenericAnimation,
48 formatNumber,
49 EllipsePointMarker,
50} from "scichart";
51
52const gradientStops = [
53 { offset: 0, color: "#942B96" },
54 { offset: 0.3, color: "#3C2D91" },
55 { offset: 0.45, color: "#47bde6" },
56 { offset: 0.5, color: appTheme.DarkIndigo },
57 { offset: 0.55, color: "#68bcae" },
58 { offset: 0.7, color: "#e97064" },
59 { offset: 1.0, color: "#ae418d" },
60];
61
62const csGradientStops = [
63 { offset: 0, color: "#942B96" },
64 { offset: 0.3, color: "#3C2D91" },
65 { offset: 0.45, color: "#47bde6" },
66 { offset: 0.5, color: "#45AEC3" },
67 { offset: 0.55, color: "#68bcae" },
68 { offset: 0.7, color: "#e97064" },
69 { offset: 1.0, color: "#ae418d" },
70];
71
72let width = 700;
73let height = 500;
74
75export const getChartsInitializationApi = () => {
76 let initialZValues: number[][];
77 let velocities: number[][];
78 // main surface
79 let mainSurface: SciChartSurface;
80 let crossSectionSurface: SciChartSurface;
81 let inputSurface: SciChartSurface;
82 let outputSurface: SciChartSurface;
83 let addInputSeries: (color: string, freq?: number) => void;
84 let addOutputSeries: (color: string) => void;
85
86 const initMainChart = async (rootElement: string | HTMLDivElement) => {
87 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
88 theme: appTheme.SciChartJsTheme,
89 });
90 const xAxis = new NumericAxis(wasmContext, { isVisible: false });
91 sciChartSurface.xAxes.add(xAxis);
92 const yAxis = new NumericAxis(wasmContext, {
93 axisAlignment: EAxisAlignment.Left,
94 isVisible: true,
95 drawLabels: false,
96 drawMajorGridLines: false,
97 drawMajorBands: false,
98 drawMajorTickLines: false,
99 drawMinorTickLines: false,
100 drawMinorGridLines: false,
101 });
102 sciChartSurface.yAxes.add(yAxis);
103
104 width = Math.floor(sciChartSurface.domCanvas2D.width);
105 height = Math.floor(sciChartSurface.domCanvas2D.height);
106 initialZValues = Array.from(Array(height), (_) => Array(width).fill(0));
107 velocities = Array.from(Array(height), (_) => Array(width).fill(0));
108
109 mainSurface = sciChartSurface;
110
111 return { sciChartSurface };
112 };
113
114 const initCrossSectionChart = async (rootElement: string | HTMLDivElement) => {
115 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
116 theme: appTheme.SciChartJsTheme,
117 });
118
119 sciChartSurface.xAxes.add(
120 new NumericAxis(wasmContext, {
121 id: "xh",
122 isInnerAxis: true,
123 visibleRange: new NumberRange(0, width),
124 visibleRangeLimit: new NumberRange(0, width),
125 drawMinorGridLines: false,
126 zoomExtentsToInitialRange: true,
127 drawLabels: false,
128 })
129 );
130
131 sciChartSurface.yAxes.add(
132 new NumericAxis(wasmContext, {
133 id: "yh",
134 visibleRange: new NumberRange(-50, 50),
135 visibleRangeLimit: new NumberRange(-100, 100),
136 axisAlignment: EAxisAlignment.Right,
137 drawMinorGridLines: false,
138 labelPrecision: 0,
139 zoomExtentsToInitialRange: true,
140 })
141 );
142
143 sciChartSurface.xAxes.add(
144 new NumericAxis(wasmContext, {
145 id: "xv",
146 isInnerAxis: true,
147 visibleRange: new NumberRange(0, height),
148 visibleRangeLimit: new NumberRange(0, height),
149 flippedCoordinates: true,
150 axisAlignment: EAxisAlignment.Left,
151 zoomExtentsToInitialRange: true,
152 drawLabels: false,
153 })
154 );
155
156 sciChartSurface.yAxes.add(
157 new NumericAxis(wasmContext, {
158 id: "yv",
159 // isInnerAxis: true,
160 visibleRange: new NumberRange(-50, 50),
161 visibleRangeLimit: new NumberRange(-100, 100),
162 axisAlignment: EAxisAlignment.Top,
163 flippedCoordinates: true,
164 labelPrecision: 0,
165 zoomExtentsToInitialRange: true,
166 })
167 );
168
169 const lineSeriesh = new FastLineRenderableSeries(wasmContext, {
170 id: "h",
171 strokeThickness: 3,
172 stroke: "steelblue",
173 dataSeries: new XyDataSeries(wasmContext, { containsNaN: false, isSorted: true }),
174 paletteProvider: new YPalette(wasmContext, csGradientStops),
175 xAxisId: "xh",
176 yAxisId: "yh",
177 });
178 sciChartSurface.renderableSeries.add(lineSeriesh);
179 const lineSeriesv = new FastLineRenderableSeries(wasmContext, {
180 id: "v",
181 strokeThickness: 3,
182 stroke: "steelblue",
183 dataSeries: new XyDataSeries(wasmContext, { containsNaN: false, isSorted: true }),
184 paletteProvider: new YPalette(wasmContext, csGradientStops),
185 xAxisId: "xv",
186 yAxisId: "yv",
187 });
188 sciChartSurface.renderableSeries.add(lineSeriesv);
189
190 sciChartSurface.annotations.add(
191 new NativeTextAnnotation({
192 x1: 0.5,
193 y1: 0.02,
194 xCoordinateMode: ECoordinateMode.Relative,
195 yCoordinateMode: ECoordinateMode.Relative,
196 verticalAnchorPoint: EVerticalAnchorPoint.Top,
197 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
198 text: "Cross sections",
199 fontSize: 18,
200 textColor: appTheme.ForegroundColor,
201 opacity: 0.5,
202 wrapTo: EWrapTo.ViewRect,
203 })
204 );
205
206 sciChartSurface.chartModifiers.add(
207 new ZoomExtentsModifier(),
208 new MouseWheelZoomModifier(),
209 new RubberBandXyZoomModifier({ executeOn: EExecuteOn.MouseRightButton }),
210 new YAxisDragModifier({})
211 );
212
213 crossSectionSurface = sciChartSurface;
214
215 return { sciChartSurface };
216 };
217
218 const inputChart = async (rootElement: string | HTMLDivElement) => {
219 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
220 theme: appTheme.SciChartJsTheme,
221 });
222 const xAxis = new NumericAxis(wasmContext, {
223 drawMinorGridLines: false,
224 visibleRange: new NumberRange(0, 30000),
225 visibleRangeLimit: new NumberRange(0, 30000),
226 axisAlignment: EAxisAlignment.Top,
227 });
228 xAxis.labelProvider.formatLabel = (dataValue) =>
229 xAxis.labelProvider.applyFormat(formatNumber(dataValue / 1000, xAxis.labelProvider.numericFormat, 0));
230
231 sciChartSurface.xAxes.add(xAxis);
232 const xValues = Array.from(Array(60)).map((_, i) => i * 500);
233 const yAxis = new NumericAxis(wasmContext, {
234 visibleRange: new NumberRange(-100, 100),
235 visibleRangeLimit: new NumberRange(-100, 100),
236 axisAlignment: EAxisAlignment.Left,
237 labelPrecision: 0,
238 drawMinorGridLines: false,
239 });
240
241 sciChartSurface.yAxes.add(yAxis);
242 const makeYValues = (frequency: number) => {
243 return Array.from(Array(60)).map(
244 (_, i) => Math.sin(((frequency * 2 - 30000) * i * 2 * Math.PI) / 60000) * 80
245 );
246 };
247
248 const addInputSeriesCallback = (color: string, freq?: number) => {
249 freq = freq ?? Math.floor((Math.random() * 30000) / 500) * 500;
250 const frequencyMarker = new DiscreteAxisMarker({
251 id: color,
252 x1: freq,
253 backgroundColor: color,
254 isEditable: true,
255 formattedValue: "Frequency",
256 });
257 frequencyMarker.stepSize = 250;
258 // hack to disable selection box while dragging
259 // @ts-ignore
260 frequencyMarker.updateAdornerInner = () => {};
261 const dataSeries = new XyDataSeries(wasmContext, {
262 containsNaN: false,
263 isSorted: true,
264 xValues,
265 yValues: makeYValues(freq),
266 metadata: { isSelected: false },
267 });
268 const lineSeries = new SplineLineRenderableSeries(wasmContext, {
269 id: color,
270 stroke: color,
271 pointMarker: new EllipsePointMarker(wasmContext, {
272 stroke: color,
273 fill: color,
274 width: 5,
275 height: 5,
276 }),
277 dataSeries,
278 onSelectedChanged: (sourceSeries: IRenderableSeries, isSelected: boolean) => {
279 lineSeries.strokeThickness = isSelected ? 4 : 2;
280 lineSeries.pointMarker.fill = isSelected ? "red" : color;
281 },
282 });
283 frequencyMarker.dragDelta.subscribe((args) => {
284 dataSeries.clear();
285 dataSeries.appendRange(xValues, makeYValues(frequencyMarker.x1));
286 });
287 sciChartSurface.renderableSeries.add(lineSeries);
288 sciChartSurface.annotations.add(frequencyMarker);
289 };
290
291 const timerLine = new VerticalLineAnnotation({
292 id: "timerLine",
293 stroke: "green",
294 x1: 0,
295 });
296 sciChartSurface.annotations.add(timerLine);
297
298 sciChartSurface.annotations.add(
299 new NativeTextAnnotation({
300 x1: 0.5,
301 y1: 0.02,
302 xCoordinateMode: ECoordinateMode.Relative,
303 yCoordinateMode: ECoordinateMode.Relative,
304 verticalAnchorPoint: EVerticalAnchorPoint.Top,
305 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
306 text: "Input drivers",
307 fontSize: 18,
308 textColor: appTheme.ForegroundColor,
309 opacity: 0.5,
310 wrapTo: EWrapTo.ViewRect,
311 })
312 );
313
314 sciChartSurface.chartModifiers.add(
315 new PointDragModifier(),
316 new ZoomPanModifier({ executeOn: EExecuteOn.MouseRightButton }),
317 new MouseWheelZoomModifier()
318 );
319
320 inputSurface = sciChartSurface;
321 addInputSeries = addInputSeriesCallback;
322
323 return { sciChartSurface, addInputSeries: addInputSeriesCallback };
324 };
325
326 const initHistoryChart = async (rootElement: string | HTMLDivElement) => {
327 const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
328 theme: appTheme.SciChartJsTheme,
329 });
330 const xAxis = new NumericAxis(wasmContext, {
331 visibleRange: new NumberRange(0, 30000),
332 visibleRangeLimit: new NumberRange(0, 30000),
333 drawMinorGridLines: false,
334 zoomExtentsToInitialRange: true,
335 axisAlignment: EAxisAlignment.Top,
336 });
337 xAxis.labelProvider.formatLabel = (dataValue) =>
338 xAxis.labelProvider.applyFormat(formatNumber(dataValue / 1000, xAxis.labelProvider.numericFormat, 0));
339 sciChartSurface.xAxes.add(xAxis);
340
341 sciChartSurface.yAxes.add(
342 new NumericAxis(wasmContext, {
343 visibleRange: new NumberRange(-30, 30),
344 visibleRangeLimit: new NumberRange(-100, 100),
345 axisAlignment: EAxisAlignment.Right,
346 drawMinorGridLines: false,
347 labelPrecision: 0,
348 cursorLabelPrecision: 1,
349 })
350 );
351
352 const rollover = new RolloverModifier();
353 sciChartSurface.chartModifiers.add(rollover);
354
355 const addOutputSeriesCallback = (color: string) => {
356 const lineData = new XyDataSeries(wasmContext, { containsNaN: true, isSorted: true });
357 const xarr = Array.from(Array(1500)).map((_, i) => i * 20);
358 const nanArr = xarr.map((x) => NaN);
359 lineData.appendRange(xarr, nanArr);
360 const lineSeries = new FastLineRenderableSeries(wasmContext, {
361 id: color,
362 strokeThickness: 3,
363 stroke: color,
364 dataSeries: lineData,
365 opacity: 0.8,
366 });
367
368 const dotSeries = new XyDataSeries(wasmContext, { containsNaN: true, isSorted: true });
369 const leadingDot = new XyScatterRenderableSeries(wasmContext, {
370 id: color + "dot",
371 pointMarker: new EllipsePointMarker(wasmContext, {
372 width: 8,
373 height: 8,
374 strokeThickness: 2,
375 fill: color,
376 stroke: color,
377 }),
378 dataSeries: dotSeries,
379 });
380 rollover.includeSeries(leadingDot, false);
381 lineData.dataChanged.subscribe((data) => {
382 const changeIndex = data.changeType === EDataChangeType.Append ? lineData.count() - 1 : data.index;
383 dotSeries.clear();
384 dotSeries.append(
385 lineData.getNativeXValues().get(changeIndex),
386 lineData.getNativeYValues().get(changeIndex)
387 );
388 });
389 sciChartSurface.renderableSeries.add(lineSeries, leadingDot);
390 };
391
392 sciChartSurface.annotations.add(
393 new NativeTextAnnotation({
394 x1: 0.5,
395 y1: 0.02,
396 xCoordinateMode: ECoordinateMode.Relative,
397 yCoordinateMode: ECoordinateMode.Relative,
398 verticalAnchorPoint: EVerticalAnchorPoint.Top,
399 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
400 text: "Point outputs",
401 fontSize: 18,
402 textColor: appTheme.ForegroundColor,
403 opacity: 0.5,
404 wrapTo: EWrapTo.ViewRect,
405 })
406 );
407
408 sciChartSurface.chartModifiers.add(
409 new ZoomPanModifier({ enableZoom: true }),
410 new ZoomExtentsModifier(),
411 new MouseWheelZoomModifier(),
412 new RubberBandXyZoomModifier({ executeOn: EExecuteOn.MouseRightButton })
413 );
414
415 outputSurface = sciChartSurface;
416 addOutputSeries = addOutputSeriesCallback;
417
418 return { sciChartSurface, addOutputSeries: addOutputSeriesCallback };
419 };
420
421 const onAllChartsInit = () => {
422 const heatmapDataSeries = new UniformHeatmapDataSeries(mainSurface.webAssemblyContext2D, {
423 xStart: 1,
424 xStep: 1,
425 yStart: 1,
426 yStep: 1,
427 zValues: initialZValues,
428 });
429
430 const heatmapSeries = new UniformHeatmapRenderableSeries(mainSurface.webAssemblyContext2D, {
431 opacity: 0.8,
432 dataSeries: heatmapDataSeries,
433 useLinearTextureFiltering: true,
434 colorMap: new HeatmapColorMap({ minimum: -80, maximum: 80, gradientStops }),
435 });
436
437 // Add heatmap to the chart
438 mainSurface.renderableSeries.add(heatmapSeries);
439
440 const lineDataSeries = crossSectionSurface.renderableSeries.getById("h").dataSeries as XyDataSeries;
441 const vlineDataSeries = crossSectionSurface.renderableSeries.getById("v").dataSeries as XyDataSeries;
442 const lineXValues = Array.from(Array(width)).map((_, i) => i);
443 const vlineXValues = Array.from(Array(height)).map((_, i) => i);
444
445 const hline = new HorizontalLineAnnotation({
446 stroke: "#E97064",
447 strokeThickness: 3,
448 y1: height / 3,
449 isEditable: true,
450 labelPlacement: ELabelPlacement.TopLeft,
451 axisLabelStroke: "#E97064",
452 showLabel: true,
453 dragOnLabel: false,
454 onDrag: () => {
455 lineDataSeries.clear();
456 const yVals = initialZValues[Math.floor(hline.y1)];
457 if (yVals) {
458 lineDataSeries.appendRange(lineXValues, yVals);
459 }
460 },
461 });
462
463 const vline = new VerticalLineAnnotation({
464 stroke: "#E97064",
465 strokeThickness: 3,
466 labelPlacement: ELabelPlacement.BottomLeft,
467 axisLabelStroke: "#E97064",
468 showLabel: true,
469 x1: width / 2,
470 isEditable: true,
471 onDrag: () => {
472 vlineDataSeries.clear();
473 const yVals = initialZValues.map((r) => r[Math.floor(vline.x1)]);
474 if (yVals) {
475 vlineDataSeries.appendRange(vlineXValues, yVals);
476 }
477 },
478 });
479
480 const inputs: BoxAnnotation[] = [];
481 const outputs: CustomAnnotation[] = [];
482
483 const outputColors = ["#0bf4cd", "#f4840b", "#0bdef4", "#f6086c", "#112cce", "#9002a1"];
484
485 const addIO = new AddIOModifier();
486
487 const inputColors = ["#68bcae", "#e97064", "#47bde6", "#ae418d", "#274b92", "#634e96"];
488
489 const removeInput = (input: BoxAnnotation) => {
490 mainSurface.annotations.remove(input);
491 const inputSeries = inputSurface.renderableSeries.getById(input.id);
492 inputSurface.renderableSeries.remove(inputSeries);
493 inputSeries.delete();
494 const freqAnn = inputSurface.annotations.getById(input.id);
495 inputSurface.annotations.remove(freqAnn);
496 freqAnn?.delete();
497 inputColors.push(input.id);
498 inputs.splice(inputs.indexOf(input), 1);
499 input.delete();
500 };
501 const addInput = (x: number, y: number, w?: number, h?: number, freq?: number) => {
502 if (inputColors.length === 0) return;
503 const color = inputColors.shift();
504 w = w ?? Math.round(width / 80);
505 h = h ?? w;
506 const boxInput = new BoxAnnotation({
507 id: color,
508 x1: x - w,
509 x2: x + w,
510 y1: y - h,
511 y2: y + h,
512 isEditable: true,
513 strokeThickness: 3,
514 stroke: color,
515 fill: "transparent",
516 dragPoints: [EDraggingGripPoint.Body, EDraggingGripPoint.x2y1],
517 onClick: (args: AnnotationClickEventArgs) => {
518 if (args.mouseArgs.button !== EExecuteOn.MouseRightButton) return;
519 removeInput(boxInput);
520 },
521 });
522
523 boxInput.selectedChanged.subscribe((isSelected: boolean) => {
524 inputSurface.renderableSeries.getById(color).isSelected = isSelected;
525 });
526 boxInput.dragDelta.subscribe((data) => {
527 if (boxInput.x1 < 1) boxInput.x1 = 1;
528 if (boxInput.x2 >= width) boxInput.x1 = width - 1;
529 if (boxInput.y1 < 1) boxInput.y1 = 1;
530 if (boxInput.y2 >= height) boxInput.y1 = height - 1;
531 });
532
533 inputs.push(boxInput);
534 mainSurface.annotations.add(boxInput);
535 addInputSeries(color, freq);
536 };
537
538 const removeOutput = (output: CustomAnnotation) => {
539 mainSurface.annotations.remove(output);
540 const os = outputSurface.renderableSeries.getById(output.id);
541 outputSurface.renderableSeries.remove(os);
542 os?.delete();
543 // remove leading dot
544 const od = outputSurface.renderableSeries.getById(output.id + "dot");
545 outputSurface.renderableSeries.remove(od);
546 od.delete();
547 outputColors.push(output.id);
548 outputs.splice(outputs.indexOf(output), 1);
549 output.delete();
550 };
551 const addOutput = (x: number, y: number) => {
552 if (outputColors.length === 0) return;
553 const color = outputColors.shift();
554 const output = new CustomAnnotation({
555 id: color,
556 x1: x,
557 y1: y,
558 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
559 verticalAnchorPoint: EVerticalAnchorPoint.Center,
560 svgString: `<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg"><ellipse style="fill: ${color}; stroke: rgb(0, 0, 0);" cx="8" cy="8" rx="7" ry="7"></ellipse></svg>`,
561 isEditable: true,
562 dragPoints: [EDraggingGripPoint.Body],
563 onClick: (args: AnnotationClickEventArgs) => {
564 if (args.mouseArgs.button !== EExecuteOn.MouseRightButton) return;
565 removeOutput(output);
566 },
567 });
568 output.selectedChanged.subscribe((isSelected: boolean) => {
569 if (isSelected) {
570 outputSurface.renderableSeries
571 .asArray()
572 .filter((rs) => rs.type === ESeriesType.LineSeries)
573 .forEach((rs) => {
574 rs.opacity = rs.id === color ? 1 : 0.3;
575 });
576 } else {
577 outputSurface.renderableSeries
578 .asArray()
579 .filter((rs) => rs.type === ESeriesType.LineSeries)
580 .forEach((rs) => (rs.opacity = 0.8));
581 }
582 outputSurface.renderableSeries.getById(color).isSelected = isSelected;
583 });
584 // @ts-ignore
585 output.updateAdornerInner = () => {};
586 output.dragDelta.subscribe((data) => {
587 if (output.x1 < 1) output.x1 = 1;
588 if (output.x1 >= width) output.x1 = width - 1;
589 if (output.y1 < 1) output.y1 = 1;
590 if (output.y1 >= height) output.y1 = height - 1;
591 });
592 outputs.push(output);
593 mainSurface.annotations.add(output);
594 addOutputSeries(color);
595 };
596
597 mainSurface.annotations.add(hline, vline);
598
599 addIO.onAddInput = addInput;
600 addIO.onAddOutput = addOutput;
601 mainSurface.chartModifiers.add(addIO);
602
603 const dampingMarker = new AxisMarkerAnnotation({
604 id: "damping",
605 y1: 100,
606 backgroundColor: "#E97064",
607 isEditable: true,
608 formattedValue: "Damping",
609 });
610 // hack to disable selection box while dragging
611 // @ts-ignore
612 dampingMarker.updateAdornerInner = () => {};
613 mainSurface.annotations.add(dampingMarker);
614
615 mainSurface.annotations.add(
616 new NativeTextAnnotation({
617 x1: 0.5,
618 y1: 0.02,
619 xCoordinateMode: ECoordinateMode.Relative,
620 yCoordinateMode: ECoordinateMode.Relative,
621 verticalAnchorPoint: EVerticalAnchorPoint.Top,
622 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
623 text: "2D Wave simulation",
624 fontSize: 18,
625 textColor: appTheme.ForegroundColor,
626 opacity: 0.5,
627 wrapTo: EWrapTo.ViewRect,
628 })
629 );
630
631 let timerId: NodeJS.Timeout;
632 const getValue = (r: number, c: number) => {
633 if (r < 0 || r === height || c < 0 || c === width) {
634 return 0;
635 } else {
636 return initialZValues[r][c];
637 }
638 };
639 let time = 0;
640 const timerLine1 = inputSurface.annotations.getById("timerLine");
641
642 const dt = 0.3;
643 let damping = 0.99;
644 const timestep = 20;
645 const updateChart = () => {
646 damping = 1 - Math.abs(dampingMarker.y1 / 10000);
647 const newZValues: number[][] = Array.from(Array(height), (_) => Array(width));
648 for (let r = 0; r < height; r++) {
649 for (let c = 0; c < width; c++) {
650 const a =
651 1 *
652 (-8 * getValue(r, c) +
653 getValue(r - 1, c) +
654 getValue(r + 1, c) +
655 getValue(r, c - 1) +
656 getValue(r, c + 1) +
657 getValue(r - 1, c - 1) +
658 getValue(r + 1, c - 1) +
659 getValue(r - 1, c + 1) +
660 getValue(r + 1, c + 1));
661 velocities[r][c] = (velocities[r][c] + a * dt) * damping;
662 newZValues[r][c] = initialZValues[r][c] + velocities[r][c] * dt * 2;
663 }
664 }
665 timerLine1.x1 = time;
666 const inputIndex = Math.floor(time / 500);
667 // Update inputs
668 for (const boxInput of inputs) {
669 const inputSeries = inputSurface.renderableSeries.getById(boxInput.id).dataSeries as XyDataSeries;
670 const inputY = inputSeries.getNativeYValues().get(inputIndex);
671 const inputY1 = inputSeries.getNativeYValues().get((inputIndex + 1) % 60);
672 const inputInterpolated = inputY + ((inputY1 - inputY) / 500) * (time - inputIndex * 500);
673 const irStart = Math.max(2, Math.min(Math.floor(boxInput.y1), Math.floor(boxInput.y2)) - 1);
674 const irEnd = Math.min(height, Math.max(Math.floor(boxInput.y1), Math.floor(boxInput.y2)) - 1);
675 const icStart = Math.max(2, Math.min(Math.floor(boxInput.x1), Math.floor(boxInput.x2)) - 1);
676 const icEnd = Math.min(width, Math.max(Math.floor(boxInput.x1), Math.floor(boxInput.x2)) - 1);
677 for (let r = irStart; r < irEnd; r++) {
678 for (let c = icStart; c < icEnd; c++) {
679 newZValues[r][c] = inputInterpolated;
680 }
681 }
682 }
683
684 initialZValues = newZValues;
685 heatmapDataSeries.setZValues(newZValues);
686 if (!hline.isHidden) {
687 lineDataSeries.clear();
688 const yVals = initialZValues[Math.floor(hline.y1)];
689 if (yVals) {
690 lineDataSeries.appendRange(lineXValues, yVals);
691 }
692 }
693 if (!vline.isHidden) {
694 vlineDataSeries.clear();
695 const yVals = initialZValues.map((r) => r[Math.floor(vline.x1)]);
696 if (yVals) {
697 vlineDataSeries.appendRange(vlineXValues, yVals);
698 }
699 }
700
701 // update outputs
702 for (const output of outputs) {
703 const outputDS = outputSurface.renderableSeries.getById(output.id).dataSeries as XyDataSeries;
704 outputDS.update(time / timestep, initialZValues[Math.floor(output.y1)][Math.floor(output.x1)]);
705 }
706
707 time = (time + timestep) % 30000;
708 timerId = setTimeout(updateChart, 20);
709 };
710
711 // Buttons for chart
712 const startUpdate = () => {
713 console.log("start animation");
714 updateChart();
715 };
716
717 const stopUpdate = () => {
718 console.log("stop animation");
719 clearTimeout(timerId);
720 timerId = undefined;
721 };
722
723 mainSurface.addDeletable({
724 delete: () => stopUpdate(),
725 });
726
727 const showHelp = () => {
728 const anim = getHelpAnnotation(
729 "This heatmap is running a 2D wave simulation. Colours correspond to wave height. Drag the Damping marker to adjust how long the waves last.",
730 mainSurface
731 );
732 mainSurface.addAnimation(anim.startAnim);
733 const anim2 = getHelpAnnotation(
734 "This shows the cross section of the waveforms at the horizontal and vertical orange lines on the heatmap.\nTry Dragging the orange lines.",
735 crossSectionSurface
736 );
737 anim.next.onNext = () => mainSurface.addAnimation(anim2.startAnim);
738 const anim3 = getHelpAnnotation(
739 "The boxes are input locations. Drag to move them around, or click, then drag in the white circle to resize.",
740 mainSurface
741 );
742 anim2.next.onNext = () => mainSurface.addAnimation(anim3.startAnim);
743 const anim4 = getHelpAnnotation(
744 "These series drive the corresponding colored inputs. Drag the frequency markers to set a regular driving input, or click and drag to edit the series directly.",
745 inputSurface
746 );
747 anim3.next.onNext = () => mainSurface.addAnimation(anim4.startAnim);
748 const anim5 = getHelpAnnotation(
749 "This chart records the values over time at the coloured output circles on the heatmap. You can drag those around too. Click on one to highlight its associated series.",
750 outputSurface
751 );
752 anim4.next.onNext = () => mainSurface.addAnimation(anim5.startAnim);
753 const anim6 = getHelpAnnotation(
754 "You can add additional inputs and outputs by left clicking on the heatmap and selecting one of the options. This is all done with annotations and a custom modifier. Right click on an input or output to remove it.",
755 mainSurface
756 );
757 anim5.next.onNext = () => mainSurface.addAnimation(anim6.startAnim);
758 };
759 showHelp();
760
761 const clearHeatmap = () => {
762 initialZValues = Array.from(Array(height), (_) => Array(width).fill(0));
763 velocities = Array.from(Array(height), (_) => Array(width).fill(0));
764 while (inputs.length > 0) {
765 const i = inputs[0];
766 removeInput(i);
767 }
768 while (outputs.length > 0) {
769 const o = outputs[0];
770 removeOutput(o);
771 }
772 };
773
774 const twoPoint = () => {
775 stopUpdate();
776 clearHeatmap();
777 addInput(width / 4, height / 2);
778 addInput((3 * width) / 4, height / 2);
779 addOutput((3 * width) / 4, (3 * height) / 4);
780 addOutput(width / 4, height / 4);
781 startUpdate();
782 };
783
784 const interference = () => {
785 stopUpdate();
786 clearHeatmap();
787 const sep = 20;
788 const gap = 10;
789 const outer = ((height - sep) / 2 - gap) / 2;
790 addInput(width / 2, height / 2, 2, sep / 2, 0);
791 addInput(width / 2, outer, 2, outer, 0);
792 addInput(width / 2, height - outer, 2, outer, 0);
793 addInput(width / 3, height / 2, 4, height / 3, 25000);
794 addOutput((3 * width) / 4, height / 2);
795 addOutput((3 * width) / 4, height / 3);
796 addOutput((3 * width) / 4, height / 4);
797 vline.x1 = (3 * width) / 4;
798 startUpdate();
799 };
800
801 twoPoint();
802
803 return {
804 startUpdate,
805 stopUpdate,
806 showHelp,
807 twoPoint,
808 interference,
809 };
810 };
811
812 return {
813 initMainChart,
814 initCrossSectionChart,
815 inputChart,
816 initHistoryChart,
817 onAllChartsInit,
818 };
819};
820
821const getHelpAnnotation = (text: string, surface: SciChartSurface) => {
822 const ann = new NativeTextAnnotation({
823 x1: 0.5,
824 y1: 0.5,
825 xCoordinateMode: ECoordinateMode.Relative,
826 yCoordinateMode: ECoordinateMode.Relative,
827 verticalAnchorPoint: EVerticalAnchorPoint.Center,
828 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
829 text,
830 opacity: 0,
831 fontSize: 18,
832 textColor: appTheme.ForegroundColor,
833 wrapTo: EWrapTo.ViewRect,
834 });
835 const next = {
836 onNext: () => {},
837 };
838 const startAnim = new GenericAnimation<number>({
839 from: 0,
840 to: 1,
841 duration: 500,
842 onAnimate(from, to, progress) {
843 if (!surface.annotations.contains(ann)) {
844 surface.annotations.add(ann);
845 } else {
846 ann.opacity = progress;
847 }
848 },
849 onCompleted() {
850 surface.addAnimation(
851 new GenericAnimation<number>({
852 delay: 4000,
853 duration: 500,
854 from: 1,
855 to: 0,
856 onAnimate(from, to, progress) {
857 ann.opacity = 1 - progress;
858 },
859 onCompleted() {
860 ann.delete();
861 surface.annotations.remove(ann);
862 if (next.onNext) next.onNext();
863 },
864 })
865 );
866 },
867 });
868 return { startAnim, next };
869};
870
871class YPalette extends BasePaletteProvider implements IStrokePaletteProvider {
872 public strokePaletteMode: EStrokePaletteMode.GRADIENT;
873 private dataSeries: XyDataSeries;
874 private wasmContext: TSciChart;
875 private colorData: number[];
876
877 constructor(wasmContext: TSciChart, stops: TGradientStop[]) {
878 super();
879 this.wasmContext = wasmContext;
880 this.colorData = PaletteFactory.createColorMap(wasmContext, stops);
881 }
882
883 public override onAttached(parentSeries: IRenderableSeries): void {
884 this.dataSeries = parentSeries.dataSeries as XyDataSeries;
885 }
886
887 public override onDetached(): void {}
888
889 public overrideStrokeArgb(xValue: number, yValue: number, index: number): number {
890 const y = this.dataSeries.getNativeYValues().get(index);
891 const lerpFactor = (y + 75) / 150;
892 const mapIndex = this.wasmContext.NumberUtil.Constrain(
893 Math.round(lerpFactor * (this.colorData.length - 1)),
894 0,
895 this.colorData.length - 1
896 );
897 const result = this.colorData[mapIndex];
898 return result;
899 }
900}
901
This demo showcases the incredible realtime performance of our Angular charts by updating the series with millions of data-points!
This demo showcases the incredible performance of our Angular Chart by loading 500 series with 500 points (250k points) instantly!
This demo showcases the incredible performance of our JavaScript Chart by loading a million points instantly.
This demo showcases the realtime performance of our Angular Chart by animating several series with thousands of data-points at 60 FPS
Demonstrating the capability of SciChart.js to create a JavaScript Audio Analyzer and visualize the Fourier-Transform of an audio waveform in realtime.
Demonstrates how to create Oil and Gas Dashboard
This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!
This dashboard demo showcases the incredible realtime performance of our Angular charts by updating the series with millions of data-points!
Demonstrates a custom modifier which can convert from single chart to grid layout and back.
Demonstrates how to repurpose a Candlestick Series into dragabble, labled, event markers
Population Pyramid of Europe and Africa