Custom Filters

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

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

vanilla.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 - JavaScript

Overview

This example demonstrates advanced custom filters within the SciChart.js library using JavaScript. The implementation showcases real-time data transformations by applying a Gaussian randomization filter and a bespoke aggregation filter that extends the XyFilterBase class to compute frequency distributions.

Technical Implementation

The custom filtering mechanism is implemented by subclassing XyFilterBase to create an aggregation filter, and by applying a transformation function via XyCustomFilter that utilizes a Gaussian random function. Real-time updates are executed using a timer-based approach with setTimeout, as detailed in the Adding Realtime Updates guide. The example also leverages WebAssembly acceleration for high-frequency data processing, a key aspect for performance optimization as described in the Performance Tips & Tricks.

Features and Capabilities

Key features include real-time updates with efficient batch data appending and advanced data transformation techniques. The Gaussian randomization modifies the incoming data series before it is aggregated, while the custom aggregation filter bins the data to produce a frequency distribution. This dual filtering approach enables developers to visualize both raw and processed data concurrently, ensuring a high level of interactivity and responsiveness.

Integration and Best Practices

Focusing solely on JavaScript, the example adheres to robust object-oriented design patterns by extending base filtering classes without relying on framework-specific hooks or builder APIs. The implementation follows best practices outlined in the SciChart.js JavaScript Charts User Manual to build high-performance, real-time charting applications. Developers are encouraged to explore these techniques further to optimize large-scale data visualization solutions.

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