React Chart with lines split by thresholds

Demonstrates how to split lines into multiple segments so they can be individually colored according to thresholds, using SciChart.js, High Performance JavaScript Charts. This uses a RenderDataTransform to calculate the intersections between the data and the thresholds and add additional points.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    BaseRenderableSeries,
3    BaseRenderDataTransform,
4    DefaultPaletteProvider,
5    ECoordinateMode,
6    EHorizontalAnchorPoint,
7    EllipsePointMarker,
8    EStrokePaletteMode,
9    EVerticalAnchorPoint,
10    FastLineRenderableSeries,
11    HorizontalLineAnnotation,
12    IPointMetadata,
13    IPointSeries,
14    MouseWheelZoomModifier,
15    NativeTextAnnotation,
16    NumberRange,
17    NumericAxis,
18    ObservableArrayBase,
19    ObservableArrayChangedArgs,
20    parseColorToUIntArgb,
21    RenderPassData,
22    RolloverModifier,
23    SciChartJsNavyTheme,
24    SciChartSurface,
25    TSciChart,
26    vectorToArrayViewF64,
27    XyDataSeries,
28    XyPointSeriesResampled,
29    ZoomExtentsModifier,
30    ZoomPanModifier,
31    DoubleVectorProvider,
32} from "scichart";
33
34import { appTheme } from "../../../theme";
35
36class ThresholdRenderDataTransform extends BaseRenderDataTransform<XyPointSeriesResampled> {
37    public thresholds: ObservableArrayBase<number> = new ObservableArrayBase();
38
39    public constructor(parentSeries: BaseRenderableSeries, wasmContext: TSciChart, thresholds: number[]) {
40        super(parentSeries, wasmContext, [parentSeries.drawingProviders[0]]);
41        this.thresholds.add(...thresholds);
42        this.onThresholdsChanged = this.onThresholdsChanged.bind(this);
43        this.thresholds.collectionChanged.subscribe(this.onThresholdsChanged);
44    }
45
46    private onThresholdsChanged(data: ObservableArrayChangedArgs) {
47        this.requiresTransform = true;
48        if (this.parentSeries.invalidateParentCallback) {
49            this.parentSeries.invalidateParentCallback();
50        }
51    }
52
53    public delete(): void {
54        this.thresholds.collectionChanged.unsubscribeAll();
55        super.delete();
56    }
57
58    protected createPointSeries(): XyPointSeriesResampled {
59        return new XyPointSeriesResampled(this.wasmContext, new NumberRange(0, 0));
60    }
61    protected runTransformInternal(renderPassData: RenderPassData): IPointSeries {
62        const numThresholds = this.thresholds.size();
63        if (numThresholds === 0) {
64            return renderPassData.pointSeries;
65        }
66        const { xValues: oldX, yValues: oldY, indexes: oldI, resampled } = renderPassData.pointSeries;
67        const iStart = resampled ? 0 : renderPassData.indexRange.min;
68        const iEnd = resampled ? oldX.size() - 1 : renderPassData.indexRange?.max;
69        // Create views over the source and target vectors for fast access.  These views are only valid as long as there is no memory allocation
70        const oldXView = vectorToArrayViewF64(oldX, this.wasmContext);
71        const oldYView = vectorToArrayViewF64(oldY, this.wasmContext);
72        const oldIndexView = vectorToArrayViewF64(oldI, this.wasmContext);
73
74        // We do not know the number of output points, so we cannot fast write to views on the output.  Instead we write to temporary arrays and then fast append them to the output
75        const xout: number[] = [];
76        const yout: number[] = [];
77        const iout: number[] = [];
78
79        // This is the index of the threshold we are currently under.
80        let level = 0;
81        let lastY = oldXView[iStart];
82        // Find the starting level
83        for (let t = 0; t < numThresholds; t++) {
84            if (lastY > this.thresholds.get(t)) {
85                level++;
86            }
87        }
88        let lastX = oldXView[iStart];
89        xout.push(lastX);
90        yout.push(lastY);
91        iout.push(0);
92        let newI = 0;
93        for (let i = iStart + 1; i <= iEnd; i++) {
94            const y = oldYView[i];
95            const x = oldXView[i];
96            if (level > 0 && lastY > this.thresholds.get(level - 1)) {
97                if (y === this.thresholds.get(level - 1)) {
98                    // decrease level but don't add a point
99                    level--;
100                }
101                while (y < this.thresholds.get(level - 1)) {
102                    // go down
103                    const t = this.thresholds.get(level - 1);
104                    // interpolate to find intersection
105                    const f = (lastY - t) / (lastY - y);
106                    const xNew = lastX + (x - lastX) * f;
107                    newI++;
108                    xout.push(xNew);
109                    yout.push(t);
110                    // use original data index so metadata works
111                    iout.push(i);
112                    // potentially push additional data to extra vectors to identify threshold level
113                    console.log(lastX, lastX, x, y, t, f, xNew);
114                    level--;
115                    if (level === 0) break;
116                }
117            }
118            if (level < numThresholds && lastY <= this.thresholds.get(level)) {
119                if (y === this.thresholds.get(level)) {
120                    // increase level but don't add a point
121                    level++;
122                }
123                while (y > this.thresholds.get(level)) {
124                    // go up
125                    const t = this.thresholds.get(level);
126                    const f = (t - lastY) / (y - lastY);
127                    const xNew = lastX + (x - lastX) * f;
128                    newI++;
129                    xout.push(xNew);
130                    yout.push(t);
131                    iout.push(i);
132                    console.log(lastX, lastX, x, y, t, f, xNew);
133                    level++;
134                    if (level === numThresholds) break;
135                }
136            }
137            lastY = y;
138            lastX = x;
139            newI++;
140            xout.push(lastX);
141            yout.push(lastY);
142            iout.push(newI);
143        }
144
145        const { xValues, yValues, indexes } = this.pointSeries;
146        // Clear the destination vectors and fast append the result
147        xValues.clear();
148        yValues.clear();
149        indexes.clear();
150        const dvp = new DoubleVectorProvider();
151        dvp.appendArray(this.wasmContext, xValues, xout);
152        dvp.appendArray(this.wasmContext, yValues, yout);
153        dvp.appendArray(this.wasmContext, indexes, iout);
154        return this.pointSeries;
155    }
156}
157
158const colorNames = [appTheme.MutedTeal, appTheme.MutedBlue, appTheme.MutedOrange, appTheme.MutedRed];
159const colors = colorNames.map((c) => parseColorToUIntArgb(c));
160
161class ThresholdPaletteProvider extends DefaultPaletteProvider {
162    strokePaletteMode = EStrokePaletteMode.SOLID;
163    lastY: number;
164    public thresholds: number[];
165
166    public override get isRangeIndependant(): boolean {
167        return true;
168    }
169
170    public constructor(thresholds: number[]) {
171        super();
172        this.thresholds = thresholds;
173    }
174
175    overrideStrokeArgb(
176        xValue: number,
177        yValue: number,
178        index: number,
179        opacity: number,
180        metadata: IPointMetadata
181    ): number {
182        if (index == 0) {
183            this.lastY = yValue;
184        }
185        for (let i = 0; i < this.thresholds.length; i++) {
186            const threshold = this.thresholds[i];
187            if (yValue <= threshold && this.lastY <= threshold) {
188                this.lastY = yValue;
189                //console.log(index, yValue, i);
190                return colors[i];
191            }
192        }
193        this.lastY = yValue;
194        //console.log(index, yValue, this.thresholds.length);
195        return colors[this.thresholds.length];
196    }
197}
198
199export const drawExample = async (rootElement: string | HTMLDivElement) => {
200    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
201        theme: new SciChartJsNavyTheme(),
202    });
203    // sciChartSurface.debugRendering = true;
204    const xAxis = new NumericAxis(wasmContext, {
205        growBy: new NumberRange(0.025, 0.025),
206    });
207    sciChartSurface.xAxes.add(xAxis);
208
209    const yAxis = new NumericAxis(wasmContext, {
210        growBy: new NumberRange(0.05, 0.05),
211    });
212    sciChartSurface.yAxes.add(yAxis);
213
214    const lineSeries = new FastLineRenderableSeries(wasmContext, {
215        pointMarker: new EllipsePointMarker(wasmContext, {
216            stroke: "black",
217            strokeThickness: 0,
218            fill: "black",
219            width: 10,
220            height: 10,
221        }),
222        dataLabels: {
223            style: {
224                fontFamily: "Arial",
225                fontSize: 10,
226            },
227            color: "white",
228        },
229        strokeThickness: 5,
230    });
231    sciChartSurface.renderableSeries.add(lineSeries);
232
233    const thresholds = [1.5, 3, 5];
234    const transform = new ThresholdRenderDataTransform(lineSeries, wasmContext, thresholds);
235    lineSeries.renderDataTransform = transform;
236    const paletteProvider = new ThresholdPaletteProvider(thresholds);
237    lineSeries.paletteProvider = paletteProvider;
238
239    const dataSeries = new XyDataSeries(wasmContext, {
240        xValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
241        yValues: [0, 0.8, 2, 3, 6, 4, 1, 1, 7, 5, 4],
242    });
243
244    lineSeries.dataSeries = dataSeries;
245
246    const makeThresholdAnnotation = (i: number) => {
247        const thresholdAnn = new HorizontalLineAnnotation({
248            isEditable: true,
249            stroke: colorNames[i + 1],
250            y1: thresholds[i],
251            showLabel: true,
252            strokeThickness: 3,
253            axisLabelFill: colorNames[i + 1],
254        });
255        thresholdAnn.dragDelta.subscribe((args) => {
256            if (
257                (i < colorNames.length - 2 && thresholdAnn.y1 >= thresholds[i + 1]) ||
258                (i > 0 && thresholdAnn.y1 <= thresholds[i - 1])
259            ) {
260                // Prevent reordering thresholds
261                thresholdAnn.y1 = thresholds[i];
262            } else {
263                // Update threshold from annotation position
264                thresholds[i] = thresholdAnn.y1;
265                paletteProvider.thresholds = thresholds;
266                transform.thresholds.set(i, thresholdAnn.y1);
267            }
268        });
269        sciChartSurface.annotations.add(thresholdAnn);
270    };
271    for (let i = 0; i < thresholds.length; i++) {
272        makeThresholdAnnotation(i);
273    }
274
275    sciChartSurface.annotations.add(
276        new NativeTextAnnotation({
277            xCoordinateMode: ECoordinateMode.Pixel,
278            yCoordinateMode: ECoordinateMode.Pixel,
279            x1: 20,
280            y1: 20,
281            horizontalAnchorPoint: EHorizontalAnchorPoint.Left,
282            verticalAnchorPoint: EVerticalAnchorPoint.Top,
283            text: "Drag the horizontal lines to adjust the thresholds",
284            fontSize: 16,
285            textColor: appTheme.ForegroundColor,
286        })
287    );
288
289    sciChartSurface.chartModifiers.add(new ZoomPanModifier({ enableZoom: true }));
290    sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
291    sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());
292
293    sciChartSurface.zoomExtents();
294    return { sciChartSurface, wasmContext };
295};
296

Line Splitting Thresholds - React

Overview

This example demonstrates how to split a line into multiple segments that are individually colored based on threshold values. Built using SciChart.js in a React application, it enables dynamic, real-time updates as users interact with the chart through draggable threshold annotations.

Technical Implementation

The core of this example lies in extending the BaseRenderDataTransform to create a custom render data transform that integrates coordinate interpolation for threshold intersections. This transform calculates where the line data crosses predefined thresholds and injects additional points to enable segmented coloring. The implementation leverages The RenderDataTransform API for its advanced transformation capabilities. In addition, a custom PaletteProvider is implemented to assign colors dynamically based on the current threshold level. This approach to per-point coloring follows The PaletteProvider API guidelines and ensures smooth visual transitions. Performance optimizations are addressed through the use of FastLineRenderableSeries, which ensures high rendering performance even when managing additional points for threshold intersections.

Features and Capabilities

The example provides several advanced features and capabilities including:

  • Real-Time Updates: As users drag the horizontal threshold annotations, the render data transform recalculates intersections and updates the line segments instantly.
  • Dynamic Coloring: The custom palette provider assigns colors to the line segments based on the current threshold levels, enabling clear visual delineation between data segments.
  • Interactive Annotations: The use of draggable HorizontalLineAnnotation components allows for intuitive threshold adjustments, further enhancing user interactivity.
  • Efficient Data Handling: Utilizing observable arrays, the code efficiently manages threshold updates through the collectionChanged event of ObservableArrayBase. For more on observable arrays, see the ObservableArrayBase documentation.

Integration and Best Practices

Integration in a React environment is achieved via the <SciChartReact/> component, making it easy to embed SciChart.js charts within modern React applications. The example follows best practices for React integration by encapsulating chart initialization and using modular components for a clean, maintainable architecture. Developers are encouraged to review the provided documentation on interactive annotations and performance optimization techniques when adapting these patterns for their own applications. Furthermore, the threshold interpolation logic demonstrated here serves as an excellent guide for implementing more complex data transformation and real-time update scenarios in a React-based charting environment.

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