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.
drawExample.ts
index.tsx
theme.ts
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};
296This 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.
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.
The example provides several advanced features and capabilities including:
collectionChanged event of ObservableArrayBase. For more on observable arrays, see the ObservableArrayBase documentation.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.

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

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

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

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

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

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

Demonstrates dashed line series in React Charts with SciChart.js

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

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

Demonstrates chart title with different position and alignment options

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