Demonstrates how FastRectangleRenderableSeries and BoxAnnotation to make dragabble, labled, event markers, using SciChart.js High Performance JavaScript Charts
drawExample.ts
RandomWalkGenerator.ts
angular.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 Angular example demonstrates how to repurpose a candlestick series into draggable, labeled event markers within a SciChart.js chart. The example integrates SciChart.js into an Angular standalone component using the scichart-angular package, providing a seamless way to embed high performance charts into Angular applications.
The implementation initializes a SciChartSurface with distinct axes for the main chart and the event markers. It synchronizes the axes by subscribing to axis.visibleRangeChanged, ensuring that both the primary and event axes remain in lock-step. A custom chart modifier, named CandleDragModifier, is implemented using the Custom Chart Modifier API provided by SciChart.js. This modifier enables precise selection via custom hit testing and allows users to drag event markers, updating the underlying data in real-time. The example also demonstrates advanced techniques such as dynamically attaching and detaching annotations, a process further explained in the Tutorial 06 - Adding Annotations documentation.
This example boasts real-time update capabilities and smooth rendering through sweep animations and WebGL enhancements. Custom data labels are generated using a tailored DataLabelProvider that formats text based on computed values from the data series. Advanced features include synchronized axis ranges and custom hit testing logic, which together create an interactive experience where event markers can be selected, dragged, and visually highlighted.
Developers can leverage the best practices for Angular integration outlined in the Getting Started with SciChart JS guide to deploy similar solutions. The example emphasizes efficient resource management and performance optimization, as detailed in the Performance Tips & Tricks documentation. Furthermore, it showcases advanced interactivity through custom hit testing and modifier interactions, ensuring that the charts remain responsive and user-friendly in complex scenarios.

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

This demo showcases the incredible performance of our Angular 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 Angular Chart by animating several series with thousands of data-points at 60 FPS

See the frequency of recordings with the Angular audio spectrum analyzer example from SciChart. This real-time audio 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 Angular charts by updating the series with millions of data-points!

This demo showcases the incredible realtime performance of our Angular 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.