@ledgerhq/lumen-ui-rnative-visualization 0.1.2 → 0.1.4

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 (152) hide show
  1. package/dist/module/lib/Components/Axis/Axis.types.js +2 -0
  2. package/dist/module/lib/Components/Axis/Axis.types.js.map +1 -0
  3. package/dist/module/lib/Components/Axis/XAxis/XAxis.js +81 -0
  4. package/dist/module/lib/Components/Axis/XAxis/XAxis.js.map +1 -0
  5. package/dist/module/lib/Components/Axis/XAxis/XAxis.test.js +121 -0
  6. package/dist/module/lib/Components/Axis/XAxis/XAxis.test.js.map +1 -0
  7. package/dist/module/lib/Components/Axis/XAxis/index.js +4 -0
  8. package/dist/module/lib/Components/Axis/XAxis/index.js.map +1 -0
  9. package/dist/module/lib/Components/Axis/XAxis/types.js +4 -0
  10. package/dist/module/lib/Components/Axis/XAxis/types.js.map +1 -0
  11. package/dist/module/lib/Components/Axis/YAxis/YAxis.js +81 -0
  12. package/dist/module/lib/Components/Axis/YAxis/YAxis.js.map +1 -0
  13. package/dist/module/lib/Components/Axis/YAxis/YAxis.test.js +121 -0
  14. package/dist/module/lib/Components/Axis/YAxis/YAxis.test.js.map +1 -0
  15. package/dist/module/lib/Components/Axis/YAxis/index.js +4 -0
  16. package/dist/module/lib/Components/Axis/YAxis/index.js.map +1 -0
  17. package/dist/module/lib/Components/Axis/YAxis/types.js +4 -0
  18. package/dist/module/lib/Components/Axis/YAxis/types.js.map +1 -0
  19. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +123 -0
  20. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -0
  21. package/dist/module/lib/Components/CartesianChart/context/cartesianChartContext.js +10 -0
  22. package/dist/module/lib/Components/CartesianChart/context/cartesianChartContext.js.map +1 -0
  23. package/dist/module/lib/Components/CartesianChart/context/index.js +5 -0
  24. package/dist/module/lib/Components/CartesianChart/context/index.js.map +1 -0
  25. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js +75 -0
  26. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.js.map +1 -0
  27. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js +279 -0
  28. package/dist/module/lib/Components/CartesianChart/context/useBuildChartContext.test.js.map +1 -0
  29. package/dist/module/lib/Components/CartesianChart/index.js +5 -0
  30. package/dist/module/lib/Components/CartesianChart/index.js.map +1 -0
  31. package/dist/module/lib/Components/CartesianChart/types.js +4 -0
  32. package/dist/module/lib/Components/CartesianChart/types.js.map +1 -0
  33. package/dist/module/lib/Components/Line/Line.js +72 -0
  34. package/dist/module/lib/Components/Line/Line.js.map +1 -0
  35. package/dist/module/lib/Components/Line/index.js +4 -0
  36. package/dist/module/lib/Components/Line/index.js.map +1 -0
  37. package/dist/module/lib/Components/Line/types.js +2 -0
  38. package/dist/module/lib/Components/Line/types.js.map +1 -0
  39. package/dist/module/lib/Components/Line/utils.js +38 -0
  40. package/dist/module/lib/Components/Line/utils.js.map +1 -0
  41. package/dist/module/lib/Components/LineChart/LineChart.js +70 -31
  42. package/dist/module/lib/Components/LineChart/LineChart.js.map +1 -1
  43. package/dist/module/lib/Components/LineChart/LineChart.stories.js +271 -2
  44. package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
  45. package/dist/module/lib/Components/LineChart/LineChart.test.js +84 -3
  46. package/dist/module/lib/Components/LineChart/LineChart.test.js.map +1 -1
  47. package/dist/module/lib/Components/LineChart/types.js +4 -0
  48. package/dist/module/lib/Components/LineChart/types.js.map +1 -0
  49. package/dist/module/lib/utils/domain/domain.js +93 -0
  50. package/dist/module/lib/utils/domain/domain.js.map +1 -0
  51. package/dist/module/lib/utils/domain/domain.test.js +152 -0
  52. package/dist/module/lib/utils/domain/domain.test.js.map +1 -0
  53. package/dist/module/lib/utils/index.js +0 -2
  54. package/dist/module/lib/utils/index.js.map +1 -1
  55. package/dist/module/lib/utils/scales/scales.js +45 -0
  56. package/dist/module/lib/utils/scales/scales.js.map +1 -0
  57. package/dist/module/lib/utils/scales/scales.test.js +173 -0
  58. package/dist/module/lib/utils/scales/scales.test.js.map +1 -0
  59. package/dist/module/lib/utils/ticks/ticks.js +71 -0
  60. package/dist/module/lib/utils/ticks/ticks.js.map +1 -0
  61. package/dist/module/lib/utils/ticks/ticks.test.js +121 -0
  62. package/dist/module/lib/utils/ticks/ticks.test.js.map +1 -0
  63. package/dist/module/lib/utils/types.js +2 -0
  64. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts +33 -0
  65. package/dist/typescript/src/lib/Components/Axis/Axis.types.d.ts.map +1 -0
  66. package/dist/typescript/src/lib/Components/Axis/XAxis/XAxis.d.ts +4 -0
  67. package/dist/typescript/src/lib/Components/Axis/XAxis/XAxis.d.ts.map +1 -0
  68. package/dist/typescript/src/lib/Components/Axis/XAxis/index.d.ts +3 -0
  69. package/dist/typescript/src/lib/Components/Axis/XAxis/index.d.ts.map +1 -0
  70. package/dist/typescript/src/lib/Components/Axis/XAxis/types.d.ts +9 -0
  71. package/dist/typescript/src/lib/Components/Axis/XAxis/types.d.ts.map +1 -0
  72. package/dist/typescript/src/lib/Components/Axis/YAxis/YAxis.d.ts +4 -0
  73. package/dist/typescript/src/lib/Components/Axis/YAxis/YAxis.d.ts.map +1 -0
  74. package/dist/typescript/src/lib/Components/Axis/YAxis/index.d.ts +3 -0
  75. package/dist/typescript/src/lib/Components/Axis/YAxis/index.d.ts.map +1 -0
  76. package/dist/typescript/src/lib/Components/Axis/YAxis/types.d.ts +14 -0
  77. package/dist/typescript/src/lib/Components/Axis/YAxis/types.d.ts.map +1 -0
  78. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +3 -0
  79. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -0
  80. package/dist/typescript/src/lib/Components/CartesianChart/context/cartesianChartContext.d.ts +8 -0
  81. package/dist/typescript/src/lib/Components/CartesianChart/context/cartesianChartContext.d.ts.map +1 -0
  82. package/dist/typescript/src/lib/Components/CartesianChart/context/index.d.ts +3 -0
  83. package/dist/typescript/src/lib/Components/CartesianChart/context/index.d.ts.map +1 -0
  84. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts +23 -0
  85. package/dist/typescript/src/lib/Components/CartesianChart/context/useBuildChartContext.d.ts.map +1 -0
  86. package/dist/typescript/src/lib/Components/CartesianChart/index.d.ts +4 -0
  87. package/dist/typescript/src/lib/Components/CartesianChart/index.d.ts.map +1 -0
  88. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +46 -0
  89. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -0
  90. package/dist/typescript/src/lib/Components/Line/Line.d.ts +3 -0
  91. package/dist/typescript/src/lib/Components/Line/Line.d.ts.map +1 -0
  92. package/dist/typescript/src/lib/Components/Line/index.d.ts +3 -0
  93. package/dist/typescript/src/lib/Components/Line/index.d.ts.map +1 -0
  94. package/dist/typescript/src/lib/Components/Line/types.d.ts +24 -0
  95. package/dist/typescript/src/lib/Components/Line/types.d.ts.map +1 -0
  96. package/dist/typescript/src/lib/Components/Line/utils.d.ts +19 -0
  97. package/dist/typescript/src/lib/Components/Line/utils.d.ts.map +1 -0
  98. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts +2 -8
  99. package/dist/typescript/src/lib/Components/LineChart/LineChart.d.ts.map +1 -1
  100. package/dist/typescript/src/lib/Components/LineChart/index.d.ts +1 -0
  101. package/dist/typescript/src/lib/Components/LineChart/index.d.ts.map +1 -1
  102. package/dist/typescript/src/lib/Components/LineChart/types.d.ts +62 -0
  103. package/dist/typescript/src/lib/Components/LineChart/types.d.ts.map +1 -0
  104. package/dist/typescript/src/lib/utils/domain/domain.d.ts +17 -0
  105. package/dist/typescript/src/lib/utils/domain/domain.d.ts.map +1 -0
  106. package/dist/typescript/src/lib/utils/index.d.ts +1 -1
  107. package/dist/typescript/src/lib/utils/index.d.ts.map +1 -1
  108. package/dist/typescript/src/lib/utils/scales/scales.d.ts +28 -0
  109. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -0
  110. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts +37 -0
  111. package/dist/typescript/src/lib/utils/ticks/ticks.d.ts.map +1 -0
  112. package/dist/typescript/src/lib/utils/types.d.ts +93 -6
  113. package/dist/typescript/src/lib/utils/types.d.ts.map +1 -1
  114. package/package.json +11 -1
  115. package/src/lib/Components/Axis/Axis.types.ts +32 -0
  116. package/src/lib/Components/Axis/XAxis/XAxis.test.tsx +89 -0
  117. package/src/lib/Components/Axis/XAxis/XAxis.tsx +116 -0
  118. package/src/lib/Components/Axis/XAxis/index.ts +2 -0
  119. package/src/lib/Components/Axis/XAxis/types.ts +9 -0
  120. package/src/lib/Components/Axis/YAxis/YAxis.test.tsx +89 -0
  121. package/src/lib/Components/Axis/YAxis/YAxis.tsx +115 -0
  122. package/src/lib/Components/Axis/YAxis/index.ts +2 -0
  123. package/src/lib/Components/Axis/YAxis/types.ts +14 -0
  124. package/src/lib/Components/CartesianChart/CartesianChart.tsx +153 -0
  125. package/src/lib/Components/CartesianChart/context/cartesianChartContext.ts +14 -0
  126. package/src/lib/Components/CartesianChart/context/index.ts +10 -0
  127. package/src/lib/Components/CartesianChart/context/useBuildChartContext.test.ts +224 -0
  128. package/src/lib/Components/CartesianChart/context/useBuildChartContext.ts +115 -0
  129. package/src/lib/Components/CartesianChart/index.ts +3 -0
  130. package/src/lib/Components/CartesianChart/types.ts +47 -0
  131. package/src/lib/Components/Line/Line.tsx +88 -0
  132. package/src/lib/Components/Line/index.ts +2 -0
  133. package/src/lib/Components/Line/types.ts +23 -0
  134. package/src/lib/Components/Line/utils.ts +72 -0
  135. package/src/lib/Components/LineChart/LineChart.stories.tsx +287 -4
  136. package/src/lib/Components/LineChart/LineChart.test.tsx +71 -2
  137. package/src/lib/Components/LineChart/LineChart.tsx +90 -34
  138. package/src/lib/Components/LineChart/index.ts +1 -0
  139. package/src/lib/Components/LineChart/types.ts +63 -0
  140. package/src/lib/utils/domain/domain.test.ts +129 -0
  141. package/src/lib/utils/domain/domain.ts +101 -0
  142. package/src/lib/utils/index.ts +11 -1
  143. package/src/lib/utils/scales/scales.test.ts +133 -0
  144. package/src/lib/utils/scales/scales.ts +74 -0
  145. package/src/lib/utils/ticks/ticks.test.ts +136 -0
  146. package/src/lib/utils/ticks/ticks.ts +108 -0
  147. package/src/lib/utils/types.ts +100 -6
  148. package/dist/module/lib/utils/math/index.js +0 -4
  149. package/dist/module/lib/utils/math/index.js.map +0 -1
  150. package/dist/typescript/src/lib/utils/math/index.d.ts +0 -1
  151. package/dist/typescript/src/lib/utils/math/index.d.ts.map +0 -1
  152. package/src/lib/utils/math/index.ts +0 -2
@@ -0,0 +1,32 @@
1
+ export type BaseAxisProps = {
2
+ /**
3
+ * Whether to render grid lines at each tick.
4
+ * @default false
5
+ */
6
+ showGrid?: boolean;
7
+ /**
8
+ * Appearance of the grid lines.
9
+ * @default 'dashed'
10
+ */
11
+ gridLineStyle?: 'solid' | 'dashed';
12
+ /**
13
+ * Whether to render the axis baseline.
14
+ * @default false
15
+ */
16
+ showLine?: boolean;
17
+ /**
18
+ * Whether to render tick marks at each tick position.
19
+ * @default false
20
+ */
21
+ showTickMark?: boolean;
22
+ /**
23
+ * Explicit tick positions along the axis.
24
+ * When omitted, ticks are computed automatically from the scale.
25
+ */
26
+ ticks?: number[];
27
+ /**
28
+ * Formats a tick value into its display label.
29
+ * Receives the raw tick value (number or string label) and must return a string.
30
+ */
31
+ tickLabelFormatter?: (value: number | string) => string;
32
+ };
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
+ import { render } from '@testing-library/react-native';
5
+
6
+ import { CartesianChart } from '../../CartesianChart';
7
+
8
+ import type { XAxisProps } from './types';
9
+ import { XAxis } from './XAxis';
10
+
11
+ const sampleSeries = [
12
+ { id: 'test', stroke: '#000', data: [10, 20, 30, 40, 50] },
13
+ ];
14
+
15
+ const renderXAxis = (props: XAxisProps = {}) =>
16
+ render(
17
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
18
+ <CartesianChart series={sampleSeries} width={400} height={200}>
19
+ <XAxis {...props} />
20
+ </CartesianChart>
21
+ </ThemeProvider>,
22
+ );
23
+
24
+ describe('XAxis', () => {
25
+ it('renders without crashing', () => {
26
+ const { getByTestId } = renderXAxis();
27
+ getByTestId('chart-svg');
28
+ });
29
+
30
+ it('renders tick labels for explicit ticks', () => {
31
+ const { getByText } = renderXAxis({ ticks: [0, 2, 4] });
32
+ getByText('0');
33
+ getByText('2');
34
+ getByText('4');
35
+ });
36
+
37
+ it('does not render tick labels for empty ticks', () => {
38
+ const { queryByText } = renderXAxis({ ticks: [] });
39
+ expect(queryByText('0')).toBeNull();
40
+ });
41
+
42
+ it('applies tickLabelFormatter', () => {
43
+ const formatter = (value: number | string) => `v${value}`;
44
+ const { getByText } = renderXAxis({
45
+ ticks: [0, 4],
46
+ tickLabelFormatter: formatter,
47
+ });
48
+ getByText('v0');
49
+ getByText('v4');
50
+ });
51
+
52
+ it('renders with showGrid enabled', () => {
53
+ const { toJSON } = renderXAxis({ showGrid: true, ticks: [0, 2] });
54
+ expect(toJSON()).toBeTruthy();
55
+ });
56
+
57
+ it('renders with showLine enabled', () => {
58
+ const { toJSON } = renderXAxis({ showLine: true, ticks: [] });
59
+ expect(toJSON()).toBeTruthy();
60
+ });
61
+
62
+ it('renders with showTickMark enabled', () => {
63
+ const { toJSON } = renderXAxis({ showTickMark: true, ticks: [0, 2] });
64
+ expect(toJSON()).toBeTruthy();
65
+ });
66
+
67
+ it('renders with all visual options enabled', () => {
68
+ const { getByText } = renderXAxis({
69
+ showGrid: true,
70
+ showLine: true,
71
+ showTickMark: true,
72
+ ticks: [0, 4],
73
+ });
74
+ getByText('0');
75
+ getByText('4');
76
+ });
77
+
78
+ it('does not render axis when width is 0', () => {
79
+ const { queryByText } = render(
80
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
81
+ <CartesianChart series={sampleSeries} width={0} height={200}>
82
+ <XAxis ticks={[0, 2, 4]} />
83
+ </CartesianChart>
84
+ </ThemeProvider>,
85
+ );
86
+ expect(queryByText('0')).toBeNull();
87
+ expect(queryByText('2')).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1,116 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { useMemo } from 'react';
3
+ import { G, Line as SvgLine, Text as SvgText } from 'react-native-svg';
4
+
5
+ import {
6
+ buildTicksData,
7
+ isTickOnXAxisDomainEdge,
8
+ } from '../../../utils/ticks/ticks';
9
+ import { useCartesianChartContext } from '../../CartesianChart/context';
10
+
11
+ import type { XAxisProps } from './types';
12
+
13
+ const STROKE_WIDTH = 1;
14
+ const TICK_MARK_SIZE = 4;
15
+ const TICK_LABEL_OFFSET = 6;
16
+ export const DEFAULT_AXIS_HEIGHT = 28;
17
+
18
+ export const XAxis = ({
19
+ gridLineStyle = 'dashed',
20
+ position = 'bottom',
21
+ showGrid = false,
22
+ showLine = false,
23
+ showTickMark = false,
24
+ ticks: ticksProp,
25
+ tickLabelFormatter,
26
+ }: XAxisProps) => {
27
+ const { getXScale, getXAxisConfig, drawingArea } = useCartesianChartContext();
28
+ const { theme } = useTheme();
29
+
30
+ const xScale = getXScale();
31
+ const xAxisConfig = getXAxisConfig();
32
+
33
+ const ticksData = useMemo(
34
+ () =>
35
+ xScale
36
+ ? buildTicksData(xScale, xAxisConfig, ticksProp, tickLabelFormatter)
37
+ : [],
38
+ [xScale, xAxisConfig, ticksProp, tickLabelFormatter],
39
+ );
40
+
41
+ if (!xScale || drawingArea.width <= 0) {
42
+ return null;
43
+ }
44
+
45
+ const isTop = position === 'top';
46
+ const axisY = isTop ? drawingArea.y : drawingArea.y + drawingArea.height;
47
+
48
+ const fontSize = theme.typographies.body4.fontSize;
49
+ const tickDirection = isTop ? -1 : 1;
50
+ const labelY = axisY + tickDirection * (TICK_MARK_SIZE + TICK_LABEL_OFFSET);
51
+ const labelDy = isTop ? 0 : fontSize * 0.8;
52
+
53
+ const gridStroke = theme.colors.border.mutedSubtle;
54
+ const lineStroke = theme.colors.border.muted;
55
+ const textFill = theme.colors.text.muted;
56
+
57
+ return (
58
+ <G>
59
+ {showGrid &&
60
+ ticksData
61
+ .filter((tick) => isTickOnXAxisDomainEdge(tick, drawingArea))
62
+ .map((tick, i) => (
63
+ <SvgLine
64
+ key={`grid-${tick.value}-${i}`}
65
+ x1={tick.position}
66
+ y1={drawingArea.y}
67
+ x2={tick.position}
68
+ y2={drawingArea.y + drawingArea.height}
69
+ stroke={gridStroke}
70
+ strokeWidth={STROKE_WIDTH}
71
+ strokeDasharray={gridLineStyle === 'dashed' ? '3 3' : undefined}
72
+ />
73
+ ))}
74
+
75
+ {showLine && (
76
+ <SvgLine
77
+ x1={drawingArea.x}
78
+ y1={axisY}
79
+ x2={drawingArea.x + drawingArea.width}
80
+ y2={axisY}
81
+ stroke={lineStroke}
82
+ strokeWidth={STROKE_WIDTH}
83
+ strokeLinecap='square'
84
+ />
85
+ )}
86
+
87
+ {showTickMark &&
88
+ ticksData.map((tick, i) => (
89
+ <SvgLine
90
+ key={`tick-${tick.value}-${i}`}
91
+ x1={tick.position}
92
+ y1={axisY}
93
+ x2={tick.position}
94
+ y2={axisY + tickDirection * TICK_MARK_SIZE}
95
+ stroke={lineStroke}
96
+ strokeWidth={STROKE_WIDTH}
97
+ />
98
+ ))}
99
+
100
+ {ticksData.map((tick, i) => (
101
+ <SvgText
102
+ key={`label-${tick.value}-${i}`}
103
+ x={tick.position}
104
+ y={labelY}
105
+ dy={labelDy}
106
+ textAnchor='middle'
107
+ fill={textFill}
108
+ fontSize={fontSize}
109
+ fontFamily={theme.fontFamilies.sans}
110
+ >
111
+ {tick.label}
112
+ </SvgText>
113
+ ))}
114
+ </G>
115
+ );
116
+ };
@@ -0,0 +1,2 @@
1
+ export { XAxis, DEFAULT_AXIS_HEIGHT } from './XAxis';
2
+ export type { XAxisProps } from './types';
@@ -0,0 +1,9 @@
1
+ import type { BaseAxisProps } from '../Axis.types';
2
+
3
+ export type XAxisProps = BaseAxisProps & {
4
+ /**
5
+ * Where the x-axis is rendered relative to the drawing area.
6
+ * @default 'bottom'
7
+ */
8
+ position?: 'top' | 'bottom';
9
+ };
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { ledgerLiveThemes } from '@ledgerhq/lumen-design-core';
3
+ import { ThemeProvider } from '@ledgerhq/lumen-ui-rnative';
4
+ import { render } from '@testing-library/react-native';
5
+
6
+ import { CartesianChart } from '../../CartesianChart';
7
+
8
+ import type { YAxisProps } from './types';
9
+ import { YAxis } from './YAxis';
10
+
11
+ const sampleSeries = [
12
+ { id: 'test', stroke: '#000', data: [10, 20, 30, 40, 50] },
13
+ ];
14
+
15
+ const renderYAxis = (props: YAxisProps = {}) =>
16
+ render(
17
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
18
+ <CartesianChart series={sampleSeries} width={400} height={200}>
19
+ <YAxis {...props} />
20
+ </CartesianChart>
21
+ </ThemeProvider>,
22
+ );
23
+
24
+ describe('YAxis', () => {
25
+ it('renders without crashing', () => {
26
+ const { getByTestId } = renderYAxis();
27
+ getByTestId('chart-svg');
28
+ });
29
+
30
+ it('renders tick labels for explicit ticks', () => {
31
+ const { getByText } = renderYAxis({ ticks: [10, 30, 50] });
32
+ getByText('10');
33
+ getByText('30');
34
+ getByText('50');
35
+ });
36
+
37
+ it('does not render tick labels for empty ticks', () => {
38
+ const { queryByText } = renderYAxis({ ticks: [] });
39
+ expect(queryByText('10')).toBeNull();
40
+ });
41
+
42
+ it('applies tickLabelFormatter', () => {
43
+ const formatter = (value: number | string) => `$${value}`;
44
+ const { getByText } = renderYAxis({
45
+ ticks: [10, 50],
46
+ tickLabelFormatter: formatter,
47
+ });
48
+ getByText('$10');
49
+ getByText('$50');
50
+ });
51
+
52
+ it('renders with showGrid enabled', () => {
53
+ const { toJSON } = renderYAxis({ showGrid: true, ticks: [10, 30] });
54
+ expect(toJSON()).toBeTruthy();
55
+ });
56
+
57
+ it('renders with showLine enabled', () => {
58
+ const { toJSON } = renderYAxis({ showLine: true, ticks: [] });
59
+ expect(toJSON()).toBeTruthy();
60
+ });
61
+
62
+ it('renders with showTickMark enabled', () => {
63
+ const { toJSON } = renderYAxis({ showTickMark: true, ticks: [10, 30] });
64
+ expect(toJSON()).toBeTruthy();
65
+ });
66
+
67
+ it('renders with all visual options enabled', () => {
68
+ const { getByText } = renderYAxis({
69
+ showGrid: true,
70
+ showLine: true,
71
+ showTickMark: true,
72
+ ticks: [10, 50],
73
+ });
74
+ getByText('10');
75
+ getByText('50');
76
+ });
77
+
78
+ it('does not render axis when height is 0', () => {
79
+ const { queryByText } = render(
80
+ <ThemeProvider themes={ledgerLiveThemes} colorScheme='light'>
81
+ <CartesianChart series={sampleSeries} width={400} height={0}>
82
+ <YAxis ticks={[10, 30, 50]} />
83
+ </CartesianChart>
84
+ </ThemeProvider>,
85
+ );
86
+ expect(queryByText('10')).toBeNull();
87
+ expect(queryByText('30')).toBeNull();
88
+ });
89
+ });
@@ -0,0 +1,115 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import { useMemo } from 'react';
3
+ import { G, Line as SvgLine, Text as SvgText } from 'react-native-svg';
4
+
5
+ import {
6
+ buildTicksData,
7
+ isTickOnYAxisDomainEdge,
8
+ } from '../../../utils/ticks/ticks';
9
+ import { useCartesianChartContext } from '../../CartesianChart/context';
10
+
11
+ import type { YAxisProps } from './types';
12
+
13
+ const STROKE_WIDTH = 1;
14
+ const TICK_MARK_SIZE = 4;
15
+ const TICK_LABEL_OFFSET = 6;
16
+ export const DEFAULT_AXIS_WIDTH = 40;
17
+
18
+ export const YAxis = ({
19
+ position = 'start',
20
+ showGrid = false,
21
+ showLine = false,
22
+ showTickMark = false,
23
+ gridLineStyle = 'dashed',
24
+ ticks: ticksProp,
25
+ tickLabelFormatter,
26
+ }: YAxisProps) => {
27
+ const { getYScale, getYAxisConfig, drawingArea } = useCartesianChartContext();
28
+ const { theme } = useTheme();
29
+
30
+ const yScale = getYScale();
31
+ const yAxisConfig = getYAxisConfig();
32
+
33
+ const ticksData = useMemo(
34
+ () =>
35
+ yScale
36
+ ? buildTicksData(yScale, yAxisConfig, ticksProp, tickLabelFormatter)
37
+ : [],
38
+ [yScale, yAxisConfig, ticksProp, tickLabelFormatter],
39
+ );
40
+
41
+ if (!yScale || drawingArea.height <= 0) {
42
+ return null;
43
+ }
44
+
45
+ const isStart = position === 'start';
46
+ const axisX = isStart ? drawingArea.x : drawingArea.x + drawingArea.width;
47
+ const tickDirection = isStart ? -1 : 1;
48
+ const labelX = axisX + tickDirection * (TICK_MARK_SIZE + TICK_LABEL_OFFSET);
49
+ const fontSize = theme.typographies.body4.fontSize;
50
+ const labelDy = fontSize * 0.35;
51
+
52
+ const gridStroke = theme.colors.border.mutedSubtle;
53
+ const lineStroke = theme.colors.border.muted;
54
+ const textFill = theme.colors.text.muted;
55
+
56
+ return (
57
+ <G>
58
+ {showGrid &&
59
+ ticksData
60
+ .filter((tick) => isTickOnYAxisDomainEdge(tick, drawingArea))
61
+ .map((tick, i) => (
62
+ <SvgLine
63
+ key={`grid-${tick.value}-${i}`}
64
+ x1={drawingArea.x}
65
+ y1={tick.position}
66
+ x2={drawingArea.x + drawingArea.width}
67
+ y2={tick.position}
68
+ stroke={gridStroke}
69
+ strokeWidth={STROKE_WIDTH}
70
+ strokeDasharray={gridLineStyle === 'dashed' ? '3 3' : undefined}
71
+ />
72
+ ))}
73
+
74
+ {showLine && (
75
+ <SvgLine
76
+ x1={axisX}
77
+ y1={drawingArea.y}
78
+ x2={axisX}
79
+ y2={drawingArea.y + drawingArea.height}
80
+ stroke={lineStroke}
81
+ strokeWidth={STROKE_WIDTH}
82
+ strokeLinecap='square'
83
+ />
84
+ )}
85
+
86
+ {showTickMark &&
87
+ ticksData.map((tick, i) => (
88
+ <SvgLine
89
+ key={`tick-${tick.value}-${i}`}
90
+ x1={axisX}
91
+ y1={tick.position}
92
+ x2={axisX + tickDirection * TICK_MARK_SIZE}
93
+ y2={tick.position}
94
+ stroke={lineStroke}
95
+ strokeWidth={STROKE_WIDTH}
96
+ />
97
+ ))}
98
+
99
+ {ticksData.map((tick, i) => (
100
+ <SvgText
101
+ key={`label-${tick.value}-${i}`}
102
+ x={labelX}
103
+ y={tick.position}
104
+ dy={labelDy}
105
+ textAnchor={position === 'start' ? 'end' : 'start'}
106
+ fill={textFill}
107
+ fontSize={theme.typographies.body4.fontSize}
108
+ fontFamily={theme.fontFamilies.sans}
109
+ >
110
+ {tick.label}
111
+ </SvgText>
112
+ ))}
113
+ </G>
114
+ );
115
+ };
@@ -0,0 +1,2 @@
1
+ export { YAxis, DEFAULT_AXIS_WIDTH } from './YAxis';
2
+ export type { YAxisProps } from './types';
@@ -0,0 +1,14 @@
1
+ import type { BaseAxisProps } from '../Axis.types';
2
+
3
+ export type YAxisProps = BaseAxisProps & {
4
+ /**
5
+ * Where the y-axis is rendered relative to the drawing area.
6
+ * @default 'start'
7
+ */
8
+ position?: 'start' | 'end';
9
+ /**
10
+ * Width of the axis in pixels.
11
+ * @default 40
12
+ */
13
+ width?: number;
14
+ };
@@ -0,0 +1,153 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { View, type LayoutChangeEvent } from 'react-native';
3
+ import { Svg } from 'react-native-svg';
4
+
5
+ import type { ChartInset } from '../../utils/types';
6
+
7
+ import { CartesianChartProvider, useBuildChartContext } from './context';
8
+ import type { CartesianChartProps } from './types';
9
+
10
+ const DEFAULT_HEIGHT = 160;
11
+ const DEFAULT_INSET: ChartInset = {
12
+ top: 5,
13
+ right: 20,
14
+ bottom: 0,
15
+ left: 20,
16
+ };
17
+
18
+ const DEFAULT_NEGATIVE_MARGIN = {
19
+ marginLeft: -20,
20
+ marginRight: -20,
21
+ };
22
+
23
+ const ZERO_PADDING: ChartInset = {
24
+ top: 0,
25
+ right: 0,
26
+ bottom: 0,
27
+ left: 0,
28
+ };
29
+
30
+ const resolveInset = (inset: CartesianChartProps['inset']): ChartInset => {
31
+ if (inset === undefined) {
32
+ return DEFAULT_INSET;
33
+ }
34
+
35
+ if (typeof inset === 'number') {
36
+ return {
37
+ top: inset + DEFAULT_INSET.top,
38
+ right: inset + DEFAULT_INSET.right,
39
+ bottom: inset + DEFAULT_INSET.bottom,
40
+ left: inset + DEFAULT_INSET.left,
41
+ };
42
+ }
43
+ return {
44
+ top: (inset.top ?? 0) + DEFAULT_INSET.top,
45
+ right: (inset.right ?? 0) + DEFAULT_INSET.right,
46
+ bottom: (inset.bottom ?? 0) + DEFAULT_INSET.bottom,
47
+ left: (inset.left ?? 0) + DEFAULT_INSET.left,
48
+ };
49
+ };
50
+
51
+ const resolveAxisPadding = (
52
+ padding: CartesianChartProps['axisPadding'],
53
+ ): ChartInset => {
54
+ if (padding === undefined) {
55
+ return ZERO_PADDING;
56
+ }
57
+
58
+ return {
59
+ top: padding.top ?? 0,
60
+ right: padding.right ?? 0,
61
+ bottom: padding.bottom ?? 0,
62
+ left: padding.left ?? 0,
63
+ };
64
+ };
65
+
66
+ export const CartesianChart = ({
67
+ series,
68
+ xAxis,
69
+ yAxis,
70
+ width,
71
+ height = DEFAULT_HEIGHT,
72
+ inset,
73
+ axisPadding,
74
+ ariaLabel = 'Chart',
75
+ children,
76
+ }: CartesianChartProps) => {
77
+ const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(width);
78
+
79
+ const needsMeasurement = width === undefined;
80
+
81
+ const handleLayout = useCallback(
82
+ (e: LayoutChangeEvent) => {
83
+ if (needsMeasurement) {
84
+ setMeasuredWidth(e.nativeEvent.layout.width);
85
+ }
86
+ },
87
+ [needsMeasurement],
88
+ );
89
+
90
+ const resolvedWidth = width ?? measuredWidth ?? 0;
91
+
92
+ const resolvedInset = useMemo(() => resolveInset(inset), [inset]);
93
+ const resolvedAxisPadding = useMemo(
94
+ () => resolveAxisPadding(axisPadding),
95
+ [axisPadding],
96
+ );
97
+
98
+ const contextValue = useBuildChartContext({
99
+ series,
100
+ xAxis,
101
+ yAxis,
102
+ width: resolvedWidth,
103
+ height,
104
+ inset: resolvedInset,
105
+ axisPadding: resolvedAxisPadding,
106
+ });
107
+
108
+ const svgContent = resolvedWidth > 0 && (
109
+ <Svg
110
+ testID='chart-svg'
111
+ width={resolvedWidth}
112
+ height={height}
113
+ style={{ overflow: 'visible' }}
114
+ >
115
+ <CartesianChartProvider value={contextValue}>
116
+ {children}
117
+ </CartesianChartProvider>
118
+ </Svg>
119
+ );
120
+
121
+ if (needsMeasurement) {
122
+ return (
123
+ <View
124
+ testID='chart-container'
125
+ onLayout={handleLayout}
126
+ style={{
127
+ height,
128
+ ...DEFAULT_NEGATIVE_MARGIN,
129
+ }}
130
+ accessibilityRole='image'
131
+ accessibilityLabel={ariaLabel}
132
+ >
133
+ {svgContent}
134
+ </View>
135
+ );
136
+ }
137
+
138
+ return (
139
+ <View
140
+ testID='chart-container'
141
+ accessibilityRole='image'
142
+ accessibilityLabel={ariaLabel}
143
+ style={{
144
+ width: resolvedWidth,
145
+ height,
146
+ marginLeft: -20,
147
+ marginRight: -20,
148
+ }}
149
+ >
150
+ {svgContent}
151
+ </View>
152
+ );
153
+ };
@@ -0,0 +1,14 @@
1
+ import { createSafeContext } from '@ledgerhq/lumen-utils-shared';
2
+
3
+ import type { CartesianChartContextValue } from '../../../utils/types';
4
+
5
+ const [CartesianChartProvider, _useCartesianChartSafeContext] =
6
+ createSafeContext<CartesianChartContextValue>('CartesianChart');
7
+
8
+ export const useCartesianChartContext = () =>
9
+ _useCartesianChartSafeContext({
10
+ consumerName: 'useCartesianChartContext',
11
+ contextRequired: true,
12
+ });
13
+
14
+ export { CartesianChartProvider };
@@ -0,0 +1,10 @@
1
+ export {
2
+ CartesianChartProvider,
3
+ useCartesianChartContext,
4
+ } from './cartesianChartContext';
5
+ export {
6
+ buildScale,
7
+ computeAxisRange,
8
+ computeDrawingArea,
9
+ useBuildChartContext,
10
+ } from './useBuildChartContext';