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

vanilla.ts

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 - JavaScript Example

Overview

This example demonstrates how to split a line series into multiple segments based on defined threshold values using SciChart.js in JavaScript. The approach allows each segment to be individually colored by detecting where the data crosses specified thresholds and then applying interpolation to insert new points.

Technical Implementation

The core of this example is a custom RenderDataTransform that calculates intersections between data points and thresholds. This technique follows the guidelines outlined in the RenderDataTransforms API documentation. In addition, a custom PaletteProvider is implemented to assign different colors to each segment based on the current threshold level, as described in the PaletteProvider API documentation. Observable arrays provided by SciChart.js, particularly ObservableArrayBase, are used to detect changes in threshold values and trigger re-rendering of the chart. The interpolation logic responsible for calculating the exact intersection points leverages standard linear interpolation techniques.

Features and Capabilities

The example showcases several advanced features:

  • Real-Time Updates: Updating threshold values via draggable horizontal annotations instantly recalculates the line segments.
  • Dynamic Coloring: The custom palette provider assigns different colors to each segment, enhancing visual clarity.
  • Rendering Performance: Utilizing the high-performance FastLineRenderableSeries ensures the chart remains responsive even with additional points created through interpolation. Developers interested in optimizing rendering can refer to the Performance Tips & Tricks.

Integration and Best Practices

Built with pure JavaScript, this example follows best integration practices for SciChart.js without relying on additional frameworks or builder APIs. Developers looking to integrate SciChart.js into standard web applications can benefit from the comprehensive guide available in Getting Started with SciChart JS. The modular structure and use of custom render transforms and palette providers make this example an excellent starting point for developing interactive, high-performance charts with dynamic data transformation capabilities.

javascript Chart Examples & Demos

See Also: Styling and Theming (11 Demos)

Chart Background Image with Transparency | SciChart.js

Chart Background Image with Transparency

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

Styling a JavaScript Chart in Code | SciChart.js Demo

Styling a JavaScript Chart in Code

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

Using Theme Manager in JavaScript Chart | SciChart.js

Using Theme Manager in JavaScript Chart

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

Create a Custom Theme for JavaScript Chart | SciChart.js

Create a Custom Theme for JavaScript Chart

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

Coloring Series per-point using the PaletteProvider

Coloring Series per-point using the PaletteProvider

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

JavaScript Point-Markers Chart | Javascript Charts | SciChart.js

JavaScript Point-Markers Chart

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

Dashed Line Styling | Javascript Charts | SciChart.js Demo

Dashed Line Styling

Demonstrates dashed line series in JavaScript Charts with SciChart.js

Data Labels | Javascript Charts | SciChart.js Demo

Data Labels

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

JavaScript Chart with Multi-Style Series | SciChart.js

JavaScript Chart with Multi-Style Series

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

JavaScript Chart Title | Javascript Charts | SciChart.js Demo

JavaScript Chart Title

Demonstrates chart title with different position and alignment options

NEW!
JavaScript Order of Rendering | Javascript Charts | SciChart.js

JavaScript Order of Rendering Example

The JavaScript Order of Rendering example gives you full control of the draw order of series and annotations for charts. Try SciChart's advanced customizations.

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