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

See Also: Styling and Theming (10 Demos)

Background Image with Transparency | SciChart.js Demo

Background Image with Transparency

Demonstrates how to create a React Chart with background image using transparency in SciChart.js

Styling a React Chart in Code | SciChart.js Demo

Styling a React Chart in Code

Demonstrates how to style a React Chart entirely in code with SciChart.js themeing API

Using Theme Manager in React Chart | SciChart.js Demo

Using Theme Manager in React Chart

Demonstrates our Light and Dark Themes for React Charts with SciChart.js ThemeManager API

Create a Custom Theme for React Chart | SciChart.js Demo

Create a Custom Theme for React Chart

Demonstrates how to create a Custom Theme for a SciChart.js React Chart using our Theming API

Coloring Series per-point using the PaletteProvider | SciChart.js Demo

Coloring Series per-point using the PaletteProvider

Demonstrates per-point coloring in JavaScript chart types with SciChart.js PaletteProvider API

React Point-Markers Chart | SciChart.js Demo

React Point-Markers Chart

Demonstrates the different point-marker types for React Scatter charts (Square, Circle, Triangle and Custom image point-marker)

Dashed Line Styling | SciChart.js Demo

Dashed Line Styling

Demonstrates dashed line series in React Charts with SciChart.js

Data Labels | SciChart.js Demo

Data Labels

Show data labels on React Chart. Get your free demo now.

React Chart with Multi-Style Series | SciChart.js Demo

React Chart with Multi-Style Series

Demonstrates how to apply multiple different styles to a single series using RenderDataTransform

React Chart Title | SciChart.js Demo

React Chart Title

Demonstrates chart title with different position and alignment options

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