@kanzo-tech/ui 0.12.0 → 0.13.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/dist/analytics.d.ts +1 -0
- package/dist/analytics.d.ts.map +1 -1
- package/dist/charts/chart-inputs.d.ts +6 -5
- package/dist/charts/chart-inputs.d.ts.map +1 -1
- package/dist/charts/chart-inputs.js +78 -77
- package/dist/charts/chart-inputs.js.map +1 -1
- package/dist/charts/chart-marks.d.ts +2 -1
- package/dist/charts/chart-marks.d.ts.map +1 -1
- package/dist/charts/chart-marks.js.map +1 -1
- package/dist/charts/chart-root.d.ts +8 -3
- package/dist/charts/chart-root.d.ts.map +1 -1
- package/dist/charts/chart-root.js +60 -60
- package/dist/charts/chart-root.js.map +1 -1
- package/dist/charts/chart-spec.d.ts +11 -4
- package/dist/charts/chart-spec.d.ts.map +1 -1
- package/dist/charts/chart-spec.js +44 -40
- package/dist/charts/chart-spec.js.map +1 -1
- package/dist/charts/chart-stat.d.ts +3 -2
- package/dist/charts/chart-stat.d.ts.map +1 -1
- package/dist/charts/chart-stat.js +10 -9
- package/dist/charts/chart-stat.js.map +1 -1
- package/dist/composites/language-picker.d.ts +7 -0
- package/dist/composites/language-picker.d.ts.map +1 -1
- package/dist/composites/language-picker.js +38 -36
- package/dist/composites/language-picker.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +661 -660
- package/dist/lib/control-size.d.ts +26 -0
- package/dist/lib/control-size.d.ts.map +1 -0
- package/dist/lib/control-size.js +11 -0
- package/dist/lib/control-size.js.map +1 -0
- package/dist/simples/button.d.ts.map +1 -1
- package/dist/simples/button.js +34 -33
- package/dist/simples/button.js.map +1 -1
- package/dist/simples/checkbox.d.ts.map +1 -1
- package/dist/simples/checkbox.js +42 -42
- package/dist/simples/checkbox.js.map +1 -1
- package/dist/simples/combobox.d.ts.map +1 -1
- package/dist/simples/combobox.js +48 -49
- package/dist/simples/combobox.js.map +1 -1
- package/dist/simples/date-picker.d.ts +58 -2
- package/dist/simples/date-picker.d.ts.map +1 -1
- package/dist/simples/date-picker.js +207 -76
- package/dist/simples/date-picker.js.map +1 -1
- package/dist/simples/input-group.d.ts +9 -9
- package/dist/simples/input-group.d.ts.map +1 -1
- package/dist/simples/input-group.js +68 -78
- package/dist/simples/input-group.js.map +1 -1
- package/dist/simples/input.d.ts +9 -9
- package/dist/simples/input.d.ts.map +1 -1
- package/dist/simples/input.js +13 -14
- package/dist/simples/input.js.map +1 -1
- package/dist/simples/native-select.d.ts +9 -9
- package/dist/simples/native-select.d.ts.map +1 -1
- package/dist/simples/native-select.js +29 -28
- package/dist/simples/native-select.js.map +1 -1
- package/dist/simples/number-input.d.ts +9 -9
- package/dist/simples/number-input.d.ts.map +1 -1
- package/dist/simples/number-input.js +27 -27
- package/dist/simples/number-input.js.map +1 -1
- package/dist/simples/password-input.d.ts.map +1 -1
- package/dist/simples/password-input.js +24 -20
- package/dist/simples/password-input.js.map +1 -1
- package/dist/simples/pin-input.d.ts +9 -9
- package/dist/simples/pin-input.d.ts.map +1 -1
- package/dist/simples/pin-input.js +30 -29
- package/dist/simples/pin-input.js.map +1 -1
- package/dist/simples/radio-group.d.ts.map +1 -1
- package/dist/simples/radio-group.js +44 -43
- package/dist/simples/radio-group.js.map +1 -1
- package/dist/simples/segment-group.d.ts +8 -0
- package/dist/simples/segment-group.d.ts.map +1 -1
- package/dist/simples/segment-group.js +55 -47
- package/dist/simples/segment-group.js.map +1 -1
- package/dist/simples/switch.d.ts.map +1 -1
- package/dist/simples/switch.js +18 -12
- package/dist/simples/switch.js.map +1 -1
- package/dist/simples/tags-input.d.ts.map +1 -1
- package/dist/simples/tags-input.js +44 -39
- package/dist/simples/tags-input.js.map +1 -1
- package/dist/simples/toggle.d.ts +9 -9
- package/dist/simples/toggle.d.ts.map +1 -1
- package/dist/simples/toggle.js +24 -23
- package/dist/simples/toggle.js.map +1 -1
- package/package.json +5 -5
package/dist/analytics.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { Coordinator, Selection, engine, type Engine } from '@kanzo-tech/mosaic'
|
|
|
26
26
|
export { loadCSV, loadJSON, loadObjects, loadParquet, loadSpatial, loadExtension, } from '@kanzo-tech/mosaic';
|
|
27
27
|
export { count, sum, avg, min, max, median, quantile, stddev, mode, bin, sql } from '@uwdata/vgplot';
|
|
28
28
|
export type { ExprValue } from '@kanzo-tech/mosaic';
|
|
29
|
+
export type { TableExpr } from '@kanzo-tech/mosaic';
|
|
29
30
|
export { asc, desc } from '@kanzo-tech/mosaic';
|
|
30
31
|
export { axisX, axisY, axisFx, axisFy, gridFx, gridFy } from '@uwdata/vgplot';
|
|
31
32
|
export { MosaicClient, makeClient, clausePoint, clausePoints, clauseInterval, clauseIntervals, clauseMatch, } from '@kanzo-tech/mosaic';
|
package/dist/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TableExpr } from '@kanzo-tech/mosaic';
|
|
1
2
|
import { ReactNode, default as React } from 'react';
|
|
2
3
|
import { ark } from '@ark-ui/react/factory';
|
|
3
4
|
import { MosaicClient, Selection, SelectionClause } from '@uwdata/mosaic-core';
|
|
@@ -66,8 +67,8 @@ export interface ChartFilterOption {
|
|
|
66
67
|
}>;
|
|
67
68
|
}
|
|
68
69
|
export interface ChartFilterProps extends Omit<React.ComponentProps<typeof ark.div>, "defaultValue">, SelectionProps {
|
|
69
|
-
/** The relation the values and their counts come from. */
|
|
70
|
-
table?:
|
|
70
|
+
/** The relation the values and their counts come from, as `ChartRoot` takes it. */
|
|
71
|
+
table?: TableExpr;
|
|
71
72
|
/** The column grouped by. */
|
|
72
73
|
column?: string;
|
|
73
74
|
/** The column named in the published clause. Defaults to `column`. */
|
|
@@ -112,8 +113,8 @@ export interface ChartFilterProps extends Omit<React.ComponentProps<typeof ark.d
|
|
|
112
113
|
*/
|
|
113
114
|
export declare function ChartFilter(props: ChartFilterProps): React.JSX.Element;
|
|
114
115
|
export interface ChartSearchProps extends Omit<React.ComponentProps<typeof ark.div>, "defaultValue">, SelectionProps {
|
|
115
|
-
/** The relation the autocomplete values come from. */
|
|
116
|
-
table?:
|
|
116
|
+
/** The relation the autocomplete values come from, as `ChartRoot` takes it. */
|
|
117
|
+
table?: TableExpr;
|
|
117
118
|
/** The column to search. */
|
|
118
119
|
column?: string;
|
|
119
120
|
/** The column named in the published clause. Defaults to `column`. */
|
|
@@ -143,7 +144,7 @@ export interface ChartSearchProps extends Omit<React.ComponentProps<typeof ark.d
|
|
|
143
144
|
export declare function ChartSearch(props: ChartSearchProps): React.JSX.Element;
|
|
144
145
|
export interface ChartSliderProps extends Omit<React.ComponentProps<typeof ark.div>, "defaultValue">, SelectionProps {
|
|
145
146
|
/** The relation the extent comes from, when `min`/`max` are not both given. */
|
|
146
|
-
table?:
|
|
147
|
+
table?: TableExpr;
|
|
147
148
|
/** The numeric column. */
|
|
148
149
|
column?: string;
|
|
149
150
|
/** The column named in the published clause. Defaults to `column`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-inputs.d.ts","sourceRoot":"","sources":["../../src/charts/chart-inputs.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1F,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAKL,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,EACL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"chart-inputs.d.ts","sourceRoot":"","sources":["../../src/charts/chart-inputs.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAqD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC1F,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAAE,GAAG,EAAE,MAAM,uBAAuB,CAAC;AAC5C,OAAO,EAKL,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAKL,KAAK,EACL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;AAkB5B,OAAO,EAAoB,KAAK,aAAa,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAErF;;;;;;;;;;;;;GAaG;AAGH,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,yEAAyE;IACzE,QAAQ,EAAE,SAAS,GAAG,IAAI,CAAC;IAC3B,oDAAoD;IACpD,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;IACrB,oFAAoF;IACpF,KAAK,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,KAAK,GAAG,IAAI,CAAC;IAC5C,6EAA6E;IAC7E,IAAI,EAAE,SAAS,OAAO,EAAE,CAAC;IACzB,MAAM,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,eAAe,CAAC;IACxE,+EAA+E;IAC/E,aAAa,EAAE,CAAC,CAAC;IACjB,mGAAmG;IACnG,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,uFAAuF;IACvF,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,IAAI,CAAC;IACjC,oFAAoF;IACpF,QAAQ,EAAE,CAAC,GAAG,SAAS,CAAC;IACxB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAC9B,OAAO,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAC7B,gBAAgB,CAAC,CAAC,CAAC,CA4DrB;AAmBD,UAAU,cAAc;IACtB,6FAA6F;IAC7F,EAAE,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACtB,kGAAkG;IAClG,QAAQ,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAC7B;AAED,KAAK,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAItC,+EAA+E;AAC/E,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,EAChE,cAAc;IAChB,mFAAmF;IACnF,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,6BAA6B;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,6DAA6D;IAC7D,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,2CAA2C;IAC3C,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yFAAyF;IACzF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4FAA4F;IAC5F,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAgED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAuGlD;AAID,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,EAChE,cAAc;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,4BAA4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,IAAI,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACnD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4FAA4F;IAC5F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAiKlD;AAID,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,EAChE,cAAc;IAChB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAgBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,qBAiIlD"}
|
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs as A, jsx as C } from "react/jsx-runtime";
|
|
3
|
-
import { useRef as B, useEffect as N, useMemo as
|
|
4
|
-
import { ark as
|
|
5
|
-
import { clausePoints as
|
|
6
|
-
import { Query as G, count as
|
|
7
|
-
import { useListCollection as
|
|
8
|
-
import { useFilter as
|
|
3
|
+
import { useRef as B, useEffect as N, useMemo as re, useState as _, useCallback as ee } from "react";
|
|
4
|
+
import { ark as oe } from "@ark-ui/react/factory";
|
|
5
|
+
import { clausePoints as ie, clausePoint as le, clauseMatch as ce, clauseInterval as se } from "@uwdata/mosaic-core";
|
|
6
|
+
import { Query as G, count as te, desc as ae, max as ue, min as me } from "@uwdata/mosaic-sql";
|
|
7
|
+
import { useListCollection as de } from "@ark-ui/react/collection";
|
|
8
|
+
import { useFilter as fe } from "@ark-ui/react/locale";
|
|
9
9
|
import { cn as J } from "../lib/cn.js";
|
|
10
|
-
import { Combobox as
|
|
11
|
-
import { FacetFilter as
|
|
12
|
-
import { Field as
|
|
13
|
-
import { Input as
|
|
14
|
-
import { Skeleton as
|
|
15
|
-
import { Slider as
|
|
10
|
+
import { Combobox as he, ComboboxInput as pe, ComboboxContent as ve, ComboboxEmpty as be, ComboboxItem as ge } from "../simples/combobox.js";
|
|
11
|
+
import { FacetFilter as ye } from "../simples/FacetFilter.js";
|
|
12
|
+
import { Field as xe, FieldLabel as Ce } from "../simples/field.js";
|
|
13
|
+
import { Input as Ne } from "../simples/input.js";
|
|
14
|
+
import { Skeleton as Se } from "../simples/skeleton.js";
|
|
15
|
+
import { Slider as Ve, SliderLabel as we, SliderValue as Fe } from "../simples/slider.js";
|
|
16
|
+
import { chartTableKey as W } from "./chart-spec.js";
|
|
16
17
|
import { useMosaic as q } from "./mosaic-provider.js";
|
|
17
|
-
import { ChartQueryClient as
|
|
18
|
-
function
|
|
18
|
+
import { ChartQueryClient as Ie } from "./query-client.js";
|
|
19
|
+
function X(t) {
|
|
19
20
|
const { filterBy: r, as: e, deps: i } = t, { coordinator: a } = q(), d = B(t);
|
|
20
21
|
d.current = t;
|
|
21
22
|
const p = B(null), [b, x] = _(null), [y, m] = _(null), [u, v] = _(void 0);
|
|
22
23
|
N(() => {
|
|
23
|
-
const s = new
|
|
24
|
+
const s = new Ie(
|
|
24
25
|
r ?? void 0,
|
|
25
26
|
(h) => d.current.build(h),
|
|
26
27
|
m
|
|
@@ -38,20 +39,20 @@ function W(t) {
|
|
|
38
39
|
e.removeEventListener("value", s), e.valueFor(b) !== void 0 && e.update(d.current.clause(b, void 0));
|
|
39
40
|
};
|
|
40
41
|
}, [e, b]);
|
|
41
|
-
const S =
|
|
42
|
+
const S = ee(
|
|
42
43
|
(s) => {
|
|
43
44
|
v(s);
|
|
44
45
|
const h = p.current;
|
|
45
46
|
e && h && e.update(d.current.clause(h, s));
|
|
46
47
|
},
|
|
47
48
|
[e]
|
|
48
|
-
), k =
|
|
49
|
+
), k = ee(() => {
|
|
49
50
|
const s = p.current;
|
|
50
51
|
e && s && e.activate(d.current.clause(s, d.current.activateValue));
|
|
51
52
|
}, [e]);
|
|
52
53
|
return { rows: y, selected: u, publish: S, activate: k };
|
|
53
54
|
}
|
|
54
|
-
function
|
|
55
|
+
function Y(t, r) {
|
|
55
56
|
return {
|
|
56
57
|
onFocus: (e) => {
|
|
57
58
|
var i;
|
|
@@ -63,24 +64,24 @@ function X(t, r) {
|
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
|
-
const
|
|
67
|
-
function
|
|
67
|
+
const Be = 50;
|
|
68
|
+
function ke(t) {
|
|
68
69
|
return typeof t == "object" && t !== null && !Array.isArray(t) && "value" in t;
|
|
69
70
|
}
|
|
70
|
-
function
|
|
71
|
+
function Ee(t) {
|
|
71
72
|
return String(t ?? "");
|
|
72
73
|
}
|
|
73
|
-
function
|
|
74
|
+
function Le(t) {
|
|
74
75
|
if (typeof t == "number") return t;
|
|
75
76
|
if (typeof t == "bigint") return Number(t);
|
|
76
77
|
}
|
|
77
|
-
function
|
|
78
|
+
function Te(t, r, e) {
|
|
78
79
|
if (t)
|
|
79
80
|
return e ? `Top ${r} values — the field searches these, not the column. Filter further for the rest.` : `Top ${r} values — filter further to see the rest.`;
|
|
80
81
|
}
|
|
81
|
-
function
|
|
82
|
+
function Me(t, r, e) {
|
|
82
83
|
return t ? t.map((i) => {
|
|
83
|
-
const a =
|
|
84
|
+
const a = ke(i) ? i : { value: i };
|
|
84
85
|
return {
|
|
85
86
|
key: String(a.value),
|
|
86
87
|
label: a.label ?? e(a.value),
|
|
@@ -91,10 +92,10 @@ function Te(t, r, e) {
|
|
|
91
92
|
key: String(i.value),
|
|
92
93
|
label: e(i.value),
|
|
93
94
|
raw: i.value,
|
|
94
|
-
count:
|
|
95
|
+
count: Le(i.count)
|
|
95
96
|
}));
|
|
96
97
|
}
|
|
97
|
-
function
|
|
98
|
+
function Ye(t) {
|
|
98
99
|
const {
|
|
99
100
|
table: r,
|
|
100
101
|
column: e,
|
|
@@ -102,11 +103,11 @@ function We(t) {
|
|
|
102
103
|
as: a,
|
|
103
104
|
filterBy: d,
|
|
104
105
|
options: p,
|
|
105
|
-
format: b =
|
|
106
|
+
format: b = Ee,
|
|
106
107
|
label: x = e,
|
|
107
108
|
multiple: y = !0,
|
|
108
109
|
defaultValue: m,
|
|
109
|
-
limit: u =
|
|
110
|
+
limit: u = Be,
|
|
110
111
|
searchable: v = !1,
|
|
111
112
|
disabled: S,
|
|
112
113
|
size: k = "md",
|
|
@@ -115,19 +116,19 @@ function We(t) {
|
|
|
115
116
|
children: E,
|
|
116
117
|
slot: P,
|
|
117
118
|
...Q
|
|
118
|
-
} = t, { crossfilter: j } = q(), R = a === void 0 ? j : a, K = d === void 0 ? j : d, g = p === void 0 && !!(r && e), { rows: V, selected: L, publish: w, activate: f } =
|
|
119
|
+
} = t, { crossfilter: j } = q(), R = a === void 0 ? j : a, K = d === void 0 ? j : d, g = p === void 0 && !!(r && e), { rows: V, selected: L, publish: w, activate: f } = X({
|
|
119
120
|
as: R,
|
|
120
121
|
filterBy: K,
|
|
121
|
-
deps: [r, e, g, u],
|
|
122
|
+
deps: [W(r), e, g, u],
|
|
122
123
|
activateValue: [0],
|
|
123
|
-
build: (n) => g ? G.from(r).select({ value: e, count:
|
|
124
|
-
clause: (n, c) => y ?
|
|
124
|
+
build: (n) => g ? G.from(r).select({ value: e, count: te() }).where(n).groupby(e).orderby(ae(te()), e).limit(u + 1) : null,
|
|
125
|
+
clause: (n, c) => y ? ie([i], c == null ? void 0 : c.map((H) => [H]), { source: n }) : le(i, c == null ? void 0 : c[0], { source: n }),
|
|
125
126
|
decode: (n) => y ? n.map((c) => c[0]) : [n]
|
|
126
127
|
}), T = B(!1);
|
|
127
128
|
N(() => {
|
|
128
129
|
T.current || m === void 0 || (T.current = !0, w(Array.isArray(m) ? m : [m]));
|
|
129
130
|
}, [m, w]);
|
|
130
|
-
const M = L ?? [], F =
|
|
131
|
+
const M = L ?? [], F = Me(p, V, b), I = F.length > u, O = I ? F.slice(0, u) : F, $ = O.map((n) => ({
|
|
131
132
|
count: n.count,
|
|
132
133
|
icon: n.icon,
|
|
133
134
|
label: n.label,
|
|
@@ -135,15 +136,15 @@ function We(t) {
|
|
|
135
136
|
})), l = new Map(O.map((n) => [n.key, n.raw]));
|
|
136
137
|
for (const n of M) l.set(String(n), n);
|
|
137
138
|
return /* @__PURE__ */ A(
|
|
138
|
-
|
|
139
|
+
oe.div,
|
|
139
140
|
{
|
|
140
141
|
className: J("w-fit", s),
|
|
141
142
|
...Q,
|
|
142
|
-
...
|
|
143
|
+
...Y(f, t),
|
|
143
144
|
"data-slot": P ?? "chart-filter",
|
|
144
145
|
children: [
|
|
145
146
|
/* @__PURE__ */ C(
|
|
146
|
-
|
|
147
|
+
ye,
|
|
147
148
|
{
|
|
148
149
|
className: h,
|
|
149
150
|
slot: "chart-filter-trigger",
|
|
@@ -152,7 +153,7 @@ function We(t) {
|
|
|
152
153
|
items: $,
|
|
153
154
|
label: x,
|
|
154
155
|
multiple: y,
|
|
155
|
-
note:
|
|
156
|
+
note: Te(I, u, v),
|
|
156
157
|
onValueChange: (n) => w(n.map((c) => l.has(c) ? l.get(c) : c)),
|
|
157
158
|
searchEmpty: I ? `No match among the top ${u} values.` : void 0,
|
|
158
159
|
searchable: v,
|
|
@@ -165,7 +166,7 @@ function We(t) {
|
|
|
165
166
|
}
|
|
166
167
|
);
|
|
167
168
|
}
|
|
168
|
-
function
|
|
169
|
+
function Ze(t) {
|
|
169
170
|
const {
|
|
170
171
|
table: r,
|
|
171
172
|
column: e,
|
|
@@ -186,17 +187,17 @@ function Xe(t) {
|
|
|
186
187
|
children: E,
|
|
187
188
|
slot: P,
|
|
188
189
|
...Q
|
|
189
|
-
} = t, { crossfilter: j } = q(), R = a === void 0 ? j : a, K = d === void 0 ? j : d, g = u > 0 && !!(r && e), { rows: V, selected: L, publish: w, activate: f } =
|
|
190
|
+
} = t, { crossfilter: j } = q(), R = a === void 0 ? j : a, K = d === void 0 ? j : d, g = u > 0 && !!(r && e), { rows: V, selected: L, publish: w, activate: f } = X({
|
|
190
191
|
as: R,
|
|
191
192
|
filterBy: K,
|
|
192
|
-
deps: [r, e, g, u],
|
|
193
|
+
deps: [W(r), e, g, u],
|
|
193
194
|
activateValue: "",
|
|
194
195
|
build: (o) => g ? G.from(r).select({ value: e }).distinct().where(o).orderby(e).limit(u) : null,
|
|
195
|
-
clause: (o, z) =>
|
|
196
|
-
}), T =
|
|
196
|
+
clause: (o, z) => ce(i, z ?? null, { source: o, method: p, caseSensitive: b })
|
|
197
|
+
}), T = re(
|
|
197
198
|
() => (V ?? []).map((o) => ({ label: String(o.value), value: String(o.value) })),
|
|
198
199
|
[V]
|
|
199
|
-
), M = T.length >= u, { contains: F } =
|
|
200
|
+
), M = T.length >= u, { contains: F } = fe({ sensitivity: "base" }), { collection: I, filter: O, set: $ } = de({ filter: F, initialItems: [] }), l = T.map((o) => o.value).join("\0");
|
|
200
201
|
N(() => {
|
|
201
202
|
$(T);
|
|
202
203
|
}, [l, $]);
|
|
@@ -207,9 +208,9 @@ function Xe(t) {
|
|
|
207
208
|
}, [L]), N(() => () => {
|
|
208
209
|
U.current && clearTimeout(U.current);
|
|
209
210
|
}, []);
|
|
210
|
-
const
|
|
211
|
+
const Z = B(!1);
|
|
211
212
|
N(() => {
|
|
212
|
-
|
|
213
|
+
Z.current || !v || (Z.current = !0, w(v));
|
|
213
214
|
}, [v, w]);
|
|
214
215
|
const D = (o) => {
|
|
215
216
|
c(o), U.current && clearTimeout(U.current);
|
|
@@ -219,15 +220,15 @@ function Xe(t) {
|
|
|
219
220
|
m > 0 ? U.current = setTimeout(z, m) : z();
|
|
220
221
|
};
|
|
221
222
|
return /* @__PURE__ */ A(
|
|
222
|
-
|
|
223
|
+
xe,
|
|
223
224
|
{
|
|
224
225
|
className: J("w-fit min-w-48 gap-1.5", s),
|
|
225
226
|
disabled: S,
|
|
226
227
|
...Q,
|
|
227
|
-
...
|
|
228
|
+
...Y(f, t),
|
|
228
229
|
slot: P ?? "chart-search",
|
|
229
230
|
children: [
|
|
230
|
-
x ? /* @__PURE__ */ C(
|
|
231
|
+
x ? /* @__PURE__ */ C(Ce, { children: x }) : null,
|
|
231
232
|
g ? (
|
|
232
233
|
// `allowCustomValue`, because this control is a FILTER and not a picker: what it publishes
|
|
233
234
|
// is a match clause over whatever you typed, and the list only ever offered completions.
|
|
@@ -235,7 +236,7 @@ function Xe(t) {
|
|
|
235
236
|
// in every browser, no empty state, and silent about its own cap. Same clause, same
|
|
236
237
|
// debounce; only the surface changed.
|
|
237
238
|
/* @__PURE__ */ A(
|
|
238
|
-
|
|
239
|
+
he,
|
|
239
240
|
{
|
|
240
241
|
allowCustomValue: !0,
|
|
241
242
|
collection: I,
|
|
@@ -250,10 +251,10 @@ function Xe(t) {
|
|
|
250
251
|
z !== void 0 && D(z);
|
|
251
252
|
},
|
|
252
253
|
children: [
|
|
253
|
-
/* @__PURE__ */ C(
|
|
254
|
-
/* @__PURE__ */ A(
|
|
255
|
-
/* @__PURE__ */ C(
|
|
256
|
-
I.items.map((o) => /* @__PURE__ */ C(
|
|
254
|
+
/* @__PURE__ */ C(pe, { className: h, placeholder: y, size: k }),
|
|
255
|
+
/* @__PURE__ */ A(ve, { children: [
|
|
256
|
+
/* @__PURE__ */ C(be, { children: "No matching values." }),
|
|
257
|
+
I.items.map((o) => /* @__PURE__ */ C(ge, { item: o, children: o.label }, o.value)),
|
|
257
258
|
M ? /* @__PURE__ */ A("p", { className: "border-t px-2 py-1.5 text-muted-foreground text-xs", children: [
|
|
258
259
|
"First ",
|
|
259
260
|
u,
|
|
@@ -264,7 +265,7 @@ function Xe(t) {
|
|
|
264
265
|
}
|
|
265
266
|
)
|
|
266
267
|
) : /* @__PURE__ */ C(
|
|
267
|
-
|
|
268
|
+
Ne,
|
|
268
269
|
{
|
|
269
270
|
className: h,
|
|
270
271
|
disabled: S,
|
|
@@ -280,10 +281,10 @@ function Xe(t) {
|
|
|
280
281
|
}
|
|
281
282
|
);
|
|
282
283
|
}
|
|
283
|
-
function
|
|
284
|
+
function je(t, r) {
|
|
284
285
|
return Number.isInteger(t) && Number.isInteger(r) ? 1 : (r - t) / 500 || 1;
|
|
285
286
|
}
|
|
286
|
-
function
|
|
287
|
+
function et(t) {
|
|
287
288
|
const {
|
|
288
289
|
table: r,
|
|
289
290
|
column: e,
|
|
@@ -302,35 +303,35 @@ function Ye(t) {
|
|
|
302
303
|
children: s,
|
|
303
304
|
slot: h,
|
|
304
305
|
...E
|
|
305
|
-
} = t, { crossfilter: P } = q(), Q = a === void 0 ? P : a, j = d === void 0 ? P : d, R = (b === void 0 || x === void 0) && !!(r && e), K = B(1), { rows: g, selected: V, publish: L, activate: w } =
|
|
306
|
+
} = t, { crossfilter: P } = q(), Q = a === void 0 ? P : a, j = d === void 0 ? P : d, R = (b === void 0 || x === void 0) && !!(r && e), K = B(1), { rows: g, selected: V, publish: L, activate: w } = X({
|
|
306
307
|
as: Q,
|
|
307
308
|
filterBy: j,
|
|
308
|
-
deps: [r, e, R],
|
|
309
|
+
deps: [W(r), e, R],
|
|
309
310
|
activateValue: p === "interval" ? [0, 0] : 0,
|
|
310
|
-
build: (l) => R ? G.select({ min:
|
|
311
|
+
build: (l) => R ? G.select({ min: me(e), max: ue(e) }).from(r).where(l) : null,
|
|
311
312
|
clause: (l, n) => {
|
|
312
|
-
if (p !== "interval") return
|
|
313
|
+
if (p !== "interval") return le(i, n, { source: l });
|
|
313
314
|
const c = n;
|
|
314
|
-
return
|
|
315
|
+
return se(i, c ?? null, {
|
|
315
316
|
source: l,
|
|
316
317
|
bin: "ceil",
|
|
317
318
|
scale: c ? { type: "identity", domain: c } : void 0,
|
|
318
319
|
pixelSize: K.current
|
|
319
320
|
});
|
|
320
321
|
}
|
|
321
|
-
}), f =
|
|
322
|
+
}), f = re(() => {
|
|
322
323
|
const l = g == null ? void 0 : g[0], n = b ?? (typeof (l == null ? void 0 : l.min) == "number" ? l.min : void 0), c = x ?? (typeof (l == null ? void 0 : l.max) == "number" ? l.max : void 0);
|
|
323
|
-
return n === void 0 || c === void 0 ? null : { min: n, max: c, step: y ??
|
|
324
|
+
return n === void 0 || c === void 0 ? null : { min: n, max: c, step: y ?? je(n, c) };
|
|
324
325
|
}, [g, b, x, y]);
|
|
325
326
|
N(() => {
|
|
326
327
|
K.current = (f == null ? void 0 : f.step) ?? 1;
|
|
327
328
|
}, [f]);
|
|
328
329
|
const [T, M] = _(
|
|
329
|
-
m === void 0 ? null : [...
|
|
330
|
+
m === void 0 ? null : [...ne(m)]
|
|
330
331
|
), F = B(!1);
|
|
331
332
|
N(() => {
|
|
332
333
|
if (V !== void 0) {
|
|
333
|
-
F.current = !0, M([...
|
|
334
|
+
F.current = !0, M([...ne(V)]);
|
|
334
335
|
return;
|
|
335
336
|
}
|
|
336
337
|
F.current && (F.current = !1, M(null));
|
|
@@ -343,15 +344,15 @@ function Ye(t) {
|
|
|
343
344
|
M(l), L(p === "interval" ? [l[0], l[1]] : l[0]);
|
|
344
345
|
}, $ = T ?? (f ? p === "interval" ? [f.min, f.max] : [f.min] : null);
|
|
345
346
|
return /* @__PURE__ */ A(
|
|
346
|
-
|
|
347
|
+
oe.div,
|
|
347
348
|
{
|
|
348
349
|
className: J("flex w-full min-w-48 flex-col gap-1.5", k),
|
|
349
350
|
...E,
|
|
350
|
-
...
|
|
351
|
+
...Y(w, t),
|
|
351
352
|
"data-slot": h ?? "chart-slider",
|
|
352
353
|
children: [
|
|
353
354
|
f && $ ? /* @__PURE__ */ C(
|
|
354
|
-
|
|
355
|
+
Ve,
|
|
355
356
|
{
|
|
356
357
|
disabled: S,
|
|
357
358
|
max: f.max,
|
|
@@ -360,23 +361,23 @@ function Ye(t) {
|
|
|
360
361
|
step: f.step,
|
|
361
362
|
value: $,
|
|
362
363
|
children: u || v ? /* @__PURE__ */ A("div", { className: "flex items-center", children: [
|
|
363
|
-
u ? /* @__PURE__ */ C(
|
|
364
|
-
v ? /* @__PURE__ */ C(
|
|
364
|
+
u ? /* @__PURE__ */ C(we, { children: u }) : null,
|
|
365
|
+
v ? /* @__PURE__ */ C(Fe, {}) : null
|
|
365
366
|
] }) : null
|
|
366
367
|
}
|
|
367
|
-
) : /* @__PURE__ */ C(
|
|
368
|
+
) : /* @__PURE__ */ C(Se, { className: "h-2 w-full rounded-full" }),
|
|
368
369
|
s
|
|
369
370
|
]
|
|
370
371
|
}
|
|
371
372
|
);
|
|
372
373
|
}
|
|
373
|
-
function
|
|
374
|
+
function ne(t) {
|
|
374
375
|
return typeof t == "number" ? [t] : t;
|
|
375
376
|
}
|
|
376
377
|
export {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
Ye as ChartFilter,
|
|
379
|
+
Ze as ChartSearch,
|
|
380
|
+
et as ChartSlider,
|
|
381
|
+
X as useMosaicInput
|
|
381
382
|
};
|
|
382
383
|
//# sourceMappingURL=chart-inputs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-inputs.js","sources":["../../src/charts/chart-inputs.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport type React from \"react\";\nimport { ark } from \"@ark-ui/react/factory\";\nimport {\n clauseInterval,\n clauseMatch,\n clausePoint,\n clausePoints,\n MosaicClient,\n type Selection,\n type SelectionClause,\n} from \"@uwdata/mosaic-core\";\nimport {\n count as sqlCount,\n desc as sqlDesc,\n max as sqlMax,\n min as sqlMin,\n Query,\n type FilterExpr,\n} from \"@uwdata/mosaic-sql\";\nimport { useListCollection } from \"@ark-ui/react/collection\";\nimport { useFilter } from \"@ark-ui/react/locale\";\nimport { cn } from \"../lib/cn.js\";\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n} from \"../simples/combobox.js\";\nimport { FacetFilter, type FacetFilterItem } from \"../simples/FacetFilter.js\";\nimport { Field, FieldLabel } from \"../simples/field.js\";\nimport { Input } from \"../simples/input.js\";\nimport { Skeleton } from \"../simples/skeleton.js\";\nimport { Slider, SliderLabel, SliderValue } from \"../simples/slider.js\";\nimport { useMosaic } from \"./mosaic-provider.js\";\nimport { ChartQueryClient, type ChartQueryRow as QueryRow } from \"./query-client.js\";\n\n/**\n * Mosaic **inputs** — controls that publish into a `Selection` without being charts.\n *\n * Unlike marks and interactors, these are real DOM parts (`ark.*`, `data-slot`, forwarded props),\n * not inert descriptors: they carry no `__chart` static and belong *outside* `<ChartRoot>`. What\n * makes them Mosaic and not just a styled `<select>` is that each one is a `MosaicClient`: it\n * declares a `query()` for the values it needs (grouped counts, distinct values, a numeric extent),\n * the coordinator answers it, and the control publishes `clausePoints` / `clauseMatch` /\n * `clauseInterval` back into the selection the charts filter by.\n *\n * The vgplot equivalents (`vg.menu`, `vg.search`, `vg.slider`) build raw HTML. These use our\n * controls, our tokens and our accessibility, and are controllable *and* observable: an external\n * change to the selection flows back into the widget.\n */\n\n\nexport interface MosaicInputOptions<T> {\n /** Filters the widget's own lookup query. `null` = the full relation. */\n filterBy: Selection | null;\n /** Where the widget publishes. `null` = nowhere. */\n as: Selection | null;\n /** The lookup query, or `null` when there is nothing to ask the coordinator for. */\n build: (filter: FilterExpr) => Query | null;\n /** Rebuilds the client — and re-runs the lookup — when any entry changes. */\n deps: readonly unknown[];\n clause: (source: MosaicClient, value: T | undefined) => SelectionClause;\n /** The representative value of the warm-up clause published on hover/focus. */\n activateValue: T;\n /** Reads the widget's own clause value back out of the selection. Defaults to an identity cast. */\n decode?: (value: unknown) => T;\n}\n\nexport interface MosaicInputState<T> {\n /** Lookup rows, or `null` while the query is in flight (or when there is no query). */\n rows: readonly QueryRow[] | null;\n /** The value this widget currently holds in the selection — its observable half. */\n selected: T | undefined;\n publish: (value: T | undefined) => void;\n activate: () => void;\n}\n\n/**\n * The conversation with Mosaic that every input in this file has: a lookup query that re-runs under\n * the crossfilter, a clause published with `source` set so a control never filters itself, and a\n * warm-up so the first interaction is not the first query.\n *\n * Exported because the three controls here are surfaces over it, not the only surfaces it allows —\n * a chip row, a segmented switch or a map lasso is the same conversation with different chrome, and\n * without this the alternative is reimplementing it.\n */\nexport function useMosaicInput<T>(\n options: MosaicInputOptions<T>\n): MosaicInputState<T> {\n const { filterBy, as, deps } = options;\n const { coordinator } = useMosaic();\n const latest = useRef(options);\n latest.current = options;\n\n const clientRef = useRef<ChartQueryClient | null>(null);\n const [client, setClient] = useState<ChartQueryClient | null>(null);\n const [rows, setRows] = useState<readonly QueryRow[] | null>(null);\n const [selected, setSelected] = useState<T | undefined>(undefined);\n\n useEffect(() => {\n const instance = new ChartQueryClient(\n filterBy ?? undefined,\n (filter) => latest.current.build(filter),\n setRows,\n );\n clientRef.current = instance;\n setRows(null);\n setClient(instance);\n coordinator.connect(instance);\n return () => {\n clientRef.current = null;\n coordinator.disconnect(instance);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [coordinator, filterBy, ...deps]);\n\n useEffect(() => {\n if (!as || !client) return;\n const sync = () => {\n const raw = as.valueFor(client);\n const { decode } = latest.current;\n setSelected(raw === undefined ? undefined : decode ? decode(raw) : (raw as T));\n };\n sync();\n as.addEventListener(\"value\", sync);\n return () => {\n as.removeEventListener(\"value\", sync);\n // `Selection.remove` returns a *clone*; the live way to retract a clause is to publish an\n // empty one, which the resolver drops because its predicate is null.\n if (as.valueFor(client) !== undefined) as.update(latest.current.clause(client, undefined));\n };\n }, [as, client]);\n\n const publish = useCallback(\n (value: T | undefined) => {\n setSelected(value);\n const instance = clientRef.current;\n if (as && instance) as.update(latest.current.clause(instance, value));\n },\n [as],\n );\n\n const activate = useCallback(() => {\n const instance = clientRef.current;\n if (as && instance) as.activate(latest.current.clause(instance, latest.current.activateValue));\n }, [as]);\n\n return { rows, selected, publish, activate };\n}\n\n/** vgplot's inputs warm the selection on hover/focus; ours do it on the wrapper, and compose. */\nfunction warmUpHandlers(\n activate: () => void,\n props: Pick<React.ComponentProps<typeof ark.div>, \"onFocus\" | \"onPointerEnter\">,\n) {\n return {\n onFocus: (event: React.FocusEvent<HTMLDivElement>) => {\n props.onFocus?.(event);\n activate();\n },\n onPointerEnter: (event: React.PointerEvent<HTMLDivElement>) => {\n props.onPointerEnter?.(event);\n if (!event.buttons) activate();\n },\n };\n}\n\ninterface SelectionProps {\n /** Where the control publishes. Defaults to the provider's crossfilter; `null` = nowhere. */\n as?: Selection | null;\n /** What the control's own lookup filters by. Defaults to the crossfilter; `null` = unfiltered. */\n filterBy?: Selection | null;\n}\n\ntype ControlSize = \"sm\" | \"md\" | \"lg\";\n\n// ── ChartFilter ──────────────────────────────────────────────────────────────\n\n/** An explicit entry. Bare values work too — they are labelled by `format`. */\nexport interface ChartFilterOption {\n value: unknown;\n label?: string;\n icon?: React.ComponentType<{ className?: string }>;\n}\n\nexport interface ChartFilterProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the values and their counts come from. */\n table?: string;\n /** The column grouped by. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /** Explicit options — a literal value or a `{ value, label, icon }` object. Skips the lookup. */\n options?: readonly unknown[];\n /** Labels an option value that carries no explicit label. */\n format?: (value: unknown) => string;\n /** Trigger label. Defaults to `column`. */\n label?: ReactNode;\n /** Tick any number of values (an `IN` list). `false` keeps at most one ticked. */\n multiple?: boolean;\n /** Published once, when the control first connects. A bare value or an array of them. */\n defaultValue?: unknown;\n /** Most-frequent values kept; past this the list says so rather than rendering the tail. */\n limit?: number;\n /**\n * Draw a filter field above the values.\n *\n * It narrows **the fetched page** — the `limit` most frequent values this control asked for — and\n * runs no query of its own. When the page is truncated the note and the no-match message say so,\n * because a field that answers \"no matching values\" about a database it never asked is worse than\n * no field. Searching the column itself is `ChartSearch`, which publishes a match clause.\n *\n * @default false\n */\n searchable?: boolean;\n disabled?: boolean;\n size?: ControlSize;\n /** Class for the trigger; `className` styles the wrapper. */\n controlClassName?: string;\n}\n\n/**\n * A `GROUP BY` over a high-cardinality column can answer with thousands of rows, and neither the\n * wire nor the DOM wants them. The lookup is capped at the *most frequent* values — the ones a\n * facet is for — and the control owns up to the truncation instead of pretending the tail is\n * absent. Needle-in-a-haystack lookup is `ChartSearch`'s job, not a facet filter's.\n */\nconst FILTER_LIMIT = 50;\n\ninterface FilterEntry {\n key: string;\n label: string;\n raw: unknown;\n count?: number;\n icon?: React.ComponentType<{ className?: string }>;\n}\n\nfunction isOptionObject(value: unknown): value is ChartFilterOption {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && \"value\" in value;\n}\n\nfunction defaultFormat(value: unknown): string {\n return String(value ?? \"\");\n}\n\n/** DuckDB answers a `count(*)` with a BigInt over the WASM bridge. */\nfunction toCount(value: unknown): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"bigint\") return Number(value);\n return undefined;\n}\n\nfunction truncationNote(truncated: boolean, limit: number, searchable: boolean) {\n if (!truncated) return undefined;\n return searchable\n ? `Top ${limit} values — the field searches these, not the column. Filter further for the rest.`\n : `Top ${limit} values — filter further to see the rest.`;\n}\n\nfunction filterEntries(\n options: readonly unknown[] | undefined,\n rows: readonly QueryRow[] | null,\n format: (value: unknown) => string,\n): FilterEntry[] {\n if (options) {\n return options.map((entry) => {\n const option = isOptionObject(entry) ? entry : { value: entry };\n return {\n key: String(option.value),\n label: option.label ?? format(option.value),\n raw: option.value,\n icon: option.icon,\n };\n });\n }\n return (rows ?? []).map((row) => ({\n key: String(row.value),\n label: format(row.value),\n raw: row.value,\n count: toCount(row.count),\n }));\n}\n\n/**\n * A facet filter over a column's values, publishing a **points** clause — the same `FacetFilter`\n * surface as the table's, so a dashboard has one filter idiom rather than two.\n *\n * The values are a query, not a prop: the control connects as a Mosaic client and asks for\n * `SELECT <column>, count(*) … GROUP BY <column>` filtered by `filterBy`. The counts therefore\n * react to the *other* filters but not to this one — a crossfilter resolver drops a clause when it\n * builds the predicate for the clause's own source, and `clausePoints` tags the clause with this\n * client. (A non-crossfilter `filterBy` has no such exemption and would count itself.)\n */\nexport function ChartFilter(props: ChartFilterProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n options,\n format = defaultFormat,\n label = column,\n multiple = true,\n defaultValue,\n limit = FILTER_LIMIT,\n searchable = false,\n disabled,\n size = \"md\",\n className,\n controlClassName,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = options === undefined && Boolean(table && column);\n\n const { rows, selected, publish, activate } = useMosaicInput<readonly unknown[]>({\n as: target,\n filterBy: source,\n deps: [table, column, lookup, limit],\n activateValue: [0],\n build: (filter) =>\n lookup\n ? Query.from(table as string)\n .select({ value: column as string, count: sqlCount() })\n .where(filter)\n .groupby(column as string)\n .orderby(sqlDesc(sqlCount()), column as string)\n // One past the cap: enough to know the tail exists without asking a second question.\n .limit(limit + 1)\n : null,\n clause: (client, value) =>\n multiple\n ? clausePoints([field as string], value?.map((entry) => [entry]), { source: client })\n : clausePoint(field as string, value?.[0], { source: client }),\n decode: (value) => (multiple ? (value as unknown[][]).map((tuple) => tuple[0]) : [value]),\n });\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || defaultValue === undefined) return;\n primed.current = true;\n publish(Array.isArray(defaultValue) ? (defaultValue as unknown[]) : [defaultValue]);\n }, [defaultValue, publish]);\n\n const values = selected ?? [];\n\n const fetched = filterEntries(options, rows, format);\n const truncated = fetched.length > limit;\n const entries = truncated ? fetched.slice(0, limit) : fetched;\n const items: FacetFilterItem[] = entries.map((entry) => ({\n count: entry.count,\n icon: entry.icon,\n label: entry.label,\n value: entry.key,\n }));\n\n // `FacetFilter` speaks strings; a clause carries the raw value the query answered with, so the\n // way back from a ticked row to `alpha` / `3` / `true` is this map. The selected values are in it\n // too: one of them may be a value the crossfilter has since faceted away, which `FacetFilter`\n // keeps listed so it can still be unticked.\n const raw = new Map<string, unknown>(entries.map((entry) => [entry.key, entry.raw]));\n for (const value of values) raw.set(String(value), value);\n\n return (\n <ark.div\n className={cn(\"w-fit\", className)}\n {...rest}\n {...warmUpHandlers(activate, props)}\n data-slot={slot ?? \"chart-filter\"}\n >\n <FacetFilter\n className={controlClassName}\n slot=\"chart-filter-trigger\"\n disabled={disabled}\n empty={lookup && rows === null ? \"Loading…\" : \"No values.\"}\n items={items}\n label={label}\n multiple={multiple}\n // A search field over a truncated list is a half-truth unless it names its own scope, so\n // both messages change when the tail was dropped: the field covers the fetched page, and a\n // miss inside it says nothing about the column.\n note={truncationNote(truncated, limit, searchable)}\n onValueChange={(next) => publish(next.map((key) => (raw.has(key) ? raw.get(key) : key)))}\n searchEmpty={truncated ? `No match among the top ${limit} values.` : undefined}\n searchable={searchable}\n size={size}\n value={values.map((value) => String(value))}\n />\n {children}\n </ark.div>\n );\n}\n\n// ── ChartSearch ──────────────────────────────────────────────────────────────\n\nexport interface ChartSearchProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the autocomplete values come from. */\n table?: string;\n /** The column to search. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /** Where the query string has to appear. */\n type?: \"contains\" | \"prefix\" | \"suffix\" | \"regexp\";\n caseSensitive?: boolean;\n label?: ReactNode;\n placeholder?: string;\n /** ms of quiet before publishing. `0` publishes on every keystroke, as vgplot does. */\n debounce?: number;\n /** Distinct values offered as a native autocomplete list. `0` skips the lookup entirely. */\n autocompleteLimit?: number;\n defaultValue?: string;\n disabled?: boolean;\n size?: ControlSize;\n /** Class for the control; `className` styles the wrapper. */\n controlClassName?: string;\n}\n\n/**\n * A text box publishing a **match** clause (`contains` / `prefix` / `suffix` / `regexp`).\n *\n * Every keystroke is a query, so the publish is debounced by default. The distinct values of the\n * column feed a native `<datalist>`, exactly as vgplot's `search` does — and, as there, the list\n * narrows with the query, since a match clause does not exempt its own client from the crossfilter.\n */\nexport function ChartSearch(props: ChartSearchProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n type = \"contains\",\n caseSensitive = false,\n label,\n placeholder = \"Search…\",\n debounce = 200,\n autocompleteLimit = 100,\n defaultValue,\n disabled,\n size = \"md\",\n className,\n controlClassName,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = autocompleteLimit > 0 && Boolean(table && column);\n\n const { rows, selected, publish, activate } = useMosaicInput<string>({\n as: target,\n filterBy: source,\n deps: [table, column, lookup, autocompleteLimit],\n activateValue: \"\",\n build: (filter) =>\n lookup\n ? Query.from(table as string)\n .select({ value: column as string })\n .distinct()\n .where(filter)\n .orderby(column as string)\n .limit(autocompleteLimit)\n : null,\n clause: (client, value) =>\n clauseMatch(field as string, value ?? null, { source: client, method: type, caseSensitive }),\n });\n\n // The completions, filtered in the browser. The query already asked the database for the\n // `autocompleteLimit` distinct values of the column; narrowing THOSE as you type is not a second\n // question, so it does not become a second query.\n const items = useMemo(\n () => (rows ?? []).map((row) => ({ label: String(row.value), value: String(row.value) })),\n [rows],\n );\n const capped = items.length >= autocompleteLimit;\n const { contains } = useFilter({ sensitivity: \"base\" });\n const { collection, filter: completions, set } = useListCollection<{\n label: string;\n value: string;\n }>({ filter: contains, initialItems: [] });\n\n // Keyed on the values, not on the array: `rows` is a fresh array on every settled query, and\n // setting the collection from an identity-unstable dep is a render loop.\n const itemsKey = items.map((item) => item.value).join(\"\\u0000\");\n useEffect(() => {\n set(items);\n // `items` is derived from `itemsKey`; depending on it directly is the loop above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [itemsKey, set]);\n\n const [text, setText] = useState(defaultValue ?? \"\");\n const published = useRef(defaultValue ?? \"\");\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n const external = selected ?? \"\";\n if (external !== published.current) {\n published.current = external;\n setText(external);\n }\n }, [selected]);\n\n useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || !defaultValue) return;\n primed.current = true;\n publish(defaultValue);\n }, [defaultValue, publish]);\n\n const change = (next: string) => {\n setText(next);\n if (timer.current) clearTimeout(timer.current);\n const send = () => {\n published.current = next;\n publish(next || undefined);\n };\n if (debounce > 0) timer.current = setTimeout(send, debounce);\n else send();\n };\n\n return (\n <Field\n className={cn(\"w-fit min-w-48 gap-1.5\", className)}\n disabled={disabled}\n {...rest}\n {...warmUpHandlers(activate, props)}\n slot={slot ?? \"chart-search\"}\n >\n {label ? <FieldLabel>{label}</FieldLabel> : null}\n {lookup ? (\n // `allowCustomValue`, because this control is a FILTER and not a picker: what it publishes\n // is a match clause over whatever you typed, and the list only ever offered completions.\n // A `<datalist>` was the honest first answer to that and a poor one — unstyleable, different\n // in every browser, no empty state, and silent about its own cap. Same clause, same\n // debounce; only the surface changed.\n <Combobox\n allowCustomValue\n collection={collection}\n slot=\"chart-search-list\"\n disabled={disabled}\n inputValue={text}\n onInputValueChange={(details) => {\n completions(details.inputValue);\n change(details.inputValue);\n }}\n onValueChange={(details) => {\n const picked = details.value[0];\n if (picked !== undefined) change(picked);\n }}\n >\n <ComboboxInput className={controlClassName} placeholder={placeholder} size={size} />\n <ComboboxContent>\n <ComboboxEmpty>No matching values.</ComboboxEmpty>\n {collection.items.map((item) => (\n <ComboboxItem item={item} key={item.value}>\n {item.label}\n </ComboboxItem>\n ))}\n {/* The cap, said out loud. The datalist could not: it silently stopped at its limit,\n so \"nothing else matches\" and \"nothing else was asked for\" looked identical. */}\n {capped ? (\n <p className=\"border-t px-2 py-1.5 text-muted-foreground text-xs\">\n First {autocompleteLimit}. Keep typing to narrow it.\n </p>\n ) : null}\n </ComboboxContent>\n </Combobox>\n ) : (\n <Input\n className={controlClassName}\n disabled={disabled}\n onChange={(event) => change(event.target.value)}\n placeholder={placeholder}\n size={size}\n type=\"search\"\n value={text}\n />\n )}\n {children}\n </Field>\n );\n}\n\n// ── ChartSlider ──────────────────────────────────────────────────────────────\n\nexport interface ChartSliderProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the extent comes from, when `min`/`max` are not both given. */\n table?: string;\n /** The numeric column. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /**\n * `point` publishes an equality clause for one thumb; `interval` publishes a two-thumb range.\n * (vgplot's slider only ever offers `[min, value]`; ours brushes a real interval.)\n */\n select?: \"point\" | \"interval\";\n min?: number;\n max?: number;\n step?: number;\n /** Published once, when the control first connects. */\n defaultValue?: number | readonly [number, number];\n label?: ReactNode;\n /** Show the current value beside the label. */\n showValue?: boolean;\n disabled?: boolean;\n}\n\ntype SliderValueType = number | readonly [number, number];\n\ninterface SliderExtent {\n min: number;\n max: number;\n step: number;\n}\n\n/** vgplot's `(max - min) / 500`, except an all-integer extent steps by 1 — 500 fractions of a count is noise. */\nfunction defaultStep(min: number, max: number): number {\n if (Number.isInteger(min) && Number.isInteger(max)) return 1;\n return (max - min) / 500 || 1;\n}\n\n/**\n * A slider over a numeric column, publishing a **point** or **interval** clause.\n *\n * The extent is a query when `min`/`max` are not both given: `SELECT min(col), max(col)`, answered\n * by the coordinator. A `Skeleton` shaped like the track holds the space until it arrives.\n */\nexport function ChartSlider(props: ChartSliderProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n select = \"point\",\n min,\n max,\n step,\n defaultValue,\n label,\n showValue = true,\n disabled,\n className,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = (min === undefined || max === undefined) && Boolean(table && column);\n // The binning hint for the interval clause. A ref, not `extent` itself: reading the memo from\n // the clause would make the widget's own row type circular.\n const pixelSize = useRef(1);\n\n const { rows, selected, publish, activate } = useMosaicInput<SliderValueType>({\n as: target,\n filterBy: source,\n deps: [table, column, lookup],\n activateValue: select === \"interval\" ? ([0, 0] as const) : 0,\n build: (filter) =>\n lookup\n ? Query.select({ min: sqlMin(column as string), max: sqlMax(column as string) })\n .from(table as string)\n .where(filter)\n : null,\n clause: (client, value) => {\n if (select !== \"interval\") return clausePoint(field as string, value, { source: client });\n const domain = value as [number, number] | undefined;\n return clauseInterval(field as string, domain ?? null, {\n source: client,\n bin: \"ceil\",\n scale: domain ? { type: \"identity\", domain } : undefined,\n pixelSize: pixelSize.current,\n });\n },\n });\n\n const extent = useMemo<SliderExtent | null>(() => {\n const row = rows?.[0];\n const low = min ?? (typeof row?.min === \"number\" ? row.min : undefined);\n const high = max ?? (typeof row?.max === \"number\" ? row.max : undefined);\n if (low === undefined || high === undefined) return null;\n return { min: low, max: high, step: step ?? defaultStep(low, high) };\n }, [rows, min, max, step]);\n\n useEffect(() => {\n pixelSize.current = extent?.step ?? 1;\n }, [extent]);\n\n const [values, setValues] = useState<number[] | null>(\n defaultValue === undefined ? null : [...toValues(defaultValue)],\n );\n /** Distinguishes \"never published\" from \"published and then cleared\" — see the effect below. */\n const held = useRef(false);\n\n useEffect(() => {\n if (selected !== undefined) {\n held.current = true;\n setValues([...toValues(selected)]);\n return;\n }\n // The clause is gone: a `Selection.reset()`, a \"Clear filters\" button, another control\n // retracting it. Fall back to the extent instead of leaving the thumbs where they were — a\n // slider that still reads 5–22 while nothing is filtered is a control lying about the state,\n // which is worse than one that never moved. Gated on `held` so the first render does not\n // stomp on `defaultValue` before the priming effect has published it.\n if (held.current) {\n held.current = false;\n setValues(null);\n }\n }, [selected]);\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || defaultValue === undefined) return;\n primed.current = true;\n publish(defaultValue);\n }, [defaultValue, publish]);\n\n const change = (next: number[]) => {\n setValues(next);\n publish(select === \"interval\" ? [next[0] as number, next[1] as number] : (next[0] as number));\n };\n\n const shown = values ?? (extent ? (select === \"interval\" ? [extent.min, extent.max] : [extent.min]) : null);\n\n return (\n <ark.div\n className={cn(\"flex w-full min-w-48 flex-col gap-1.5\", className)}\n {...rest}\n {...warmUpHandlers(activate, props)}\n data-slot={slot ?? \"chart-slider\"}\n >\n {extent && shown ? (\n <Slider\n disabled={disabled}\n max={extent.max}\n min={extent.min}\n onValueChange={(details) => change(details.value)}\n step={extent.step}\n value={shown}\n >\n {label || showValue ? (\n <div className=\"flex items-center\">\n {label ? <SliderLabel>{label}</SliderLabel> : null}\n {showValue ? <SliderValue /> : null}\n </div>\n ) : null}\n </Slider>\n ) : (\n <Skeleton className=\"h-2 w-full rounded-full\" />\n )}\n {children}\n </ark.div>\n );\n}\n\nfunction toValues(value: SliderValueType): readonly number[] {\n return typeof value === \"number\" ? [value] : value;\n}\n"],"names":["min","max"],"mappings":";;;;;;;;;;;;;;;;;AA0FO;AAGL;AAGA;AAEA;AAKA;AACE;AAAqB;AACP;AAC2B;AACvC;AAEF;AAKE;AAC+B;AACjC;AAKA;AACA;AACE;AAEA;AAA6E;AAE/E;AAGE;AAGyF;AAC3F;AAGF;AAAgB;AAEZ;AACA;AACA;AAAoE;AACtE;AACG;AAIH;AACA;AAA6F;AAG/F;AACF;AAGA;AAIE;AAAO;;AAEH;AACA;AACF;;AAEE;AACoB;AACtB;AAEJ;AAgEA;AAUA;AACE;AACF;AAEA;AACE;AACF;AAGA;AACE;AACA;AAEF;AAEA;AACE;AACA;AAGF;AAEA;AAKE;AAEI;AACA;AAAO;AACmB;AACkB;AAC9B;AACC;AAAA;AAIe;AACX;AACE;AACd;AACe;AAE5B;AAYO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACA;AACS;AACD;AACG;AACX;AACQ;AACK;AACb;AACO;AACP;AACA;AACA;AACA;AACG;AAO4E;AAC3E;AACM;AACyB;AAClB;AAUX;AAI2D;AACsB;AAIzF;AACE;AAEkF;AAGpF;AAKyD;AAC1C;AACD;AACC;AACA;AAQf;AAEA;AACE;AAAK;AAAJ;AACiC;AAC5B;AAC8B;AACf;AAEnB;AAAA;AAAC;AAAA;AACY;AACN;AACL;AAC8C;AAC9C;AACA;AACA;AAIiD;AACsC;AAClB;AACrE;AACA;AAC0C;AAAA;AAAA;AAE3C;AAAA;AAAA;AAGP;AAoCO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACO;AACS;AAChB;AACc;AACH;AACS;AACpB;AACA;AACO;AACP;AACA;AACA;AACA;AACG;AAOgE;AAC/D;AACM;AACqC;AAChC;AAST;AAEuF;AAMjF;AAC4E;AACnF;AAYP;AACE;AAAS;AAKX;AAIA;AACE;AACA;AAEkB;AAII;AAA6C;AAErE;AACA;AACE;AAEoB;AAGtB;AACE;AAEA;AACE;AACyB;AAE3B;AACK;AAGP;AACE;AAAC;AAAA;AACkD;AACjD;AACI;AAC8B;AACpB;AAEb;AAA2C;AAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAMC;AAAC;AAAA;AACiB;AAChB;AACK;AACL;AACY;AAEV;AACyB;AAC3B;AAEE;AACA;AAAuC;AACzC;AAEA;AAAkF;AAEhF;AAAkC;AAKjC;AAImE;AAAA;AACzD;AAAkB;AAEzB;AACN;AAAA;AAAA;AAAA;AAGF;AAAC;AAAA;AACY;AACX;AAC8C;AAC9C;AACA;AACK;AACE;AAAA;AAAA;AAGV;AAAA;AAAA;AAGP;AAsCA;AACE;AAEF;AAQO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACS;AACTA;AACAC;AACA;AACA;AACA;AACY;AACZ;AACA;AACA;AACA;AACG;AAUyE;AACxE;AACM;AACkB;AAC+B;AAMrD;AAEJ;AACA;AACA;AAAuD;AAC7C;AACH;AAC0C;AAC1B;AACtB;AACH;AAIA;AAGA;AACiE;AAGnE;AACE;AAAoC;AAGtC;AAA4B;AACoC;AAKhE;AACE;AACE;AAEA;AAAA;AAOF;AAEgB;AAIlB;AACA;AACE;AAEoB;AAGtB;AACE;AAC4F;AAK9F;AACE;AAAK;AAAJ;AACiE;AAC5D;AAC8B;AACf;AAElB;AACC;AAAC;AAAA;AACC;AACY;AACA;AACoC;AACnC;AACN;AAIF;AAA6C;AACf;AAE/B;AAAA;AAGwC;AAE/C;AAAA;AAAA;AAGP;AAEA;AACE;AACF;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"chart-inputs.js","sources":["../../src/charts/chart-inputs.tsx"],"sourcesContent":["\"use client\";\n\nimport type { TableExpr } from \"@kanzo-tech/mosaic\";\nimport { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from \"react\";\nimport type React from \"react\";\nimport { ark } from \"@ark-ui/react/factory\";\nimport {\n clauseInterval,\n clauseMatch,\n clausePoint,\n clausePoints,\n MosaicClient,\n type Selection,\n type SelectionClause,\n} from \"@uwdata/mosaic-core\";\nimport {\n count as sqlCount,\n desc as sqlDesc,\n max as sqlMax,\n min as sqlMin,\n Query,\n type FilterExpr,\n} from \"@uwdata/mosaic-sql\";\nimport { useListCollection } from \"@ark-ui/react/collection\";\nimport { useFilter } from \"@ark-ui/react/locale\";\nimport { cn } from \"../lib/cn.js\";\nimport {\n Combobox,\n ComboboxContent,\n ComboboxEmpty,\n ComboboxInput,\n ComboboxItem,\n} from \"../simples/combobox.js\";\nimport { FacetFilter, type FacetFilterItem } from \"../simples/FacetFilter.js\";\nimport { Field, FieldLabel } from \"../simples/field.js\";\nimport { Input } from \"../simples/input.js\";\nimport { Skeleton } from \"../simples/skeleton.js\";\nimport { Slider, SliderLabel, SliderValue } from \"../simples/slider.js\";\nimport { chartTableKey } from \"./chart-spec.js\";\nimport { useMosaic } from \"./mosaic-provider.js\";\nimport { ChartQueryClient, type ChartQueryRow as QueryRow } from \"./query-client.js\";\n\n/**\n * Mosaic **inputs** — controls that publish into a `Selection` without being charts.\n *\n * Unlike marks and interactors, these are real DOM parts (`ark.*`, `data-slot`, forwarded props),\n * not inert descriptors: they carry no `__chart` static and belong *outside* `<ChartRoot>`. What\n * makes them Mosaic and not just a styled `<select>` is that each one is a `MosaicClient`: it\n * declares a `query()` for the values it needs (grouped counts, distinct values, a numeric extent),\n * the coordinator answers it, and the control publishes `clausePoints` / `clauseMatch` /\n * `clauseInterval` back into the selection the charts filter by.\n *\n * The vgplot equivalents (`vg.menu`, `vg.search`, `vg.slider`) build raw HTML. These use our\n * controls, our tokens and our accessibility, and are controllable *and* observable: an external\n * change to the selection flows back into the widget.\n */\n\n\nexport interface MosaicInputOptions<T> {\n /** Filters the widget's own lookup query. `null` = the full relation. */\n filterBy: Selection | null;\n /** Where the widget publishes. `null` = nowhere. */\n as: Selection | null;\n /** The lookup query, or `null` when there is nothing to ask the coordinator for. */\n build: (filter: FilterExpr) => Query | null;\n /** Rebuilds the client — and re-runs the lookup — when any entry changes. */\n deps: readonly unknown[];\n clause: (source: MosaicClient, value: T | undefined) => SelectionClause;\n /** The representative value of the warm-up clause published on hover/focus. */\n activateValue: T;\n /** Reads the widget's own clause value back out of the selection. Defaults to an identity cast. */\n decode?: (value: unknown) => T;\n}\n\nexport interface MosaicInputState<T> {\n /** Lookup rows, or `null` while the query is in flight (or when there is no query). */\n rows: readonly QueryRow[] | null;\n /** The value this widget currently holds in the selection — its observable half. */\n selected: T | undefined;\n publish: (value: T | undefined) => void;\n activate: () => void;\n}\n\n/**\n * The conversation with Mosaic that every input in this file has: a lookup query that re-runs under\n * the crossfilter, a clause published with `source` set so a control never filters itself, and a\n * warm-up so the first interaction is not the first query.\n *\n * Exported because the three controls here are surfaces over it, not the only surfaces it allows —\n * a chip row, a segmented switch or a map lasso is the same conversation with different chrome, and\n * without this the alternative is reimplementing it.\n */\nexport function useMosaicInput<T>(\n options: MosaicInputOptions<T>\n): MosaicInputState<T> {\n const { filterBy, as, deps } = options;\n const { coordinator } = useMosaic();\n const latest = useRef(options);\n latest.current = options;\n\n const clientRef = useRef<ChartQueryClient | null>(null);\n const [client, setClient] = useState<ChartQueryClient | null>(null);\n const [rows, setRows] = useState<readonly QueryRow[] | null>(null);\n const [selected, setSelected] = useState<T | undefined>(undefined);\n\n useEffect(() => {\n const instance = new ChartQueryClient(\n filterBy ?? undefined,\n (filter) => latest.current.build(filter),\n setRows,\n );\n clientRef.current = instance;\n setRows(null);\n setClient(instance);\n coordinator.connect(instance);\n return () => {\n clientRef.current = null;\n coordinator.disconnect(instance);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [coordinator, filterBy, ...deps]);\n\n useEffect(() => {\n if (!as || !client) return;\n const sync = () => {\n const raw = as.valueFor(client);\n const { decode } = latest.current;\n setSelected(raw === undefined ? undefined : decode ? decode(raw) : (raw as T));\n };\n sync();\n as.addEventListener(\"value\", sync);\n return () => {\n as.removeEventListener(\"value\", sync);\n // `Selection.remove` returns a *clone*; the live way to retract a clause is to publish an\n // empty one, which the resolver drops because its predicate is null.\n if (as.valueFor(client) !== undefined) as.update(latest.current.clause(client, undefined));\n };\n }, [as, client]);\n\n const publish = useCallback(\n (value: T | undefined) => {\n setSelected(value);\n const instance = clientRef.current;\n if (as && instance) as.update(latest.current.clause(instance, value));\n },\n [as],\n );\n\n const activate = useCallback(() => {\n const instance = clientRef.current;\n if (as && instance) as.activate(latest.current.clause(instance, latest.current.activateValue));\n }, [as]);\n\n return { rows, selected, publish, activate };\n}\n\n/** vgplot's inputs warm the selection on hover/focus; ours do it on the wrapper, and compose. */\nfunction warmUpHandlers(\n activate: () => void,\n props: Pick<React.ComponentProps<typeof ark.div>, \"onFocus\" | \"onPointerEnter\">,\n) {\n return {\n onFocus: (event: React.FocusEvent<HTMLDivElement>) => {\n props.onFocus?.(event);\n activate();\n },\n onPointerEnter: (event: React.PointerEvent<HTMLDivElement>) => {\n props.onPointerEnter?.(event);\n if (!event.buttons) activate();\n },\n };\n}\n\ninterface SelectionProps {\n /** Where the control publishes. Defaults to the provider's crossfilter; `null` = nowhere. */\n as?: Selection | null;\n /** What the control's own lookup filters by. Defaults to the crossfilter; `null` = unfiltered. */\n filterBy?: Selection | null;\n}\n\ntype ControlSize = \"sm\" | \"md\" | \"lg\";\n\n// ── ChartFilter ──────────────────────────────────────────────────────────────\n\n/** An explicit entry. Bare values work too — they are labelled by `format`. */\nexport interface ChartFilterOption {\n value: unknown;\n label?: string;\n icon?: React.ComponentType<{ className?: string }>;\n}\n\nexport interface ChartFilterProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the values and their counts come from, as `ChartRoot` takes it. */\n table?: TableExpr;\n /** The column grouped by. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /** Explicit options — a literal value or a `{ value, label, icon }` object. Skips the lookup. */\n options?: readonly unknown[];\n /** Labels an option value that carries no explicit label. */\n format?: (value: unknown) => string;\n /** Trigger label. Defaults to `column`. */\n label?: ReactNode;\n /** Tick any number of values (an `IN` list). `false` keeps at most one ticked. */\n multiple?: boolean;\n /** Published once, when the control first connects. A bare value or an array of them. */\n defaultValue?: unknown;\n /** Most-frequent values kept; past this the list says so rather than rendering the tail. */\n limit?: number;\n /**\n * Draw a filter field above the values.\n *\n * It narrows **the fetched page** — the `limit` most frequent values this control asked for — and\n * runs no query of its own. When the page is truncated the note and the no-match message say so,\n * because a field that answers \"no matching values\" about a database it never asked is worse than\n * no field. Searching the column itself is `ChartSearch`, which publishes a match clause.\n *\n * @default false\n */\n searchable?: boolean;\n disabled?: boolean;\n size?: ControlSize;\n /** Class for the trigger; `className` styles the wrapper. */\n controlClassName?: string;\n}\n\n/**\n * A `GROUP BY` over a high-cardinality column can answer with thousands of rows, and neither the\n * wire nor the DOM wants them. The lookup is capped at the *most frequent* values — the ones a\n * facet is for — and the control owns up to the truncation instead of pretending the tail is\n * absent. Needle-in-a-haystack lookup is `ChartSearch`'s job, not a facet filter's.\n */\nconst FILTER_LIMIT = 50;\n\ninterface FilterEntry {\n key: string;\n label: string;\n raw: unknown;\n count?: number;\n icon?: React.ComponentType<{ className?: string }>;\n}\n\nfunction isOptionObject(value: unknown): value is ChartFilterOption {\n return typeof value === \"object\" && value !== null && !Array.isArray(value) && \"value\" in value;\n}\n\nfunction defaultFormat(value: unknown): string {\n return String(value ?? \"\");\n}\n\n/** DuckDB answers a `count(*)` with a BigInt over the WASM bridge. */\nfunction toCount(value: unknown): number | undefined {\n if (typeof value === \"number\") return value;\n if (typeof value === \"bigint\") return Number(value);\n return undefined;\n}\n\nfunction truncationNote(truncated: boolean, limit: number, searchable: boolean) {\n if (!truncated) return undefined;\n return searchable\n ? `Top ${limit} values — the field searches these, not the column. Filter further for the rest.`\n : `Top ${limit} values — filter further to see the rest.`;\n}\n\nfunction filterEntries(\n options: readonly unknown[] | undefined,\n rows: readonly QueryRow[] | null,\n format: (value: unknown) => string,\n): FilterEntry[] {\n if (options) {\n return options.map((entry) => {\n const option = isOptionObject(entry) ? entry : { value: entry };\n return {\n key: String(option.value),\n label: option.label ?? format(option.value),\n raw: option.value,\n icon: option.icon,\n };\n });\n }\n return (rows ?? []).map((row) => ({\n key: String(row.value),\n label: format(row.value),\n raw: row.value,\n count: toCount(row.count),\n }));\n}\n\n/**\n * A facet filter over a column's values, publishing a **points** clause — the same `FacetFilter`\n * surface as the table's, so a dashboard has one filter idiom rather than two.\n *\n * The values are a query, not a prop: the control connects as a Mosaic client and asks for\n * `SELECT <column>, count(*) … GROUP BY <column>` filtered by `filterBy`. The counts therefore\n * react to the *other* filters but not to this one — a crossfilter resolver drops a clause when it\n * builds the predicate for the clause's own source, and `clausePoints` tags the clause with this\n * client. (A non-crossfilter `filterBy` has no such exemption and would count itself.)\n */\nexport function ChartFilter(props: ChartFilterProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n options,\n format = defaultFormat,\n label = column,\n multiple = true,\n defaultValue,\n limit = FILTER_LIMIT,\n searchable = false,\n disabled,\n size = \"md\",\n className,\n controlClassName,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = options === undefined && Boolean(table && column);\n\n const { rows, selected, publish, activate } = useMosaicInput<readonly unknown[]>({\n as: target,\n filterBy: source,\n deps: [chartTableKey(table), column, lookup, limit],\n activateValue: [0],\n build: (filter) =>\n lookup\n ? Query.from(table as TableExpr)\n .select({ value: column as string, count: sqlCount() })\n .where(filter)\n .groupby(column as string)\n .orderby(sqlDesc(sqlCount()), column as string)\n // One past the cap: enough to know the tail exists without asking a second question.\n .limit(limit + 1)\n : null,\n clause: (client, value) =>\n multiple\n ? clausePoints([field as string], value?.map((entry) => [entry]), { source: client })\n : clausePoint(field as string, value?.[0], { source: client }),\n decode: (value) => (multiple ? (value as unknown[][]).map((tuple) => tuple[0]) : [value]),\n });\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || defaultValue === undefined) return;\n primed.current = true;\n publish(Array.isArray(defaultValue) ? (defaultValue as unknown[]) : [defaultValue]);\n }, [defaultValue, publish]);\n\n const values = selected ?? [];\n\n const fetched = filterEntries(options, rows, format);\n const truncated = fetched.length > limit;\n const entries = truncated ? fetched.slice(0, limit) : fetched;\n const items: FacetFilterItem[] = entries.map((entry) => ({\n count: entry.count,\n icon: entry.icon,\n label: entry.label,\n value: entry.key,\n }));\n\n // `FacetFilter` speaks strings; a clause carries the raw value the query answered with, so the\n // way back from a ticked row to `alpha` / `3` / `true` is this map. The selected values are in it\n // too: one of them may be a value the crossfilter has since faceted away, which `FacetFilter`\n // keeps listed so it can still be unticked.\n const raw = new Map<string, unknown>(entries.map((entry) => [entry.key, entry.raw]));\n for (const value of values) raw.set(String(value), value);\n\n return (\n <ark.div\n className={cn(\"w-fit\", className)}\n {...rest}\n {...warmUpHandlers(activate, props)}\n data-slot={slot ?? \"chart-filter\"}\n >\n <FacetFilter\n className={controlClassName}\n slot=\"chart-filter-trigger\"\n disabled={disabled}\n empty={lookup && rows === null ? \"Loading…\" : \"No values.\"}\n items={items}\n label={label}\n multiple={multiple}\n // A search field over a truncated list is a half-truth unless it names its own scope, so\n // both messages change when the tail was dropped: the field covers the fetched page, and a\n // miss inside it says nothing about the column.\n note={truncationNote(truncated, limit, searchable)}\n onValueChange={(next) => publish(next.map((key) => (raw.has(key) ? raw.get(key) : key)))}\n searchEmpty={truncated ? `No match among the top ${limit} values.` : undefined}\n searchable={searchable}\n size={size}\n value={values.map((value) => String(value))}\n />\n {children}\n </ark.div>\n );\n}\n\n// ── ChartSearch ──────────────────────────────────────────────────────────────\n\nexport interface ChartSearchProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the autocomplete values come from, as `ChartRoot` takes it. */\n table?: TableExpr;\n /** The column to search. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /** Where the query string has to appear. */\n type?: \"contains\" | \"prefix\" | \"suffix\" | \"regexp\";\n caseSensitive?: boolean;\n label?: ReactNode;\n placeholder?: string;\n /** ms of quiet before publishing. `0` publishes on every keystroke, as vgplot does. */\n debounce?: number;\n /** Distinct values offered as a native autocomplete list. `0` skips the lookup entirely. */\n autocompleteLimit?: number;\n defaultValue?: string;\n disabled?: boolean;\n size?: ControlSize;\n /** Class for the control; `className` styles the wrapper. */\n controlClassName?: string;\n}\n\n/**\n * A text box publishing a **match** clause (`contains` / `prefix` / `suffix` / `regexp`).\n *\n * Every keystroke is a query, so the publish is debounced by default. The distinct values of the\n * column feed a native `<datalist>`, exactly as vgplot's `search` does — and, as there, the list\n * narrows with the query, since a match clause does not exempt its own client from the crossfilter.\n */\nexport function ChartSearch(props: ChartSearchProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n type = \"contains\",\n caseSensitive = false,\n label,\n placeholder = \"Search…\",\n debounce = 200,\n autocompleteLimit = 100,\n defaultValue,\n disabled,\n size = \"md\",\n className,\n controlClassName,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = autocompleteLimit > 0 && Boolean(table && column);\n\n const { rows, selected, publish, activate } = useMosaicInput<string>({\n as: target,\n filterBy: source,\n deps: [chartTableKey(table), column, lookup, autocompleteLimit],\n activateValue: \"\",\n build: (filter) =>\n lookup\n ? Query.from(table as TableExpr)\n .select({ value: column as string })\n .distinct()\n .where(filter)\n .orderby(column as string)\n .limit(autocompleteLimit)\n : null,\n clause: (client, value) =>\n clauseMatch(field as string, value ?? null, { source: client, method: type, caseSensitive }),\n });\n\n // The completions, filtered in the browser. The query already asked the database for the\n // `autocompleteLimit` distinct values of the column; narrowing THOSE as you type is not a second\n // question, so it does not become a second query.\n const items = useMemo(\n () => (rows ?? []).map((row) => ({ label: String(row.value), value: String(row.value) })),\n [rows],\n );\n const capped = items.length >= autocompleteLimit;\n const { contains } = useFilter({ sensitivity: \"base\" });\n const { collection, filter: completions, set } = useListCollection<{\n label: string;\n value: string;\n }>({ filter: contains, initialItems: [] });\n\n // Keyed on the values, not on the array: `rows` is a fresh array on every settled query, and\n // setting the collection from an identity-unstable dep is a render loop.\n const itemsKey = items.map((item) => item.value).join(\"\\u0000\");\n useEffect(() => {\n set(items);\n // `items` is derived from `itemsKey`; depending on it directly is the loop above.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [itemsKey, set]);\n\n const [text, setText] = useState(defaultValue ?? \"\");\n const published = useRef(defaultValue ?? \"\");\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n const external = selected ?? \"\";\n if (external !== published.current) {\n published.current = external;\n setText(external);\n }\n }, [selected]);\n\n useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || !defaultValue) return;\n primed.current = true;\n publish(defaultValue);\n }, [defaultValue, publish]);\n\n const change = (next: string) => {\n setText(next);\n if (timer.current) clearTimeout(timer.current);\n const send = () => {\n published.current = next;\n publish(next || undefined);\n };\n if (debounce > 0) timer.current = setTimeout(send, debounce);\n else send();\n };\n\n return (\n <Field\n className={cn(\"w-fit min-w-48 gap-1.5\", className)}\n disabled={disabled}\n {...rest}\n {...warmUpHandlers(activate, props)}\n slot={slot ?? \"chart-search\"}\n >\n {label ? <FieldLabel>{label}</FieldLabel> : null}\n {lookup ? (\n // `allowCustomValue`, because this control is a FILTER and not a picker: what it publishes\n // is a match clause over whatever you typed, and the list only ever offered completions.\n // A `<datalist>` was the honest first answer to that and a poor one — unstyleable, different\n // in every browser, no empty state, and silent about its own cap. Same clause, same\n // debounce; only the surface changed.\n <Combobox\n allowCustomValue\n collection={collection}\n slot=\"chart-search-list\"\n disabled={disabled}\n inputValue={text}\n onInputValueChange={(details) => {\n completions(details.inputValue);\n change(details.inputValue);\n }}\n onValueChange={(details) => {\n const picked = details.value[0];\n if (picked !== undefined) change(picked);\n }}\n >\n <ComboboxInput className={controlClassName} placeholder={placeholder} size={size} />\n <ComboboxContent>\n <ComboboxEmpty>No matching values.</ComboboxEmpty>\n {collection.items.map((item) => (\n <ComboboxItem item={item} key={item.value}>\n {item.label}\n </ComboboxItem>\n ))}\n {/* The cap, said out loud. The datalist could not: it silently stopped at its limit,\n so \"nothing else matches\" and \"nothing else was asked for\" looked identical. */}\n {capped ? (\n <p className=\"border-t px-2 py-1.5 text-muted-foreground text-xs\">\n First {autocompleteLimit}. Keep typing to narrow it.\n </p>\n ) : null}\n </ComboboxContent>\n </Combobox>\n ) : (\n <Input\n className={controlClassName}\n disabled={disabled}\n onChange={(event) => change(event.target.value)}\n placeholder={placeholder}\n size={size}\n type=\"search\"\n value={text}\n />\n )}\n {children}\n </Field>\n );\n}\n\n// ── ChartSlider ──────────────────────────────────────────────────────────────\n\nexport interface ChartSliderProps\n extends Omit<React.ComponentProps<typeof ark.div>, \"defaultValue\">,\n SelectionProps {\n /** The relation the extent comes from, when `min`/`max` are not both given. */\n table?: TableExpr;\n /** The numeric column. */\n column?: string;\n /** The column named in the published clause. Defaults to `column`. */\n field?: string;\n /**\n * `point` publishes an equality clause for one thumb; `interval` publishes a two-thumb range.\n * (vgplot's slider only ever offers `[min, value]`; ours brushes a real interval.)\n */\n select?: \"point\" | \"interval\";\n min?: number;\n max?: number;\n step?: number;\n /** Published once, when the control first connects. */\n defaultValue?: number | readonly [number, number];\n label?: ReactNode;\n /** Show the current value beside the label. */\n showValue?: boolean;\n disabled?: boolean;\n}\n\ntype SliderValueType = number | readonly [number, number];\n\ninterface SliderExtent {\n min: number;\n max: number;\n step: number;\n}\n\n/** vgplot's `(max - min) / 500`, except an all-integer extent steps by 1 — 500 fractions of a count is noise. */\nfunction defaultStep(min: number, max: number): number {\n if (Number.isInteger(min) && Number.isInteger(max)) return 1;\n return (max - min) / 500 || 1;\n}\n\n/**\n * A slider over a numeric column, publishing a **point** or **interval** clause.\n *\n * The extent is a query when `min`/`max` are not both given: `SELECT min(col), max(col)`, answered\n * by the coordinator. A `Skeleton` shaped like the track holds the space until it arrives.\n */\nexport function ChartSlider(props: ChartSliderProps) {\n const {\n table,\n column,\n field = column,\n as,\n filterBy,\n select = \"point\",\n min,\n max,\n step,\n defaultValue,\n label,\n showValue = true,\n disabled,\n className,\n children,\n slot,\n ...rest\n } = props;\n const { crossfilter } = useMosaic();\n const target = as === undefined ? crossfilter : as;\n const source = filterBy === undefined ? crossfilter : filterBy;\n const lookup = (min === undefined || max === undefined) && Boolean(table && column);\n // The binning hint for the interval clause. A ref, not `extent` itself: reading the memo from\n // the clause would make the widget's own row type circular.\n const pixelSize = useRef(1);\n\n const { rows, selected, publish, activate } = useMosaicInput<SliderValueType>({\n as: target,\n filterBy: source,\n deps: [chartTableKey(table), column, lookup],\n activateValue: select === \"interval\" ? ([0, 0] as const) : 0,\n build: (filter) =>\n lookup\n ? Query.select({ min: sqlMin(column as string), max: sqlMax(column as string) })\n .from(table as TableExpr)\n .where(filter)\n : null,\n clause: (client, value) => {\n if (select !== \"interval\") return clausePoint(field as string, value, { source: client });\n const domain = value as [number, number] | undefined;\n return clauseInterval(field as string, domain ?? null, {\n source: client,\n bin: \"ceil\",\n scale: domain ? { type: \"identity\", domain } : undefined,\n pixelSize: pixelSize.current,\n });\n },\n });\n\n const extent = useMemo<SliderExtent | null>(() => {\n const row = rows?.[0];\n const low = min ?? (typeof row?.min === \"number\" ? row.min : undefined);\n const high = max ?? (typeof row?.max === \"number\" ? row.max : undefined);\n if (low === undefined || high === undefined) return null;\n return { min: low, max: high, step: step ?? defaultStep(low, high) };\n }, [rows, min, max, step]);\n\n useEffect(() => {\n pixelSize.current = extent?.step ?? 1;\n }, [extent]);\n\n const [values, setValues] = useState<number[] | null>(\n defaultValue === undefined ? null : [...toValues(defaultValue)],\n );\n /** Distinguishes \"never published\" from \"published and then cleared\" — see the effect below. */\n const held = useRef(false);\n\n useEffect(() => {\n if (selected !== undefined) {\n held.current = true;\n setValues([...toValues(selected)]);\n return;\n }\n // The clause is gone: a `Selection.reset()`, a \"Clear filters\" button, another control\n // retracting it. Fall back to the extent instead of leaving the thumbs where they were — a\n // slider that still reads 5–22 while nothing is filtered is a control lying about the state,\n // which is worse than one that never moved. Gated on `held` so the first render does not\n // stomp on `defaultValue` before the priming effect has published it.\n if (held.current) {\n held.current = false;\n setValues(null);\n }\n }, [selected]);\n\n const primed = useRef(false);\n useEffect(() => {\n if (primed.current || defaultValue === undefined) return;\n primed.current = true;\n publish(defaultValue);\n }, [defaultValue, publish]);\n\n const change = (next: number[]) => {\n setValues(next);\n publish(select === \"interval\" ? [next[0] as number, next[1] as number] : (next[0] as number));\n };\n\n const shown = values ?? (extent ? (select === \"interval\" ? [extent.min, extent.max] : [extent.min]) : null);\n\n return (\n <ark.div\n className={cn(\"flex w-full min-w-48 flex-col gap-1.5\", className)}\n {...rest}\n {...warmUpHandlers(activate, props)}\n data-slot={slot ?? \"chart-slider\"}\n >\n {extent && shown ? (\n <Slider\n disabled={disabled}\n max={extent.max}\n min={extent.min}\n onValueChange={(details) => change(details.value)}\n step={extent.step}\n value={shown}\n >\n {label || showValue ? (\n <div className=\"flex items-center\">\n {label ? <SliderLabel>{label}</SliderLabel> : null}\n {showValue ? <SliderValue /> : null}\n </div>\n ) : null}\n </Slider>\n ) : (\n <Skeleton className=\"h-2 w-full rounded-full\" />\n )}\n {children}\n </ark.div>\n );\n}\n\nfunction toValues(value: SliderValueType): readonly number[] {\n return typeof value === \"number\" ? [value] : value;\n}\n"],"names":["min","max"],"mappings":";;;;;;;;;;;;;;;;;;AA4FO;AAGL;AAGA;AAEA;AAKA;AACE;AAAqB;AACP;AAC2B;AACvC;AAEF;AAKE;AAC+B;AACjC;AAKA;AACA;AACE;AAEA;AAA6E;AAE/E;AAGE;AAGyF;AAC3F;AAGF;AAAgB;AAEZ;AACA;AACA;AAAoE;AACtE;AACG;AAIH;AACA;AAA6F;AAG/F;AACF;AAGA;AAIE;AAAO;;AAEH;AACA;AACF;;AAEE;AACoB;AACtB;AAEJ;AAgEA;AAUA;AACE;AACF;AAEA;AACE;AACF;AAGA;AACE;AACA;AAEF;AAEA;AACE;AACA;AAGF;AAEA;AAKE;AAEI;AACA;AAAO;AACmB;AACkB;AAC9B;AACC;AAAA;AAIe;AACX;AACE;AACd;AACe;AAE5B;AAYO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACA;AACS;AACD;AACG;AACX;AACQ;AACK;AACb;AACO;AACP;AACA;AACA;AACA;AACG;AAO4E;AAC3E;AACM;AACwC;AACjC;AAUX;AAI2D;AACsB;AAIzF;AACE;AAEkF;AAGpF;AAKyD;AAC1C;AACD;AACC;AACA;AAQf;AAEA;AACE;AAAK;AAAJ;AACiC;AAC5B;AAC8B;AACf;AAEnB;AAAA;AAAC;AAAA;AACY;AACN;AACL;AAC8C;AAC9C;AACA;AACA;AAIiD;AACsC;AAClB;AACrE;AACA;AAC0C;AAAA;AAAA;AAE3C;AAAA;AAAA;AAGP;AAoCO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACO;AACS;AAChB;AACc;AACH;AACS;AACpB;AACA;AACO;AACP;AACA;AACA;AACA;AACG;AAOgE;AAC/D;AACM;AACoD;AAC/C;AAST;AAEuF;AAMjF;AAC4E;AACnF;AAYP;AACE;AAAS;AAKX;AAIA;AACE;AACA;AAEkB;AAII;AAA6C;AAErE;AACA;AACE;AAEoB;AAGtB;AACE;AAEA;AACE;AACyB;AAE3B;AACK;AAGP;AACE;AAAC;AAAA;AACkD;AACjD;AACI;AAC8B;AACpB;AAEb;AAA2C;AAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAMC;AAAC;AAAA;AACiB;AAChB;AACK;AACL;AACY;AAEV;AACyB;AAC3B;AAEE;AACA;AAAuC;AACzC;AAEA;AAAkF;AAEhF;AAAkC;AAKjC;AAImE;AAAA;AACzD;AAAkB;AAEzB;AACN;AAAA;AAAA;AAAA;AAGF;AAAC;AAAA;AACY;AACX;AAC8C;AAC9C;AACA;AACK;AACE;AAAA;AAAA;AAGV;AAAA;AAAA;AAGP;AAsCA;AACE;AAEF;AAQO;AACL;AAAM;AACJ;AACA;AACQ;AACR;AACA;AACS;AACTA;AACAC;AACA;AACA;AACA;AACY;AACZ;AACA;AACA;AACA;AACG;AAUyE;AACxE;AACM;AACiC;AACgB;AAMrD;AAEJ;AACA;AACA;AAAuD;AAC7C;AACH;AAC0C;AAC1B;AACtB;AACH;AAIA;AAGA;AACiE;AAGnE;AACE;AAAoC;AAGtC;AAA4B;AACoC;AAKhE;AACE;AACE;AAEA;AAAA;AAOF;AAEgB;AAIlB;AACA;AACE;AAEoB;AAGtB;AACE;AAC4F;AAK9F;AACE;AAAK;AAAJ;AACiE;AAC5D;AAC8B;AACf;AAElB;AACC;AAAC;AAAA;AACC;AACY;AACA;AACoC;AACnC;AACN;AAIF;AAA6C;AACf;AAE/B;AAAA;AAGwC;AAE/C;AAAA;AAAA;AAGP;AAEA;AACE;AACF;;;;;;;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Selection } from '@uwdata/mosaic-core';
|
|
2
|
+
import { TableExpr } from '@kanzo-tech/mosaic';
|
|
2
3
|
import { ChartDescriptor } from './chart-spec.js';
|
|
3
4
|
/**
|
|
4
5
|
* The mark vocabulary. Every component here renders `null` — `ChartRoot` reads the `__chart`
|
|
@@ -35,7 +36,7 @@ export interface ChartMarkProps {
|
|
|
35
36
|
* until a plot needs two — a node-link view draws nodes from one relation and edges from
|
|
36
37
|
* another, and both still filter by the same selection.
|
|
37
38
|
*/
|
|
38
|
-
table?:
|
|
39
|
+
table?: TableExpr;
|
|
39
40
|
/** Literal rows instead of the root's table. */
|
|
40
41
|
data?: readonly unknown[];
|
|
41
42
|
/** Positions a rule/tick without a table: `at={0}` is the zero line. */
|