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: 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};
304This 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.