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.html
vanilla.ts
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 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.
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.
The example showcases several advanced features:
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.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.

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

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

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

Demonstrates how to create a Custom Theme for a SciChart.js JavaScript 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 JavaScript Scatter charts (Square, Circle, Triangle and Custom image point-marker)

Demonstrates dashed line series in JavaScript Charts with SciChart.js

Show data labels on JavaScript 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 JavaScript Order of Rendering example gives you full control of the draw order of series and annotations for charts. Try SciChart's advanced customizations.