Custom Filters

Demonstrates simple and advanced Custom Filters, with realtime updates using SciChart.js, High Performance JavaScript Charts

Fullscreen

Edit

 Edit

Docs

drawExample.ts

angular.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import { appTheme } from "../../../theme";
2
3import {
4    BaseDataSeries,
5    NumericAxis,
6    EAutoRange,
7    EAxisAlignment,
8    ELabelAlignment,
9    FastColumnRenderableSeries,
10    FastLineRenderableSeries,
11    EllipsePointMarker,
12    LegendModifier,
13    NumberRange,
14    SciChartSurface,
15    XyCustomFilter,
16    XyDataSeries,
17    XyFilterBase,
18    XyScatterRenderableSeries,
19} from "scichart";
20
21// A custom filter which calculates the frequency distribution of the original data
22class AggregationFilter extends XyFilterBase {
23    private bins: Map<number, number> = new Map<number, number>();
24    private binWidthProperty = 1;
25
26    constructor(originalSeries: BaseDataSeries, binWidth: number, dataSeriesName: string) {
27        super(originalSeries, { dataSeriesName });
28        this.binWidthProperty = binWidth;
29        this.filterAll();
30    }
31
32    public get binWidth() {
33        return this.binWidthProperty;
34    }
35
36    public set binWidth(value: number) {
37        this.binWidthProperty = value;
38        this.filterAll();
39    }
40
41    protected filterAll() {
42        this.clear();
43        this.bins.clear();
44        this.filter(0, this.getOriginalCount());
45    }
46
47    protected override filterOnAppend(count: number): void {
48        // Overriding this so we do not have to reprocess the entire series on append
49        this.filter(this.getOriginalCount() - count, count);
50    }
51
52    protected filter(start: number, count: number): void {
53        const numUtil = this.originalSeries.webAssemblyContext.NumberUtil;
54        for (let i = start; i < start + count; i++) {
55            const bin = numUtil.RoundDown(this.getOriginalYValues().get(i), this.binWidth);
56            if (this.bins.has(bin)) {
57                const newVal = this.bins.get(bin) + 1;
58                this.bins.set(bin, newVal);
59            } else {
60                this.bins.set(bin, 1);
61            }
62        }
63        // Map data is unsorted, so we must sort it before recreating the output series
64        const keys = Array.from(this.bins.keys()).sort((a, b) => a - b);
65        this.clear();
66        const yValues: number[] = [];
67        for (const key of keys) {
68            yValues.push(this.bins.get(key));
69        }
70        this.appendRange(keys, yValues);
71    }
72
73    protected override onClear() {
74        this.clear();
75        this.bins.clear();
76    }
77}
78
79let lastX = 0;
80// Straight line data
81const getData = (n: number) => {
82    const xValues: number[] = [];
83    const yValues: number[] = [];
84    for (let i = 0; i < n; i++) {
85        xValues.push(lastX);
86        yValues.push(50 + lastX / 1000);
87        lastX++;
88    }
89    return { xValues, yValues };
90};
91
92export const drawExample = async (rootElement: string | HTMLDivElement) => {
93    // Define some constants
94    const numberOfPointsPerTimerTick = 500; // 1,000 points every timer tick
95    const timerInterval = 10; // timer tick every 10 milliseconds
96    const maxPoints = 100_000; // max points for a single series before the demo stops
97
98    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
99        theme: appTheme.SciChartJsTheme,
100    });
101    const rawXAxis = new NumericAxis(wasmContext, { id: "rawX", isVisible: false, autoRange: EAutoRange.Always });
102    const aggXAxis = new NumericAxis(wasmContext, {
103        id: "aggX",
104        axisTitle: "Value",
105        autoRange: EAutoRange.Always,
106        labelPrecision: 0,
107    });
108    sciChartSurface.xAxes.add(rawXAxis, aggXAxis);
109
110    const rawYAxis = new NumericAxis(wasmContext, {
111        autoRange: EAutoRange.Always,
112        axisTitle: "Raw Data",
113        id: "rawY",
114        labelPrecision: 0,
115        labelStyle: { alignment: ELabelAlignment.Right },
116    });
117    const aggYAxis = new NumericAxis(wasmContext, {
118        axisTitle: "Frequency (Aggregated)",
119        id: "aggY",
120        autoRange: EAutoRange.Always,
121        axisAlignment: EAxisAlignment.Left,
122        growBy: new NumberRange(0, 0.5),
123        labelPrecision: 0,
124    });
125    sciChartSurface.yAxes.add(aggYAxis, rawYAxis);
126
127    const dataSeries = new XyDataSeries(wasmContext, { dataSeriesName: "Original Data" });
128
129    // Create a simple custom filter.  We just have to specify the filter function and this will be applied efficiently to data changes
130    const gaussFilter = new XyCustomFilter(dataSeries, { dataSeriesName: "Custom Filter: Original x Gaussian Random" });
131    // This function exploits the central limit theorem to approximate a normal distribution
132    const gaussianRand = () => {
133        let rand = 0;
134        for (let i = 0; i < 6; i += 1) {
135            rand += Math.random() + 0.5;
136        }
137        return rand / 6;
138    };
139    gaussFilter.filterFunction = (i, y) => y * gaussianRand();
140
141    // Add the randomised data using a custom filter which takes original data * random value
142    sciChartSurface.renderableSeries.add(
143        new XyScatterRenderableSeries(wasmContext, {
144            pointMarker: new EllipsePointMarker(wasmContext, {
145                width: 3,
146                height: 3,
147                strokeThickness: 0,
148                fill: appTheme.VividOrange,
149                opacity: 0.77,
150            }),
151            stroke: appTheme.VividOrange,
152            dataSeries: gaussFilter,
153            xAxisId: "rawX",
154            yAxisId: "rawY",
155        })
156    );
157
158    // Add the original data to the chart
159    sciChartSurface.renderableSeries.add(
160        new FastLineRenderableSeries(wasmContext, {
161            dataSeries,
162            stroke: appTheme.VividTeal,
163            strokeThickness: 3,
164            xAxisId: "rawX",
165            yAxisId: "rawY",
166        })
167    );
168
169    // Pass the randomised data into the aggregation filter.
170    const aggFilter = new AggregationFilter(gaussFilter, 5, "Custom Filter: Aggregation");
171
172    // Plot the aggregation filter as a column chart
173    sciChartSurface.renderableSeries.add(
174        new FastColumnRenderableSeries(wasmContext, {
175            id: "col",
176            fill: appTheme.VividSkyBlue + "33",
177            stroke: appTheme.MutedSkyBlue,
178            dataSeries: aggFilter,
179            xAxisId: "aggX",
180            yAxisId: "aggY",
181            cornerRadius: 10,
182        })
183    );
184
185    let timerId: NodeJS.Timeout;
186
187    // Function called when the user clicks stopUpdate button
188    const stopUpdate = () => {
189        clearTimeout(timerId);
190        timerId = undefined;
191        lastX = 0;
192    };
193
194    // Function called when the user clicks startUpdate button
195    const startUpdate = () => {
196        if (timerId) {
197            stopUpdate();
198            dataSeries.clear();
199        }
200        const updateFunc = () => {
201            if (dataSeries.count() >= maxPoints) {
202                stopUpdate();
203                return;
204            }
205
206            // Get the next N random walk x,y values
207            const { xValues, yValues } = getData(numberOfPointsPerTimerTick);
208            // Append these to the dataSeries. This will cause the chart to redraw
209            dataSeries.appendRange(xValues, yValues);
210
211            timerId = setTimeout(updateFunc, timerInterval);
212        };
213
214        dataSeries.clear();
215
216        timerId = setTimeout(updateFunc, timerInterval);
217    };
218
219    sciChartSurface.chartModifiers.add(new LegendModifier());
220
221    return { wasmContext, sciChartSurface, controls: { startUpdate, stopUpdate } };
222};
223

Custom Filters (Custom Filters in Angular)

Overview

This example demonstrates advanced use of custom filters within the SciChart.js library integrated into an Angular standalone component. The purpose is to showcase real-time data transformations and aggregations by applying custom filtering algorithms to streaming data.

Technical Implementation

The application leverages Angular event binding with the SciChart Angular component to handle asynchronous chart initialization and real-time updates via the Angular lifecycle hooks onInit and onDelete as documented in Component Lifecycle - Angular. A custom aggregation filter is implemented by extending the SciChart.js XyFilterBase, which computes frequency distributions from data series. Additionally, a Gaussian randomization filter is applied using the XyCustomFilter functionality, aligning with the guidelines provided in the SciChart.js Custom Filters Documentation.

Features and Capabilities

The example supports high-frequency real-time updates by using a timer-based mechanism to append data points to the series, triggering dynamic updates in the rendered line, scatter, and column charts. This real-time data management is optimized using WebAssembly acceleration, a technique further detailed in the Adding Realtime Updates | JavaScript Chart Documentation - SciChart.

Integration and Best Practices

Angular integration is achieved using the scichart-angular component, which simplifies embedding complex charts into Angular applications. The use of lifecycle hooks ensures that the chart initialization and cleanup are performed efficiently, following Angular best practices as described in Component Lifecycle - Angular. Developers are encouraged to review the Getting Started with SciChart JS guide for additional context on setting up high-performance charts and consult the Performance Tips & Tricks | JavaScript Chart Documentation for strategies on optimizing real-time updates in their applications.

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.