@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,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import renderWithTheme from '../../../../testHelpers/renderWithTheme';
|
|
4
|
+
import ColumnChart, { ColumnChartProps } from '../index';
|
|
5
|
+
|
|
6
|
+
const baseData: ColumnChartProps['data'] = [
|
|
7
|
+
{
|
|
8
|
+
label: 'Series 1',
|
|
9
|
+
data: [10, 20, 30],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: 'Series 2',
|
|
13
|
+
data: [5, 15, 25],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const xAxisConfig = {
|
|
18
|
+
labels: ['A', 'B', 'C'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('ColumnChart', () => {
|
|
22
|
+
it('renders without crashing', () => {
|
|
23
|
+
const { getByTestId, toJSON, getByText } = renderWithTheme(
|
|
24
|
+
<ColumnChart
|
|
25
|
+
data={baseData}
|
|
26
|
+
xAxisConfig={xAxisConfig}
|
|
27
|
+
testID="column-chart"
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
// Simulate the onLayout event
|
|
31
|
+
fireEvent(getByTestId('column-chart'), 'layout', {
|
|
32
|
+
nativeEvent: {
|
|
33
|
+
layout: { x: 0, y: 0, width: 100, height: 200 },
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
expect(toJSON()).toMatchSnapshot();
|
|
37
|
+
|
|
38
|
+
// Check all expected y-axis labels
|
|
39
|
+
for (let value = 0; value <= 60; value += 10) {
|
|
40
|
+
expect(getByText(String(value))).toBeTruthy();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(getByText('A')).toBeTruthy();
|
|
44
|
+
expect(getByText('B')).toBeTruthy();
|
|
45
|
+
expect(getByText('C')).toBeTruthy();
|
|
46
|
+
|
|
47
|
+
// Check a few segments for correct accessibilityLabel and testID
|
|
48
|
+
const segmentA1 = getByTestId('column-segment-A-Series 1');
|
|
49
|
+
expect(segmentA1.props.accessibilityLabel).toContain('value 10');
|
|
50
|
+
expect(segmentA1.props.accessibilityLabel).toContain('x-label A');
|
|
51
|
+
expect(segmentA1.props.accessibilityLabel).toContain('series Series 1');
|
|
52
|
+
|
|
53
|
+
const segmentB2 = getByTestId('column-segment-B-Series 2');
|
|
54
|
+
expect(segmentB2.props.accessibilityLabel).toContain('value 15');
|
|
55
|
+
expect(segmentB2.props.accessibilityLabel).toContain('x-label B');
|
|
56
|
+
expect(segmentB2.props.accessibilityLabel).toContain('series Series 2');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('throws error if xAxisConfig.labels length does not match data length', () => {
|
|
60
|
+
const badData = [{ label: 'Series 1', data: [1, 2] }];
|
|
61
|
+
const badXAxisConfig = { labels: ['A', 'B', 'C'] };
|
|
62
|
+
expect(() =>
|
|
63
|
+
renderWithTheme(
|
|
64
|
+
<ColumnChart
|
|
65
|
+
data={badData}
|
|
66
|
+
xAxisConfig={badXAxisConfig}
|
|
67
|
+
testID="bad-chart"
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
).toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders emptyText when data is empty', () => {
|
|
74
|
+
const { getByTestId, getByText, toJSON } = renderWithTheme(
|
|
75
|
+
<ColumnChart
|
|
76
|
+
data={[]}
|
|
77
|
+
xAxisConfig={xAxisConfig}
|
|
78
|
+
testID="column-chart-empty"
|
|
79
|
+
emptyText="No data available"
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
// Simulate the onLayout event
|
|
83
|
+
fireEvent(getByTestId('column-chart-empty'), 'layout', {
|
|
84
|
+
nativeEvent: {
|
|
85
|
+
layout: { x: 0, y: 0, width: 100, height: 200 },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
expect(getByText('No data available')).toBeTruthy();
|
|
89
|
+
expect(toJSON()).toMatchSnapshot();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('throws error if any data value is negative', () => {
|
|
93
|
+
const negativeData = [
|
|
94
|
+
{ label: 'Series 1', data: [10, -5, 20] },
|
|
95
|
+
{ label: 'Series 2', data: [5, 15, 25] },
|
|
96
|
+
];
|
|
97
|
+
expect(() =>
|
|
98
|
+
renderWithTheme(
|
|
99
|
+
<ColumnChart
|
|
100
|
+
data={negativeData}
|
|
101
|
+
xAxisConfig={xAxisConfig}
|
|
102
|
+
testID="negative-chart"
|
|
103
|
+
/>
|
|
104
|
+
)
|
|
105
|
+
).toThrow('Negative values are not supported in ColumnChart');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('calls onBarPress with correct info when a bar is pressed', () => {
|
|
109
|
+
const onBarPress = jest.fn();
|
|
110
|
+
const { getByTestId } = renderWithTheme(
|
|
111
|
+
<ColumnChart
|
|
112
|
+
data={baseData}
|
|
113
|
+
xAxisConfig={xAxisConfig}
|
|
114
|
+
testID="column-chart"
|
|
115
|
+
onBarPress={onBarPress}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
// Simulate the onLayout event
|
|
119
|
+
fireEvent(getByTestId('column-chart'), 'layout', {
|
|
120
|
+
nativeEvent: {
|
|
121
|
+
layout: { x: 0, y: 0, width: 100, height: 200 },
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
// Press the first segment (A, Series 1)
|
|
125
|
+
fireEvent.press(getByTestId('column-segment-A-Series 1'));
|
|
126
|
+
expect(onBarPress).toHaveBeenCalledWith({
|
|
127
|
+
value: 10,
|
|
128
|
+
xLabel: 'A',
|
|
129
|
+
seriesLabel: 'Series 1',
|
|
130
|
+
seriesIndex: 0,
|
|
131
|
+
xIndex: 0,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// ColumnChart/index.tsx
|
|
2
|
+
// This is the main entry point for the ColumnChart component in the RN chart library.
|
|
3
|
+
// It composes the full column chart, handling layout, axis configuration, empty state, and rendering grouped/stacked columns.
|
|
4
|
+
// The component is highly configurable and uses subcomponents for axes, content, and frame.
|
|
5
|
+
|
|
6
|
+
import React, { useMemo, useState, useCallback, useEffect } from 'react';
|
|
7
|
+
import { ViewStyle, LayoutChangeEvent } from 'react-native';
|
|
8
|
+
import ChartFrame from '../shared/ChartFrame';
|
|
9
|
+
import {
|
|
10
|
+
DataValue,
|
|
11
|
+
HeaderConfig,
|
|
12
|
+
Series,
|
|
13
|
+
XAxisConfig,
|
|
14
|
+
YAxisConfig,
|
|
15
|
+
} from '../types';
|
|
16
|
+
import ColumnChartContent from './ColumnChartContent';
|
|
17
|
+
import { createNiceScale } from '../shared/niceNumbers';
|
|
18
|
+
import { StyledColumnChartWrapper } from './StyledColumnChart';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Props for the ColumnChart component.
|
|
22
|
+
*
|
|
23
|
+
* @property data - The data to be displayed in the chart. Each item represents a series.
|
|
24
|
+
* @property yAxisConfig - Optional configuration for the Y axis.
|
|
25
|
+
* @property xAxisConfig - Optional configuration for the X axis.
|
|
26
|
+
* @property style - Additional style for the root View. Follows the same convention as other components (e.g., Button).
|
|
27
|
+
* @property testID - Testing id of the component. Passed to the root View and used as a suffix for inner elements.
|
|
28
|
+
* @property headerConfig - Optional header configuration (title, actions).
|
|
29
|
+
* @property emptyText - Optional text to display when data is empty.
|
|
30
|
+
* @property onBarPress - Called when a bar (column segment) is pressed. Receives info about the bar.
|
|
31
|
+
*/
|
|
32
|
+
export interface ColumnChartProps {
|
|
33
|
+
/**
|
|
34
|
+
* The data to be displayed in the chart. Each item represents a series.
|
|
35
|
+
*/
|
|
36
|
+
data: Array<Series<Array<DataValue>>>;
|
|
37
|
+
/**
|
|
38
|
+
* Optional configuration for the Y axis.
|
|
39
|
+
* Note: minValue is omitted and always set to zero internally by the component.
|
|
40
|
+
*/
|
|
41
|
+
yAxisConfig?: Omit<YAxisConfig, 'minValue'>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional configuration for the X axis.
|
|
44
|
+
*/
|
|
45
|
+
xAxisConfig?: XAxisConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Additional style for the root View.
|
|
48
|
+
*/
|
|
49
|
+
style?: ViewStyle;
|
|
50
|
+
/**
|
|
51
|
+
* Testing id of the component.
|
|
52
|
+
*/
|
|
53
|
+
testID?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Header configuration for the chart.
|
|
56
|
+
*/
|
|
57
|
+
headerConfig?: HeaderConfig;
|
|
58
|
+
/**
|
|
59
|
+
* Text to display when the chart has no data (empty state).
|
|
60
|
+
*/
|
|
61
|
+
emptyText?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Called when a bar (column segment) is pressed. Receives info about the bar.
|
|
64
|
+
*/
|
|
65
|
+
onBarPress?: (info: {
|
|
66
|
+
value: number | undefined;
|
|
67
|
+
xLabel: string;
|
|
68
|
+
seriesLabel: string;
|
|
69
|
+
seriesIndex: number;
|
|
70
|
+
xIndex: number;
|
|
71
|
+
}) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* ColumnChart component for rendering grouped/stacked column charts.
|
|
76
|
+
* Handles layout, axis configuration, empty state, and data validation.
|
|
77
|
+
* Uses ChartFrame for layout and axes, and ColumnChartContent for rendering columns.
|
|
78
|
+
*
|
|
79
|
+
* @param data - Array of series, each with a label and array of values.
|
|
80
|
+
* @param yAxisConfig - Optional Y axis configuration.
|
|
81
|
+
* @param xAxisConfig - Optional X axis configuration.
|
|
82
|
+
* @param style - Optional style for the chart container.
|
|
83
|
+
* @param testID - Optional test ID for testing.
|
|
84
|
+
* @param headerConfig - Optional header configuration (title, actions).
|
|
85
|
+
* @param emptyText - Optional text to display when data is empty.
|
|
86
|
+
* @param onBarPress - Called when a bar (column segment) is pressed. Receives info about the bar.
|
|
87
|
+
*
|
|
88
|
+
* Example usage:
|
|
89
|
+
* <ColumnChart
|
|
90
|
+
* data={data}
|
|
91
|
+
* xAxisConfig={xAxisConfig}
|
|
92
|
+
* yAxisConfig={yAxisConfig}
|
|
93
|
+
* emptyText="No data available"
|
|
94
|
+
* />
|
|
95
|
+
*/
|
|
96
|
+
const ColumnChart = ({
|
|
97
|
+
data,
|
|
98
|
+
yAxisConfig = {},
|
|
99
|
+
xAxisConfig = {},
|
|
100
|
+
style,
|
|
101
|
+
testID,
|
|
102
|
+
headerConfig,
|
|
103
|
+
emptyText,
|
|
104
|
+
onBarPress,
|
|
105
|
+
}: ColumnChartProps) => {
|
|
106
|
+
const xLabels = useMemo(() => {
|
|
107
|
+
return xAxisConfig.labels && xAxisConfig.labels.length > 0
|
|
108
|
+
? xAxisConfig.labels
|
|
109
|
+
: data[0]?.data.map((_, index) => index.toString()) || [];
|
|
110
|
+
}, [data[0]?.data.length, xAxisConfig.labels?.length]);
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
// Validation: xLabels length must match each series' data length, only if labels are explicitly provided
|
|
114
|
+
if (xAxisConfig.labels && xAxisConfig.labels.length > 0) {
|
|
115
|
+
data.forEach((series) => {
|
|
116
|
+
if (series.data.length !== xLabels.length) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`xAxisConfig.labels length (${xLabels.length}) does not match data length (${series.data.length}) in series label: ${series.label}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}, [data, xAxisConfig]);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
// Assert that the chart does not support negative values
|
|
127
|
+
data.forEach((series) => {
|
|
128
|
+
series.data.forEach((value, idx) => {
|
|
129
|
+
if (typeof value === 'number' && value < 0) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Negative values are not supported in ColumnChart. Found value ${value} in series "${series.label}" at index ${idx}.`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}, [data]);
|
|
137
|
+
|
|
138
|
+
const [dimensions, setDimensions] = useState<{
|
|
139
|
+
width: number;
|
|
140
|
+
height: number;
|
|
141
|
+
} | null>(null);
|
|
142
|
+
|
|
143
|
+
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
144
|
+
const { width, height } = event.nativeEvent.layout;
|
|
145
|
+
if (width > 0 && height > 0) {
|
|
146
|
+
setDimensions({ width, height });
|
|
147
|
+
}
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
// Calculate stacked maxY
|
|
151
|
+
const yMax = useMemo(
|
|
152
|
+
() =>
|
|
153
|
+
Math.max(
|
|
154
|
+
...xLabels.map((_, xIdx) =>
|
|
155
|
+
data.reduce((sum, series) => sum + (series.data[xIdx] ?? 0), 0)
|
|
156
|
+
)
|
|
157
|
+
),
|
|
158
|
+
[data, xLabels]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const niceValues = useMemo(() => {
|
|
162
|
+
const maxDataValue = yMax;
|
|
163
|
+
const minDataValue = 0;
|
|
164
|
+
return createNiceScale(minDataValue, maxDataValue);
|
|
165
|
+
}, [data]);
|
|
166
|
+
|
|
167
|
+
const yAxisStep = yAxisConfig?.step ?? niceValues.tickSpacing;
|
|
168
|
+
const yAxisInterval = yAxisConfig?.tick?.interval ?? yAxisStep;
|
|
169
|
+
|
|
170
|
+
const calculatedYAxisConfig = useMemo(() => {
|
|
171
|
+
return {
|
|
172
|
+
...yAxisConfig,
|
|
173
|
+
maxValue: yAxisConfig?.maxValue ?? niceValues.niceMax,
|
|
174
|
+
minValue: 0,
|
|
175
|
+
step: yAxisConfig?.step ?? niceValues.tickSpacing,
|
|
176
|
+
tick: {
|
|
177
|
+
interval: yAxisInterval,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}, [yAxisConfig, niceValues, yAxisInterval]);
|
|
181
|
+
|
|
182
|
+
const calculatedXAxisConfig = useMemo(() => {
|
|
183
|
+
return {
|
|
184
|
+
...xAxisConfig,
|
|
185
|
+
labels: xLabels,
|
|
186
|
+
};
|
|
187
|
+
}, [xAxisConfig, xLabels]);
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<StyledColumnChartWrapper style={style} testID={testID} onLayout={onLayout}>
|
|
191
|
+
{dimensions && (
|
|
192
|
+
<ChartFrame
|
|
193
|
+
isEmpty={data.length === 0}
|
|
194
|
+
xAxisConfig={calculatedXAxisConfig}
|
|
195
|
+
yAxisConfig={calculatedYAxisConfig}
|
|
196
|
+
headerConfig={headerConfig}
|
|
197
|
+
width={dimensions.width}
|
|
198
|
+
height={dimensions.height}
|
|
199
|
+
hideXAxisGrid
|
|
200
|
+
emptyText={emptyText}
|
|
201
|
+
renderContent={({ coordinates }) => (
|
|
202
|
+
<ColumnChartContent
|
|
203
|
+
coordinates={coordinates}
|
|
204
|
+
data={data}
|
|
205
|
+
xAxisConfig={calculatedXAxisConfig}
|
|
206
|
+
yAxisConfig={calculatedYAxisConfig}
|
|
207
|
+
onBarPress={onBarPress}
|
|
208
|
+
/>
|
|
209
|
+
)}
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</StyledColumnChartWrapper>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export default ColumnChart;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as d3 from 'd3';
|
|
2
|
+
import React, { useMemo } from 'react';
|
|
3
|
+
import { Path } from 'react-native-svg';
|
|
4
|
+
import { DEFAULT_LINE_STROKE_WIDTH } from '../shared/constants';
|
|
5
|
+
import useScaleBandX from '../shared/hooks/useScaleBandX';
|
|
6
|
+
import useScaleLinearY from '../shared/hooks/useScaleLinearY';
|
|
7
|
+
import { AxisCoordinates, DataValue } from '../types';
|
|
8
|
+
import { useTheme } from '../../../theme';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Props for the Line chart component
|
|
12
|
+
*/
|
|
13
|
+
type LineProps = {
|
|
14
|
+
/** Array of data points to be plotted on the line chart */
|
|
15
|
+
data: DataValue[];
|
|
16
|
+
/** Maximum value for the Y-axis scale */
|
|
17
|
+
maxValue: number;
|
|
18
|
+
/** Minimum value for the Y-axis scale */
|
|
19
|
+
minValue: number;
|
|
20
|
+
/** Array of labels for the X-axis points */
|
|
21
|
+
labels: string[];
|
|
22
|
+
/** Coordinates defining the chart's boundaries and scale */
|
|
23
|
+
coordinates: AxisCoordinates;
|
|
24
|
+
/** Color for the line */
|
|
25
|
+
color?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Line = ({
|
|
29
|
+
data,
|
|
30
|
+
maxValue,
|
|
31
|
+
minValue,
|
|
32
|
+
labels,
|
|
33
|
+
coordinates,
|
|
34
|
+
color,
|
|
35
|
+
}: LineProps) => {
|
|
36
|
+
const { xStart, xEnd, yStart, yEnd } = coordinates;
|
|
37
|
+
const theme = useTheme();
|
|
38
|
+
|
|
39
|
+
// Create scales
|
|
40
|
+
const xScale = useScaleBandX({
|
|
41
|
+
labels,
|
|
42
|
+
xStart,
|
|
43
|
+
xEnd,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const yScale = useScaleLinearY({
|
|
47
|
+
maxValue,
|
|
48
|
+
minValue,
|
|
49
|
+
yStart,
|
|
50
|
+
yEnd,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Create line generator with proper curve
|
|
54
|
+
const lineGenerator = useMemo(
|
|
55
|
+
() =>
|
|
56
|
+
d3
|
|
57
|
+
.line<DataValue>()
|
|
58
|
+
.x((_, i) => (xScale(labels[i]) ?? 0) + xScale.bandwidth() / 2)
|
|
59
|
+
.y((d) => yScale(d ?? 0))
|
|
60
|
+
.curve(d3.curveBasis),
|
|
61
|
+
[xScale, yScale, labels]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Generate path data
|
|
65
|
+
const pathData = useMemo(() => lineGenerator(data), [data, lineGenerator]);
|
|
66
|
+
|
|
67
|
+
return pathData ? (
|
|
68
|
+
<Path
|
|
69
|
+
testID="line-path"
|
|
70
|
+
accessibilityLabel={`chart-line-maxValue:${maxValue},minValue:${minValue},labelsLength:${labels.length}`}
|
|
71
|
+
d={pathData}
|
|
72
|
+
stroke={color || theme.colors.secondary}
|
|
73
|
+
strokeWidth={DEFAULT_LINE_STROKE_WIDTH}
|
|
74
|
+
fill="none"
|
|
75
|
+
strokeLinecap="round"
|
|
76
|
+
strokeLinejoin="round"
|
|
77
|
+
/>
|
|
78
|
+
) : null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default Line;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderWithTheme from '../../../../testHelpers/renderWithTheme';
|
|
3
|
+
import Line from '../Line';
|
|
4
|
+
|
|
5
|
+
describe('Line', () => {
|
|
6
|
+
it('should render with basic data', () => {
|
|
7
|
+
const { getByTestId, toJSON } = renderWithTheme(
|
|
8
|
+
<Line
|
|
9
|
+
data={[1, 2, 3]}
|
|
10
|
+
maxValue={3}
|
|
11
|
+
minValue={1}
|
|
12
|
+
labels={['1', '2', '3']}
|
|
13
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(toJSON()).toMatchSnapshot();
|
|
18
|
+
const path = getByTestId('line-path');
|
|
19
|
+
expect(path).toBeTruthy();
|
|
20
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
21
|
+
'chart-line-maxValue:3,minValue:1,labelsLength:3'
|
|
22
|
+
);
|
|
23
|
+
expect(path.props.d).toBe(
|
|
24
|
+
'M96.875,250L109.896,233.333C122.917,216.667,148.958,183.333,175,150C201.042,116.667,227.083,83.333,240.104,66.667L253.125,50'
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render with negative values', () => {
|
|
29
|
+
const { getByTestId } = renderWithTheme(
|
|
30
|
+
<Line
|
|
31
|
+
data={[-1, -2, -3]}
|
|
32
|
+
maxValue={-1}
|
|
33
|
+
minValue={-3}
|
|
34
|
+
labels={['1', '2', '3']}
|
|
35
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const path = getByTestId('line-path');
|
|
40
|
+
expect(path).toBeTruthy();
|
|
41
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
42
|
+
'chart-line-maxValue:-1,minValue:-3,labelsLength:3'
|
|
43
|
+
);
|
|
44
|
+
expect(path.props.d).toBe(
|
|
45
|
+
'M96.875,50L109.896,66.667C122.917,83.333,148.958,116.667,175,150C201.042,183.333,227.083,216.667,240.104,233.333L253.125,250'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should render with mixed positive and negative values', () => {
|
|
50
|
+
const { getByTestId } = renderWithTheme(
|
|
51
|
+
<Line
|
|
52
|
+
data={[-1, 0, 1]}
|
|
53
|
+
maxValue={1}
|
|
54
|
+
minValue={-1}
|
|
55
|
+
labels={['1', '2', '3']}
|
|
56
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const path = getByTestId('line-path');
|
|
61
|
+
expect(path).toBeTruthy();
|
|
62
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
63
|
+
'chart-line-maxValue:1,minValue:-1,labelsLength:3'
|
|
64
|
+
);
|
|
65
|
+
expect(path.props.d).toBe(
|
|
66
|
+
'M96.875,250L109.896,233.333C122.917,216.667,148.958,183.333,175,150C201.042,116.667,227.083,83.333,240.104,66.667L253.125,50'
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should render with decimal values', () => {
|
|
71
|
+
const { getByTestId } = renderWithTheme(
|
|
72
|
+
<Line
|
|
73
|
+
data={[1.5, 2.5, 3.5]}
|
|
74
|
+
maxValue={3.5}
|
|
75
|
+
minValue={1.5}
|
|
76
|
+
labels={['1', '2', '3']}
|
|
77
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const path = getByTestId('line-path');
|
|
82
|
+
expect(path).toBeTruthy();
|
|
83
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
84
|
+
'chart-line-maxValue:3.5,minValue:1.5,labelsLength:3'
|
|
85
|
+
);
|
|
86
|
+
expect(path.props.d).toBe(
|
|
87
|
+
'M96.875,250L109.896,233.333C122.917,216.667,148.958,183.333,175,150C201.042,116.667,227.083,83.333,240.104,66.667L253.125,50'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should render with different coordinates', () => {
|
|
92
|
+
const { getByTestId } = renderWithTheme(
|
|
93
|
+
<Line
|
|
94
|
+
data={[1, 2, 3]}
|
|
95
|
+
maxValue={3}
|
|
96
|
+
minValue={1}
|
|
97
|
+
labels={['1', '2', '3']}
|
|
98
|
+
coordinates={{ xStart: 100, xEnd: 400, yStart: 100, yEnd: 300 }}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const path = getByTestId('line-path');
|
|
103
|
+
expect(path).toBeTruthy();
|
|
104
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
105
|
+
'chart-line-maxValue:3,minValue:1,labelsLength:3'
|
|
106
|
+
);
|
|
107
|
+
expect(path.props.d).toBe(
|
|
108
|
+
'M156.25,300L171.875,283.333C187.5,266.667,218.75,233.333,250,200C281.25,166.667,312.5,133.333,328.125,116.667L343.75,100'
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should not render when path data is null', () => {
|
|
113
|
+
const { queryByTestId } = renderWithTheme(
|
|
114
|
+
<Line
|
|
115
|
+
data={[]}
|
|
116
|
+
maxValue={0}
|
|
117
|
+
minValue={0}
|
|
118
|
+
labels={[]}
|
|
119
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(queryByTestId('line-path')).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should render with custom color', () => {
|
|
127
|
+
const { getByTestId } = renderWithTheme(
|
|
128
|
+
<Line
|
|
129
|
+
data={[1, 2, 3]}
|
|
130
|
+
maxValue={3}
|
|
131
|
+
minValue={1}
|
|
132
|
+
labels={['1', '2', '3']}
|
|
133
|
+
coordinates={{ xStart: 50, xEnd: 300, yStart: 50, yEnd: 250 }}
|
|
134
|
+
color="#FF0000"
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const path = getByTestId('line-path');
|
|
139
|
+
expect(path).toBeTruthy();
|
|
140
|
+
expect(path.props.accessibilityLabel).toBe(
|
|
141
|
+
'chart-line-maxValue:3,minValue:1,labelsLength:3'
|
|
142
|
+
);
|
|
143
|
+
expect(path.props.d).toBe(
|
|
144
|
+
'M96.875,250L109.896,233.333C122.917,216.667,148.958,183.333,175,150C201.042,116.667,227.083,83.333,240.104,66.667L253.125,50'
|
|
145
|
+
);
|
|
146
|
+
expect(path.props.stroke).toEqual({ payload: 4294901760, type: 0 });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`Line should render with basic data 1`] = `
|
|
4
|
+
<View
|
|
5
|
+
style={
|
|
6
|
+
{
|
|
7
|
+
"flex": 1,
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
>
|
|
11
|
+
<RNSVGPath
|
|
12
|
+
accessibilityLabel="chart-line-maxValue:3,minValue:1,labelsLength:3"
|
|
13
|
+
d="M96.875,250L109.896,233.333C122.917,216.667,148.958,183.333,175,150C201.042,116.667,227.083,83.333,240.104,66.667L253.125,50"
|
|
14
|
+
fill={null}
|
|
15
|
+
propList={
|
|
16
|
+
[
|
|
17
|
+
"fill",
|
|
18
|
+
"stroke",
|
|
19
|
+
"strokeWidth",
|
|
20
|
+
"strokeLinecap",
|
|
21
|
+
"strokeLinejoin",
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
stroke={
|
|
25
|
+
{
|
|
26
|
+
"payload": 4286144144,
|
|
27
|
+
"type": 0,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
strokeLinecap={1}
|
|
31
|
+
strokeLinejoin={1}
|
|
32
|
+
strokeWidth={2}
|
|
33
|
+
testID="line-path"
|
|
34
|
+
/>
|
|
35
|
+
<View
|
|
36
|
+
pointerEvents="box-none"
|
|
37
|
+
position="bottom"
|
|
38
|
+
style={
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
"bottom": 0,
|
|
42
|
+
"elevation": 9999,
|
|
43
|
+
"flexDirection": "column-reverse",
|
|
44
|
+
"left": 0,
|
|
45
|
+
"paddingHorizontal": 24,
|
|
46
|
+
"paddingVertical": 16,
|
|
47
|
+
"position": "absolute",
|
|
48
|
+
"right": 0,
|
|
49
|
+
"top": 0,
|
|
50
|
+
},
|
|
51
|
+
undefined,
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
/>
|
|
55
|
+
</View>
|
|
56
|
+
`;
|