Demonstrates how FastRectangleRenderableSeries and BoxAnnotation to make dragabble, labled, event markers, using SciChart.js High Performance JavaScript Charts
drawExample.ts
index.html
RandomWalkGenerator.ts
vanilla.ts
theme.ts
1import {
2 AUTO_COLOR,
3 BoxAnnotation,
4 CustomChartModifier2D,
5 deleteSafe,
6 EAxisAlignment,
7 EColumnMode,
8 EColumnYMode,
9 ECoordinateMode,
10 EDraggingGripPoint,
11 EHorizontalAnchorPoint,
12 ENumericFormat,
13 EValueName,
14 EVerticalAnchorPoint,
15 EXyDirection,
16 FastLineRenderableSeries,
17 FastRectangleRenderableSeries,
18 formatNumber,
19 IChartModifierBaseOptions,
20 ModifierMouseArgs,
21 MouseWheelZoomModifier,
22 NumberRange,
23 NumericAxis,
24 RectangleDataLabelState,
25 RectangleSeriesDataLabelProvider,
26 SciChartSurface,
27 SweepAnimation,
28 TextAnnotation,
29 XyDataSeries,
30 XyxDataSeries,
31 ZoomExtentsModifier,
32 ZoomPanModifier,
33} from "scichart";
34import { RandomWalkGenerator } from "../../../ExampleData/RandomWalkGenerator";
35import { appTheme } from "../../../theme";
36
37const defaultHeight = 1;
38class RectangleDragModifier extends CustomChartModifier2D {
39 private series: FastRectangleRenderableSeries;
40 private dataSeries: XyxDataSeries;
41 private annotation: BoxAnnotation;
42 private selectedIndex: number = -1;
43
44 public constructor(series: FastRectangleRenderableSeries, options?: IChartModifierBaseOptions) {
45 super(options);
46 this.series = series;
47 this.dataSeries = series.dataSeries as XyxDataSeries;
48 }
49
50 public override onAttach(): void {
51 super.onAttach();
52 // Create an annotation where only the selection box will be visible
53 this.annotation = new BoxAnnotation({
54 xAxisId: this.series.xAxisId,
55 yAxisId: this.series.yAxisId,
56 strokeThickness: 0,
57 stroke: "transparent",
58 selectionBoxStroke: "#88888844",
59 opacity: 0,
60 isEditable: true,
61 resizeDirections: EXyDirection.XDirection,
62 // dragPoints: [
63 // EDraggingGripPoint.Body,
64 // EDraggingGripPoint.x1y1,
65 // EDraggingGripPoint.x2y1,
66 // EDraggingGripPoint.x1y2,
67 // EDraggingGripPoint.x2y2,
68 // ],
69 });
70
71 // Update the selected data point when the annotation is dragged
72 this.annotation.dragDelta.subscribe((data) => {
73 if (this.selectedIndex >= 0) {
74 const newX = this.annotation.x1;
75 const newY = Math.floor(this.annotation.y1 + defaultHeight);
76 const newx1 = this.annotation.x2;
77
78 // Do not allow close to be less than open as this breaks our custom hitTest
79 this.dataSeries.updateXyz(
80 this.selectedIndex,
81 newX,
82 newY,
83 newx1,
84 this.dataSeries.getMetadataAt(this.selectedIndex)
85 );
86 }
87 });
88
89 // Manually set the selected status of the point using metadata. This will drive the DataPointSelectionPaletteProvider
90 this.annotation.selectedChanged.subscribe((data) => {
91 this.dataSeries.getMetadataAt(this.selectedIndex).isSelected = data;
92 });
93
94 this.parentSurface.modifierAnnotations.add(this.annotation);
95 }
96
97 public override onDetach(): void {
98 this.parentSurface.modifierAnnotations.remove(this.annotation);
99 this.annotation = deleteSafe(this.annotation);
100 }
101
102 public override modifierMouseUp(args: ModifierMouseArgs): void {
103 const point = args.mousePoint;
104 const hitTestInfo = this.series.hitTestProvider.hitTest(point.x, point.y, 5);
105
106 if (hitTestInfo.isHit) {
107 if (this.selectedIndex >= 0 && this.selectedIndex !== hitTestInfo.dataSeriesIndex) {
108 this.dataSeries.getMetadataAt(this.selectedIndex).isSelected = false;
109 }
110
111 const x1Values = this.dataSeries.getYValuesByName(EValueName.X1);
112
113 const x1Value = x1Values.get(hitTestInfo.dataSeriesIndex);
114
115 // Place the annotation over the selected box
116 this.selectedIndex = hitTestInfo.dataSeriesIndex;
117 this.annotation.x1 = hitTestInfo.xValue;
118 this.annotation.x2 = x1Value;
119 this.annotation.y1 = Math.round(hitTestInfo.yValue) - defaultHeight / 2;
120 this.annotation.y2 = Math.round(hitTestInfo.yValue) + defaultHeight / 2;
121 // Make the annotation selected. Both these lines are required.
122 this.annotation.isSelected = true;
123 this.dataSeries.getMetadataAt(this.selectedIndex).isSelected = true;
124 this.parentSurface.adornerLayer.selectedAnnotation = this.annotation;
125 }
126 }
127}
128
129export const drawExample = async (rootElement: string | HTMLDivElement) => {
130 // Create a SciChartSurface
131 const { wasmContext, sciChartSurface } = await SciChartSurface.create(rootElement, {
132 theme: appTheme.SciChartJsTheme,
133 });
134
135 const POINTS = 1000;
136
137 // Shared X axis
138 const xAxis = new NumericAxis(wasmContext, { visibleRange: new NumberRange(0, POINTS) });
139 sciChartSurface.xAxes.add(xAxis);
140
141 // Separate Y axes for the data and events
142 const yAxis = new NumericAxis(wasmContext, {
143 axisAlignment: EAxisAlignment.Left,
144 growBy: new NumberRange(0.05, 0.1),
145 });
146 const yEventAxis = new NumericAxis(wasmContext, {
147 isVisible: false,
148 visibleRange: new NumberRange(0, 12),
149 });
150
151 sciChartSurface.yAxes.add(yAxis, yEventAxis);
152
153 // Create arrays of x, y values (just arrays of numbers)
154 const { xValues, yValues } = new RandomWalkGenerator().getRandomWalkSeries(POINTS);
155
156 // Create a line Series and add to the chart
157 sciChartSurface.renderableSeries.add(
158 new FastLineRenderableSeries(wasmContext, {
159 dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
160 stroke: AUTO_COLOR,
161 strokeThickness: 3,
162 animation: new SweepAnimation({ duration: 500, fadeEffect: true }),
163 })
164 );
165
166 // Our event bars will have variable width, but fixed height, so we use Xyx data
167 const eventDataSeries = new XyxDataSeries(wasmContext);
168
169 // Create event data. Prevent overlap of events
170 const rows = new Map<number, number>();
171 const EVENTCOUNT = 30;
172 let start = 0;
173 for (let i = 0; i < EVENTCOUNT; i++) {
174 start = start + Math.random() * ((2 * POINTS) / EVENTCOUNT);
175 const end = start + 1 + Math.random() * ((2 * POINTS) / EVENTCOUNT);
176 let row = 10;
177 if (i === 0) {
178 rows.set(row, end);
179 } else {
180 let last = rows.get(row);
181 while (last > start) {
182 row -= defaultHeight;
183 last = rows.get(row) ?? 0;
184 }
185 rows.set(row, end);
186 }
187
188 eventDataSeries.append(start, row, end, { isSelected: false });
189 }
190
191 // Create a custom DataLabelProvider which uses the width of the bar as the value to display
192 class BarWidthDataLabelProvider extends RectangleSeriesDataLabelProvider {
193 public getText(state: RectangleDataLabelState): string {
194 const usefinal = !this.updateTextInAnimation && state.parentSeries.isRunningAnimation;
195 const yval = usefinal ? state.yValAfterAnimation() : state.yVal();
196 if (isNaN(yval)) {
197 return undefined;
198 } else {
199 const diff = Math.abs(state.x1Val() - state.xVal());
200 if (this.engineeringPrefix) {
201 return formatNumber(diff, this.numericFormat, this.precision, this.engineeringPrefixProperty);
202 } else {
203 return formatNumber(diff, this.numericFormat ?? ENumericFormat.Decimal, this.precision);
204 }
205 }
206 }
207 }
208
209 const eventSeries = new FastRectangleRenderableSeries(wasmContext, {
210 dataSeries: eventDataSeries,
211 yAxisId: yEventAxis.id,
212 columnXMode: EColumnMode.StartEnd,
213 columnYMode: EColumnYMode.CenterHeight,
214 defaultY1: defaultHeight,
215 stroke: "ff0000cc",
216 strokeThickness: 1,
217 fill: "ff00004D",
218 // opacity: 0.8,
219 dataLabelProvider: new BarWidthDataLabelProvider({
220 style: {
221 fontSize: 12,
222 },
223 color: "white",
224 }),
225 });
226
227 sciChartSurface.renderableSeries.add(eventSeries);
228
229 // Configure modifiers to only affect the data Y axis
230 sciChartSurface.chartModifiers.add(
231 new ZoomExtentsModifier({ includedYAxisIds: [yAxis.id] }),
232 new MouseWheelZoomModifier({ includedYAxisIds: [yAxis.id] }),
233 new ZoomPanModifier({ includedYAxisIds: [yAxis.id] }),
234 new RectangleDragModifier(eventSeries)
235 );
236
237 // Add instructions
238 sciChartSurface.annotations.add(
239 new TextAnnotation({
240 x1: 0.01,
241 y1: 0.03,
242 xCoordinateMode: ECoordinateMode.Relative,
243 yCoordinateMode: ECoordinateMode.Relative,
244 horizontalAnchorPoint: EHorizontalAnchorPoint.Left,
245 verticalAnchorPoint: EVerticalAnchorPoint.Top,
246 text: "The boxes are rendered with a fast rectangle series and can be selected and dragged like an annotation.",
247 textColor: appTheme.ForegroundColor + "77",
248 })
249 );
250
251 xAxis.visibleRange = xAxis.getMaximumRange();
252
253 return { wasmContext, sciChartSurface };
254};
255This example, "Event Markers", demonstrates advanced charting features using SciChart.js with JavaScript. It showcases key techniques such as custom chart modifiers, data label customization, custom hit testing, axis range synchronization, interactive annotations, and performance optimizations.
The chart is created by instantiating a SciChartSurface with multiple axes and renderable series including FastLineRenderableSeries for a primary line chart and FastCandlestickRenderableSeries for event markers. A custom modifier, implemented by extending the CustomChartModifier2D API, enables selection and drag interactions on candlestick event markers. For more details on creating such custom modifiers, see the Custom Chart Modifier API.
Custom data labels are generated via an overridden DataLabelProvider, which dynamically calculates label positions and text by based on open and close values. Developers can refer to the DataLabel API documentation for similar implementations.
In addition, the example overrides the default hit testing behavior with custom logic that accounts for multiple overlapping series. This enables precise interaction with the candlestick markers and demonstrates techniques found in the RenderableSeries Hit-Test API demo.
The implementation also includes performance optimizations by utilizing FastLineRenderableSeries and applying a SweepAnimation for series startup, as described in the Series Startup Animations documentation. The getDataPointWidth() method is overridden to ensure fixed pixel width rendering for event markers, supporting custom rendering requirements.
This example offers dynamic update capabilities through a drag-enabled custom chart modifier that updates the underlying OhlcDataSeries. Axis range synchronization is implemented whereby the axis.visibleRange of a hidden event axis is kept in sync with the main axis, ensuring consistent display during zoom and pan operations. Interactive annotations are integrated to let users select and drag markers, and the overall implementation follows best practices for resource management by returning a destructor function for proper disposal of the SciChartSurface. For guidance on resource cleanup, see the Memory Best Practices page.
The JavaScript integration is self-contained: the chart is created within an asynchronous initialization function that returns a cleanup function to properly dispose of the chart when necessary. This pattern ensures efficient memory management and aligns with best practices outlined in the SciChart documentation. Additionally, the combination of custom annotations and modifiers demonstrates how to build interactive and highly configurable chart components. Developers are encouraged to explore further customizations, such as advanced zooming and data streaming, by consulting the Editable Annotations documentation. Overall, the implementation serves as a solid foundation for integrating advanced SciChart.js features using JavaScript.

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

This demo showcases the incredible performance of our JavaScript Chart by loading 500 series with 500 points (250k points) instantly!

This demo showcases the incredible performance of our JavaScript Chart by loading a million points instantly.

This demo showcases the realtime performance of our JavaScript Chart by animating several series with thousands of data-points at 60 FPS

See the frequency of recordings with the JavaScript audio spectrum analyzer example from SciChart. This real-time visualizer demo uses a Fourier Transform.

Demonstrates how to create Oil and Gas Dashboard

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

This dashboard demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Demonstrates a custom modifier which can convert from single chart to grid layout and back.

Population Pyramid of Europe and Africa

Demonstrates how to use the SVG render layer in SciChart.js to maintain smooth cursor interaction on heavy charts with millions of points.