@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,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
+ `;