@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.
- package/.turbo/turbo-build.log +8 -3
- package/CHANGELOG.md +17 -0
- package/es/index.js +5621 -690
- package/jest.config.js +1 -1
- package/lib/index.js +5545 -613
- package/package.json +4 -2
- package/src/components/Avatar/AvatarStack/utils.ts +6 -4
- package/src/components/Badge/Status.tsx +1 -1
- package/src/components/Badge/__tests__/Status.spec.tsx +20 -0
- package/src/components/Badge/__tests__/__snapshots__/Status.spec.tsx.snap +2 -0
- package/src/components/Chart/ChartSelect/StyledChartSelect.tsx +9 -0
- package/src/components/Chart/ChartSelect/__tests__/StyledChartSelect.spec.tsx +15 -0
- package/src/components/Chart/ChartSelect/__tests__/__snapshots__/StyledChartSelect.spec.tsx.snap +20 -0
- package/src/components/Chart/ChartSelect/__tests__/index.spec.tsx +111 -0
- package/src/components/Chart/ChartSelect/index.tsx +137 -0
- package/src/components/Chart/ColumnChart/ColumnChartContent.tsx +84 -0
- package/src/components/Chart/ColumnChart/Segment.tsx +66 -0
- package/src/components/Chart/ColumnChart/StackedSegment.tsx +99 -0
- package/src/components/Chart/ColumnChart/StyledColumnChart.tsx +9 -0
- package/src/components/Chart/ColumnChart/__tests__/ColumnChartContent.spec.tsx +68 -0
- package/src/components/Chart/ColumnChart/__tests__/Segment.spec.tsx +99 -0
- package/src/components/Chart/ColumnChart/__tests__/StackedSegment.spec.tsx +115 -0
- package/src/components/Chart/ColumnChart/__tests__/__snapshots__/StackedSegment.spec.tsx.snap +120 -0
- package/src/components/Chart/ColumnChart/__tests__/__snapshots__/index.spec.tsx.snap +1405 -0
- package/src/components/Chart/ColumnChart/__tests__/index.spec.tsx +134 -0
- package/src/components/Chart/ColumnChart/index.tsx +216 -0
- package/src/components/Chart/Line/Line.tsx +81 -0
- package/src/components/Chart/Line/__tests__/Line.spec.tsx +148 -0
- package/src/components/Chart/Line/__tests__/__snapshots__/Line.spec.tsx.snap +56 -0
- package/src/components/Chart/Line/__tests__/__snapshots__/index.spec.tsx.snap +1461 -0
- package/src/components/Chart/Line/__tests__/index.spec.tsx +112 -0
- package/src/components/Chart/Line/index.tsx +143 -0
- package/src/components/Chart/StyledChart.tsx +16 -0
- package/src/components/Chart/index.tsx +13 -0
- package/src/components/Chart/shared/AxisLabel.tsx +25 -0
- package/src/components/Chart/shared/ChartFrame.tsx +131 -0
- package/src/components/Chart/shared/ChartHeader.tsx +19 -0
- package/src/components/Chart/shared/EmptyState.tsx +83 -0
- package/src/components/Chart/shared/XAxis.tsx +69 -0
- package/src/components/Chart/shared/XAxisGrid.tsx +42 -0
- package/src/components/Chart/shared/YAxis.tsx +104 -0
- package/src/components/Chart/shared/YAxisGrid.tsx +58 -0
- package/src/components/Chart/shared/__tests__/ChartFrame.spec.tsx +125 -0
- package/src/components/Chart/shared/__tests__/ChartHeader.spec.tsx +22 -0
- package/src/components/Chart/shared/__tests__/EmptyState.spec.tsx +29 -0
- package/src/components/Chart/shared/__tests__/XAXisGrid.spec.tsx +30 -0
- package/src/components/Chart/shared/__tests__/XAxis.spec.tsx +42 -0
- package/src/components/Chart/shared/__tests__/YAxis.spec.tsx +72 -0
- package/src/components/Chart/shared/__tests__/YAxisGrid.spec.tsx +35 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/ChartFrame.spec.tsx.snap +3058 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/ChartHeader.spec.tsx.snap +160 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/EmptyState.spec.tsx.snap +155 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/XAXisGrid.spec.tsx.snap +197 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/XAxis.spec.tsx.snap +369 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/YAxis.spec.tsx.snap +1013 -0
- package/src/components/Chart/shared/__tests__/__snapshots__/YAxisGrid.spec.tsx.snap +228 -0
- package/src/components/Chart/shared/__tests__/niceNumbers.spec.tsx +127 -0
- package/src/components/Chart/shared/constants.ts +2 -0
- package/src/components/Chart/shared/hooks/useColorScale.ts +25 -0
- package/src/components/Chart/shared/hooks/useGenerateTicks.ts +27 -0
- package/src/components/Chart/shared/hooks/useScaleBandX.ts +17 -0
- package/src/components/Chart/shared/hooks/useScaleLinearY.ts +30 -0
- package/src/components/Chart/shared/niceNumbers.ts +68 -0
- package/src/components/Chart/types.ts +100 -0
- package/src/components/Select/MultiSelect/OptionList.tsx +1 -1
- package/src/components/Select/MultiSelect/index.tsx +2 -6
- package/src/components/Select/MultiSelect/utils.ts +1 -1
- package/src/components/Select/SingleSelect/OptionList.tsx +1 -1
- package/src/components/Select/SingleSelect/index.tsx +2 -7
- package/src/components/Select/__tests__/helpers.spec.tsx +0 -36
- package/src/components/Select/helpers.tsx +0 -75
- package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/Option.spec.tsx.snap +3 -0
- package/src/components/Switch/SelectorSwitch/__tests__/__snapshots__/index.spec.tsx.snap +1 -0
- package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabs.spec.tsx.snap +3 -0
- package/src/components/Tabs/__tests__/__snapshots__/ScrollableTabsHeader.spec.tsx.snap +2 -0
- package/src/components/Tabs/__tests__/__snapshots__/TabWithBadge.spec.tsx.snap +1 -0
- package/src/components/Tabs/__tests__/__snapshots__/index.spec.tsx.snap +3 -0
- package/src/index.ts +2 -0
- package/src/theme/__tests__/__snapshots__/index.spec.ts.snap +28 -0
- package/src/theme/components/chart.ts +28 -0
- package/src/theme/components/columnChart.ts +15 -0
- package/src/theme/getTheme.ts +6 -0
- package/src/types.ts +4 -0
- package/src/utils/__tests__/helpers.spec.ts +36 -1
- package/src/utils/helpers.ts +76 -1
- package/stats/8.100.0/rn-stats.html +4842 -0
- package/stats/8.99.4/rn-stats.html +1 -1
- package/types/components/Badge/Status.d.ts +0 -1
- package/types/components/Chart/ChartSelect/StyledChartSelect.d.ts +8 -0
- package/types/components/Chart/ChartSelect/index.d.ts +63 -0
- package/types/components/Chart/ColumnChart/ColumnChartContent.d.ts +25 -0
- package/types/components/Chart/ColumnChart/Segment.d.ts +14 -0
- package/types/components/Chart/ColumnChart/StackedSegment.d.ts +28 -0
- package/types/components/Chart/ColumnChart/StyledColumnChart.d.ts +8 -0
- package/types/components/Chart/ColumnChart/index.d.ts +80 -0
- package/types/components/Chart/Line/Line.d.ts +21 -0
- package/types/components/Chart/Line/index.d.ts +35 -0
- package/types/components/Chart/StyledChart.d.ts +9 -0
- package/types/components/Chart/index.d.ts +9 -0
- package/types/components/Chart/shared/AxisLabel.d.ts +7 -0
- package/types/components/Chart/shared/ChartFrame.d.ts +30 -0
- package/types/components/Chart/shared/ChartHeader.d.ts +8 -0
- package/types/components/Chart/shared/EmptyState.d.ts +8 -0
- package/types/components/Chart/shared/XAxis.d.ts +8 -0
- package/types/components/Chart/shared/XAxisGrid.d.ts +8 -0
- package/types/components/Chart/shared/YAxis.d.ts +10 -0
- package/types/components/Chart/shared/YAxisGrid.d.ts +10 -0
- package/types/components/Chart/shared/constants.d.ts +2 -0
- package/types/components/Chart/shared/hooks/useColorScale.d.ts +7 -0
- package/types/components/Chart/shared/hooks/useGenerateTicks.d.ts +6 -0
- package/types/components/Chart/shared/hooks/useScaleBandX.d.ts +8 -0
- package/types/components/Chart/shared/hooks/useScaleLinearY.d.ts +9 -0
- package/types/components/Chart/shared/niceNumbers.d.ts +12 -0
- package/types/components/Chart/types.d.ts +84 -0
- package/types/components/Select/helpers.d.ts +0 -5
- package/types/index.d.ts +2 -1
- package/types/theme/components/chart.d.ts +22 -0
- package/types/theme/components/columnChart.d.ts +10 -0
- package/types/theme/getTheme.d.ts +4 -0
- package/types/types.d.ts +3 -1
- 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;
|