bento-charts 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/ChartConfigProvider.js +1 -12
  2. package/dist/ChartConfigProvider.js.map +1 -1
  3. package/dist/Components/Charts/BentoBarChart.d.ts +1 -1
  4. package/dist/Components/Charts/BentoBarChart.js +25 -37
  5. package/dist/Components/Charts/BentoBarChart.js.map +1 -1
  6. package/dist/Components/Charts/BentoPie.d.ts +1 -1
  7. package/dist/Components/Charts/BentoPie.js +55 -47
  8. package/dist/Components/Charts/BentoPie.js.map +1 -1
  9. package/dist/Components/Charts/ChartWrapper.d.ts +6 -0
  10. package/dist/Components/Charts/ChartWrapper.js +10 -0
  11. package/dist/Components/Charts/ChartWrapper.js.map +1 -0
  12. package/dist/Components/Maps/BentoChoroplethMap.d.ts +1 -1
  13. package/dist/Components/Maps/BentoChoroplethMap.js +15 -32
  14. package/dist/Components/Maps/BentoChoroplethMap.js.map +1 -1
  15. package/dist/Components/Maps/BentoMapContainer.js +1 -12
  16. package/dist/Components/Maps/BentoMapContainer.js.map +1 -1
  17. package/dist/Components/Maps/BentoPointMap.js +4 -15
  18. package/dist/Components/Maps/BentoPointMap.js.map +1 -1
  19. package/dist/Components/Maps/controls/MapLegendContinuous.js +1 -12
  20. package/dist/Components/Maps/controls/MapLegendContinuous.js.map +1 -1
  21. package/dist/Components/Maps/controls/MapLegendDiscrete.js +2 -13
  22. package/dist/Components/Maps/controls/MapLegendDiscrete.js.map +1 -1
  23. package/dist/Components/NoData.js +2 -13
  24. package/dist/Components/NoData.js.map +1 -1
  25. package/dist/constants/chartConstants.d.ts +0 -3
  26. package/dist/constants/chartConstants.js +2 -3
  27. package/dist/constants/chartConstants.js.map +1 -1
  28. package/dist/types/chartTypes.d.ts +10 -6
  29. package/dist/types/mapTypes.d.ts +2 -3
  30. package/dist/util/chartUtils.d.ts +2 -0
  31. package/dist/util/chartUtils.js +30 -0
  32. package/dist/util/chartUtils.js.map +1 -1
  33. package/package.json +17 -16
  34. package/src/Components/Charts/BentoBarChart.tsx +46 -53
  35. package/src/Components/Charts/BentoPie.tsx +63 -56
  36. package/src/Components/Charts/ChartWrapper.tsx +15 -0
  37. package/src/Components/Maps/BentoChoroplethMap.tsx +4 -12
  38. package/src/Components/Maps/BentoPointMap.tsx +13 -4
  39. package/src/constants/chartConstants.ts +2 -3
  40. package/src/types/chartTypes.ts +13 -7
  41. package/src/types/mapTypes.ts +3 -3
  42. package/src/util/chartUtils.ts +29 -0
  43. package/webpack.config.js +9 -1
@@ -1,5 +1,15 @@
1
- import React, { useState } from 'react';
2
- import { PieChart, Pie, Cell, Curve, Tooltip, Sector, PieProps, PieLabelRenderProps } from 'recharts';
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import {
3
+ PieChart,
4
+ Pie,
5
+ Cell,
6
+ Curve,
7
+ Tooltip,
8
+ Sector,
9
+ PieProps,
10
+ PieLabelRenderProps,
11
+ ResponsiveContainer,
12
+ } from 'recharts';
3
13
  import type CSS from 'csstype';
4
14
 
5
15
  import {
@@ -7,9 +17,7 @@ import {
7
17
  LABEL_STYLE,
8
18
  COUNT_STYLE,
9
19
  CHART_MISSING_FILL,
10
- CHART_WRAPPER_STYLE,
11
20
  RADIAN,
12
- CHART_ASPECT_RATIO,
13
21
  LABEL_THRESHOLD,
14
22
  COUNT_TEXT_STYLE,
15
23
  TEXT_STYLE,
@@ -21,8 +29,9 @@ import {
21
29
  useChartThreshold,
22
30
  useChartMaxLabelChars,
23
31
  } from '../../ChartConfigProvider';
24
- import { polarToCartesian } from '../../util/chartUtils';
32
+ import { polarToCartesian, useTransformedChartData } from '../../util/chartUtils';
25
33
  import NoData from '../NoData';
34
+ import ChartWrapper from './ChartWrapper';
26
35
 
27
36
  const labelShortName = (name: string, maxChars: number) => {
28
37
  if (name.length <= maxChars) {
@@ -33,17 +42,14 @@ const labelShortName = (name: string, maxChars: number) => {
33
42
  };
34
43
 
35
44
  const BentoPie = ({
36
- data,
37
45
  height,
38
- preFilter,
39
- dataMap,
40
- postFilter,
46
+ width,
41
47
  onClick,
42
48
  sort = true,
43
- removeEmpty = true,
44
49
  colorTheme = 'default',
45
50
  chartThreshold = useChartThreshold(),
46
51
  maxLabelChars = useChartMaxLabelChars(),
52
+ ...params
47
53
  }: PieChartProps) => {
48
54
  const t = useChartTranslation();
49
55
  const theme = useChartTheme().pie[colorTheme];
@@ -52,63 +58,63 @@ const BentoPie = ({
52
58
 
53
59
  // ##################### Data processing #####################
54
60
 
55
- data = [...data]; // Changing immutable data to mutable data
56
- if (preFilter) data = data.filter(preFilter);
57
- if (dataMap) data = data.map(dataMap);
58
- if (postFilter) data = data.filter(postFilter);
59
-
60
- // removing empty values
61
- if (removeEmpty) data = data.filter((e) => e.y !== 0);
62
-
63
- if (sort) data.sort((a, b) => a.y - b.y);
61
+ const transformedData = useTransformedChartData(params, true, sort);
62
+ const { data, sum } = useMemo(() => {
63
+ let data = [...transformedData];
64
64
 
65
- // combining sections with less than OTHER_THRESHOLD
66
- const sum = data.reduce((acc, e) => acc + e.y, 0);
67
- const length = data.length;
68
- const threshold = chartThreshold * sum;
69
- const temp = data.filter((e) => e.y > threshold);
65
+ // combining sections with less than chartThreshold
66
+ const sum = data.reduce((acc, e) => acc + e.y, 0);
67
+ const length = data.length;
68
+ const threshold = chartThreshold * sum;
69
+ const dataAboveThreshold = data.filter((e) => e.y > threshold);
70
+ // length - 1 intentional: if there is just one category below threshold, the "Other" category is not necessary.
71
+ data = dataAboveThreshold.length === length - 1 ? data : dataAboveThreshold;
72
+ if (data.length !== length) {
73
+ data.push({
74
+ x: t['Other'],
75
+ y: sum - data.reduce((acc, e) => acc + e.y, 0),
76
+ });
77
+ }
70
78
 
71
- // length - 1 intentional: if there is just one category bellow threshold the "Other" category is not necessary
72
- data = temp.length === length - 1 ? data : temp;
73
- if (data.length !== length) {
74
- data.push({
75
- x: t['Other'],
76
- y: sum - data.reduce((acc, e) => acc + e.y, 0),
77
- });
78
- }
79
+ return {
80
+ data: data.map((e) => ({ name: e.x, value: e.y })),
81
+ sum,
82
+ };
83
+ }, [transformedData, sort, chartThreshold]);
79
84
 
80
85
  if (data.length === 0) {
81
86
  return <NoData height={height} />;
82
87
  }
83
88
 
84
- const bentoFormatData = data.map((e) => ({ name: e.x, value: e.y }));
85
-
86
89
  // ##################### Rendering #####################
87
- const onEnter: PieProps['onMouseEnter'] = (_data, index) => {
90
+ const onEnter: PieProps['onMouseEnter'] = useCallback((_data, index) => {
88
91
  setActiveIndex(index);
89
- };
92
+ }, []);
90
93
 
91
- const onHover: PieProps['onMouseOver'] = (data, _index, e) => {
92
- const { target } = e;
93
- if (onClick && target && data.name !== t['Other']) (target as SVGElement).style.cursor = 'pointer';
94
- };
94
+ const onHover: PieProps['onMouseOver'] = useCallback(
95
+ (data, _index, e) => {
96
+ const { target } = e;
97
+ if (onClick && target && data.name !== t['Other']) (target as SVGElement).style.cursor = 'pointer';
98
+ },
99
+ [onClick]
100
+ );
95
101
 
96
- const onLeave: PieProps['onMouseLeave'] = () => {
102
+ const onLeave: PieProps['onMouseLeave'] = useCallback(() => {
97
103
  setActiveIndex(undefined);
98
- };
104
+ }, []);
99
105
 
100
106
  return (
101
- <>
102
- <div style={CHART_WRAPPER_STYLE}>
103
- <PieChart height={height} width={height * CHART_ASPECT_RATIO}>
107
+ <ChartWrapper>
108
+ <ResponsiveContainer width={width ?? "100%"} height={height}>
109
+ <PieChart>
104
110
  <Pie
105
- data={bentoFormatData}
111
+ data={data}
106
112
  dataKey="value"
107
113
  cx="50%"
108
114
  cy="50%"
109
- innerRadius={35}
110
- outerRadius={80}
111
- label={RenderLabel(maxLabelChars)}
115
+ innerRadius="25%"
116
+ outerRadius="55%"
117
+ label={renderLabel(maxLabelChars)}
112
118
  labelLine={false}
113
119
  isAnimationActive={false}
114
120
  onMouseEnter={onEnter}
@@ -119,8 +125,7 @@ const BentoPie = ({
119
125
  onClick={onClick}
120
126
  >
121
127
  {data.map((entry, index) => {
122
- let fill = theme[index % theme.length];
123
- fill = entry.x.toLowerCase() === 'missing' ? CHART_MISSING_FILL : fill;
128
+ const fill = entry.name.toLowerCase() === 'missing' ? CHART_MISSING_FILL : theme[index % theme.length];
124
129
  return <Cell key={index} fill={fill} />;
125
130
  })}
126
131
  </Pie>
@@ -130,8 +135,8 @@ const BentoPie = ({
130
135
  allowEscapeViewBox={{ x: true, y: true }}
131
136
  />
132
137
  </PieChart>
133
- </div>
134
- </>
138
+ </ResponsiveContainer>
139
+ </ChartWrapper>
135
140
  );
136
141
  };
137
142
 
@@ -144,9 +149,8 @@ const toNumber = (val: number | string | undefined, defaultValue?: number): numb
144
149
  return defaultValue || 0;
145
150
  };
146
151
 
147
- const RenderLabel =
148
- (maxLabelChars: number): PieProps['label'] =>
149
- (params: PieLabelRenderProps) => { // eslint-disable-line
152
+ const renderLabel = (maxLabelChars: number): PieProps['label'] => {
153
+ const BentoPieLabel = (params: PieLabelRenderProps) => {
150
154
  const { fill, payload, index, activeIndex } = params;
151
155
  const percent = params.percent || 0;
152
156
  const midAngle = params.midAngle || 0;
@@ -204,6 +208,9 @@ const RenderLabel =
204
208
  </g>
205
209
  );
206
210
  };
211
+ BentoPieLabel.displayName = BentoPieLabel;
212
+ return BentoPieLabel;
213
+ };
207
214
 
208
215
  const RenderActiveLabel: PieProps['activeShape'] = (params) => {
209
216
  const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = params;
@@ -0,0 +1,15 @@
1
+ import React, { ForwardedRef, forwardRef, ReactNode } from 'react';
2
+ import { CHART_WRAPPER_STYLE } from '../../constants/chartConstants';
3
+
4
+ interface ChartWrapperProps {
5
+ children: ReactNode;
6
+ }
7
+
8
+ const ChartWrapper = forwardRef(({children}: ChartWrapperProps, ref: ForwardedRef<HTMLDivElement>) => (
9
+ <div style={CHART_WRAPPER_STYLE} ref={ref}>
10
+ {children}
11
+ </div>
12
+ ));
13
+ ChartWrapper.displayName = "ChartWrapper";
14
+
15
+ export default ChartWrapper;
@@ -15,16 +15,13 @@ import type { ChoroplethMapProps } from '../../types/mapTypes';
15
15
  import BentoMapContainer from './BentoMapContainer';
16
16
  import MapLegendContinuous from './controls/MapLegendContinuous';
17
17
  import MapLegendDiscrete from './controls/MapLegendDiscrete';
18
+ import { useTransformedChartData } from '../../util/chartUtils';
18
19
 
19
20
  const DEFAULT_CATEGORY = '';
20
21
  const POS_BOTTOM_RIGHT: ControlPosition = 'bottomright';
21
22
 
22
23
  const BentoChoroplethMap = ({
23
24
  height,
24
- data: originalData,
25
- preFilter,
26
- dataMap,
27
- postFilter,
28
25
  center,
29
26
  zoom,
30
27
  tileLayer,
@@ -33,14 +30,9 @@ const BentoChoroplethMap = ({
33
30
  categoryProp,
34
31
  onClick,
35
32
  renderPopupBody,
33
+ ...params
36
34
  }: ChoroplethMapProps) => {
37
- const data = useMemo(() => {
38
- let data = [...originalData];
39
- if (preFilter) data = data.filter(preFilter);
40
- if (dataMap) data = data.map(dataMap);
41
- if (postFilter) data = data.filter(postFilter);
42
- return data;
43
- }, [originalData]);
35
+ const data = useTransformedChartData(params);
44
36
 
45
37
  const dataByFeatureCat = useMemo(() => Object.fromEntries(data.map((d) => [d.x, d.y])), [data]);
46
38
 
@@ -102,7 +94,7 @@ const BentoChoroplethMap = ({
102
94
  </div>
103
95
  );
104
96
  },
105
- } as LeafletEventHandlerFnMap),
97
+ }) as LeafletEventHandlerFnMap,
106
98
  [onClick, categoryProp, renderPopupBody]
107
99
  );
108
100
 
@@ -16,10 +16,19 @@ const BentoPointMap = ({ height, center, zoom, tileLayer, data, onClick, renderP
16
16
  <Marker key={i} position={coordinatesLatLongOrder}>
17
17
  <Popup>
18
18
  <h4 style={{ marginBottom: renderPopupBody ? 6 : 0 }}>
19
- {onClick ? <a href="#" onClick={(e) => {
20
- onClick(point);
21
- e.preventDefault();
22
- }}>{title}</a> : <>{title}</>}
19
+ {onClick ? (
20
+ <a
21
+ href="#"
22
+ onClick={(e) => {
23
+ onClick(point);
24
+ e.preventDefault();
25
+ }}
26
+ >
27
+ {title}
28
+ </a>
29
+ ) : (
30
+ <>{title}</>
31
+ )}
23
32
  </h4>
24
33
  {renderPopupBody ? renderPopupBody(point) : null}
25
34
  </Popup>
@@ -83,6 +83,8 @@ export const CHART_WRAPPER_STYLE: CSS.Properties = {
83
83
  display: 'flex',
84
84
  flexDirection: 'column',
85
85
  alignItems: 'center',
86
+ overflowX: 'auto',
87
+ overflowY: 'hidden',
86
88
  };
87
89
 
88
90
  // bar chart
@@ -104,16 +106,13 @@ export const COUNT_TEXT_STYLE: CSS.Properties = {
104
106
 
105
107
  // ################### CHART CONSTANTS ###################
106
108
  // bar chart
107
- export const ASPECT_RATIO = 1.2;
108
109
  export const MAX_TICK_LABEL_CHARS = 15;
109
110
  export const UNITS_LABEL_OFFSET = -75;
110
111
  export const TICKS_SHOW_ALL_LABELS_BELOW = 11; // Below this # of X-axis ticks, force-show all labels
111
112
  export const TICK_MARGIN = 5; // vertical spacing between tick line and tick label
112
113
 
113
114
  // pie chart
114
- export const CHART_ASPECT_RATIO = 1.4;
115
115
  export const LABEL_THRESHOLD = 0.05;
116
- export const OTHER_THRESHOLD = 0.01;
117
116
 
118
117
  // ################### UTIL CONSTANTS ###################
119
118
  export const RADIAN = Math.PI / 180;
@@ -37,6 +37,7 @@ export type UnitaryMapCallback<T> = (value: T, index: number, array: T[]) => T;
37
37
  // export type BinaryMapCallback<T, U> = (value: T, index: number, array: T[]) => U;
38
38
 
39
39
  export type ChartFilterCallback = FilterCallback<CategoricalChartDataItem>;
40
+ export type ChartDataMapCallback = UnitaryMapCallback<CategoricalChartDataItem>;
40
41
 
41
42
  export type SupportedLng = 'en' | 'fr';
42
43
 
@@ -50,19 +51,24 @@ export type TranslationObject = {
50
51
  [key in SupportedLng]: LngDictionary;
51
52
  };
52
53
 
53
- // ################### COMPONENT PROPS #####################
54
- export interface BaseChartComponentProps {
55
- height: number;
54
+ export interface CategoricalChartDataWithTransforms {
55
+ data: CategoricalChartDataType;
56
56
  preFilter?: ChartFilterCallback;
57
- dataMap?: UnitaryMapCallback<CategoricalChartDataItem>;
57
+ dataMap?: ChartDataMapCallback;
58
58
  postFilter?: ChartFilterCallback;
59
+ removeEmpty?: boolean;
59
60
  }
60
61
 
61
- interface BaseCategoricalChartProps extends BaseChartComponentProps {
62
- data: CategoricalChartDataType;
63
- removeEmpty?: boolean;
62
+ // ################### COMPONENT PROPS #####################
63
+ export interface BaseChartComponentProps {
64
+ height: number;
65
+ // Width is useful to have, to force re-render / force a specific width, but it is optional.
66
+ // Otherwise, it will be set to 100%.
67
+ width?: number | string;
64
68
  }
65
69
 
70
+ export interface BaseCategoricalChartProps extends BaseChartComponentProps, CategoricalChartDataWithTransforms {}
71
+
66
72
  export interface PieChartProps extends BaseCategoricalChartProps {
67
73
  colorTheme?: keyof ChartTheme['pie'];
68
74
  sort?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { ReactElement, ReactNode } from 'react';
2
2
  import type { Feature as GeoJSONFeatureType } from 'geojson';
3
3
 
4
- import { BaseChartComponentProps, CategoricalChartDataType } from './chartTypes';
4
+ import { BaseCategoricalChartProps, BaseChartComponentProps } from './chartTypes';
5
5
  import type { GeoJSONPolygonOnlyFeatureCollection } from './geoJSONTypes';
6
6
 
7
7
  export interface GeoPointDataItem {
@@ -42,8 +42,8 @@ export interface ChoroplethMapColorModeDiscrete {
42
42
  legendItems: MapDiscreteLegendItem[];
43
43
  }
44
44
 
45
- export interface ChoroplethMapProps extends BaseMapProps {
46
- data: CategoricalChartDataType; // heatmaps are 'categorical' + geographical
45
+ // heatmaps are 'categorical' + geographical:
46
+ export interface ChoroplethMapProps extends BaseCategoricalChartProps, BaseMapProps {
47
47
  features: GeoJSONPolygonOnlyFeatureCollection;
48
48
  colorMode: ChoroplethMapColorModeContinuous | ChoroplethMapColorModeDiscrete;
49
49
  categoryProp: string;
@@ -1,4 +1,6 @@
1
+ import { useMemo } from 'react';
1
2
  import { RADIAN } from '../constants/chartConstants';
3
+ import type { CategoricalChartDataWithTransforms } from '../types/chartTypes';
2
4
 
3
5
  export const polarToCartesian = (cx: number, cy: number, radius: number, angle: number) => {
4
6
  return {
@@ -6,3 +8,30 @@ export const polarToCartesian = (cx: number, cy: number, radius: number, angle:
6
8
  y: cy + Math.sin(-RADIAN * angle) * radius,
7
9
  };
8
10
  };
11
+
12
+ export const useTransformedChartData = (
13
+ {
14
+ data: originalData,
15
+ preFilter,
16
+ dataMap,
17
+ postFilter,
18
+ removeEmpty: origRemoveEmpty,
19
+ }: CategoricalChartDataWithTransforms,
20
+ defaultRemoveEmpty = true,
21
+ sortY = false,
22
+ ) =>
23
+ useMemo(() => {
24
+ const removeEmpty = origRemoveEmpty ?? defaultRemoveEmpty;
25
+
26
+ let data = [...originalData];
27
+
28
+ if (preFilter) data = data.filter(preFilter);
29
+ if (dataMap) data = data.map(dataMap);
30
+ if (postFilter) data = data.filter(postFilter);
31
+
32
+ if (removeEmpty) data = data.filter((e) => e.y !== 0);
33
+
34
+ if (sortY) data.sort((a, b) => a.y - b.y);
35
+
36
+ return data;
37
+ }, [originalData, preFilter, dataMap, postFilter, origRemoveEmpty]);
package/webpack.config.js CHANGED
@@ -12,7 +12,14 @@ const config = {
12
12
  },
13
13
  module: {
14
14
  rules: [
15
- { test: /\.[tj](sx|s)?$/, use: { loader: 'ts-loader' }, exclude: /node_modules/ },
15
+ {
16
+ test: /\.[tj](sx|s)?$/,
17
+ loader: 'ts-loader',
18
+ exclude: /node_modules/,
19
+ options: {
20
+ configFile: 'test/tsconfig.json'
21
+ }
22
+ },
16
23
  {
17
24
  test: /\.html$/i,
18
25
  loader: 'html-loader',
@@ -43,6 +50,7 @@ const config = {
43
50
  devtool: 'source-map',
44
51
  devServer: {
45
52
  static: './test/dist',
53
+ historyApiFallback: true,
46
54
  },
47
55
  resolve: {
48
56
  alias: {