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

src/drawExample.ts
import {
    BaseRenderDataTransform,
    NumberRange,
    RenderPassData,
    IPointSeries,
    XyDataSeries,
    SciChartSurface,
    NumericAxis,
    TrianglePointMarker,
    EllipsePointMarker,
    PointMarkerDrawingProvider,
    IPointMarker,
    FastColumnRenderableSeries,
    GradientParams,
    Point,
    ColumnSeriesDrawingProvider,
    DataPointSelectionModifier,
    LineSeriesDrawingProvider,
    FastLineRenderableSeries,
    ILineSeriesDrawingProviderProperties,
    ELineDrawMode,
    OhlcPointSeriesResampled,
    IOhlcPointSeries,
    NativeTextAnnotation,
    ECoordinateMode,
    EHorizontalAnchorPoint,
    EVerticalAnchorPoint,
    vectorToArrayViewF64,
} from "scichart";
import { appTheme } from "./appTheme";

/**
 * This transform turns xy data into ohlc.  Unselected points are in y (close).
 * Selected points in low for pointmarkers, and selected plus points either side in high for lines.
 * If you only need this for points or columns, you could transform to Xyy instead
 */
class SplitRenderDataTransform extends BaseRenderDataTransform<OhlcPointSeriesResampled> {
    protected createPointSeries(): OhlcPointSeriesResampled {
        return new OhlcPointSeriesResampled(this.wasmContext, new NumberRange(0, 0));
    }
    protected runTransformInternal(renderPassData: RenderPassData): IPointSeries {
        const { xValues: oldX, yValues: oldY, indexes: oldI, resampled } = renderPassData.pointSeries;
        // this.pointSeries is the target.  Clear the existing values
        const { xValues, yValues, highValues, lowValues, indexes } = this.pointSeries;
        // Resampled buffers already cover the visible range; raw buffers need the index range.
        const iStart = resampled ? 0 : renderPassData.indexRange.min;
        const iEnd = resampled ? oldX.size() - 1 : renderPassData.indexRange?.max;
        const length = iEnd - iStart + 1;
        // Resize before creating array views: a later allocation would invalidate those views.
        xValues.resizeFast(length);
        yValues.resizeFast(length);
        highValues.resizeFast(length);
        lowValues.resizeFast(length);
        indexes.resizeFast(length);
        // Views provide fast access to the native source and target buffers.
        const oldXView = vectorToArrayViewF64(oldX, this.wasmContext);
        const oldYView = vectorToArrayViewF64(oldY, this.wasmContext);
        const oldIndexView = vectorToArrayViewF64(oldI, this.wasmContext);
        const xView = vectorToArrayViewF64(xValues, this.wasmContext);
        const yView = vectorToArrayViewF64(yValues, this.wasmContext);
        const highView = vectorToArrayViewF64(highValues, this.wasmContext);
        const lowView = vectorToArrayViewF64(lowValues, this.wasmContext);
        const indexView = vectorToArrayViewF64(indexes, this.wasmContext);

        const ds = this.parentSeries.dataSeries as XyDataSeries;
        let prevSelected = false;
        let iOut = 0;
        for (let i = iStart; i <= iEnd; i++) {
            const index = resampled ? oldIndexView[i] : i;
            const md = ds.getMetadataAt(index);
            xView[iOut] = oldXView[i];
            indexView[iOut] = oldIndexView[i];
            let nextSelected = false;
            if (i < iEnd) {
                const nextmd = ds.getMetadataAt(index + 1);
                nextSelected = nextmd.isSelected;
            }
            yView[iOut] = md.isSelected ? NaN : oldYView[i];
            // For pointmarkers we just need the point itself
            lowView[iOut] = md.isSelected ? oldYView[i] : NaN;
            // need points either side of the selected value for the line to draw.
            highView[iOut] = prevSelected || md.isSelected || nextSelected ? oldYView[i] : NaN;
            prevSelected = md.isSelected;
            iOut++;
        }
        return this.pointSeries;
    }
}

export const drawExample = async (rootElement: string | HTMLDivElement) => {
    // Create a SciChartSurface
    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
        theme: appTheme.SciChartJsTheme,
    });

    // Create X,Y Axis
    sciChartSurface.xAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.05, 0.05) }));
    sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0, 0.05) }));

    // Column series with different gradient fill for selected columns
    const xValues = Array.from({ length: 20 }, (x, i) => i);
    const colyValues = xValues.map(() => 10 + Math.random() * 40);
    const colmetadata = xValues.map(() => ({
        isSelected: Math.random() < 0.3,
    }));
    const columnSeries = new FastColumnRenderableSeries(wasmContext, {
        fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
            { color: appTheme.MutedRed, offset: 0 },
            { color: appTheme.MutedTeal, offset: 1 },
        ]),
        dataSeries: new XyDataSeries(wasmContext, {
            xValues,
            yValues: colyValues,
            metadata: colmetadata,
            containsNaN: true,
        }),
        stroke: "transparent",
    });
    // We cannot use a paletteProvider to change a gradient fill, so we have to use a second drawingProvider
    const selectedColDP = new ColumnSeriesDrawingProvider(
        wasmContext,
        columnSeries,
        // configure this to draw using the selected points
        (ps) => (ps as IOhlcPointSeries).lowValues
    );
    selectedColDP.getProperties = (parentSeries) => {
        const { stroke, strokeThickness, fill } = parentSeries;
        return {
            opacity: 1,
            // Opacity setting does not currently apply to the gradient colors, so we have to apply it individually
            fillLinearGradient: new GradientParams(new Point(0, 0), new Point(0, 1), [
                { color: appTheme.MutedRed + "88", offset: 0 },
                { color: appTheme.MutedSkyBlue + "88", offset: 1 },
            ]),
            stroke,
            strokeThickness,
            fill,
        };
    };
    columnSeries.drawingProviders.push(selectedColDP);
    columnSeries.renderDataTransform = new SplitRenderDataTransform(
        columnSeries,
        wasmContext,
        columnSeries.drawingProviders
    );
    sciChartSurface.renderableSeries.add(columnSeries);

    const lineyValues = xValues.map((x) => 30 + x + x * Math.random());
    const linemetadata = xValues.map(() => ({
        isSelected: Math.random() < 0.3,
    }));
    // Line series with different pointmarker and dashed line for selected sections
    const lineSeries = new FastLineRenderableSeries(wasmContext, {
        dataSeries: new XyDataSeries(wasmContext, {
            xValues,
            yValues: lineyValues,
            metadata: linemetadata,
            containsNaN: true,
        }),
        pointMarker: new EllipsePointMarker(wasmContext, {
            width: 14,
            height: 14,
            strokeThickness: 0,
            fill: appTheme.VividSkyBlue,
        }),
        stroke: appTheme.VividTeal,
        strokeThickness: 3,
        drawNaNAs: ELineDrawMode.DiscontinuousLine,
    });

    const trianglePM = new TrianglePointMarker(wasmContext, {
        width: 15,
        height: 15,
        strokeThickness: 0,
        fill: appTheme.VividOrange,
    });
    // Additional line drawing for selected segments
    const selectedLineDP = new LineSeriesDrawingProvider(
        wasmContext,
        lineSeries,
        (ps) => (ps as IOhlcPointSeries).highValues
    );
    // Make this drawingProvider use dashed lines
    selectedLineDP.getProperties = (parentSeries) => {
        const { stroke, strokeThickness, isDigitalLine, lineType, drawNaNAs } = parentSeries;
        return {
            stroke,
            strokeThickness,
            strokeDashArray: [3, 4],
            isDigitalLine,
            lineType,
            drawNaNAs,
            containsNaN: true,
        } as ILineSeriesDrawingProviderProperties;
    };
    // Add this as the first drawingProviders so it draws behind all pointmarkers
    lineSeries.drawingProviders.unshift(selectedLineDP);

    // Additional point drawing for selected points
    const triangleDP = new PointMarkerDrawingProvider(
        wasmContext,
        lineSeries,
        (ps) => (ps as IOhlcPointSeries).lowValues
    );
    triangleDP.getProperties = () => {
        return { pointMarker: trianglePM as IPointMarker };
    };
    lineSeries.drawingProviders.push(triangleDP);

    // Apply the transform to all the drawingProviders
    lineSeries.renderDataTransform = new SplitRenderDataTransform(lineSeries, wasmContext, lineSeries.drawingProviders);
    sciChartSurface.renderableSeries.add(lineSeries);

    sciChartSurface.annotations.add(
        new NativeTextAnnotation({
            xCoordinateMode: ECoordinateMode.Pixel,
            yCoordinateMode: ECoordinateMode.Pixel,
            x1: 20,
            y1: 20,
            horizontalAnchorPoint: EHorizontalAnchorPoint.Left,
            verticalAnchorPoint: EVerticalAnchorPoint.Top,
            text: "Selected points are styled differently.  Click and drag to change the selection",
            textColor: appTheme.ForegroundColor,
            fontSize: 16,
            opacity: 0.77,
        })
    );

    // Optional: Add Interactivity Modifiers
    sciChartSurface.chartModifiers.add(
        new DataPointSelectionModifier({
            allowClickSelect: true,
            onSelectionChanged: () => {
                lineSeries.renderDataTransform.requiresTransform = true;
                columnSeries.renderDataTransform.requiresTransform = true;
            },
        })
    );

    sciChartSurface.zoomExtents();
    return { sciChartSurface, wasmContext };
};

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.