@misoto22/design 0.5.0 → 0.6.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/README.md +124 -0
- package/bin/misoto22-design.mjs +205 -0
- package/dist/agent/Accordion.md +41 -0
- package/dist/agent/Alert.md +42 -0
- package/dist/agent/AppShell.md +40 -0
- package/dist/agent/AreaChart.md +68 -0
- package/dist/agent/Article.md +41 -0
- package/dist/agent/Avatar.md +32 -0
- package/dist/agent/Badge.md +40 -0
- package/dist/agent/BarChart.md +65 -0
- package/dist/agent/BarList.md +40 -0
- package/dist/agent/BigNumber.md +45 -0
- package/dist/agent/BoxPlot.md +58 -0
- package/dist/agent/Breadcrumb.md +32 -0
- package/dist/agent/BulletChart.md +48 -0
- package/dist/agent/Button.md +56 -0
- package/dist/agent/Calendar.md +42 -0
- package/dist/agent/Card.md +49 -0
- package/dist/agent/Checkbox.md +35 -0
- package/dist/agent/Collapsible.md +46 -0
- package/dist/agent/Combobox.md +50 -0
- package/dist/agent/Command.md +93 -0
- package/dist/agent/ComposedChart.md +57 -0
- package/dist/agent/ContextMenu.md +59 -0
- package/dist/agent/DatePicker.md +75 -0
- package/dist/agent/Diagram.md +46 -0
- package/dist/agent/Dialog.md +52 -0
- package/dist/agent/DropdownMenu.md +70 -0
- package/dist/agent/EmptyState.md +37 -0
- package/dist/agent/ErrorState.md +41 -0
- package/dist/agent/Facet.md +82 -0
- package/dist/agent/Field.md +37 -0
- package/dist/agent/FigureBand.md +37 -0
- package/dist/agent/FloatingIconButton.md +38 -0
- package/dist/agent/FunnelChart.md +52 -0
- package/dist/agent/Heatmap.md +53 -0
- package/dist/agent/Histogram.md +63 -0
- package/dist/agent/Input.md +30 -0
- package/dist/agent/Kbd.md +29 -0
- package/dist/agent/LineChart.md +60 -0
- package/dist/agent/LinkArrow.md +29 -0
- package/dist/agent/NativeSelect.md +40 -0
- package/dist/agent/NavItem.md +35 -0
- package/dist/agent/Pagination.md +43 -0
- package/dist/agent/PieChart.md +55 -0
- package/dist/agent/Popover.md +57 -0
- package/dist/agent/Progress.md +33 -0
- package/dist/agent/RadarChart.md +54 -0
- package/dist/agent/RadialChart.md +57 -0
- package/dist/agent/RadioGroup.md +44 -0
- package/dist/agent/SankeyChart.md +61 -0
- package/dist/agent/ScatterChart.md +54 -0
- package/dist/agent/ScrollArea.md +40 -0
- package/dist/agent/SearchableMenu.md +46 -0
- package/dist/agent/Select.md +75 -0
- package/dist/agent/Separator.md +35 -0
- package/dist/agent/Sheet.md +57 -0
- package/dist/agent/Skeleton.md +73 -0
- package/dist/agent/Slider.md +39 -0
- package/dist/agent/Sparkline.md +45 -0
- package/dist/agent/Spinner.md +39 -0
- package/dist/agent/StatusDot.md +36 -0
- package/dist/agent/StatusPill.md +28 -0
- package/dist/agent/Steps.md +44 -0
- package/dist/agent/Switch.md +29 -0
- package/dist/agent/Table.md +85 -0
- package/dist/agent/Tabs.md +48 -0
- package/dist/agent/Tag.md +35 -0
- package/dist/agent/Textarea.md +26 -0
- package/dist/agent/Toast.md +26 -0
- package/dist/agent/ToggleGroup.md +44 -0
- package/dist/agent/Tooltip.md +51 -0
- package/dist/agent/TreemapChart.md +52 -0
- package/dist/agent/WaterfallChart.md +62 -0
- package/dist/agent/catalog.json +1791 -0
- package/dist/agent/index.md +131 -0
- package/dist/charts/AreaChart/AreaChart.d.ts +191 -0
- package/dist/charts/AreaChart/AreaChart.js +480 -0
- package/dist/charts/AreaChart/AreaChart.js.map +1 -0
- package/dist/charts/BarChart/BarChart.d.ts +179 -0
- package/dist/charts/BarChart/BarChart.js +591 -0
- package/dist/charts/BarChart/BarChart.js.map +1 -0
- package/dist/charts/BarList/BarList.d.ts +57 -0
- package/dist/charts/BarList/BarList.js +71 -0
- package/dist/charts/BarList/BarList.js.map +1 -0
- package/dist/charts/BigNumber/BigNumber.d.ts +56 -0
- package/dist/charts/BigNumber/BigNumber.js +31 -0
- package/dist/charts/BigNumber/BigNumber.js.map +1 -0
- package/dist/charts/BoxPlot/BoxPlot.d.ts +177 -0
- package/dist/charts/BoxPlot/BoxPlot.js +495 -0
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -0
- package/dist/charts/BulletChart/BulletChart.d.ts +100 -0
- package/dist/charts/BulletChart/BulletChart.js +162 -0
- package/dist/charts/BulletChart/BulletChart.js.map +1 -0
- package/dist/charts/ComposedChart/ComposedChart.d.ts +171 -0
- package/dist/charts/ComposedChart/ComposedChart.js +667 -0
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -0
- package/dist/charts/Facet/Facet.d.ts +240 -0
- package/dist/charts/Facet/Facet.js +133 -0
- package/dist/charts/Facet/Facet.js.map +1 -0
- package/dist/charts/Facet/panels.d.ts +187 -0
- package/dist/charts/Facet/panels.js +203 -0
- package/dist/charts/Facet/panels.js.map +1 -0
- package/dist/charts/FunnelChart/FunnelChart.d.ts +101 -0
- package/dist/charts/FunnelChart/FunnelChart.js +147 -0
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -0
- package/dist/charts/Heatmap/Heatmap.d.ts +78 -0
- package/dist/charts/Heatmap/Heatmap.js +108 -0
- package/dist/charts/Heatmap/Heatmap.js.map +1 -0
- package/dist/charts/Histogram/Histogram.d.ts +165 -0
- package/dist/charts/Histogram/Histogram.js +350 -0
- package/dist/charts/Histogram/Histogram.js.map +1 -0
- package/dist/charts/LineChart/LineChart.d.ts +167 -0
- package/dist/charts/LineChart/LineChart.js +486 -0
- package/dist/charts/LineChart/LineChart.js.map +1 -0
- package/dist/charts/PieChart/PieChart.d.ts +128 -0
- package/dist/charts/PieChart/PieChart.js +309 -0
- package/dist/charts/PieChart/PieChart.js.map +1 -0
- package/dist/charts/RadarChart/RadarChart.d.ts +123 -0
- package/dist/charts/RadarChart/RadarChart.js +299 -0
- package/dist/charts/RadarChart/RadarChart.js.map +1 -0
- package/dist/charts/RadialChart/RadialChart.d.ts +124 -0
- package/dist/charts/RadialChart/RadialChart.js +257 -0
- package/dist/charts/RadialChart/RadialChart.js.map +1 -0
- package/dist/charts/SankeyChart/SankeyChart.d.ts +137 -0
- package/dist/charts/SankeyChart/SankeyChart.js +501 -0
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -0
- package/dist/charts/ScatterChart/ScatterChart.d.ts +128 -0
- package/dist/charts/ScatterChart/ScatterChart.js +269 -0
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -0
- package/dist/charts/Sparkline/Sparkline.d.ts +53 -0
- package/dist/charts/Sparkline/Sparkline.js +93 -0
- package/dist/charts/Sparkline/Sparkline.js.map +1 -0
- package/dist/charts/TreemapChart/TreemapChart.d.ts +93 -0
- package/dist/charts/TreemapChart/TreemapChart.js +118 -0
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -0
- package/dist/charts/WaterfallChart/WaterfallChart.d.ts +164 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js +319 -0
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -0
- package/dist/charts/index.d.ts +41 -0
- package/dist/charts/index.js +128 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/lib/annotations.d.ts +103 -0
- package/dist/charts/lib/annotations.js +99 -0
- package/dist/charts/lib/annotations.js.map +1 -0
- package/dist/charts/lib/axis.d.ts +18 -0
- package/dist/charts/lib/axis.js +21 -0
- package/dist/charts/lib/axis.js.map +1 -0
- package/dist/charts/lib/background.d.ts +33 -0
- package/dist/charts/lib/background.js +173 -0
- package/dist/charts/lib/background.js.map +1 -0
- package/dist/charts/lib/brush.d.ts +94 -0
- package/dist/charts/lib/brush.js +535 -0
- package/dist/charts/lib/brush.js.map +1 -0
- package/dist/charts/lib/chart.d.ts +94 -0
- package/dist/charts/lib/chart.js +134 -0
- package/dist/charts/lib/chart.js.map +1 -0
- package/dist/charts/lib/dot.d.ts +29 -0
- package/dist/charts/lib/dot.js +78 -0
- package/dist/charts/lib/dot.js.map +1 -0
- package/dist/charts/lib/empty.d.ts +31 -0
- package/dist/charts/lib/empty.js +13 -0
- package/dist/charts/lib/empty.js.map +1 -0
- package/dist/charts/lib/export.d.ts +103 -0
- package/dist/charts/lib/export.js +204 -0
- package/dist/charts/lib/export.js.map +1 -0
- package/dist/charts/lib/figure.d.ts +61 -0
- package/dist/charts/lib/figure.js +59 -0
- package/dist/charts/lib/figure.js.map +1 -0
- package/dist/charts/lib/format.d.ts +46 -0
- package/dist/charts/lib/format.js +56 -0
- package/dist/charts/lib/format.js.map +1 -0
- package/dist/charts/lib/legend.d.ts +45 -0
- package/dist/charts/lib/legend.js +117 -0
- package/dist/charts/lib/legend.js.map +1 -0
- package/dist/charts/lib/loading.d.ts +48 -0
- package/dist/charts/lib/loading.js +105 -0
- package/dist/charts/lib/loading.js.map +1 -0
- package/dist/charts/lib/paint.d.ts +154 -0
- package/dist/charts/lib/paint.js +551 -0
- package/dist/charts/lib/paint.js.map +1 -0
- package/dist/charts/lib/sonify-control.d.ts +163 -0
- package/dist/charts/lib/sonify-control.js +172 -0
- package/dist/charts/lib/sonify-control.js.map +1 -0
- package/dist/charts/lib/sonify.d.ts +248 -0
- package/dist/charts/lib/sonify.js +262 -0
- package/dist/charts/lib/sonify.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +143 -0
- package/dist/charts/lib/toolbar.js +156 -0
- package/dist/charts/lib/toolbar.js.map +1 -0
- package/dist/charts/lib/tooltip.d.ts +59 -0
- package/dist/charts/lib/tooltip.js +135 -0
- package/dist/charts/lib/tooltip.js.map +1 -0
- package/dist/charts/lib/values.d.ts +56 -0
- package/dist/charts/lib/values.js +71 -0
- package/dist/charts/lib/values.js.map +1 -0
- package/dist/charts/lib/zoom.d.ts +139 -0
- package/dist/charts/lib/zoom.js +314 -0
- package/dist/charts/lib/zoom.js.map +1 -0
- package/dist/semantic.css +19 -0
- package/dist/styles.css +1 -1
- package/dist/themes.css +50 -0
- package/dist/tokens.css +118 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +101 -0
- package/dist/tokens.json +101 -0
- package/package.json +26 -4
- package/skills/misoto22-design/SKILL.md +171 -0
- package/skills/misoto22-design/evals/claims.json +95 -0
- package/skills/misoto22-design/rules/a11y.md +115 -0
- package/skills/misoto22-design/rules/composition.md +108 -0
- package/skills/misoto22-design/rules/forms.md +128 -0
- package/skills/misoto22-design/rules/naming.md +155 -0
- package/skills/misoto22-design/rules/tokens.md +131 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grouping, ordering, capping and domain arithmetic behind `<Facet>`.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the component on purpose. Everything here is a pure function over
|
|
5
|
+
* plain rows, which means the part of faceting that is actually easy to get
|
|
6
|
+
* wrong — the shared domain, the sort direction, what happens to the tail past
|
|
7
|
+
* the cap — is unit-testable without a DOM, a rendering engine or a layout.
|
|
8
|
+
*
|
|
9
|
+
* Named `panels` rather than `facet` because macOS and Windows resolve paths
|
|
10
|
+
* case-insensitively: a `facet.ts` beside `Facet.tsx` makes `./Facet/Facet`
|
|
11
|
+
* ambiguous, and TypeScript resolves it to the wrong one of the two.
|
|
12
|
+
*/
|
|
13
|
+
/** How the panels are ordered. */
|
|
14
|
+
type FacetSortKey = 'name' | 'input' | 'min' | 'max' | 'sum' | 'mean' | 'last';
|
|
15
|
+
/**
|
|
16
|
+
* The panel order: a named key, an explicit list of group names, or a
|
|
17
|
+
* comparator over the groups' own statistics.
|
|
18
|
+
*
|
|
19
|
+
* An explicit list is the escape hatch for an order the data does not carry —
|
|
20
|
+
* a funnel's stages, a week's days, a severity scale. Names the list does not
|
|
21
|
+
* mention keep their relative order and follow the ones it does.
|
|
22
|
+
*/
|
|
23
|
+
type FacetSort<TRow> = FacetSortKey | string[] | ((a: FacetGroup<TRow>, b: FacetGroup<TRow>) => number);
|
|
24
|
+
/** Whether every panel is drawn against one domain or against its own. */
|
|
25
|
+
type FacetScales = 'shared' | 'independent';
|
|
26
|
+
/** What happens to the groups past the cap. */
|
|
27
|
+
type FacetOverflow = 'note' | 'fold';
|
|
28
|
+
/** What one group's numbers add up to. The sort and the domain read these. */
|
|
29
|
+
interface FacetStats {
|
|
30
|
+
/** The smallest finite value across the group's rows and value fields. */
|
|
31
|
+
min: number;
|
|
32
|
+
/** The largest finite value across the group's rows and value fields. */
|
|
33
|
+
max: number;
|
|
34
|
+
/** Every finite value added together — the group's grand total. */
|
|
35
|
+
sum: number;
|
|
36
|
+
/** `sum / count`, or 0 when the group holds no finite value. */
|
|
37
|
+
mean: number;
|
|
38
|
+
/**
|
|
39
|
+
* The last row that holds a finite number, summed across the value fields.
|
|
40
|
+
*
|
|
41
|
+
* "Where did this end up", which is the question a grid of trends is usually
|
|
42
|
+
* sorted by — and a different question from "how big was it overall".
|
|
43
|
+
*/
|
|
44
|
+
last: number;
|
|
45
|
+
/** How many finite numbers the rest of these were computed from. */
|
|
46
|
+
count: number;
|
|
47
|
+
}
|
|
48
|
+
/** A group of rows and what its numbers add up to — what a custom sort sees. */
|
|
49
|
+
interface FacetGroup<TRow> {
|
|
50
|
+
/** The group's value on the `by` field, as text. Also the panel's label. */
|
|
51
|
+
name: string;
|
|
52
|
+
/** The rows this panel draws. */
|
|
53
|
+
rows: TRow[];
|
|
54
|
+
stats: FacetStats;
|
|
55
|
+
}
|
|
56
|
+
/** One panel of the grid: a group, and the domain it draws against. */
|
|
57
|
+
interface FacetPanel<TRow> extends FacetGroup<TRow> {
|
|
58
|
+
/**
|
|
59
|
+
* The `[min, max]` this panel's value axis should use.
|
|
60
|
+
*
|
|
61
|
+
* Under the default shared scales this is the SAME array for every panel,
|
|
62
|
+
* computed across all of them. Handing it to the panel's value axis is the
|
|
63
|
+
* whole point of the component — see the note on `FacetProps.scales`.
|
|
64
|
+
*/
|
|
65
|
+
domain: [number, number];
|
|
66
|
+
/** Position in the grid, from 0. */
|
|
67
|
+
index: number;
|
|
68
|
+
/** How many panels the grid holds, so a panel can say "3 of 12". */
|
|
69
|
+
total: number;
|
|
70
|
+
/** How many source groups this panel stands for. Above 1 only for the fold. */
|
|
71
|
+
size: number;
|
|
72
|
+
/** True for the one panel the capped tail was folded into. */
|
|
73
|
+
isOther: boolean;
|
|
74
|
+
}
|
|
75
|
+
interface FacetOptions<TRow> {
|
|
76
|
+
/** The row field the data is split on. */
|
|
77
|
+
by: keyof TRow & string;
|
|
78
|
+
/** The row field, or fields, holding the numbers. */
|
|
79
|
+
value: (keyof TRow & string) | (keyof TRow & string)[];
|
|
80
|
+
/** The shared category field — the x axis. Only the fold needs it. */
|
|
81
|
+
xDataKey?: keyof TRow & string;
|
|
82
|
+
scales?: FacetScales;
|
|
83
|
+
/** Overrides the computed domain outright. */
|
|
84
|
+
domain?: [number, number];
|
|
85
|
+
includeZero?: boolean;
|
|
86
|
+
nice?: boolean;
|
|
87
|
+
sort?: FacetSort<TRow>;
|
|
88
|
+
order?: 'asc' | 'desc';
|
|
89
|
+
limit?: number | false;
|
|
90
|
+
overflow?: FacetOverflow;
|
|
91
|
+
otherLabel?: string;
|
|
92
|
+
}
|
|
93
|
+
interface FacetResult<TRow> {
|
|
94
|
+
/** The panels to render, in order, capped. */
|
|
95
|
+
panels: FacetPanel<TRow>[];
|
|
96
|
+
/** How many groups the data held, before the cap. */
|
|
97
|
+
total: number;
|
|
98
|
+
/** How many groups the cap left out — folded or simply not drawn. */
|
|
99
|
+
hidden: number;
|
|
100
|
+
/** The one domain every panel shares, or null under independent scales. */
|
|
101
|
+
domain: [number, number] | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Splits rows into groups, in the order the groups first appear.
|
|
105
|
+
*
|
|
106
|
+
* A row whose `by` field is null or undefined is dropped rather than collected
|
|
107
|
+
* under an "undefined" panel: a row that says nothing about which group it
|
|
108
|
+
* belongs to cannot be faceted, and a panel labelled `undefined` is a defect
|
|
109
|
+
* the reader is asked to interpret.
|
|
110
|
+
*/
|
|
111
|
+
declare function groupRows<TRow extends Record<string, unknown>>(data: TRow[], by: keyof TRow & string): {
|
|
112
|
+
name: string;
|
|
113
|
+
rows: TRow[];
|
|
114
|
+
}[];
|
|
115
|
+
/**
|
|
116
|
+
* One group's statistics.
|
|
117
|
+
*
|
|
118
|
+
* Empty and all-non-numeric groups return zeroes with `count: 0` rather than
|
|
119
|
+
* `Infinity`, so a sort never has to special-case them and a domain built from
|
|
120
|
+
* them is still a pair of numbers.
|
|
121
|
+
*/
|
|
122
|
+
declare function statsOf<TRow extends Record<string, unknown>>(rows: TRow[], value: FacetOptions<TRow>['value']): FacetStats;
|
|
123
|
+
interface DomainOptions {
|
|
124
|
+
/**
|
|
125
|
+
* Pulls the domain out to include zero.
|
|
126
|
+
*
|
|
127
|
+
* On by default, and the same default Recharts applies on its own: a bar or
|
|
128
|
+
* an area read against a truncated baseline overstates every difference in
|
|
129
|
+
* the grid, and a grid exists to be compared. Turn it off when the values
|
|
130
|
+
* live far from zero and the differences between them ARE the reading —
|
|
131
|
+
* temperatures, latencies, ratings out of five.
|
|
132
|
+
*/
|
|
133
|
+
includeZero?: boolean;
|
|
134
|
+
/** Rounds the domain out to round numbers, so the ticks are readable. */
|
|
135
|
+
nice?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A `[min, max]` extended to something an axis can label.
|
|
139
|
+
*
|
|
140
|
+
* A degenerate extent — one row, or a group where every value is identical —
|
|
141
|
+
* is given a span rather than returned as a zero-height plot.
|
|
142
|
+
*/
|
|
143
|
+
declare function niceDomain(extent: [number, number], { includeZero, nice }?: DomainOptions): [number, number];
|
|
144
|
+
/** The extent across every group — the shared domain, before it is rounded. */
|
|
145
|
+
declare function sharedExtent(stats: FacetStats[]): [number, number];
|
|
146
|
+
/**
|
|
147
|
+
* Orders the panels.
|
|
148
|
+
*
|
|
149
|
+
* `order` is optional because the natural direction depends on what is being
|
|
150
|
+
* sorted: names read A→Z, a statistic reads biggest first. Passing it makes the
|
|
151
|
+
* direction explicit either way.
|
|
152
|
+
*
|
|
153
|
+
* The sort is stable, so groups that tie keep the order the data gave them.
|
|
154
|
+
*/
|
|
155
|
+
declare function sortGroups<TRow, TGroup extends FacetGroup<TRow>>(groups: TGroup[], sort: FacetSort<TRow>, order?: 'asc' | 'desc'): TGroup[];
|
|
156
|
+
/**
|
|
157
|
+
* Sums the capped tail into one group, aligned on the category axis.
|
|
158
|
+
*
|
|
159
|
+
* With `xDataKey` the tail's rows are added together at each category, which is
|
|
160
|
+
* what makes the "Other" panel a readable series rather than a scribble. Without
|
|
161
|
+
* one the rows are concatenated in the order they arrived, which is only
|
|
162
|
+
* meaningful for a chart that bins its own x — so pass the key.
|
|
163
|
+
*/
|
|
164
|
+
declare function foldGroups<TRow extends Record<string, unknown>>(tail: {
|
|
165
|
+
name: string;
|
|
166
|
+
rows: TRow[];
|
|
167
|
+
}[], { by, value, xDataKey, label, }: {
|
|
168
|
+
by: keyof TRow & string;
|
|
169
|
+
value: FacetOptions<TRow>['value'];
|
|
170
|
+
xDataKey?: keyof TRow & string;
|
|
171
|
+
label: string;
|
|
172
|
+
}): {
|
|
173
|
+
name: string;
|
|
174
|
+
rows: TRow[];
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* The whole pipeline: group, summarise, order, cap, and hand every panel a
|
|
178
|
+
* domain.
|
|
179
|
+
*
|
|
180
|
+
* One function rather than four calls at the call site, because the ORDER of
|
|
181
|
+
* those four is itself a decision — the domain is computed across the groups
|
|
182
|
+
* that survive the cap, so a folded "Other" panel cannot push the shared scale
|
|
183
|
+
* past every panel it is meant to be compared against.
|
|
184
|
+
*/
|
|
185
|
+
declare function buildPanels<TRow extends Record<string, unknown>>(data: TRow[], options: FacetOptions<TRow>): FacetResult<TRow>;
|
|
186
|
+
|
|
187
|
+
export { type DomainOptions, type FacetGroup, type FacetOptions, type FacetOverflow, type FacetPanel, type FacetResult, type FacetScales, type FacetSort, type FacetSortKey, type FacetStats, buildPanels, foldGroups, groupRows, niceDomain, sharedExtent, sortGroups, statsOf };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
function keys(value) {
|
|
2
|
+
return Array.isArray(value) ? value : [value];
|
|
3
|
+
}
|
|
4
|
+
function numbers(row, fields) {
|
|
5
|
+
const found = [];
|
|
6
|
+
for (const field of fields) {
|
|
7
|
+
const raw = row[field];
|
|
8
|
+
if (raw === null || raw === void 0 || raw === "") continue;
|
|
9
|
+
const entry = Number(raw);
|
|
10
|
+
if (Number.isFinite(entry)) found.push(entry);
|
|
11
|
+
}
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
function groupRows(data, by) {
|
|
15
|
+
const found = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const row of data) {
|
|
17
|
+
const raw = row[by];
|
|
18
|
+
if (raw === null || raw === void 0) continue;
|
|
19
|
+
const name = String(raw);
|
|
20
|
+
const rows = found.get(name);
|
|
21
|
+
if (rows) rows.push(row);
|
|
22
|
+
else found.set(name, [row]);
|
|
23
|
+
}
|
|
24
|
+
return [...found].map(([name, rows]) => ({ name, rows }));
|
|
25
|
+
}
|
|
26
|
+
function statsOf(rows, value) {
|
|
27
|
+
const fields = keys(value);
|
|
28
|
+
let min = Number.POSITIVE_INFINITY;
|
|
29
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
30
|
+
let sum = 0;
|
|
31
|
+
let count = 0;
|
|
32
|
+
let last = 0;
|
|
33
|
+
for (const row of rows) {
|
|
34
|
+
const found = numbers(row, fields);
|
|
35
|
+
if (found.length === 0) continue;
|
|
36
|
+
last = found.reduce((total, entry) => total + entry, 0);
|
|
37
|
+
for (const entry of found) {
|
|
38
|
+
if (entry < min) min = entry;
|
|
39
|
+
if (entry > max) max = entry;
|
|
40
|
+
sum += entry;
|
|
41
|
+
count += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (count === 0) return { min: 0, max: 0, sum: 0, mean: 0, last: 0, count: 0 };
|
|
45
|
+
return { min, max, sum, mean: sum / count, last, count };
|
|
46
|
+
}
|
|
47
|
+
function tickStep(span, count) {
|
|
48
|
+
if (!(span > 0)) return 1;
|
|
49
|
+
const raw = span / count;
|
|
50
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
51
|
+
const normalised = raw / magnitude;
|
|
52
|
+
const step = normalised >= 7.5 ? 10 : normalised >= 3.5 ? 5 : normalised >= 1.5 ? 2 : 1;
|
|
53
|
+
return step * magnitude;
|
|
54
|
+
}
|
|
55
|
+
function clean(value) {
|
|
56
|
+
return Number(value.toPrecision(12));
|
|
57
|
+
}
|
|
58
|
+
function niceDomain(extent, { includeZero = true, nice = true } = {}) {
|
|
59
|
+
let [min, max] = extent;
|
|
60
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 1];
|
|
61
|
+
if (includeZero) {
|
|
62
|
+
min = Math.min(min, 0);
|
|
63
|
+
max = Math.max(max, 0);
|
|
64
|
+
}
|
|
65
|
+
if (max === min) max = min + (Math.abs(min) || 1);
|
|
66
|
+
if (!nice) return [min, max];
|
|
67
|
+
const step = tickStep(max - min, 5);
|
|
68
|
+
return [clean(Math.floor(min / step) * step), clean(Math.ceil(max / step) * step)];
|
|
69
|
+
}
|
|
70
|
+
function sharedExtent(stats) {
|
|
71
|
+
const scored = stats.filter((entry) => entry.count > 0);
|
|
72
|
+
if (scored.length === 0) return [0, 1];
|
|
73
|
+
return [
|
|
74
|
+
Math.min(...scored.map((entry) => entry.min)),
|
|
75
|
+
Math.max(...scored.map((entry) => entry.max))
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function statistic(stats, key) {
|
|
79
|
+
switch (key) {
|
|
80
|
+
case "min":
|
|
81
|
+
return stats.min;
|
|
82
|
+
case "sum":
|
|
83
|
+
return stats.sum;
|
|
84
|
+
case "mean":
|
|
85
|
+
return stats.mean;
|
|
86
|
+
case "last":
|
|
87
|
+
return stats.last;
|
|
88
|
+
default:
|
|
89
|
+
return stats.max;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function sortGroups(groups, sort, order) {
|
|
93
|
+
const sorted = [...groups];
|
|
94
|
+
if (sort === "input") return order === "desc" ? sorted.reverse() : sorted;
|
|
95
|
+
const compare = comparator(sort);
|
|
96
|
+
const sign = (order ?? defaultOrder(sort)) === "desc" ? -1 : 1;
|
|
97
|
+
sorted.sort((a, b) => compare(a, b) * sign);
|
|
98
|
+
return sorted;
|
|
99
|
+
}
|
|
100
|
+
function defaultOrder(sort) {
|
|
101
|
+
if (typeof sort === "function" || Array.isArray(sort) || sort === "name") return "asc";
|
|
102
|
+
return "desc";
|
|
103
|
+
}
|
|
104
|
+
function comparator(sort) {
|
|
105
|
+
if (typeof sort === "function") return sort;
|
|
106
|
+
if (Array.isArray(sort)) {
|
|
107
|
+
const rank = new Map(sort.map((name, index) => [name, index]));
|
|
108
|
+
const place = (name) => rank.get(name) ?? Number.MAX_SAFE_INTEGER;
|
|
109
|
+
return (a, b) => place(a.name) - place(b.name);
|
|
110
|
+
}
|
|
111
|
+
if (sort === "name") {
|
|
112
|
+
return (a, b) => a.name.localeCompare(b.name, void 0, { numeric: true });
|
|
113
|
+
}
|
|
114
|
+
return (a, b) => statistic(a.stats, sort) - statistic(b.stats, sort);
|
|
115
|
+
}
|
|
116
|
+
function foldGroups(tail, {
|
|
117
|
+
by,
|
|
118
|
+
value,
|
|
119
|
+
xDataKey,
|
|
120
|
+
label
|
|
121
|
+
}) {
|
|
122
|
+
const rows = tail.flatMap((group) => group.rows);
|
|
123
|
+
if (!xDataKey) return { name: label, rows };
|
|
124
|
+
const fields = keys(value);
|
|
125
|
+
const bins = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
const category = String(row[xDataKey]);
|
|
128
|
+
let bin = bins.get(category);
|
|
129
|
+
if (!bin) {
|
|
130
|
+
bin = { [xDataKey]: row[xDataKey], [by]: label };
|
|
131
|
+
for (const field of fields) bin[field] = 0;
|
|
132
|
+
bins.set(category, bin);
|
|
133
|
+
}
|
|
134
|
+
for (const field of fields) {
|
|
135
|
+
const raw = row[field];
|
|
136
|
+
if (raw === null || raw === void 0 || raw === "") continue;
|
|
137
|
+
const entry = Number(raw);
|
|
138
|
+
if (Number.isFinite(entry)) bin[field] = Number(bin[field]) + entry;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { name: label, rows: [...bins.values()] };
|
|
142
|
+
}
|
|
143
|
+
function buildPanels(data, options) {
|
|
144
|
+
const {
|
|
145
|
+
by,
|
|
146
|
+
value,
|
|
147
|
+
xDataKey,
|
|
148
|
+
scales = "shared",
|
|
149
|
+
domain,
|
|
150
|
+
includeZero = true,
|
|
151
|
+
nice = true,
|
|
152
|
+
sort = "max",
|
|
153
|
+
order,
|
|
154
|
+
limit = 12,
|
|
155
|
+
overflow = "note",
|
|
156
|
+
otherLabel = "Other"
|
|
157
|
+
} = options;
|
|
158
|
+
const grouped = groupRows(data, by).map((group) => ({
|
|
159
|
+
...group,
|
|
160
|
+
stats: statsOf(group.rows, value)
|
|
161
|
+
}));
|
|
162
|
+
const total = grouped.length;
|
|
163
|
+
if (total === 0) return { panels: [], total: 0, hidden: 0, domain: null };
|
|
164
|
+
const ordered = sortGroups(grouped, sort, order);
|
|
165
|
+
const capped = typeof limit === "number" && limit >= 0 && ordered.length > limit;
|
|
166
|
+
const head = capped ? ordered.slice(0, limit) : ordered;
|
|
167
|
+
const tail = capped ? ordered.slice(limit) : [];
|
|
168
|
+
const folded = capped && overflow === "fold" ? foldGroups(tail, { by, value, xDataKey, label: otherLabel }) : void 0;
|
|
169
|
+
const shown = [
|
|
170
|
+
...head.map((group) => ({ ...group, size: 1, isOther: false })),
|
|
171
|
+
...folded ? [
|
|
172
|
+
{
|
|
173
|
+
...folded,
|
|
174
|
+
stats: statsOf(folded.rows, value),
|
|
175
|
+
size: tail.length,
|
|
176
|
+
isOther: true
|
|
177
|
+
}
|
|
178
|
+
] : []
|
|
179
|
+
];
|
|
180
|
+
const sharedDomain = domain ?? niceDomain(sharedExtent(shown.map((group) => group.stats)), { includeZero, nice });
|
|
181
|
+
const independent = scales === "independent" && !domain;
|
|
182
|
+
const panels = shown.map((group, index) => ({
|
|
183
|
+
name: group.name,
|
|
184
|
+
rows: group.rows,
|
|
185
|
+
domain: independent ? niceDomain([group.stats.min, group.stats.max], { includeZero, nice }) : sharedDomain,
|
|
186
|
+
stats: group.stats,
|
|
187
|
+
index,
|
|
188
|
+
total: shown.length,
|
|
189
|
+
size: group.size,
|
|
190
|
+
isOther: group.isOther
|
|
191
|
+
}));
|
|
192
|
+
return { panels, total, hidden: tail.length, domain: independent ? null : sharedDomain };
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
buildPanels,
|
|
196
|
+
foldGroups,
|
|
197
|
+
groupRows,
|
|
198
|
+
niceDomain,
|
|
199
|
+
sharedExtent,
|
|
200
|
+
sortGroups,
|
|
201
|
+
statsOf
|
|
202
|
+
};
|
|
203
|
+
//# sourceMappingURL=panels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/Facet/panels.ts"],"sourcesContent":["/**\n * The grouping, ordering, capping and domain arithmetic behind `<Facet>`.\n *\n * Kept out of the component on purpose. Everything here is a pure function over\n * plain rows, which means the part of faceting that is actually easy to get\n * wrong — the shared domain, the sort direction, what happens to the tail past\n * the cap — is unit-testable without a DOM, a rendering engine or a layout.\n *\n * Named `panels` rather than `facet` because macOS and Windows resolve paths\n * case-insensitively: a `facet.ts` beside `Facet.tsx` makes `./Facet/Facet`\n * ambiguous, and TypeScript resolves it to the wrong one of the two.\n */\n\n/** How the panels are ordered. */\nexport type FacetSortKey = 'name' | 'input' | 'min' | 'max' | 'sum' | 'mean' | 'last'\n\n/**\n * The panel order: a named key, an explicit list of group names, or a\n * comparator over the groups' own statistics.\n *\n * An explicit list is the escape hatch for an order the data does not carry —\n * a funnel's stages, a week's days, a severity scale. Names the list does not\n * mention keep their relative order and follow the ones it does.\n */\nexport type FacetSort<TRow> =\n | FacetSortKey\n | string[]\n | ((a: FacetGroup<TRow>, b: FacetGroup<TRow>) => number)\n\n/** Whether every panel is drawn against one domain or against its own. */\nexport type FacetScales = 'shared' | 'independent'\n\n/** What happens to the groups past the cap. */\nexport type FacetOverflow = 'note' | 'fold'\n\n/** What one group's numbers add up to. The sort and the domain read these. */\nexport interface FacetStats {\n /** The smallest finite value across the group's rows and value fields. */\n min: number\n /** The largest finite value across the group's rows and value fields. */\n max: number\n /** Every finite value added together — the group's grand total. */\n sum: number\n /** `sum / count`, or 0 when the group holds no finite value. */\n mean: number\n /**\n * The last row that holds a finite number, summed across the value fields.\n *\n * \"Where did this end up\", which is the question a grid of trends is usually\n * sorted by — and a different question from \"how big was it overall\".\n */\n last: number\n /** How many finite numbers the rest of these were computed from. */\n count: number\n}\n\n/** A group of rows and what its numbers add up to — what a custom sort sees. */\nexport interface FacetGroup<TRow> {\n /** The group's value on the `by` field, as text. Also the panel's label. */\n name: string\n /** The rows this panel draws. */\n rows: TRow[]\n stats: FacetStats\n}\n\n/** One panel of the grid: a group, and the domain it draws against. */\nexport interface FacetPanel<TRow> extends FacetGroup<TRow> {\n /**\n * The `[min, max]` this panel's value axis should use.\n *\n * Under the default shared scales this is the SAME array for every panel,\n * computed across all of them. Handing it to the panel's value axis is the\n * whole point of the component — see the note on `FacetProps.scales`.\n */\n domain: [number, number]\n /** Position in the grid, from 0. */\n index: number\n /** How many panels the grid holds, so a panel can say \"3 of 12\". */\n total: number\n /** How many source groups this panel stands for. Above 1 only for the fold. */\n size: number\n /** True for the one panel the capped tail was folded into. */\n isOther: boolean\n}\n\nexport interface FacetOptions<TRow> {\n /** The row field the data is split on. */\n by: keyof TRow & string\n /** The row field, or fields, holding the numbers. */\n value: (keyof TRow & string) | (keyof TRow & string)[]\n /** The shared category field — the x axis. Only the fold needs it. */\n xDataKey?: keyof TRow & string\n scales?: FacetScales\n /** Overrides the computed domain outright. */\n domain?: [number, number]\n includeZero?: boolean\n nice?: boolean\n sort?: FacetSort<TRow>\n order?: 'asc' | 'desc'\n limit?: number | false\n overflow?: FacetOverflow\n otherLabel?: string\n}\n\nexport interface FacetResult<TRow> {\n /** The panels to render, in order, capped. */\n panels: FacetPanel<TRow>[]\n /** How many groups the data held, before the cap. */\n total: number\n /** How many groups the cap left out — folded or simply not drawn. */\n hidden: number\n /** The one domain every panel shares, or null under independent scales. */\n domain: [number, number] | null\n}\n\n/** The value option, always as a list. */\nfunction keys<TRow>(value: FacetOptions<TRow>['value']): (keyof TRow & string)[] {\n return Array.isArray(value) ? value : [value]\n}\n\n/**\n * A row's finite numbers under the value fields, in field order.\n *\n * `null` and `''` are skipped before the coercion rather than after it, because\n * `Number(null)` is 0 — so a gap in the data would otherwise read as a series\n * that fell to zero, and both the domain and a sort by \"where it ended up\"\n * would be built on it.\n */\nfunction numbers<TRow>(row: TRow, fields: (keyof TRow & string)[]): number[] {\n const found: number[] = []\n for (const field of fields) {\n const raw = row[field]\n if (raw === null || raw === undefined || raw === '') continue\n const entry = Number(raw)\n if (Number.isFinite(entry)) found.push(entry)\n }\n return found\n}\n\n/**\n * Splits rows into groups, in the order the groups first appear.\n *\n * A row whose `by` field is null or undefined is dropped rather than collected\n * under an \"undefined\" panel: a row that says nothing about which group it\n * belongs to cannot be faceted, and a panel labelled `undefined` is a defect\n * the reader is asked to interpret.\n */\nexport function groupRows<TRow extends Record<string, unknown>>(\n data: TRow[],\n by: keyof TRow & string,\n): { name: string; rows: TRow[] }[] {\n const found = new Map<string, TRow[]>()\n\n for (const row of data) {\n const raw = row[by]\n if (raw === null || raw === undefined) continue\n const name = String(raw)\n const rows = found.get(name)\n if (rows) rows.push(row)\n else found.set(name, [row])\n }\n\n return [...found].map(([name, rows]) => ({ name, rows }))\n}\n\n/**\n * One group's statistics.\n *\n * Empty and all-non-numeric groups return zeroes with `count: 0` rather than\n * `Infinity`, so a sort never has to special-case them and a domain built from\n * them is still a pair of numbers.\n */\nexport function statsOf<TRow extends Record<string, unknown>>(\n rows: TRow[],\n value: FacetOptions<TRow>['value'],\n): FacetStats {\n const fields = keys<TRow>(value)\n let min = Number.POSITIVE_INFINITY\n let max = Number.NEGATIVE_INFINITY\n let sum = 0\n let count = 0\n let last = 0\n\n for (const row of rows) {\n const found = numbers(row, fields)\n if (found.length === 0) continue\n // The LAST row that carried anything, not the last row — a trailing row of\n // nulls should not read as a series that fell to zero.\n last = found.reduce((total, entry) => total + entry, 0)\n for (const entry of found) {\n if (entry < min) min = entry\n if (entry > max) max = entry\n sum += entry\n count += 1\n }\n }\n\n if (count === 0) return { min: 0, max: 0, sum: 0, mean: 0, last: 0, count: 0 }\n return { min, max, sum, mean: sum / count, last, count }\n}\n\n/**\n * The tick interval a reader recognises for a span — d3's rule, because it is\n * the one every axis in every tool already agrees on.\n *\n * Without it a shared domain of `[0, 314]` gives ticks at 78.5 and 157, and the\n * component would have made the panels comparable and the numbers unreadable in\n * the same move.\n */\nfunction tickStep(span: number, count: number): number {\n if (!(span > 0)) return 1\n const raw = span / count\n const magnitude = 10 ** Math.floor(Math.log10(raw))\n const normalised = raw / magnitude\n const step = normalised >= 7.5 ? 10 : normalised >= 3.5 ? 5 : normalised >= 1.5 ? 2 : 1\n return step * magnitude\n}\n\n/** Kills the float dust `Math.floor(0.3 / 0.1) * 0.1` leaves behind. */\nfunction clean(value: number): number {\n return Number(value.toPrecision(12))\n}\n\nexport interface DomainOptions {\n /**\n * Pulls the domain out to include zero.\n *\n * On by default, and the same default Recharts applies on its own: a bar or\n * an area read against a truncated baseline overstates every difference in\n * the grid, and a grid exists to be compared. Turn it off when the values\n * live far from zero and the differences between them ARE the reading —\n * temperatures, latencies, ratings out of five.\n */\n includeZero?: boolean\n /** Rounds the domain out to round numbers, so the ticks are readable. */\n nice?: boolean\n}\n\n/**\n * A `[min, max]` extended to something an axis can label.\n *\n * A degenerate extent — one row, or a group where every value is identical —\n * is given a span rather than returned as a zero-height plot.\n */\nexport function niceDomain(\n extent: [number, number],\n { includeZero = true, nice = true }: DomainOptions = {},\n): [number, number] {\n let [min, max] = extent\n if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 1]\n\n if (includeZero) {\n min = Math.min(min, 0)\n max = Math.max(max, 0)\n }\n if (max === min) max = min + (Math.abs(min) || 1)\n if (!nice) return [min, max]\n\n const step = tickStep(max - min, 5)\n return [clean(Math.floor(min / step) * step), clean(Math.ceil(max / step) * step)]\n}\n\n/** The extent across every group — the shared domain, before it is rounded. */\nexport function sharedExtent(stats: FacetStats[]): [number, number] {\n const scored = stats.filter((entry) => entry.count > 0)\n if (scored.length === 0) return [0, 1]\n return [\n Math.min(...scored.map((entry) => entry.min)),\n Math.max(...scored.map((entry) => entry.max)),\n ]\n}\n\n/** Reads the statistic a named sort orders on. */\nfunction statistic(stats: FacetStats, key: FacetSortKey): number {\n switch (key) {\n case 'min':\n return stats.min\n case 'sum':\n return stats.sum\n case 'mean':\n return stats.mean\n case 'last':\n return stats.last\n default:\n return stats.max\n }\n}\n\n/**\n * Orders the panels.\n *\n * `order` is optional because the natural direction depends on what is being\n * sorted: names read A→Z, a statistic reads biggest first. Passing it makes the\n * direction explicit either way.\n *\n * The sort is stable, so groups that tie keep the order the data gave them.\n */\nexport function sortGroups<TRow, TGroup extends FacetGroup<TRow>>(\n groups: TGroup[],\n sort: FacetSort<TRow>,\n order?: 'asc' | 'desc',\n): TGroup[] {\n const sorted = [...groups]\n\n // Reversing the array would reverse the ties along with everything else. A\n // sign on the comparator leaves equal groups in the order the data gave them,\n // which is what makes a re-render deterministic.\n if (sort === 'input') return order === 'desc' ? sorted.reverse() : sorted\n\n const compare = comparator<TRow, TGroup>(sort)\n const sign = (order ?? defaultOrder(sort)) === 'desc' ? -1 : 1\n sorted.sort((a, b) => compare(a, b) * sign)\n return sorted\n}\n\n/** Ascending for a name or an explicit list, descending for a statistic. */\nfunction defaultOrder<TRow>(sort: FacetSort<TRow>): 'asc' | 'desc' {\n if (typeof sort === 'function' || Array.isArray(sort) || sort === 'name') return 'asc'\n return 'desc'\n}\n\n/** The ascending comparator behind one sort. */\nfunction comparator<TRow, TGroup extends FacetGroup<TRow>>(\n sort: Exclude<FacetSort<TRow>, 'input'>,\n): (a: TGroup, b: TGroup) => number {\n if (typeof sort === 'function') return sort\n\n if (Array.isArray(sort)) {\n // Names the list does not mention sort after the ones it does, keeping the\n // order the data gave them — a partial list is a partial instruction, not\n // an instruction to discard everything it forgot.\n const rank = new Map(sort.map((name, index) => [name, index]))\n const place = (name: string) => rank.get(name) ?? Number.MAX_SAFE_INTEGER\n return (a, b) => place(a.name) - place(b.name)\n }\n\n if (sort === 'name') {\n // `numeric` so \"Region 2\" precedes \"Region 10\", which is the one thing a\n // plain string sort gets wrong on every dataset that numbers its groups.\n return (a, b) => a.name.localeCompare(b.name, undefined, { numeric: true })\n }\n\n return (a, b) => statistic(a.stats, sort) - statistic(b.stats, sort)\n}\n\n/**\n * Sums the capped tail into one group, aligned on the category axis.\n *\n * With `xDataKey` the tail's rows are added together at each category, which is\n * what makes the \"Other\" panel a readable series rather than a scribble. Without\n * one the rows are concatenated in the order they arrived, which is only\n * meaningful for a chart that bins its own x — so pass the key.\n */\nexport function foldGroups<TRow extends Record<string, unknown>>(\n tail: { name: string; rows: TRow[] }[],\n {\n by,\n value,\n xDataKey,\n label,\n }: {\n by: keyof TRow & string\n value: FacetOptions<TRow>['value']\n xDataKey?: keyof TRow & string\n label: string\n },\n): { name: string; rows: TRow[] } {\n const rows = tail.flatMap((group) => group.rows)\n if (!xDataKey) return { name: label, rows }\n\n const fields = keys<TRow>(value)\n const bins = new Map<string, Record<string, unknown>>()\n\n for (const row of rows) {\n const category = String(row[xDataKey])\n let bin = bins.get(category)\n if (!bin) {\n bin = { [xDataKey]: row[xDataKey], [by]: label }\n for (const field of fields) bin[field] = 0\n bins.set(category, bin)\n }\n for (const field of fields) {\n const raw = row[field]\n if (raw === null || raw === undefined || raw === '') continue\n const entry = Number(raw)\n if (Number.isFinite(entry)) bin[field] = Number(bin[field]) + entry\n }\n }\n\n // A summed row is a row this module built, not one the caller handed in. The\n // cast is the boundary where that is admitted: it carries the same fields the\n // panels read, and nothing else the caller's own type may promise.\n return { name: label, rows: [...bins.values()] as unknown as TRow[] }\n}\n\n/**\n * The whole pipeline: group, summarise, order, cap, and hand every panel a\n * domain.\n *\n * One function rather than four calls at the call site, because the ORDER of\n * those four is itself a decision — the domain is computed across the groups\n * that survive the cap, so a folded \"Other\" panel cannot push the shared scale\n * past every panel it is meant to be compared against.\n */\nexport function buildPanels<TRow extends Record<string, unknown>>(\n data: TRow[],\n options: FacetOptions<TRow>,\n): FacetResult<TRow> {\n const {\n by,\n value,\n xDataKey,\n scales = 'shared',\n domain,\n includeZero = true,\n nice = true,\n sort = 'max',\n order,\n limit = 12,\n overflow = 'note',\n otherLabel = 'Other',\n } = options\n\n const grouped: FacetGroup<TRow>[] = groupRows(data, by).map((group) => ({\n ...group,\n stats: statsOf(group.rows, value),\n }))\n\n const total = grouped.length\n if (total === 0) return { panels: [], total: 0, hidden: 0, domain: null }\n\n const ordered = sortGroups<TRow, FacetGroup<TRow>>(grouped, sort, order)\n\n const capped = typeof limit === 'number' && limit >= 0 && ordered.length > limit\n const head = capped ? ordered.slice(0, limit) : ordered\n const tail = capped ? ordered.slice(limit) : []\n\n const folded =\n capped && overflow === 'fold'\n ? foldGroups(tail, { by, value, xDataKey, label: otherLabel })\n : undefined\n\n const shown: (FacetGroup<TRow> & { size: number; isOther: boolean })[] = [\n ...head.map((group) => ({ ...group, size: 1, isOther: false })),\n ...(folded\n ? [\n {\n ...folded,\n stats: statsOf(folded.rows, value),\n size: tail.length,\n isOther: true,\n },\n ]\n : []),\n ]\n\n // Computed after the cap and after the fold, so it covers exactly what is\n // drawn: a group the cap left out cannot stretch the scale into dead space,\n // and a folded \"Other\" — which IS drawn, and on this same axis — cannot\n // overflow the plot it is drawn in.\n const sharedDomain =\n domain ?? niceDomain(sharedExtent(shown.map((group) => group.stats)), { includeZero, nice })\n\n const independent = scales === 'independent' && !domain\n\n const panels = shown.map((group, index) => ({\n name: group.name,\n rows: group.rows,\n domain: independent\n ? niceDomain([group.stats.min, group.stats.max], { includeZero, nice })\n : sharedDomain,\n stats: group.stats,\n index,\n total: shown.length,\n size: group.size,\n isOther: group.isOther,\n }))\n\n return { panels, total, hidden: tail.length, domain: independent ? null : sharedDomain }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, ComponentProps } from 'react';
|
|
3
|
+
import { FunnelChart as FunnelChart$1, Funnel, LabelList } from 'recharts';
|
|
4
|
+
import { ChartConfig } from '../lib/chart.js';
|
|
5
|
+
import { ChartTooltipSlotProps } from '../AreaChart/AreaChart.js';
|
|
6
|
+
import '../lib/empty.js';
|
|
7
|
+
import '../lib/values.js';
|
|
8
|
+
import '../lib/sonify-control.js';
|
|
9
|
+
import '../lib/sonify.js';
|
|
10
|
+
import '../lib/background.js';
|
|
11
|
+
import '../lib/brush.js';
|
|
12
|
+
import '../lib/toolbar.js';
|
|
13
|
+
import '../lib/zoom.js';
|
|
14
|
+
import '../lib/figure.js';
|
|
15
|
+
import '../lib/paint.js';
|
|
16
|
+
import '../lib/dot.js';
|
|
17
|
+
import '../lib/legend.js';
|
|
18
|
+
import '../lib/tooltip.js';
|
|
19
|
+
import 'recharts/types/component/DefaultTooltipContent';
|
|
20
|
+
import '../lib/annotations.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How each stage is painted.
|
|
24
|
+
*
|
|
25
|
+
* `stepped` is the honest default: every stage keeps the same fill and the
|
|
26
|
+
* TAPER carries the drop. A ramp that also darkens each stage encodes the same
|
|
27
|
+
* fact twice and invites the reader to compare two things that are one.
|
|
28
|
+
*/
|
|
29
|
+
type FunnelVariant = 'stepped' | 'ramp';
|
|
30
|
+
interface FunnelChartProps<TData extends Record<string, unknown>> {
|
|
31
|
+
/** Stage names → their label and paint. Keys must match the `nameKey` values. */
|
|
32
|
+
config: ChartConfig;
|
|
33
|
+
/** The stages, widest first. Order is the funnel; it is not sorted for you. */
|
|
34
|
+
data: TData[];
|
|
35
|
+
/** The row field holding each stage's number. */
|
|
36
|
+
dataKey: keyof TData & string;
|
|
37
|
+
/** The row field naming each stage. */
|
|
38
|
+
nameKey: keyof TData & string;
|
|
39
|
+
/**
|
|
40
|
+
* What the chart shows, in a sentence a reader could act on. Required, and
|
|
41
|
+
* announced to a screen reader even when it is not printed.
|
|
42
|
+
*/
|
|
43
|
+
title: string;
|
|
44
|
+
/** Prints the title above the plot instead of hiding it from sight. */
|
|
45
|
+
showTitle?: boolean;
|
|
46
|
+
/** A line under the title — the unit, the window, the caveat. */
|
|
47
|
+
description?: ReactNode;
|
|
48
|
+
/** The composed parts — `<Funnel>`, `<Tooltip>`. */
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
/** Merged onto the figure, last, so a call site can size or space it. */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Escape hatch onto the raw Recharts chart element. */
|
|
53
|
+
chartProps?: ComponentProps<typeof FunnelChart$1>;
|
|
54
|
+
/** Drops the hidden table view. Only correct when the page prints the data itself. */
|
|
55
|
+
hideDataTable?: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Stages that only ever narrow — a signup flow, a hiring pipeline, a checkout.
|
|
59
|
+
*
|
|
60
|
+
* The caveat is the same one every funnel has: the taper encodes a RATIO
|
|
61
|
+
* between neighbouring stages, and the eye reads the enclosed area, so a funnel
|
|
62
|
+
* exaggerates a shallow drop and flattens a steep one. Where the exact fall-off
|
|
63
|
+
* is the point, put the percentages on the stages — `<Funnel.Label>` does — or
|
|
64
|
+
* use a `<BarChart>`, which encodes each stage on one honest scale.
|
|
65
|
+
*
|
|
66
|
+
* Reach for `<SankeyChart>` instead when the flow can SPLIT rather than only
|
|
67
|
+
* shrink: a funnel has one path through it by construction.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <FunnelChart title="Signup funnel" config={config} data={stages} dataKey="people" nameKey="stage">
|
|
71
|
+
* <FunnelChart.Funnel>
|
|
72
|
+
* <FunnelChart.Label />
|
|
73
|
+
* </FunnelChart.Funnel>
|
|
74
|
+
* <FunnelChart.Tooltip />
|
|
75
|
+
* </FunnelChart>
|
|
76
|
+
*/
|
|
77
|
+
declare function FunnelChart<TData extends Record<string, unknown>>({ config, data, dataKey, nameKey, title, showTitle, description, children, className, chartProps, hideDataTable, }: FunnelChartProps<TData>): react.JSX.Element;
|
|
78
|
+
declare namespace FunnelChart {
|
|
79
|
+
var Funnel: ({ variant, orientation, gap, children, funnelProps, }: FunnelProps) => react.JSX.Element;
|
|
80
|
+
var Label: ({ dataKey, position, labelListProps }: FunnelLabelProps) => react.JSX.Element;
|
|
81
|
+
var Tooltip: ({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) => react.JSX.Element;
|
|
82
|
+
}
|
|
83
|
+
interface FunnelProps {
|
|
84
|
+
variant?: FunnelVariant;
|
|
85
|
+
/** Whether the funnel narrows downward or along the inline axis. */
|
|
86
|
+
orientation?: 'vertical' | 'horizontal';
|
|
87
|
+
/** Gap between stages, in pixels. */
|
|
88
|
+
gap?: number;
|
|
89
|
+
/** `<FunnelChart.Label>`. */
|
|
90
|
+
children?: ReactNode;
|
|
91
|
+
funnelProps?: Omit<ComponentProps<typeof Funnel>, 'data' | 'dataKey'>;
|
|
92
|
+
}
|
|
93
|
+
interface FunnelLabelProps {
|
|
94
|
+
/** The field printed on each stage. Defaults to the stage's name. */
|
|
95
|
+
dataKey?: string;
|
|
96
|
+
/** Where the label sits relative to its stage. */
|
|
97
|
+
position?: ComponentProps<typeof LabelList>['position'];
|
|
98
|
+
labelListProps?: Omit<ComponentProps<typeof LabelList>, 'dataKey'>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { FunnelChart, type FunnelChartProps, type FunnelLabelProps, type FunnelProps, type FunnelVariant, FunnelChart as default };
|