@bicharts/chart-host 0.6.103 → 0.6.106

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/index.mjs CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  formatSourceDateFor,
6
6
  isExcelDateFormat,
7
7
  normaliseAggregation
8
- } from "./chunk-D6A37G2G.mjs";
8
+ } from "./chunk-MU4BWWO6.mjs";
9
9
  import {
10
10
  captureSvgSnapshot,
11
11
  chartSvgOf,
@@ -63,7 +63,6 @@ import {
63
63
  censusColourSpread,
64
64
  censusMarks,
65
65
  censusValuePlacement,
66
- clearGeoCache,
67
66
  clippedTextFlag,
68
67
  colourSpreadFlag,
69
68
  compileRenderFn,
@@ -79,21 +78,16 @@ import {
79
78
  exportFileName,
80
79
  exportFormatsFor,
81
80
  exportTableService,
82
- geoAssetFor,
83
- geoFromCache,
84
81
  glyphSampleGrid,
85
82
  invalidSentinelReason,
86
83
  isBlankRender,
87
84
  isInvalidSentinelError,
88
85
  isPillBackdropAlpha,
89
- loadGeo,
90
86
  minContrastOver,
91
87
  newIdScope,
92
88
  noopViewStateProvider,
93
89
  pickColorOver,
94
90
  pillBacksGlyph,
95
- registerGeo,
96
- registerGeoAsset,
97
91
  relativeLuminance,
98
92
  requiredD3Plugins,
99
93
  resolveAutoColorScale,
@@ -111,7 +105,7 @@ import {
111
105
  toRGBA,
112
106
  toSourceRows,
113
107
  valuePlacementFlag
114
- } from "./chunk-4BR6VJQX.mjs";
108
+ } from "./chunk-K6UXTDMR.mjs";
115
109
  import {
116
110
  LASSO_AXES_SLOT,
117
111
  LASSO_COUNT_CLASS,
@@ -200,10 +194,18 @@ import {
200
194
  unpinScrolledAxis
201
195
  } from "./chunk-IE7TM6DX.mjs";
202
196
  import {
203
- _3,
197
+ clearGeoCache,
198
+ geoAssetFor,
199
+ geoFromCache,
200
+ loadGeo,
201
+ registerGeo,
202
+ registerGeoAsset
203
+ } from "./chunk-5WTQN5DL.mjs";
204
+ import {
205
+ Kt,
204
206
  l,
205
207
  o
206
- } from "./chunk-CNT3QA3R.mjs";
208
+ } from "./chunk-SYUJ4TPX.mjs";
207
209
  import "./chunk-A2GMXZP7.mjs";
208
210
  import {
209
211
  ACTIVE_TICK_CLASS,
@@ -334,7 +336,12 @@ var OPTIONS_VOCABULARY = Object.freeze({
334
336
  * that made the chart asked for them. "on" / "off" is a reader's choice and wins either way.
335
337
  */
336
338
  hoverDropLines: entry({ timing: "live", default: "", values: ["", "on", "off"] }),
339
+ /** A province map draws the national borders over its provinces unless this is false (a reader's "Country borders"). */
340
+ geoBorders: entry({ timing: "live", default: true }),
337
341
  // ---- the next generated chart ----
342
+ /** A reader's "Derive provinces": false measures no city or coordinate rollup, so only a real province column offers
343
+ * a province map. It changes what the profile measures, so it travels in the shape rather than as a field. */
344
+ deriveProvinces: entry({ timing: "next-generate", default: true }),
338
345
  levelOfDetail: entry({ timing: "next-generate", default: "", values: ["", "High", "Medium", "Low"], wire: "clientHints" }),
339
346
  favorLimitTo: entry({ timing: "next-generate", default: "", values: ["", "latest", "top / greatest", "bottom / smallest", "most common", "outliers"], wire: "clientHints" }),
340
347
  /** 0 asks for no limit, and travels as absent (`wireLimitCount`): any value the server receives is a limit. */
@@ -821,10 +828,10 @@ function buildProductFeedbackPayload(input) {
821
828
  };
822
829
  }
823
830
  var DEFAULT_TEXT2 = {
824
- label: "Ideas to improve this product",
825
- placeholder: "A feature you'd like, or something that got in your way",
826
- hint: "This sends a note to our team. It won't draw a chart.",
827
- open: "Suggest an improvement",
831
+ label: "Suggestions for the team that makes this product",
832
+ placeholder: "Message to the makers of this product: a feature you'd like, or something that got in your way. This is not a chart request.",
833
+ hint: "This goes to the people who build this product, not to the chart. It won't draw a chart.",
834
+ open: "Send a suggestion to the product team",
828
835
  send: "Send",
829
836
  sending: "Sending...",
830
837
  sent: "Thank you - your suggestion was sent.",
@@ -2290,7 +2297,7 @@ function fingerprintComponentDigest(components) {
2290
2297
 
2291
2298
  // src/index.ts
2292
2299
  function registerCityTable(packed) {
2293
- _3(packed);
2300
+ Kt(packed);
2294
2301
  }
2295
2302
  export {
2296
2303
  ACTIVE_TICK_CLASS,
@@ -0,0 +1,229 @@
1
+ /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
+ import {
3
+ geoFromCache,
4
+ registerGeo
5
+ } from "./chunk-5WTQN5DL.mjs";
6
+ import {
7
+ Si,
8
+ W,
9
+ bt
10
+ } from "./chunk-SYUJ4TPX.mjs";
11
+ import "./chunk-A2GMXZP7.mjs";
12
+ import "./chunk-GHODWOAL.mjs";
13
+
14
+ // src/provinceGeo.ts
15
+ var PROVINCE_REGIONS = ["europe", "asia", "africa", "south-america", "north-america", "oceania"];
16
+ function provinceGeoPath(region) {
17
+ return `/geo/admin1-${String(region).toLowerCase()}.v1.json`;
18
+ }
19
+ function provinceGeoKey(region) {
20
+ return `admin1:${String(region).toLowerCase()}`;
21
+ }
22
+ function geoKeyFor(res) {
23
+ if (res.geoGrain && res.geoGrain.toLowerCase() === "admin1" && res.geoRegion) return provinceGeoKey(res.geoRegion);
24
+ return res.geoKind ?? null;
25
+ }
26
+ var inflight = /* @__PURE__ */ new Map();
27
+ async function loadProvinceGeo(region, opts = {}) {
28
+ const key = provinceGeoKey(region);
29
+ const cached = geoFromCache(key);
30
+ if (cached) return cached;
31
+ if (!PROVINCE_REGIONS.includes(String(region).toLowerCase())) return void 0;
32
+ const url = opts.url ?? (opts.origin ? opts.origin.replace(/\/+$/, "") : "") + provinceGeoPath(region);
33
+ let p = inflight.get(key);
34
+ if (!p) {
35
+ const started = Date.now();
36
+ p = (async () => {
37
+ try {
38
+ const doc = opts.fetchJson ? await opts.fetchJson(url) : await (await fetch(url, { credentials: "omit" })).json();
39
+ if (!doc || doc.type !== "FeatureCollection" || !Array.isArray(doc.features) || doc.features.length === 0)
40
+ throw new Error("not a FeatureCollection");
41
+ registerGeo(key, doc);
42
+ opts.log?.("province-geo-loaded", { region, ms: Date.now() - started, features: doc.features.length });
43
+ return doc;
44
+ } catch (e) {
45
+ opts.log?.("province-geo-failed", { region, url, error: String(e).slice(0, 200) });
46
+ return void 0;
47
+ } finally {
48
+ inflight.delete(key);
49
+ }
50
+ })();
51
+ inflight.set(key, p);
52
+ }
53
+ return p;
54
+ }
55
+ var PROVINCE_SNAP_DEG = 0.05;
56
+ function shapesOf(geo) {
57
+ const out = [];
58
+ for (const f of geo?.features ?? []) {
59
+ const g = f?.geometry;
60
+ if (!g || f?.properties?.kind === "border" || f.id === null || f.id === void 0) continue;
61
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
62
+ if (!polys.length) continue;
63
+ let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
64
+ for (const poly of polys) for (const [x, y] of poly[0] ?? []) {
65
+ if (x < x0) x0 = x;
66
+ if (x > x1) x1 = x;
67
+ if (y < y0) y0 = y;
68
+ if (y > y1) y1 = y;
69
+ }
70
+ out.push({ id: String(f.id), polys, box: [x0, y0, x1, y1] });
71
+ }
72
+ return out;
73
+ }
74
+ function inRing(x, y, ring) {
75
+ let inside = false;
76
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
77
+ const [xi, yi] = ring[i], [xj, yj] = ring[j];
78
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
79
+ }
80
+ return inside;
81
+ }
82
+ function inShape(x, y, s) {
83
+ if (x < s.box[0] || x > s.box[2] || y < s.box[1] || y > s.box[3]) return false;
84
+ return s.polys.some((poly) => poly.length > 0 && inRing(x, y, poly[0]) && !poly.slice(1).some((h) => inRing(x, y, h)));
85
+ }
86
+ function segDist2(px, py, ax, ay, bx, by) {
87
+ const dx = bx - ax, dy = by - ay;
88
+ const t = dx === 0 && dy === 0 ? 0 : Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
89
+ const ex = ax + t * dx - px, ey = ay + t * dy - py;
90
+ return ex * ex + ey * ey;
91
+ }
92
+ var locators = /* @__PURE__ */ new WeakMap();
93
+ function provinceLocator(geo, snapDeg = PROVINCE_SNAP_DEG) {
94
+ if (geo && typeof geo === "object" && snapDeg === PROVINCE_SNAP_DEG) {
95
+ const hit = locators.get(geo);
96
+ if (hit) return hit;
97
+ }
98
+ const shapes = shapesOf(geo);
99
+ const locate = (lat, lon) => {
100
+ if (!isFinite(lat) || !isFinite(lon)) return null;
101
+ for (const x of [lon, lon + 360, lon - 360]) {
102
+ for (const s of shapes) if (inShape(x, lat, s)) return s.id;
103
+ }
104
+ const k = Math.cos(lat * Math.PI / 180) || 1e-6;
105
+ let best = null, bestD = snapDeg * snapDeg;
106
+ for (const s of shapes) {
107
+ if (lon < s.box[0] - snapDeg / k || lon > s.box[2] + snapDeg / k || lat < s.box[1] - snapDeg || lat > s.box[3] + snapDeg) continue;
108
+ for (const poly of s.polys) for (const ring of poly) for (let i = 1; i < ring.length; i++) {
109
+ const d = segDist2(lon * k, lat, ring[i - 1][0] * k, ring[i - 1][1], ring[i][0] * k, ring[i][1]);
110
+ if (d <= bestD) {
111
+ bestD = d;
112
+ best = s.id;
113
+ }
114
+ }
115
+ }
116
+ return best;
117
+ };
118
+ if (geo && typeof geo === "object" && snapDeg === PROVINCE_SNAP_DEG) locators.set(geo, locate);
119
+ return locate;
120
+ }
121
+
122
+ // src/admin1LookupLoader.ts
123
+ var ADMIN1_LOOKUP_PATH = "/geo/admin1-lookup.v1.json";
124
+ var inflight2 = null;
125
+ async function loadAdmin1Lookup(url, opts = {}) {
126
+ if (W()) return true;
127
+ const log = opts.log;
128
+ if (!inflight2) {
129
+ const started = Date.now();
130
+ inflight2 = (async () => {
131
+ try {
132
+ const doc = opts.fetchJson ? await opts.fetchJson(url) : await (await fetch(url, { credentials: "omit" })).json();
133
+ const counts = bt(doc);
134
+ log?.("admin1-lookup-loaded", { url, ms: Date.now() - started, ...counts });
135
+ return true;
136
+ } catch (e) {
137
+ log?.("admin1-lookup-failed", { url, ms: Date.now() - started, error: String(e).slice(0, 200) });
138
+ inflight2 = null;
139
+ return false;
140
+ }
141
+ })();
142
+ }
143
+ const timeoutMs = opts.timeoutMs ?? 3e3;
144
+ let timer;
145
+ const timeout = new Promise((resolve) => {
146
+ timer = setTimeout(() => {
147
+ log?.("admin1-lookup-timeout", { url, timeoutMs });
148
+ resolve(false);
149
+ }, timeoutMs);
150
+ });
151
+ try {
152
+ return await Promise.race([inflight2, timeout]);
153
+ } finally {
154
+ if (timer) clearTimeout(timer);
155
+ }
156
+ }
157
+ var PLACE_TOKENS = /(^|[^a-z])(city|cities|town|ville|stadt|ciudad|citta|cidade|state|province|provincia|provinz|county|department|departement|departamento|prefecture|oblast|canton|kanton|bundesland|municipality)([^a-z]|$)/i;
158
+ function needsAdmin1Lookup(columns) {
159
+ if (!columns) return false;
160
+ const geographic = columns.some((c) => {
161
+ const kind = (c?.geoKind ?? "").toLowerCase();
162
+ return kind.startsWith("country") || kind.startsWith("city");
163
+ });
164
+ if (geographic) return true;
165
+ return columns.some((c) => !!c && !c.isMeasure && !!c.name && PLACE_TOKENS.test(c.name.replace(/[_-]+/g, " ")));
166
+ }
167
+
168
+ // src/provinceHost.ts
169
+ async function prepareProvinceProfile(profile, opts) {
170
+ if (!profile || typeof profile.getColumnsWithStats !== "function") return;
171
+ let invalidate = false;
172
+ if (profile.deriveProvinces !== opts.deriveProvinces) {
173
+ profile.deriveProvinces = opts.deriveProvinces;
174
+ invalidate = true;
175
+ }
176
+ const origin = typeof opts.origin === "string" ? opts.origin.trim() : "";
177
+ if (!W() && origin) {
178
+ let cols = [];
179
+ try {
180
+ cols = profile.getColumnsWithStats(opts.privacyLevel, opts.locale);
181
+ } catch {
182
+ }
183
+ if (needsAdmin1Lookup(cols)) {
184
+ const ok = await loadAdmin1Lookup(
185
+ origin.replace(/\/+$/, "") + ADMIN1_LOOKUP_PATH,
186
+ { timeoutMs: opts.timeoutMs ?? 3e3, fetchJson: opts.fetchJson, log: opts.log }
187
+ );
188
+ invalidate = invalidate || ok;
189
+ }
190
+ }
191
+ if (invalidate) profile.invalidateStats?.();
192
+ }
193
+ function provinceBinding(res, cols, opts) {
194
+ if (!res.geoColumn || !res.geoKind) return null;
195
+ if (String(res.geoGrain ?? "").toLowerCase() !== "admin1") return { column: res.geoColumn, kind: res.geoKind };
196
+ const dims = cols.filter((c) => !c.isMeasure && c.name !== res.geoColumn);
197
+ const kindOf = (c) => String(c.geoKind ?? "");
198
+ const words = (n) => n.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z]+/);
199
+ const named = (re) => cols.find((c) => words(c.name).some((w) => re.test(w)))?.name ?? null;
200
+ return {
201
+ column: res.geoColumn,
202
+ kind: res.geoKind,
203
+ grain: "admin1",
204
+ region: res.geoRegion || null,
205
+ fallback: {
206
+ city: dims.find((c) => kindOf(c).startsWith("city"))?.name ?? null,
207
+ country: dims.find((c) => kindOf(c).startsWith("country"))?.name ?? null,
208
+ lat: named(/^(lat|latitude)$/),
209
+ lon: named(/^(lon|lng|long|longitude)$/)
210
+ },
211
+ deriveProvinces: opts.deriveProvinces,
212
+ join: Si,
213
+ locate: opts.geo ? provinceLocator(opts.geo) : null
214
+ };
215
+ }
216
+ function geoKeyOfFields(res) {
217
+ return geoKeyFor({ geoKind: res.geoKind, geoRegion: res.geoRegion, geoGrain: res.geoGrain });
218
+ }
219
+ export {
220
+ ADMIN1_LOOKUP_PATH,
221
+ geoKeyFor,
222
+ geoKeyOfFields,
223
+ loadAdmin1Lookup,
224
+ loadProvinceGeo,
225
+ needsAdmin1Lookup,
226
+ prepareProvinceProfile,
227
+ provinceBinding,
228
+ provinceLocator
229
+ };
package/dist/react.mjs CHANGED
@@ -18,16 +18,18 @@ import {
18
18
  assembleD3,
19
19
  createChartGroup,
20
20
  createChartHost,
21
- geoFromCache,
22
- loadGeo,
23
21
  syncMemberSelection,
24
22
  toSourceRows
25
- } from "./chunk-4BR6VJQX.mjs";
23
+ } from "./chunk-K6UXTDMR.mjs";
26
24
  import "./chunk-IE426WU6.mjs";
27
25
  import "./chunk-CPLWAW3Z.mjs";
28
26
  import "./chunk-P2DLK7WZ.mjs";
29
27
  import "./chunk-IE7TM6DX.mjs";
30
- import "./chunk-CNT3QA3R.mjs";
28
+ import {
29
+ geoFromCache,
30
+ loadGeo
31
+ } from "./chunk-5WTQN5DL.mjs";
32
+ import "./chunk-SYUJ4TPX.mjs";
31
33
  import "./chunk-A2GMXZP7.mjs";
32
34
  import "./chunk-7XRQUFVH.mjs";
33
35
  import "./chunk-GHODWOAL.mjs";
@@ -0,0 +1,26 @@
1
+ /** Where the lookup is served, relative to the site origin. Versioned in the file name, so a rebuilt lookup is a new
2
+ * URL rather than a stale cache entry. */
3
+ export declare const ADMIN1_LOOKUP_PATH = "/geo/admin1-lookup.v1.json";
4
+ /**
5
+ * Fetch and register the lookup. Resolves true once it is registered (immediately when it already is), false on a
6
+ * timeout, a failed fetch or a body that is not a lookup - and never throws. One fetch is in flight at a time, so N
7
+ * charts on a page cause one request. `timeoutMs` bounds how long a host waits before profiling without it (default
8
+ * 3000); a late arrival still registers, for the next profile.
9
+ */
10
+ export declare function loadAdmin1Lookup(url: string, opts?: {
11
+ timeoutMs?: number;
12
+ fetchJson?: (url: string) => Promise<any>;
13
+ log?: (tag: string, data?: any) => void;
14
+ }): Promise<boolean>;
15
+ /**
16
+ * Is the lookup worth loading for a table, judged from its profile? True when a column was profiled as a country or a
17
+ * city, or when a dimension's name reads as a city or a first-level division. Takes plain column records, so a host
18
+ * passes its profile as is.
19
+ */
20
+ export declare function needsAdmin1Lookup(columns: ReadonlyArray<{
21
+ name?: string | null;
22
+ geoKind?: string | null;
23
+ isMeasure?: boolean | null;
24
+ }> | null | undefined): boolean;
25
+ /** Forget an in-flight load. Tests only. */
26
+ export declare function resetAdmin1LookupLoaderForTests(): void;
@@ -157,6 +157,7 @@ export interface RenderOptions {
157
157
  colorScaleSelfClampPct?: number;
158
158
  geoLandColor?: string;
159
159
  geoNoDataColor?: string;
160
+ geoBorders?: boolean;
160
161
  aggregation?: string;
161
162
  uiState?: unknown;
162
163
  setUiState?: (s: unknown) => void;
@@ -7,7 +7,7 @@ export { loadGeo, geoFromCache, registerGeo, registerGeoAsset, geoAssetFor, clea
7
7
  /** REPLACE the bundled city gazetteer with another packed table. Rarely needed — see the
8
8
  * note above registerCityTable in shape-core's geoPoint.ts before using it. */
9
9
  export declare function registerCityTable(packed: string): void;
10
- export { buildRenderPayload, type RenderPayload, type GeoPointBinding } from "./payload";
10
+ export { buildRenderPayload, type RenderPayload, type GeoPointBinding, type GeoBinding, type GeoProvinceReport, type ProvinceJoin } from "./payload";
11
11
  export { codeReadsDatesInLocalTime, vegaSpecReadsDatesInLocalTime } from "./dateCells";
12
12
  export { createChartHost, compileRenderFn, stripEsmExports, requiredD3Plugins, explainRenderFailure, sessionViewStateProvider, noopViewStateProvider, assembleD3, D3_PLUGIN_PACKAGES, type ChartHost, type ChartHostConfig, type RenderFn, type SelectionPaintReport } from "./host";
13
13
  export { ANNOTATION_CLASS, ANNOTATION_LAYER_CLASS, type MarkAnnotation, type AnnotationReport } from "./annotations";
@@ -71,6 +71,11 @@ export declare const OPTIONS_VOCABULARY: Readonly<{
71
71
  * that made the chart asked for them. "on" / "off" is a reader's choice and wins either way.
72
72
  */
73
73
  hoverDropLines: OptionVocabularyEntry;
74
+ /** A province map draws the national borders over its provinces unless this is false (a reader's "Country borders"). */
75
+ geoBorders: OptionVocabularyEntry;
76
+ /** A reader's "Derive provinces": false measures no city or coordinate rollup, so only a real province column offers
77
+ * a province map. It changes what the profile measures, so it travels in the shape rather than as a field. */
78
+ deriveProvinces: OptionVocabularyEntry;
74
79
  levelOfDetail: OptionVocabularyEntry;
75
80
  favorLimitTo: OptionVocabularyEntry;
76
81
  /** 0 asks for no limit, and travels as absent (`wireLimitCount`): any value the server receives is a limit. */
@@ -9,6 +9,7 @@ export interface GeoPointBinding {
9
9
  lon?: string;
10
10
  country?: string;
11
11
  mapKind?: GeoMapKind;
12
+ region?: string | null;
12
13
  }
13
14
  /** What a point binding's resolution ACHIEVED, so the chart can annotate honestly rather
14
15
  * than implying a precision it does not have. Named (rather than inlined on RenderPayload)
@@ -25,9 +26,70 @@ export interface GeoPointReport {
25
26
  rolesBackfilled?: string[];
26
27
  rolesRefused?: string[];
27
28
  }
29
+ /** How a province map's rows were placed (geo binding with grain "admin1"), so the chart can say what its provinces
30
+ * rest on: the reader's province values, their coordinates, or their cities rolled up. */
31
+ export interface GeoProvinceReport {
32
+ /** Rows placed by each source. */
33
+ sources: {
34
+ province: number;
35
+ coordinates: number;
36
+ city: number;
37
+ };
38
+ /** Rows whose city matched more than one province: not placed, never guessed. */
39
+ ambiguousRows: number;
40
+ ambiguousExamples: string[];
41
+ /** Rows whose coordinates landed in no province. */
42
+ unplacedPoints: number;
43
+ }
44
+ /** The province join a host hands a province-map binding: shape-core's buildProvinceIsoColumn. Typed here, not imported,
45
+ * because the province code lives off this package's main entry (its eager budget): a host already loads shape-core to
46
+ * profile, so it passes the function it has rather than this bundle carrying a second copy. */
47
+ export type ProvinceJoin = (rows: Array<{
48
+ province?: unknown;
49
+ city?: unknown;
50
+ country?: unknown;
51
+ lat?: unknown;
52
+ lon?: unknown;
53
+ }>, opts: {
54
+ provinceKind?: string | null;
55
+ deriveProvinces?: boolean;
56
+ locate?: (lat: number, lon: number) => string | null;
57
+ }) => {
58
+ iso: (string | null)[];
59
+ source: ("province" | "coordinates" | "city" | null)[];
60
+ unmatched: string[];
61
+ matchedRows: number;
62
+ totalRows: number;
63
+ ambiguousRows: number;
64
+ ambiguousExamples: string[];
65
+ unplacedPoints: number;
66
+ };
67
+ /** A geo binding. A province map adds `grain: "admin1"`, its continent and `join`; the province value, a blank province's
68
+ * coordinates (through `locate`, from "@bicharts/chart-host/provinces" provinceLocator) and its city are then read in that
69
+ * order (the city and coordinate columns may be named in `fallback` beside the bound column), unless the reader turned
70
+ * deriving provinces off. A province binding without `join` builds no join column. */
71
+ export interface GeoBinding {
72
+ column: string;
73
+ kind: string;
74
+ grain?: string | null;
75
+ region?: string | null;
76
+ fallback?: {
77
+ city?: string | null;
78
+ country?: string | null;
79
+ lat?: string | null;
80
+ lon?: string | null;
81
+ } | null;
82
+ deriveProvinces?: boolean;
83
+ /** Places a coordinate in its province (provinceLocator over the continent's geometry). Absent: coordinates do not place. */
84
+ locate?: ((lat: number, lon: number) => string | null) | null;
85
+ /** Builds the province join (shape-core buildProvinceIsoColumn). Required for grain "admin1". */
86
+ join?: ProvinceJoin | null;
87
+ }
28
88
  export interface RenderPayload {
29
89
  columns: any[];
30
90
  rows: any[][];
91
+ /** Present only for a province map: what placed its rows. */
92
+ geoProvince?: GeoProvinceReport;
31
93
  geoUnmatched?: {
32
94
  count: number;
33
95
  examples: string[];
@@ -77,10 +139,7 @@ export interface RenderPayload {
77
139
  }
78
140
  export declare function buildRenderPayload(cols: Array<{
79
141
  name: string;
80
- } & Record<string, any>>, rowObjs: Array<Record<string, any>>, geo?: {
81
- column: string;
82
- kind: string;
83
- } | null, point?: GeoPointBinding | null, destination?: GeoPointBinding | null, opts?: {
142
+ } & Record<string, any>>, rowObjs: Array<Record<string, any>>, geo?: GeoBinding | null, point?: GeoPointBinding | null, destination?: GeoPointBinding | null, opts?: {
84
143
  utcDays?: boolean;
85
144
  localWallClock?: boolean;
86
145
  aliases?: ReadonlyArray<{
@@ -0,0 +1,38 @@
1
+ /** The continents a province map is drawn for. */
2
+ export declare const PROVINCE_REGIONS: readonly ["europe", "asia", "africa", "south-america", "north-america", "oceania"];
3
+ export type ProvinceRegion = typeof PROVINCE_REGIONS[number];
4
+ /** Where a continent's provinces are served, relative to the site origin. Versioned in the name, like the lookup. */
5
+ export declare function provinceGeoPath(region: string): string;
6
+ /** The geometry key a province map renders against: pass it as the host's geoKind. */
7
+ export declare function provinceGeoKey(region: string): string;
8
+ /**
9
+ * The geometry key for a generation's geo fields: a province map (grain "admin1") renders against its continent's
10
+ * provinces, every other geo chart against its geoKind as before.
11
+ */
12
+ export declare function geoKeyFor(res: {
13
+ geoKind?: string | null;
14
+ geoRegion?: string | null;
15
+ geoGrain?: string | null;
16
+ }): string | null;
17
+ /**
18
+ * Fetch and register a continent's provinces. Resolves the FeatureCollection once registered (immediately when it
19
+ * already is), undefined on a failed fetch or a body that is not one - and never throws. One fetch per continent is in
20
+ * flight at a time. The file is fetched from `origin` (default: the page's own) at its served path, or from `url`.
21
+ */
22
+ export declare function loadProvinceGeo(region: string, opts?: {
23
+ origin?: string;
24
+ url?: string;
25
+ fetchJson?: (url: string) => Promise<any>;
26
+ log?: (tag: string, data?: any) => void;
27
+ }): Promise<any | undefined>;
28
+ /** How far outside every province a point may sit and still be placed in the nearest one, in degrees (about 5 km at
29
+ * the equator, less toward the poles): simplification moves coastlines, and a harbour city must not fall in the sea. */
30
+ export declare const PROVINCE_SNAP_DEG = 0.05;
31
+ /**
32
+ * A function placing a coordinate in its province: the province that contains it, else the nearest one within
33
+ * PROVINCE_SNAP_DEG (east-west distance scaled to the latitude), else null. Border lines are never a province. Built once
34
+ * per geometry object and reused.
35
+ */
36
+ export declare function provinceLocator(geo: any, snapDeg?: number): (lat: number, lon: number) => string | null;
37
+ /** Forget in-flight loads. Tests only. */
38
+ export declare function resetProvinceGeoForTests(): void;
@@ -0,0 +1,47 @@
1
+ import type { GeoBinding } from "./payload";
2
+ /** The profile a host measures with: shape-core's IndexedText, by the members this needs. */
3
+ export interface ProvinceProfile {
4
+ deriveProvinces?: boolean;
5
+ getColumnsWithStats(privacyLevel: string, locale?: string): ReadonlyArray<{
6
+ name?: string | null;
7
+ geoKind?: string | null;
8
+ isMeasure?: boolean | null;
9
+ }>;
10
+ invalidateStats?(): void;
11
+ }
12
+ /**
13
+ * Before a request is profiled: hand the profile the reader's Derive Provinces setting, and when the table could carry
14
+ * provinces, wait (up to `timeoutMs`, default 3000) for the hosted admin-1 lookup served at `origin`. Either change
15
+ * re-measures the profile, which keeps its rows. Never throws; a late or failed load costs a province map, never a chart.
16
+ */
17
+ export declare function prepareProvinceProfile(profile: ProvinceProfile | null | undefined, opts: {
18
+ origin: string;
19
+ deriveProvinces: boolean;
20
+ privacyLevel: string;
21
+ locale?: string;
22
+ timeoutMs?: number;
23
+ fetchJson?: (url: string) => Promise<any>;
24
+ log?: (tag: string, data?: any) => void;
25
+ }): Promise<void>;
26
+ /** The geo fields a generation names, as every host keeps them. */
27
+ export interface GeoFields {
28
+ geoColumn?: string | null;
29
+ geoKind?: string | null;
30
+ geoRegion?: string | null;
31
+ geoGrain?: string | null;
32
+ }
33
+ /**
34
+ * The geo binding for a render. A province map (grain "admin1") gets the province join, a locator over `geo` (the
35
+ * continent's province geometry, when the host has it), the reader's Derive Provinces setting and the columns a blank
36
+ * province falls back to, named from the profile; every other geo chart its column + kind. Null when nothing is bound.
37
+ */
38
+ export declare function provinceBinding(res: GeoFields, cols: ReadonlyArray<{
39
+ name: string;
40
+ geoKind?: string | null;
41
+ isMeasure?: boolean | null;
42
+ }>, opts: {
43
+ deriveProvinces: boolean;
44
+ geo?: any;
45
+ }): GeoBinding | null;
46
+ /** The geometry key a generation renders against (geoKeyFor over a host's kept fields). */
47
+ export declare function geoKeyOfFields(res: GeoFields): string | null;
@@ -0,0 +1,3 @@
1
+ export { loadProvinceGeo, geoKeyFor, provinceLocator } from "./provinceGeo";
2
+ export { loadAdmin1Lookup, needsAdmin1Lookup, ADMIN1_LOOKUP_PATH } from "./admin1LookupLoader";
3
+ export { prepareProvinceProfile, provinceBinding, geoKeyOfFields, type ProvinceProfile, type GeoFields } from "./provinceHost";
@@ -1,7 +1,7 @@
1
1
  /*! @bicharts/chart-host — Apache-2.0. Bundled reference data: GeoNames (https://www.geonames.org/) CC BY 4.0; Natural Earth and US Census/TIGER (public domain). Full text: NOTICE in this package. */
2
2
  import {
3
3
  K
4
- } from "./chunk-CNT3QA3R.mjs";
4
+ } from "./chunk-SYUJ4TPX.mjs";
5
5
  import "./chunk-GHODWOAL.mjs";
6
6
 
7
7
  // src/zoomMemory/zoomMemory.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bicharts/chart-host",
3
- "version": "0.6.103",
3
+ "version": "0.6.106",
4
4
  "description": "Run a BIC-generated D3 chart in any web host: compiles the generated render() function, applies the shared option defaults, resolves mark clicks (through tooltip overlays), owns the selection affordance, and translates row indices between cross-filtered charts. The same contract the BIC Power BI visual implements, minus Power BI. React bindings at @bicharts/chart-host/react.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -64,6 +64,11 @@
64
64
  "import": "./dist/legend-fit.mjs",
65
65
  "default": "./dist/legend-fit.mjs"
66
66
  },
67
+ "./provinces": {
68
+ "types": "./dist/types/provinces.d.ts",
69
+ "import": "./dist/provinces.mjs",
70
+ "default": "./dist/provinces.mjs"
71
+ },
67
72
  "./zoom-memory": {
68
73
  "types": "./dist/types/zoomMemory/index.d.ts",
69
74
  "import": "./dist/zoom-memory.mjs",