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