Using the default multi-chart sync APIs, create a multi-pane stock chart example with indicator panels. Zooming, panning, cursors are synchronised between the charts. This is a simpler way to create charts than subcharts, but will have a performance hit on some browsers.
drawExample.ts
index.tsx
ExampleDataProvider.ts
theme.ts
1import {
2 SciChartVerticalGroup,
3 DiscontinuousDateAxis,
4 EAxisAlignment,
5 SciChartSurface,
6 EAutoRange,
7 NumberRange,
8 NumericAxis,
9 OhlcDataSeries,
10 FastCandlestickRenderableSeries,
11 XyDataSeries,
12 calcAverageForArray,
13 FastLineRenderableSeries,
14 ZoomPanModifier,
15 ZoomExtentsModifier,
16 MouseWheelZoomModifier,
17 RolloverModifier,
18 FastBandRenderableSeries,
19 XyyDataSeries,
20 FastColumnRenderableSeries,
21 EXyDirection,
22 EFillPaletteMode,
23 EStrokePaletteMode,
24 IFillPaletteProvider,
25 IStrokePaletteProvider,
26 IRenderableSeries,
27 parseColorToUIntArgb,
28 ENumericFormat,
29 SmartDateLabelProvider,
30 XyMovingAverageFilter,
31 EDataSeriesField,
32 ELabelAlignment,
33 SeriesInfo,
34 EDataSeriesType,
35 OhlcSeriesInfo,
36 RolloverLegendSvgAnnotation,
37 SciChartOverview,
38 ESeriesType,
39 FastMountainRenderableSeries,
40 GradientParams,
41 Point,
42 TextAnnotation,
43 ECoordinateMode,
44 EHorizontalAnchorPoint,
45 EVerticalAnchorPoint,
46 EAnnotationLayer,
47} from "scichart";
48import { fetchMultiPaneData } from "../../../ExampleData/ExampleDataProvider";
49import { appTheme } from "../../../theme";
50
51const getTradingData = async (startPoints?: number, maxPoints?: number) => {
52 const { dateValues, openValues, highValues, lowValues, closeValues, volumeValues } = await fetchMultiPaneData();
53
54 if (maxPoints !== undefined) {
55 return {
56 dateValues: dateValues.slice(startPoints, startPoints + maxPoints),
57 openValues: openValues.slice(startPoints, startPoints + maxPoints),
58 highValues: highValues.slice(startPoints, startPoints + maxPoints),
59 lowValues: lowValues.slice(startPoints, startPoints + maxPoints),
60 closeValues: closeValues.slice(startPoints, startPoints + maxPoints),
61 volumeValues: volumeValues.slice(startPoints, startPoints + maxPoints),
62 };
63 }
64
65 return { dateValues, openValues, highValues, lowValues, closeValues, volumeValues };
66};
67
68// Override the standard legend displayed by RolloverModifier
69const getTooltipLegendTemplate = (seriesInfos: SeriesInfo[], svgAnnotation: RolloverLegendSvgAnnotation) => {
70 let outputSvgString = "";
71
72 // Foreach series there will be a seriesInfo supplied by SciChart. This contains info about the series under the house
73 seriesInfos.forEach((seriesInfo, index) => {
74 const y = 20 + index * 20;
75 const textColor = seriesInfo.stroke;
76 let legendText = seriesInfo.formattedYValue;
77 if (seriesInfo.dataSeriesType === EDataSeriesType.Ohlc) {
78 const o = seriesInfo as OhlcSeriesInfo;
79 legendText = `Open=${o.formattedOpenValue} High=${o.formattedHighValue} Low=${o.formattedLowValue} Close=${o.formattedCloseValue}`;
80 }
81 outputSvgString += `<text x="8" y="${y}" font-size="13" font-family="Verdana" fill="${textColor}">
82 ${seriesInfo.seriesName}: ${legendText}
83 </text>`;
84 });
85
86 return `<svg width="100%" height="100%">
87 ${outputSvgString}
88 </svg>`;
89};
90
91// Override the Renderableseries to display on the scichart overview
92const getOverviewSeries = (defaultSeries: IRenderableSeries) => {
93 if (defaultSeries.type === ESeriesType.CandlestickSeries) {
94 // Swap the default candlestick series on the overview chart for a mountain series. Same data
95 return new FastMountainRenderableSeries(defaultSeries.parentSurface.webAssemblyContext2D, {
96 dataSeries: defaultSeries.dataSeries,
97 fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
98 { color: appTheme.VividSkyBlue + "77", offset: 0 },
99 { color: "Transparent", offset: 1 },
100 ]),
101 stroke: appTheme.VividSkyBlue,
102 });
103 }
104 // hide all other series
105 return undefined;
106};
107
108export const getChartsInitializationAPI = () => {
109 // We can group together charts using VerticalChartGroup type
110 const verticalGroup = new SciChartVerticalGroup();
111
112 const dataPromise = getTradingData();
113
114 let chart1XAxis: DiscontinuousDateAxis;
115 let chart2XAxis: DiscontinuousDateAxis;
116 let chart3XAxis: DiscontinuousDateAxis;
117 let priceChartSurface: SciChartSurface;
118 let macdChartSurface: SciChartSurface;
119 let rsiChartSurface: SciChartSurface;
120 const axisAlignment = EAxisAlignment.Right;
121
122 const upCol = appTheme.VividGreen;
123 const downCol = appTheme.MutedRed;
124 const opacity = "AA";
125
126 // CHART 1
127 const drawPriceChart = async (rootElement: string | HTMLDivElement) => {
128 const [chart, data] = await Promise.all([
129 SciChartSurface.create(rootElement, {
130 // prevent default size settings
131 disableAspect: true,
132 theme: appTheme.SciChartJsTheme,
133 }),
134 dataPromise,
135 ]);
136 const { wasmContext, sciChartSurface } = chart;
137 const { dateValues, openValues, highValues, lowValues, closeValues, volumeValues } = data;
138
139 chart1XAxis = new DiscontinuousDateAxis(wasmContext, {
140 drawLabels: false,
141 drawMajorTickLines: false,
142 drawMinorTickLines: false,
143 });
144 sciChartSurface.xAxes.add(chart1XAxis);
145
146 const yAxis = new NumericAxis(wasmContext, {
147 maxAutoTicks: 5,
148 autoRange: EAutoRange.Always,
149 growBy: new NumberRange(0.3, 0.11),
150 labelFormat: ENumericFormat.Decimal,
151 labelPrecision: 4,
152 cursorLabelFormat: ENumericFormat.Decimal,
153 cursorLabelPrecision: 4,
154 labelPrefix: "$",
155 axisAlignment,
156 });
157 sciChartSurface.yAxes.add(yAxis);
158
159 // OHLC DATA SERIES
160 const usdDataSeries = new OhlcDataSeries(wasmContext, {
161 dataSeriesName: "EUR/USD",
162 xValues: dateValues,
163 openValues,
164 highValues,
165 lowValues,
166 closeValues,
167 });
168 const fcRendSeries = new FastCandlestickRenderableSeries(wasmContext, {
169 dataSeries: usdDataSeries,
170 stroke: appTheme.ForegroundColor, // Used for legend template
171 brushUp: upCol + "77",
172 brushDown: downCol + "77",
173 strokeUp: upCol,
174 strokeDown: downCol,
175 });
176 sciChartSurface.renderableSeries.add(fcRendSeries);
177
178 // MA1 SERIES
179 const maLowDataSeries = new XyMovingAverageFilter(usdDataSeries, {
180 dataSeriesName: "MA 50 Low",
181 length: 50,
182 field: EDataSeriesField.Low,
183 });
184 const maLowRenderableSeries = new FastLineRenderableSeries(wasmContext, {
185 dataSeries: maLowDataSeries,
186 });
187 sciChartSurface.renderableSeries.add(maLowRenderableSeries);
188 maLowRenderableSeries.rolloverModifierProps.tooltipColor = "red";
189 maLowRenderableSeries.rolloverModifierProps.markerColor = "red";
190 maLowRenderableSeries.stroke = appTheme.VividPink;
191 maLowRenderableSeries.strokeThickness = 2;
192
193 // MA2 SERIES
194 const maHighDataSeries = new XyMovingAverageFilter(usdDataSeries, {
195 dataSeriesName: "MA 200 High",
196 length: 200,
197 field: EDataSeriesField.High,
198 });
199 const maHighRenderableSeries = new FastLineRenderableSeries(wasmContext, {
200 dataSeries: maHighDataSeries,
201 });
202 sciChartSurface.renderableSeries.add(maHighRenderableSeries);
203 maHighRenderableSeries.stroke = appTheme.VividSkyBlue;
204 maHighRenderableSeries.strokeThickness = 2;
205
206 // VOLUME SERIES
207 const yAxis2 = new NumericAxis(wasmContext, {
208 id: "yAxis2",
209 isVisible: false,
210 autoRange: EAutoRange.Always,
211 growBy: new NumberRange(0, 3),
212 });
213 sciChartSurface.yAxes.add(yAxis2);
214
215 const volumeRenderableSeries = new FastColumnRenderableSeries(wasmContext, {
216 yAxisId: "yAxis2",
217 dataSeries: new XyDataSeries(wasmContext, {
218 dataSeriesName: "Volume",
219 xValues: dateValues,
220 yValues: volumeValues,
221 }),
222 dataPointWidth: 0.5,
223 strokeThickness: 1,
224 paletteProvider: new VolumePaletteProvider(usdDataSeries, upCol + opacity, downCol + opacity),
225 });
226 sciChartSurface.renderableSeries.add(volumeRenderableSeries);
227
228 // Add a watermark annotation
229 const watermarkAnnotation = new TextAnnotation({
230 x1: 0.5,
231 y1: 0.5,
232 xCoordinateMode: ECoordinateMode.Relative,
233 yCoordinateMode: ECoordinateMode.Relative,
234 horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
235 verticalAnchorPoint: EVerticalAnchorPoint.Center,
236 opacity: 0.17,
237 textColor: appTheme.ForegroundColor,
238 fontSize: 48,
239 fontWeight: "Bold",
240 text: "Euro / U.S. Dollar - Daily",
241 });
242 sciChartSurface.annotations.add(watermarkAnnotation);
243
244 // MODIFIERS
245 sciChartSurface.chartModifiers.add(new ZoomPanModifier({ enableZoom: true }));
246 sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
247 sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier());
248 sciChartSurface.chartModifiers.add(
249 new RolloverModifier({
250 modifierGroup: "cursorGroup",
251 showTooltip: false,
252 tooltipLegendTemplate: getTooltipLegendTemplate,
253 })
254 );
255
256 verticalGroup.addSurfaceToGroup(sciChartSurface);
257 priceChartSurface = sciChartSurface;
258 sciChartSurface.zoomExtentsX();
259
260 return { wasmContext, sciChartSurface };
261 };
262
263 // CHART 2 - MACD
264 const drawMacdChart = async (rootElement: string | HTMLDivElement) => {
265 const [{ wasmContext, sciChartSurface }, { dateValues, closeValues }] = await Promise.all([
266 SciChartSurface.create(rootElement, {
267 // prevent default size settings
268 disableAspect: true,
269 theme: appTheme.SciChartJsTheme,
270 }),
271 dataPromise,
272 ]);
273
274 chart2XAxis = new DiscontinuousDateAxis(wasmContext, {
275 drawLabels: false,
276 drawMajorTickLines: false,
277 drawMinorTickLines: false,
278 });
279 sciChartSurface.xAxes.add(chart2XAxis);
280
281 const yAxis = new NumericAxis(wasmContext, {
282 autoRange: EAutoRange.Always,
283 growBy: new NumberRange(0.1, 0.1),
284 axisAlignment,
285 labelPrecision: 2,
286 cursorLabelPrecision: 2,
287 labelStyle: { alignment: ELabelAlignment.Right },
288 });
289 yAxis.labelProvider.numericFormat = ENumericFormat.Decimal;
290 sciChartSurface.yAxes.add(yAxis);
291
292 const macdArray: number[] = [];
293 const signalArray: number[] = [];
294 const divergenceArray: number[] = [];
295 for (let i = 0; i < dateValues.length; i++) {
296 const maSlow = calcAverageForArray(closeValues, 12, i);
297 const maFast = calcAverageForArray(closeValues, 25, i);
298 const macd = maSlow - maFast;
299 macdArray.push(macd);
300 const signal = calcAverageForArray(macdArray, 9, i);
301 signalArray.push(signal);
302 const divergence = macd - signal;
303 divergenceArray.push(divergence);
304 }
305
306 const bandSeries = new FastBandRenderableSeries(wasmContext, {
307 dataSeries: new XyyDataSeries(wasmContext, {
308 dataSeriesName: "MACD",
309 xValues: dateValues,
310 yValues: signalArray,
311 y1Values: macdArray,
312 }),
313 stroke: downCol,
314 strokeY1: upCol,
315 fill: upCol + "77",
316 fillY1: downCol + "77",
317 });
318 sciChartSurface.renderableSeries.add(bandSeries);
319
320 const columnSeries = new FastColumnRenderableSeries(wasmContext, {
321 dataSeries: new XyDataSeries(wasmContext, {
322 dataSeriesName: "Divergence",
323 xValues: dateValues,
324 yValues: divergenceArray,
325 }),
326 paletteProvider: new MacdHistogramPaletteProvider(upCol + "AA", downCol + "AA"),
327 dataPointWidth: 0.5,
328 });
329 sciChartSurface.renderableSeries.add(columnSeries);
330
331 sciChartSurface.chartModifiers.add(
332 new ZoomPanModifier({ enableZoom: true, xyDirection: EXyDirection.XDirection })
333 );
334 sciChartSurface.chartModifiers.add(new ZoomExtentsModifier({ xyDirection: EXyDirection.XDirection }));
335 sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }));
336 sciChartSurface.chartModifiers.add(
337 new RolloverModifier({
338 modifierGroup: "cursorGroup",
339 showTooltip: false,
340 tooltipLegendTemplate: getTooltipLegendTemplate,
341 })
342 );
343
344 verticalGroup.addSurfaceToGroup(sciChartSurface);
345 macdChartSurface = sciChartSurface;
346
347 return { wasmContext, sciChartSurface };
348 };
349
350 // CHART 3 - RSI
351 const drawRsiChart = async (rootElement: string | HTMLDivElement) => {
352 const [{ wasmContext, sciChartSurface }, { dateValues, closeValues }] = await Promise.all([
353 SciChartSurface.create(rootElement, {
354 // prevent default size settings
355 disableAspect: true,
356 theme: appTheme.SciChartJsTheme,
357 }),
358 dataPromise,
359 ]);
360
361 chart3XAxis = new DiscontinuousDateAxis(wasmContext, {
362 autoRange: EAutoRange.Once,
363 labelProvider: new SmartDateLabelProvider(),
364 });
365 sciChartSurface.xAxes.add(chart3XAxis);
366
367 const yAxis = new NumericAxis(wasmContext, {
368 autoRange: EAutoRange.Always,
369 growBy: new NumberRange(0.1, 0.1),
370 labelPrecision: 0,
371 cursorLabelPrecision: 0,
372 axisAlignment,
373 labelStyle: { alignment: ELabelAlignment.Right },
374 });
375 yAxis.labelProvider.numericFormat = ENumericFormat.Decimal;
376 sciChartSurface.yAxes.add(yAxis);
377
378 const RSI_PERIOD = 14;
379 const rsiArray: number[] = [];
380 const gainArray: number[] = [];
381 const lossArray: number[] = [];
382
383 if (dateValues.length) {
384 rsiArray.push(NaN);
385 gainArray.push(NaN);
386 lossArray.push(NaN);
387 }
388
389 for (let i = 1; i < dateValues.length; i++) {
390 const previousClose = closeValues[i - 1];
391 const currentClose = closeValues[i];
392 const gain = currentClose > previousClose ? currentClose - previousClose : 0;
393 gainArray.push(gain);
394 const loss = previousClose > currentClose ? previousClose - currentClose : 0;
395 lossArray.push(loss);
396 const relativeStrength =
397 calcAverageForArray(gainArray, RSI_PERIOD) / calcAverageForArray(lossArray, RSI_PERIOD);
398 const rsi = 100 - 100 / (1 + relativeStrength);
399 rsiArray.push(rsi);
400 }
401 const rsiRenderableSeries = new FastLineRenderableSeries(wasmContext, {
402 dataSeries: new XyDataSeries(wasmContext, {
403 dataSeriesName: "RSI",
404 xValues: dateValues,
405 yValues: rsiArray,
406 }),
407 stroke: appTheme.MutedBlue,
408 strokeThickness: 2,
409 });
410 sciChartSurface.renderableSeries.add(rsiRenderableSeries);
411
412 sciChartSurface.chartModifiers.add(
413 new ZoomPanModifier({ enableZoom: true, xyDirection: EXyDirection.XDirection })
414 );
415 sciChartSurface.chartModifiers.add(new ZoomExtentsModifier({ xyDirection: EXyDirection.XDirection }));
416 sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier({ xyDirection: EXyDirection.XDirection }));
417 sciChartSurface.chartModifiers.add(
418 new RolloverModifier({
419 modifierGroup: "cursorGroup",
420 showTooltip: false,
421 tooltipLegendTemplate: getTooltipLegendTemplate,
422 })
423 );
424
425 verticalGroup.addSurfaceToGroup(sciChartSurface);
426 rsiChartSurface = sciChartSurface;
427
428 return { wasmContext, sciChartSurface };
429 };
430
431 // DRAW OVERVIEW
432 // Must be done after main chart creation
433 const drawOverview = (mainSurface: SciChartSurface) => async (rootElement: string | HTMLDivElement) => {
434 const overview = await SciChartOverview.create(mainSurface, rootElement, {
435 // prevent default size settings
436 disableAspect: true,
437 theme: appTheme.SciChartJsTheme,
438 transformRenderableSeries: getOverviewSeries,
439 });
440
441 return { sciChartSurface: overview.overviewSciChartSurface };
442 };
443
444 const configureAfterInit = () => {
445 const synchronizeAxes = () => {
446 // TODO refactor using AxisSynchroniser
447
448 // SYNCHRONIZE VISIBLE RANGES
449 chart1XAxis.visibleRangeChanged.subscribe((data1) => {
450 chart2XAxis.visibleRange = data1.visibleRange;
451 chart3XAxis.visibleRange = data1.visibleRange;
452 });
453 chart2XAxis.visibleRangeChanged.subscribe((data1) => {
454 chart1XAxis.visibleRange = data1.visibleRange;
455 chart3XAxis.visibleRange = data1.visibleRange;
456 });
457 chart3XAxis.visibleRangeChanged.subscribe((data1) => {
458 chart1XAxis.visibleRange = data1.visibleRange;
459 chart2XAxis.visibleRange = data1.visibleRange;
460 });
461 };
462
463 synchronizeAxes();
464
465 // Force showing the latest 200 bars
466 const twoHundredDaysOfSeconds = 60 * 60 * 24 * 200;
467 chart1XAxis.visibleRange = new NumberRange(
468 chart1XAxis.visibleRange.max - twoHundredDaysOfSeconds,
469 chart1XAxis.visibleRange.max
470 );
471 };
472
473 return { drawPriceChart, drawMacdChart, drawRsiChart, drawOverview, configureAfterInit };
474};
475
476/**
477 * An example PaletteProvider applied to the volume column series. It will return green / red
478 * fills and strokes when the main price data bar is up or down
479 */
480class VolumePaletteProvider implements IStrokePaletteProvider, IFillPaletteProvider {
481 public readonly strokePaletteMode: EStrokePaletteMode = EStrokePaletteMode.SOLID;
482 public readonly fillPaletteMode: EFillPaletteMode = EFillPaletteMode.SOLID;
483 private priceData: OhlcDataSeries;
484 private volumeUpArgb: number;
485 private volumnDownArgb: number;
486
487 constructor(priceData: OhlcDataSeries, volumeUpColor: string, volumeDownColor: string) {
488 this.priceData = priceData;
489 this.volumeUpArgb = parseColorToUIntArgb(volumeUpColor);
490 this.volumnDownArgb = parseColorToUIntArgb(volumeDownColor);
491 }
492
493 onAttached(parentSeries: IRenderableSeries): void {}
494
495 onDetached(): void {}
496
497 overrideFillArgb(xValue: number, yValue: number, index: number): number {
498 const open = this.priceData.getNativeOpenValues().get(index);
499 const close = this.priceData.getNativeCloseValues().get(index);
500
501 return close >= open ? this.volumeUpArgb : this.volumnDownArgb;
502 }
503
504 overrideStrokeArgb(xValue: number, yValue: number, index: number): number {
505 return this.overrideFillArgb(xValue, yValue, index);
506 }
507}
508
509// tslint:disable-next-line:max-classes-per-file
510class MacdHistogramPaletteProvider implements IStrokePaletteProvider, IFillPaletteProvider {
511 public readonly strokePaletteMode: EStrokePaletteMode = EStrokePaletteMode.SOLID;
512 public readonly fillPaletteMode: EFillPaletteMode = EFillPaletteMode.SOLID;
513 private aboveZeroArgb: number;
514 private belowZeroArgb: number;
515
516 constructor(aboveZeroColor: string, belowZeroColor: string) {
517 this.aboveZeroArgb = parseColorToUIntArgb(aboveZeroColor);
518 this.belowZeroArgb = parseColorToUIntArgb(belowZeroColor);
519 }
520
521 onAttached(parentSeries: IRenderableSeries): void {}
522
523 onDetached(): void {}
524
525 overrideFillArgb(xValue: number, yValue: number, index: number): number {
526 return yValue >= 0 ? this.aboveZeroArgb : this.belowZeroArgb;
527 }
528
529 overrideStrokeArgb(xValue: number, yValue: number, index: number): number {
530 return this.overrideFillArgb(xValue, yValue, index);
531 }
532}
533This example demonstrates a multi-pane stock chart built exclusively with JavaScript and SciChart.js. It features a primary price chart along with indicator panels for MACD and RSI, plus an additional overview chart that synchronizes with the main charts.
The charts are initialized asynchronously using functions such as drawPriceChart, drawMacdChart, drawRsiChart, and drawOverview. These functions leverage the SciChartSurface.create method to establish high-performance WebGL charts. Custom palette providers are implemented for the volume and MACD histogram series by extending the IStrokePaletteProvider and IFillPaletteProvider interfaces, as detailed in The PaletteProvider API. Additionally, the charts are grouped using the SciChartVerticalGroup and synchronized by subscribing to the x-axis visibleRangeChanged events, which is explained in the Tutorial 09 - Linking Multiple Charts.
The example supports real-time chart updates, advanced technical analysis through moving average filters (see XyMovingAverageFilter API Documentation), and interactive tooltips configured via the RolloverModifier. The integration of chart modifiers such as ZoomPanModifier, ZoomExtentsModifier, and MouseWheelZoomModifier ensures seamless navigation and interaction.
Built with JavaScript, this implementation follows asynchronous initialization best practices outlined in Getting Started with SciChart JS, and employs performance optimization techniques recommended in Performance Tips & Tricks. The synchronized overview chart, created using the transformRenderableSeries feature, further enhances usability by providing a compact visual summary, as described in How to Link JavaScript Charts and Synchronise zooming, panning ....
Overall, the example offers a comprehensive demonstration of chart synchronization, advanced customization, and efficient performance optimizations, making it a valuable reference for developers building high-performance financial charting applications using SciChart.js and JavaScript.

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

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

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

Create a Javascript heatmap chart showing historical orderbook levels using the high performance SciChart.js chart library. Get free demo now.

Create a JavaScript 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 JavaScript Depth Chart, using the high performance SciChart.js chart library. Get free demo now.

Demonstrates how to place Buy/Sell arrow markers on a JavaScript 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.