React Population Pyramid

Population Pyramid of Europe and Africa using SciChart.js High Performance JavaScript Charts. This also demonstrates the use of DataLabelLayoutManager to Modify the positions of data labels from different series to prevent overlap

Fullscreen

Edit

 Edit

Docs

src/drawExample.ts
import {
    EAxisAlignment,
    MouseWheelZoomModifier,
    NumberRange,
    NumericAxis,
    XyDataSeries,
    ZoomExtentsModifier,
    SciChartSurface,
    ENumericFormat,
    EColumnDataLabelPosition,
    StackedColumnRenderableSeries,
    Thickness,
    LegendModifier,
    StackedColumnCollection,
    IDataLabelLayoutManager,
    RenderPassInfo,
    IRenderableSeries,
    IStackedColumnSeriesDataLabelProviderOptions,
    BottomAlignedOuterHorizontallyStackedAxisLayoutStrategy,
    ELegendPlacement,
    WaveAnimation,
} from "scichart";
import { appTheme } from "./appTheme";

// custom label manager to avoid overlapping labels
class CustomDataLabelManager implements IDataLabelLayoutManager {
    performTextLayout(sciChartSurface: SciChartSurface, renderPassInfo: RenderPassInfo): void {
        const renderableSeries = sciChartSurface.renderableSeries.asArray() as IRenderableSeries[];

        for (let i = 0; i < renderableSeries.length; i++) {
            // loop through all series (i.e. 2 stacked series - Male and Female)

            const currentSeries = renderableSeries[i] as StackedColumnRenderableSeries;
            if (currentSeries instanceof StackedColumnCollection) {
                // @ts-ignore
                const stackedSeries: StackedColumnRenderableSeries[] = currentSeries.asArray();

                const outerSeries = stackedSeries[1]; // the outer Series (i.e. Africa),
                const innerSeries = stackedSeries[0]; // the inner Series (i.e. Europe)

                if (!innerSeries.isVisible) {
                    continue; // to NOT use accumulated value to outer series if inner series is hidden
                }

                const outerLabels = outerSeries.dataLabelProvider?.dataLabels || [];
                const innerLabels = innerSeries.dataLabelProvider?.dataLabels || [];

                let outerIndex = 0; // used to sync the outer labels with the inner labels

                for (let k = 0; k < innerLabels.length; k++) {
                    const outerLabel = outerLabels[outerIndex];
                    const innerLabel = innerLabels[k];

                    if (outerLabel && innerLabel) {
                        const outerLabelPosition = outerLabel.position;
                        const innerLabelPosition = innerLabel.position;

                        if (Math.abs(outerLabelPosition.y - innerLabelPosition.y) > outerLabel.rect.height / 2) {
                            continue; // do not align labels if they are not on the same level
                        }

                        outerIndex++;

                        // calculate threshold for overlapping
                        const limitWidth = i == 0 ? outerLabel.rect.width : innerLabel.rect.width;

                        // minimum margin between 2 labels, feel free to experiment with different values
                        const marginBetweenLabels = 12;

                        if (Math.abs(outerLabelPosition.x - innerLabelPosition.x) < limitWidth) {
                            // console.log(`Aligning labels: ${outerLabel.text} with ${innerLabel.text}`);
                            let newX;
                            if (i == 0) {
                                // if we are in Male (left) chart, draw left
                                newX = innerLabel.position.x - outerLabel.rect.width - marginBetweenLabels;
                            } else {
                                // if we are in Female (right) chart, draw right
                                newX = innerLabel.rect.right + marginBetweenLabels;
                            }

                            outerLabel.position = {
                                x: newX,
                                y: outerLabel.position.y,
                            };
                        }
                    }
                }
            }
        }
    }
}

// Population Pyramid Data
const PopulationData = {
    xValues: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100],
    yValues: {
        Africa: {
            male: [
                35754890, 31813896, 28672207, 24967595, 20935790, 17178324, 14422055, 12271907, 10608417, 8608183,
                6579937, 5035598, 3832420, 2738448, 1769284, 1013988, 470834, 144795, 26494, 2652, 140,
            ],
            female: [
                34834623, 31000760, 27861135, 24206021, 20338468, 16815440, 14207659, 12167437, 10585531, 8658614,
                6721555, 5291815, 4176910, 3076943, 2039952, 1199203, 591092, 203922, 45501, 5961, 425,
            ],
        },
        Europe: {
            male: [
                4869936, 5186991, 5275063, 5286053, 5449038, 5752398, 6168124, 6375035, 6265554, 5900833, 6465830,
                7108184, 6769524, 5676968, 4828153, 3734266, 2732054, 1633630, 587324, 128003, 12023,
            ],
            female: [
                4641147, 4940521, 5010242, 5010526, 5160160, 5501673, 6022599, 6329356, 6299693, 5930345, 6509757,
                7178487, 7011569, 6157651, 5547296, 4519433, 3704145, 2671974, 1276597, 399148, 60035,
            ],
        },
    },
};

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

    // Create XAxis, and the 2 YAxes
    const xAxis = new NumericAxis(wasmContext, {
        labelPrecision: 0,
        autoTicks: false,
        majorDelta: 5,
        flippedCoordinates: true,
        axisAlignment: EAxisAlignment.Left,
        axisTitle: "Age",
    });

    // Force the visible range to always be a fixed value, overriding any zoom behaviour
    xAxis.visibleRangeChanged.subscribe(() => {
        xAxis.visibleRange = new NumberRange(-3, 103); // +-3 for extra padding
    });

    // 2 Y Axes (left and right)
    const yAxisRight = new NumericAxis(wasmContext, {
        axisAlignment: EAxisAlignment.Bottom,
        flippedCoordinates: true,
        axisTitle: "Female",
        labelStyle: {
            fontSize: 12,
        },
        growBy: new NumberRange(0, 0.15), // to have the furthest right labels visible
        labelFormat: ENumericFormat.Engineering,
        id: "femaleAxis",
    });

    // Sync the visible range of the 2 Y axes
    yAxisRight.visibleRangeChanged.subscribe((args: any) => {
        if (args.visibleRange.min > 0) {
            yAxisRight.visibleRange = new NumberRange(0, args.visibleRange.max);
        }
        yAxisLeft.visibleRange = new NumberRange(0, args.visibleRange.max);
    });

    const yAxisLeft = new NumericAxis(wasmContext, {
        axisAlignment: EAxisAlignment.Bottom,
        axisTitle: "Male",
        labelStyle: {
            fontSize: 12,
        },
        growBy: new NumberRange(0, 0.15), // to have the furthest left labels visible
        labelFormat: ENumericFormat.Engineering,
        id: "maleAxis",
    });

    // Sync the visible range of the 2 Y axes
    yAxisLeft.visibleRangeChanged.subscribe((args: any) => {
        if (args.visibleRange.min > 0) {
            yAxisLeft.visibleRange = new NumberRange(0, args.visibleRange.max);
        }
        yAxisRight.visibleRange = new NumberRange(0, args.visibleRange.max);
    });

    sciChartSurface.xAxes.add(xAxis);
    sciChartSurface.yAxes.add(yAxisLeft, yAxisRight);

    const dataLabels: IStackedColumnSeriesDataLabelProviderOptions = {
        positionMode: EColumnDataLabelPosition.Outside,
        style: {
            fontFamily: "Arial",
            fontSize: 12,
            padding: new Thickness(0, 3, 0, 3),
        },
        color: appTheme.TextColor,
        numericFormat: ENumericFormat.Engineering,
    };

    // Create some RenderableSeries or each part of the stacked column
    const maleChartEurope = new StackedColumnRenderableSeries(wasmContext, {
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: PopulationData.xValues,
            yValues: PopulationData.yValues.Europe.male,
            dataSeriesName: "Male Europe",
        }),
        fill: appTheme.VividBlue + "99",
        stroke: appTheme.TextColor,
        stackedGroupId: "MaleSeries",
        dataLabels,
    });

    const maleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: PopulationData.xValues,
            yValues: PopulationData.yValues.Africa.male,
            dataSeriesName: "Male Africa",
        }),
        fill: appTheme.VividBlue + "cc",
        stroke: appTheme.TextColor,
        stackedGroupId: "MaleSeries",
        dataLabels,
    });

    // female charts
    const femaleChartEurope = new StackedColumnRenderableSeries(wasmContext, {
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: PopulationData.xValues,
            yValues: PopulationData.yValues.Europe.female,
            dataSeriesName: "Female Europe",
        }),
        fill: appTheme.VividRed + "99",
        stroke: appTheme.TextColor,
        stackedGroupId: "FemaleSeries",
        dataLabels,
    });

    const femaleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
        dataSeries: new XyDataSeries(wasmContext, {
            xValues: PopulationData.xValues,
            yValues: PopulationData.yValues.Africa.female,
            dataSeriesName: "Female Africa",
        }),
        fill: appTheme.VividRed + "cc",
        stroke: appTheme.TextColor,
        stackedGroupId: "FemaleSeries",
        dataLabels,
    });

    const stackedColumnCollectionMale = new StackedColumnCollection(wasmContext, {
        dataPointWidth: 0.9,
        yAxisId: "maleAxis",
    });
    const stackedColumnCollectionFemale = new StackedColumnCollection(wasmContext, {
        dataPointWidth: 0.9,
        yAxisId: "femaleAxis",
    });

    stackedColumnCollectionMale.add(maleChartEurope, maleChartAfrica);
    stackedColumnCollectionFemale.add(femaleChartEurope, femaleChartAfrica);

    // add wave animation to the series
    stackedColumnCollectionMale.animation = new WaveAnimation({ duration: 1000 });
    stackedColumnCollectionFemale.animation = new WaveAnimation({ duration: 1000 });

    // manage data labels overlapping with custom layout manager
    sciChartSurface.dataLabelLayoutManager = new CustomDataLabelManager();

    // Add the Stacked Column collection to the chart
    sciChartSurface.renderableSeries.add(stackedColumnCollectionMale, stackedColumnCollectionFemale);

    sciChartSurface.layoutManager.bottomOuterAxesLayoutStrategy =
        new BottomAlignedOuterHorizontallyStackedAxisLayoutStrategy(); // stack and sync the 2 Y axes

    const maleLegend = new LegendModifier({
        showCheckboxes: true,
        showSeriesMarkers: true,
        showLegend: true,
        placement: ELegendPlacement.TopLeft,
    });

    const femaleLegend = new LegendModifier({
        showCheckboxes: true,
        showSeriesMarkers: true,
        showLegend: true,
        placement: ELegendPlacement.TopRight,
    });

    // Add zooming and panning behaviour
    sciChartSurface.chartModifiers.add(
        new ZoomExtentsModifier(),
        new MouseWheelZoomModifier(),
        maleLegend,
        femaleLegend
    );

    // exclude Male series for the Female legend
    femaleLegend.includeSeries(maleChartEurope, false);
    femaleLegend.includeSeries(maleChartAfrica, false);

    // exclude Female series for the Male legend
    maleLegend.includeSeries(femaleChartEurope, false);
    maleLegend.includeSeries(femaleChartAfrica, false);

    sciChartSurface.zoomExtents();

    return { sciChartSurface, wasmContext };
};

React Population Pyramid Example

Overview

This example demonstrates a population pyramid chart implemented in a React application using SciChart.js. It visualizes demographic data for Europe and Africa, showcasing a highly interactive chart with animated stacked column series and custom data label management to avoid overlapping. The project leverages the SciChart React component for seamless integration with React.

Technical Implementation

The implementation uses the SciChart.js API in a React environment by invoking the drawExample function via the <SciChartReact/> component. The code initializes a chart with two y-axes for male and female data and a single x-axis representing age groups. The example also has a CustomDataLabelManager implementing IDataLabelLayoutManager to modify overlapping label positions, in line with techniques outlined in the Data Label Positioning documentation. Additionally, the chart introduces a wave animation effect on the stacked column series, which is explained in the JavaScript Stacked Column Chart example.

Features and Capabilities

The example features real-time animation for both male and female data series with stacked column collections. It employs dynamic legends, zooming, and panning modifiers such as ZoomExtentsModifier and MouseWheelZoomModifier to enhance user interactivity. The axes are synchronized so that any changes in the axis.visibleRange of one are reflected in the other, following best practices described in the Synchronizing Multiple Charts documentation. This ensures a coherent presentation of comparative data across the two populations.

Integration and Best Practices

The React integration leverages the <SciChartReact/> component to instantiate and render the chart within a React component, ensuring maintainability and separation of concerns. Developers can refer to the Creating a React Dashboard with SciChart.js, SciChart-React and DeepSeek R1 for further insights on integrating SciChart.js in React applications. The example also highlights performance optimization techniques through the efficient use of WebGL and a custom data label layout manager to handle overlapping labels without sacrificing performance. Additionally, theming is applied using a custom appTheme, the approach for which is documented in the Using Theme Manager - JavaScript Chart - SciChart article.