panelui-native 0.87.0 → 0.88.1
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/lib/module/components/animated-badge/index.js +451 -0
- package/lib/module/components/animated-badge/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +17 -6
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/bubble-chart/index.js +557 -41
- package/lib/module/components/bubble-chart/index.js.map +1 -1
- package/lib/module/components/feedback/index.js +474 -0
- package/lib/module/components/feedback/index.js.map +1 -0
- package/lib/module/components/scroll-blur/index.js +363 -0
- package/lib/module/components/scroll-blur/index.js.map +1 -0
- package/lib/module/components/scroll-canvas/index.js +2 -0
- package/lib/module/components/scroll-canvas/index.js.map +1 -1
- package/lib/module/components/scroll-text/index.js +3 -0
- package/lib/module/components/scroll-text/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +390 -45
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +21 -1
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-reveal-progress.js +21 -1
- package/lib/module/hooks/use-reveal-progress.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +224 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +1 -15
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bubble-chart/index.d.ts +150 -7
- package/lib/typescript/src/components/bubble-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/feedback/index.d.ts +156 -0
- package/lib/typescript/src/components/feedback/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-blur/index.d.ts +113 -0
- package/lib/typescript/src/components/scroll-blur/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts +142 -7
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts +6 -27
- package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +513 -0
- package/src/components/bottom-sheet/index.tsx +23 -8
- package/src/components/bubble-chart/index.tsx +664 -46
- package/src/components/feedback/index.tsx +557 -0
- package/src/components/scroll-blur/index.tsx +466 -0
- package/src/components/scroll-canvas/index.tsx +2 -1
- package/src/components/scroll-text/index.tsx +3 -3
- package/src/components/search-bar/index.tsx +471 -41
- package/src/hooks/use-keyboard-avoidance.ts +24 -1
- package/src/hooks/use-reveal-progress.ts +30 -1
- package/src/index.ts +27 -0
|
@@ -57,6 +57,7 @@ import { cn } from "../../utils/cn.js";
|
|
|
57
57
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
58
58
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
59
59
|
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
60
|
+
const AnimatedLine = Animated.createAnimatedComponent(SvgLine);
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* How much of the reveal is spent handing out the bubbles' start times. The
|
|
@@ -131,7 +132,22 @@ const HIT_RADIUS = 22;
|
|
|
131
132
|
const PALETTE_SIZE = 5;
|
|
132
133
|
|
|
133
134
|
/** Steps each axis is rounded out to. Matches the labels an axis draws. */
|
|
134
|
-
const AXIS_STEPS =
|
|
135
|
+
const AXIS_STEPS = 4;
|
|
136
|
+
|
|
137
|
+
/** Room under the numbers for an axis's own name, when it is given one. */
|
|
138
|
+
const AXIS_TITLE_HEIGHT = 18;
|
|
139
|
+
|
|
140
|
+
/** Gap between a quadrant's caption and the corner it is written in. */
|
|
141
|
+
const QUADRANT_LABEL_INSET = 6;
|
|
142
|
+
|
|
143
|
+
/** Column the size key's values are written in, beside its circles. */
|
|
144
|
+
const SIZE_KEY_LABEL_WIDTH = 44;
|
|
145
|
+
|
|
146
|
+
/** Gap between the size key's circles and the values naming them. */
|
|
147
|
+
const SIZE_KEY_GAP = 6;
|
|
148
|
+
|
|
149
|
+
/** Room beside the numbers for the y axis's name, which is turned on its side. */
|
|
150
|
+
const AXIS_TITLE_WIDTH = 18;
|
|
135
151
|
|
|
136
152
|
/** One bubble, resolved back to the row it came from. */
|
|
137
153
|
|
|
@@ -257,14 +273,33 @@ const BubbleChartRoot = /*#__PURE__*/forwardRef(function BubbleChartRoot({
|
|
|
257
273
|
const chart4 = useSeriesColor(undefined, 4);
|
|
258
274
|
const chart5 = useSeriesColor(undefined, 5);
|
|
259
275
|
const palette = useMemo(() => [chart1, chart2, chart3, chart4, chart5], [chart1, chart2, chart3, chart4, chart5]);
|
|
260
|
-
|
|
261
|
-
|
|
276
|
+
|
|
277
|
+
/*
|
|
278
|
+
* What the axes are going to need before anything has been laid out. The
|
|
279
|
+
* gutter for the y labels and the strip under the x ones are the plot's
|
|
280
|
+
* padding, so they have to be known here rather than by the parts that draw
|
|
281
|
+
* them — a part that reserved its own room would be positioned against a
|
|
282
|
+
* plot that had already been sized without it.
|
|
283
|
+
*/
|
|
284
|
+
const axes = useMemo(() => {
|
|
285
|
+
let y = false;
|
|
286
|
+
let yTitle = false;
|
|
287
|
+
let xTitle = false;
|
|
262
288
|
Children.forEach(children, child => {
|
|
263
|
-
if (/*#__PURE__*/isValidElement(child)
|
|
264
|
-
|
|
289
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
290
|
+
const axis = child.type.axis;
|
|
291
|
+
const labelled = Boolean(child.props.label);
|
|
292
|
+
if (axis === 'y') {
|
|
293
|
+
y = true;
|
|
294
|
+
if (labelled) yTitle = true;
|
|
265
295
|
}
|
|
296
|
+
if (axis === 'x' && labelled) xTitle = true;
|
|
266
297
|
});
|
|
267
|
-
return
|
|
298
|
+
return {
|
|
299
|
+
y,
|
|
300
|
+
yTitle,
|
|
301
|
+
xTitle
|
|
302
|
+
};
|
|
268
303
|
}, [children]);
|
|
269
304
|
|
|
270
305
|
/*
|
|
@@ -278,8 +313,8 @@ const BubbleChartRoot = /*#__PURE__*/forwardRef(function BubbleChartRoot({
|
|
|
278
313
|
const pad = {
|
|
279
314
|
top: Math.max(PADDING.top, reach),
|
|
280
315
|
right: Math.max(PADDING.right, reach),
|
|
281
|
-
bottom: Math.max(PADDING.bottom, reach),
|
|
282
|
-
left: Math.max(
|
|
316
|
+
bottom: Math.max(PADDING.bottom, reach) + (axes.xTitle ? AXIS_TITLE_HEIGHT : 0),
|
|
317
|
+
left: Math.max(axes.y ? Y_AXIS_WIDTH + (axes.yTitle ? AXIS_TITLE_WIDTH : 0) : PADDING.left, reach)
|
|
283
318
|
};
|
|
284
319
|
const plot = {
|
|
285
320
|
left: pad.left,
|
|
@@ -451,6 +486,8 @@ const BubbleChartRoot = /*#__PURE__*/forwardRef(function BubbleChartRoot({
|
|
|
451
486
|
yMax,
|
|
452
487
|
xExtent: extents.x,
|
|
453
488
|
yExtent: extents.y,
|
|
489
|
+
sizeExtent,
|
|
490
|
+
sizeRange,
|
|
454
491
|
reveal,
|
|
455
492
|
activeIndex,
|
|
456
493
|
activeIndexJS,
|
|
@@ -458,7 +495,7 @@ const BubbleChartRoot = /*#__PURE__*/forwardRef(function BubbleChartRoot({
|
|
|
458
495
|
}),
|
|
459
496
|
// `plot` is rebuilt every render from `size`, so it is compared by value.
|
|
460
497
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
461
|
-
[data, xDataKey, yDataKey, labelKey, plot.width, plot.height, plot.left, plot.top, status, bubbles, xMin, xMax, yMin, yMax, extents, reveal, activeIndex, activeIndexJS, setActivePoint]);
|
|
498
|
+
[data, xDataKey, yDataKey, labelKey, plot.width, plot.height, plot.left, plot.top, status, bubbles, xMin, xMax, yMin, yMax, extents, sizeExtent, sizeRange, reveal, activeIndex, activeIndexJS, setActivePoint]);
|
|
462
499
|
const {
|
|
463
500
|
svg,
|
|
464
501
|
overlay,
|
|
@@ -520,8 +557,9 @@ BubbleChartRoot.displayName = 'BubbleChart';
|
|
|
520
557
|
|
|
521
558
|
/** Reference lines both ways, so a bubble can be placed against two numbers. */
|
|
522
559
|
function BubbleChartGrid({
|
|
523
|
-
rows =
|
|
524
|
-
columns =
|
|
560
|
+
rows = 8,
|
|
561
|
+
columns = 8,
|
|
562
|
+
dashArray = '4,6',
|
|
525
563
|
color,
|
|
526
564
|
opacity = 1
|
|
527
565
|
}) {
|
|
@@ -529,7 +567,9 @@ function BubbleChartGrid({
|
|
|
529
567
|
plot
|
|
530
568
|
} = useChart('BubbleChart.Grid');
|
|
531
569
|
const token = useCSSVariable('--color-border');
|
|
532
|
-
|
|
570
|
+
// The fallback is grey rather than black: it stands in when the theme cannot
|
|
571
|
+
// be read, and a black hairline is invisible on a dark background.
|
|
572
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
533
573
|
const horizontals = Array.from({
|
|
534
574
|
length: rows + 1
|
|
535
575
|
}, (_unused, i) => i / rows);
|
|
@@ -544,19 +584,169 @@ function BubbleChartGrid({
|
|
|
544
584
|
y1: plot.top + plot.height * fraction,
|
|
545
585
|
y2: plot.top + plot.height * fraction,
|
|
546
586
|
stroke: stroke,
|
|
547
|
-
strokeWidth: 1
|
|
587
|
+
strokeWidth: 1,
|
|
588
|
+
strokeDasharray: dashArray
|
|
548
589
|
}, `h${fraction}`)), verticals.map(fraction => /*#__PURE__*/_jsx(SvgLine, {
|
|
549
590
|
x1: plot.left + plot.width * fraction,
|
|
550
591
|
x2: plot.left + plot.width * fraction,
|
|
551
592
|
y1: plot.top,
|
|
552
593
|
y2: plot.top + plot.height,
|
|
553
594
|
stroke: stroke,
|
|
554
|
-
strokeWidth: 1
|
|
595
|
+
strokeWidth: 1,
|
|
596
|
+
strokeDasharray: dashArray
|
|
555
597
|
}, `v${fraction}`))]
|
|
556
598
|
});
|
|
557
599
|
}
|
|
558
600
|
BubbleChartGrid.displayName = 'BubbleChart.Grid';
|
|
559
601
|
BubbleChartGrid.layer = 'svg';
|
|
602
|
+
/**
|
|
603
|
+
* The straight line that fits the cloud best, drawn across the plot.
|
|
604
|
+
*
|
|
605
|
+
* It is dashed and drawn under the circles, because it is not data — it is a
|
|
606
|
+
* summary of the data, and a solid rule through the middle of a field of
|
|
607
|
+
* bubbles reads as a value somebody plotted.
|
|
608
|
+
*
|
|
609
|
+
* The fit is least squares on the raw values, so it moves with the data rather
|
|
610
|
+
* than with the frame: resizing the chart never changes the line's meaning.
|
|
611
|
+
* Fewer than two bubbles, or every bubble on one vertical, has no line to draw
|
|
612
|
+
* and none is drawn.
|
|
613
|
+
*/
|
|
614
|
+
function BubbleChartTrend({
|
|
615
|
+
onFit,
|
|
616
|
+
color,
|
|
617
|
+
strokeWidth = 1.5,
|
|
618
|
+
dashArray = '6,5',
|
|
619
|
+
opacity = 0.7
|
|
620
|
+
}) {
|
|
621
|
+
const {
|
|
622
|
+
bubbles,
|
|
623
|
+
plot,
|
|
624
|
+
status,
|
|
625
|
+
xMin,
|
|
626
|
+
xMax,
|
|
627
|
+
yMin,
|
|
628
|
+
yMax,
|
|
629
|
+
reveal
|
|
630
|
+
} = useChart('BubbleChart.Trend');
|
|
631
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
632
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.8)');
|
|
633
|
+
const fit = useMemo(() => {
|
|
634
|
+
const n = bubbles.length;
|
|
635
|
+
if (n < 2) return null;
|
|
636
|
+
let sumX = 0;
|
|
637
|
+
let sumY = 0;
|
|
638
|
+
for (const bubble of bubbles) {
|
|
639
|
+
sumX += bubble.x;
|
|
640
|
+
sumY += bubble.y;
|
|
641
|
+
}
|
|
642
|
+
const meanX = sumX / n;
|
|
643
|
+
const meanY = sumY / n;
|
|
644
|
+
let sxy = 0;
|
|
645
|
+
let sxx = 0;
|
|
646
|
+
let syy = 0;
|
|
647
|
+
for (const bubble of bubbles) {
|
|
648
|
+
const dx = bubble.x - meanX;
|
|
649
|
+
const dy = bubble.y - meanY;
|
|
650
|
+
sxy += dx * dy;
|
|
651
|
+
sxx += dx * dx;
|
|
652
|
+
syy += dy * dy;
|
|
653
|
+
}
|
|
654
|
+
// Every bubble on one vertical: the best fit is that vertical, which has no
|
|
655
|
+
// slope and nothing useful to draw.
|
|
656
|
+
if (sxx === 0) return null;
|
|
657
|
+
const slope = sxy / sxx;
|
|
658
|
+
const denominator = Math.sqrt(sxx * syy);
|
|
659
|
+
return {
|
|
660
|
+
slope,
|
|
661
|
+
intercept: meanY - slope * meanX,
|
|
662
|
+
r: denominator === 0 ? 0 : Math.abs(sxy / denominator)
|
|
663
|
+
};
|
|
664
|
+
}, [bubbles]);
|
|
665
|
+
const fitRef = useRef(onFit);
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
fitRef.current = onFit;
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
/*
|
|
671
|
+
* Reported when the numbers change, not when the object does.
|
|
672
|
+
*
|
|
673
|
+
* `bubbles` is rebuilt whenever any of its inputs changes identity, and
|
|
674
|
+
* `sizeRange={[14, 30]}` written at a call site is a new array every render —
|
|
675
|
+
* so the fit is a new object every render even when the data has not moved.
|
|
676
|
+
* Handing that to a caller who puts it in state is a render loop, and the
|
|
677
|
+
* caller has no way to see that coming.
|
|
678
|
+
*/
|
|
679
|
+
const reported = useRef(null);
|
|
680
|
+
useEffect(() => {
|
|
681
|
+
if (!fit) return;
|
|
682
|
+
const last = reported.current;
|
|
683
|
+
if (last && last.slope === fit.slope && last.intercept === fit.intercept && last.r === fit.r) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
reported.current = fit;
|
|
687
|
+
fitRef.current?.(fit);
|
|
688
|
+
}, [fit]);
|
|
689
|
+
const slope = fit?.slope ?? 0;
|
|
690
|
+
const intercept = fit?.intercept ?? 0;
|
|
691
|
+
const animatedProps = useAnimatedProps(() => {
|
|
692
|
+
const x0 = xMin.value;
|
|
693
|
+
const x1 = xMax.value;
|
|
694
|
+
const lowY = yMin.value;
|
|
695
|
+
const highY = yMax.value;
|
|
696
|
+
|
|
697
|
+
/*
|
|
698
|
+
* Solved in data space and then clipped there, rather than drawn across the
|
|
699
|
+
* plot and clipped by the frame: a line that leaves the top of the chart
|
|
700
|
+
* has to stop where it leaves it, and the x of that point is only knowable
|
|
701
|
+
* from the equation.
|
|
702
|
+
*/
|
|
703
|
+
let ax = x0;
|
|
704
|
+
let bx = x1;
|
|
705
|
+
if (slope !== 0) {
|
|
706
|
+
const atLow = (lowY - intercept) / slope;
|
|
707
|
+
const atHigh = (highY - intercept) / slope;
|
|
708
|
+
const enter = Math.min(atLow, atHigh);
|
|
709
|
+
const exit = Math.max(atLow, atHigh);
|
|
710
|
+
ax = Math.max(ax, enter);
|
|
711
|
+
bx = Math.min(bx, exit);
|
|
712
|
+
}
|
|
713
|
+
if (bx < ax) {
|
|
714
|
+
// The line never crosses the visible box.
|
|
715
|
+
return {
|
|
716
|
+
x1: 0,
|
|
717
|
+
x2: 0,
|
|
718
|
+
y1: 0,
|
|
719
|
+
y2: 0,
|
|
720
|
+
opacity: 0
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Drawn out from the middle as the bubbles land, so the line arrives with
|
|
725
|
+
// the field rather than being there waiting for it.
|
|
726
|
+
const grown = Math.max(0, Math.min(1, reveal.value));
|
|
727
|
+
const midpoint = (ax + bx) / 2;
|
|
728
|
+
const half = (bx - ax) / 2 * grown;
|
|
729
|
+
const startX = midpoint - half;
|
|
730
|
+
const endX = midpoint + half;
|
|
731
|
+
return {
|
|
732
|
+
x1: xAt(startX, plot, x0, x1),
|
|
733
|
+
x2: xAt(endX, plot, x0, x1),
|
|
734
|
+
y1: yOf(intercept + slope * startX, plot, lowY, highY),
|
|
735
|
+
y2: yOf(intercept + slope * endX, plot, lowY, highY),
|
|
736
|
+
opacity: opacity * grown
|
|
737
|
+
};
|
|
738
|
+
});
|
|
739
|
+
if (status === 'loading' || !fit) return null;
|
|
740
|
+
return /*#__PURE__*/_jsx(AnimatedLine, {
|
|
741
|
+
animatedProps: animatedProps,
|
|
742
|
+
stroke: stroke,
|
|
743
|
+
strokeWidth: strokeWidth,
|
|
744
|
+
strokeDasharray: dashArray,
|
|
745
|
+
strokeLinecap: "round"
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
BubbleChartTrend.displayName = 'BubbleChart.Trend';
|
|
749
|
+
BubbleChartTrend.layer = 'svg';
|
|
560
750
|
/** The circles. */
|
|
561
751
|
function BubbleChartBubbles({
|
|
562
752
|
opacity = 0.9,
|
|
@@ -767,6 +957,274 @@ function BubbleChartLabels({
|
|
|
767
957
|
}
|
|
768
958
|
BubbleChartLabels.displayName = 'BubbleChart.Labels';
|
|
769
959
|
BubbleChartLabels.layer = 'overlay';
|
|
960
|
+
/**
|
|
961
|
+
* A crosshair splitting the plot into four, with a name for each corner.
|
|
962
|
+
*
|
|
963
|
+
* A field of bubbles is usually read as four groups rather than as a cloud —
|
|
964
|
+
* which of these is doing well on both counts, which on neither — and without
|
|
965
|
+
* a divider the reader draws that line by eye, in a different place each time.
|
|
966
|
+
* Putting it on the chart makes it one line everybody sees.
|
|
967
|
+
*
|
|
968
|
+
* It stands at the mean of each axis by default, because that is the split the
|
|
969
|
+
* data itself argues for. Pass `x` and `y` for a target, a budget or last
|
|
970
|
+
* year's number — a threshold somebody decided rather than one the data
|
|
971
|
+
* produced.
|
|
972
|
+
*
|
|
973
|
+
* The tint marks the two corners a reading usually ends at. Turn it off where
|
|
974
|
+
* all four corners matter equally.
|
|
975
|
+
*/
|
|
976
|
+
function BubbleChartQuadrants({
|
|
977
|
+
x,
|
|
978
|
+
y,
|
|
979
|
+
labels,
|
|
980
|
+
tint = true,
|
|
981
|
+
color,
|
|
982
|
+
className
|
|
983
|
+
}) {
|
|
984
|
+
const {
|
|
985
|
+
bubbles,
|
|
986
|
+
plot,
|
|
987
|
+
status,
|
|
988
|
+
xMin,
|
|
989
|
+
xMax,
|
|
990
|
+
yMin,
|
|
991
|
+
yMax
|
|
992
|
+
} = useChart('BubbleChart.Quadrants');
|
|
993
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
994
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.8)');
|
|
995
|
+
const centre = useMemo(() => {
|
|
996
|
+
if (!bubbles.length) return null;
|
|
997
|
+
let sumX = 0;
|
|
998
|
+
let sumY = 0;
|
|
999
|
+
for (const bubble of bubbles) {
|
|
1000
|
+
sumX += bubble.x;
|
|
1001
|
+
sumY += bubble.y;
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
x: x ?? sumX / bubbles.length,
|
|
1005
|
+
y: y ?? sumY / bubbles.length
|
|
1006
|
+
};
|
|
1007
|
+
}, [bubbles, x, y]);
|
|
1008
|
+
const atX = centre?.x ?? 0;
|
|
1009
|
+
const atY = centre?.y ?? 0;
|
|
1010
|
+
const verticalStyle = useAnimatedStyle(() => ({
|
|
1011
|
+
left: xAt(atX, plot, xMin.value, xMax.value)
|
|
1012
|
+
}));
|
|
1013
|
+
const horizontalStyle = useAnimatedStyle(() => ({
|
|
1014
|
+
top: yOf(atY, plot, yMin.value, yMax.value)
|
|
1015
|
+
}));
|
|
1016
|
+
/*
|
|
1017
|
+
* Two rectangles rather than four: the pair that is tinted is the pair the
|
|
1018
|
+
* reader is being pointed at, and shading all four would only be a checked
|
|
1019
|
+
* background.
|
|
1020
|
+
*/
|
|
1021
|
+
const highStyle = useAnimatedStyle(() => {
|
|
1022
|
+
const cx = xAt(atX, plot, xMin.value, xMax.value);
|
|
1023
|
+
const cy = yOf(atY, plot, yMin.value, yMax.value);
|
|
1024
|
+
return {
|
|
1025
|
+
left: cx,
|
|
1026
|
+
top: plot.top,
|
|
1027
|
+
width: Math.max(plot.left + plot.width - cx, 0),
|
|
1028
|
+
height: Math.max(cy - plot.top, 0)
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
const lowStyle = useAnimatedStyle(() => {
|
|
1032
|
+
const cx = xAt(atX, plot, xMin.value, xMax.value);
|
|
1033
|
+
const cy = yOf(atY, plot, yMin.value, yMax.value);
|
|
1034
|
+
return {
|
|
1035
|
+
left: plot.left,
|
|
1036
|
+
top: cy,
|
|
1037
|
+
width: Math.max(cx - plot.left, 0),
|
|
1038
|
+
height: Math.max(plot.top + plot.height - cy, 0)
|
|
1039
|
+
};
|
|
1040
|
+
});
|
|
1041
|
+
if (status === 'loading' || !centre) return null;
|
|
1042
|
+
const corner = {
|
|
1043
|
+
position: 'absolute',
|
|
1044
|
+
width: plot.width / 2 - QUADRANT_LABEL_INSET
|
|
1045
|
+
};
|
|
1046
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1047
|
+
pointerEvents: "none",
|
|
1048
|
+
style: {
|
|
1049
|
+
position: 'absolute',
|
|
1050
|
+
inset: 0
|
|
1051
|
+
},
|
|
1052
|
+
className: cn(className),
|
|
1053
|
+
children: [tint ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
1054
|
+
children: [/*#__PURE__*/_jsx(Animated.View, {
|
|
1055
|
+
style: [{
|
|
1056
|
+
position: 'absolute'
|
|
1057
|
+
}, highStyle],
|
|
1058
|
+
className: "bg-foreground/[0.04]"
|
|
1059
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
1060
|
+
style: [{
|
|
1061
|
+
position: 'absolute'
|
|
1062
|
+
}, lowStyle],
|
|
1063
|
+
className: "bg-foreground/[0.04]"
|
|
1064
|
+
})]
|
|
1065
|
+
}) : null, /*#__PURE__*/_jsx(Animated.View, {
|
|
1066
|
+
style: [{
|
|
1067
|
+
position: 'absolute',
|
|
1068
|
+
top: plot.top,
|
|
1069
|
+
height: plot.height,
|
|
1070
|
+
width: 1,
|
|
1071
|
+
backgroundColor: stroke,
|
|
1072
|
+
opacity: 0.4
|
|
1073
|
+
}, verticalStyle]
|
|
1074
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
1075
|
+
style: [{
|
|
1076
|
+
position: 'absolute',
|
|
1077
|
+
left: plot.left,
|
|
1078
|
+
width: plot.width,
|
|
1079
|
+
height: 1,
|
|
1080
|
+
backgroundColor: stroke,
|
|
1081
|
+
opacity: 0.4
|
|
1082
|
+
}, horizontalStyle]
|
|
1083
|
+
}), labels?.topLeft ? /*#__PURE__*/_jsx(Text, {
|
|
1084
|
+
size: "xs",
|
|
1085
|
+
muted: true,
|
|
1086
|
+
numberOfLines: 1,
|
|
1087
|
+
style: {
|
|
1088
|
+
...corner,
|
|
1089
|
+
left: plot.left + QUADRANT_LABEL_INSET,
|
|
1090
|
+
top: plot.top + QUADRANT_LABEL_INSET
|
|
1091
|
+
},
|
|
1092
|
+
children: labels.topLeft
|
|
1093
|
+
}) : null, labels?.topRight ? /*#__PURE__*/_jsx(Text, {
|
|
1094
|
+
size: "xs",
|
|
1095
|
+
muted: true,
|
|
1096
|
+
numberOfLines: 1,
|
|
1097
|
+
style: {
|
|
1098
|
+
...corner,
|
|
1099
|
+
left: plot.left + plot.width / 2,
|
|
1100
|
+
top: plot.top + QUADRANT_LABEL_INSET,
|
|
1101
|
+
textAlign: 'right'
|
|
1102
|
+
},
|
|
1103
|
+
children: labels.topRight
|
|
1104
|
+
}) : null, labels?.bottomLeft ? /*#__PURE__*/_jsx(Text, {
|
|
1105
|
+
size: "xs",
|
|
1106
|
+
muted: true,
|
|
1107
|
+
numberOfLines: 1,
|
|
1108
|
+
style: {
|
|
1109
|
+
...corner,
|
|
1110
|
+
left: plot.left + QUADRANT_LABEL_INSET,
|
|
1111
|
+
top: plot.top + plot.height - QUADRANT_LABEL_INSET - AXIS_LABEL_HEIGHT
|
|
1112
|
+
},
|
|
1113
|
+
children: labels.bottomLeft
|
|
1114
|
+
}) : null, labels?.bottomRight ? /*#__PURE__*/_jsx(Text, {
|
|
1115
|
+
size: "xs",
|
|
1116
|
+
muted: true,
|
|
1117
|
+
numberOfLines: 1,
|
|
1118
|
+
style: {
|
|
1119
|
+
...corner,
|
|
1120
|
+
left: plot.left + plot.width / 2,
|
|
1121
|
+
top: plot.top + plot.height - QUADRANT_LABEL_INSET - AXIS_LABEL_HEIGHT,
|
|
1122
|
+
textAlign: 'right'
|
|
1123
|
+
},
|
|
1124
|
+
children: labels.bottomRight
|
|
1125
|
+
}) : null]
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
BubbleChartQuadrants.displayName = 'BubbleChart.Quadrants';
|
|
1129
|
+
BubbleChartQuadrants.layer = 'overlay';
|
|
1130
|
+
/**
|
|
1131
|
+
* Three nested circles saying what a bubble's area is worth.
|
|
1132
|
+
*
|
|
1133
|
+
* A bubble chart's third quantity is the one it cannot state: position can be
|
|
1134
|
+
* read off the axes, but area has no axis, so a reader can see that one circle
|
|
1135
|
+
* is bigger than another and has no way to know by how much. This is the only
|
|
1136
|
+
* part of the chart that answers that.
|
|
1137
|
+
*
|
|
1138
|
+
* Nested and sharing a baseline, which is how a difference in area is compared
|
|
1139
|
+
* — three circles in a row are three sizes, three circles inside one another
|
|
1140
|
+
* are one scale.
|
|
1141
|
+
*
|
|
1142
|
+
* It needs a `sizeKey` on the chart. Without one every bubble is the same size
|
|
1143
|
+
* and there is no scale to key.
|
|
1144
|
+
*/
|
|
1145
|
+
function BubbleChartSizeKey({
|
|
1146
|
+
placement = 'bottom-right',
|
|
1147
|
+
format,
|
|
1148
|
+
label,
|
|
1149
|
+
className
|
|
1150
|
+
}) {
|
|
1151
|
+
const {
|
|
1152
|
+
plot,
|
|
1153
|
+
status,
|
|
1154
|
+
sizeExtent,
|
|
1155
|
+
sizeRange
|
|
1156
|
+
} = useChart('BubbleChart.SizeKey');
|
|
1157
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
1158
|
+
const stroke = typeof token === 'string' ? token : 'rgba(128,128,128,0.8)';
|
|
1159
|
+
const steps = useMemo(() => {
|
|
1160
|
+
if (!sizeExtent) return null;
|
|
1161
|
+
const [min, max] = sizeExtent;
|
|
1162
|
+
const middle = (min + max) / 2;
|
|
1163
|
+
// Largest first, so the smallest is drawn last and stays visible inside it.
|
|
1164
|
+
return [max, middle, min].map(value => ({
|
|
1165
|
+
value,
|
|
1166
|
+
r: bubbleRadius(value, sizeExtent, sizeRange)
|
|
1167
|
+
}));
|
|
1168
|
+
}, [sizeExtent, sizeRange]);
|
|
1169
|
+
if (status === 'loading' || !steps) return null;
|
|
1170
|
+
const outer = steps[0].r;
|
|
1171
|
+
const width = outer * 2 + SIZE_KEY_LABEL_WIDTH;
|
|
1172
|
+
const height = outer * 2 + (label ? AXIS_LABEL_HEIGHT : 0);
|
|
1173
|
+
const top = placement.startsWith('top') ? plot.top : plot.top + plot.height - height;
|
|
1174
|
+
const left = placement.endsWith('left') ? plot.left : plot.left + plot.width - width;
|
|
1175
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1176
|
+
pointerEvents: "none",
|
|
1177
|
+
style: {
|
|
1178
|
+
position: 'absolute',
|
|
1179
|
+
left,
|
|
1180
|
+
top,
|
|
1181
|
+
width,
|
|
1182
|
+
height
|
|
1183
|
+
},
|
|
1184
|
+
className: cn(className),
|
|
1185
|
+
children: [label ? /*#__PURE__*/_jsx(Text, {
|
|
1186
|
+
size: "xs",
|
|
1187
|
+
muted: true,
|
|
1188
|
+
numberOfLines: 1,
|
|
1189
|
+
children: label
|
|
1190
|
+
}) : null, /*#__PURE__*/_jsxs(View, {
|
|
1191
|
+
style: {
|
|
1192
|
+
height: outer * 2,
|
|
1193
|
+
width
|
|
1194
|
+
},
|
|
1195
|
+
children: [steps.map(step => /*#__PURE__*/_jsx(View, {
|
|
1196
|
+
style: {
|
|
1197
|
+
position: 'absolute',
|
|
1198
|
+
// Shared bottom edge and shared centre line: the circles nest
|
|
1199
|
+
// rather than stack, so the areas are laid over one another.
|
|
1200
|
+
bottom: 0,
|
|
1201
|
+
left: outer - step.r,
|
|
1202
|
+
width: step.r * 2,
|
|
1203
|
+
height: step.r * 2,
|
|
1204
|
+
borderRadius: step.r,
|
|
1205
|
+
borderWidth: 1,
|
|
1206
|
+
borderColor: stroke,
|
|
1207
|
+
opacity: 0.6
|
|
1208
|
+
}
|
|
1209
|
+
}, step.value)), steps.map(step => /*#__PURE__*/_jsx(Text, {
|
|
1210
|
+
size: "xs",
|
|
1211
|
+
muted: true,
|
|
1212
|
+
numberOfLines: 1,
|
|
1213
|
+
style: {
|
|
1214
|
+
position: 'absolute',
|
|
1215
|
+
left: outer * 2 + SIZE_KEY_GAP,
|
|
1216
|
+
// Level with the top of the circle it names, which is the only
|
|
1217
|
+
// edge the three do not share.
|
|
1218
|
+
top: outer * 2 - step.r * 2 - AXIS_LABEL_HEIGHT / 2,
|
|
1219
|
+
width: SIZE_KEY_LABEL_WIDTH - SIZE_KEY_GAP
|
|
1220
|
+
},
|
|
1221
|
+
children: format ? format(step.value) : compactNumber(step.value)
|
|
1222
|
+
}, `v${step.value}`))]
|
|
1223
|
+
})]
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
BubbleChartSizeKey.displayName = 'BubbleChart.SizeKey';
|
|
1227
|
+
BubbleChartSizeKey.layer = 'overlay';
|
|
770
1228
|
function BubbleLabel({
|
|
771
1229
|
bubble,
|
|
772
1230
|
text,
|
|
@@ -827,8 +1285,9 @@ function BubbleLabel({
|
|
|
827
1285
|
* rows. There is no bubble for a label to sit under.
|
|
828
1286
|
*/
|
|
829
1287
|
function BubbleChartXAxis({
|
|
830
|
-
ticks =
|
|
1288
|
+
ticks = 4,
|
|
831
1289
|
format,
|
|
1290
|
+
label,
|
|
832
1291
|
className
|
|
833
1292
|
}) {
|
|
834
1293
|
const {
|
|
@@ -848,38 +1307,60 @@ function BubbleChartXAxis({
|
|
|
848
1307
|
};
|
|
849
1308
|
});
|
|
850
1309
|
}, [xExtent, ticks, format]);
|
|
851
|
-
return /*#__PURE__*/
|
|
1310
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
852
1311
|
style: {
|
|
853
1312
|
position: 'absolute',
|
|
854
1313
|
inset: 0,
|
|
855
1314
|
pointerEvents: 'none'
|
|
856
1315
|
},
|
|
857
1316
|
className: cn(className),
|
|
858
|
-
children:
|
|
1317
|
+
children: [label ? /*#__PURE__*/_jsx(Text, {
|
|
859
1318
|
size: "xs",
|
|
860
1319
|
muted: true,
|
|
861
1320
|
numberOfLines: 1,
|
|
862
1321
|
style: {
|
|
863
1322
|
position: 'absolute',
|
|
864
1323
|
bottom: 0,
|
|
1324
|
+
left: plot.left,
|
|
1325
|
+
width: plot.width,
|
|
1326
|
+
textAlign: 'center'
|
|
1327
|
+
},
|
|
1328
|
+
children: label
|
|
1329
|
+
}) : null, labels.map(tick => /*#__PURE__*/_jsx(Text, {
|
|
1330
|
+
size: "xs",
|
|
1331
|
+
muted: true,
|
|
1332
|
+
numberOfLines: 1,
|
|
1333
|
+
style: {
|
|
1334
|
+
position: 'absolute',
|
|
1335
|
+
bottom: label ? AXIS_TITLE_HEIGHT : 0,
|
|
865
1336
|
// Centred on its tick, then held inside the chart. The first and
|
|
866
1337
|
// last ticks sit on the plot's own edges, so a box centred on them
|
|
867
1338
|
// hangs half its width off the side — the clamp slides those two
|
|
868
1339
|
// back in rather than letting the numbers leave the frame.
|
|
869
|
-
left: Math.max(0, Math.min(plot.left + plot.width / ticks *
|
|
1340
|
+
left: Math.max(0, Math.min(plot.left + plot.width / ticks * tick.key - AXIS_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - AXIS_LABEL_WIDTH)),
|
|
870
1341
|
width: AXIS_LABEL_WIDTH,
|
|
871
1342
|
textAlign: 'center'
|
|
872
1343
|
},
|
|
873
|
-
children:
|
|
874
|
-
},
|
|
1344
|
+
children: tick.text
|
|
1345
|
+
}, tick.key))]
|
|
875
1346
|
});
|
|
876
1347
|
}
|
|
877
1348
|
BubbleChartXAxis.displayName = 'BubbleChart.XAxis';
|
|
878
1349
|
BubbleChartXAxis.layer = 'overlay';
|
|
879
|
-
|
|
1350
|
+
// Read by the root, which has to leave room under the numbers before it lays
|
|
1351
|
+
// the plot out.
|
|
1352
|
+
BubbleChartXAxis.axis = 'x';
|
|
1353
|
+
/**
|
|
1354
|
+
* Value labels down the side, evenly over the axis, and the gutter they sit in.
|
|
1355
|
+
*
|
|
1356
|
+
* They land on every second line of the default grid rather than on all of
|
|
1357
|
+
* them: a number beside every line of a grid fine enough to read against is a
|
|
1358
|
+
* column of numbers, and the reader stops seeing the chart.
|
|
1359
|
+
*/
|
|
880
1360
|
function BubbleChartYAxis({
|
|
881
|
-
ticks =
|
|
1361
|
+
ticks = 4,
|
|
882
1362
|
format,
|
|
1363
|
+
label,
|
|
883
1364
|
className
|
|
884
1365
|
}) {
|
|
885
1366
|
const {
|
|
@@ -899,25 +1380,57 @@ function BubbleChartYAxis({
|
|
|
899
1380
|
};
|
|
900
1381
|
});
|
|
901
1382
|
}, [yExtent, ticks, format]);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1383
|
+
const titleWidth = label ? AXIS_TITLE_WIDTH : 0;
|
|
1384
|
+
return /*#__PURE__*/_jsxs(_Fragment, {
|
|
1385
|
+
children: [label ?
|
|
1386
|
+
/*#__PURE__*/
|
|
1387
|
+
/*
|
|
1388
|
+
* Turned on its side, which is the only way a word fits in a gutter
|
|
1389
|
+
* sized for numbers. The box is laid out as tall as the plot and then
|
|
1390
|
+
* rotated about its own centre, so the text runs the length of the axis
|
|
1391
|
+
* it names rather than of whatever it happens to say.
|
|
1392
|
+
*/
|
|
1393
|
+
_jsx(View, {
|
|
1394
|
+
pointerEvents: "none",
|
|
1395
|
+
style: {
|
|
1396
|
+
position: 'absolute',
|
|
1397
|
+
left: titleWidth / 2 - plot.height / 2,
|
|
1398
|
+
top: plot.top + plot.height / 2 - AXIS_LABEL_HEIGHT / 2,
|
|
1399
|
+
width: plot.height,
|
|
1400
|
+
height: AXIS_LABEL_HEIGHT,
|
|
1401
|
+
transform: [{
|
|
1402
|
+
rotate: '-90deg'
|
|
1403
|
+
}]
|
|
1404
|
+
},
|
|
1405
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
1406
|
+
size: "xs",
|
|
1407
|
+
muted: true,
|
|
1408
|
+
numberOfLines: 1,
|
|
1409
|
+
style: {
|
|
1410
|
+
textAlign: 'center'
|
|
1411
|
+
},
|
|
1412
|
+
children: label
|
|
1413
|
+
})
|
|
1414
|
+
}) : null, /*#__PURE__*/_jsx(View, {
|
|
1415
|
+
pointerEvents: "none",
|
|
1416
|
+
style: {
|
|
1417
|
+
position: 'absolute',
|
|
1418
|
+
left: titleWidth,
|
|
1419
|
+
// Centred on the grid line each label names: the strip is lifted half
|
|
1420
|
+
// a label and grown by a whole one, so `justify-between` lands the
|
|
1421
|
+
// text's middle on the line rather than its top edge on the first.
|
|
1422
|
+
top: plot.top - AXIS_LABEL_HEIGHT / 2,
|
|
1423
|
+
height: plot.height + AXIS_LABEL_HEIGHT,
|
|
1424
|
+
width: Math.max(plot.left - Y_AXIS_GUTTER - titleWidth, 0)
|
|
1425
|
+
},
|
|
1426
|
+
className: cn('items-end justify-between', className),
|
|
1427
|
+
children: labels.map(tick => /*#__PURE__*/_jsx(Text, {
|
|
1428
|
+
size: "xs",
|
|
1429
|
+
muted: true,
|
|
1430
|
+
numberOfLines: 1,
|
|
1431
|
+
children: tick.text
|
|
1432
|
+
}, tick.key))
|
|
1433
|
+
})]
|
|
921
1434
|
});
|
|
922
1435
|
}
|
|
923
1436
|
BubbleChartYAxis.displayName = 'BubbleChart.YAxis';
|
|
@@ -1255,8 +1768,11 @@ BubbleChartHeader.layer = 'header';
|
|
|
1255
1768
|
export const BubbleChart = Object.assign(BubbleChartRoot, {
|
|
1256
1769
|
Header: BubbleChartHeader,
|
|
1257
1770
|
Grid: BubbleChartGrid,
|
|
1771
|
+
Quadrants: BubbleChartQuadrants,
|
|
1772
|
+
Trend: BubbleChartTrend,
|
|
1258
1773
|
Bubbles: BubbleChartBubbles,
|
|
1259
1774
|
Labels: BubbleChartLabels,
|
|
1775
|
+
SizeKey: BubbleChartSizeKey,
|
|
1260
1776
|
Skeleton: BubbleChartSkeleton,
|
|
1261
1777
|
XAxis: BubbleChartXAxis,
|
|
1262
1778
|
YAxis: BubbleChartYAxis,
|