React Windrose Plot | React Polar Stacked Radial Column Chart

Creates a React Windrose Column Chart using SciChart.js, via PolarStackedColumnRenderableSeries and a custom axis LabelProvider for cardinal directions.

This plot type is Known As: Wind Rose Chart and Wind Direction Chart or Wind Speed Chart.

Fullscreen

Edit

 Edit

Docs

src/drawExample.ts
import {
    PolarMouseWheelZoomModifier,
    PolarZoomExtentsModifier,
    PolarPanModifier,
    XyDataSeries,
    PolarNumericAxis,
    SciChartPolarSurface,
    EPolarAxisMode,
    NumberRange,
    EAxisAlignment,
    PolarStackedColumnCollection,
    PolarStackedColumnRenderableSeries,
    TFormatLabelFn,
    NumericLabelProvider,
    EDataPointWidthMode,
    WaveAnimation,
} from "scichart";
import { appTheme } from "./appTheme";

function getBiasedRandomWalkInBounds(min: number, max: number, count: number) {
    // Generate the base random walk
    const baseValues = [min];
    for (let i = 1; i < count; i++) {
        const next = baseValues[i - 1] + Math.random() - 0.5;
        baseValues.push(Math.min(max, Math.max(min, next)));
    }

    // Apply an angular bias so that the random walk values become
    return baseValues.map((val, i) => {
        const angle = (i * 360) / count;
        const angleRad = (angle * Math.PI) / 180;
        // bias ranges from 0.5 to 1.5: peaks at 0°/180°, dips at 90°/270°
        const bias = 1 + 0.3 * Math.sin(2 * angleRad);
        return val * bias;
    });
}

/**
 * Custom label provider that displays compass directions at 45 degree intervals,
 * if any label value is NOT from `[0, 45, 90, 135, 180, 225, 270, 315]` it will be a decimal.
 */
class CustomNESWLabelProvider extends NumericLabelProvider {
    public LABELS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];

    public get formatLabel(): TFormatLabelFn {
        return (dataValue: number) => {
            if (dataValue % 45 === 0) {
                return this.LABELS[dataValue / 45];
            }
            return dataValue.toFixed(0) + "°";
        };
    }
}

const COLUMN_COUNT = 24;

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

    const radialYAxis = new PolarNumericAxis(wasmContext, {
        axisAlignment: EAxisAlignment.Right,
        polarAxisMode: EPolarAxisMode.Radial,
        visibleRange: new NumberRange(0, 6),
        zoomExtentsToInitialRange: true,

        drawMinorGridLines: false,
        drawMajorTickLines: false,
        drawMinorTickLines: false,
        startAngle: Math.PI / 2, // draw labels at 12 o'clock
        autoTicks: false,
        majorDelta: 1,
        labelPrecision: 0,
        innerRadius: 0.05, // center hole
    });
    sciChartSurface.yAxes.add(radialYAxis);

    const polarXAxis = new PolarNumericAxis(wasmContext, {
        polarAxisMode: EPolarAxisMode.Angular,
        visibleRange: new NumberRange(0, 360),
        flippedCoordinates: true, // go clockwise
        startAngle: Math.PI / 2, // start at 12 o'clock
        axisAlignment: EAxisAlignment.Top,
        useNativeText: true,
        labelProvider: new CustomNESWLabelProvider(),
        autoTicks: false,
        majorDelta: 15,
        drawMinorGridLines: false,
        zoomExtentsToInitialRange: true,
    });
    sciChartSurface.xAxes.add(polarXAxis);

    const xValues = Array.from({ length: COLUMN_COUNT }, (_, i) => (i * 360) / COLUMN_COUNT); // [0, 10, ..., 350],
    const yValues = [
        getBiasedRandomWalkInBounds(1, 2, COLUMN_COUNT),
        getBiasedRandomWalkInBounds(0.3, 1, COLUMN_COUNT),
        getBiasedRandomWalkInBounds(0.3, 1, COLUMN_COUNT),
        getBiasedRandomWalkInBounds(0.5, 2, COLUMN_COUNT),
        getBiasedRandomWalkInBounds(0.2, 0.4, COLUMN_COUNT),
    ];

    const COLORS = [
        appTheme.DarkIndigo,
        appTheme.Indigo,
        appTheme.VividGreen,
        appTheme.VividOrange,
        appTheme.VividPink,
    ];

    const collection = new PolarStackedColumnCollection(wasmContext, {
        isOneHundredPercent: false,
    });

    for (let i = 0; i < yValues.length; i++) {
        const dataSeries = new XyDataSeries(wasmContext, { xValues, yValues: yValues[i] });
        const polarColumn = new PolarStackedColumnRenderableSeries(wasmContext, {
            dataSeries,
            fill: COLORS[i],
            stroke: appTheme.DarkIndigo,
            strokeThickness: 2,
            dataPointWidthMode: EDataPointWidthMode.Range,
            animation: new WaveAnimation({ duration: 1000 }),
        });
        collection.add(polarColumn);
    }
    sciChartSurface.renderableSeries.add(collection);

    sciChartSurface.chartModifiers.add(
        new PolarPanModifier(),
        new PolarZoomExtentsModifier(),
        new PolarMouseWheelZoomModifier()
    );

    return { sciChartSurface, wasmContext };
};

Polar Windrose Column Chart - React

Overview

This React example showcases a Polar Windrose Column Chart using SciChart.js, wrapped in a reusable component. The chart displays directional data with stacked columns in a polar layout.

Technical Implementation

The implementation uses SciChart React component with an initChart prop pointing to the drawExample function. The polar surface configuration matches the JavaScript version, using EPolarAxisMode for axis setup.

Features and Capabilities

The chart features compass-direction labels via custom label provider and stacked columns with distinct colors. The PolarMouseWheelZoomModifier enables intuitive zooming.

Integration and Best Practices

The example demonstrates React best practices by encapsulating chart logic in a separate function and using CSS modules for styling. For performance, consider memoizing the initChart callback in production apps.