@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
@@ -0,0 +1,112 @@
1
+ import React from 'react';
2
+ import { fireEvent } from '@testing-library/react-native';
3
+ import Button from '../../../Button/Button';
4
+ import renderWithTheme from '../../../../testHelpers/renderWithTheme';
5
+ import LineChart from '..';
6
+
7
+ const onActionPress = jest.fn();
8
+
9
+ const lineChartProps = {
10
+ data: [
11
+ { label: 'Series 1', data: [1, 2, 3] },
12
+ { label: 'Series 2', data: [4, 5, 6] },
13
+ ],
14
+ xAxisConfig: {
15
+ labels: ['May 2025', 'Jun 2025', 'Jul 2025'],
16
+ },
17
+ yAxisConfig: {
18
+ labels: ['$1', '$2', '$3'],
19
+ },
20
+ headerConfig: {
21
+ title: 'Line Chart',
22
+ actionsExtra: (
23
+ <Button
24
+ variant="inline-text-compact"
25
+ text="Chart extra"
26
+ onPress={onActionPress}
27
+ />
28
+ ),
29
+ },
30
+ };
31
+
32
+ describe('LineChart', () => {
33
+ it('should render', () => {
34
+ const { getByText, toJSON } = renderWithTheme(
35
+ <LineChart {...lineChartProps} />
36
+ );
37
+
38
+ expect(toJSON()).toMatchSnapshot();
39
+
40
+ // X axis labels
41
+ expect(getByText('May 2025')).toBeVisible();
42
+ expect(getByText('Jun 2025')).toBeVisible();
43
+ expect(getByText('Jul 2025')).toBeVisible();
44
+
45
+ // Y axis labels
46
+ expect(getByText('$1')).toBeVisible();
47
+ expect(getByText('$2')).toBeVisible();
48
+ expect(getByText('$3')).toBeVisible();
49
+
50
+ // Header actions
51
+ expect(getByText('Chart extra')).toBeVisible();
52
+ fireEvent.press(getByText('Chart extra'));
53
+ expect(onActionPress).toHaveBeenCalled();
54
+ });
55
+
56
+ it('renders empty chart', () => {
57
+ const { getByText, queryByText } = renderWithTheme(
58
+ <LineChart data={[]} emptyText="No data" />
59
+ );
60
+
61
+ expect(getByText('No data')).toBeVisible();
62
+ expect(queryByText('Series 1')).toBeNull();
63
+ expect(queryByText('Series 2')).toBeNull();
64
+ });
65
+
66
+ it('renders chart with custom header title', () => {
67
+ const customTitleProps = {
68
+ ...lineChartProps,
69
+ headerConfig: {
70
+ ...lineChartProps.headerConfig,
71
+ title: 'Custom Chart Title',
72
+ },
73
+ };
74
+ const { getByText } = renderWithTheme(<LineChart {...customTitleProps} />);
75
+ expect(getByText('Custom Chart Title')).toBeVisible();
76
+ });
77
+
78
+ it('renders chart with different data lengths', () => {
79
+ const differentLengthProps = {
80
+ ...lineChartProps,
81
+ data: [
82
+ { label: 'Series 1', data: [1, 2, 3, 4] },
83
+ { label: 'Series 2', data: [4, 5] },
84
+ ],
85
+ };
86
+ const { queryByText } = renderWithTheme(
87
+ <LineChart {...differentLengthProps} />
88
+ );
89
+
90
+ // Verify that the chart still renders even with mismatched data lengths
91
+ expect(queryByText('May 2025')).toBeVisible();
92
+ expect(queryByText('Jun 2025')).toBeVisible();
93
+ });
94
+
95
+ it('renders chart with negative values', () => {
96
+ const negativeValuesProps = {
97
+ ...lineChartProps,
98
+ data: [
99
+ { label: 'Series 1', data: [-1, -2, -3] },
100
+ { label: 'Series 2', data: [-4, -5, -6] },
101
+ ],
102
+ };
103
+ const { getByText } = renderWithTheme(
104
+ <LineChart {...negativeValuesProps} />
105
+ );
106
+
107
+ // Verify that the chart renders with negative values
108
+ expect(getByText('May 2025')).toBeVisible();
109
+ expect(getByText('Jun 2025')).toBeVisible();
110
+ expect(getByText('Jul 2025')).toBeVisible();
111
+ });
112
+ });
@@ -0,0 +1,143 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { LayoutChangeEvent, ViewStyle } from 'react-native';
3
+ import ChartFrame from '../shared/ChartFrame';
4
+ import useColorScale from '../shared/hooks/useColorScale';
5
+ import {
6
+ createNiceScale,
7
+ maxValueFromDataSet,
8
+ minValueFromDataSet,
9
+ } from '../shared/niceNumbers';
10
+ import { StyledLineChartWrapper } from '../StyledChart';
11
+ import {
12
+ AxisCoordinates,
13
+ DataValue,
14
+ HeaderConfig,
15
+ Series,
16
+ XAxisConfig,
17
+ YAxisConfig,
18
+ } from '../types';
19
+ import Line from './Line';
20
+
21
+ export interface LineChartProps {
22
+ /**
23
+ * The data to be displayed in the chart. Each item represents a series.
24
+ */
25
+ data: Array<Series<Array<DataValue>>>;
26
+ /**
27
+ * Optional configuration for the Y axis.
28
+ */
29
+ yAxisConfig?: YAxisConfig;
30
+ /**
31
+ * Optional configuration for the X axis.
32
+ */
33
+ xAxisConfig?: XAxisConfig;
34
+ /**
35
+ * Additional style for the root View.
36
+ */
37
+ style?: ViewStyle;
38
+ /**
39
+ * Testing id of the component.
40
+ */
41
+ testID?: string;
42
+ /**
43
+ * Header configuration for the chart.
44
+ */
45
+ headerConfig?: HeaderConfig;
46
+ /**
47
+ * Text to display when the chart is empty.
48
+ */
49
+ emptyText?: string;
50
+ }
51
+
52
+ const LineChart = ({
53
+ data,
54
+ yAxisConfig,
55
+ xAxisConfig,
56
+ style,
57
+ testID,
58
+ headerConfig,
59
+ emptyText,
60
+ }: LineChartProps) => {
61
+ const [chartSize, setChartSize] = useState({ width: 0, height: 0 });
62
+
63
+ const colorScale = useColorScale(data.map((series) => series.label));
64
+
65
+ const niceValues = useMemo(() => {
66
+ const maxDataValue = maxValueFromDataSet(data);
67
+ const minDataValue = minValueFromDataSet(data);
68
+ return createNiceScale(minDataValue, maxDataValue);
69
+ }, [data]);
70
+
71
+ const yAxisStep = yAxisConfig?.step ?? niceValues.tickSpacing;
72
+ const yAxisInterval = yAxisConfig?.tick?.interval ?? yAxisStep;
73
+
74
+ const calculatedYAxisConfig = useMemo(() => {
75
+ return {
76
+ ...yAxisConfig,
77
+ maxValue: yAxisConfig?.maxValue ?? niceValues.niceMax,
78
+ minValue: niceValues.niceMin,
79
+ step: yAxisConfig?.step ?? niceValues.tickSpacing,
80
+ tick: {
81
+ interval: yAxisInterval,
82
+ },
83
+ };
84
+ }, [yAxisConfig, niceValues, yAxisInterval]);
85
+
86
+ const calculatedXAxisConfig = useMemo(() => {
87
+ return {
88
+ ...xAxisConfig,
89
+ labels: xAxisConfig?.labels || [],
90
+ };
91
+ }, [xAxisConfig]);
92
+
93
+ const onChartLayout = useCallback(
94
+ (event: LayoutChangeEvent) => {
95
+ const { width, height } = event.nativeEvent.layout;
96
+
97
+ if (width !== chartSize.width || height !== chartSize.height) {
98
+ setChartSize({ width, height });
99
+ }
100
+ },
101
+ [setChartSize, chartSize]
102
+ );
103
+
104
+ const renderContent = useCallback(
105
+ ({ coordinates }: { coordinates: AxisCoordinates }) =>
106
+ data.map((series) => (
107
+ <Line
108
+ color={colorScale(series.label)}
109
+ key={series.label}
110
+ data={series.data}
111
+ coordinates={coordinates}
112
+ maxValue={calculatedYAxisConfig.maxValue || 0}
113
+ labels={Array.from(
114
+ { length: calculatedXAxisConfig.labels?.length || 0 },
115
+ (_, i) => i.toString()
116
+ )}
117
+ minValue={calculatedYAxisConfig.minValue || 0}
118
+ />
119
+ )),
120
+ [data, calculatedYAxisConfig, calculatedXAxisConfig, colorScale]
121
+ );
122
+
123
+ return (
124
+ <StyledLineChartWrapper
125
+ onLayout={onChartLayout}
126
+ style={style}
127
+ testID={testID}
128
+ >
129
+ <ChartFrame
130
+ xAxisConfig={calculatedXAxisConfig}
131
+ yAxisConfig={calculatedYAxisConfig}
132
+ width={chartSize.width}
133
+ height={chartSize.height}
134
+ headerConfig={headerConfig}
135
+ renderContent={renderContent}
136
+ isEmpty={data.length === 0}
137
+ emptyText={emptyText}
138
+ />
139
+ </StyledLineChartWrapper>
140
+ );
141
+ };
142
+
143
+ export default LineChart;
@@ -0,0 +1,16 @@
1
+ import styled from '@emotion/native';
2
+ import Box from '../Box';
3
+
4
+ const StyledChartHeader = styled(Box)(({ theme }) => ({
5
+ marginBottom: theme.__hd__.chart.space.headerMarginBottom,
6
+ flexDirection: 'row',
7
+ justifyContent: 'space-between',
8
+ alignItems: 'center',
9
+ }));
10
+
11
+ const StyledLineChartWrapper = styled(Box)(() => ({
12
+ width: '100%',
13
+ height: '100%',
14
+ }));
15
+
16
+ export { StyledChartHeader, StyledLineChartWrapper };
@@ -0,0 +1,13 @@
1
+ import SelectAction from './ChartSelect';
2
+ import LineChart from './Line';
3
+ import ColumnChart from './ColumnChart';
4
+
5
+ const Chart = {
6
+ Column: ColumnChart,
7
+ Line: LineChart,
8
+ SelectAction,
9
+ };
10
+
11
+ export type { LineChart, ColumnChart };
12
+
13
+ export default Chart;
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import Typography from '../../Typography';
3
+
4
+ const AxisLabel = ({
5
+ label,
6
+ numberOfLines = 1,
7
+ textAlign = 'right',
8
+ }: {
9
+ label: string;
10
+ numberOfLines?: number;
11
+ textAlign?: 'right' | 'center';
12
+ }) => (
13
+ <Typography.Label
14
+ style={{ textAlign }}
15
+ intent="subdued"
16
+ ellipsizeMode="tail"
17
+ numberOfLines={numberOfLines}
18
+ adjustsFontSizeToFit
19
+ minimumFontScale={0.6}
20
+ >
21
+ {label}
22
+ </Typography.Label>
23
+ );
24
+
25
+ export default AxisLabel;
@@ -0,0 +1,131 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { LayoutChangeEvent } from 'react-native';
3
+ import Svg from 'react-native-svg';
4
+ import { useTheme } from '../../../theme';
5
+ import Box from '../../Box';
6
+ import {
7
+ AxisCoordinates,
8
+ HeaderConfig,
9
+ XAxisConfig,
10
+ YAxisConfig,
11
+ } from '../types';
12
+ import ChartHeader from './ChartHeader';
13
+ import EmptyState from './EmptyState';
14
+ import XAxis from './XAxis';
15
+ import XAxisGrid from './XAxisGrid';
16
+ import YAxis from './YAxis';
17
+ import YAxisGrid from './YAxisGrid';
18
+
19
+ export type ChartFrameProps = {
20
+ isEmpty?: boolean;
21
+ xAxisConfig: XAxisConfig;
22
+ yAxisConfig: YAxisConfig;
23
+ headerConfig?: HeaderConfig;
24
+ width: number;
25
+ height: number;
26
+ renderContent: (props: { coordinates: AxisCoordinates }) => React.ReactNode;
27
+ /**
28
+ * Text to display when the chart has no data (empty state).
29
+ * If undefined, no empty label will appear even if the chart is empty.
30
+ */
31
+ emptyText?: string;
32
+ /**
33
+ * If true, hides the grid lines for the Y-axis.
34
+ * Defaults to false.
35
+ */
36
+ hideYAxisGrid?: boolean;
37
+ /**
38
+ * If true, hides the grid lines for the X-axis.
39
+ * Defaults to false.
40
+ */
41
+ hideXAxisGrid?: boolean;
42
+ };
43
+
44
+ const yStart = 0;
45
+
46
+ const ChartFrame = ({
47
+ xAxisConfig,
48
+ yAxisConfig,
49
+ width,
50
+ height,
51
+ headerConfig,
52
+ renderContent,
53
+ isEmpty,
54
+ emptyText,
55
+ hideYAxisGrid = false,
56
+ hideXAxisGrid = false,
57
+ }: ChartFrameProps) => {
58
+ const theme = useTheme();
59
+ const { title, actionsExtra } = headerConfig || {};
60
+ const [headerHeight, setHeaderHeight] = useState<number>(0);
61
+ const onHeaderLayout = useCallback((event: LayoutChangeEvent) => {
62
+ setHeaderHeight(event.nativeEvent.layout.height);
63
+ }, []);
64
+
65
+ // Sizing and coordinates setup
66
+ const {
67
+ space: {
68
+ yAxisGridTextMarginRight,
69
+ xAxisGridTextMarginTop,
70
+ headerMarginBottom,
71
+ },
72
+ sizes: {
73
+ yAxisDefaultTextWidth,
74
+ xAxisDefaultTextHeight,
75
+ yAxisDefaultTextHeight,
76
+ },
77
+ } = theme.__hd__.chart;
78
+
79
+ // Drawable height for the whole chart: content, axis, grid, etc.
80
+ const chartContentHeight =
81
+ height - headerHeight - (headerHeight > 0 ? headerMarginBottom : 0);
82
+
83
+ // Start offsets for various chart components, after accounting for the charts axis text size
84
+ const xStartOffset = yAxisDefaultTextWidth + yAxisGridTextMarginRight;
85
+ const yStartOffset = xAxisDefaultTextHeight + xAxisGridTextMarginTop;
86
+
87
+ const coordinates = useMemo(
88
+ () => ({
89
+ yStart,
90
+ yEnd: yStart + chartContentHeight - yStartOffset,
91
+ xStart: xStartOffset,
92
+ xEnd: width,
93
+ }),
94
+ [chartContentHeight, yStartOffset, xStartOffset, width]
95
+ );
96
+
97
+ return (
98
+ <Box style={{ width, height }}>
99
+ {headerConfig && (
100
+ <ChartHeader
101
+ title={title}
102
+ actionsExtra={actionsExtra}
103
+ onLayout={onHeaderLayout}
104
+ />
105
+ )}
106
+
107
+ <Box style={{ paddingTop: yAxisDefaultTextHeight }}>
108
+ <Svg style={{ width, height: chartContentHeight, overflow: 'visible' }}>
109
+ <XAxis xAxisConfig={xAxisConfig} coordinates={coordinates} />
110
+ <YAxis yAxisConfig={yAxisConfig} coordinates={coordinates} />
111
+ {!hideXAxisGrid && (
112
+ <XAxisGrid coordinates={coordinates} xAxisConfig={xAxisConfig} />
113
+ )}
114
+ {!hideYAxisGrid && (
115
+ <YAxisGrid coordinates={coordinates} yAxisConfig={yAxisConfig} />
116
+ )}
117
+ {/** Chart data component */}
118
+ {/* If the chart is empty and emptyText is provided, show the empty state text.
119
+ If emptyText is undefined, the empty label will not appear. */}
120
+ {!isEmpty
121
+ ? renderContent({ coordinates })
122
+ : emptyText && (
123
+ <EmptyState content={emptyText} coordinates={coordinates} />
124
+ )}
125
+ </Svg>
126
+ </Box>
127
+ </Box>
128
+ );
129
+ };
130
+
131
+ export default ChartFrame;
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import { LayoutChangeEvent } from 'react-native';
3
+ import Typography from '../../Typography';
4
+ import { StyledChartHeader } from '../StyledChart';
5
+ import { HeaderConfig } from '../types';
6
+ import Box from '../../Box';
7
+
8
+ export type ChartHeaderProps = HeaderConfig & {
9
+ onLayout?: (event: LayoutChangeEvent) => void;
10
+ };
11
+
12
+ const ChartHeader = ({ title, actionsExtra, onLayout }: ChartHeaderProps) => (
13
+ <StyledChartHeader onLayout={onLayout}>
14
+ <Box>{title && <Typography.Body>{title}</Typography.Body>}</Box>
15
+ <Box>{actionsExtra}</Box>
16
+ </StyledChartHeader>
17
+ );
18
+
19
+ export default ChartHeader;
@@ -0,0 +1,83 @@
1
+ import React, { useCallback, useState, useMemo } from 'react';
2
+ import { LayoutChangeEvent, Platform } from 'react-native';
3
+ import { ForeignObject, G } from 'react-native-svg';
4
+ import Box from '../../Box';
5
+ import Typography from '../../Typography';
6
+ import { AxisCoordinates } from '../types';
7
+ import { useTheme } from '../../../theme';
8
+
9
+ type EmptyStateProps = {
10
+ coordinates: AxisCoordinates;
11
+ content: string;
12
+ };
13
+
14
+ const EmptyStateText = ({ content }: { content: string }) => {
15
+ return (
16
+ <Typography.Caption
17
+ fontWeight="semi-bold"
18
+ intent="muted"
19
+ numberOfLines={1}
20
+ ellipsizeMode="tail"
21
+ style={{ textAlign: 'center' }}
22
+ >
23
+ {content}
24
+ </Typography.Caption>
25
+ );
26
+ };
27
+
28
+ const EmptyState = ({ content, coordinates }: EmptyStateProps) => {
29
+ const [textSize, setTextSize] = useState({ width: 0, height: 0 });
30
+ const { xStart, xEnd, yStart, yEnd } = coordinates;
31
+ const theme = useTheme();
32
+ const {
33
+ sizes: { defaultWebEmptyStateWidth, defaultWebEmptyStateHeight },
34
+ } = theme.__hd__.chart;
35
+
36
+ const centerPosition = useMemo(() => {
37
+ const centerX = xStart + (xEnd - xStart) / 2;
38
+ const centerY = yStart + (yEnd - yStart) / 2;
39
+ return { centerX, centerY };
40
+ }, [xStart, xEnd, yStart, yEnd]);
41
+
42
+ const onTextLayout = useCallback((event: LayoutChangeEvent) => {
43
+ const { width, height } = event.nativeEvent.layout;
44
+ setTextSize({ width, height });
45
+ }, []);
46
+
47
+ return (
48
+ <G>
49
+ {Platform.OS === 'web' ? (
50
+ <ForeignObject
51
+ x={centerPosition.centerX - defaultWebEmptyStateWidth / 2}
52
+ y={centerPosition.centerY - defaultWebEmptyStateHeight / 2}
53
+ width={defaultWebEmptyStateWidth}
54
+ height={defaultWebEmptyStateHeight}
55
+ >
56
+ <Box
57
+ style={{
58
+ width: '100%',
59
+ height: '100%',
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ }}
63
+ >
64
+ <EmptyStateText content={content} />
65
+ </Box>
66
+ </ForeignObject>
67
+ ) : (
68
+ <Box
69
+ onLayout={onTextLayout}
70
+ style={{
71
+ position: 'absolute',
72
+ left: centerPosition.centerX - textSize.width / 2,
73
+ top: centerPosition.centerY - textSize.height / 2,
74
+ }}
75
+ >
76
+ <EmptyStateText content={content} />
77
+ </Box>
78
+ )}
79
+ </G>
80
+ );
81
+ };
82
+
83
+ export default EmptyState;
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { Platform } from 'react-native';
3
+ import { ForeignObject, G } from 'react-native-svg';
4
+ import { useTheme } from '../../../theme';
5
+ import Box from '../../Box';
6
+ import { AxisCoordinates, XAxisConfig } from '../types';
7
+ import AxisLabel from './AxisLabel';
8
+ import useScaleBandX from './hooks/useScaleBandX';
9
+
10
+ export type XAxisProps = {
11
+ // X axis config
12
+ xAxisConfig: XAxisConfig;
13
+ // X axis start offset
14
+ coordinates: AxisCoordinates;
15
+ };
16
+
17
+ const XAxis = ({ xAxisConfig, coordinates }: XAxisProps) => {
18
+ const { xStart, xEnd, yEnd } = coordinates;
19
+ const { labels = [] } = xAxisConfig;
20
+ const theme = useTheme();
21
+ const {
22
+ space: { xAxisGridTextMarginTop },
23
+ sizes: { xAxisDefaultTextHeight },
24
+ } = theme.__hd__.chart;
25
+
26
+ const scaleX = useScaleBandX({ labels, xStart, xEnd });
27
+
28
+ return (
29
+ <G>
30
+ {labels.map((label) => (
31
+ <G key={label}>
32
+ {/*
33
+ Cross-platform Y-axis label rendering:
34
+ - On web: Use ForeignObject to allow HTML/CSS styling and ellipsis with Typography.Label.
35
+ The x position is set to xStart - yAxisDefaultTextWidth to right-align the label box.
36
+ - On native: Use Box with absolute positioning and Typography.Label for compatibility.
37
+ The transform centers the label vertically.
38
+ */}
39
+ {Platform.OS === 'web' ? (
40
+ <ForeignObject
41
+ x={(scaleX(label) ?? 0) + scaleX.bandwidth() / 2}
42
+ y={yEnd + xAxisGridTextMarginTop}
43
+ width={scaleX.bandwidth()}
44
+ height={xAxisDefaultTextHeight}
45
+ >
46
+ <AxisLabel label={label} numberOfLines={2} textAlign="center" />
47
+ </ForeignObject>
48
+ ) : (
49
+ <Box
50
+ testID={`x-axis-label-${label}-container`}
51
+ style={{
52
+ width: scaleX.bandwidth(),
53
+ maxHeight: xAxisDefaultTextHeight,
54
+ position: 'absolute',
55
+ left: scaleX(label) ?? 0 + scaleX.bandwidth() / 2,
56
+ top: yEnd + xAxisGridTextMarginTop,
57
+ }}
58
+ alignItems="center"
59
+ >
60
+ <AxisLabel label={label} numberOfLines={2} textAlign="center" />
61
+ </Box>
62
+ )}
63
+ </G>
64
+ ))}
65
+ </G>
66
+ );
67
+ };
68
+
69
+ export default XAxis;
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { G, Line } from 'react-native-svg';
3
+ import { useTheme } from '../../../theme';
4
+ import { AxisCoordinates, XAxisConfig } from '../types';
5
+ import { DASH_ARRAY } from './constants';
6
+ import useScaleBandX from './hooks/useScaleBandX';
7
+
8
+ export type XAxisGridProps = {
9
+ xAxisConfig: XAxisConfig;
10
+ coordinates: AxisCoordinates;
11
+ };
12
+
13
+ const XAxisGrid = ({ xAxisConfig, coordinates }: XAxisGridProps) => {
14
+ const { xStart, xEnd, yEnd } = coordinates;
15
+ const theme = useTheme();
16
+ const { labels = [] } = xAxisConfig;
17
+
18
+ const {
19
+ colors: { gridStroke },
20
+ } = theme.__hd__.chart;
21
+
22
+ const scaleX = useScaleBandX({ labels, xStart, xEnd });
23
+
24
+ return (
25
+ <G testID="x-axis-grid">
26
+ {labels.map((label) => (
27
+ <Line
28
+ testID={`x-axis-grid-${label}`}
29
+ key={label}
30
+ x1={(scaleX(label) || 0) + scaleX.bandwidth() / 2}
31
+ y1={0}
32
+ x2={(scaleX(label) || 0) + scaleX.bandwidth() / 2}
33
+ y2={yEnd}
34
+ stroke={gridStroke}
35
+ strokeDasharray={DASH_ARRAY}
36
+ />
37
+ ))}
38
+ </G>
39
+ );
40
+ };
41
+
42
+ export default XAxisGrid;