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