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

drawExample.ts

index.tsx

theme.ts

Copy to clipboard
Minimise
Fullscreen
1import {
2    EAxisAlignment,
3    MouseWheelZoomModifier,
4    NumberRange,
5    NumericAxis,
6    XyDataSeries,
7    ZoomExtentsModifier,
8    SciChartSurface,
9    ENumericFormat,
10    EColumnDataLabelPosition,
11    StackedColumnRenderableSeries,
12    Thickness,
13    LegendModifier,
14    StackedColumnCollection,
15    IDataLabelLayoutManager,
16    RenderPassInfo,
17    IRenderableSeries,
18    IStackedColumnSeriesDataLabelProviderOptions,
19    BottomAlignedOuterHorizontallyStackedAxisLayoutStrategy,
20    ELegendPlacement,
21    WaveAnimation,
22    SciChartDefaults,
23} from "scichart";
24import { appTheme } from "../../../theme";
25
26// custom label manager to avoid overlapping labels
27class CustomDataLabelManager implements IDataLabelLayoutManager {
28    performTextLayout(sciChartSurface: SciChartSurface, renderPassInfo: RenderPassInfo): void {
29        const renderableSeries = sciChartSurface.renderableSeries.asArray() as IRenderableSeries[];
30
31        for (let i = 0; i < renderableSeries.length; i++) {
32            // loop through all series (i.e. 2 stacked series - Male and Female)
33
34            const currentSeries = renderableSeries[i] as StackedColumnRenderableSeries;
35            if (currentSeries instanceof StackedColumnCollection) {
36                // @ts-ignore
37                const stackedSeries: StackedColumnRenderableSeries[] = currentSeries.asArray();
38
39                const outerSeries = stackedSeries[1]; // the outer Series (i.e. Africa),
40                const innerSeries = stackedSeries[0]; // the inner Series (i.e. Europe)
41
42                if (!innerSeries.isVisible) {
43                    continue; // to NOT use accumulated value to outer series if inner series is hidden
44                }
45
46                const outerLabels = outerSeries.dataLabelProvider?.dataLabels || [];
47                const innerLabels = innerSeries.dataLabelProvider?.dataLabels || [];
48
49                let outerIndex = 0; // used to sync the outer labels with the inner labels
50
51                for (let k = 0; k < innerLabels.length; k++) {
52                    const outerLabel = outerLabels[outerIndex];
53                    const innerLabel = innerLabels[k];
54
55                    if (outerLabel && innerLabel) {
56                        const outerLabelPosition = outerLabel.position;
57                        const innerLabelPosition = innerLabel.position;
58
59                        if (Math.abs(outerLabelPosition.y - innerLabelPosition.y) > outerLabel.rect.height / 2) {
60                            continue; // do not align labels if they are not on the same level
61                        }
62
63                        outerIndex++;
64
65                        // calculate threshold for overlapping
66                        const limitWidth = i == 0 ? outerLabel.rect.width : innerLabel.rect.width;
67
68                        // minimum margin between 2 labels, feel free to experiment with different values
69                        const marginBetweenLabels = 12;
70
71                        if (Math.abs(outerLabelPosition.x - innerLabelPosition.x) < limitWidth) {
72                            // console.log(`Aligning labels: ${outerLabel.text} with ${innerLabel.text}`);
73                            let newX;
74                            if (i == 0) {
75                                // if we are in Male (left) chart, draw left
76                                newX = innerLabel.position.x - outerLabel.rect.width - marginBetweenLabels;
77                            } else {
78                                // if we are in Female (right) chart, draw right
79                                newX = innerLabel.rect.right + marginBetweenLabels;
80                            }
81
82                            outerLabel.position = {
83                                x: newX,
84                                y: outerLabel.position.y,
85                            };
86                        }
87                    }
88                }
89            }
90        }
91    }
92}
93
94// Population Pyramid Data
95const PopulationData = {
96    xValues: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100],
97    yValues: {
98        Africa: {
99            male: [
100                35754890, 31813896, 28672207, 24967595, 20935790, 17178324, 14422055, 12271907, 10608417, 8608183,
101                6579937, 5035598, 3832420, 2738448, 1769284, 1013988, 470834, 144795, 26494, 2652, 140,
102            ],
103            female: [
104                34834623, 31000760, 27861135, 24206021, 20338468, 16815440, 14207659, 12167437, 10585531, 8658614,
105                6721555, 5291815, 4176910, 3076943, 2039952, 1199203, 591092, 203922, 45501, 5961, 425,
106            ],
107        },
108        Europe: {
109            male: [
110                4869936, 5186991, 5275063, 5286053, 5449038, 5752398, 6168124, 6375035, 6265554, 5900833, 6465830,
111                7108184, 6769524, 5676968, 4828153, 3734266, 2732054, 1633630, 587324, 128003, 12023,
112            ],
113            female: [
114                4641147, 4940521, 5010242, 5010526, 5160160, 5501673, 6022599, 6329356, 6299693, 5930345, 6509757,
115                7178487, 7011569, 6157651, 5547296, 4519433, 3704145, 2671974, 1276597, 399148, 60035,
116            ],
117        },
118    },
119};
120
121export const drawExample = async (rootElement: string | HTMLDivElement) => {
122    const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
123        theme: appTheme.SciChartJsTheme,
124    });
125
126    // Create XAxis, and the 2 YAxes
127    const xAxis = new NumericAxis(wasmContext, {
128        labelPrecision: 0,
129        autoTicks: false,
130        majorDelta: 5,
131        flippedCoordinates: true,
132        axisAlignment: EAxisAlignment.Left,
133        axisTitle: "Age",
134    });
135
136    // Force the visible range to always be a fixed value, overriding any zoom behaviour
137    xAxis.visibleRangeChanged.subscribe(() => {
138        xAxis.visibleRange = new NumberRange(-3, 103); // +-3 for extra padding
139    });
140
141    // 2 Y Axes (left and right)
142    const yAxisRight = new NumericAxis(wasmContext, {
143        axisAlignment: EAxisAlignment.Bottom,
144        flippedCoordinates: true,
145        axisTitle: "Female",
146        labelStyle: {
147            fontSize: 12,
148        },
149        growBy: new NumberRange(0, 0.15), // to have the furthest right labels visible
150        labelFormat: ENumericFormat.Engineering,
151        id: "femaleAxis",
152    });
153
154    // Sync the visible range of the 2 Y axes
155    yAxisRight.visibleRangeChanged.subscribe((args: any) => {
156        if (args.visibleRange.min > 0) {
157            yAxisRight.visibleRange = new NumberRange(0, args.visibleRange.max);
158        }
159        yAxisLeft.visibleRange = new NumberRange(0, args.visibleRange.max);
160    });
161
162    const yAxisLeft = new NumericAxis(wasmContext, {
163        axisAlignment: EAxisAlignment.Bottom,
164        axisTitle: "Male",
165        labelStyle: {
166            fontSize: 12,
167        },
168        growBy: new NumberRange(0, 0.15), // to have the furthest left labels visible
169        labelFormat: ENumericFormat.Engineering,
170        id: "maleAxis",
171    });
172
173    // Sync the visible range of the 2 Y axes
174    yAxisLeft.visibleRangeChanged.subscribe((args: any) => {
175        if (args.visibleRange.min > 0) {
176            yAxisLeft.visibleRange = new NumberRange(0, args.visibleRange.max);
177        }
178        yAxisRight.visibleRange = new NumberRange(0, args.visibleRange.max);
179    });
180
181    sciChartSurface.xAxes.add(xAxis);
182    sciChartSurface.yAxes.add(yAxisLeft, yAxisRight);
183
184    const dataLabels: IStackedColumnSeriesDataLabelProviderOptions = {
185        positionMode: EColumnDataLabelPosition.Outside,
186        style: {
187            fontFamily: "Arial",
188            fontSize: 12,
189            padding: new Thickness(0, 3, 0, 3),
190        },
191        color: appTheme.TextColor,
192        numericFormat: ENumericFormat.Engineering,
193    };
194
195    // Create some RenderableSeries or each part of the stacked column
196    const maleChartEurope = new StackedColumnRenderableSeries(wasmContext, {
197        dataSeries: new XyDataSeries(wasmContext, {
198            xValues: PopulationData.xValues,
199            yValues: PopulationData.yValues.Europe.male,
200            dataSeriesName: "Male Europe",
201        }),
202        fill: appTheme.VividBlue + "99",
203        stroke: appTheme.TextColor,
204        stackedGroupId: "MaleSeries",
205        dataLabels,
206    });
207
208    const maleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
209        dataSeries: new XyDataSeries(wasmContext, {
210            xValues: PopulationData.xValues,
211            yValues: PopulationData.yValues.Africa.male,
212            dataSeriesName: "Male Africa",
213        }),
214        fill: appTheme.VividBlue + "cc",
215        stroke: appTheme.TextColor,
216        stackedGroupId: "MaleSeries",
217        dataLabels,
218    });
219
220    // female charts
221    const femaleChartEurope = new StackedColumnRenderableSeries(wasmContext, {
222        dataSeries: new XyDataSeries(wasmContext, {
223            xValues: PopulationData.xValues,
224            yValues: PopulationData.yValues.Europe.female,
225            dataSeriesName: "Female Europe",
226        }),
227        fill: appTheme.VividRed + "99",
228        stroke: appTheme.TextColor,
229        stackedGroupId: "FemaleSeries",
230        dataLabels,
231    });
232
233    const femaleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
234        dataSeries: new XyDataSeries(wasmContext, {
235            xValues: PopulationData.xValues,
236            yValues: PopulationData.yValues.Africa.female,
237            dataSeriesName: "Female Africa",
238        }),
239        fill: appTheme.VividRed + "cc",
240        stroke: appTheme.TextColor,
241        stackedGroupId: "FemaleSeries",
242        dataLabels,
243    });
244
245    const stackedColumnCollectionMale = new StackedColumnCollection(wasmContext, {
246        dataPointWidth: 0.9,
247        yAxisId: "maleAxis",
248    });
249    const stackedColumnCollectionFemale = new StackedColumnCollection(wasmContext, {
250        dataPointWidth: 0.9,
251        yAxisId: "femaleAxis",
252    });
253
254    stackedColumnCollectionMale.add(maleChartEurope, maleChartAfrica);
255    stackedColumnCollectionFemale.add(femaleChartEurope, femaleChartAfrica);
256
257    // add wave animation to the series
258    stackedColumnCollectionMale.animation = new WaveAnimation({ duration: 1000 });
259    stackedColumnCollectionFemale.animation = new WaveAnimation({ duration: 1000 });
260
261    // manage data labels overlapping with custom layout manager
262    sciChartSurface.dataLabelLayoutManager = new CustomDataLabelManager();
263
264    // Add the Stacked Column collection to the chart
265    sciChartSurface.renderableSeries.add(stackedColumnCollectionMale, stackedColumnCollectionFemale);
266
267    sciChartSurface.layoutManager.bottomOuterAxesLayoutStrategy =
268        new BottomAlignedOuterHorizontallyStackedAxisLayoutStrategy(); // stack and sync the 2 Y axes
269
270    const maleLegend = new LegendModifier({
271        showCheckboxes: true,
272        showSeriesMarkers: true,
273        showLegend: true,
274        placement: ELegendPlacement.TopLeft,
275    });
276
277    const femaleLegend = new LegendModifier({
278        showCheckboxes: true,
279        showSeriesMarkers: true,
280        showLegend: true,
281        placement: ELegendPlacement.TopRight,
282    });
283
284    // Add zooming and panning behaviour
285    sciChartSurface.chartModifiers.add(
286        new ZoomExtentsModifier(),
287        new MouseWheelZoomModifier(),
288        maleLegend,
289        femaleLegend
290    );
291
292    // exclude Male series for the Female legend
293    femaleLegend.includeSeries(maleChartEurope, false);
294    femaleLegend.includeSeries(maleChartAfrica, false);
295
296    // exclude Female series for the Male legend
297    maleLegend.includeSeries(femaleChartEurope, false);
298    maleLegend.includeSeries(femaleChartAfrica, false);
299
300    sciChartSurface.zoomExtents();
301
302    return { sciChartSurface, wasmContext };
303};
304

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.

react Chart Examples & Demos

See Also: Performance Demos & Showcases (12 Demos)

Realtime React Chart Performance Demo | SciChart.js Demo

Realtime React Chart Performance Demo

This demo showcases the incredible realtime performance of our React charts by updating the series with millions of data-points!

Load 500 Series x 500 Points Performance Demo | SciChart

Load 500 Series x 500 Points Performance Demo

This demo showcases the incredible performance of our React Chart by loading 500 series with 500 points (250k points) instantly!

Load 1 Million Points Performance Demo | SciChart.js Demo

Load 1 Million Points Performance Demo

This demo showcases the incredible performance of our JavaScript Chart by loading a million points instantly.

Realtime Ghosted Traces | React Charts | SciChart.js Demo

Realtime Ghosted Traces

This demo showcases the realtime performance of our React Chart by animating several series with thousands of data-points at 60 FPS

Realtime Audio Spectrum Analyzer Chart | SciChart.js Demo

Realtime Audio Spectrum Analyzer Chart Example

See the frequency of recordings with the React audio spectrum analyzer example from SciChart. This real-time audio visualizer demo uses a Fourier Transform.

Oil & Gas Explorer React Dashboard | SciChart.js Demo

Oil & Gas Explorer React Dashboard

Demonstrates how to create Oil and Gas Dashboard

Client/Server Websocket Data Streaming | SciChart.js Demo

Client/Server Websocket Data Streaming

This demo showcases the incredible realtime performance of our JavaScript charts by updating the series with millions of data-points!

Server Traffic Dashboard | React Charts | SciChart.js Demo

Server Traffic Dashboard

This dashboard demo showcases the incredible realtime performance of our React charts by updating the series with millions of data-points!

Rich Interactions Showcase | React Charts | SciChart.js

Rich Interactions Showcase

This demo showcases the incredible realtime performance of our React charts by updating the series with millions of data-points!

Dynamic Layout Showcase | React Charts | SciChart.js Demo

Dynamic Layout Showcase

Demonstrates a custom modifier which can convert from single chart to grid layout and back.

Dragabble Event Markers | React Charts | SciChart.js Demo

Dragabble Event Markers

Demonstrates how to repurpose a Candlestick Series into dragabble, labled, event markers

NEW!
High Performance SVG Cursor & Rollover | SciChart.js Demo

High Performance SVG Cursor & Rollover

Demonstrates how to use the SVG render layer in SciChart.js to maintain smooth cursor interaction on heavy charts with millions of points.

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