Dragabble Event Markers

Demonstrates how FastRectangleRenderableSeries and BoxAnnotation to make dragabble, labled, event markers, using SciChart.js High Performance JavaScript Charts

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.html

RandomWalkGenerator.ts

vanilla.ts

theme.ts

Copy to clipboard
Minimise
Fullscreen
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};
255

Draggable Event Markers on JavaScript Charts

Overview

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

Technical Implementation

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.

Features and Capabilities

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.

Integration and Best Practices

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.

javascript Chart Examples & Demos

See Also: Performance Demos & Showcases (12 Demos)

Realtime JavaScript Chart Performance Demo | SciChart.js

Realtime JavaScript Chart Performance Demo

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

Load 500 Series x 500 Points Performance Demo | SciChart

Load 500 Series x 500 Points Performance Demo

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

Load 1 Million Points Performance Demo | SciChart.js Demo

Load 1 Million Points Performance Demo

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

Realtime Ghosted Traces | Javascript Charts | SciChart.js Demo

Realtime Ghosted Traces

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

Realtime Audio Spectrum Analyzer Chart | SciChart.js Demo

Realtime Audio Spectrum Analyzer Chart Example

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

Oil & Gas Explorer JavaScript Dashboard | SciChart.js

Oil & Gas Explorer JavaScript Dashboard

Demonstrates how to create Oil and Gas Dashboard

Client/Server Websocket Data Streaming | SciChart.js Demo

Client/Server Websocket Data Streaming

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

Server Traffic Dashboard | Javascript Charts | SciChart.js Demo

Server Traffic Dashboard

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

Rich Interactions Showcase | Javascript Charts | SciChart.js

Rich Interactions Showcase

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

Dynamic Layout Showcase | Javascript Charts | SciChart.js Demo

Dynamic Layout Showcase

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

JavaScript Population Pyramid | Javascript Charts | SciChart.js

JavaScript Population Pyramid

Population Pyramid of Europe and Africa

NEW!
High Performance SVG Cursor & Rollover | SciChart.js Demo

High Performance SVG Cursor & Rollover

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

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.