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
drawExample.ts
index.tsx
theme.ts
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: "#EEEEEE",
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 stackedGroupId: "MaleSeries",
204 dataLabels,
205 });
206
207 const maleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
208 dataSeries: new XyDataSeries(wasmContext, {
209 xValues: PopulationData.xValues,
210 yValues: PopulationData.yValues.Africa.male,
211 dataSeriesName: "Male Africa",
212 }),
213 fill: appTheme.VividBlue,
214 stackedGroupId: "MaleSeries",
215 dataLabels,
216 });
217
218 // female charts
219 const femaleChartEurope = new StackedColumnRenderableSeries(wasmContext, {
220 dataSeries: new XyDataSeries(wasmContext, {
221 xValues: PopulationData.xValues,
222 yValues: PopulationData.yValues.Europe.female,
223 dataSeriesName: "Female Europe",
224 }),
225 fill: appTheme.VividRed + "99",
226 stackedGroupId: "FemaleSeries",
227 dataLabels,
228 });
229
230 const femaleChartAfrica = new StackedColumnRenderableSeries(wasmContext, {
231 dataSeries: new XyDataSeries(wasmContext, {
232 xValues: PopulationData.xValues,
233 yValues: PopulationData.yValues.Africa.female,
234 dataSeriesName: "Female Africa",
235 }),
236 fill: appTheme.VividRed,
237 stackedGroupId: "FemaleSeries",
238 dataLabels,
239 });
240
241 const stackedColumnCollectionMale = new StackedColumnCollection(wasmContext, {
242 dataPointWidth: 0.9,
243 yAxisId: "maleAxis",
244 });
245 const stackedColumnCollectionFemale = new StackedColumnCollection(wasmContext, {
246 dataPointWidth: 0.9,
247 yAxisId: "femaleAxis",
248 });
249
250 stackedColumnCollectionMale.add(maleChartEurope, maleChartAfrica);
251 stackedColumnCollectionFemale.add(femaleChartEurope, femaleChartAfrica);
252
253 // add wave animation to the series
254 stackedColumnCollectionMale.animation = new WaveAnimation({ duration: 1000 });
255 stackedColumnCollectionFemale.animation = new WaveAnimation({ duration: 1000 });
256
257 // manage data labels overlapping with custom layout manager
258 sciChartSurface.dataLabelLayoutManager = new CustomDataLabelManager();
259
260 // Add the Stacked Column collection to the chart
261 sciChartSurface.renderableSeries.add(stackedColumnCollectionMale, stackedColumnCollectionFemale);
262
263 sciChartSurface.layoutManager.bottomOuterAxesLayoutStrategy =
264 new BottomAlignedOuterHorizontallyStackedAxisLayoutStrategy(); // stack and sync the 2 Y axes
265
266 const maleLegend = new LegendModifier({
267 showCheckboxes: true,
268 showSeriesMarkers: true,
269 showLegend: true,
270 backgroundColor: "#222",
271 placement: ELegendPlacement.TopLeft,
272 });
273
274 const femaleLegend = new LegendModifier({
275 showCheckboxes: true,
276 showSeriesMarkers: true,
277 showLegend: true,
278 backgroundColor: "#222",
279 placement: ELegendPlacement.TopRight,
280 });
281
282 // Add zooming and panning behaviour
283 sciChartSurface.chartModifiers.add(
284 new ZoomExtentsModifier(),
285 new MouseWheelZoomModifier(),
286 maleLegend,
287 femaleLegend
288 );
289
290 // exclude Male series for the Female legend
291 femaleLegend.includeSeries(maleChartEurope, false);
292 femaleLegend.includeSeries(maleChartAfrica, false);
293
294 // exclude Female series for the Male legend
295 maleLegend.includeSeries(femaleChartEurope, false);
296 maleLegend.includeSeries(femaleChartAfrica, false);
297
298 sciChartSurface.zoomExtents();
299
300 return { sciChartSurface, wasmContext };
301};
302This 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.
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.
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.
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.

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

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

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

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

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

Demonstrates how to create Oil and Gas Dashboard

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

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

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

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

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

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