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.
Files changed (52) hide show
  1. package/lib/module/components/animated-badge/index.js +451 -0
  2. package/lib/module/components/animated-badge/index.js.map +1 -0
  3. package/lib/module/components/bottom-sheet/index.js +17 -6
  4. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  5. package/lib/module/components/bubble-chart/index.js +557 -41
  6. package/lib/module/components/bubble-chart/index.js.map +1 -1
  7. package/lib/module/components/feedback/index.js +474 -0
  8. package/lib/module/components/feedback/index.js.map +1 -0
  9. package/lib/module/components/scroll-blur/index.js +363 -0
  10. package/lib/module/components/scroll-blur/index.js.map +1 -0
  11. package/lib/module/components/scroll-canvas/index.js +2 -0
  12. package/lib/module/components/scroll-canvas/index.js.map +1 -1
  13. package/lib/module/components/scroll-text/index.js +3 -0
  14. package/lib/module/components/scroll-text/index.js.map +1 -1
  15. package/lib/module/components/search-bar/index.js +390 -45
  16. package/lib/module/components/search-bar/index.js.map +1 -1
  17. package/lib/module/hooks/use-keyboard-avoidance.js +21 -1
  18. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  19. package/lib/module/hooks/use-reveal-progress.js +21 -1
  20. package/lib/module/hooks/use-reveal-progress.js.map +1 -1
  21. package/lib/module/index.js +3 -0
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/typescript/src/components/animated-badge/index.d.ts +224 -0
  24. package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -0
  25. package/lib/typescript/src/components/bottom-sheet/index.d.ts +1 -15
  26. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  27. package/lib/typescript/src/components/bubble-chart/index.d.ts +150 -7
  28. package/lib/typescript/src/components/bubble-chart/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/feedback/index.d.ts +156 -0
  30. package/lib/typescript/src/components/feedback/index.d.ts.map +1 -0
  31. package/lib/typescript/src/components/scroll-blur/index.d.ts +113 -0
  32. package/lib/typescript/src/components/scroll-blur/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/search-bar/index.d.ts +142 -7
  35. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
  36. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  37. package/lib/typescript/src/hooks/use-reveal-progress.d.ts +6 -27
  38. package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -1
  39. package/lib/typescript/src/index.d.ts +5 -2
  40. package/lib/typescript/src/index.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/components/animated-badge/index.tsx +513 -0
  43. package/src/components/bottom-sheet/index.tsx +23 -8
  44. package/src/components/bubble-chart/index.tsx +664 -46
  45. package/src/components/feedback/index.tsx +557 -0
  46. package/src/components/scroll-blur/index.tsx +466 -0
  47. package/src/components/scroll-canvas/index.tsx +2 -1
  48. package/src/components/scroll-text/index.tsx +3 -3
  49. package/src/components/search-bar/index.tsx +471 -41
  50. package/src/hooks/use-keyboard-avoidance.ts +24 -1
  51. package/src/hooks/use-reveal-progress.ts +30 -1
  52. 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 = 2;
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
- const hasYAxis = useMemo(() => {
261
- let found = false;
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) && child.type.axis === 'y') {
264
- found = true;
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 found;
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(hasYAxis ? Y_AXIS_WIDTH : PADDING.left, reach)
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 = 5,
524
- columns = 5,
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
- const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
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 = 2,
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__*/_jsx(View, {
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: labels.map(label => /*#__PURE__*/_jsx(Text, {
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 * label.key - AXIS_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - AXIS_LABEL_WIDTH)),
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: label.text
874
- }, label.key))
1344
+ children: tick.text
1345
+ }, tick.key))]
875
1346
  });
876
1347
  }
877
1348
  BubbleChartXAxis.displayName = 'BubbleChart.XAxis';
878
1349
  BubbleChartXAxis.layer = 'overlay';
879
- /** Value labels down the side, one per grid line. Reserves its own gutter. */
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 = 2,
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
- return /*#__PURE__*/_jsx(View, {
903
- pointerEvents: "none",
904
- style: {
905
- position: 'absolute',
906
- left: 0,
907
- // Centred on the grid line each label names: the strip is lifted half a
908
- // label and grown by a whole one, so `justify-between` lands the text's
909
- // middle on the line rather than its top edge on the first.
910
- top: plot.top - AXIS_LABEL_HEIGHT / 2,
911
- height: plot.height + AXIS_LABEL_HEIGHT,
912
- width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
913
- },
914
- className: cn('items-end justify-between', className),
915
- children: labels.map(label => /*#__PURE__*/_jsx(Text, {
916
- size: "xs",
917
- muted: true,
918
- numberOfLines: 1,
919
- children: label.text
920
- }, label.key))
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,