React Animated Bar Chart Example

Creates a React Animated Bar Chart using SciChart.js that displays animated ATP Year-end Top Ten rankings from 1990 to 2024.

Fullscreen

Edit

 Edit

Docs

src/drawExample.ts
import {
    SciChartSurface,
    NumericAxis,
    FastRectangleRenderableSeries,
    EColumnMode,
    EColumnYMode,
    Thickness,
    EAxisAlignment,
    ETextAlignment,
    ETitlePosition,
    IPointMetadata,
    easing,
    EDataLabelSkipMode,
    EVerticalTextPosition,
    EHorizontalTextPosition,
    NumberRange,
    XyxDataSeries,
    ColumnAnimation,
    XyDataSeries,
    DefaultPaletteProvider,
    TSciChart,
    parseColorToUIntArgb,
} from "scichart";

import { appTheme } from "./appTheme";
import { data } from "./atp-rankings";

type ATPMetadata = IPointMetadata & {
    rank: number;
    name: string;
    country: string;
};
class CountryPaletteProvider extends DefaultPaletteProvider {
    private colorMap: Map<string, { stroke: number; fill: number }> = new Map<
        string,
        { stroke: number; fill: number }
    >();

    constructor(wasmContext: TSciChart) {
        super();
        const countries = [
            "SE",
            "CS",
            "US",
            "EC",
            "AT",
            "HR",
            "NL",
            "UA",
            "RU",
            "ZA",
            "AU",
            "GB",
            "CL",
            "SK",
            "BR",
            "CH",
            "CZ",
            "AR",
            "RS",
            "JP",
            "ES",
            "YU",
            "FR",
            "CA",
            "BG",
            "BE",
            "GR",
            "IT",
            "NO",
            "DE",
            "PL",
            "DK",
        ];
        const max = countries.length - 1;
        for (let i = 0; i < countries.length; i++) {
            const country = countries[i];
            const stroke = parseColorToUIntArgb(appTheme.SciChartJsTheme.getStrokeColor(i, max, wasmContext));
            const fill = parseColorToUIntArgb(appTheme.SciChartJsTheme.getFillColor(i, max, wasmContext));
            this.colorMap.set(country, { stroke, fill });
        }
    }

    public overrideStrokeArgb(
        xValue: number,
        yValue: number,
        index: number,
        opacity?: number,
        metadata?: IPointMetadata
    ): number | undefined {
        const country = (metadata as ATPMetadata).country;
        return this.colorMap.get(country).stroke;
    }

    public overrideFillArgb(
        xValue: number,
        yValue: number,
        index: number,
        opacity?: number,
        metadata?: IPointMetadata
    ): number | undefined {
        const country = (metadata as ATPMetadata).country;
        return this.colorMap.get(country).fill;
    }
}

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

    // Setup axes
    sciChartSurface.xAxes.add(
        new NumericAxis(wasmContext, {
            visibleRange: new NumberRange(0, 15),
            isVisible: false,
        })
    );

    sciChartSurface.yAxes.add(
        new NumericAxis(wasmContext, {
            visibleRange: new NumberRange(0.5, 10.5),
            axisTitle: "Rank",
            axisTitleStyle: {
                fontSize: 18,
            },
            axisAlignment: EAxisAlignment.Left,
            drawMajorBands: false,
            drawLabels: true,
            drawMinorGridLines: false,
            drawMajorGridLines: false,
            drawMinorTickLines: false,
            drawMajorTickLines: false,
            keepLabelsWithinAxis: false,
            autoTicks: false,
            flippedCoordinates: true,
            majorDelta: 1,
            labelPrecision: 0,
            labelStyle: {
                fontSize: 22,
                fontFamily: "Arial",
            },
        })
    );

    sciChartSurface.yAxes.get(0).axisRenderer.hideOverlappingLabels = false;

    sciChartSurface.title = [`ATP Year-end Top 10 in ${data[0].year.toString()}`];

    const topMargin = 30;
    const rightMargin = 30;
    const bottomMargin = 30;
    const leftMargin = 30;

    sciChartSurface.padding = new Thickness(topMargin, rightMargin, bottomMargin, leftMargin);

    const xValues: number[] = [];
    const x1Values: number[] = [];
    const yValues: number[] = [];
    const metadata: ATPMetadata[] = [];

    for (const element of data[0].top10) {
        xValues.push(0);
        yValues.push(element.rank);
        x1Values.push(16 - element.rank);
        metadata.push({ isSelected: false, ...element });
    }

    // setup data
    const dataSeriesA = new XyxDataSeries(wasmContext, { xValues, yValues, x1Values, metadata });
    const dataSeriesB = new XyxDataSeries(wasmContext);

    const rectangleSeries = new FastRectangleRenderableSeries(wasmContext, {
        dataSeries: dataSeriesA,
        columnXMode: EColumnMode.StartEnd,
        columnYMode: EColumnYMode.CenterHeight,
        defaultY1: 1,
        strokeThickness: 4,
        opacity: 0.3,
        paletteProvider: new CountryPaletteProvider(wasmContext),
        dataLabels: {
            skipMode: EDataLabelSkipMode.ShowAll,
            verticalTextPosition: EVerticalTextPosition.Center,
            horizontalTextPosition: EHorizontalTextPosition.Center,
            style: {
                fontFamily: "Arial",
                fontSize: 16,
            },
            color: appTheme.ForegroundColor,
            metaDataSelector: (md) => {
                const metadata = md as ATPMetadata;
                return `${metadata.name.toString()} (${metadata.country.toString()})`;
            },
            //updateTextInAnimation: true
        },
    });
    sciChartSurface.renderableSeries.add(rectangleSeries);
    // Setup animations

    const updateData = (i: number, curDataSeries: XyxDataSeries, nextDataSeries: XyxDataSeries) => {
        sciChartSurface.title = [`ATP Year-end Top 10 in ${data[i].year.toString()}`];
        nextDataSeries.clear();
        const cur: ATPMetadata[] = [];
        const next = data[i].top10;
        // Series animations work by animating values at the same index, so it is important to preserve the order of entries, which may be totally unrelated to the display order
        for (let p = 0; p < curDataSeries.count(); p++) {
            // Look at all existing entries
            const e = curDataSeries.getMetadataAt(p) as ATPMetadata;
            // see if they should still be on the chart in the next period
            const eNext = next.find((n) => n.name === e.name);
            if (eNext) {
                // Add to next data with new value
                nextDataSeries.append(0, eNext.rank, 16 - eNext.rank, { isSelected: false, ...eNext });
            } else {
                if (curDataSeries.getNativeYValues().get(p) > 0) {
                    // If they are currently in view, set them to be out of view in next period
                    nextDataSeries.append(0, 12, 0, e);
                }
            }
            // track all the current entries
            cur.push(e);
        }
        for (const element of next) {
            // Find entries that are completely new
            const isNew = cur.find((e) => e.name === element.name) === undefined;
            if (isNew) {
                // add out of view in current data, and with new value in next data
                curDataSeries.append(0, 12, 0, { isSelected: false, ...element });
                nextDataSeries.append(0, element.rank, 16 - element.rank, { isSelected: false, ...element });
            }
        }
        //Create an animation which will call the update for the following period when it completes
        const animation = new ColumnAnimation({
            duration: 1000,
            ease: easing.inOutQuart,
            dataSeries: nextDataSeries as any as XyDataSeries,
            onCompleted: () => {
                if (i < data.length - 2) {
                    updateData(i + 1, curDataSeries, nextDataSeries);
                }
            },
        });
        rectangleSeries.runAnimation(animation);
    };

    sciChartSurface.titleStyle = {
        color: appTheme.ForegroundColor,
        fontSize: 30,
        alignment: ETextAlignment.Center,
        position: ETitlePosition.Top,
        placeWithinChart: false,
        padding: Thickness.fromString("40 0 0 0"),
    };
    updateData(1, dataSeriesA, dataSeriesB);

    return { sciChartSurface, wasmContext };
};

Animated Columns Chart - React

Overview

This React example creates an animated visualization of ATP tennis rankings using SciChart.js. The component leverages the SciChartReact wrapper for seamless integration with React's lifecycle.

Technical Implementation

The chart is initialized via the initChart prop which creates a SciChartSurface with a FastRectangleRenderableSeries. The implementation uses EColumnMode.StartEnd for precise column positioning and a custom palette provider for country-based coloring.

Features and Capabilities

The component automatically animates between yearly ranking data using ColumnAnimation with easing functions. Data labels show player information, and the chart title updates dynamically. The flipped Y-axis properly displays rankings with #1 at the top.

Integration and Best Practices

The example demonstrates React best practices by encapsulating chart logic in the drawExample function. The SciChartReact component handles surface creation and cleanup automatically. For larger datasets, consider implementing virtualized data loading.