jfs-components 0.0.84 → 0.0.86

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 (51) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
  3. package/lib/commonjs/components/AppBar/AppBar.js +36 -22
  4. package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
  5. package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
  6. package/lib/commonjs/components/Attached/Attached.js +34 -4
  7. package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
  8. package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
  9. package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +52 -89
  11. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
  12. package/lib/commonjs/components/index.js +34 -0
  13. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
  16. package/lib/module/components/AppBar/AppBar.js +36 -22
  17. package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
  18. package/lib/module/components/AreaLineChart/chartMath.js +242 -0
  19. package/lib/module/components/Attached/Attached.js +34 -4
  20. package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
  21. package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
  22. package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
  23. package/lib/module/components/FullscreenModal/FullscreenModal.js +53 -90
  24. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
  25. package/lib/module/components/index.js +4 -0
  26. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  27. package/lib/module/icons/registry.js +1 -1
  28. package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
  29. package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
  30. package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
  31. package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
  32. package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
  33. package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
  34. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +21 -25
  35. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
  36. package/lib/typescript/src/components/index.d.ts +4 -0
  37. package/lib/typescript/src/icons/registry.d.ts +1 -1
  38. package/package.json +1 -1
  39. package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
  40. package/src/components/AppBar/AppBar.tsx +37 -24
  41. package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
  42. package/src/components/AreaLineChart/chartMath.ts +265 -0
  43. package/src/components/Attached/Attached.tsx +36 -5
  44. package/src/components/BubbleChart/BubbleChart.tsx +319 -0
  45. package/src/components/BubbleChart/bubblePacking.ts +397 -0
  46. package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
  47. package/src/components/FullscreenModal/FullscreenModal.tsx +61 -119
  48. package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
  49. package/src/components/index.ts +4 -0
  50. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  51. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Pure, framework-agnostic math helpers for `AreaLineChart`.
5
+ *
6
+ * Everything here is intentionally dependency-free (no d3, no react-native)
7
+ * so the chart can stay lightweight and be unit-reasoned in isolation. The
8
+ * functions cover the four primitives a line/area chart needs:
9
+ * 1. linear scales (data domain -> pixel range)
10
+ * 2. "nice" tick generation for the value axis
11
+ * 3. SVG path generation for lines and filled areas (straight + smooth)
12
+ * 4. interaction lookup (which data index is nearest a touch x)
13
+ */
14
+
15
+ /** A resolved point in data space. `x` is the categorical/linear index. */
16
+
17
+ /** A point already projected into pixel space. */
18
+
19
+ /** Linear interpolation scale: maps a value from `domain` into `range`. */
20
+
21
+ /**
22
+ * Create a linear scale mapping `domain` -> `range`. Mirrors the subset of
23
+ * `d3-scale.scaleLinear` we rely on, without the dependency. A zero-width
24
+ * domain collapses to the midpoint of the range so we never divide by zero.
25
+ */
26
+ export function createLinearScale(domain, range) {
27
+ const [d0, d1] = domain;
28
+ const [r0, r1] = range;
29
+ const domainSpan = d1 - d0;
30
+ const scale = value => {
31
+ if (domainSpan === 0) {
32
+ return (r0 + r1) / 2;
33
+ }
34
+ const t = (value - d0) / domainSpan;
35
+ return r0 + t * (r1 - r0);
36
+ };
37
+ scale.domain = domain;
38
+ scale.range = range;
39
+ return scale;
40
+ }
41
+
42
+ /**
43
+ * Generate up to ~`count` "nice" evenly-spaced tick values spanning
44
+ * `[min, max]`, snapping the step to a 1/2/5 * 10^n progression so labels
45
+ * read cleanly (e.g. 0, 30K, 60K, 90K). Returns ascending values that
46
+ * always include the rounded-down min and rounded-up max bounds.
47
+ */
48
+ export function niceTicks(min, max, count = 5) {
49
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
50
+ return [];
51
+ }
52
+ if (min === max) {
53
+ return [min];
54
+ }
55
+ const safeCount = Math.max(1, Math.floor(count));
56
+ const span = max - min;
57
+ const rawStep = span / safeCount;
58
+ const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep)));
59
+ const normalized = rawStep / magnitude;
60
+ let niceNormalized;
61
+ if (normalized <= 1) niceNormalized = 1;else if (normalized <= 2) niceNormalized = 2;else if (normalized <= 5) niceNormalized = 5;else niceNormalized = 10;
62
+ const step = niceNormalized * magnitude;
63
+ const niceMin = Math.floor(min / step) * step;
64
+ const niceMax = Math.ceil(max / step) * step;
65
+ const ticks = [];
66
+ // Guard against floating point drift by rounding to the step's precision.
67
+ const decimals = Math.max(0, -Math.floor(Math.log10(step)));
68
+ for (let v = niceMin; v <= niceMax + step / 2; v += step) {
69
+ ticks.push(Number(v.toFixed(decimals)));
70
+ }
71
+ return ticks;
72
+ }
73
+
74
+ /**
75
+ * Compute the [min, max] extent of an array of resolved points along the
76
+ * requested axis. Returns `[0, 0]` for an empty list.
77
+ */
78
+ export function extent(points, axis) {
79
+ if (points.length === 0) {
80
+ return [0, 0];
81
+ }
82
+ let min = Infinity;
83
+ let max = -Infinity;
84
+ for (const p of points) {
85
+ const v = p[axis];
86
+ if (v < min) min = v;
87
+ if (v > max) max = v;
88
+ }
89
+ return [min, max];
90
+ }
91
+
92
+ /**
93
+ * Normalize the loose `number[] | ChartPoint[]` series data into a uniform
94
+ * `ResolvedPoint[]`. Bare numbers use their array index as the x value.
95
+ */
96
+ export function resolvePoints(data) {
97
+ return data.map((entry, index) => {
98
+ if (typeof entry === 'number') {
99
+ return {
100
+ x: index,
101
+ y: entry,
102
+ projected: false
103
+ };
104
+ }
105
+ const x = typeof entry.x === 'number' ? entry.x : index;
106
+ return {
107
+ x,
108
+ y: entry.y,
109
+ projected: entry.projected === true
110
+ };
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Catmull-Rom -> cubic Bézier control point helper. Produces a smooth,
116
+ * monotone-ish curve through the points without overshooting wildly. Used
117
+ * for the `curve="monotone"` mode. `tension` of 0 yields a standard
118
+ * Catmull-Rom spline.
119
+ */
120
+ function controlPoints(p0, p1, p2, p3) {
121
+ const t = 1 / 6;
122
+ return {
123
+ cp1x: p1.x + (p2.x - p0.x) * t,
124
+ cp1y: p1.y + (p2.y - p0.y) * t,
125
+ cp2x: p2.x - (p3.x - p1.x) * t,
126
+ cp2y: p2.y - (p3.y - p1.y) * t
127
+ };
128
+ }
129
+
130
+ /**
131
+ * One drawable line segment. `dashed` mirrors the `projected` flag of the
132
+ * point the segment ends at, letting the renderer split the polyline into
133
+ * solid and dashed `Path`s.
134
+ */
135
+
136
+ /**
137
+ * Build the SVG path(s) for a polyline through `points`, split into runs of
138
+ * solid vs dashed segments. Each individual segment ("move to A, line/curve
139
+ * to B") is emitted as its own sub-path so that a single projected point can
140
+ * dash exactly one segment while leaving its neighbours solid.
141
+ */
142
+ export function buildLineSegments(points, curve) {
143
+ if (points.length < 2) {
144
+ return [];
145
+ }
146
+ const segments = [];
147
+ for (let i = 0; i < points.length - 1; i++) {
148
+ const start = points[i];
149
+ const end = points[i + 1];
150
+ const dashed = end.projected;
151
+ let d;
152
+ if (curve === 'monotone') {
153
+ const p0 = points[i - 1] ?? start;
154
+ const p3 = points[i + 2] ?? end;
155
+ const {
156
+ cp1x,
157
+ cp1y,
158
+ cp2x,
159
+ cp2y
160
+ } = controlPoints(p0, start, end, p3);
161
+ d = `M ${start.x} ${start.y} C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${end.x} ${end.y}`;
162
+ } else {
163
+ d = `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
164
+ }
165
+ segments.push({
166
+ d,
167
+ dashed
168
+ });
169
+ }
170
+
171
+ // Merge adjacent segments that share the same dashed-ness into one path
172
+ // string to minimize the number of rendered <Path> nodes.
173
+ const merged = [];
174
+ for (const seg of segments) {
175
+ const last = merged[merged.length - 1];
176
+ if (last && last.dashed === seg.dashed) {
177
+ // Drop the redundant leading "M x y" of the appended segment.
178
+ const continuation = seg.d.replace(/^M [^A-Za-z]+/, '');
179
+ last.d += ' ' + continuation;
180
+ } else {
181
+ merged.push({
182
+ ...seg
183
+ });
184
+ }
185
+ }
186
+ return merged;
187
+ }
188
+
189
+ /**
190
+ * Build a single closed area path (the line, then down to `baselineY` and
191
+ * back) for a filled area fill. Curve smoothing matches `buildLineSegments`.
192
+ */
193
+ export function buildAreaPath(points, baselineY, curve) {
194
+ if (points.length === 0) {
195
+ return '';
196
+ }
197
+ if (points.length === 1) {
198
+ const p = points[0];
199
+ return `M ${p.x} ${baselineY} L ${p.x} ${p.y} Z`;
200
+ }
201
+ let d = `M ${points[0].x} ${baselineY} L ${points[0].x} ${points[0].y}`;
202
+ for (let i = 0; i < points.length - 1; i++) {
203
+ const start = points[i];
204
+ const end = points[i + 1];
205
+ if (curve === 'monotone') {
206
+ const p0 = points[i - 1] ?? start;
207
+ const p3 = points[i + 2] ?? end;
208
+ const {
209
+ cp1x,
210
+ cp1y,
211
+ cp2x,
212
+ cp2y
213
+ } = controlPoints(p0, start, end, p3);
214
+ d += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${end.x} ${end.y}`;
215
+ } else {
216
+ d += ` L ${end.x} ${end.y}`;
217
+ }
218
+ }
219
+ const lastX = points[points.length - 1].x;
220
+ d += ` L ${lastX} ${baselineY} Z`;
221
+ return d;
222
+ }
223
+
224
+ /**
225
+ * Find the index of the point whose pixel-x is nearest to `targetX`. Used to
226
+ * snap the crosshair / tooltip to the closest data point during hover/drag.
227
+ */
228
+ export function nearestIndex(pixelXs, targetX) {
229
+ if (pixelXs.length === 0) {
230
+ return -1;
231
+ }
232
+ let best = 0;
233
+ let bestDist = Infinity;
234
+ for (let i = 0; i < pixelXs.length; i++) {
235
+ const dist = Math.abs(pixelXs[i] - targetX);
236
+ if (dist < bestDist) {
237
+ bestDist = dist;
238
+ best = i;
239
+ }
240
+ }
241
+ return best;
242
+ }
@@ -3,6 +3,7 @@
3
3
  import React, { useCallback, useMemo, useState } from 'react';
4
4
  import { View } from 'react-native';
5
5
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
7
  import { cloneChildrenWithModes, EMPTY_MODES } from '../../utils/react-utils';
7
8
 
8
9
  /**
@@ -113,6 +114,29 @@ function Attached({
113
114
  overflow: 'hidden'
114
115
  };
115
116
  }, [badgeSize, badgeRadius]);
117
+
118
+ // Forced slot ring. Like the corner radius, the border color/width are driven
119
+ // by design tokens (`attached/slot/border`, `attached/slot/borderWidth`) and
120
+ // applied to the slot regardless of the node passed. Rendered as an *outside*
121
+ // border: the ring lives on a wrapper that is pulled out by `-borderWidth` on
122
+ // every side (negative margin) so it grows outward instead of eating into the
123
+ // content, and its layout footprint stays equal to the slot box (keeping the
124
+ // anchor centering intact).
125
+ const slotBorderStyle = useMemo(() => {
126
+ const borderColor = getVariableByName('attached/slot/border', modes);
127
+ const borderWidth = parseFloat(getVariableByName('attached/slot/borderWidth', modes) || '0');
128
+ if (!borderColor || !(borderWidth > 0)) return null;
129
+
130
+ // Match the inner clip radius so the ring stays concentric. The outer edge
131
+ // sits `borderWidth` further out, hence `innerRadius + borderWidth`.
132
+ const innerRadius = badgeBoxStyle != null ? badgeBoxStyle.borderRadius : badgeRadius ?? 9999;
133
+ return {
134
+ borderWidth,
135
+ borderColor,
136
+ borderRadius: (typeof innerRadius === 'number' ? innerRadius : 9999) + borderWidth,
137
+ margin: -borderWidth
138
+ };
139
+ }, [modes, badgeBoxStyle, badgeRadius]);
116
140
  const badgePlacement = useMemo(() => {
117
141
  const {
118
142
  fx,
@@ -154,10 +178,16 @@ function Attached({
154
178
  style: badgePlacement,
155
179
  onLayout: onBadgeLayout,
156
180
  pointerEvents: "box-none",
157
- children: badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
158
- style: badgeBoxStyle,
159
- children: forceBadgeFill(badgeChildren)
160
- }) : badgeChildren
181
+ children: (() => {
182
+ const slot = badgeBoxStyle != null ? /*#__PURE__*/_jsx(View, {
183
+ style: badgeBoxStyle,
184
+ children: forceBadgeFill(badgeChildren)
185
+ }) : badgeChildren;
186
+ return slotBorderStyle != null ? /*#__PURE__*/_jsx(View, {
187
+ style: slotBorderStyle,
188
+ children: slot
189
+ }) : slot;
190
+ })()
161
191
  })]
162
192
  });
163
193
  }
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { StyleSheet, View } from 'react-native';
5
+ import { EMPTY_MODES } from '../../utils/react-utils';
6
+ import ClusterBubble from '../ClusterBubble/ClusterBubble';
7
+ import { chooseLabelDirections, estimateLabelBox, fitRadiiToBox, scaleRadii, simulateCluster } from './bubblePacking';
8
+
9
+ /** One bubble in the chart. */
10
+ import { jsx as _jsx } from "react/jsx-runtime";
11
+ const DEFAULT_APPEARANCE_CYCLE = ['Primary', 'Secondary', 'Tertiary', 'Quaternary', 'Quinary', 'Senary', 'Neutral'];
12
+ const toText = (node, fallback = '') => typeof node === 'string' || typeof node === 'number' ? String(node) : fallback;
13
+ /**
14
+ * `BubbleChart` arranges a set of `ClusterBubble`s with a lightweight **force
15
+ * simulation** — bubbles repel one another (collision), a gentle gravity keeps
16
+ * the cluster balanced, and the container box acts as the walls of a pool, so
17
+ * nothing ever overflows. Each datum's `value` drives its area (`sqrt`-scaled),
18
+ * colors resolve from the `dataViz/bg` token via a cycling `appearance`, and the
19
+ * emphasis comes from the `Emphasis / DataViz` mode. When a bubble is too small
20
+ * to hold its text, the label is anchored just outside the circle on whichever
21
+ * side has the most free space.
22
+ *
23
+ * @component
24
+ */
25
+ function BubbleChart({
26
+ data,
27
+ minBubbleSize = 48,
28
+ maxBubbleSize = 170,
29
+ gap = 8,
30
+ labelGap,
31
+ height,
32
+ labelPlacement = 'auto',
33
+ autoInsideMinSize = 88,
34
+ appearanceCycle = DEFAULT_APPEARANCE_CYCLE,
35
+ iterations = 500,
36
+ onBubblePress,
37
+ modes: propModes = EMPTY_MODES,
38
+ style,
39
+ accessibilityLabel
40
+ }) {
41
+ const modes = propModes;
42
+ const resolvedLabelGap = labelGap ?? gap;
43
+ const [width, setWidth] = useState(0);
44
+ const handleLayout = useCallback(e => {
45
+ const w = e.nativeEvent.layout.width;
46
+ setWidth(prev => Math.abs(prev - w) > 0.5 ? w : prev);
47
+ }, []);
48
+ const layout = useMemo(() => {
49
+ if (data.length === 0 || width <= 0) {
50
+ return {
51
+ bubbles: [],
52
+ poolHeight: height ?? 0
53
+ };
54
+ }
55
+ const minR = Math.max(1, minBubbleSize / 2);
56
+ const maxR = Math.max(minR, maxBubbleSize / 2);
57
+ const baseRadii = scaleRadii(data.map(d => d.value), minR, maxR);
58
+
59
+ // Estimate label sizes up front (independent of radius) so we can
60
+ // reserve a margin band around the bubbles for any outside labels.
61
+ const labelEstimates = data.map(d => estimateLabelBox(toText(d.display, String(d.value)), toText(d.label)));
62
+ const mayHaveOutside = labelPlacement !== 'inside' && (minBubbleSize < autoInsideMinSize || data.some(d => d.labelPlacement === 'outside'));
63
+ let insetX = gap;
64
+ let insetY = gap;
65
+ if (mayHaveOutside) {
66
+ let maxHalfW = 0;
67
+ let maxH = 0;
68
+ for (const e of labelEstimates) {
69
+ if (e.w / 2 > maxHalfW) maxHalfW = e.w / 2;
70
+ if (e.h > maxH) maxH = e.h;
71
+ }
72
+ insetX = Math.max(gap, maxHalfW + resolvedLabelGap);
73
+ insetY = Math.max(gap, maxH + resolvedLabelGap);
74
+ }
75
+
76
+ // Derive a pool height from the bubble area when none is supplied, then
77
+ // shrink radii (if needed) so everything fits inside the inner box.
78
+ let circleArea = 0;
79
+ for (const r of baseRadii) circleArea += Math.PI * r * r;
80
+ const derivedHeight = Math.min(width * 1.35, Math.max(width * 0.6, circleArea / 0.42 / width + 2 * insetY));
81
+ const poolHeight = Math.max(1, height ?? derivedHeight);
82
+ const innerW = Math.max(1, width - 2 * insetX);
83
+ const innerH = Math.max(1, poolHeight - 2 * insetY);
84
+ const radii = fitRadiiToBox(baseRadii, innerW, innerH, {
85
+ density: 0.5,
86
+ labelArea: 0,
87
+ minRadius: 6
88
+ });
89
+
90
+ // Resolve placement per bubble (outside-labelled bubbles want the
91
+ // perimeter so their labels reach the open band along the walls).
92
+ const placements = data.map((d, i) => {
93
+ const pref = d.labelPlacement ?? labelPlacement;
94
+ const diameter = (radii[i] ?? 0) * 2;
95
+ if (pref === 'auto') return diameter >= autoInsideMinSize ? 'inside' : 'outside';
96
+ return pref;
97
+ });
98
+ const perimeter = placements.map(p => p === 'outside');
99
+ const nodes = simulateCluster(radii, {
100
+ width,
101
+ height: poolHeight,
102
+ gap,
103
+ iterations,
104
+ insetX,
105
+ insetY,
106
+ perimeter
107
+ });
108
+ const labelBoxes = data.map((d, i) => {
109
+ if (placements[i] !== 'outside') return null;
110
+ const v = toText(d.display, String(d.value));
111
+ const l = toText(d.label);
112
+ if (!v && !l) return null;
113
+ return labelEstimates[i] ?? estimateLabelBox(v, l);
114
+ });
115
+ const directions = chooseLabelDirections(nodes, labelBoxes, {
116
+ width,
117
+ height: poolHeight,
118
+ gap: resolvedLabelGap
119
+ });
120
+ const bubbles = data.map((datum, index) => {
121
+ const node = nodes[index];
122
+ const r = radii[index] ?? 0;
123
+ return {
124
+ datum,
125
+ index,
126
+ appearance: datum.appearance ?? appearanceCycle[index % appearanceCycle.length] ?? 'Primary',
127
+ placement: placements[index] ?? 'inside',
128
+ direction: directions[index] ?? 'bottom',
129
+ left: node.x - r,
130
+ top: node.y - r,
131
+ size: r * 2
132
+ };
133
+ });
134
+ return {
135
+ bubbles,
136
+ poolHeight
137
+ };
138
+ }, [data, width, height, minBubbleSize, maxBubbleSize, gap, resolvedLabelGap, labelPlacement, autoInsideMinSize, appearanceCycle, iterations]);
139
+ return /*#__PURE__*/_jsx(View, {
140
+ style: [styles.container, {
141
+ height: layout.poolHeight
142
+ }, style],
143
+ onLayout: handleLayout,
144
+ accessibilityRole: "image",
145
+ accessibilityLabel: accessibilityLabel,
146
+ children: layout.bubbles.map(b => /*#__PURE__*/_jsx(View, {
147
+ style: [styles.bubble, {
148
+ left: b.left,
149
+ top: b.top
150
+ }],
151
+ pointerEvents: "box-none",
152
+ children: /*#__PURE__*/_jsx(ClusterBubble, {
153
+ size: b.size,
154
+ value: b.datum.display ?? String(b.datum.value),
155
+ label: b.datum.label,
156
+ appearance: b.appearance,
157
+ labelPlacement: b.placement,
158
+ labelDirection: b.direction,
159
+ labelGap: resolvedLabelGap,
160
+ autoInsideMinSize: autoInsideMinSize,
161
+ modes: modes,
162
+ ...(b.datum.color ? {
163
+ color: b.datum.color
164
+ } : null),
165
+ ...(b.datum.accessibilityLabel ? {
166
+ accessibilityLabel: b.datum.accessibilityLabel
167
+ } : null),
168
+ ...(onBubblePress ? {
169
+ onPress: () => onBubblePress(b.datum, b.index)
170
+ } : null)
171
+ })
172
+ }, b.datum.id ?? b.index))
173
+ });
174
+ }
175
+ const styles = StyleSheet.create({
176
+ container: {
177
+ width: '100%',
178
+ position: 'relative',
179
+ overflow: 'hidden'
180
+ },
181
+ bubble: {
182
+ position: 'absolute'
183
+ }
184
+ });
185
+ export default BubbleChart;