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

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 (61) hide show
  1. package/dist/module/lib/Components/CartesianChart/CartesianChart.js +6 -53
  2. package/dist/module/lib/Components/CartesianChart/CartesianChart.js.map +1 -1
  3. package/dist/module/lib/Components/CartesianChart/utils.js +63 -0
  4. package/dist/module/lib/Components/CartesianChart/utils.js.map +1 -0
  5. package/dist/module/lib/Components/CartesianChart/utils.test.js +86 -0
  6. package/dist/module/lib/Components/CartesianChart/utils.test.js.map +1 -0
  7. package/dist/module/lib/Components/LineChart/LineChart.stories.js.map +1 -1
  8. package/dist/module/lib/Components/Point/Point.js +84 -0
  9. package/dist/module/lib/Components/Point/Point.js.map +1 -0
  10. package/dist/module/lib/Components/Point/Point.stories.js +178 -0
  11. package/dist/module/lib/Components/Point/Point.stories.js.map +1 -0
  12. package/dist/module/lib/Components/Point/Point.test.js +228 -0
  13. package/dist/module/lib/Components/Point/Point.test.js.map +1 -0
  14. package/dist/module/lib/Components/Point/index.js +4 -0
  15. package/dist/module/lib/Components/Point/index.js.map +1 -0
  16. package/dist/module/lib/Components/Point/types.js +4 -0
  17. package/dist/module/lib/Components/Point/types.js.map +1 -0
  18. package/dist/module/lib/Components/Point/utils.js +44 -0
  19. package/dist/module/lib/Components/Point/utils.js.map +1 -0
  20. package/dist/module/lib/Components/Point/utils.test.js +91 -0
  21. package/dist/module/lib/Components/Point/utils.test.js.map +1 -0
  22. package/dist/module/lib/Components/index.js +1 -0
  23. package/dist/module/lib/Components/index.js.map +1 -1
  24. package/dist/module/lib/utils/scales/scales.js +15 -0
  25. package/dist/module/lib/utils/scales/scales.js.map +1 -1
  26. package/dist/module/lib/utils/scales/scales.test.js +90 -1
  27. package/dist/module/lib/utils/scales/scales.test.js.map +1 -1
  28. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts +1 -1
  29. package/dist/typescript/src/lib/Components/CartesianChart/CartesianChart.d.ts.map +1 -1
  30. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts +4 -2
  31. package/dist/typescript/src/lib/Components/CartesianChart/types.d.ts.map +1 -1
  32. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts +19 -0
  33. package/dist/typescript/src/lib/Components/CartesianChart/utils.d.ts.map +1 -0
  34. package/dist/typescript/src/lib/Components/Point/Point.d.ts +4 -0
  35. package/dist/typescript/src/lib/Components/Point/Point.d.ts.map +1 -0
  36. package/dist/typescript/src/lib/Components/Point/index.d.ts +3 -0
  37. package/dist/typescript/src/lib/Components/Point/index.d.ts.map +1 -0
  38. package/dist/typescript/src/lib/Components/Point/types.d.ts +74 -0
  39. package/dist/typescript/src/lib/Components/Point/types.d.ts.map +1 -0
  40. package/dist/typescript/src/lib/Components/Point/utils.d.ts +24 -0
  41. package/dist/typescript/src/lib/Components/Point/utils.d.ts.map +1 -0
  42. package/dist/typescript/src/lib/Components/index.d.ts +1 -0
  43. package/dist/typescript/src/lib/Components/index.d.ts.map +1 -1
  44. package/dist/typescript/src/lib/utils/scales/scales.d.ts +10 -0
  45. package/dist/typescript/src/lib/utils/scales/scales.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/src/lib/Components/CartesianChart/CartesianChart.tsx +12 -65
  48. package/src/lib/Components/CartesianChart/types.ts +4 -2
  49. package/src/lib/Components/CartesianChart/utils.test.ts +88 -0
  50. package/src/lib/Components/CartesianChart/utils.ts +65 -0
  51. package/src/lib/Components/LineChart/LineChart.stories.tsx +1 -1
  52. package/src/lib/Components/Point/Point.stories.tsx +152 -0
  53. package/src/lib/Components/Point/Point.test.tsx +197 -0
  54. package/src/lib/Components/Point/Point.tsx +100 -0
  55. package/src/lib/Components/Point/index.ts +2 -0
  56. package/src/lib/Components/Point/types.ts +76 -0
  57. package/src/lib/Components/Point/utils.test.ts +118 -0
  58. package/src/lib/Components/Point/utils.ts +70 -0
  59. package/src/lib/Components/index.ts +1 -0
  60. package/src/lib/utils/scales/scales.test.ts +54 -0
  61. package/src/lib/utils/scales/scales.ts +21 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/CartesianChart/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,UAK7B,CAAC;AACF,eAAO,MAAM,wBAAwB;;;;;CAKpC,CAAC;AACF,eAAO,MAAM,YAAY,EAAE,UAK1B,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,OAAO,mBAAmB,CAAC,OAAO,CAAC,KAClC,UAqBF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,SAAS,mBAAmB,CAAC,aAAa,CAAC,KAC1C,UAQF,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { PointLabelProps, PointProps } from './types';
2
+ export declare function PointLabel({ textAnchor, ...props }: Readonly<PointLabelProps>): import("react/jsx-runtime").JSX.Element;
3
+ export declare function Point({ dataX, dataY, color, label, LabelComponent, labelPosition, hidePoint, showLabelArrow, size, onPress, }: Readonly<PointProps>): import("react/jsx-runtime").JSX.Element | null;
4
+ //# sourceMappingURL=Point.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Point.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/Point.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAU3D,wBAAgB,UAAU,CAAC,EACzB,UAAqB,EACrB,GAAG,KAAK,EACT,EAAE,QAAQ,CAAC,eAAe,CAAC,2CAa3B;AAED,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,KAAK,EACL,KAAK,EACL,KAAK,EACL,cAAc,EACd,aAAqB,EACrB,SAAiB,EACjB,cAAqB,EACrB,IAAmB,EACnB,OAAO,GACR,EAAE,QAAQ,CAAC,UAAU,CAAC,kDAqDtB"}
@@ -0,0 +1,3 @@
1
+ export { Point, PointLabel } from './Point';
2
+ export type { PointLabelComponent, PointLabelProps, PointProps } from './types';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,74 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { GestureResponderEvent } from 'react-native';
3
+ import type { TextProps } from 'react-native-svg';
4
+ export type PointLabelProps = {
5
+ x: number;
6
+ y: number;
7
+ children: string;
8
+ } & Omit<TextProps, 'x' | 'y' | 'children'>;
9
+ export type PointLabelComponent = ComponentType<PointLabelProps>;
10
+ export type PointProps = {
11
+ /**
12
+ * X coordinate in data space (index or explicit value).
13
+ */
14
+ dataX: number;
15
+ /**
16
+ * Y coordinate in data space.
17
+ */
18
+ dataY: number;
19
+ /**
20
+ * Fill color of the point circle.
21
+ * Defaults to `theme.colors.bg.mutedStrong`.
22
+ *
23
+ * Use the `useTheme` hook from `@ledgerhq/lumen-ui-rnative` to
24
+ * access design-token colors at runtime:
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * import { useTheme } from '@ledgerhq/lumen-ui-rnative';
29
+ *
30
+ * const { theme } = useTheme();
31
+ * <Point dataX={0} dataY={42} color={theme.colors.bg.errorStrong} />
32
+ * ```
33
+ */
34
+ color?: string;
35
+ /**
36
+ * Text label displayed near the point.
37
+ * A string is rendered directly; a function receives `dataX` and returns
38
+ * the label string, enabling index-based formatting.
39
+ */
40
+ label?: string | ((dataIndex: number) => string);
41
+ /**
42
+ * Custom component rendered instead of the default `PointLabel`.
43
+ * Receives pixel coordinates (`x`, `y`) and the resolved label as
44
+ * `children`. Use `PointLabel` inside your component to retain
45
+ * theme styling while customising layout or appearance.
46
+ */
47
+ LabelComponent?: PointLabelComponent;
48
+ /**
49
+ * Placement of the label relative to the point.
50
+ * @default 'top'
51
+ */
52
+ labelPosition?: 'top' | 'bottom';
53
+ /**
54
+ * When `true`, the point circle is hidden but the label (if any) is still rendered.
55
+ * @default false
56
+ */
57
+ hidePoint?: boolean;
58
+ /**
59
+ * Diameter of the point circle in pixels.
60
+ * Internally converted to an SVG radius (`size / 2`).
61
+ * @default 10
62
+ */
63
+ size?: number;
64
+ /**
65
+ * When `true`, an arrow is displayed pointing from the point to the label.
66
+ * @default true
67
+ */
68
+ showLabelArrow?: boolean;
69
+ /**
70
+ * Called when the point is pressed.
71
+ */
72
+ onPress?: (event: GestureResponderEvent) => void;
73
+ };
74
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElD,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,UAAU,CAAC,CAAC;AAE5C,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC,eAAe,CAAC,CAAC;AAEjE,MAAM,MAAM,UAAU,GAAG;IACvB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACjC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAClD,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { DrawingArea } from '../../utils/types';
2
+ export declare const DEFAULT_SIZE = 10;
3
+ export declare const STROKE_WIDTH = 2;
4
+ export declare const LABEL_FONT_SIZE = 10;
5
+ export declare const ARROW_WIDTH = 6;
6
+ export declare const ARROW_HEIGHT = 4;
7
+ export declare const GAP = 4;
8
+ export declare const isWithinBounds: (px: number, py: number, area: DrawingArea) => boolean;
9
+ /**
10
+ * Builds the three SVG points of the arrow triangle.
11
+ *
12
+ * `'top'` → arrow sits below the label, tip pointing **down** toward the circle.
13
+ * `'bottom'` → arrow sits above the label, tip pointing **up** toward the circle.
14
+ */
15
+ export declare const buildArrowPoints: (cx: number, cy: number, radius: number, position: "top" | "bottom") => string;
16
+ /**
17
+ * Resolves the label prop to a string or undefined.
18
+ */
19
+ export declare const resolveLabel: (label: string | ((dataIndex: number) => string) | undefined, dataX: number) => string | undefined;
20
+ /**
21
+ * Computes the vertical position of the label text baseline.
22
+ */
23
+ export declare const computeLabelY: (pixelY: number, radius: number, labelPosition: "top" | "bottom", renderArrow: boolean) => number;
24
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/Components/Point/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,GAAG,IAAI,CAAC;AAErB,eAAO,MAAM,cAAc,GACzB,IAAI,MAAM,EACV,IAAI,MAAM,EACV,MAAM,WAAW,KAChB,OAIyB,CAAC;AAE7B;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,IAAI,MAAM,EACV,IAAI,MAAM,EACV,QAAQ,MAAM,EACd,UAAU,KAAK,GAAG,QAAQ,KACzB,MAYF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,EAC3D,OAAO,MAAM,KACZ,MAAM,GAAG,SAGX,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,MAAM,EACd,QAAQ,MAAM,EACd,eAAe,KAAK,GAAG,QAAQ,EAC/B,aAAa,OAAO,KACnB,MAMF,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export * from './LineChart';
2
+ export * from './Point';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/Components/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
@@ -25,4 +25,14 @@ export declare const isCategoricalScale: (scale: ChartScaleFunction) => scale is
25
25
  * Checks if a scale is a numeric scale.
26
26
  */
27
27
  export declare const isNumericScale: (scale: ChartScaleFunction) => scale is NumericScale;
28
+ /**
29
+ * Projects a single data-space coordinate pair to pixel-space
30
+ * using the provided x and y scale functions.
31
+ *
32
+ * For categorical (band) scales the point is anchored at the center of the band.
33
+ */
34
+ export declare const projectPoint: (dataX: number, dataY: number, xScale: ChartScaleFunction, yScale: ChartScaleFunction) => {
35
+ x: number;
36
+ y: number;
37
+ };
28
38
  //# sourceMappingURL=scales.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scales.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/utils/scales/scales.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACb,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,+BAI7B;IACD,SAAS,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;CACnB,KAAG,YAMH,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,6BAIjC;IACD,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,gBAUH,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,eAAe,CAAC,WAAW,CAAC,KACtC,SAAS,IAAI,MAA8B,CAAC;AAE/C;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,kBAAkB,KACxB,KAAK,IAAI,gBAEX,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,kBAAkB,KACxB,KAAK,IAAI,YAEX,CAAC"}
1
+ {"version":3,"file":"scales.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/utils/scales/scales.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACb,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,+BAI7B;IACD,SAAS,EAAE,QAAQ,GAAG,KAAK,CAAC;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;CACnB,KAAG,YAMH,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,6BAIjC;IACD,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,UAAU,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,gBAUH,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,eAAe,CAAC,WAAW,CAAC,KACtC,SAAS,IAAI,MAA8B,CAAC;AAE/C;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,kBAAkB,KACxB,KAAK,IAAI,gBAEX,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GACzB,OAAO,kBAAkB,KACxB,KAAK,IAAI,YAEX,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GACvB,OAAO,MAAM,EACb,OAAO,MAAM,EACb,QAAQ,kBAAkB,EAC1B,QAAQ,kBAAkB,KACzB;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAQxB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ledgerhq/lumen-ui-rnative-visualization",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "keywords": [
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@types/react": "^19.0.0",
47
- "@ledgerhq/lumen-design-core": "0.1.11",
48
- "@ledgerhq/lumen-ui-rnative": "0.1.27",
47
+ "@ledgerhq/lumen-design-core": "0.1.12",
48
+ "@ledgerhq/lumen-ui-rnative": "0.1.29",
49
49
  "react": "^19.0.0",
50
50
  "react-native": "~0.79.7",
51
51
  "react-native-svg": "^15.0.0"
@@ -2,68 +2,16 @@ import { useCallback, useMemo, useState } from 'react';
2
2
  import { View, type LayoutChangeEvent } from 'react-native';
3
3
  import { Svg } from 'react-native-svg';
4
4
 
5
- import type { ChartInset } from '../../utils/types';
6
-
7
5
  import { CartesianChartProvider, useBuildChartContext } from './context';
8
6
  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 = ({
7
+ import {
8
+ DEFAULT_HEIGHT,
9
+ OVERFLOW_NEGATIVE_MARGIN,
10
+ resolveAxisPadding,
11
+ resolveInset,
12
+ } from './utils';
13
+
14
+ export function CartesianChart({
67
15
  series,
68
16
  xAxis,
69
17
  yAxis,
@@ -73,7 +21,7 @@ export const CartesianChart = ({
73
21
  axisPadding,
74
22
  ariaLabel = 'Chart',
75
23
  children,
76
- }: CartesianChartProps) => {
24
+ }: Readonly<CartesianChartProps>) {
77
25
  const [measuredWidth, setMeasuredWidth] = useState<number | undefined>(width);
78
26
 
79
27
  const needsMeasurement = width === undefined;
@@ -125,7 +73,7 @@ export const CartesianChart = ({
125
73
  onLayout={handleLayout}
126
74
  style={{
127
75
  height,
128
- ...DEFAULT_NEGATIVE_MARGIN,
76
+ ...OVERFLOW_NEGATIVE_MARGIN,
129
77
  }}
130
78
  accessibilityRole='image'
131
79
  accessibilityLabel={ariaLabel}
@@ -143,11 +91,10 @@ export const CartesianChart = ({
143
91
  style={{
144
92
  width: resolvedWidth,
145
93
  height,
146
- marginLeft: -20,
147
- marginRight: -20,
94
+ ...OVERFLOW_NEGATIVE_MARGIN,
148
95
  }}
149
96
  >
150
97
  {svgContent}
151
98
  </View>
152
99
  );
153
- };
100
+ }
@@ -26,8 +26,10 @@ export type CartesianChartProps = {
26
26
  */
27
27
  height?: number;
28
28
  /**
29
- * Padding between the SVG edge and the drawing area.
30
- * A number applies uniformly; a partial object overrides individual sides.
29
+ * Extra padding between the SVG edge and the drawing area, added on top of
30
+ * a built-in overflow buffer that prevents content (labels, points, ticks)
31
+ * from being clipped. A number applies uniformly; a partial object sets
32
+ * individual sides (unset sides default to `0`).
31
33
  */
32
34
  inset?: number | Partial<ChartInset>;
33
35
  /**
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+
3
+ import {
4
+ OVERFLOW_BUFFER,
5
+ ZERO_PADDING,
6
+ resolveAxisPadding,
7
+ resolveInset,
8
+ } from './utils';
9
+
10
+ describe('resolveInset', () => {
11
+ it('should return DEFAULT_INSET when undefined', () => {
12
+ expect(resolveInset(undefined)).toEqual(OVERFLOW_BUFFER);
13
+ });
14
+
15
+ it('should add a uniform number to each default side', () => {
16
+ expect(resolveInset(10)).toEqual({
17
+ top: 10 + OVERFLOW_BUFFER.top,
18
+ right: 10 + OVERFLOW_BUFFER.right,
19
+ bottom: 10 + OVERFLOW_BUFFER.bottom,
20
+ left: 10 + OVERFLOW_BUFFER.left,
21
+ });
22
+ });
23
+
24
+ it('should handle zero as a uniform number', () => {
25
+ expect(resolveInset(0)).toEqual(OVERFLOW_BUFFER);
26
+ });
27
+
28
+ it('should handle negative uniform values', () => {
29
+ expect(resolveInset(-5)).toEqual({
30
+ top: -5 + OVERFLOW_BUFFER.top,
31
+ right: -5 + OVERFLOW_BUFFER.right,
32
+ bottom: -5 + OVERFLOW_BUFFER.bottom,
33
+ left: -5 + OVERFLOW_BUFFER.left,
34
+ });
35
+ });
36
+
37
+ it('should merge a full partial object with defaults', () => {
38
+ expect(resolveInset({ top: 10, right: 15, bottom: 20, left: 25 })).toEqual({
39
+ top: 10 + OVERFLOW_BUFFER.top,
40
+ right: 15 + OVERFLOW_BUFFER.right,
41
+ bottom: 20 + OVERFLOW_BUFFER.bottom,
42
+ left: 25 + OVERFLOW_BUFFER.left,
43
+ });
44
+ });
45
+
46
+ it('should treat missing sides as 0 when given a partial object', () => {
47
+ expect(resolveInset({ top: 10 })).toEqual({
48
+ top: 10 + OVERFLOW_BUFFER.top,
49
+ right: OVERFLOW_BUFFER.right,
50
+ bottom: OVERFLOW_BUFFER.bottom,
51
+ left: OVERFLOW_BUFFER.left,
52
+ });
53
+ });
54
+
55
+ it('should handle an empty object', () => {
56
+ expect(resolveInset({})).toEqual(OVERFLOW_BUFFER);
57
+ });
58
+ });
59
+
60
+ describe('resolveAxisPadding', () => {
61
+ it('should return ZERO_PADDING when undefined', () => {
62
+ expect(resolveAxisPadding(undefined)).toEqual(ZERO_PADDING);
63
+ });
64
+
65
+ it('should resolve a full padding object', () => {
66
+ expect(
67
+ resolveAxisPadding({ top: 5, right: 10, bottom: 15, left: 20 }),
68
+ ).toEqual({
69
+ top: 5,
70
+ right: 10,
71
+ bottom: 15,
72
+ left: 20,
73
+ });
74
+ });
75
+
76
+ it('should treat missing sides as 0', () => {
77
+ expect(resolveAxisPadding({ bottom: 30 })).toEqual({
78
+ top: 0,
79
+ right: 0,
80
+ bottom: 30,
81
+ left: 0,
82
+ });
83
+ });
84
+
85
+ it('should handle an empty object', () => {
86
+ expect(resolveAxisPadding({})).toEqual(ZERO_PADDING);
87
+ });
88
+ });
@@ -0,0 +1,65 @@
1
+ import type { ChartInset } from '../../utils/types';
2
+ import type { CartesianChartProps } from './types';
3
+
4
+ export const DEFAULT_HEIGHT = 160;
5
+
6
+ /**
7
+ * Internal buffer that prevents SVG content (labels, points, ticks) from being
8
+ * clipped at the edge of the container. Compensated by negative margins on the
9
+ * wrapper so the chart's visual footprint stays unchanged.
10
+ */
11
+ export const OVERFLOW_BUFFER: ChartInset = {
12
+ top: 30,
13
+ right: 20,
14
+ bottom: 30,
15
+ left: 20,
16
+ };
17
+ export const OVERFLOW_NEGATIVE_MARGIN = {
18
+ marginTop: -OVERFLOW_BUFFER.top,
19
+ marginRight: -OVERFLOW_BUFFER.right,
20
+ marginBottom: -OVERFLOW_BUFFER.bottom,
21
+ marginLeft: -OVERFLOW_BUFFER.left,
22
+ };
23
+ export const ZERO_PADDING: ChartInset = {
24
+ top: 0,
25
+ right: 0,
26
+ bottom: 0,
27
+ left: 0,
28
+ };
29
+
30
+ export const resolveInset = (
31
+ inset: CartesianChartProps['inset'],
32
+ ): ChartInset => {
33
+ let consumer: ChartInset;
34
+ if (inset === undefined) {
35
+ consumer = ZERO_PADDING;
36
+ } else if (typeof inset === 'number') {
37
+ consumer = { top: inset, right: inset, bottom: inset, left: inset };
38
+ } else {
39
+ consumer = {
40
+ top: inset.top ?? 0,
41
+ right: inset.right ?? 0,
42
+ bottom: inset.bottom ?? 0,
43
+ left: inset.left ?? 0,
44
+ };
45
+ }
46
+
47
+ return {
48
+ top: consumer.top + OVERFLOW_BUFFER.top,
49
+ right: consumer.right + OVERFLOW_BUFFER.right,
50
+ bottom: consumer.bottom + OVERFLOW_BUFFER.bottom,
51
+ left: consumer.left + OVERFLOW_BUFFER.left,
52
+ };
53
+ };
54
+
55
+ export const resolveAxisPadding = (
56
+ padding: CartesianChartProps['axisPadding'],
57
+ ): ChartInset => {
58
+ if (padding === undefined) return ZERO_PADDING;
59
+ return {
60
+ top: padding.top ?? 0,
61
+ right: padding.right ?? 0,
62
+ bottom: padding.bottom ?? 0,
63
+ left: padding.left ?? 0,
64
+ };
65
+ };
@@ -1,7 +1,7 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
2
2
  import { View } from 'react-native';
3
- import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
4
3
 
4
+ import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
5
5
  import { LineChart } from './LineChart';
6
6
 
7
7
  const meta = {
@@ -0,0 +1,152 @@
1
+ import { useTheme } from '@ledgerhq/lumen-ui-rnative';
2
+ import type { Meta, StoryObj } from '@storybook/react-native-web-vite';
3
+ import { View } from 'react-native';
4
+ import { G } from 'react-native-svg';
5
+
6
+ import { StoryDecorator } from '../../../../.storybook/StoryDecorator.tsx';
7
+ import { LineChart } from '../LineChart';
8
+ import { Point, PointLabel } from './Point';
9
+
10
+ const sampleSeries = [
11
+ {
12
+ id: 'prices',
13
+ stroke: '#7B61FF',
14
+ data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
15
+ },
16
+ ];
17
+
18
+ const meta = {
19
+ component: Point,
20
+ title: 'Visualization/Point',
21
+ tags: ['experimental'],
22
+ decorators: [
23
+ (Story, context) => {
24
+ return (
25
+ <StoryDecorator context={context}>
26
+ <View style={{ width: 600, padding: 16 }}>
27
+ <Story />
28
+ </View>
29
+ </StoryDecorator>
30
+ );
31
+ },
32
+ ],
33
+ } satisfies Meta<typeof Point>;
34
+
35
+ export default meta;
36
+ type Story = StoryObj<typeof Point>;
37
+
38
+ export const Base: Story = {
39
+ parameters: {
40
+ layout: 'centered',
41
+ backgrounds: { default: 'light' },
42
+ },
43
+ render: () => {
44
+ const { theme } = useTheme();
45
+ return (
46
+ <LineChart series={sampleSeries} height={250} showArea>
47
+ <Point
48
+ dataX={9}
49
+ dataY={4}
50
+ label='$4.00'
51
+ color={theme.colors.bg.errorStrong}
52
+ labelPosition='bottom'
53
+ />
54
+ <Point
55
+ dataX={4}
56
+ dataY={98}
57
+ label='$98.00'
58
+ color={theme.colors.bg.successStrong}
59
+ labelPosition='top'
60
+ />
61
+ </LineChart>
62
+ );
63
+ },
64
+ };
65
+
66
+ export const WithCustomLabelComponent: Story = {
67
+ parameters: {
68
+ layout: 'centered',
69
+ backgrounds: { default: 'light' },
70
+ },
71
+ render: () => {
72
+ const { theme } = useTheme();
73
+ return (
74
+ <LineChart
75
+ series={sampleSeries}
76
+ height={250}
77
+ inset={{ top: 15 }}
78
+ showArea
79
+ >
80
+ <Point
81
+ dataX={9}
82
+ dataY={4}
83
+ label='$4.00'
84
+ color={theme.colors.bg.errorStrong}
85
+ labelPosition='bottom'
86
+ LabelComponent={({ children, ...props }) => (
87
+ <PointLabel {...props}>{children}</PointLabel>
88
+ )}
89
+ />
90
+ <Point
91
+ dataX={4}
92
+ dataY={98}
93
+ label='$98.00'
94
+ color={theme.colors.bg.successStrong}
95
+ labelPosition='top'
96
+ LabelComponent={({ x, y, children }) => (
97
+ <G>
98
+ <PointLabel x={x} y={y - 16}>
99
+ {children}
100
+ </PointLabel>
101
+ <PointLabel x={x} y={y} fontSize={10}>
102
+ +5.2%
103
+ </PointLabel>
104
+ </G>
105
+ )}
106
+ />
107
+ </LineChart>
108
+ );
109
+ },
110
+ };
111
+
112
+ export const NoPoint: Story = {
113
+ parameters: {
114
+ layout: 'centered',
115
+ backgrounds: { default: 'light' },
116
+ },
117
+ render: () => (
118
+ <LineChart series={sampleSeries} height={250} showArea>
119
+ <Point dataX={4} dataY={98} label='ATH' hidePoint />
120
+ <Point dataX={9} dataY={4} label='Low' hidePoint labelPosition='bottom' />
121
+ </LineChart>
122
+ ),
123
+ };
124
+
125
+ export const CustomSize: Story = {
126
+ parameters: {
127
+ layout: 'centered',
128
+ backgrounds: { default: 'light' },
129
+ },
130
+ render: () => {
131
+ const { theme } = useTheme();
132
+ return (
133
+ <LineChart series={sampleSeries} height={250} showArea>
134
+ <Point
135
+ dataX={4}
136
+ dataY={98}
137
+ size={16}
138
+ label={(i) => `Index ${i}`}
139
+ color={theme.colors.bg.successStrong}
140
+ />
141
+ <Point
142
+ dataX={9}
143
+ dataY={4}
144
+ size={6}
145
+ label='$4.00'
146
+ color={theme.colors.bg.errorStrong}
147
+ labelPosition='bottom'
148
+ />
149
+ </LineChart>
150
+ );
151
+ },
152
+ };