jfs-components 0.0.84 → 0.0.85

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 (43) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/commonjs/components/AppBar/AppBar.js +36 -22
  3. package/lib/commonjs/components/AreaLineChart/AreaLineChart.js +866 -0
  4. package/lib/commonjs/components/AreaLineChart/chartMath.js +252 -0
  5. package/lib/commonjs/components/Attached/Attached.js +34 -4
  6. package/lib/commonjs/components/BubbleChart/BubbleChart.js +191 -0
  7. package/lib/commonjs/components/BubbleChart/bubblePacking.js +378 -0
  8. package/lib/commonjs/components/ClusterBubble/ClusterBubble.js +272 -0
  9. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +7 -1
  10. package/lib/commonjs/components/index.js +27 -0
  11. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  12. package/lib/commonjs/icons/registry.js +1 -1
  13. package/lib/module/components/AppBar/AppBar.js +36 -22
  14. package/lib/module/components/AreaLineChart/AreaLineChart.js +859 -0
  15. package/lib/module/components/AreaLineChart/chartMath.js +242 -0
  16. package/lib/module/components/Attached/Attached.js +34 -4
  17. package/lib/module/components/BubbleChart/BubbleChart.js +185 -0
  18. package/lib/module/components/BubbleChart/bubblePacking.js +370 -0
  19. package/lib/module/components/ClusterBubble/ClusterBubble.js +267 -0
  20. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +7 -1
  21. package/lib/module/components/index.js +3 -0
  22. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  23. package/lib/module/icons/registry.js +1 -1
  24. package/lib/typescript/src/components/AreaLineChart/AreaLineChart.d.ts +212 -0
  25. package/lib/typescript/src/components/AreaLineChart/chartMath.d.ts +90 -0
  26. package/lib/typescript/src/components/BubbleChart/BubbleChart.d.ts +81 -0
  27. package/lib/typescript/src/components/BubbleChart/bubblePacking.d.ts +83 -0
  28. package/lib/typescript/src/components/ClusterBubble/ClusterBubble.d.ts +76 -0
  29. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +7 -1
  30. package/lib/typescript/src/components/index.d.ts +3 -0
  31. package/lib/typescript/src/icons/registry.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/src/components/AppBar/AppBar.tsx +37 -24
  34. package/src/components/AreaLineChart/AreaLineChart.tsx +1161 -0
  35. package/src/components/AreaLineChart/chartMath.ts +265 -0
  36. package/src/components/Attached/Attached.tsx +36 -5
  37. package/src/components/BubbleChart/BubbleChart.tsx +319 -0
  38. package/src/components/BubbleChart/bubblePacking.ts +397 -0
  39. package/src/components/ClusterBubble/ClusterBubble.tsx +359 -0
  40. package/src/components/MetricLegendItem/MetricLegendItem.tsx +20 -6
  41. package/src/components/index.ts +3 -0
  42. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  43. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,859 @@
1
+ "use strict";
2
+
3
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { PanResponder, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
5
+ import Svg, { Circle, Line, Path } from 'react-native-svg';
6
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
8
+ import { EMPTY_MODES } from '../../utils/react-utils';
9
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem';
10
+ import { buildAreaPath, buildLineSegments, createLinearScale, extent, nearestIndex, niceTicks, resolvePoints } from './chartMath';
11
+
12
+ // --- Public types ---------------------------------------------------------
13
+
14
+ /** A single data point. Bare numbers are also accepted in `data`. */
15
+
16
+ /** One line+area series. Pass one for a single chart, several to overlap. */
17
+
18
+ // --- Internal resolved types ----------------------------------------------
19
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
20
+ const ChartContext = /*#__PURE__*/createContext(null);
21
+
22
+ /** Access the surrounding chart geometry from a decorator/sub-component. */
23
+ export function useChart() {
24
+ const ctx = useContext(ChartContext);
25
+ if (!ctx) {
26
+ throw new Error('AreaLineChart sub-components must be used within <AreaLineChart>');
27
+ }
28
+ return ctx;
29
+ }
30
+
31
+ // --- Helpers ---------------------------------------------------------------
32
+
33
+ const DEFAULT_APPEARANCE_CYCLE = ['Primary', 'Secondary', 'Tertiary', 'Quaternary', 'Quinary', 'Senary'];
34
+ const DEFAULT_INSET = {
35
+ top: 16,
36
+ bottom: 8,
37
+ left: 8,
38
+ right: 8
39
+ };
40
+ const toNumber = (value, fallback) => {
41
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
42
+ if (typeof value === 'string') {
43
+ const parsed = Number(value);
44
+ if (Number.isFinite(parsed)) return parsed;
45
+ }
46
+ return fallback;
47
+ };
48
+ const toFontWeight = (value, fallback) => {
49
+ if (typeof value === 'number') return String(value);
50
+ if (typeof value === 'string') return value;
51
+ return fallback;
52
+ };
53
+ const appearanceFor = index => DEFAULT_APPEARANCE_CYCLE[index % DEFAULT_APPEARANCE_CYCLE.length];
54
+
55
+ /** Resolve a series' strong (line/dot) color via the `dataViz/bg` token. */
56
+ const resolveLineColor = (color, appearance, modes) => {
57
+ if (color) return color;
58
+ return getVariableByName('dataViz/bg', {
59
+ ...modes,
60
+ 'Appearance / DataViz': appearance,
61
+ 'Emphasis / DataViz': 'High'
62
+ }) ?? '#5d00b5';
63
+ };
64
+
65
+ /** Resolve a series' light area-fill color via the `dataViz/bg` token. */
66
+ const resolveAreaColor = (color, lineColor, appearance, modes) => {
67
+ if (color) return color;
68
+ return getVariableByName('dataViz/bg', {
69
+ ...modes,
70
+ 'Appearance / DataViz': appearance,
71
+ 'Emphasis / DataViz': 'Low'
72
+ }) ?? lineColor;
73
+ };
74
+ const defaultFormatY = value => String(value);
75
+ const defaultFormatX = label => String(label);
76
+
77
+ // --- Main component --------------------------------------------------------
78
+
79
+ /**
80
+ * `AreaLineChart` is a lightweight, token-driven area/line chart built
81
+ * entirely on `react-native-svg`. A single `series` renders one filled
82
+ * area with a line on top (plus an optional goal pin); multiple `series`
83
+ * overlap with an automatic legend. It supports smooth/linear curves,
84
+ * dashed "projected" segments, a configurable grid and axes, and a
85
+ * cross-platform crosshair tooltip (hover on web, press-drag on native).
86
+ *
87
+ * The reusable building blocks (`AreaLineChart.Grid`, `.XAxis`, `.YAxis`,
88
+ * `.GoalPin`) read the shared chart geometry through `useChart()`, so you
89
+ * can also compose them manually or add your own SVG decorators as
90
+ * children.
91
+ *
92
+ * @component
93
+ */
94
+ function AreaLineChart({
95
+ series,
96
+ xLabels,
97
+ yMin,
98
+ yMax,
99
+ numberOfTicks = 4,
100
+ curve = 'linear',
101
+ height = 218,
102
+ contentInset,
103
+ showGrid = true,
104
+ showXAxis = true,
105
+ showYAxis = true,
106
+ showLegend = true,
107
+ showDots = false,
108
+ formatX = defaultFormatX,
109
+ formatY = defaultFormatY,
110
+ formatValue,
111
+ goalPin,
112
+ activeIndex: activeIndexProp,
113
+ defaultActiveIndex = null,
114
+ onActiveIndexChange,
115
+ interactive = true,
116
+ modes: propModes = EMPTY_MODES,
117
+ style,
118
+ children,
119
+ accessibilityLabel
120
+ }) {
121
+ const {
122
+ modes: globalModes
123
+ } = useTokens();
124
+ const modes = useMemo(() => ({
125
+ ...globalModes,
126
+ ...propModes
127
+ }), [globalModes, propModes]);
128
+ const inset = useMemo(() => ({
129
+ ...DEFAULT_INSET,
130
+ ...(contentInset || {})
131
+ }), [contentInset]);
132
+
133
+ // Plot width is measured; height is fixed by the prop.
134
+ const [plotWidth, setPlotWidth] = useState(0);
135
+ const handlePlotLayout = useCallback(e => {
136
+ const w = e.nativeEvent.layout.width;
137
+ setPlotWidth(prev => Math.abs(prev - w) > 0.5 ? w : prev);
138
+ }, []);
139
+
140
+ // Active index (controlled or uncontrolled).
141
+ const isControlled = activeIndexProp !== undefined;
142
+ const [uncontrolledActive, setUncontrolledActive] = useState(defaultActiveIndex);
143
+ const activeIndex = isControlled ? activeIndexProp : uncontrolledActive;
144
+ const setActiveIndex = useCallback(index => {
145
+ if (!isControlled) setUncontrolledActive(index);
146
+ onActiveIndexChange?.(index);
147
+ }, [isControlled, onActiveIndexChange]);
148
+
149
+ // Resolve every series (points + colors).
150
+ const resolvedSeries = useMemo(() => {
151
+ return series.map((s, index) => {
152
+ const appearance = s.appearance ?? appearanceFor(index);
153
+ const lineColor = resolveLineColor(s.color, appearance, modes);
154
+ const areaColor = resolveAreaColor(s.areaColor, lineColor, appearance, modes);
155
+ return {
156
+ key: s.key ?? `series-${index}`,
157
+ label: s.label,
158
+ appearance,
159
+ lineColor,
160
+ areaColor,
161
+ showArea: s.showArea !== false,
162
+ showLine: s.showLine !== false,
163
+ points: resolvePoints(s.data)
164
+ };
165
+ });
166
+ }, [series, modes]);
167
+
168
+ // Canonical point count comes from the longest series.
169
+ const count = useMemo(() => resolvedSeries.reduce((max, s) => Math.max(max, s.points.length), 0), [resolvedSeries]);
170
+
171
+ // Domains.
172
+ const xDomain = useMemo(() => {
173
+ let min = Infinity;
174
+ let max = -Infinity;
175
+ for (const s of resolvedSeries) {
176
+ const [lo, hi] = extent(s.points, 'x');
177
+ if (s.points.length) {
178
+ if (lo < min) min = lo;
179
+ if (hi > max) max = hi;
180
+ }
181
+ }
182
+ if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 0];
183
+ return [min, max];
184
+ }, [resolvedSeries]);
185
+ const {
186
+ yDomain,
187
+ yTicks
188
+ } = useMemo(() => {
189
+ let dataMin = Infinity;
190
+ let dataMax = -Infinity;
191
+ for (const s of resolvedSeries) {
192
+ const [lo, hi] = extent(s.points, 'y');
193
+ if (s.points.length) {
194
+ if (lo < dataMin) dataMin = lo;
195
+ if (hi > dataMax) dataMax = hi;
196
+ }
197
+ }
198
+ if (!Number.isFinite(dataMin) || !Number.isFinite(dataMax)) {
199
+ dataMin = 0;
200
+ dataMax = 1;
201
+ }
202
+ const lo = yMin !== undefined ? yMin : Math.min(0, dataMin);
203
+ const hi = yMax !== undefined ? yMax : dataMax;
204
+ const ticks = niceTicks(lo, hi, numberOfTicks);
205
+ const domain = ticks.length >= 2 ? [ticks[0], ticks[ticks.length - 1]] : [lo, hi === lo ? lo + 1 : hi];
206
+ return {
207
+ yDomain: domain,
208
+ yTicks: ticks
209
+ };
210
+ }, [resolvedSeries, yMin, yMax, numberOfTicks]);
211
+
212
+ // Scales.
213
+ const xScale = useMemo(() => createLinearScale(xDomain[0] === xDomain[1] ? [xDomain[0], xDomain[0] + 1] : xDomain, [inset.left, Math.max(inset.left, plotWidth - inset.right)]), [xDomain, inset.left, inset.right, plotWidth]);
214
+ const yScale = useMemo(() => createLinearScale(yDomain, [height - inset.bottom, inset.top]), [yDomain, height, inset.bottom, inset.top]);
215
+
216
+ // Canonical x pixel positions (from the longest series).
217
+ const indexXs = useMemo(() => {
218
+ const base = resolvedSeries.find(s => s.points.length === count);
219
+ if (!base) return [];
220
+ return base.points.map(p => xScale(p.x));
221
+ }, [resolvedSeries, count, xScale]);
222
+ const ctx = useMemo(() => ({
223
+ width: plotWidth,
224
+ height,
225
+ inset,
226
+ xScale,
227
+ yScale,
228
+ yTicks,
229
+ indexXs,
230
+ count,
231
+ series: resolvedSeries,
232
+ curve,
233
+ activeIndex,
234
+ setActiveIndex,
235
+ xLabels,
236
+ formatX,
237
+ formatY,
238
+ showDots,
239
+ modes
240
+ }), [plotWidth, height, inset, xScale, yScale, yTicks, indexXs, count, resolvedSeries, curve, activeIndex, setActiveIndex, xLabels, formatX, formatY, showDots, modes]);
241
+ const isMultiSeries = resolvedSeries.length > 1;
242
+ const resolvedFormatValue = formatValue ?? (v => formatY(v));
243
+ return /*#__PURE__*/_jsx(ChartContext.Provider, {
244
+ value: ctx,
245
+ children: /*#__PURE__*/_jsxs(View, {
246
+ style: [styles.container, style],
247
+ accessibilityRole: "image",
248
+ accessibilityLabel: accessibilityLabel,
249
+ children: [showLegend && isMultiSeries ? /*#__PURE__*/_jsx(ChartLegend, {}) : null, /*#__PURE__*/_jsxs(View, {
250
+ style: styles.body,
251
+ children: [showYAxis ? /*#__PURE__*/_jsx(ChartYAxis, {}) : null, /*#__PURE__*/_jsxs(View, {
252
+ style: styles.plotColumn,
253
+ children: [/*#__PURE__*/_jsx(View, {
254
+ style: [styles.plot, {
255
+ height
256
+ }],
257
+ onLayout: handlePlotLayout,
258
+ children: plotWidth > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
259
+ children: [showGrid ? /*#__PURE__*/_jsx(ChartGrid, {}) : null, /*#__PURE__*/_jsx(ChartSeriesLayer, {}), goalPin ? /*#__PURE__*/_jsx(ChartGoalPin, {
260
+ value: goalPin.value,
261
+ atIndex: goalPin.atIndex,
262
+ seriesIndex: goalPin.seriesIndex
263
+ }) : null, children, interactive ? /*#__PURE__*/_jsx(ChartInteractionLayer, {
264
+ formatValue: resolvedFormatValue,
265
+ series: series
266
+ }) : null]
267
+ }) : null
268
+ }), showXAxis ? /*#__PURE__*/_jsx(ChartXAxis, {}) : null]
269
+ })]
270
+ })]
271
+ })
272
+ });
273
+ }
274
+
275
+ // --- Series layer (areas + lines + static dots) ---------------------------
276
+
277
+ function ChartSeriesLayer() {
278
+ const {
279
+ width,
280
+ height,
281
+ series,
282
+ xScale,
283
+ yScale,
284
+ yDomainBaseline,
285
+ curve,
286
+ showDots
287
+ } = useChartWithBaseline();
288
+ return /*#__PURE__*/_jsxs(Svg, {
289
+ style: StyleSheet.absoluteFill,
290
+ width: width,
291
+ height: height,
292
+ children: [series.map(s => s.showArea && s.points.length ? /*#__PURE__*/_jsx(Path, {
293
+ d: buildAreaPath(toPixelPoints(s.points, xScale, yScale), yDomainBaseline, curve),
294
+ fill: s.areaColor
295
+ }, `area-${s.key}`) : null), series.map(s => {
296
+ if (!s.showLine || s.points.length < 2) return null;
297
+ const pixelPoints = toPixelPoints(s.points, xScale, yScale);
298
+ const segments = buildLineSegments(pixelPoints, curve);
299
+ return segments.map((seg, i) => /*#__PURE__*/_jsx(Path, {
300
+ d: seg.d,
301
+ stroke: s.lineColor,
302
+ strokeWidth: 2,
303
+ fill: "none",
304
+ strokeLinecap: "round",
305
+ strokeLinejoin: "round",
306
+ strokeDasharray: seg.dashed ? '5,4' : undefined
307
+ }, `line-${s.key}-${i}`));
308
+ }), showDots ? series.map(s => s.points.map((p, i) => /*#__PURE__*/_jsx(Circle, {
309
+ cx: xScale(p.x),
310
+ cy: yScale(p.y),
311
+ r: 4,
312
+ fill: s.lineColor
313
+ }, `dot-${s.key}-${i}`))) : null]
314
+ });
315
+ }
316
+
317
+ // --- Grid ------------------------------------------------------------------
318
+
319
+ /** Background grid lines aligned to the y-ticks (horizontal) and x data points (vertical). */
320
+ function ChartGrid({
321
+ direction = 'horizontal',
322
+ stroke = 'rgba(0,0,0,0.08)',
323
+ strokeWidth = 1,
324
+ strokeDasharray
325
+ }) {
326
+ const {
327
+ width,
328
+ height,
329
+ inset,
330
+ xScale,
331
+ yScale,
332
+ yTicks,
333
+ indexXs
334
+ } = useChart();
335
+ const showH = direction === 'horizontal' || direction === 'both';
336
+ const showV = direction === 'vertical' || direction === 'both';
337
+ return /*#__PURE__*/_jsxs(Svg, {
338
+ style: StyleSheet.absoluteFill,
339
+ width: width,
340
+ height: height,
341
+ pointerEvents: "none",
342
+ children: [showH ? yTicks.map(t => {
343
+ const y = yScale(t);
344
+ return /*#__PURE__*/_jsx(Line, {
345
+ x1: inset.left,
346
+ x2: width - inset.right,
347
+ y1: y,
348
+ y2: y,
349
+ stroke: stroke,
350
+ strokeWidth: strokeWidth,
351
+ strokeDasharray: strokeDasharray
352
+ }, `gh-${t}`);
353
+ }) : null, showV ? indexXs.map((x, i) => /*#__PURE__*/_jsx(Line, {
354
+ x1: x,
355
+ x2: x,
356
+ y1: inset.top,
357
+ y2: height - inset.bottom,
358
+ stroke: stroke,
359
+ strokeWidth: strokeWidth,
360
+ strokeDasharray: strokeDasharray
361
+ }, `gv-${i}`)) : null]
362
+ });
363
+ }
364
+
365
+ // --- Y axis ----------------------------------------------------------------
366
+
367
+ /** Y-axis tick labels, vertically positioned to align with the grid. */
368
+ function ChartYAxis({
369
+ showLabels = true,
370
+ showTicks = false,
371
+ tickLength = 4,
372
+ showAxisLine = false,
373
+ formatLabel
374
+ }) {
375
+ const {
376
+ height,
377
+ inset,
378
+ yScale,
379
+ yTicks,
380
+ formatY,
381
+ modes
382
+ } = useChart();
383
+ const typo = useAxisTypography(modes);
384
+ const format = formatLabel ?? formatY;
385
+ const lineHeight = typo.lineHeight;
386
+ return /*#__PURE__*/_jsxs(View, {
387
+ style: {
388
+ height,
389
+ justifyContent: 'flex-start',
390
+ flexDirection: 'row'
391
+ },
392
+ children: [/*#__PURE__*/_jsx(View, {
393
+ style: {
394
+ width: undefined,
395
+ height
396
+ },
397
+ children: yTicks.map(t => {
398
+ const y = yScale(t);
399
+ return /*#__PURE__*/_jsx(View, {
400
+ style: {
401
+ position: 'absolute',
402
+ right: showTicks ? tickLength + 4 : 0,
403
+ top: y - lineHeight / 2,
404
+ flexDirection: 'row',
405
+ alignItems: 'center'
406
+ },
407
+ children: showLabels ? /*#__PURE__*/_jsx(Text, {
408
+ style: typo.style,
409
+ numberOfLines: 1,
410
+ children: format(t)
411
+ }) : null
412
+ }, `yl-${t}`);
413
+ })
414
+ }), showTicks ? /*#__PURE__*/_jsxs(Svg, {
415
+ width: tickLength,
416
+ height: height,
417
+ pointerEvents: "none",
418
+ children: [yTicks.map(t => {
419
+ const y = yScale(t);
420
+ return /*#__PURE__*/_jsx(Line, {
421
+ x1: 0,
422
+ x2: tickLength,
423
+ y1: y,
424
+ y2: y,
425
+ stroke: "rgba(0,0,0,0.2)",
426
+ strokeWidth: 1
427
+ }, `yt-${t}`);
428
+ }), showAxisLine ? /*#__PURE__*/_jsx(Line, {
429
+ x1: tickLength,
430
+ x2: tickLength,
431
+ y1: inset.top,
432
+ y2: height - inset.bottom,
433
+ stroke: "rgba(0,0,0,0.2)",
434
+ strokeWidth: 1
435
+ }) : null]
436
+ }) : null]
437
+ });
438
+ }
439
+
440
+ // --- X axis ----------------------------------------------------------------
441
+
442
+ /** X-axis labels, horizontally positioned to align with the data points. */
443
+ function ChartXAxis({
444
+ showLabels = true,
445
+ showTicks = false,
446
+ tickLength = 4,
447
+ selectable = true,
448
+ formatLabel
449
+ }) {
450
+ const {
451
+ width,
452
+ inset,
453
+ xScale,
454
+ indexXs,
455
+ count,
456
+ xLabels,
457
+ formatX,
458
+ modes,
459
+ activeIndex,
460
+ setActiveIndex
461
+ } = useChart();
462
+ const typo = useAxisTypography(modes);
463
+ const format = formatLabel ?? formatX;
464
+ const activeColor = getVariableByName('dataViz/bg', {
465
+ ...modes,
466
+ 'Appearance / DataViz': 'Primary',
467
+ 'Emphasis / DataViz': 'High'
468
+ }) ?? '#5d00b5';
469
+ const labels = xLabels ?? indexXs.map((_, i) => i);
470
+ const labelCount = labels.length;
471
+ return /*#__PURE__*/_jsxs(View, {
472
+ style: {
473
+ width: '100%',
474
+ height: typo.lineHeight + (showTicks ? tickLength : 0)
475
+ },
476
+ children: [showTicks ? /*#__PURE__*/_jsx(Svg, {
477
+ style: StyleSheet.absoluteFill,
478
+ width: width,
479
+ height: tickLength,
480
+ pointerEvents: "none",
481
+ children: indexXs.map((x, i) => /*#__PURE__*/_jsx(Line, {
482
+ x1: x,
483
+ x2: x,
484
+ y1: 0,
485
+ y2: tickLength,
486
+ stroke: "rgba(0,0,0,0.2)",
487
+ strokeWidth: 1
488
+ }, `xt-${i}`))
489
+ }) : null, showLabels ? /*#__PURE__*/_jsx(View, {
490
+ style: {
491
+ position: 'absolute',
492
+ left: 0,
493
+ right: 0,
494
+ top: showTicks ? tickLength : 0
495
+ },
496
+ children: labels.map((label, i) => {
497
+ // Map a label to its data index (handles fewer labels than points).
498
+ const dataIndex = labelCount === count ? i : Math.round(i / Math.max(1, labelCount - 1) * (count - 1));
499
+ const x = labelCount === count ? indexXs[i] ?? xScale(i) : inset.left + i / Math.max(1, labelCount - 1) * (width - inset.left - inset.right);
500
+ const isActive = activeIndex === dataIndex;
501
+ const content = /*#__PURE__*/_jsx(Text, {
502
+ style: [typo.style, isActive ? {
503
+ color: activeColor,
504
+ fontWeight: '700'
505
+ } : null],
506
+ numberOfLines: 1,
507
+ children: format(label, i)
508
+ });
509
+ return /*#__PURE__*/_jsx(View, {
510
+ style: {
511
+ position: 'absolute',
512
+ left: x,
513
+ transform: [{
514
+ translateX: -50
515
+ }],
516
+ width: 100,
517
+ alignItems: 'center'
518
+ },
519
+ children: selectable ? /*#__PURE__*/_jsx(Pressable, {
520
+ onPress: () => setActiveIndex(isActive ? null : dataIndex),
521
+ hitSlop: 8,
522
+ children: content
523
+ }) : content
524
+ }, `xl-${i}`);
525
+ })
526
+ }) : null]
527
+ });
528
+ }
529
+
530
+ // --- Goal pin --------------------------------------------------------------
531
+
532
+ /** A pill marker anchored to a data point, with a dashed connector to the baseline. */
533
+ function ChartGoalPin({
534
+ value,
535
+ atIndex,
536
+ seriesIndex = 0,
537
+ color,
538
+ textColor
539
+ }) {
540
+ const {
541
+ height,
542
+ inset,
543
+ xScale,
544
+ yScale,
545
+ series,
546
+ count,
547
+ modes
548
+ } = useChart();
549
+ const s = series[seriesIndex] ?? series[0];
550
+ if (!s || s.points.length === 0) return null;
551
+ const index = atIndex ?? count - 1;
552
+ const point = s.points[Math.min(Math.max(0, index), s.points.length - 1)];
553
+ if (!point) return null;
554
+ const x = xScale(point.x);
555
+ const y = yScale(point.y);
556
+ const pinColor = color ?? s.lineColor;
557
+ const pinTextColor = textColor ?? getVariableByName('mode/Grey/2500', modes) ?? '#ffffff';
558
+ const PIN_SIZE = 32;
559
+ return /*#__PURE__*/_jsxs(View, {
560
+ style: StyleSheet.absoluteFill,
561
+ pointerEvents: "none",
562
+ children: [/*#__PURE__*/_jsxs(Svg, {
563
+ style: StyleSheet.absoluteFill,
564
+ width: "100%",
565
+ height: height,
566
+ children: [/*#__PURE__*/_jsx(Line, {
567
+ x1: x,
568
+ x2: x,
569
+ y1: PIN_SIZE / 2,
570
+ y2: height - inset.bottom,
571
+ stroke: pinColor,
572
+ strokeWidth: 1.5,
573
+ strokeDasharray: "4,4"
574
+ }), /*#__PURE__*/_jsx(Circle, {
575
+ cx: x,
576
+ cy: y,
577
+ r: 5,
578
+ fill: pinColor,
579
+ stroke: "#ffffff",
580
+ strokeWidth: 2
581
+ })]
582
+ }), /*#__PURE__*/_jsx(View, {
583
+ style: {
584
+ position: 'absolute',
585
+ left: x - PIN_SIZE / 2,
586
+ top: 0,
587
+ width: PIN_SIZE,
588
+ height: PIN_SIZE,
589
+ borderRadius: 999,
590
+ backgroundColor: pinColor,
591
+ alignItems: 'center',
592
+ justifyContent: 'center',
593
+ paddingHorizontal: 4
594
+ },
595
+ children: /*#__PURE__*/_jsx(Text, {
596
+ style: {
597
+ color: pinTextColor,
598
+ fontSize: 10,
599
+ lineHeight: 13,
600
+ textAlign: 'center'
601
+ },
602
+ children: value
603
+ })
604
+ })]
605
+ });
606
+ }
607
+
608
+ // --- Interaction layer (crosshair + active dots + tooltip) ----------------
609
+
610
+ function ChartInteractionLayer({
611
+ formatValue,
612
+ series: rawSeries
613
+ }) {
614
+ const {
615
+ width,
616
+ height,
617
+ inset,
618
+ xScale,
619
+ yScale,
620
+ indexXs,
621
+ series,
622
+ activeIndex,
623
+ setActiveIndex,
624
+ modes
625
+ } = useChart();
626
+ const viewRef = useRef(null);
627
+ const updateFromX = useCallback(locationX => {
628
+ const idx = nearestIndex(indexXs, locationX);
629
+ if (idx >= 0) setActiveIndex(idx);
630
+ }, [indexXs, setActiveIndex]);
631
+ const panResponder = useMemo(() => PanResponder.create({
632
+ onStartShouldSetPanResponder: () => true,
633
+ onMoveShouldSetPanResponder: () => true,
634
+ onPanResponderGrant: e => updateFromX(e.nativeEvent.locationX),
635
+ onPanResponderMove: e => updateFromX(e.nativeEvent.locationX)
636
+ }), [updateFromX]);
637
+
638
+ // Web-only hover support (no button pressed) via DOM listeners.
639
+ useEffect(() => {
640
+ if (Platform.OS !== 'web') return;
641
+ const node = viewRef.current;
642
+ if (!node) return;
643
+ const onMove = ev => {
644
+ const rect = node.getBoundingClientRect();
645
+ updateFromX(ev.clientX - rect.left);
646
+ };
647
+ const onLeave = () => setActiveIndex(null);
648
+ node.addEventListener('mousemove', onMove);
649
+ node.addEventListener('mouseleave', onLeave);
650
+ return () => {
651
+ node.removeEventListener('mousemove', onMove);
652
+ node.removeEventListener('mouseleave', onLeave);
653
+ };
654
+ }, [updateFromX, setActiveIndex]);
655
+ const hasActive = activeIndex !== null && activeIndex >= 0;
656
+ const activeX = hasActive ? indexXs[activeIndex] : 0;
657
+ const tooltipItems = useMemo(() => {
658
+ if (!hasActive) return [];
659
+ return series.map((s, sIndex) => {
660
+ const point = s.points[activeIndex];
661
+ if (!point) return null;
662
+ return {
663
+ key: String(s.key),
664
+ label: s.label ?? `Series ${sIndex + 1}`,
665
+ value: formatValue(point.y, rawSeries[sIndex]),
666
+ color: s.lineColor,
667
+ y: yScale(point.y)
668
+ };
669
+ }).filter(Boolean);
670
+ }, [hasActive, series, activeIndex, formatValue, rawSeries, yScale]);
671
+ return /*#__PURE__*/_jsxs(_Fragment, {
672
+ children: [/*#__PURE__*/_jsx(View, {
673
+ ref: viewRef,
674
+ style: StyleSheet.absoluteFill,
675
+ ...panResponder.panHandlers
676
+ }), hasActive ? /*#__PURE__*/_jsxs(_Fragment, {
677
+ children: [/*#__PURE__*/_jsxs(Svg, {
678
+ style: StyleSheet.absoluteFill,
679
+ width: width,
680
+ height: height,
681
+ pointerEvents: "none",
682
+ children: [/*#__PURE__*/_jsx(Line, {
683
+ x1: activeX,
684
+ x2: activeX,
685
+ y1: inset.top,
686
+ y2: height - inset.bottom,
687
+ stroke: "#0f0d0a",
688
+ strokeWidth: 1
689
+ }), tooltipItems.map(item => /*#__PURE__*/_jsx(Circle, {
690
+ cx: activeX,
691
+ cy: item.y,
692
+ r: 6,
693
+ fill: item.color,
694
+ stroke: "#ffffff",
695
+ strokeWidth: 2
696
+ }, `active-${item.key}`))]
697
+ }), /*#__PURE__*/_jsx(ChartTooltip, {
698
+ x: activeX,
699
+ width: width,
700
+ items: tooltipItems,
701
+ modes: modes
702
+ })]
703
+ }) : null]
704
+ });
705
+ }
706
+
707
+ // --- Inline tooltip --------------------------------------------------------
708
+
709
+ function ChartTooltip({
710
+ x,
711
+ width,
712
+ items,
713
+ modes
714
+ }) {
715
+ const [size, setSize] = useState(null);
716
+ const bg = getVariableByName('tooltip/background', modes) ?? '#0f0d0a';
717
+ const paddingH = toNumber(getVariableByName('tooltip/padding/horizontal', modes), 12);
718
+ const paddingV = toNumber(getVariableByName('tooltip/padding/vertical', modes), 8);
719
+ const radius = toNumber(getVariableByName('radius', modes), 8);
720
+ const labelColor = getVariableByName('tooltip/label/color', modes) ?? '#ffffff';
721
+ if (items.length === 0) return null;
722
+
723
+ // Horizontally clamp so the box stays inside the plot.
724
+ const boxW = size?.width ?? 0;
725
+ const screenPad = 4;
726
+ let left = x - boxW / 2;
727
+ if (boxW > 0) {
728
+ left = Math.max(screenPad, Math.min(left, width - boxW - screenPad));
729
+ }
730
+ return /*#__PURE__*/_jsx(View, {
731
+ pointerEvents: "none",
732
+ onLayout: e => setSize({
733
+ width: e.nativeEvent.layout.width,
734
+ height: e.nativeEvent.layout.height
735
+ }),
736
+ style: {
737
+ position: 'absolute',
738
+ top: 0,
739
+ left,
740
+ backgroundColor: bg,
741
+ borderRadius: radius,
742
+ paddingHorizontal: paddingH,
743
+ paddingVertical: paddingV,
744
+ gap: 4,
745
+ opacity: size ? 1 : 0,
746
+ shadowColor: '#000',
747
+ shadowOffset: {
748
+ width: 0,
749
+ height: 2
750
+ },
751
+ shadowOpacity: 0.25,
752
+ shadowRadius: 3.84,
753
+ elevation: 5
754
+ },
755
+ children: items.map(item => /*#__PURE__*/_jsx(MetricLegendItem, {
756
+ label: item.label,
757
+ value: item.value,
758
+ indicatorColor: item.color,
759
+ modes: modes,
760
+ labelStyle: {
761
+ color: labelColor
762
+ },
763
+ valueStyle: {
764
+ color: labelColor,
765
+ fontWeight: '700'
766
+ },
767
+ style: {
768
+ gap: 8
769
+ }
770
+ }, item.key))
771
+ });
772
+ }
773
+
774
+ // --- Legend ----------------------------------------------------------------
775
+
776
+ function ChartLegend() {
777
+ const {
778
+ series,
779
+ modes
780
+ } = useChart();
781
+ return /*#__PURE__*/_jsx(View, {
782
+ style: styles.legend,
783
+ children: series.map((s, i) => /*#__PURE__*/_jsx(MetricLegendItem, {
784
+ label: s.label ?? `Series ${i + 1}`,
785
+ indicatorColor: s.lineColor,
786
+ indicatorShape: s.showArea === false ? 'line' : 'dot',
787
+ modes: modes
788
+ }, `legend-${s.key}`))
789
+ });
790
+ }
791
+
792
+ // --- Shared hooks / utils --------------------------------------------------
793
+
794
+ /** Resolve `axisItem/*` typography tokens into a memoized text style. */
795
+ function useAxisTypography(modes) {
796
+ return useMemo(() => {
797
+ const color = getVariableByName('axisItem/color', modes) ?? '#000000';
798
+ const fontFamily = getVariableByName('axisItem/fontFamily', modes) ?? 'JioType Var';
799
+ const fontSize = toNumber(getVariableByName('axisItem/fontSize', modes), 12);
800
+ const lineHeight = toNumber(getVariableByName('axisItem/lineHeight', modes), 16);
801
+ const fontWeight = toFontWeight(getVariableByName('axisItem/fontWeight', modes), '400');
802
+ return {
803
+ lineHeight,
804
+ style: {
805
+ color,
806
+ fontFamily,
807
+ fontSize,
808
+ lineHeight,
809
+ fontWeight
810
+ }
811
+ };
812
+ }, [modes]);
813
+ }
814
+
815
+ /** Like `useChart` but also exposes the area baseline pixel-y. */
816
+ function useChartWithBaseline() {
817
+ const ctx = useChart();
818
+ const yDomainBaseline = ctx.yScale(ctx.yScale.domain[0]);
819
+ return {
820
+ ...ctx,
821
+ yDomainBaseline
822
+ };
823
+ }
824
+ const toPixelPoints = (points, xScale, yScale) => points.map(p => ({
825
+ x: xScale(p.x),
826
+ y: yScale(p.y),
827
+ projected: p.projected
828
+ }));
829
+ const styles = StyleSheet.create({
830
+ container: {
831
+ width: '100%',
832
+ gap: 8
833
+ },
834
+ body: {
835
+ flexDirection: 'row',
836
+ gap: 8
837
+ },
838
+ plotColumn: {
839
+ flex: 1,
840
+ minWidth: 0,
841
+ gap: 8
842
+ },
843
+ plot: {
844
+ width: '100%',
845
+ position: 'relative'
846
+ },
847
+ legend: {
848
+ flexDirection: 'row',
849
+ flexWrap: 'wrap',
850
+ gap: 12
851
+ }
852
+ });
853
+
854
+ // Attach reusable sub-components.
855
+ AreaLineChart.Grid = ChartGrid;
856
+ AreaLineChart.XAxis = ChartXAxis;
857
+ AreaLineChart.YAxis = ChartYAxis;
858
+ AreaLineChart.GoalPin = ChartGoalPin;
859
+ export default AreaLineChart;