Extend PolyLineAnnotation with custom paired dash-line rendering in React.
drawExample.ts
index.tsx
tradingAnnotationExampleUtils.ts
1import {
2 CoordinateCalculatorBase,
3 deleteSafe,
4 EAnnotationClippingMode,
5 EBaseType,
6 ETextAlignment,
7 EVerticalTextPosition,
8 IRenderContext2D,
9 Rect,
10 registerType,
11 SciChartSurface,
12 Thickness,
13} from "scichart";
14import {
15 MultiPointAnnotationPlacementModifier,
16 EAnnotationVisibilityMode,
17 EMultiPointLabelAnchorMode,
18 EAxisLabelDrawMode,
19 ESegmentLabelRotationMode,
20 PolyLineAnnotation,
21 IPolyLineAnnotationOptions,
22 IMultiPointLabelStyleFormatParams
23} from "scichart-financial-tools";
24import {
25 addDefaultFinancialModifiers,
26 createFinancialChart,
27 createTradingAnnotationOptions,
28} from "../_shared/tradingAnnotationExampleUtils";
29
30const PAIRED_DASHED_POLYLINE = "PairedDashedPolylineAnnotation";
31
32const PAIRED_POLYLINE_COLORS = {
33 primaryStroke: "#4F8EF7",
34 primaryFill: "#4F8EF724",
35 primaryConnector: "#A7C7FF",
36
37 secondaryStroke: "#B77AF7",
38 secondaryFill: "#B77AF720",
39 secondaryConnector: "#F8FAFC",
40
41 placementStroke: "#16A34A",
42 placementFill: "#16A34A24",
43 placementConnector: "#86EFAC",
44};
45
46const DEFAULT_PAIR_STROKE_DASH_ARRAY = [6, 4];
47
48const normalizePlacementPointCount = (pointCount = 5) => {
49 const safeNumber = Number.isFinite(pointCount) ? Math.floor(pointCount) : 5;
50
51 return Math.max(3, Math.min(9, safeNumber));
52};
53
54interface IPairedDashedPolylineAnnotationOptions extends IPolyLineAnnotationOptions {
55 pairStroke?: string;
56 pairStrokeThickness?: number;
57 pairStrokeDashArray?: number[];
58 showPairConnectors?: boolean;
59 placementPointCount?: number;
60}
61
62// @ts-ignore - placementPointCountProperty is private in `PolyLineAnnotation`
63class PairedDashedPolylineAnnotation extends PolyLineAnnotation {
64 public readonly type = PAIRED_DASHED_POLYLINE as any;
65
66 private pairStrokeProperty = PAIRED_POLYLINE_COLORS.primaryConnector;
67 private pairStrokeThicknessProperty = 1.5;
68 private pairStrokeDashArrayProperty = DEFAULT_PAIR_STROKE_DASH_ARRAY;
69 private showPairConnectorsProperty = true;
70 // @ts-ignore - placementPointCountProperty is private in `PolyLineAnnotation`
71 private placementPointCountProperty = 5;
72 private pairStrokePenCache: any;
73
74 constructor(options?: IPairedDashedPolylineAnnotationOptions) {
75 super(options);
76
77 this.pairStrokeProperty = options?.pairStroke ?? this.pairStrokeProperty;
78 this.pairStrokeThicknessProperty = options?.pairStrokeThickness ?? this.pairStrokeThicknessProperty;
79 this.pairStrokeDashArrayProperty = options?.pairStrokeDashArray ?? this.pairStrokeDashArrayProperty;
80 this.showPairConnectorsProperty = options?.showPairConnectors ?? this.showPairConnectorsProperty;
81 this.placementPointCountProperty = normalizePlacementPointCount(
82 options?.placementPointCount ?? this.placementPointCountProperty
83 );
84 }
85
86 public get showPairConnectors(): boolean {
87 return this.showPairConnectorsProperty;
88 }
89
90 public set showPairConnectors(value: boolean) {
91 this.showPairConnectorsProperty = !!value;
92 this.notifyPropertyChanged("showPairConnectors");
93 }
94
95 public override onAttach(scs: SciChartSurface): void {
96 super.onAttach(scs);
97
98 this.pairStrokePenCache = this.updatePenCache(
99 this.pairStrokePenCache,
100 this.pairStrokeProperty,
101 this.pairStrokeThicknessProperty,
102 this.opacity,
103 this.pairStrokeDashArrayProperty
104 );
105 }
106
107 public override delete(): void {
108 this.pairStrokePenCache = deleteSafe(this.pairStrokePenCache);
109 super.delete();
110 }
111
112 public override drawWithContext(
113 renderContext: IRenderContext2D,
114 xCalc: CoordinateCalculatorBase,
115 yCalc: CoordinateCalculatorBase,
116 seriesViewRect: Rect,
117 surfaceViewRect: Rect,
118 chartViewRect: Rect
119 ): void {
120 super.drawWithContext(renderContext, xCalc, yCalc, seriesViewRect, surfaceViewRect, chartViewRect);
121
122 if (!this.showPairConnectorsProperty || this.points.length < 4) {
123 return;
124 }
125
126 const pixelPoints = this.getPixelPoints(xCalc, yCalc);
127 const clipRect = this.getClippingRect(this.clipping, seriesViewRect, surfaceViewRect, chartViewRect);
128 const pairPen = this.resolvePenFromCache(
129 this.pairStrokePenCache,
130 this.pairStrokeThicknessProperty,
131 true
132 );
133
134 if (!pairPen) return;
135
136 this.getConnectorPairs().forEach(([startIndex, endIndex]) => {
137 const start = pixelPoints[startIndex];
138 const end = pixelPoints[endIndex];
139
140 if (!start || !end) return;
141
142 renderContext.drawLines([start.x, start.y, end.x, end.y], pairPen, seriesViewRect, clipRect);
143 });
144 }
145
146 private getConnectorPairs(): Array<readonly [number, number]> {
147 const lastPointIndex = this.points.length - 1;
148 const pairs: Array<readonly [number, number]> = [];
149
150 for (let startIndex = 1; startIndex + 2 <= lastPointIndex; startIndex += 2) {
151 pairs.push([startIndex, startIndex + 2]);
152 }
153
154 if (lastPointIndex > 0) {
155 pairs.push([0, lastPointIndex]);
156 }
157
158 return pairs;
159 }
160
161 public override toJSON() {
162 const json = super.toJSON();
163
164 Object.assign(json.options, {
165 pairStroke: this.pairStrokeProperty,
166 pairStrokeThickness: this.pairStrokeThicknessProperty,
167 pairStrokeDashArray: this.pairStrokeDashArrayProperty,
168 showPairConnectors: this.showPairConnectorsProperty,
169 placementPointCount: this.placementPointCountProperty,
170 });
171
172 return json;
173 }
174
175 protected override getPlacementPointCountInternal(): number {
176 return this.placementPointCountProperty;
177 }
178
179 protected override notifyPropertyChanged(propertyName: string): void {
180 super.notifyPropertyChanged(propertyName);
181
182 if (["pairStroke", "pairStrokeThickness", "pairStrokeDashArray", "opacity"].includes(propertyName)) {
183 this.pairStrokePenCache = this.updatePenCache(
184 this.pairStrokePenCache,
185 this.pairStrokeProperty,
186 this.pairStrokeThicknessProperty,
187 this.opacity,
188 this.pairStrokeDashArrayProperty
189 );
190 }
191 }
192}
193
194registerType(
195 EBaseType.Annotation,
196 PAIRED_DASHED_POLYLINE,
197 (options?: IPairedDashedPolylineAnnotationOptions) => new PairedDashedPolylineAnnotation(options),
198 true
199);
200
201const createConnectorLabels = (pointCount: number, prefix: string, drawPointLabels: boolean = true) => {
202 const lastPointIndex = pointCount - 1;
203 const labels: any[] = [];
204
205 for (let startIndex = 1; startIndex + 2 <= lastPointIndex; startIndex += 2) {
206 labels.push({
207 id: `${prefix}-peak-${startIndex + 1}-${startIndex + 3}`,
208 anchorMode: EMultiPointLabelAnchorMode.Segment,
209 segmentStartIndex: startIndex,
210 segmentEndIndex: startIndex + 2,
211 segmentRatio: 0.5,
212 segmentLabelRotationMode: ESegmentLabelRotationMode.Parallel,
213 verticalTextPosition: EVerticalTextPosition.Above,
214 alignment: ETextAlignment.Center,
215 text: `Peak ${startIndex + 1} -> ${startIndex + 3}`,
216 padding: new Thickness(2, 8, 2, 8),
217 });
218 }
219
220 labels.push({
221 id: `${prefix}-odd-1-${pointCount}`,
222 anchorMode: EMultiPointLabelAnchorMode.Segment,
223 segmentStartIndex: 0,
224 segmentEndIndex: lastPointIndex,
225 segmentRatio: 0.5,
226 segmentLabelRotationMode: ESegmentLabelRotationMode.Parallel,
227 verticalTextPosition: EVerticalTextPosition.Below,
228 alignment: ETextAlignment.Center,
229 text: `Odd 1 -> ${pointCount}`,
230 padding: new Thickness(2, 8, 2, 8),
231 });
232
233 labels.push(
234 {
235 id: `${prefix}-axis-1`,
236 anchorMode: EMultiPointLabelAnchorMode.Axis,
237 axisLabelDrawMode: EAxisLabelDrawMode.Both,
238 pointIndex: 0,
239 },
240 {
241 id: `${prefix}-axis-${pointCount}`,
242 anchorMode: EMultiPointLabelAnchorMode.Axis,
243 axisLabelDrawMode: EAxisLabelDrawMode.Both,
244 pointIndex: lastPointIndex,
245 }
246 );
247
248 // also add point labels:
249 if (drawPointLabels) {
250 for (let i = 0; i <= lastPointIndex; i++) {
251 labels.push({
252 id: `${prefix}-point-${i}`,
253 anchorMode: EMultiPointLabelAnchorMode.Point,
254 pointIndex: i,
255 verticalTextPosition: i % 2 === 0 ? EVerticalTextPosition.Below : EVerticalTextPosition.Above,
256 alignment: ETextAlignment.Center,
257 yOffset: i % 2 === 0 ? 5 : -5, // add more spacing between labels and grips
258 text: `(${i + 1})`,
259 });
260 }
261 }
262
263 return labels;
264};
265
266export const drawExample = async (rootElement: string | HTMLDivElement) => {
267 const { sciChartSurface, xAt, yAt, candlestickSeries } = await createFinancialChart(rootElement, {
268 volatility: 0.0022,
269 title: "BTC / USDT - Custom Paired Polyline",
270 });
271
272 const placementModifier = new MultiPointAnnotationPlacementModifier();
273
274 addDefaultFinancialModifiers(sciChartSurface);
275 sciChartSurface.chartModifiers.add(placementModifier);
276
277 const createPrimaryDemoAnnotation = () =>
278 new PairedDashedPolylineAnnotation({
279 ...createTradingAnnotationOptions("PAIR", 5),
280 isEditable: true,
281 stroke: PAIRED_POLYLINE_COLORS.primaryStroke,
282 strokeThickness: 2,
283 fill: PAIRED_POLYLINE_COLORS.primaryFill,
284 pairStroke: PAIRED_POLYLINE_COLORS.primaryConnector,
285 pairStrokeThickness: 1.5,
286 pairStrokeDashArray: [6, 4],
287 placementPointCount: 5,
288 points: [
289 { x: xAt(226), y: yAt(226, -0.1) },
290 { x: xAt(245), y: yAt(245, 0.1) },
291 { x: xAt(278), y: yAt(278, -0.12) },
292 { x: xAt(304), y: yAt(304, 0.1) },
293 { x: xAt(324), y: yAt(324, -0.1) },
294 ],
295 labels: createConnectorLabels(5, "PAIR", false),
296 segmentLabelVisibility: EAnnotationVisibilityMode.Always,
297 axisLabelVisibility: EAnnotationVisibilityMode.OnInteraction,
298 clipping: EAnnotationClippingMode.SeriesViewRect,
299 });
300
301 const createSecondaryDemoAnnotation = () =>
302 new PairedDashedPolylineAnnotation({
303 ...createTradingAnnotationOptions("LAD", 7),
304 isEditable: true,
305 stroke: PAIRED_POLYLINE_COLORS.secondaryStroke,
306 strokeThickness: 2,
307 fill: PAIRED_POLYLINE_COLORS.secondaryFill,
308 pairStroke: PAIRED_POLYLINE_COLORS.secondaryConnector,
309 pairStrokeThickness: 1.5,
310 pairStrokeDashArray: [4, 3],
311 placementPointCount: 7,
312 points: [
313 { x: xAt(336), y: yAt(336, -0.1) },
314 { x: xAt(358), y: yAt(358, 0.1) },
315 { x: xAt(380), y: yAt(380, -0.1) },
316 { x: xAt(404), y: yAt(404, 0.1) },
317 { x: xAt(428), y: yAt(428, -0.1) },
318 { x: xAt(452), y: yAt(452, 0.1) },
319 { x: xAt(476), y: yAt(476, -0.1) },
320 ],
321 labels: createConnectorLabels(7, "LAD"),
322 formatLabelStyle: ({ label, defaultStyle, ...rest }: IMultiPointLabelStyleFormatParams) => {
323 if (label.anchorMode === EMultiPointLabelAnchorMode.Point) {
324 return { ...defaultStyle, color: "#FFF"};
325 }
326 return { ...defaultStyle };
327 },
328 segmentLabelVisibility: EAnnotationVisibilityMode.Always,
329 axisLabelVisibility: EAnnotationVisibilityMode.OnInteraction,
330 clipping: EAnnotationClippingMode.SeriesViewRect,
331 });
332
333 const reset = () => {
334 sciChartSurface.annotations.clear(true);
335 sciChartSurface.annotations.add(createPrimaryDemoAnnotation(), createSecondaryDemoAnnotation());
336 };
337
338 reset();
339
340 return {
341 sciChartSurface,
342
343 startPlacement: (pointCount = 5) => {
344 const safePointCount = normalizePlacementPointCount(pointCount);
345
346 placementModifier.startPlacement({
347 type: PAIRED_DASHED_POLYLINE as any,
348 options: {
349 ...createTradingAnnotationOptions("NEW", safePointCount),
350 isEditable: true,
351 stroke: PAIRED_POLYLINE_COLORS.placementStroke,
352 strokeThickness: 2,
353 fill: PAIRED_POLYLINE_COLORS.placementFill,
354 pairStroke: PAIRED_POLYLINE_COLORS.placementConnector,
355 pairStrokeThickness: 1.5,
356 pairStrokeDashArray: [7, 4],
357 showPairConnectors: true,
358 placementPointCount: safePointCount,
359 labels: createConnectorLabels(safePointCount, "NEW"),
360 pointLabelVisibility: EAnnotationVisibilityMode.OnInteraction,
361 segmentLabelVisibility: EAnnotationVisibilityMode.Always,
362 axisLabelVisibility: EAnnotationVisibilityMode.OnInteraction,
363 clipping: EAnnotationClippingMode.SeriesViewRect,
364 } as any,
365 });
366 },
367
368 stopPlacement: () => placementModifier.stopPlacement(true),
369
370 reset,
371
372 deleteAllAnnotations: () => {
373 placementModifier.stopPlacement(true);
374 sciChartSurface.annotations.clear(true);
375 },
376
377 togglePairConnectors: () => {
378 sciChartSurface.annotations.asArray().forEach((annotation) => {
379 if (annotation instanceof PairedDashedPolylineAnnotation) {
380 annotation.showPairConnectors = !annotation.showPairConnectors;
381 }
382 });
383 },
384 };
385};
386This example registers a custom annotation type and uses MultiPointAnnotationPlacementModifier to place it interactively. The custom class inherits from PolyLineAnnotation and draws additional dashed peak connectors during drawWithContext, with center labels attached to the custom connector segments.

Discover how to create a React Candlestick Chart or Stock Chart using SciChart.js. For high Performance JavaScript Charts, get your free demo now.

Easily create React OHLC Chart or Stock Chart using feature-rich SciChart.js chart library. Supports custom colors. Get your free trial now.

Create a React Realtime Ticking Candlestick / Stock Chart with live ticking and updating, using the high performance SciChart.js chart library. Get free demo now.

Render a real-time React BTC order book heatmap with ease. Optimize your crypto exchange with a fast heatmap order book. Give the SciChart demo a try.

Create a React Multi-Pane Candlestick / Stock Chart with indicator panels, synchronized zooming, panning and cursors. Get your free trial of SciChart.js now.

Demonstrating the capability of SciChart.js to create a composite 2D & 3D Chart application. An example like this could be used to visualize Tenor curves in a financial setting, or other 2D/3D data combined on a single screen.

Create a React Multi-Pane Candlestick / Stock Chart with indicator panels, synchronized zooming, panning and cursors. Get your free trial of SciChart.js now.

Shows one seeded OHLC data set toggled between no filter, Heikin-Ashi, Renko, and Point & Figure views.

Create a React Depth Chart, using the high performance SciChart.js chart library. Get free demo now.

Demonstrates how to place Buy/Sell arrow markers on a React Stock Chart using SciChart.js - Annotations API

This demo shows you how to create a <strong>{frameworkName} User Annotated Stock Chart</strong> using SciChart.js. Custom modifiers allow you to add lines and markers, then use the built in serialisation functions to save and reload the chart, including the data and all your custom annotations.

Build high-performance financial apps with React trading drawing tools. Try SciChart’s demo to see the advanced drawing tools for trading apps in action.

Deploy the ultimate React freehand drawing tool in your app. Render complex freehand drawing tools with a smooth UI experience. Try the SciChart demo today.