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 an Angular implementation of SciChart.js where a line series is split into multiple segments to enable individual coloring based on specific threshold values. The solution leverages a custom RenderDataTransform to calculate and interpolate the intersections between the original data and defined thresholds, allowing for dynamic, real-time updates.
The core functionality is implemented by extending the SciChart.js API with a custom RenderDataTransform that detects when the line series crosses threshold levels and injects additional points using linear interpolation. This allows the visualization to precisely mark the threshold crossings. In addition, a custom PaletteProvider is implemented to assign colors to each segment based on its current threshold. For detailed guidance on creating custom render transforms in Angular, developers can check out the RenderDataTransforms API Documentation.
The example includes several advanced features:
PaletteProvider dynamically assigns colors to line segments based on their threshold levels using the PaletteProvider API.FastLineRenderableSeries and efficient observable event handling, the chart maintains excellent rendering performance even with added interpolation points.This Angular example demonstrates how to integrate SciChart.js seamlessly within an Angular application without relying on additional APIs like hooks or builder APIs. High-performance practices are emphasized through the use of FastLineRenderableSeries and observable arrays for efficient data handling. Developers are encouraged to review performance optimization strategies as detailed in the Performance Tips & Tricks documentation and adopt similar patterns in their own Angular integrations. By combining custom render transforms with interactive annotations and dynamic color assignment, the example serves as a comprehensive guide to implementing advanced charting features in Angular using SciChart.js.

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

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

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

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

Demonstrates dashed line series in Angular Charts with SciChart.js

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