React Chart with Multi-Style Series

Demonstrates how to use multiple styles on a single series on React Charts using SciChart.js, High Performance JavaScript Charts. This uses a RenderDataTransform to split the data so that we can draw the selected points using additional customised drawingProviders. This means that modifiers still see a single series with the original data.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    BaseRenderDataTransform,
3    XyyPointSeriesResampled,
4    NumberRange,
5    RenderPassData,
6    IPointSeries,
7    XyDataSeries,
8    SciChartSurface,
9    NumericAxis,
10    TrianglePointMarker,
11    XyScatterRenderableSeries,
12    EllipsePointMarker,
13    PointMarkerDrawingProvider,
14    IXyyPointSeries,
15    IPointMarker,
16    FastColumnRenderableSeries,
17    GradientParams,
18    Point,
19    ColumnSeriesDrawingProvider,
20    ZoomExtentsModifier,
21    MouseWheelZoomModifier,
22    DataPointSelectionModifier,
23    LineSeriesDrawingProvider,
24    FastLineRenderableSeries,
25    ILineSeriesDrawingProviderProperties,
26    ELineDrawMode,
27    OhlcPointSeriesResampled,
28    IOhlcPointSeries,
29    RolloverModifier,
30    IPointMetadata,
31    SeriesInfo,
32    NativeTextAnnotation,
33    ECoordinateMode,
34    EHorizontalAnchorPoint,
35    EVerticalAnchorPoint,
36    IStrokePaletteProvider,
37    parseColorToUIntArgb,
38    EStrokePaletteMode,
39    IRenderableSeries,
40    vectorToArrayViewF64,
41} from "scichart";
42import { appTheme } from "../../../theme";
43
44/**
45 * This transform turns xy data into ohlc.  Unselected points are in y (close).
46 * Selected points in low for pointmarkers, and selected plus points either side in high for lines.
47 * If you only need this for points or columns, you could transform to Xyy instead
48 */
49class SplitRenderDataTransform extends BaseRenderDataTransform<OhlcPointSeriesResampled> {
50    protected createPointSeries(): OhlcPointSeriesResampled {
51        return new OhlcPointSeriesResampled(this.wasmContext, new NumberRange(0, 0));
52    }
53    protected runTransformInternal(renderPassData: RenderPassData): IPointSeries {
54        const { xValues: oldX, yValues: oldY, indexes: oldI, resampled } = renderPassData.pointSeries;
55        // this.pointSeries is the target.  Clear the existing values
56        const { xValues, yValues, highValues, lowValues, indexes } = this.pointSeries;
57        // This shows how to properly handled resampled data, though this is not necessary here.
58        const iStart = resampled ? 0 : renderPassData.indexRange.min;
59        const iEnd = resampled ? oldX.size() - 1 : renderPassData.indexRange?.max;
60        const length = iEnd - iStart + 1;
61        // Since this produces a known number of points we can just fast resize the target pointSeries to the desired length.  All this will be overritten
62        xValues.resizeFast(length);
63        yValues.resizeFast(length);
64        highValues.resizeFast(length);
65        lowValues.resizeFast(length);
66        indexes.resizeFast(length);
67        // Create views over the source and target vectors for fast access.  These views are only valid as long as there is no memory allocation
68        const oldXView = vectorToArrayViewF64(oldX, this.wasmContext);
69        const oldYView = vectorToArrayViewF64(oldY, this.wasmContext);
70        const oldIndexView = vectorToArrayViewF64(oldI, this.wasmContext);
71        const xView = vectorToArrayViewF64(xValues, this.wasmContext);
72        const yView = vectorToArrayViewF64(yValues, this.wasmContext);
73        const highView = vectorToArrayViewF64(highValues, this.wasmContext);
74        const lowView = vectorToArrayViewF64(lowValues, this.wasmContext);
75        const indexView = vectorToArrayViewF64(indexes, this.wasmContext);
76
77        const ds = this.parentSeries.dataSeries as XyDataSeries;
78        let prevSelected = false;
79        let iOut = 0;
80        for (let i = iStart; i <= iEnd; i++) {
81            const index = resampled ? oldIndexView[i] : i;
82            const md = ds.getMetadataAt(index);
83            xView[iOut] = oldXView[i];
84            indexView[iOut] = oldIndexView[i];
85            let nextSelected = false;
86            if (i < iEnd) {
87                const nextmd = ds.getMetadataAt(index + 1);
88                nextSelected = nextmd.isSelected;
89            }
90            yView[iOut] = md.isSelected ? NaN : oldYView[i];
91            // For pointmarkers we just need the point itself
92            lowView[iOut] = md.isSelected ? oldYView[i] : NaN;
93            // need points either side of the selected value for the line to draw.
94            highView[iOut] = prevSelected || md.isSelected || nextSelected ? oldYView[i] : NaN;
95            prevSelected = md.isSelected;
96            iOut++;
97        }
98        return this.pointSeries;
99    }
100}
101
102export const drawExample = async (rootElement: string | HTMLDivElement) => {
103    // Create a SciChartSurface
104    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
105        theme: appTheme.SciChartJsTheme,
106    });
107
108    // Create X,Y Axis
109    sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.05, 0.05) }));
110    sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0, 0.05) }));
111
112    // Column series with different gradient fill for selected columns
113    const xValues = Array.from({ length: 20 }, (x, i) => i);
114    const colyValues = xValues.map((x) => 10 + Math.random() * 40);
115    const colmetadata = xValues.map((x) => ({
116        isSelected: Math.random() < 0.3,
117    }));
118    const columnSeries = new FastColumnRenderableSeries(wasmContext, {
119        fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
120            { color: appTheme.MutedRed, offset: 0 },
121            { color: appTheme.MutedTeal, offset: 1 },
122        ]),
123        dataSeries: new XyDataSeries(wasmContext, {
124            xValues: xValues,
125            yValues: colyValues,
126            metadata: colmetadata,
127            containsNaN: true,
128        }),
129        stroke: "transparent",
130    });
131    // We cannot use a paletteProvider to change a gradient fill, so we have to use a second drawingProvider
132    const selectedColDP = new ColumnSeriesDrawingProvider(
133        wasmContext,
134        columnSeries,
135        // configure this to draw using the selected points
136        (ps) => (ps as IOhlcPointSeries).lowValues
137    );
138    selectedColDP.getProperties = (parentSeries) => {
139        const { stroke, strokeThickness, fill } = parentSeries;
140        return {
141            opacity: 1,
142            // Opacity setting does not currently apply to the gradient colors, so we have to apply it individually
143            fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
144                { color: appTheme.MutedRed + "88", offset: 0 },
145                { color: appTheme.MutedSkyBlue + "88", offset: 1 },
146            ]),
147            stroke,
148            strokeThickness,
149            fill,
150        };
151    };
152    columnSeries.drawingProviders.push(selectedColDP);
153    columnSeries.renderDataTransform = new SplitRenderDataTransform(
154        columnSeries,
155        wasmContext,
156        columnSeries.drawingProviders
157    );
158    sciChartSurface.renderableSeries.add(columnSeries);
159
160    const lineyValues = xValues.map((x) => 30 + x + x * Math.random());
161    const linemetadata = xValues.map((x) => ({
162        isSelected: Math.random() < 0.3,
163    }));
164    // Line series with different pointmarker and dashed line for selected sections
165    const lineSeries = new FastLineRenderableSeries(wasmContext, {
166        dataSeries: new XyDataSeries(wasmContext, {
167            xValues: xValues,
168            yValues: lineyValues,
169            metadata: linemetadata,
170            containsNaN: true,
171        }),
172        pointMarker: new EllipsePointMarker(wasmContext, {
173            width: 14,
174            height: 14,
175            strokeThickness: 0,
176            fill: appTheme.VividSkyBlue,
177        }),
178        stroke: appTheme.VividTeal,
179        strokeThickness: 3,
180        drawNaNAs: ELineDrawMode.DiscontinuousLine,
181    });
182
183    const trianglePM = new TrianglePointMarker(wasmContext, {
184        width: 15,
185        height: 15,
186        strokeThickness: 0,
187        fill: appTheme.VividOrange,
188    });
189    // Additional line drawing for selected segments
190    const selectedLineDP = new LineSeriesDrawingProvider(
191        wasmContext,
192        lineSeries,
193        (ps) => (ps as IOhlcPointSeries).highValues
194    );
195    // Make this drawingProvider used dashed lines
196    selectedLineDP.getProperties = (parentSeries) => {
197        const { stroke, strokeThickness, opacity, isDigitalLine, lineType, drawNaNAs } = parentSeries;
198        return {
199            stroke,
200            strokeThickness,
201            strokeDashArray: [3, 4],
202            isDigitalLine,
203            lineType,
204            drawNaNAs,
205            containsNaN: true,
206        } as ILineSeriesDrawingProviderProperties;
207    };
208    // Add this as the first drawingProviders so it draws behind all pointmarkers
209    lineSeries.drawingProviders.unshift(selectedLineDP);
210
211    // Additional point drawing for selecetd points
212    const triangleDP = new PointMarkerDrawingProvider(
213        wasmContext,
214        lineSeries,
215        (ps) => (ps as IOhlcPointSeries).lowValues
216    );
217    triangleDP.getProperties = (series) => {
218        return { pointMarker: trianglePM as IPointMarker };
219    };
220    lineSeries.drawingProviders.push(triangleDP);
221
222    // Apply the transform to all the drawingProviders
223    lineSeries.renderDataTransform = new SplitRenderDataTransform(lineSeries, wasmContext, lineSeries.drawingProviders);
224    sciChartSurface.renderableSeries.add(lineSeries);
225
226    sciChartSurface.annotations.add(
227        new NativeTextAnnotation({
228            xCoordinateMode: ECoordinateMode.Pixel,
229            yCoordinateMode: ECoordinateMode.Pixel,
230            x1: 20,
231            y1: 20,
232            horizontalAnchorPoint: EHorizontalAnchorPoint.Left,
233            verticalAnchorPoint: EVerticalAnchorPoint.Top,
234            text: "Selected points are styled differently.  Click and drag to change the selection",
235            textColor: appTheme.ForegroundColor,
236            fontSize: 16,
237            opacity: 0.77,
238        })
239    );
240
241    // Optional: Add Interactivity Modifiers
242    sciChartSurface.chartModifiers.add(
243        new DataPointSelectionModifier({
244            allowClickSelect: true,
245            onSelectionChanged: (args) => {
246                lineSeries.renderDataTransform.requiresTransform = true;
247                columnSeries.renderDataTransform.requiresTransform = true;
248            },
249        })
250    );
251    // sciChartSurface.chartModifiers.add(new RolloverModifier({
252    //     tooltipDataTemplate: (seriesInfo: SeriesInfo) => {
253    //         const vals: string[] = [];
254    //         vals.push(`X ${seriesInfo.formattedXValue}`);
255    //         vals.push(`Y ${seriesInfo.formattedYValue}`);
256    //         vals.push(`selected ${(seriesInfo.pointMetadata as IPointMetadata).isSelected}`);
257    //         return vals;
258    //     }
259    // }));
260
261    sciChartSurface.zoomExtents();
262    return { sciChartSurface, wasmContext };
263};
264

Multi Style Series (React)

Overview

This example demonstrates how to create a multi-style series in SciChart.js using React. It shows how to display charts where selected data points are styled differently from the rest, by leveraging a custom RenderDataTransform (see SplitRenderDataTransform) and multiple drawing providers. The example uses high-performance series such as FastLineRenderableSeries and FastColumnRenderableSeries, and integrates interactive modifiers like DataPointSelectionModifier to enable dynamic selection.

Technical Implementation

The core technical concept is the use of a custom RenderDataTransform to split the original data into segments that can be rendered with multiple visual styles. This is achieved by processing each data point and, based on custom metadata (i.e. whether metadata.isSelected is true), assigning different values for drawing providers. The transform is applied to both the line and column series to determine which segments should display different styles. For more details, see the RenderDataTransforms API Documentation.

Features and Capabilities

The example highlights several advanced features:

Integration and Best Practices

This example is fully integrated with React through the <SciChartReact/> component. The implementation follows best practices for React integration by encapsulating the chart initialization within a dedicated React component and leveraging the WebAssembly wasmContext for optimal performance. Developers can explore more about efficient React integration in the React Charts with SciChart.js: Introducing “SciChart React”. Additionally, performance is optimized by using fast-rendering series and handling data updates efficiently with the render data transform.

This comprehensive approach combining real-time data transformation, custom drawing customization, and interactive components provides a robust template for developers looking to create dynamic and performant charts using SciChart.js in React.

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