@delacour/react-native-ui 0.1.0-alpha.20260925053522
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/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type { CurveType } from "@delacour/react-native-charts/core";
|
|
2
|
+
export { Chart, type ChartProps } from "./chart";
|
|
3
|
+
export {
|
|
4
|
+
ChartContext,
|
|
5
|
+
type ChartContextValue,
|
|
6
|
+
ChartProvider,
|
|
7
|
+
type ChartSlots,
|
|
8
|
+
useChart,
|
|
9
|
+
useChartContext,
|
|
10
|
+
useChartSeries,
|
|
11
|
+
useSeriesColor,
|
|
12
|
+
} from "./chart.context";
|
|
13
|
+
export type {
|
|
14
|
+
ChartAreaSpec,
|
|
15
|
+
ChartBarLayout,
|
|
16
|
+
ChartBarLayoutMode,
|
|
17
|
+
ChartBarSlot,
|
|
18
|
+
ChartBarSpec,
|
|
19
|
+
ChartCandleColors,
|
|
20
|
+
ChartCandlestickKeys,
|
|
21
|
+
ChartConfig,
|
|
22
|
+
ChartDatum,
|
|
23
|
+
ChartResolvedSeries,
|
|
24
|
+
ChartSeriesConfig,
|
|
25
|
+
ChartTooltipInput,
|
|
26
|
+
ChartTooltipRow,
|
|
27
|
+
} from "./chart.types";
|
|
28
|
+
export {
|
|
29
|
+
AREA_FILL_OPACITY,
|
|
30
|
+
AREA_STACKED_FILL_OPACITY,
|
|
31
|
+
type AreaFill,
|
|
32
|
+
type AreaFillInput,
|
|
33
|
+
applyChartColors,
|
|
34
|
+
BAR_RADIUS_MULTIPLIER,
|
|
35
|
+
BAR_RADIUS_STEP,
|
|
36
|
+
CANDLE_SENTIMENT_TOKENS,
|
|
37
|
+
CANDLE_Y_PADDING,
|
|
38
|
+
CATEGORY_TICK_CAP,
|
|
39
|
+
CHART_CURVES,
|
|
40
|
+
CHART_MAX_TOKEN_SERIES,
|
|
41
|
+
CHART_SERIES_TOKENS,
|
|
42
|
+
CHART_SIZES,
|
|
43
|
+
type ChartColorPartition,
|
|
44
|
+
type ChartCurve,
|
|
45
|
+
type ChartSeriesToken,
|
|
46
|
+
type ChartSize,
|
|
47
|
+
type ChartTooltipCandle,
|
|
48
|
+
type ChartVariantProps,
|
|
49
|
+
chartAxisFontSize,
|
|
50
|
+
chartSeriesToken,
|
|
51
|
+
chartTickCount,
|
|
52
|
+
chartTooltipOffset,
|
|
53
|
+
chartVariants,
|
|
54
|
+
type DomainDefaults,
|
|
55
|
+
type DomainDefaultsInput,
|
|
56
|
+
EDGE_X_PADDING,
|
|
57
|
+
PIE_DEFAULT_INNER_RADIUS,
|
|
58
|
+
type PieLabelFormat,
|
|
59
|
+
type PieLabelSlice,
|
|
60
|
+
type PieResolvedSeries,
|
|
61
|
+
partitionChartColors,
|
|
62
|
+
pieInnerRadiusSpec,
|
|
63
|
+
pieLabelText,
|
|
64
|
+
pieSlicePercent,
|
|
65
|
+
resolveAreaFill,
|
|
66
|
+
resolveBarLayout,
|
|
67
|
+
resolveBarRadius,
|
|
68
|
+
resolveCategoryTickCount,
|
|
69
|
+
resolveChartKeys,
|
|
70
|
+
resolveChartSeries,
|
|
71
|
+
resolveDomainDefaults,
|
|
72
|
+
resolvePieSeries,
|
|
73
|
+
resolveStackedAreaKeys,
|
|
74
|
+
resolveTooltipRows,
|
|
75
|
+
resolveXValueFormat,
|
|
76
|
+
} from "./chart.variants";
|
|
77
|
+
export type { ChartAreaProps } from "./chart-area";
|
|
78
|
+
export type { ChartBarProps } from "./chart-bar";
|
|
79
|
+
export type { ChartCandlestickProps } from "./chart-candlestick";
|
|
80
|
+
export type { ChartGridProps } from "./chart-grid";
|
|
81
|
+
export type { ChartLegendProps } from "./chart-legend";
|
|
82
|
+
export type { ChartLineProps } from "./chart-line";
|
|
83
|
+
export type { ChartScatterProps } from "./chart-scatter";
|
|
84
|
+
export type { ChartTooltipProps } from "./chart-tooltip";
|
|
85
|
+
export type { ChartTooltipDotProps } from "./chart-tooltip-dot";
|
|
86
|
+
export type { ChartTooltipXProps } from "./chart-tooltip-x";
|
|
87
|
+
export type { ChartTooltipYProps } from "./chart-tooltip-y";
|
|
88
|
+
export type { ChartXAxisProps } from "./chart-x-axis";
|
|
89
|
+
export type { ChartYAxisProps } from "./chart-y-axis";
|
|
90
|
+
export { PieChart, type PieChartProps } from "./pie-chart";
|
|
91
|
+
export {
|
|
92
|
+
PieChartContext,
|
|
93
|
+
type PieChartContextValue,
|
|
94
|
+
PieChartProvider,
|
|
95
|
+
usePieChart,
|
|
96
|
+
usePieChartContext,
|
|
97
|
+
} from "./pie-chart.context";
|
|
98
|
+
export type { PieChartCenterProps } from "./pie-chart-center";
|
|
99
|
+
export type { PieChartLabelProps } from "./pie-chart-label";
|
|
100
|
+
export type { PieChartSliceProps } from "./pie-chart-slice";
|
|
101
|
+
export type { PieChartTooltipProps } from "./pie-chart-tooltip";
|
|
102
|
+
export { useChartFont } from "./use-chart-font";
|
|
103
|
+
export { useChartPalette } from "./use-chart-palette";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { usePieChart } from "./pie-chart.context";
|
|
6
|
+
|
|
7
|
+
export type PieChartCenterProps = {
|
|
8
|
+
/** The headline figure — a total, a percentage. On the type scale, in `foreground`. */
|
|
9
|
+
value?: string;
|
|
10
|
+
/** A caption under the value, in `muted-foreground`. */
|
|
11
|
+
label?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
/** Anything else to place in the hole, after the value and the label. */
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Content in the hole of a donut.
|
|
19
|
+
*
|
|
20
|
+
* A React Native view, not a Skia mark, because what goes in a donut's hole
|
|
21
|
+
* is a headline number and a caption, and those want the type scale, a colour
|
|
22
|
+
* token and a className — none of which exist on a canvas. It is centred over
|
|
23
|
+
* the whole frame, which is where the circle's centre is: the engine centres
|
|
24
|
+
* the largest circle the canvas holds, and this chart gives it no padding.
|
|
25
|
+
*
|
|
26
|
+
* It takes no touches, so a tap through it still lands on a slice.
|
|
27
|
+
*/
|
|
28
|
+
export function PieChartCenter({ value, label, className, children }: PieChartCenterProps): ReactElement {
|
|
29
|
+
const { slots } = usePieChart();
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<View className={cn(slots.pieCenter(), className)} pointerEvents="none">
|
|
33
|
+
{value === undefined ? null : <Text className={slots.pieCenterValue()}>{value}</Text>}
|
|
34
|
+
{label === undefined ? null : <Text className={slots.pieCenterLabel()}>{label}</Text>}
|
|
35
|
+
{children}
|
|
36
|
+
</View>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PieChartCenter.displayName = "DelacourUI.PieChart.Center";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PieLabel } from "@delacour/react-native-charts";
|
|
2
|
+
import type { PieSliceData } from "@delacour/react-native-charts/core";
|
|
3
|
+
import { type ReactElement, useCallback } from "react";
|
|
4
|
+
import { type PieLabelFormat, pieLabelText } from "./chart.variants";
|
|
5
|
+
import { usePieChart } from "./pie-chart.context";
|
|
6
|
+
|
|
7
|
+
export type PieChartLabelProps = {
|
|
8
|
+
/** What each label prints. Defaults to the slice's share, as `42%`. */
|
|
9
|
+
format?: PieLabelFormat;
|
|
10
|
+
/**
|
|
11
|
+
* Overrides the chart's background, which the labels are drawn in.
|
|
12
|
+
*
|
|
13
|
+
* A literal, or a value already resolved — a Skia mark cannot look a token
|
|
14
|
+
* up, for the reason the folder's `AGENTS.md` opens with.
|
|
15
|
+
*/
|
|
16
|
+
color?: string;
|
|
17
|
+
/** Across the annulus: 0 the inner edge, 1 the outer, more than 1 outside. */
|
|
18
|
+
radiusOffset?: number;
|
|
19
|
+
/** Slices narrower than this, in degrees, get no label. */
|
|
20
|
+
minSweep?: number;
|
|
21
|
+
opacity?: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A text label on every slice wide enough to hold one.
|
|
26
|
+
*
|
|
27
|
+
* Skia text in the chart's background colour, so it reads on any slice of the
|
|
28
|
+
* ramp; the axis labels are the precedent for text living in the canvas. A
|
|
29
|
+
* slice under twelve degrees gets none, because a label wider than its wedge
|
|
30
|
+
* lands on the neighbours and three overlapping labels are less legible than
|
|
31
|
+
* none.
|
|
32
|
+
*/
|
|
33
|
+
export function PieChartLabel({
|
|
34
|
+
format,
|
|
35
|
+
color,
|
|
36
|
+
radiusOffset,
|
|
37
|
+
minSweep = 12,
|
|
38
|
+
opacity,
|
|
39
|
+
}: PieChartLabelProps): ReactElement | null {
|
|
40
|
+
const { surfaceColor } = usePieChart();
|
|
41
|
+
const formatLabel = useCallback((slice: PieSliceData) => pieLabelText(format, slice), [format]);
|
|
42
|
+
const paint = color ?? surfaceColor;
|
|
43
|
+
if (paint === undefined) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<PieLabel
|
|
47
|
+
color={paint}
|
|
48
|
+
formatLabel={formatLabel}
|
|
49
|
+
minSweep={minSweep}
|
|
50
|
+
opacity={opacity}
|
|
51
|
+
radiusOffset={radiusOffset}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
PieChartLabel.displayName = "DelacourUI.PieChart.Label";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PieInset, PieSlices } from "@delacour/react-native-charts";
|
|
2
|
+
import { type ReactElement, useMemo } from "react";
|
|
3
|
+
import { usePieChart } from "./pie-chart.context";
|
|
4
|
+
|
|
5
|
+
export type PieChartSliceProps = {
|
|
6
|
+
opacity?: number;
|
|
7
|
+
/**
|
|
8
|
+
* A hairline between neighbouring slices, in the chart's background.
|
|
9
|
+
*
|
|
10
|
+
* On by default: two ramp colours side by side with no gap read as one
|
|
11
|
+
* shape with a stripe, and the gap is what makes each wedge its own.
|
|
12
|
+
*/
|
|
13
|
+
stroke?: boolean;
|
|
14
|
+
/** The hairline's width. */
|
|
15
|
+
strokeWidth?: number;
|
|
16
|
+
/** Opacity of every slice but the selected one, while one is selected. `1` turns the dim off. */
|
|
17
|
+
dimUnselected?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Every slice of the pie, one wedge per row, in the series' colours.
|
|
22
|
+
*
|
|
23
|
+
* The colours come from the context already resolved, so this is the engine's
|
|
24
|
+
* `PieSlices` given a list of strings. While a slice is selected — by a tap or
|
|
25
|
+
* through `selectedIndex` — the others dim, because a selection that changes
|
|
26
|
+
* nothing on screen looks like a tap that missed.
|
|
27
|
+
*/
|
|
28
|
+
export function PieChartSlice({
|
|
29
|
+
opacity,
|
|
30
|
+
stroke = true,
|
|
31
|
+
strokeWidth = 2,
|
|
32
|
+
dimUnselected = 0.4,
|
|
33
|
+
}: PieChartSliceProps): ReactElement {
|
|
34
|
+
const { series, surfaceColor } = usePieChart();
|
|
35
|
+
const colors = useMemo(() => series.map((entry) => entry.color), [series]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<PieSlices colors={colors} dimUnselected={dimUnselected} opacity={opacity} />
|
|
40
|
+
{stroke && surfaceColor !== undefined ? <PieInset color={surfaceColor} strokeWidth={strokeWidth} /> : null}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
PieChartSlice.displayName = "DelacourUI.PieChart.Slice";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { sliceLabelPosition } from "@delacour/react-native-charts/core";
|
|
2
|
+
import { type ReactElement, useState } from "react";
|
|
3
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
4
|
+
import { View } from "react-native";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { Text } from "../text";
|
|
7
|
+
import { chartTooltipOffset, formatPieValue, pieSlicePercent } from "./chart.variants";
|
|
8
|
+
import { usePieChart } from "./pie-chart.context";
|
|
9
|
+
|
|
10
|
+
export type PieChartTooltipProps = {
|
|
11
|
+
className?: string;
|
|
12
|
+
/** Prints the selected slice's value. Defaults to digits grouped in threes, `1,240`. */
|
|
13
|
+
formatValue?: (value: number, name: string) => string;
|
|
14
|
+
/** Prints the selected slice's share. Defaults to a whole-number percentage. */
|
|
15
|
+
formatPercent?: (percent: number) => string;
|
|
16
|
+
/** Overrides the measured size used to keep the tooltip inside the frame. */
|
|
17
|
+
size?: { width: number; height: number };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Assumed size before layout, so the first frame is not placed off-screen. */
|
|
21
|
+
const DEFAULT_SIZE = { width: 120, height: 48 };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A readout for the selected slice: its label, its value and its share.
|
|
25
|
+
*
|
|
26
|
+
* Selection-driven, not scrub-driven. A pie has no x to scrub along, so the
|
|
27
|
+
* readout answers a tap rather than following a finger — which is also why it
|
|
28
|
+
* needs no shared values and no `useAnimatedStyle`: it moves when the
|
|
29
|
+
* selection changes, a few times a session, not once a frame.
|
|
30
|
+
*
|
|
31
|
+
* It sits just outside the slice's outer edge on its bisector, the position a
|
|
32
|
+
* callout line would point at, and `chartTooltipOffset` keeps it inside the
|
|
33
|
+
* frame the same way the cartesian tooltip is kept. Hidden while nothing is
|
|
34
|
+
* selected; a tap outside every slice clears the selection and hides it.
|
|
35
|
+
*/
|
|
36
|
+
export function PieChartTooltip({
|
|
37
|
+
className,
|
|
38
|
+
formatValue,
|
|
39
|
+
formatPercent,
|
|
40
|
+
size,
|
|
41
|
+
}: PieChartTooltipProps): ReactElement | null {
|
|
42
|
+
const { series, values, total, slices, slots, frame, selectedIndex } = usePieChart();
|
|
43
|
+
const [measured, setMeasured] = useState(DEFAULT_SIZE);
|
|
44
|
+
const box = size ?? measured;
|
|
45
|
+
|
|
46
|
+
const onLayout = (event: LayoutChangeEvent): void => {
|
|
47
|
+
const { width, height } = event.nativeEvent.layout;
|
|
48
|
+
setMeasured((current) => (current.width === width && current.height === height ? current : { width, height }));
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (selectedIndex === null) return null;
|
|
52
|
+
const entry = series[selectedIndex];
|
|
53
|
+
const slice = slices[selectedIndex];
|
|
54
|
+
const value = values[selectedIndex];
|
|
55
|
+
if (entry === undefined || slice === undefined || value === undefined) return null;
|
|
56
|
+
|
|
57
|
+
const anchor = sliceLabelPosition(slice, 1.05);
|
|
58
|
+
const offset = chartTooltipOffset({
|
|
59
|
+
x: anchor.x,
|
|
60
|
+
y: anchor.y,
|
|
61
|
+
width: box.width,
|
|
62
|
+
height: box.height,
|
|
63
|
+
frameWidth: frame.width,
|
|
64
|
+
frameHeight: frame.height,
|
|
65
|
+
});
|
|
66
|
+
const percent = pieSlicePercent(value, total);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View
|
|
70
|
+
className={cn(slots.tooltip(), className)}
|
|
71
|
+
onLayout={onLayout}
|
|
72
|
+
pointerEvents="none"
|
|
73
|
+
style={{ transform: [{ translateX: offset.x }, { translateY: offset.y }] }}
|
|
74
|
+
>
|
|
75
|
+
<View className={slots.tooltipRow()}>
|
|
76
|
+
<View className={slots.tooltipSwatch()} style={{ backgroundColor: entry.color }} />
|
|
77
|
+
<Text className={slots.tooltipName()}>{entry.label}</Text>
|
|
78
|
+
<Text className={slots.tooltipValue()}>{formatValue?.(value, entry.label) ?? formatPieValue(value)}</Text>
|
|
79
|
+
</View>
|
|
80
|
+
<Text className={slots.tooltipHeading()}>{formatPercent?.(percent) ?? `${Math.round(percent)}%`}</Text>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
PieChartTooltip.displayName = "DelacourUI.PieChart.Tooltip";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { PieSliceData } from "@delacour/react-native-charts/core";
|
|
2
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
3
|
+
import type { ChartSlots } from "./chart.context";
|
|
4
|
+
import type { ChartResolvedSeries } from "./chart.types";
|
|
5
|
+
import type { ChartSize } from "./chart.variants";
|
|
6
|
+
|
|
7
|
+
export type PieChartContextValue = {
|
|
8
|
+
/** One per drawn slice, in data order, colours already resolved to values. */
|
|
9
|
+
readonly series: readonly ChartResolvedSeries[];
|
|
10
|
+
/** The slices' values, index-aligned with `series`. */
|
|
11
|
+
readonly values: readonly number[];
|
|
12
|
+
readonly total: number;
|
|
13
|
+
/**
|
|
14
|
+
* The slices' geometry, index-aligned with `series`.
|
|
15
|
+
*
|
|
16
|
+
* Resolved here as well as in the engine, from the same pure functions on
|
|
17
|
+
* the same frame, because the tooltip is a React Native view outside the
|
|
18
|
+
* canvas and the engine's context does not reach it.
|
|
19
|
+
*/
|
|
20
|
+
readonly slices: readonly PieSliceData[];
|
|
21
|
+
readonly slots: ChartSlots;
|
|
22
|
+
readonly size: ChartSize;
|
|
23
|
+
/** The surface under the pie — what the hairline between slices is painted in. */
|
|
24
|
+
readonly surfaceColor: string | undefined;
|
|
25
|
+
/** The frame's measured size, for placing an overlay inside it. */
|
|
26
|
+
readonly frame: { readonly width: number; readonly height: number };
|
|
27
|
+
readonly nameKey: string;
|
|
28
|
+
readonly valueKey: string;
|
|
29
|
+
/** The hole, as a fraction of the radius. `0` is a pie. */
|
|
30
|
+
readonly innerRadius: number;
|
|
31
|
+
/** The slice singled out by a tap or by the caller, or `null`. */
|
|
32
|
+
readonly selectedIndex: number | null;
|
|
33
|
+
/** Selects a slice by index, or clears the selection with `null`. */
|
|
34
|
+
readonly select: (index: number | null) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The resolved pie, published twice from one value — once around the React
|
|
39
|
+
* Native subtree and once inside the canvas — for the reason `ChartContext`
|
|
40
|
+
* documents: Skia's renderer is its own reconciler and context does not cross
|
|
41
|
+
* into it.
|
|
42
|
+
*/
|
|
43
|
+
export const PieChartContext = createContext<PieChartContextValue | null>(null);
|
|
44
|
+
|
|
45
|
+
PieChartContext.displayName = "DelacourUI.PieChartContext";
|
|
46
|
+
|
|
47
|
+
export function PieChartProvider({
|
|
48
|
+
value,
|
|
49
|
+
children,
|
|
50
|
+
}: {
|
|
51
|
+
readonly value: PieChartContextValue;
|
|
52
|
+
readonly children: ReactNode;
|
|
53
|
+
}): ReactNode {
|
|
54
|
+
return <PieChartContext.Provider value={value}>{children}</PieChartContext.Provider>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
PieChartProvider.displayName = "DelacourUI.PieChartProvider";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The pie a part belongs to. Throws outside one, for the reason `useChart`
|
|
61
|
+
* does: a mark that silently drew nothing would be a blank canvas with no
|
|
62
|
+
* error to read.
|
|
63
|
+
*/
|
|
64
|
+
export function usePieChart(): PieChartContextValue {
|
|
65
|
+
const value = useContext(PieChartContext);
|
|
66
|
+
if (value === null) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"[DelacourUI] a PieChart part was used outside a <PieChart>. Every part reads the chart from context."
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The pie, or `null` outside one. */
|
|
75
|
+
export function usePieChartContext(): PieChartContextValue | null {
|
|
76
|
+
return useContext(PieChartContext);
|
|
77
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { PolarChart } from "@delacour/react-native-charts";
|
|
2
|
+
import { type PieSliceData, resolvePolarLayout, resolveSlices } from "@delacour/react-native-charts/core";
|
|
3
|
+
import { Children, isValidElement, type ReactElement, type ReactNode, useMemo, useState } from "react";
|
|
4
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
5
|
+
import { View } from "react-native";
|
|
6
|
+
import { useControllableState } from "../../hooks/use-controllable-state";
|
|
7
|
+
import { useThemeColor } from "../../hooks/use-theme-color";
|
|
8
|
+
import type { ChartConfig, ChartDatum } from "./chart.types";
|
|
9
|
+
import {
|
|
10
|
+
type ChartSize,
|
|
11
|
+
chartAxisFontSize,
|
|
12
|
+
chartVariants,
|
|
13
|
+
PIE_DEFAULT_INNER_RADIUS,
|
|
14
|
+
pieInnerRadiusSpec,
|
|
15
|
+
resolvePieSeries,
|
|
16
|
+
} from "./chart.variants";
|
|
17
|
+
import { ChartLegend } from "./chart-legend";
|
|
18
|
+
import { type PieChartContextValue, PieChartProvider } from "./pie-chart.context";
|
|
19
|
+
import { PieChartCenter } from "./pie-chart-center";
|
|
20
|
+
import { PieChartLabel } from "./pie-chart-label";
|
|
21
|
+
import { PieChartSlice } from "./pie-chart-slice";
|
|
22
|
+
import { PieChartTooltip } from "./pie-chart-tooltip";
|
|
23
|
+
import { useChartFont } from "./use-chart-font";
|
|
24
|
+
import { useChartPalette } from "./use-chart-palette";
|
|
25
|
+
|
|
26
|
+
export type PieChartProps = {
|
|
27
|
+
/**
|
|
28
|
+
* Labels and colours by row name, for the rows that want one.
|
|
29
|
+
*
|
|
30
|
+
* Optional, because a pie's categories are its rows and the ramp already
|
|
31
|
+
* colours them in order. An entry is keyed by the row's `nameKey` value.
|
|
32
|
+
*/
|
|
33
|
+
config?: ChartConfig;
|
|
34
|
+
data: readonly ChartDatum[];
|
|
35
|
+
/** The field that names a slice. */
|
|
36
|
+
nameKey: string;
|
|
37
|
+
/** The field that sizes a slice. Negative, missing and unreadable values are dropped. */
|
|
38
|
+
valueKey: string;
|
|
39
|
+
size?: ChartSize;
|
|
40
|
+
/** The hole, as a fraction of the radius. `0` is a pie; `0.6` is a donut. */
|
|
41
|
+
innerRadius?: number;
|
|
42
|
+
/** Where the first slice begins. Degrees clockwise from 12 o'clock. */
|
|
43
|
+
startAngle?: number;
|
|
44
|
+
/** The selected slice's index in the drawn order, or `null`. Controlled when given. */
|
|
45
|
+
selectedIndex?: number | null;
|
|
46
|
+
/** Called with a tapped slice's index, or `null` for a tap outside every slice. */
|
|
47
|
+
onSelect?: (index: number | null) => void;
|
|
48
|
+
className?: string;
|
|
49
|
+
/** Named on the frame, so a capture flow or a test can find the plot. */
|
|
50
|
+
testID?: string;
|
|
51
|
+
children?: ReactNode;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** The field names the engine rows are built with. */
|
|
55
|
+
const ENGINE_NAME_KEY = "name";
|
|
56
|
+
const ENGINE_VALUE_KEY = "value";
|
|
57
|
+
|
|
58
|
+
type EngineRow = { readonly name: string; readonly value: number };
|
|
59
|
+
|
|
60
|
+
function PieChartRoot({
|
|
61
|
+
config,
|
|
62
|
+
data,
|
|
63
|
+
nameKey,
|
|
64
|
+
valueKey,
|
|
65
|
+
size = "md",
|
|
66
|
+
innerRadius = PIE_DEFAULT_INNER_RADIUS,
|
|
67
|
+
startAngle = 0,
|
|
68
|
+
selectedIndex,
|
|
69
|
+
onSelect,
|
|
70
|
+
className,
|
|
71
|
+
testID,
|
|
72
|
+
children,
|
|
73
|
+
}: PieChartProps): ReactElement {
|
|
74
|
+
const slots = chartVariants({ size });
|
|
75
|
+
const [frame, setFrame] = useState({ width: 0, height: 0 });
|
|
76
|
+
|
|
77
|
+
const resolved = useMemo(
|
|
78
|
+
() => resolvePieSeries(data, nameKey, valueKey, config ?? {}),
|
|
79
|
+
[data, nameKey, valueKey, config]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Every theme lookup happens here, above the canvas — see `Chart`.
|
|
83
|
+
const series = useChartPalette(resolved.series);
|
|
84
|
+
const surfaceColor = useThemeColor("background");
|
|
85
|
+
const font = useChartFont(chartAxisFontSize(size));
|
|
86
|
+
|
|
87
|
+
const [selected, select] = useControllableState<number | null>({
|
|
88
|
+
value: selectedIndex,
|
|
89
|
+
defaultValue: null,
|
|
90
|
+
onChange: onSelect,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// The engine draws the rows this resolved to, not the caller's data, so a
|
|
94
|
+
// dropped row is dropped everywhere and the slice indices, the series and
|
|
95
|
+
// the values all agree.
|
|
96
|
+
const rows = useMemo<EngineRow[]>(
|
|
97
|
+
() => series.map((entry, index) => ({ name: entry.label, value: resolved.values[index] ?? 0 })),
|
|
98
|
+
[series, resolved.values]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const innerRadiusSpec = pieInnerRadiusSpec(innerRadius);
|
|
102
|
+
|
|
103
|
+
// The same geometry the engine resolves, resolved again here for the parts
|
|
104
|
+
// that live outside the canvas: the tooltip needs a slice's bisector, and
|
|
105
|
+
// the engine's context stops at the canvas boundary.
|
|
106
|
+
const slices = useMemo<PieSliceData[]>(() => {
|
|
107
|
+
const layout = resolvePolarLayout({ canvas: frame, innerRadius: innerRadiusSpec });
|
|
108
|
+
return resolveSlices({
|
|
109
|
+
values: resolved.values,
|
|
110
|
+
labels: series.map((entry) => entry.label),
|
|
111
|
+
layout,
|
|
112
|
+
startAngle,
|
|
113
|
+
});
|
|
114
|
+
}, [frame, innerRadiusSpec, resolved.values, series, startAngle]);
|
|
115
|
+
|
|
116
|
+
const value = useMemo<PieChartContextValue>(
|
|
117
|
+
() => ({
|
|
118
|
+
series,
|
|
119
|
+
values: resolved.values,
|
|
120
|
+
total: resolved.total,
|
|
121
|
+
slices,
|
|
122
|
+
slots,
|
|
123
|
+
size,
|
|
124
|
+
surfaceColor,
|
|
125
|
+
frame,
|
|
126
|
+
nameKey,
|
|
127
|
+
valueKey,
|
|
128
|
+
innerRadius,
|
|
129
|
+
selectedIndex: selected,
|
|
130
|
+
select,
|
|
131
|
+
}),
|
|
132
|
+
[series, resolved, slices, slots, size, surfaceColor, frame, nameKey, valueKey, innerRadius, selected, select]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const { canvas, overlay, below, selectable } = useMemo(() => partitionPieChildren(children), [children]);
|
|
136
|
+
|
|
137
|
+
const onLayout = (event: LayoutChangeEvent): void => {
|
|
138
|
+
const { width, height } = event.nativeEvent.layout;
|
|
139
|
+
setFrame((current) => (current.width === width && current.height === height ? current : { width, height }));
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// A tap selects only when something would show it: a tooltip, a caller's
|
|
143
|
+
// handler, or a controlled selection. Otherwise the chart takes no gesture
|
|
144
|
+
// at all, and a pie in a list is nothing more than a picture.
|
|
145
|
+
const onSlicePress = selectable || onSelect !== undefined || selectedIndex !== undefined ? select : undefined;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<PieChartProvider value={value}>
|
|
149
|
+
<View className={slots.root({ className })}>
|
|
150
|
+
<View className={slots.frame()} onLayout={onLayout} testID={testID}>
|
|
151
|
+
<PolarChart
|
|
152
|
+
data={rows}
|
|
153
|
+
font={font}
|
|
154
|
+
innerRadius={innerRadiusSpec}
|
|
155
|
+
labelKey={ENGINE_NAME_KEY}
|
|
156
|
+
onSlicePress={onSlicePress}
|
|
157
|
+
selectedIndex={selected}
|
|
158
|
+
startAngle={startAngle}
|
|
159
|
+
valueKey={ENGINE_VALUE_KEY}
|
|
160
|
+
>
|
|
161
|
+
<PieChartProvider value={value}>{canvas}</PieChartProvider>
|
|
162
|
+
</PolarChart>
|
|
163
|
+
{overlay}
|
|
164
|
+
</View>
|
|
165
|
+
{below}
|
|
166
|
+
</View>
|
|
167
|
+
</PieChartProvider>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Splits the children by where they have to be mounted, the way `Chart` does.
|
|
173
|
+
*
|
|
174
|
+
* Slices and labels are Skia marks and go inside the canvas; the centre and
|
|
175
|
+
* the tooltip are React Native views floated over it; a legend, or anything
|
|
176
|
+
* else, sits under it. Matching is by component identity, so a part must be a
|
|
177
|
+
* direct child of `<PieChart>` — an array from `.map()` is fine, a part wrapped
|
|
178
|
+
* in a caller's own component is not.
|
|
179
|
+
*
|
|
180
|
+
* It also reports whether a tooltip was placed, because that is what decides
|
|
181
|
+
* whether a tap should select anything.
|
|
182
|
+
*/
|
|
183
|
+
function partitionPieChildren(children: ReactNode): {
|
|
184
|
+
canvas: ReactNode[];
|
|
185
|
+
overlay: ReactNode[];
|
|
186
|
+
below: ReactNode[];
|
|
187
|
+
selectable: boolean;
|
|
188
|
+
} {
|
|
189
|
+
const canvas: ReactNode[] = [];
|
|
190
|
+
const overlay: ReactNode[] = [];
|
|
191
|
+
const below: ReactNode[] = [];
|
|
192
|
+
let selectable = false;
|
|
193
|
+
|
|
194
|
+
for (const child of Children.toArray(children)) {
|
|
195
|
+
if (!isValidElement(child)) continue;
|
|
196
|
+
if (child.type === PieChartSlice || child.type === PieChartLabel) canvas.push(child);
|
|
197
|
+
else if (child.type === PieChartCenter) overlay.push(child);
|
|
198
|
+
else if (child.type === PieChartTooltip) {
|
|
199
|
+
overlay.push(child);
|
|
200
|
+
selectable = true;
|
|
201
|
+
} else below.push(child);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { canvas, overlay, below, selectable };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A pie or donut, wearing the theme's five-colour series ramp.
|
|
209
|
+
*
|
|
210
|
+
* A second root beside `Chart` rather than a part of it, because a pie shares
|
|
211
|
+
* nothing with a cartesian plot on screen — no axes, no grid, no scrub — and
|
|
212
|
+
* only the palette, the legend and the font underneath. Its categories are its
|
|
213
|
+
* **rows**: the first row is `chart-1`, the second `chart-2`, and a `config`
|
|
214
|
+
* entry keyed by a row's name overrides that one row's label or colour.
|
|
215
|
+
*
|
|
216
|
+
* `PieChart.Slice` and `PieChart.Label` draw into the canvas; `PieChart.Center`
|
|
217
|
+
* and `PieChart.Tooltip` float over it as React Native views; `PieChart.Legend`
|
|
218
|
+
* sits beneath. The tooltip answers a tap rather than a scrub — tap a slice to
|
|
219
|
+
* select it and dim the rest, tap outside to clear — and `selectedIndex` with
|
|
220
|
+
* `onSelect` makes the selection controlled.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* <PieChart data={rows} nameKey="browser" valueKey="visitors">
|
|
224
|
+
* <PieChart.Slice />
|
|
225
|
+
* <PieChart.Legend />
|
|
226
|
+
* </PieChart>
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* <PieChart data={rows} innerRadius={0.6} nameKey="browser" valueKey="visitors">
|
|
230
|
+
* <PieChart.Slice />
|
|
231
|
+
* <PieChart.Center label="Visitors" value="1,125" />
|
|
232
|
+
* <PieChart.Tooltip />
|
|
233
|
+
* </PieChart>
|
|
234
|
+
*/
|
|
235
|
+
export const PieChart = Object.assign(PieChartRoot, {
|
|
236
|
+
/** Every slice, in the series' colours, with a hairline between them. */
|
|
237
|
+
Slice: PieChartSlice,
|
|
238
|
+
/** A percentage — or a value, or the name — on every slice wide enough to hold one. */
|
|
239
|
+
Label: PieChartLabel,
|
|
240
|
+
/** A headline figure and a caption in the hole of a donut. A React Native view. */
|
|
241
|
+
Center: PieChartCenter,
|
|
242
|
+
/** A readout for the tapped slice. A React Native view, over the canvas. */
|
|
243
|
+
Tooltip: PieChartTooltip,
|
|
244
|
+
/** A swatch and a label per slice, under the chart. */
|
|
245
|
+
Legend: ChartLegend,
|
|
246
|
+
displayName: "DelacourUI.PieChart",
|
|
247
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ChartFont, useSystemFont } from "@delacour/react-native-charts";
|
|
2
|
+
import { useCSSVariable } from "uniwind";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The font Skia draws axis labels with.
|
|
6
|
+
*
|
|
7
|
+
* The family comes from `--font-sans`, which `theme.css` declares per platform
|
|
8
|
+
* under `@variant ios` / `@variant android`. It is deliberately **not** aliased
|
|
9
|
+
* through `@theme inline` — aliasing a font would redefine each name as itself
|
|
10
|
+
* and every `font-*` utility would silently draw nothing — so the raw variable
|
|
11
|
+
* is the only thing there to read, which is exactly what `useCSSVariable` takes.
|
|
12
|
+
*
|
|
13
|
+
* An unresolved family is not a failure. `matchFont` then picks the platform's
|
|
14
|
+
* own default, which is what `--font-sans` names on both platforms anyway; the
|
|
15
|
+
* variable only matters once an app overrides it.
|
|
16
|
+
*
|
|
17
|
+
* No font file ships with this package or with `@delacour/react-native-charts`. That is the
|
|
18
|
+
* same choice `theme.css` already made for the rest of the type: OS-bundled
|
|
19
|
+
* families mean nothing has to be loaded through `expo-font` and a consuming
|
|
20
|
+
* app needs no config plugin.
|
|
21
|
+
*/
|
|
22
|
+
export function useChartFont(size: number): ChartFont {
|
|
23
|
+
const family = useCSSVariable("--font-sans") as string | undefined;
|
|
24
|
+
return useSystemFont(family, size);
|
|
25
|
+
}
|