panelui-native 0.87.0 → 0.88.0
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-dialog/index.js +474 -0
- package/lib/module/components/feedback-dialog/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-dialog/index.d.ts +156 -0
- package/lib/typescript/src/components/feedback-dialog/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-dialog/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
|
@@ -88,6 +88,7 @@ import { cn } from '../../utils/cn';
|
|
|
88
88
|
|
|
89
89
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
90
90
|
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
91
|
+
const AnimatedLine = Animated.createAnimatedComponent(SvgLine);
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* How much of the reveal is spent handing out the bubbles' start times. The
|
|
@@ -156,7 +157,22 @@ const HIT_RADIUS = 22;
|
|
|
156
157
|
const PALETTE_SIZE = 5;
|
|
157
158
|
|
|
158
159
|
/** Steps each axis is rounded out to. Matches the labels an axis draws. */
|
|
159
|
-
const AXIS_STEPS =
|
|
160
|
+
const AXIS_STEPS = 4;
|
|
161
|
+
|
|
162
|
+
/** Room under the numbers for an axis's own name, when it is given one. */
|
|
163
|
+
const AXIS_TITLE_HEIGHT = 18;
|
|
164
|
+
|
|
165
|
+
/** Gap between a quadrant's caption and the corner it is written in. */
|
|
166
|
+
const QUADRANT_LABEL_INSET = 6;
|
|
167
|
+
|
|
168
|
+
/** Column the size key's values are written in, beside its circles. */
|
|
169
|
+
const SIZE_KEY_LABEL_WIDTH = 44;
|
|
170
|
+
|
|
171
|
+
/** Gap between the size key's circles and the values naming them. */
|
|
172
|
+
const SIZE_KEY_GAP = 6;
|
|
173
|
+
|
|
174
|
+
/** Room beside the numbers for the y axis's name, which is turned on its side. */
|
|
175
|
+
const AXIS_TITLE_WIDTH = 18;
|
|
160
176
|
|
|
161
177
|
type Layer = 'svg' | 'overlay' | 'header' | 'footer';
|
|
162
178
|
|
|
@@ -200,6 +216,10 @@ interface BubbleChartContextValue {
|
|
|
200
216
|
/** The settled domains, for the parts that draw text rather than geometry. */
|
|
201
217
|
xExtent: [number, number];
|
|
202
218
|
yExtent: [number, number];
|
|
219
|
+
/** Lowest and highest value behind the areas, or null without a `sizeKey`. */
|
|
220
|
+
sizeExtent: [number, number] | null;
|
|
221
|
+
/** The radii those two values map onto. */
|
|
222
|
+
sizeRange: [number, number];
|
|
203
223
|
/** 0 to 1 as the bubbles grow in. Shared, so they arrive as one chart. */
|
|
204
224
|
reveal: SharedValue<number>;
|
|
205
225
|
activeIndex: SharedValue<number>;
|
|
@@ -373,14 +393,28 @@ const BubbleChartRoot = forwardRef<BubbleChartHandle, BubbleChartProps>(
|
|
|
373
393
|
[chart1, chart2, chart3, chart4, chart5]
|
|
374
394
|
);
|
|
375
395
|
|
|
376
|
-
|
|
377
|
-
|
|
396
|
+
/*
|
|
397
|
+
* What the axes are going to need before anything has been laid out. The
|
|
398
|
+
* gutter for the y labels and the strip under the x ones are the plot's
|
|
399
|
+
* padding, so they have to be known here rather than by the parts that draw
|
|
400
|
+
* them — a part that reserved its own room would be positioned against a
|
|
401
|
+
* plot that had already been sized without it.
|
|
402
|
+
*/
|
|
403
|
+
const axes = useMemo(() => {
|
|
404
|
+
let y = false;
|
|
405
|
+
let yTitle = false;
|
|
406
|
+
let xTitle = false;
|
|
378
407
|
Children.forEach(children, (child) => {
|
|
379
|
-
if (isValidElement(child)
|
|
380
|
-
|
|
408
|
+
if (!isValidElement(child)) return;
|
|
409
|
+
const axis = (child.type as { axis?: string }).axis;
|
|
410
|
+
const labelled = Boolean((child.props as { label?: string }).label);
|
|
411
|
+
if (axis === 'y') {
|
|
412
|
+
y = true;
|
|
413
|
+
if (labelled) yTitle = true;
|
|
381
414
|
}
|
|
415
|
+
if (axis === 'x' && labelled) xTitle = true;
|
|
382
416
|
});
|
|
383
|
-
return
|
|
417
|
+
return { y, yTitle, xTitle };
|
|
384
418
|
}, [children]);
|
|
385
419
|
|
|
386
420
|
/*
|
|
@@ -394,8 +428,12 @@ const BubbleChartRoot = forwardRef<BubbleChartHandle, BubbleChartProps>(
|
|
|
394
428
|
const pad = {
|
|
395
429
|
top: Math.max(PADDING.top, reach),
|
|
396
430
|
right: Math.max(PADDING.right, reach),
|
|
397
|
-
bottom:
|
|
398
|
-
|
|
431
|
+
bottom:
|
|
432
|
+
Math.max(PADDING.bottom, reach) + (axes.xTitle ? AXIS_TITLE_HEIGHT : 0),
|
|
433
|
+
left: Math.max(
|
|
434
|
+
axes.y ? Y_AXIS_WIDTH + (axes.yTitle ? AXIS_TITLE_WIDTH : 0) : PADDING.left,
|
|
435
|
+
reach
|
|
436
|
+
),
|
|
399
437
|
};
|
|
400
438
|
const plot: Plot = {
|
|
401
439
|
left: pad.left,
|
|
@@ -583,6 +621,8 @@ const BubbleChartRoot = forwardRef<BubbleChartHandle, BubbleChartProps>(
|
|
|
583
621
|
yMax,
|
|
584
622
|
xExtent: extents.x,
|
|
585
623
|
yExtent: extents.y,
|
|
624
|
+
sizeExtent,
|
|
625
|
+
sizeRange,
|
|
586
626
|
reveal,
|
|
587
627
|
activeIndex,
|
|
588
628
|
activeIndexJS,
|
|
@@ -606,6 +646,8 @@ const BubbleChartRoot = forwardRef<BubbleChartHandle, BubbleChartProps>(
|
|
|
606
646
|
yMin,
|
|
607
647
|
yMax,
|
|
608
648
|
extents,
|
|
649
|
+
sizeExtent,
|
|
650
|
+
sizeRange,
|
|
609
651
|
reveal,
|
|
610
652
|
activeIndex,
|
|
611
653
|
activeIndexJS,
|
|
@@ -672,29 +714,37 @@ BubbleChartRoot.displayName = 'BubbleChart';
|
|
|
672
714
|
/* -------------------------------------------------------------------------- */
|
|
673
715
|
|
|
674
716
|
export interface BubbleChartGridProps {
|
|
675
|
-
/**
|
|
717
|
+
/**
|
|
718
|
+
* Horizontal rules across the plot.
|
|
719
|
+
*
|
|
720
|
+
* Eight, which is twice the four intervals an axis is divided into by
|
|
721
|
+
* default, so every second line carries a number and the ones between it are
|
|
722
|
+
* halves of a labelled step rather than an unrelated rhythm. Squares this
|
|
723
|
+
* size recede behind the circles; the coarse grid a smaller number draws
|
|
724
|
+
* reads as blocks laid over the plot.
|
|
725
|
+
*/
|
|
676
726
|
rows?: number;
|
|
677
727
|
/** Vertical rules up it. Both axes are measured, so both earn lines. */
|
|
678
728
|
columns?: number;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* read as blocks behind the bubbles rather than as reference lines; a finer
|
|
682
|
-
* one recedes and lets the circles be the thing on the chart.
|
|
683
|
-
*/
|
|
729
|
+
/** Dash pattern for the rules. Pass `undefined` for solid ones. */
|
|
730
|
+
dashArray?: string;
|
|
684
731
|
color?: string;
|
|
685
732
|
opacity?: number;
|
|
686
733
|
}
|
|
687
734
|
|
|
688
735
|
/** Reference lines both ways, so a bubble can be placed against two numbers. */
|
|
689
736
|
function BubbleChartGrid({
|
|
690
|
-
rows =
|
|
691
|
-
columns =
|
|
737
|
+
rows = 8,
|
|
738
|
+
columns = 8,
|
|
739
|
+
dashArray = '4,6',
|
|
692
740
|
color,
|
|
693
741
|
opacity = 1,
|
|
694
742
|
}: BubbleChartGridProps) {
|
|
695
743
|
const { plot } = useChart('BubbleChart.Grid');
|
|
696
744
|
const token = useCSSVariable('--color-border');
|
|
697
|
-
|
|
745
|
+
// The fallback is grey rather than black: it stands in when the theme cannot
|
|
746
|
+
// be read, and a black hairline is invisible on a dark background.
|
|
747
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
698
748
|
|
|
699
749
|
const horizontals = Array.from({ length: rows + 1 }, (_unused, i) => i / rows);
|
|
700
750
|
const verticals = Array.from({ length: columns + 1 }, (_unused, i) => i / columns);
|
|
@@ -710,6 +760,7 @@ function BubbleChartGrid({
|
|
|
710
760
|
y2={plot.top + plot.height * fraction}
|
|
711
761
|
stroke={stroke}
|
|
712
762
|
strokeWidth={1}
|
|
763
|
+
strokeDasharray={dashArray}
|
|
713
764
|
/>
|
|
714
765
|
))}
|
|
715
766
|
{verticals.map((fraction) => (
|
|
@@ -721,6 +772,7 @@ function BubbleChartGrid({
|
|
|
721
772
|
y2={plot.top + plot.height}
|
|
722
773
|
stroke={stroke}
|
|
723
774
|
strokeWidth={1}
|
|
775
|
+
strokeDasharray={dashArray}
|
|
724
776
|
/>
|
|
725
777
|
))}
|
|
726
778
|
</G>
|
|
@@ -729,6 +781,176 @@ function BubbleChartGrid({
|
|
|
729
781
|
BubbleChartGrid.displayName = 'BubbleChart.Grid';
|
|
730
782
|
BubbleChartGrid.layer = 'svg' as Layer;
|
|
731
783
|
|
|
784
|
+
export interface BubbleChartTrendProps {
|
|
785
|
+
/**
|
|
786
|
+
* The line's slope and intercept, and how tightly the cloud sits on it, once
|
|
787
|
+
* they have been computed. `r` runs 0 to 1: 1 is every bubble on the line,
|
|
788
|
+
* 0 is a cloud with no direction at all.
|
|
789
|
+
*
|
|
790
|
+
* Given here rather than left for the caller to work out, because the fit is
|
|
791
|
+
* already being computed to draw the line and doing it twice invites the two
|
|
792
|
+
* answers to disagree.
|
|
793
|
+
*
|
|
794
|
+
* It fires when the numbers change, not on every render that produced the
|
|
795
|
+
* same ones, so putting the fit straight into state is safe.
|
|
796
|
+
*/
|
|
797
|
+
onFit?: (fit: { slope: number; intercept: number; r: number }) => void;
|
|
798
|
+
color?: string;
|
|
799
|
+
strokeWidth?: number;
|
|
800
|
+
/** Dash pattern. Dashed by default: the line is a reading, not a measurement. */
|
|
801
|
+
dashArray?: string;
|
|
802
|
+
opacity?: number;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* The straight line that fits the cloud best, drawn across the plot.
|
|
807
|
+
*
|
|
808
|
+
* It is dashed and drawn under the circles, because it is not data — it is a
|
|
809
|
+
* summary of the data, and a solid rule through the middle of a field of
|
|
810
|
+
* bubbles reads as a value somebody plotted.
|
|
811
|
+
*
|
|
812
|
+
* The fit is least squares on the raw values, so it moves with the data rather
|
|
813
|
+
* than with the frame: resizing the chart never changes the line's meaning.
|
|
814
|
+
* Fewer than two bubbles, or every bubble on one vertical, has no line to draw
|
|
815
|
+
* and none is drawn.
|
|
816
|
+
*/
|
|
817
|
+
function BubbleChartTrend({
|
|
818
|
+
onFit,
|
|
819
|
+
color,
|
|
820
|
+
strokeWidth = 1.5,
|
|
821
|
+
dashArray = '6,5',
|
|
822
|
+
opacity = 0.7,
|
|
823
|
+
}: BubbleChartTrendProps) {
|
|
824
|
+
const { bubbles, plot, status, xMin, xMax, yMin, yMax, reveal } =
|
|
825
|
+
useChart('BubbleChart.Trend');
|
|
826
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
827
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.8)');
|
|
828
|
+
|
|
829
|
+
const fit = useMemo(() => {
|
|
830
|
+
const n = bubbles.length;
|
|
831
|
+
if (n < 2) return null;
|
|
832
|
+
let sumX = 0;
|
|
833
|
+
let sumY = 0;
|
|
834
|
+
for (const bubble of bubbles) {
|
|
835
|
+
sumX += bubble.x;
|
|
836
|
+
sumY += bubble.y;
|
|
837
|
+
}
|
|
838
|
+
const meanX = sumX / n;
|
|
839
|
+
const meanY = sumY / n;
|
|
840
|
+
let sxy = 0;
|
|
841
|
+
let sxx = 0;
|
|
842
|
+
let syy = 0;
|
|
843
|
+
for (const bubble of bubbles) {
|
|
844
|
+
const dx = bubble.x - meanX;
|
|
845
|
+
const dy = bubble.y - meanY;
|
|
846
|
+
sxy += dx * dy;
|
|
847
|
+
sxx += dx * dx;
|
|
848
|
+
syy += dy * dy;
|
|
849
|
+
}
|
|
850
|
+
// Every bubble on one vertical: the best fit is that vertical, which has no
|
|
851
|
+
// slope and nothing useful to draw.
|
|
852
|
+
if (sxx === 0) return null;
|
|
853
|
+
const slope = sxy / sxx;
|
|
854
|
+
const denominator = Math.sqrt(sxx * syy);
|
|
855
|
+
return {
|
|
856
|
+
slope,
|
|
857
|
+
intercept: meanY - slope * meanX,
|
|
858
|
+
r: denominator === 0 ? 0 : Math.abs(sxy / denominator),
|
|
859
|
+
};
|
|
860
|
+
}, [bubbles]);
|
|
861
|
+
|
|
862
|
+
const fitRef = useRef(onFit);
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
fitRef.current = onFit;
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
/*
|
|
868
|
+
* Reported when the numbers change, not when the object does.
|
|
869
|
+
*
|
|
870
|
+
* `bubbles` is rebuilt whenever any of its inputs changes identity, and
|
|
871
|
+
* `sizeRange={[14, 30]}` written at a call site is a new array every render —
|
|
872
|
+
* so the fit is a new object every render even when the data has not moved.
|
|
873
|
+
* Handing that to a caller who puts it in state is a render loop, and the
|
|
874
|
+
* caller has no way to see that coming.
|
|
875
|
+
*/
|
|
876
|
+
const reported = useRef<{ slope: number; intercept: number; r: number } | null>(null);
|
|
877
|
+
useEffect(() => {
|
|
878
|
+
if (!fit) return;
|
|
879
|
+
const last = reported.current;
|
|
880
|
+
if (
|
|
881
|
+
last &&
|
|
882
|
+
last.slope === fit.slope &&
|
|
883
|
+
last.intercept === fit.intercept &&
|
|
884
|
+
last.r === fit.r
|
|
885
|
+
) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
reported.current = fit;
|
|
889
|
+
fitRef.current?.(fit);
|
|
890
|
+
}, [fit]);
|
|
891
|
+
|
|
892
|
+
const slope = fit?.slope ?? 0;
|
|
893
|
+
const intercept = fit?.intercept ?? 0;
|
|
894
|
+
|
|
895
|
+
const animatedProps = useAnimatedProps(() => {
|
|
896
|
+
const x0 = xMin.value;
|
|
897
|
+
const x1 = xMax.value;
|
|
898
|
+
const lowY = yMin.value;
|
|
899
|
+
const highY = yMax.value;
|
|
900
|
+
|
|
901
|
+
/*
|
|
902
|
+
* Solved in data space and then clipped there, rather than drawn across the
|
|
903
|
+
* plot and clipped by the frame: a line that leaves the top of the chart
|
|
904
|
+
* has to stop where it leaves it, and the x of that point is only knowable
|
|
905
|
+
* from the equation.
|
|
906
|
+
*/
|
|
907
|
+
let ax = x0;
|
|
908
|
+
let bx = x1;
|
|
909
|
+
if (slope !== 0) {
|
|
910
|
+
const atLow = (lowY - intercept) / slope;
|
|
911
|
+
const atHigh = (highY - intercept) / slope;
|
|
912
|
+
const enter = Math.min(atLow, atHigh);
|
|
913
|
+
const exit = Math.max(atLow, atHigh);
|
|
914
|
+
ax = Math.max(ax, enter);
|
|
915
|
+
bx = Math.min(bx, exit);
|
|
916
|
+
}
|
|
917
|
+
if (bx < ax) {
|
|
918
|
+
// The line never crosses the visible box.
|
|
919
|
+
return { x1: 0, x2: 0, y1: 0, y2: 0, opacity: 0 };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Drawn out from the middle as the bubbles land, so the line arrives with
|
|
923
|
+
// the field rather than being there waiting for it.
|
|
924
|
+
const grown = Math.max(0, Math.min(1, reveal.value));
|
|
925
|
+
const midpoint = (ax + bx) / 2;
|
|
926
|
+
const half = ((bx - ax) / 2) * grown;
|
|
927
|
+
|
|
928
|
+
const startX = midpoint - half;
|
|
929
|
+
const endX = midpoint + half;
|
|
930
|
+
return {
|
|
931
|
+
x1: xAt(startX, plot, x0, x1),
|
|
932
|
+
x2: xAt(endX, plot, x0, x1),
|
|
933
|
+
y1: yOf(intercept + slope * startX, plot, lowY, highY),
|
|
934
|
+
y2: yOf(intercept + slope * endX, plot, lowY, highY),
|
|
935
|
+
opacity: opacity * grown,
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
if (status === 'loading' || !fit) return null;
|
|
940
|
+
|
|
941
|
+
return (
|
|
942
|
+
<AnimatedLine
|
|
943
|
+
animatedProps={animatedProps}
|
|
944
|
+
stroke={stroke}
|
|
945
|
+
strokeWidth={strokeWidth}
|
|
946
|
+
strokeDasharray={dashArray}
|
|
947
|
+
strokeLinecap="round"
|
|
948
|
+
/>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
BubbleChartTrend.displayName = 'BubbleChart.Trend';
|
|
952
|
+
BubbleChartTrend.layer = 'svg' as Layer;
|
|
953
|
+
|
|
732
954
|
export interface BubbleChartBubblesProps {
|
|
733
955
|
/**
|
|
734
956
|
* Fill opacity. Below 1 by default so that overlapping bubbles read as
|
|
@@ -967,6 +1189,332 @@ function BubbleChartLabels({
|
|
|
967
1189
|
BubbleChartLabels.displayName = 'BubbleChart.Labels';
|
|
968
1190
|
BubbleChartLabels.layer = 'overlay' as Layer;
|
|
969
1191
|
|
|
1192
|
+
export interface BubbleChartQuadrantsProps {
|
|
1193
|
+
/** Where the vertical rule stands. Defaults to the mean of the x values. */
|
|
1194
|
+
x?: number;
|
|
1195
|
+
/** Where the horizontal rule lies. Defaults to the mean of the y values. */
|
|
1196
|
+
y?: number;
|
|
1197
|
+
/** A word for each corner, written in the corner it belongs to. */
|
|
1198
|
+
labels?: {
|
|
1199
|
+
topLeft?: string;
|
|
1200
|
+
topRight?: string;
|
|
1201
|
+
bottomLeft?: string;
|
|
1202
|
+
bottomRight?: string;
|
|
1203
|
+
};
|
|
1204
|
+
/** Tint the high-high and low-low corners. On by default. */
|
|
1205
|
+
tint?: boolean;
|
|
1206
|
+
color?: string;
|
|
1207
|
+
className?: string;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* A crosshair splitting the plot into four, with a name for each corner.
|
|
1212
|
+
*
|
|
1213
|
+
* A field of bubbles is usually read as four groups rather than as a cloud —
|
|
1214
|
+
* which of these is doing well on both counts, which on neither — and without
|
|
1215
|
+
* a divider the reader draws that line by eye, in a different place each time.
|
|
1216
|
+
* Putting it on the chart makes it one line everybody sees.
|
|
1217
|
+
*
|
|
1218
|
+
* It stands at the mean of each axis by default, because that is the split the
|
|
1219
|
+
* data itself argues for. Pass `x` and `y` for a target, a budget or last
|
|
1220
|
+
* year's number — a threshold somebody decided rather than one the data
|
|
1221
|
+
* produced.
|
|
1222
|
+
*
|
|
1223
|
+
* The tint marks the two corners a reading usually ends at. Turn it off where
|
|
1224
|
+
* all four corners matter equally.
|
|
1225
|
+
*/
|
|
1226
|
+
function BubbleChartQuadrants({
|
|
1227
|
+
x,
|
|
1228
|
+
y,
|
|
1229
|
+
labels,
|
|
1230
|
+
tint = true,
|
|
1231
|
+
color,
|
|
1232
|
+
className,
|
|
1233
|
+
}: BubbleChartQuadrantsProps) {
|
|
1234
|
+
const { bubbles, plot, status, xMin, xMax, yMin, yMax } =
|
|
1235
|
+
useChart('BubbleChart.Quadrants');
|
|
1236
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
1237
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.8)');
|
|
1238
|
+
|
|
1239
|
+
const centre = useMemo(() => {
|
|
1240
|
+
if (!bubbles.length) return null;
|
|
1241
|
+
let sumX = 0;
|
|
1242
|
+
let sumY = 0;
|
|
1243
|
+
for (const bubble of bubbles) {
|
|
1244
|
+
sumX += bubble.x;
|
|
1245
|
+
sumY += bubble.y;
|
|
1246
|
+
}
|
|
1247
|
+
return { x: x ?? sumX / bubbles.length, y: y ?? sumY / bubbles.length };
|
|
1248
|
+
}, [bubbles, x, y]);
|
|
1249
|
+
|
|
1250
|
+
const atX = centre?.x ?? 0;
|
|
1251
|
+
const atY = centre?.y ?? 0;
|
|
1252
|
+
|
|
1253
|
+
const verticalStyle = useAnimatedStyle(() => ({
|
|
1254
|
+
left: xAt(atX, plot, xMin.value, xMax.value),
|
|
1255
|
+
}));
|
|
1256
|
+
const horizontalStyle = useAnimatedStyle(() => ({
|
|
1257
|
+
top: yOf(atY, plot, yMin.value, yMax.value),
|
|
1258
|
+
}));
|
|
1259
|
+
/*
|
|
1260
|
+
* Two rectangles rather than four: the pair that is tinted is the pair the
|
|
1261
|
+
* reader is being pointed at, and shading all four would only be a checked
|
|
1262
|
+
* background.
|
|
1263
|
+
*/
|
|
1264
|
+
const highStyle = useAnimatedStyle(() => {
|
|
1265
|
+
const cx = xAt(atX, plot, xMin.value, xMax.value);
|
|
1266
|
+
const cy = yOf(atY, plot, yMin.value, yMax.value);
|
|
1267
|
+
return {
|
|
1268
|
+
left: cx,
|
|
1269
|
+
top: plot.top,
|
|
1270
|
+
width: Math.max(plot.left + plot.width - cx, 0),
|
|
1271
|
+
height: Math.max(cy - plot.top, 0),
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1274
|
+
const lowStyle = useAnimatedStyle(() => {
|
|
1275
|
+
const cx = xAt(atX, plot, xMin.value, xMax.value);
|
|
1276
|
+
const cy = yOf(atY, plot, yMin.value, yMax.value);
|
|
1277
|
+
return {
|
|
1278
|
+
left: plot.left,
|
|
1279
|
+
top: cy,
|
|
1280
|
+
width: Math.max(cx - plot.left, 0),
|
|
1281
|
+
height: Math.max(plot.top + plot.height - cy, 0),
|
|
1282
|
+
};
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (status === 'loading' || !centre) return null;
|
|
1286
|
+
|
|
1287
|
+
const corner = {
|
|
1288
|
+
position: 'absolute' as const,
|
|
1289
|
+
width: plot.width / 2 - QUADRANT_LABEL_INSET,
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
return (
|
|
1293
|
+
<View
|
|
1294
|
+
pointerEvents="none"
|
|
1295
|
+
style={{ position: 'absolute', inset: 0 }}
|
|
1296
|
+
className={cn(className)}
|
|
1297
|
+
>
|
|
1298
|
+
{tint ? (
|
|
1299
|
+
<>
|
|
1300
|
+
<Animated.View
|
|
1301
|
+
style={[{ position: 'absolute' }, highStyle]}
|
|
1302
|
+
className="bg-foreground/[0.04]"
|
|
1303
|
+
/>
|
|
1304
|
+
<Animated.View
|
|
1305
|
+
style={[{ position: 'absolute' }, lowStyle]}
|
|
1306
|
+
className="bg-foreground/[0.04]"
|
|
1307
|
+
/>
|
|
1308
|
+
</>
|
|
1309
|
+
) : null}
|
|
1310
|
+
<Animated.View
|
|
1311
|
+
style={[
|
|
1312
|
+
{
|
|
1313
|
+
position: 'absolute',
|
|
1314
|
+
top: plot.top,
|
|
1315
|
+
height: plot.height,
|
|
1316
|
+
width: 1,
|
|
1317
|
+
backgroundColor: stroke,
|
|
1318
|
+
opacity: 0.4,
|
|
1319
|
+
},
|
|
1320
|
+
verticalStyle,
|
|
1321
|
+
]}
|
|
1322
|
+
/>
|
|
1323
|
+
<Animated.View
|
|
1324
|
+
style={[
|
|
1325
|
+
{
|
|
1326
|
+
position: 'absolute',
|
|
1327
|
+
left: plot.left,
|
|
1328
|
+
width: plot.width,
|
|
1329
|
+
height: 1,
|
|
1330
|
+
backgroundColor: stroke,
|
|
1331
|
+
opacity: 0.4,
|
|
1332
|
+
},
|
|
1333
|
+
horizontalStyle,
|
|
1334
|
+
]}
|
|
1335
|
+
/>
|
|
1336
|
+
{/*
|
|
1337
|
+
Pinned to the plot's corners rather than to the crosshair. A caption
|
|
1338
|
+
names the region, and a region's name belongs at the far end of it —
|
|
1339
|
+
following the rules it would crowd them as the split moved.
|
|
1340
|
+
*/}
|
|
1341
|
+
{labels?.topLeft ? (
|
|
1342
|
+
<Text
|
|
1343
|
+
size="xs"
|
|
1344
|
+
muted
|
|
1345
|
+
numberOfLines={1}
|
|
1346
|
+
style={{
|
|
1347
|
+
...corner,
|
|
1348
|
+
left: plot.left + QUADRANT_LABEL_INSET,
|
|
1349
|
+
top: plot.top + QUADRANT_LABEL_INSET,
|
|
1350
|
+
}}
|
|
1351
|
+
>
|
|
1352
|
+
{labels.topLeft}
|
|
1353
|
+
</Text>
|
|
1354
|
+
) : null}
|
|
1355
|
+
{labels?.topRight ? (
|
|
1356
|
+
<Text
|
|
1357
|
+
size="xs"
|
|
1358
|
+
muted
|
|
1359
|
+
numberOfLines={1}
|
|
1360
|
+
style={{
|
|
1361
|
+
...corner,
|
|
1362
|
+
left: plot.left + plot.width / 2,
|
|
1363
|
+
top: plot.top + QUADRANT_LABEL_INSET,
|
|
1364
|
+
textAlign: 'right',
|
|
1365
|
+
}}
|
|
1366
|
+
>
|
|
1367
|
+
{labels.topRight}
|
|
1368
|
+
</Text>
|
|
1369
|
+
) : null}
|
|
1370
|
+
{labels?.bottomLeft ? (
|
|
1371
|
+
<Text
|
|
1372
|
+
size="xs"
|
|
1373
|
+
muted
|
|
1374
|
+
numberOfLines={1}
|
|
1375
|
+
style={{
|
|
1376
|
+
...corner,
|
|
1377
|
+
left: plot.left + QUADRANT_LABEL_INSET,
|
|
1378
|
+
top: plot.top + plot.height - QUADRANT_LABEL_INSET - AXIS_LABEL_HEIGHT,
|
|
1379
|
+
}}
|
|
1380
|
+
>
|
|
1381
|
+
{labels.bottomLeft}
|
|
1382
|
+
</Text>
|
|
1383
|
+
) : null}
|
|
1384
|
+
{labels?.bottomRight ? (
|
|
1385
|
+
<Text
|
|
1386
|
+
size="xs"
|
|
1387
|
+
muted
|
|
1388
|
+
numberOfLines={1}
|
|
1389
|
+
style={{
|
|
1390
|
+
...corner,
|
|
1391
|
+
left: plot.left + plot.width / 2,
|
|
1392
|
+
top: plot.top + plot.height - QUADRANT_LABEL_INSET - AXIS_LABEL_HEIGHT,
|
|
1393
|
+
textAlign: 'right',
|
|
1394
|
+
}}
|
|
1395
|
+
>
|
|
1396
|
+
{labels.bottomRight}
|
|
1397
|
+
</Text>
|
|
1398
|
+
) : null}
|
|
1399
|
+
</View>
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
BubbleChartQuadrants.displayName = 'BubbleChart.Quadrants';
|
|
1403
|
+
BubbleChartQuadrants.layer = 'overlay' as Layer;
|
|
1404
|
+
|
|
1405
|
+
export interface BubbleChartSizeKeyProps {
|
|
1406
|
+
/** Which corner of the plot it sits in. */
|
|
1407
|
+
placement?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
1408
|
+
/** Turn a value into its label. Defaults to a compact number. */
|
|
1409
|
+
format?: (value: number) => string;
|
|
1410
|
+
/** A word for what the area means — "people", "revenue". */
|
|
1411
|
+
label?: string;
|
|
1412
|
+
className?: string;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Three nested circles saying what a bubble's area is worth.
|
|
1417
|
+
*
|
|
1418
|
+
* A bubble chart's third quantity is the one it cannot state: position can be
|
|
1419
|
+
* read off the axes, but area has no axis, so a reader can see that one circle
|
|
1420
|
+
* is bigger than another and has no way to know by how much. This is the only
|
|
1421
|
+
* part of the chart that answers that.
|
|
1422
|
+
*
|
|
1423
|
+
* Nested and sharing a baseline, which is how a difference in area is compared
|
|
1424
|
+
* — three circles in a row are three sizes, three circles inside one another
|
|
1425
|
+
* are one scale.
|
|
1426
|
+
*
|
|
1427
|
+
* It needs a `sizeKey` on the chart. Without one every bubble is the same size
|
|
1428
|
+
* and there is no scale to key.
|
|
1429
|
+
*/
|
|
1430
|
+
function BubbleChartSizeKey({
|
|
1431
|
+
placement = 'bottom-right',
|
|
1432
|
+
format,
|
|
1433
|
+
label,
|
|
1434
|
+
className,
|
|
1435
|
+
}: BubbleChartSizeKeyProps) {
|
|
1436
|
+
const { plot, status, sizeExtent, sizeRange } = useChart('BubbleChart.SizeKey');
|
|
1437
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
1438
|
+
const stroke = typeof token === 'string' ? token : 'rgba(128,128,128,0.8)';
|
|
1439
|
+
|
|
1440
|
+
const steps = useMemo(() => {
|
|
1441
|
+
if (!sizeExtent) return null;
|
|
1442
|
+
const [min, max] = sizeExtent;
|
|
1443
|
+
const middle = (min + max) / 2;
|
|
1444
|
+
// Largest first, so the smallest is drawn last and stays visible inside it.
|
|
1445
|
+
return [max, middle, min].map((value) => ({
|
|
1446
|
+
value,
|
|
1447
|
+
r: bubbleRadius(value, sizeExtent, sizeRange),
|
|
1448
|
+
}));
|
|
1449
|
+
}, [sizeExtent, sizeRange]);
|
|
1450
|
+
|
|
1451
|
+
if (status === 'loading' || !steps) return null;
|
|
1452
|
+
|
|
1453
|
+
const outer = steps[0]!.r;
|
|
1454
|
+
const width = outer * 2 + SIZE_KEY_LABEL_WIDTH;
|
|
1455
|
+
const height = outer * 2 + (label ? AXIS_LABEL_HEIGHT : 0);
|
|
1456
|
+
const top = placement.startsWith('top')
|
|
1457
|
+
? plot.top
|
|
1458
|
+
: plot.top + plot.height - height;
|
|
1459
|
+
const left = placement.endsWith('left')
|
|
1460
|
+
? plot.left
|
|
1461
|
+
: plot.left + plot.width - width;
|
|
1462
|
+
|
|
1463
|
+
return (
|
|
1464
|
+
<View
|
|
1465
|
+
pointerEvents="none"
|
|
1466
|
+
style={{ position: 'absolute', left, top, width, height }}
|
|
1467
|
+
className={cn(className)}
|
|
1468
|
+
>
|
|
1469
|
+
{label ? (
|
|
1470
|
+
<Text size="xs" muted numberOfLines={1}>
|
|
1471
|
+
{label}
|
|
1472
|
+
</Text>
|
|
1473
|
+
) : null}
|
|
1474
|
+
<View style={{ height: outer * 2, width }}>
|
|
1475
|
+
{steps.map((step) => (
|
|
1476
|
+
<View
|
|
1477
|
+
key={step.value}
|
|
1478
|
+
style={{
|
|
1479
|
+
position: 'absolute',
|
|
1480
|
+
// Shared bottom edge and shared centre line: the circles nest
|
|
1481
|
+
// rather than stack, so the areas are laid over one another.
|
|
1482
|
+
bottom: 0,
|
|
1483
|
+
left: outer - step.r,
|
|
1484
|
+
width: step.r * 2,
|
|
1485
|
+
height: step.r * 2,
|
|
1486
|
+
borderRadius: step.r,
|
|
1487
|
+
borderWidth: 1,
|
|
1488
|
+
borderColor: stroke,
|
|
1489
|
+
opacity: 0.6,
|
|
1490
|
+
}}
|
|
1491
|
+
/>
|
|
1492
|
+
))}
|
|
1493
|
+
{steps.map((step) => (
|
|
1494
|
+
<Text
|
|
1495
|
+
key={`v${step.value}`}
|
|
1496
|
+
size="xs"
|
|
1497
|
+
muted
|
|
1498
|
+
numberOfLines={1}
|
|
1499
|
+
style={{
|
|
1500
|
+
position: 'absolute',
|
|
1501
|
+
left: outer * 2 + SIZE_KEY_GAP,
|
|
1502
|
+
// Level with the top of the circle it names, which is the only
|
|
1503
|
+
// edge the three do not share.
|
|
1504
|
+
top: outer * 2 - step.r * 2 - AXIS_LABEL_HEIGHT / 2,
|
|
1505
|
+
width: SIZE_KEY_LABEL_WIDTH - SIZE_KEY_GAP,
|
|
1506
|
+
}}
|
|
1507
|
+
>
|
|
1508
|
+
{format ? format(step.value) : compactNumber(step.value)}
|
|
1509
|
+
</Text>
|
|
1510
|
+
))}
|
|
1511
|
+
</View>
|
|
1512
|
+
</View>
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
BubbleChartSizeKey.displayName = 'BubbleChart.SizeKey';
|
|
1516
|
+
BubbleChartSizeKey.layer = 'overlay' as Layer;
|
|
1517
|
+
|
|
970
1518
|
function BubbleLabel({
|
|
971
1519
|
bubble,
|
|
972
1520
|
text,
|
|
@@ -1031,10 +1579,18 @@ function BubbleLabel({
|
|
|
1031
1579
|
}
|
|
1032
1580
|
|
|
1033
1581
|
export interface BubbleChartXAxisProps {
|
|
1034
|
-
/**
|
|
1582
|
+
/**
|
|
1583
|
+
* How many intervals to divide the axis into. Yields `ticks + 1` labels.
|
|
1584
|
+
*
|
|
1585
|
+
* Four, and the domain is rounded out to four steps to match, so the numbers
|
|
1586
|
+
* come out round. Fewer leaves most of the grid unnamed — a line with nothing
|
|
1587
|
+
* beside it is a line the reader has to count their way to.
|
|
1588
|
+
*/
|
|
1035
1589
|
ticks?: number;
|
|
1036
1590
|
/** Turn a value into its label. Defaults to a compact number. */
|
|
1037
1591
|
format?: (value: number) => string;
|
|
1592
|
+
/** What the axis measures, written under the numbers. */
|
|
1593
|
+
label?: string;
|
|
1038
1594
|
className?: string;
|
|
1039
1595
|
}
|
|
1040
1596
|
|
|
@@ -1044,7 +1600,7 @@ export interface BubbleChartXAxisProps {
|
|
|
1044
1600
|
* Evenly spaced, because this axis is a continuous scale rather than a list of
|
|
1045
1601
|
* rows. There is no bubble for a label to sit under.
|
|
1046
1602
|
*/
|
|
1047
|
-
function BubbleChartXAxis({ ticks =
|
|
1603
|
+
function BubbleChartXAxis({ ticks = 4, format, label, className }: BubbleChartXAxisProps) {
|
|
1048
1604
|
const { plot, xExtent } = useChart('BubbleChart.XAxis');
|
|
1049
1605
|
|
|
1050
1606
|
const labels = useMemo(() => {
|
|
@@ -1061,15 +1617,31 @@ function BubbleChartXAxis({ ticks = 2, format, className }: BubbleChartXAxisProp
|
|
|
1061
1617
|
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
|
1062
1618
|
className={cn(className)}
|
|
1063
1619
|
>
|
|
1064
|
-
{
|
|
1620
|
+
{label ? (
|
|
1065
1621
|
<Text
|
|
1066
|
-
key={label.key}
|
|
1067
1622
|
size="xs"
|
|
1068
1623
|
muted
|
|
1069
1624
|
numberOfLines={1}
|
|
1070
1625
|
style={{
|
|
1071
1626
|
position: 'absolute',
|
|
1072
1627
|
bottom: 0,
|
|
1628
|
+
left: plot.left,
|
|
1629
|
+
width: plot.width,
|
|
1630
|
+
textAlign: 'center',
|
|
1631
|
+
}}
|
|
1632
|
+
>
|
|
1633
|
+
{label}
|
|
1634
|
+
</Text>
|
|
1635
|
+
) : null}
|
|
1636
|
+
{labels.map((tick) => (
|
|
1637
|
+
<Text
|
|
1638
|
+
key={tick.key}
|
|
1639
|
+
size="xs"
|
|
1640
|
+
muted
|
|
1641
|
+
numberOfLines={1}
|
|
1642
|
+
style={{
|
|
1643
|
+
position: 'absolute',
|
|
1644
|
+
bottom: label ? AXIS_TITLE_HEIGHT : 0,
|
|
1073
1645
|
// Centred on its tick, then held inside the chart. The first and
|
|
1074
1646
|
// last ticks sit on the plot's own edges, so a box centred on them
|
|
1075
1647
|
// hangs half its width off the side — the clamp slides those two
|
|
@@ -1077,7 +1649,7 @@ function BubbleChartXAxis({ ticks = 2, format, className }: BubbleChartXAxisProp
|
|
|
1077
1649
|
left: Math.max(
|
|
1078
1650
|
0,
|
|
1079
1651
|
Math.min(
|
|
1080
|
-
plot.left + (plot.width / ticks) *
|
|
1652
|
+
plot.left + (plot.width / ticks) * tick.key - AXIS_LABEL_WIDTH / 2,
|
|
1081
1653
|
plot.left + plot.width + PADDING.right - AXIS_LABEL_WIDTH
|
|
1082
1654
|
)
|
|
1083
1655
|
),
|
|
@@ -1085,7 +1657,7 @@ function BubbleChartXAxis({ ticks = 2, format, className }: BubbleChartXAxisProp
|
|
|
1085
1657
|
textAlign: 'center',
|
|
1086
1658
|
}}
|
|
1087
1659
|
>
|
|
1088
|
-
{
|
|
1660
|
+
{tick.text}
|
|
1089
1661
|
</Text>
|
|
1090
1662
|
))}
|
|
1091
1663
|
</View>
|
|
@@ -1093,17 +1665,33 @@ function BubbleChartXAxis({ ticks = 2, format, className }: BubbleChartXAxisProp
|
|
|
1093
1665
|
}
|
|
1094
1666
|
BubbleChartXAxis.displayName = 'BubbleChart.XAxis';
|
|
1095
1667
|
BubbleChartXAxis.layer = 'overlay' as Layer;
|
|
1668
|
+
// Read by the root, which has to leave room under the numbers before it lays
|
|
1669
|
+
// the plot out.
|
|
1670
|
+
BubbleChartXAxis.axis = 'x' as const;
|
|
1096
1671
|
|
|
1097
1672
|
export interface BubbleChartYAxisProps {
|
|
1098
|
-
/**
|
|
1673
|
+
/**
|
|
1674
|
+
* How many intervals to divide the axis into. Yields `ticks + 1` labels.
|
|
1675
|
+
*
|
|
1676
|
+
* Four, matching the four steps the domain is rounded out to and every second
|
|
1677
|
+
* line of the default grid.
|
|
1678
|
+
*/
|
|
1099
1679
|
ticks?: number;
|
|
1100
1680
|
/** Turn a value into its label. Defaults to a compact number. */
|
|
1101
1681
|
format?: (value: number) => string;
|
|
1682
|
+
/** What the axis measures, written up the side of it. */
|
|
1683
|
+
label?: string;
|
|
1102
1684
|
className?: string;
|
|
1103
1685
|
}
|
|
1104
1686
|
|
|
1105
|
-
/**
|
|
1106
|
-
|
|
1687
|
+
/**
|
|
1688
|
+
* Value labels down the side, evenly over the axis, and the gutter they sit in.
|
|
1689
|
+
*
|
|
1690
|
+
* They land on every second line of the default grid rather than on all of
|
|
1691
|
+
* them: a number beside every line of a grid fine enough to read against is a
|
|
1692
|
+
* column of numbers, and the reader stops seeing the chart.
|
|
1693
|
+
*/
|
|
1694
|
+
function BubbleChartYAxis({ ticks = 4, format, label, className }: BubbleChartYAxisProps) {
|
|
1107
1695
|
const { plot, yExtent } = useChart('BubbleChart.YAxis');
|
|
1108
1696
|
|
|
1109
1697
|
const labels = useMemo(() => {
|
|
@@ -1115,27 +1703,54 @@ function BubbleChartYAxis({ ticks = 2, format, className }: BubbleChartYAxisProp
|
|
|
1115
1703
|
});
|
|
1116
1704
|
}, [yExtent, ticks, format]);
|
|
1117
1705
|
|
|
1706
|
+
const titleWidth = label ? AXIS_TITLE_WIDTH : 0;
|
|
1707
|
+
|
|
1118
1708
|
return (
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1709
|
+
<>
|
|
1710
|
+
{label ? (
|
|
1711
|
+
/*
|
|
1712
|
+
* Turned on its side, which is the only way a word fits in a gutter
|
|
1713
|
+
* sized for numbers. The box is laid out as tall as the plot and then
|
|
1714
|
+
* rotated about its own centre, so the text runs the length of the axis
|
|
1715
|
+
* it names rather than of whatever it happens to say.
|
|
1716
|
+
*/
|
|
1717
|
+
<View
|
|
1718
|
+
pointerEvents="none"
|
|
1719
|
+
style={{
|
|
1720
|
+
position: 'absolute',
|
|
1721
|
+
left: titleWidth / 2 - plot.height / 2,
|
|
1722
|
+
top: plot.top + plot.height / 2 - AXIS_LABEL_HEIGHT / 2,
|
|
1723
|
+
width: plot.height,
|
|
1724
|
+
height: AXIS_LABEL_HEIGHT,
|
|
1725
|
+
transform: [{ rotate: '-90deg' }],
|
|
1726
|
+
}}
|
|
1727
|
+
>
|
|
1728
|
+
<Text size="xs" muted numberOfLines={1} style={{ textAlign: 'center' }}>
|
|
1729
|
+
{label}
|
|
1730
|
+
</Text>
|
|
1731
|
+
</View>
|
|
1732
|
+
) : null}
|
|
1733
|
+
<View
|
|
1734
|
+
pointerEvents="none"
|
|
1735
|
+
style={{
|
|
1736
|
+
position: 'absolute',
|
|
1737
|
+
left: titleWidth,
|
|
1738
|
+
// Centred on the grid line each label names: the strip is lifted half
|
|
1739
|
+
// a label and grown by a whole one, so `justify-between` lands the
|
|
1740
|
+
// text's middle on the line rather than its top edge on the first.
|
|
1741
|
+
top: plot.top - AXIS_LABEL_HEIGHT / 2,
|
|
1742
|
+
height: plot.height + AXIS_LABEL_HEIGHT,
|
|
1743
|
+
width: Math.max(plot.left - Y_AXIS_GUTTER - titleWidth, 0),
|
|
1744
|
+
}}
|
|
1745
|
+
className={cn('items-end justify-between', className)}
|
|
1746
|
+
>
|
|
1747
|
+
{labels.map((tick) => (
|
|
1748
|
+
<Text key={tick.key} size="xs" muted numberOfLines={1}>
|
|
1749
|
+
{tick.text}
|
|
1750
|
+
</Text>
|
|
1751
|
+
))}
|
|
1752
|
+
</View>
|
|
1753
|
+
</>
|
|
1139
1754
|
);
|
|
1140
1755
|
}
|
|
1141
1756
|
BubbleChartYAxis.displayName = 'BubbleChart.YAxis';
|
|
@@ -1517,8 +2132,11 @@ BubbleChartHeader.layer = 'header' as Layer;
|
|
|
1517
2132
|
export const BubbleChart = Object.assign(BubbleChartRoot, {
|
|
1518
2133
|
Header: BubbleChartHeader,
|
|
1519
2134
|
Grid: BubbleChartGrid,
|
|
2135
|
+
Quadrants: BubbleChartQuadrants,
|
|
2136
|
+
Trend: BubbleChartTrend,
|
|
1520
2137
|
Bubbles: BubbleChartBubbles,
|
|
1521
2138
|
Labels: BubbleChartLabels,
|
|
2139
|
+
SizeKey: BubbleChartSizeKey,
|
|
1522
2140
|
Skeleton: BubbleChartSkeleton,
|
|
1523
2141
|
XAxis: BubbleChartXAxis,
|
|
1524
2142
|
YAxis: BubbleChartYAxis,
|