@hero-design/rn 8.99.4 → 8.100.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 (121) hide show
  1. package/.turbo/turbo-build.log +8 -3
  2. package/CHANGELOG.md +17 -0
  3. package/es/index.js +5621 -690
  4. package/jest.config.js +1 -1
  5. package/lib/index.js +5545 -613
  6. package/package.json +4 -2
  7. package/src/components/Avatar/AvatarStack/utils.ts +6 -4
  8. package/src/components/Badge/Status.tsx +1 -1
  9. package/src/components/Badge/__tests__/Status.spec.tsx +20 -0
  10. package/src/components/Badge/__tests__/__snapshots__/Status.spec.tsx.snap +2 -0
  11. package/src/components/Chart/ChartSelect/StyledChartSelect.tsx +9 -0
  12. package/src/components/Chart/ChartSelect/__tests__/StyledChartSelect.spec.tsx +15 -0
  13. package/src/components/Chart/ChartSelect/__tests__/__snapshots__/StyledChartSelect.spec.tsx.snap +20 -0
  14. package/src/components/Chart/ChartSelect/__tests__/index.spec.tsx +111 -0
  15. package/src/components/Chart/ChartSelect/index.tsx +137 -0
  16. package/src/components/Chart/ColumnChart/ColumnChartContent.tsx +84 -0
  17. package/src/components/Chart/ColumnChart/Segment.tsx +66 -0
  18. package/src/components/Chart/ColumnChart/StackedSegment.tsx +99 -0
  19. package/src/components/Chart/ColumnChart/StyledColumnChart.tsx +9 -0
  20. package/src/components/Chart/ColumnChart/__tests__/ColumnChartContent.spec.tsx +68 -0
  21. package/src/components/Chart/ColumnChart/__tests__/Segment.spec.tsx +99 -0
  22. package/src/components/Chart/ColumnChart/__tests__/StackedSegment.spec.tsx +115 -0
  23. package/src/components/Chart/ColumnChart/__tests__/__snapshots__/StackedSegment.spec.tsx.snap +120 -0
  24. package/src/components/Chart/ColumnChart/__tests__/__snapshots__/index.spec.tsx.snap +1405 -0
  25. package/src/components/Chart/ColumnChart/__tests__/index.spec.tsx +134 -0
  26. package/src/components/Chart/ColumnChart/index.tsx +216 -0
  27. package/src/components/Chart/Line/Line.tsx +81 -0
  28. package/src/components/Chart/Line/__tests__/Line.spec.tsx +148 -0
  29. package/src/components/Chart/Line/__tests__/__snapshots__/Line.spec.tsx.snap +56 -0
  30. package/src/components/Chart/Line/__tests__/__snapshots__/index.spec.tsx.snap +1461 -0
  31. package/src/components/Chart/Line/__tests__/index.spec.tsx +112 -0
  32. package/src/components/Chart/Line/index.tsx +143 -0
  33. package/src/components/Chart/StyledChart.tsx +16 -0
  34. package/src/components/Chart/index.tsx +13 -0
  35. package/src/components/Chart/shared/AxisLabel.tsx +25 -0
  36. package/src/components/Chart/shared/ChartFrame.tsx +131 -0
  37. package/src/components/Chart/shared/ChartHeader.tsx +19 -0
  38. package/src/components/Chart/shared/EmptyState.tsx +83 -0
  39. package/src/components/Chart/shared/XAxis.tsx +69 -0
  40. package/src/components/Chart/shared/XAxisGrid.tsx +42 -0
  41. package/src/components/Chart/shared/YAxis.tsx +104 -0
  42. package/src/components/Chart/shared/YAxisGrid.tsx +58 -0
  43. package/src/components/Chart/shared/__tests__/ChartFrame.spec.tsx +125 -0
  44. package/src/components/Chart/shared/__tests__/ChartHeader.spec.tsx +22 -0
  45. package/src/components/Chart/shared/__tests__/EmptyState.spec.tsx +29 -0
  46. package/src/components/Chart/shared/__tests__/XAXisGrid.spec.tsx +30 -0
  47. package/src/components/Chart/shared/__tests__/XAxis.spec.tsx +42 -0
  48. package/src/components/Chart/shared/__tests__/YAxis.spec.tsx +72 -0
  49. package/src/components/Chart/shared/__tests__/YAxisGrid.spec.tsx +35 -0
  50. package/src/components/Chart/shared/__tests__/__snapshots__/ChartFrame.spec.tsx.snap +3058 -0
  51. package/src/components/Chart/shared/__tests__/__snapshots__/ChartHeader.spec.tsx.snap +160 -0
  52. package/src/components/Chart/shared/__tests__/__snapshots__/EmptyState.spec.tsx.snap +155 -0
  53. package/src/components/Chart/shared/__tests__/__snapshots__/XAXisGrid.spec.tsx.snap +197 -0
  54. package/src/components/Chart/shared/__tests__/__snapshots__/XAxis.spec.tsx.snap +369 -0
  55. package/src/components/Chart/shared/__tests__/__snapshots__/YAxis.spec.tsx.snap +1013 -0
  56. package/src/components/Chart/shared/__tests__/__snapshots__/YAxisGrid.spec.tsx.snap +228 -0
  57. package/src/components/Chart/shared/__tests__/niceNumbers.spec.tsx +127 -0
  58. package/src/components/Chart/shared/constants.ts +2 -0
  59. package/src/components/Chart/shared/hooks/useColorScale.ts +25 -0
  60. package/src/components/Chart/shared/hooks/useGenerateTicks.ts +27 -0
  61. package/src/components/Chart/shared/hooks/useScaleBandX.ts +17 -0
  62. package/src/components/Chart/shared/hooks/useScaleLinearY.ts +30 -0
  63. package/src/components/Chart/shared/niceNumbers.ts +68 -0
  64. package/src/components/Chart/types.ts +100 -0
  65. package/src/components/Select/MultiSelect/OptionList.tsx +1 -1
  66. package/src/components/Select/MultiSelect/index.tsx +2 -6
  67. package/src/components/Select/MultiSelect/utils.ts +1 -1
  68. package/src/components/Select/SingleSelect/OptionList.tsx +1 -1
  69. package/src/components/Select/SingleSelect/index.tsx +2 -7
  70. package/src/components/Select/__tests__/helpers.spec.tsx +0 -36
  71. package/src/components/Select/helpers.tsx +0 -75
  72. package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/Option.spec.tsx.snap +3 -0
  73. package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/index.spec.tsx.snap +1 -0
  74. package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabs.spec.tsx.snap +3 -0
  75. package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabsHeader.spec.tsx.snap +2 -0
  76. package/src/components/Tabs/__tests__/__snapshots__/TabWithBadge.spec.tsx.snap +1 -0
  77. package/src/components/Tabs/__tests__/__snapshots__/index.spec.tsx.snap +3 -0
  78. package/src/index.ts +2 -0
  79. package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +28 -0
  80. package/src/theme/components/chart.ts +28 -0
  81. package/src/theme/components/columnChart.ts +15 -0
  82. package/src/theme/getTheme.ts +6 -0
  83. package/src/types.ts +4 -0
  84. package/src/utils/__tests__/helpers.spec.ts +36 -1
  85. package/src/utils/helpers.ts +76 -1
  86. package/stats/8.100.0/rn-stats.html +4842 -0
  87. package/stats/8.99.4/rn-stats.html +1 -1
  88. package/types/components/Badge/Status.d.ts +0 -1
  89. package/types/components/Chart/ChartSelect/StyledChartSelect.d.ts +8 -0
  90. package/types/components/Chart/ChartSelect/index.d.ts +63 -0
  91. package/types/components/Chart/ColumnChart/ColumnChartContent.d.ts +25 -0
  92. package/types/components/Chart/ColumnChart/Segment.d.ts +14 -0
  93. package/types/components/Chart/ColumnChart/StackedSegment.d.ts +28 -0
  94. package/types/components/Chart/ColumnChart/StyledColumnChart.d.ts +8 -0
  95. package/types/components/Chart/ColumnChart/index.d.ts +80 -0
  96. package/types/components/Chart/Line/Line.d.ts +21 -0
  97. package/types/components/Chart/Line/index.d.ts +35 -0
  98. package/types/components/Chart/StyledChart.d.ts +9 -0
  99. package/types/components/Chart/index.d.ts +9 -0
  100. package/types/components/Chart/shared/AxisLabel.d.ts +7 -0
  101. package/types/components/Chart/shared/ChartFrame.d.ts +30 -0
  102. package/types/components/Chart/shared/ChartHeader.d.ts +8 -0
  103. package/types/components/Chart/shared/EmptyState.d.ts +8 -0
  104. package/types/components/Chart/shared/XAxis.d.ts +8 -0
  105. package/types/components/Chart/shared/XAxisGrid.d.ts +8 -0
  106. package/types/components/Chart/shared/YAxis.d.ts +10 -0
  107. package/types/components/Chart/shared/YAxisGrid.d.ts +10 -0
  108. package/types/components/Chart/shared/constants.d.ts +2 -0
  109. package/types/components/Chart/shared/hooks/useColorScale.d.ts +7 -0
  110. package/types/components/Chart/shared/hooks/useGenerateTicks.d.ts +6 -0
  111. package/types/components/Chart/shared/hooks/useScaleBandX.d.ts +8 -0
  112. package/types/components/Chart/shared/hooks/useScaleLinearY.d.ts +9 -0
  113. package/types/components/Chart/shared/niceNumbers.d.ts +12 -0
  114. package/types/components/Chart/types.d.ts +84 -0
  115. package/types/components/Select/helpers.d.ts +0 -5
  116. package/types/index.d.ts +2 -1
  117. package/types/theme/components/chart.d.ts +22 -0
  118. package/types/theme/components/columnChart.d.ts +10 -0
  119. package/types/theme/getTheme.d.ts +4 -0
  120. package/types/types.d.ts +3 -1
  121. package/types/utils/helpers.d.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/rn",
3
- "version": "8.99.4",
3
+ "version": "8.100.0",
4
4
  "license": "MIT",
5
5
  "main": "lib/index.js",
6
6
  "module": "es/index.js",
@@ -22,7 +22,8 @@
22
22
  "@emotion/native": "^11.9.3",
23
23
  "@emotion/primitives-core": "11.0.0",
24
24
  "@emotion/react": "^11.9.3",
25
- "@hero-design/colors": "8.46.0",
25
+ "@hero-design/colors": "8.46.1",
26
+ "d3": "^7.8.5",
26
27
  "date-fns": "^2.30.0",
27
28
  "hero-editor": "^1.15.5",
28
29
  "nanoid": "^5.0.9"
@@ -76,6 +77,7 @@
76
77
  "@testing-library/jest-native": "^5.4.2",
77
78
  "@testing-library/react-hooks": "^8.0.1",
78
79
  "@testing-library/react-native": "^9.1.0",
80
+ "@types/d3": "^7.4.3",
79
81
  "@types/events": "^3.0.3",
80
82
  "@types/jest": "^29.5.3",
81
83
  "@types/react": "^18.2.0",
@@ -1,6 +1,11 @@
1
1
  import { useMemo } from 'react';
2
2
  import { mobileVisualisationPalette } from '@hero-design/colors';
3
3
 
4
+ // Only use maasstrichtBlue colors for avatar stack
5
+ const DEFAULT_COLORS = Object.entries(mobileVisualisationPalette)
6
+ .filter(([key]) => key.startsWith('maasstrichtBlue'))
7
+ .map(([, value]) => value);
8
+
4
9
  const shuffleArray = <T>(array: Array<T>): Array<T> =>
5
10
  array
6
11
  .map((value) => ({ value, sort: Math.random() }))
@@ -11,9 +16,6 @@ const shuffleArray = <T>(array: Array<T>): Array<T> =>
11
16
  * Hook that returns a memoized and shuffled array of visualisation colors for Avatar.
12
17
  */
13
18
  export const useAvatarColors = () => {
14
- const shuffledColors = useMemo(
15
- () => shuffleArray(Object.values(mobileVisualisationPalette)),
16
- []
17
- );
19
+ const shuffledColors = useMemo(() => shuffleArray(DEFAULT_COLORS), []);
18
20
  return shuffledColors;
19
21
  };
@@ -13,7 +13,6 @@ export interface StatusProps extends ViewProps {
13
13
  * Whether the Status Badge is visible.
14
14
  */
15
15
  visible?: boolean;
16
- /**
17
16
  /**
18
17
  * Visual intent color to apply to Status Badge.
19
18
  */
@@ -71,6 +70,7 @@ const Status = ({
71
70
  ],
72
71
  }}
73
72
  themeIntent={intent}
73
+ testID="status-dot"
74
74
  />
75
75
  </View>
76
76
  );
@@ -24,4 +24,24 @@ describe('Status Badge', () => {
24
24
  expect(toJSON()).toMatchSnapshot();
25
25
  expect(getByText('Activity')).toBeDefined();
26
26
  });
27
+
28
+ it.each`
29
+ visible | expectedOpacity
30
+ ${true} | ${1}
31
+ ${false} | ${0}
32
+ `(
33
+ 'status-dot opacity when visible is $visible',
34
+ ({ visible, expectedOpacity }) => {
35
+ const { getByTestId } = renderWithTheme(
36
+ <Badge.Status visible={visible}>
37
+ <Typography.Body variant="small">Dot Visibility</Typography.Body>
38
+ </Badge.Status>
39
+ );
40
+ const dot = getByTestId('status-dot');
41
+ const style = Array.isArray(dot.props.style)
42
+ ? Object.assign({}, ...dot.props.style)
43
+ : dot.props.style;
44
+ expect(style.opacity).toBe(expectedOpacity);
45
+ }
46
+ );
27
47
  });
@@ -43,6 +43,7 @@ exports[`Status Badge renders correctly 1`] = `
43
43
  "width": 8,
44
44
  }
45
45
  }
46
+ testID="status-dot"
46
47
  themeIntent="danger"
47
48
  />
48
49
  </View>
@@ -117,6 +118,7 @@ exports[`Status Badge renders correctly with intent 1`] = `
117
118
  "width": 8,
118
119
  }
119
120
  }
121
+ testID="status-dot"
120
122
  themeIntent="success"
121
123
  />
122
124
  </View>
@@ -0,0 +1,9 @@
1
+ import styled from '@emotion/native';
2
+ import { View } from 'react-native';
3
+
4
+ const StyledHeaderContainer = styled(View)({
5
+ flexDirection: 'row',
6
+ alignItems: 'center',
7
+ });
8
+
9
+ export { StyledHeaderContainer };
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react-native';
3
+ import { Text } from 'react-native';
4
+ import { StyledHeaderContainer } from '../StyledChartSelect';
5
+
6
+ describe('StyledHeaderContainer', () => {
7
+ it('applies correct styles', () => {
8
+ const { toJSON } = render(
9
+ <StyledHeaderContainer testID="styled-header">
10
+ <Text>Child</Text>
11
+ </StyledHeaderContainer>
12
+ );
13
+ expect(toJSON()).toMatchSnapshot();
14
+ });
15
+ });
@@ -0,0 +1,20 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`StyledHeaderContainer applies correct styles 1`] = `
4
+ <View
5
+ style={
6
+ [
7
+ {
8
+ "alignItems": "center",
9
+ "flexDirection": "row",
10
+ },
11
+ undefined,
12
+ ]
13
+ }
14
+ testID="styled-header"
15
+ >
16
+ <Text>
17
+ Child
18
+ </Text>
19
+ </View>
20
+ `;
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { fireEvent, waitFor, within } from '@testing-library/react-native';
3
+ import renderWithTheme from '../../../../testHelpers/renderWithTheme';
4
+ import ChartSelect from '../index';
5
+
6
+ const options = [
7
+ { text: 'Option 1', value: '1' },
8
+ { text: 'Option 2', value: '2' },
9
+ { text: 'Option 3', value: '3', disabled: true },
10
+ ];
11
+
12
+ describe('ChartSelect', () => {
13
+ it('renders with a selected value', () => {
14
+ const { getByText } = renderWithTheme(
15
+ <ChartSelect value="1" options={options} onConfirm={jest.fn()} />
16
+ );
17
+ expect(getByText('Option 1')).toBeVisible();
18
+ });
19
+
20
+ it('renders with no value selected', () => {
21
+ const { getByText } = renderWithTheme(
22
+ <ChartSelect value={null} options={options} onConfirm={jest.fn()} />
23
+ );
24
+ // Should render empty string or placeholder
25
+ expect(getByText('')).toBeTruthy();
26
+ });
27
+
28
+ it('opens bottom sheet and selects an option', async () => {
29
+ const onConfirm = jest.fn();
30
+ const { getByText } = renderWithTheme(
31
+ <ChartSelect
32
+ value="1"
33
+ options={options}
34
+ onConfirm={onConfirm}
35
+ bottomSheetConfig={{ header: 'Select Option' }}
36
+ />
37
+ );
38
+ fireEvent.press(getByText('Option 1'));
39
+ await waitFor(() => expect(getByText('Option 2')).toBeVisible());
40
+ expect(getByText('Option 2')).toBeTruthy();
41
+ // verify botomsheet header
42
+ expect(getByText('Select Option')).toBeVisible();
43
+
44
+ fireEvent.press(getByText('Option 2'));
45
+ expect(onConfirm).toHaveBeenCalledWith('2');
46
+ await waitFor(() => expect(getByText('Select Option')).toBeVisible());
47
+ });
48
+
49
+ it('calls onDismiss when bottom sheet is closed', () => {
50
+ const onDismiss = jest.fn();
51
+ const { getByText, getByTestId } = renderWithTheme(
52
+ <ChartSelect
53
+ value="1"
54
+ options={options}
55
+ onConfirm={jest.fn()}
56
+ onDismiss={onDismiss}
57
+ />
58
+ );
59
+ fireEvent.press(getByText('Option 1'));
60
+ // Simulate closing the bottom sheet
61
+ fireEvent(getByTestId('bottomSheet'), 'onRequestClose');
62
+ expect(onDismiss).toHaveBeenCalled();
63
+ });
64
+
65
+ it('does not open when disabled', () => {
66
+ const { getByText, queryByText } = renderWithTheme(
67
+ <ChartSelect value="1" options={options} onConfirm={jest.fn()} disabled />
68
+ );
69
+ fireEvent.press(getByText('Option 1'));
70
+ expect(queryByText('Option 2')).toBeNull();
71
+ });
72
+
73
+ it('renders all options', () => {
74
+ const { getByText, getByTestId } = renderWithTheme(
75
+ <ChartSelect value="1" options={options} onConfirm={jest.fn()} />
76
+ );
77
+ fireEvent.press(getByText('Option 1'));
78
+
79
+ const bottomSheet = getByTestId('bottomSheet');
80
+ const bottomSheetQueries = within(bottomSheet);
81
+
82
+ expect(bottomSheetQueries.getByText('Option 1')).toBeVisible();
83
+ expect(bottomSheetQueries.getByText('Option 2')).toBeVisible();
84
+ expect(bottomSheetQueries.getByText('Option 3')).toBeVisible();
85
+ });
86
+
87
+ it('renders custom header from bottomSheetConfig', () => {
88
+ const { getByText } = renderWithTheme(
89
+ <ChartSelect
90
+ value="1"
91
+ options={options}
92
+ onConfirm={jest.fn()}
93
+ bottomSheetConfig={{ header: 'Custom Header' }}
94
+ />
95
+ );
96
+ fireEvent.press(getByText('Option 1'));
97
+ expect(getByText('Custom Header')).toBeTruthy();
98
+ });
99
+
100
+ it('applies testID to the correct element', () => {
101
+ const { getByTestId } = renderWithTheme(
102
+ <ChartSelect
103
+ value="1"
104
+ options={options}
105
+ onConfirm={jest.fn()}
106
+ testID="chart-select"
107
+ />
108
+ );
109
+ expect(getByTestId('chart-select')).toBeTruthy();
110
+ });
111
+ });
@@ -0,0 +1,137 @@
1
+ import React, { useState } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import { FlatList, TouchableOpacity, View } from 'react-native';
4
+ import BottomSheet, { BottomSheetProps } from '../../BottomSheet';
5
+ import Typography from '../../Typography';
6
+ import Icon from '../../Icon';
7
+ import List from '../../List';
8
+ import { deepCompareValue, useKeyboard } from '../../../utils/helpers';
9
+ import { StyledHeaderContainer } from './StyledChartSelect';
10
+
11
+ /**
12
+ * Represents a selectable option for ChartSelect.
13
+ * @template V The type of the option value.
14
+ * @property value The value of the option. Used for selection and comparison.
15
+ * @property text The display text for the option.
16
+ * @property key (Optional) A unique key for the option. If provided, used as the FlatList key for better performance and stability.
17
+ */
18
+ type OptionType<V> = {
19
+ value: V;
20
+ text: string;
21
+ key?: string;
22
+ };
23
+
24
+ export interface SingleSelectProps<V> {
25
+ /**
26
+ * The currently selected value. Should match one of the values in the options array.
27
+ */
28
+ value: V | null;
29
+ /**
30
+ * Callback fired when an option is selected.
31
+ * @param value The value of the selected option.
32
+ */
33
+ onConfirm: (value: V) => void;
34
+ /**
35
+ * Array of supported orientations for the Select modal (iOS only).
36
+ * Defaults to ['portrait'].
37
+ */
38
+ supportedOrientations?: ('portrait' | 'landscape')[];
39
+ /**
40
+ * The list of selectable options. Each option should have a value and display text.
41
+ */
42
+ options: OptionType<V>[];
43
+ /**
44
+ * Callback fired when the selection modal is dismissed without selecting an option.
45
+ */
46
+ onDismiss?: () => void;
47
+ /**
48
+ * Configuration for the bottom sheet modal.
49
+ * - variant: The visual variant of the bottom sheet.
50
+ * - header: Optional header text to display at the top of the bottom sheet.
51
+ */
52
+ bottomSheetConfig?: {
53
+ variant?: BottomSheetProps['variant'];
54
+ header?: BottomSheetProps['header'];
55
+ };
56
+ /**
57
+ * If true, the select is disabled and cannot be interacted with.
58
+ * @default false
59
+ */
60
+ disabled?: boolean;
61
+ /**
62
+ * Additional style to apply to the select container.
63
+ */
64
+ style?: StyleProp<ViewStyle>;
65
+ /**
66
+ * Optional test ID for testing purposes. Applied to the main touchable area.
67
+ */
68
+ testID?: string;
69
+ }
70
+
71
+ const SingleSelect = <V,>({
72
+ onConfirm,
73
+ onDismiss,
74
+ options = [],
75
+ disabled = false,
76
+ style,
77
+ testID,
78
+ value,
79
+ supportedOrientations = ['portrait'],
80
+ bottomSheetConfig = {},
81
+ }: SingleSelectProps<V>) => {
82
+ const { isKeyboardVisible, keyboardHeight } = useKeyboard();
83
+ const [open, setOpen] = useState(false);
84
+
85
+ const displayedValue =
86
+ options.find((opt) => deepCompareValue(opt.value, value))?.text || '';
87
+
88
+ const { variant: bottomSheetVariant, header: bottomSheetHeader = '' } =
89
+ bottomSheetConfig;
90
+
91
+ return (
92
+ <>
93
+ <View pointerEvents={disabled ? 'none' : 'auto'} style={style}>
94
+ <TouchableOpacity onPress={() => setOpen(true)}>
95
+ <StyledHeaderContainer pointerEvents="none" testID={testID}>
96
+ <Typography.Body variant="small-bold">
97
+ {displayedValue}
98
+ </Typography.Body>
99
+ <Icon icon="arrow-down" intent="primary" size="small" />
100
+ </StyledHeaderContainer>
101
+ </TouchableOpacity>
102
+ </View>
103
+
104
+ <BottomSheet
105
+ variant={bottomSheetVariant || 'fixed'}
106
+ open={open}
107
+ onRequestClose={() => {
108
+ onDismiss?.();
109
+ setOpen(false);
110
+ }}
111
+ header={bottomSheetHeader}
112
+ style={{
113
+ paddingBottom: isKeyboardVisible ? keyboardHeight : 0,
114
+ }}
115
+ supportedOrientations={supportedOrientations}
116
+ testID="bottomSheet"
117
+ >
118
+ <FlatList
119
+ data={options}
120
+ keyExtractor={(item) => item.key ?? String(item.value)}
121
+ renderItem={({ item }) => (
122
+ <List.BasicItem
123
+ selected={deepCompareValue(item.value, value)}
124
+ title={item.text}
125
+ onPress={() => {
126
+ setOpen(false);
127
+ onConfirm?.(item.value);
128
+ }}
129
+ />
130
+ )}
131
+ />
132
+ </BottomSheet>
133
+ </>
134
+ );
135
+ };
136
+
137
+ export default SingleSelect;
@@ -0,0 +1,84 @@
1
+ // ColumnChartContent.tsx
2
+ // This component renders the grouped/stacked columns for a column chart, using StackedSegment for each x-axis category.
3
+ // It handles the layout and mapping of data series to visual columns.
4
+
5
+ import React, { memo, useMemo } from 'react';
6
+ import { G } from 'react-native-svg';
7
+ import { DataValue, Series, XAxisConfig, YAxisConfig } from '../types';
8
+ import useScaleBandX from '../shared/hooks/useScaleBandX';
9
+ import StackedSegment from './StackedSegment';
10
+ import { deepCompareValue } from '../../../utils/helpers';
11
+
12
+ interface ColumnChartContentProps {
13
+ coordinates: { yStart: number; yEnd: number; xStart: number; xEnd: number };
14
+ data: Array<Series<Array<DataValue>>>;
15
+ yAxisConfig: Omit<YAxisConfig, 'minValue'>;
16
+ xAxisConfig: XAxisConfig;
17
+ /**
18
+ * Called when a bar (column segment) is pressed.
19
+ */
20
+ onBarPress?: (info: {
21
+ value: number | undefined;
22
+ xLabel: string;
23
+ seriesLabel: string;
24
+ seriesIndex: number;
25
+ xIndex: number;
26
+ }) => void;
27
+ }
28
+
29
+ /**
30
+ * Renders the grouped/stacked columns for a column chart.
31
+ * For each x-axis category, renders a StackedSegment with the values from all series.
32
+ * Handles layout and mapping of data to visual columns.
33
+ */
34
+ const ColumnChartContent = ({
35
+ coordinates,
36
+ data,
37
+ yAxisConfig,
38
+ xAxisConfig,
39
+ onBarPress,
40
+ }: ColumnChartContentProps) => {
41
+ const { yStart, yEnd, xStart, xEnd } = coordinates;
42
+
43
+ const xLabels = xAxisConfig.labels ?? [];
44
+
45
+ // Render columns (fixed width, center of group/column aligns to grid)
46
+ const scaleX = useScaleBandX({
47
+ labels: xAxisConfig.labels ?? [],
48
+ xStart,
49
+ xEnd,
50
+ });
51
+ const columns = useMemo(() => {
52
+ if (data.length === 0) {
53
+ return null;
54
+ }
55
+ return xLabels.flatMap((_, xIdx) => {
56
+ const stackedData = data.map((series) => {
57
+ return series.data[xIdx] ?? undefined;
58
+ });
59
+
60
+ const x = scaleX(xLabels[xIdx]);
61
+ if (!x) {
62
+ return null; // Handle case where x is undefined
63
+ }
64
+ const seriesLabels = data.map((series) => series.label);
65
+ const xLabel = xLabels[xIdx];
66
+ return (
67
+ <StackedSegment
68
+ key={`${xLabel}-series`}
69
+ stackedData={stackedData}
70
+ xLabel={xLabel}
71
+ yAxisConfig={yAxisConfig}
72
+ coordinates={coordinates}
73
+ xCenter={x + scaleX.bandwidth() / 2}
74
+ seriesLabels={seriesLabels}
75
+ xIndex={xIdx}
76
+ onBarPress={onBarPress}
77
+ />
78
+ );
79
+ });
80
+ }, [xLabels, data, xStart, yStart, xEnd, yEnd, onBarPress, yAxisConfig]);
81
+
82
+ return <G testID="column-chart-content">{columns}</G>;
83
+ };
84
+ export default memo(ColumnChartContent, deepCompareValue);
@@ -0,0 +1,66 @@
1
+ // Segment.tsx
2
+ // This component renders a single column segment (bar) for a column chart, with support for accessibility and testID.
3
+
4
+ import React from 'react';
5
+ import { Rect } from 'react-native-svg';
6
+ import { useTheme } from '../../../theme';
7
+
8
+ interface SegmentProps {
9
+ xCenter: number;
10
+ y: number;
11
+ height: number;
12
+ color?: string;
13
+ value?: number;
14
+ xLabel: string;
15
+ seriesLabel: string;
16
+ testID: string;
17
+ onPress?: () => void;
18
+ }
19
+
20
+ /**
21
+ * Renders a single column segment (bar) for a column chart.
22
+ * Applies theming, segment gap, and accessibility label.
23
+ * Used by StackedSegment and other chart content components.
24
+ */
25
+ const Segment = ({
26
+ xCenter,
27
+ y,
28
+ height,
29
+ color,
30
+ value,
31
+ xLabel,
32
+ seriesLabel,
33
+ testID,
34
+ onPress,
35
+ }: SegmentProps) => {
36
+ const theme = useTheme();
37
+ const { segmentGap } = theme.__hd__.columnChart.space;
38
+ const width = theme.__hd__.columnChart.sizes.columnWidth;
39
+
40
+ // Apply segment gap logic
41
+ const minBarHeight = 10;
42
+ let adjustedHeight = height - segmentGap;
43
+ if (height > 0 && adjustedHeight < minBarHeight) {
44
+ adjustedHeight = minBarHeight;
45
+ } else if (height <= 0) {
46
+ adjustedHeight = 0;
47
+ }
48
+ const adjustedY = y + segmentGap / 2;
49
+ const x = xCenter - width / 2;
50
+
51
+ return (
52
+ <Rect
53
+ x={x}
54
+ y={adjustedY}
55
+ width={width}
56
+ height={adjustedHeight}
57
+ rx={width / 2}
58
+ fill={color}
59
+ accessibilityLabel={`Column segment: value ${value}, x-label ${xLabel}, series ${seriesLabel}`}
60
+ testID={testID}
61
+ onPress={onPress}
62
+ />
63
+ );
64
+ };
65
+
66
+ export default React.memo(Segment);
@@ -0,0 +1,99 @@
1
+ // StackedSegment.tsx
2
+ // This component renders a stack of column segments for a single x-axis category in a grouped/stacked column chart.
3
+ // Each segment represents a value from a different series, stacked vertically. Uses the Segment component for rendering each bar.
4
+
5
+ import React, { memo } from 'react';
6
+ import Segment from './Segment';
7
+ import { DataValue, YAxisConfig } from '../types';
8
+ import useScaleLinearY from '../shared/hooks/useScaleLinearY';
9
+ import useColorScale from '../shared/hooks/useColorScale';
10
+ import { deepCompareValue } from '../../../utils/helpers';
11
+
12
+ interface StackedSegmentProps {
13
+ stackedData: Array<DataValue>;
14
+ seriesLabels: Array<string>;
15
+ xLabel: string;
16
+ yAxisConfig: YAxisConfig;
17
+ coordinates: { yStart: number; yEnd: number; xStart: number; xEnd: number };
18
+ xCenter: number;
19
+ xIndex: number;
20
+ /**
21
+ * Called when a bar (column segment) is pressed.
22
+ */
23
+ onBarPress?: (info: {
24
+ value: number | undefined;
25
+ xLabel: string;
26
+ seriesLabel: string;
27
+ seriesIndex: number;
28
+ xIndex: number;
29
+ }) => void;
30
+ }
31
+
32
+ /**
33
+ * Renders a stack of column segments for a single x-axis category.
34
+ * Each segment corresponds to a value in stackedData and is colored by series.
35
+ * Skips undefined values. Used for grouped/stacked column charts.
36
+ */
37
+ const StackedSegment: React.FC<StackedSegmentProps> = ({
38
+ stackedData,
39
+ seriesLabels,
40
+ xLabel,
41
+ yAxisConfig,
42
+ coordinates,
43
+ xCenter,
44
+ xIndex,
45
+ onBarPress,
46
+ }) => {
47
+ const { yStart, yEnd } = coordinates;
48
+ const stackedMaxY = yAxisConfig.maxValue ?? 0;
49
+ const scaleY = useScaleLinearY({
50
+ maxValue: stackedMaxY,
51
+ minValue: 0,
52
+ yStart,
53
+ yEnd,
54
+ });
55
+
56
+ let yStack = 0; // running sum for stacking
57
+ const colorScale = useColorScale(seriesLabels);
58
+ return stackedData.map((value, index) => {
59
+ // If value is undefined, skip this segment
60
+ if (value === undefined) {
61
+ return null;
62
+ }
63
+ const prevYStack = yStack;
64
+ yStack += value ?? 0;
65
+
66
+ // y0 is the bottom of the stack, y1 is the top
67
+ const y0 = scaleY(prevYStack);
68
+ const y1 = scaleY(yStack);
69
+ const colHeight = y0 - y1; // since y increases downward in SVG
70
+ const seriesLabel = seriesLabels[index];
71
+ return (
72
+ <Segment
73
+ key={`${xLabel}-series-${seriesLabel}`}
74
+ xCenter={xCenter}
75
+ y={y1}
76
+ height={colHeight}
77
+ color={colorScale(seriesLabel)}
78
+ value={value}
79
+ xLabel={xLabel}
80
+ seriesLabel={seriesLabel}
81
+ testID={`column-segment-${xLabel}-${seriesLabel}`}
82
+ onPress={
83
+ onBarPress
84
+ ? () =>
85
+ onBarPress({
86
+ value,
87
+ xLabel,
88
+ seriesLabel,
89
+ seriesIndex: index,
90
+ xIndex,
91
+ })
92
+ : undefined
93
+ }
94
+ />
95
+ );
96
+ });
97
+ };
98
+
99
+ export default memo(StackedSegment, deepCompareValue);
@@ -0,0 +1,9 @@
1
+ import styled from '@emotion/native';
2
+ import { View } from 'react-native';
3
+
4
+ const StyledColumnChartWrapper = styled(View)`
5
+ width: 100%;
6
+ height: 100%;
7
+ `;
8
+
9
+ export { StyledColumnChartWrapper };