structured-context-map-views 0.1.0 → 0.2.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.
Files changed (32) hide show
  1. package/dist/{definition-KuPoA1mO.js → definition-Cft28YUA.js} +41 -7
  2. package/dist/definition-Cft28YUA.js.map +1 -0
  3. package/dist/index.js +385 -229
  4. package/dist/index.js.map +1 -1
  5. package/dist/node.js +2 -2
  6. package/dist/node.js.map +1 -1
  7. package/dist/styles.css +31 -0
  8. package/dist/types/core/annotations.d.ts +47 -0
  9. package/dist/types/core/definition.d.ts +6 -1
  10. package/dist/types/core/{schemas/definition.d.ts → format/definition-schema.d.ts} +18 -0
  11. package/dist/types/core/{schemas → format}/index.d.ts +33 -1
  12. package/dist/types/core/{schemas → format}/settings.d.ts +14 -0
  13. package/dist/types/index.d.ts +4 -1
  14. package/dist/types/layouts/EdgeOverlay.d.ts +1 -1
  15. package/dist/types/layouts/LandscapeLayout.d.ts +1 -1
  16. package/dist/types/layouts/MapView.d.ts +8 -2
  17. package/dist/types/layouts/NodeCard.d.ts +1 -1
  18. package/dist/types/layouts/ParameterControls.d.ts +3 -3
  19. package/dist/types/layouts/ParameterInputs.d.ts +21 -0
  20. package/dist/types/layouts/TreeLayout.d.ts +1 -1
  21. package/dist/types/layouts/XYLayout.d.ts +1 -1
  22. package/dist/types/layouts/annotations-context.d.ts +4 -0
  23. package/dist/types/layouts/useParameterControls.d.ts +13 -0
  24. package/dist/types/node/index.d.ts +1 -1
  25. package/package.json +3 -3
  26. package/dist/definition-KuPoA1mO.js.map +0 -1
  27. package/dist/types/core/index.d.ts +0 -6
  28. /package/dist/types/core/{schemas → format}/landscape-layout.d.ts +0 -0
  29. /package/dist/types/core/{schemas → format}/param-ref.d.ts +0 -0
  30. /package/dist/types/core/{schemas → format}/schema-url.d.ts +0 -0
  31. /package/dist/types/core/{schemas → format}/tree-layout.d.ts +0 -0
  32. /package/dist/types/core/{schemas → format}/xy-layout.d.ts +0 -0
package/dist/index.js CHANGED
@@ -1,6 +1,70 @@
1
- import { a as resolveRegions, i as resolveParameterValues, n as mapViewsForSchema, o as selectionFilter, r as parseMapViewDefinition, s as MAP_VIEW_SCHEMAS, t as MapViewDefinitionError } from "./definition-KuPoA1mO.js";
2
- import { useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import { a as resolveRegions, c as MAP_VIEW_SCHEMAS, i as resolveParameterValues, n as mapViewsForSchema, o as selectionFilter, r as parseMapViewDefinition, s as viewValidationLevel, t as MapViewDefinitionError } from "./definition-Cft28YUA.js";
2
+ import { createContext, useCallback, useContext, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/core/annotations.ts
5
+ /**
6
+ * Attach validation results to the nodes of `graph`, by node id. Results that can't be attached are
7
+ * returned in `unattached` for the host to surface: global-scope rules (no node), and nodes that
8
+ * aren't in the graph, which includes every node that failed its schema.
9
+ */
10
+ function annotationsFrom(input, graph) {
11
+ const annotations = /* @__PURE__ */ new Map();
12
+ const unattached = [];
13
+ const add = (nodeId, annotation, location) => {
14
+ if (nodeId !== void 0 && graph.nodes.has(nodeId)) annotations.set(nodeId, [...annotations.get(nodeId) ?? [], annotation]);
15
+ else unattached.push({
16
+ ...annotation,
17
+ nodeId,
18
+ ...location && { location }
19
+ });
20
+ };
21
+ for (const { nodeId, source, issues } of input.nodeErrors ?? []) for (const issue of issues) add(nodeId, {
22
+ severity: "error",
23
+ source: "schema",
24
+ message: issue.message
25
+ }, source);
26
+ for (const { nodeId, description } of input.hierarchyViolations ?? []) add(nodeId, {
27
+ severity: "error",
28
+ source: "hierarchy",
29
+ message: description
30
+ });
31
+ for (const { nodeId, description, severity, ruleId } of input.ruleViolations ?? []) add(nodeId, {
32
+ severity,
33
+ source: "rule",
34
+ message: description,
35
+ ruleId
36
+ });
37
+ return {
38
+ annotations,
39
+ unattached
40
+ };
41
+ }
42
+ var RANK = {
43
+ error: 0,
44
+ warning: 1,
45
+ info: 2
46
+ };
47
+ /** The most serious severity among annotations, or undefined if there are none. */
48
+ function worstSeverity(annotations) {
49
+ return annotations.reduce((worst, { severity }) => worst === void 0 || RANK[severity] < RANK[worst] ? severity : worst, void 0);
50
+ }
51
+ /**
52
+ * The annotations a map view shows: those at `level` or more serious, from `sources`. `off` shows none.
53
+ * Nodes left with no annotations are dropped from the index.
54
+ */
55
+ function filterAnnotations(index, level, sources) {
56
+ if (level === "off") return /* @__PURE__ */ new Map();
57
+ const filtered = /* @__PURE__ */ new Map();
58
+ for (const [id, annotations] of index) {
59
+ const kept = annotations.filter((a) => RANK[a.severity] <= RANK[level] && sources.includes(a.source));
60
+ if (kept.length > 0) filtered.set(id, kept);
61
+ }
62
+ return filtered;
63
+ }
64
+ /** The annotations a map view shows, already narrowed by its validation settings. Read by node cards. */
65
+ var AnnotationsContext = createContext(/* @__PURE__ */ new Map());
66
+ var useAnnotations = () => useContext(AnnotationsContext);
67
+ //#endregion
4
68
  //#region src/core/graph.ts
5
69
  var NONE = [];
6
70
  /**
@@ -163,165 +227,6 @@ function hasValue(node, { field, value }) {
163
227
  return valuesOf(node, field).includes(value);
164
228
  }
165
229
  //#endregion
166
- //#region src/core/tree.ts
167
- /**
168
- * Lay a space out as a tree over its hierarchy.
169
- *
170
- * Roots are root-type nodes with no hierarchy parent. Traversal is depth-first in space order,
171
- * so a node's primary occurrence is under the parent reached first. Under its other parents a
172
- * node is either repeated without its subtree (`repeat`), or left out and linked by a cross-edge
173
- * (`cross-edge`). Either way traversal stops there, which also keeps cycles from recursing.
174
- */
175
- function buildTree(graph, regionId, dag = "repeat") {
176
- const { hierarchy } = graph;
177
- const levelTypes = new Set(hierarchy?.levels.map((level) => level.type));
178
- const rootType = hierarchy?.levels[0]?.type;
179
- const primary = /* @__PURE__ */ new Map();
180
- const pendingCross = [];
181
- const hierarchyParents = (title) => parentEdges(graph, title).filter((edge) => isHierarchyEdge(edge, hierarchy));
182
- const visit = (node, path) => {
183
- const nodePath = [...path, node.id];
184
- const key = occurrenceKey(regionId, nodePath);
185
- primary.set(node.id, key);
186
- const childTitles = new Set(childEdges(graph, node.id).filter((edge) => isHierarchyEdge(edge, hierarchy)).map((edge) => edge.child));
187
- const children = [];
188
- for (const title of childTitles) {
189
- const child = graph.nodes.get(title);
190
- const repeatOf = primary.get(title);
191
- if (repeatOf === void 0) children.push(visit(child, nodePath));
192
- else if (dag === "repeat") children.push({
193
- key: occurrenceKey(regionId, [...nodePath, title]),
194
- node: child,
195
- children: [],
196
- repeatOf
197
- });
198
- else pendingCross.push({
199
- from: key,
200
- child: title
201
- });
202
- }
203
- return {
204
- key,
205
- node,
206
- children
207
- };
208
- };
209
- const levelNodes = [...graph.nodes.values()].filter((node) => levelTypes.has(node.resolvedType));
210
- const unparented = levelNodes.filter((node) => hierarchyParents(node.id).length === 0);
211
- const roots = unparented.filter((node) => node.resolvedType === rootType).map((node) => visit(node, []));
212
- const orphans = unparented.filter((node) => node.resolvedType !== rootType).map((node) => visit(node, []));
213
- for (const node of levelNodes) if (!primary.has(node.id)) orphans.push(visit(node, []));
214
- return {
215
- roots,
216
- orphans,
217
- primary,
218
- crossLinks: pendingCross.map(({ from, child }) => ({
219
- from,
220
- to: primary.get(child),
221
- kind: "cross"
222
- }))
223
- };
224
- }
225
- /** Parent → child pairs of occurrence keys: one per rendered parent-child link, then cross-links. */
226
- function treeLinks(tree) {
227
- const links = [];
228
- const walk = (occurrence) => {
229
- for (const child of occurrence.children) {
230
- links.push({
231
- from: occurrence.key,
232
- to: child.key,
233
- kind: "tree"
234
- });
235
- walk(child);
236
- }
237
- };
238
- for (const root of [...tree.roots, ...tree.orphans]) walk(root);
239
- return [...links, ...tree.crossLinks];
240
- }
241
- //#endregion
242
- //#region src/core/xy.ts
243
- /**
244
- * Position nodes by two numeric fields. Every node of a plotted type appears once: on the plot
245
- * when both its values are numbers inside the axes' domains, otherwise in `unplaced` with the
246
- * reason. Positions are normalised; layouts decide the plot's size.
247
- */
248
- function buildXYPlot(graph, regionId, layout) {
249
- const { x, y } = layout;
250
- const nodes = [...graph.nodes.values()];
251
- const types = new Set(layout.types.length > 0 ? layout.types : nodes.filter((n) => isNumber(n.schemaData[x.field]) || isNumber(n.schemaData[y.field])).map((n) => n.resolvedType));
252
- const plotted = nodes.filter((n) => types.has(n.resolvedType));
253
- const domains = {
254
- x: domainOf(x, plotted),
255
- y: domainOf(y, plotted)
256
- };
257
- const points = [];
258
- const unplaced = [];
259
- for (const node of plotted) {
260
- const key = occurrenceKey(regionId, [node.id]);
261
- const px = position(node, x, domains.x);
262
- const py = position(node, y, domains.y);
263
- if (typeof px === "number" && typeof py === "number") points.push({
264
- key,
265
- node,
266
- x: px,
267
- y: py
268
- });
269
- else unplaced.push({
270
- key,
271
- node,
272
- reasons: [px, py].filter((r) => typeof r === "string")
273
- });
274
- }
275
- const keys = new Map(points.map((p) => [p.node.id, p.key]));
276
- const fields = new Set(layout.edgeFields);
277
- const links = edgesWithin(graph).filter((e) => keys.has(e.parent) && keys.has(e.child) && (fields.size === 0 || fields.has(e.field))).map((e) => ({
278
- from: keys.get(e.parent),
279
- to: keys.get(e.child),
280
- field: e.field,
281
- hierarchy: isHierarchyEdge(e, graph.hierarchy),
282
- lane: 0
283
- }));
284
- assignLanes(links);
285
- return {
286
- points,
287
- unplaced,
288
- links,
289
- domains
290
- };
291
- }
292
- function isNumber(value) {
293
- return typeof value === "number" && Number.isFinite(value);
294
- }
295
- /** The axis's fixed domain, or the range of plotted values (widened when they're all equal). */
296
- function domainOf(axis, nodes) {
297
- if (Array.isArray(axis.domain)) return [axis.domain[0], axis.domain[1]];
298
- const values = nodes.map((n) => n.schemaData[axis.field]).filter(isNumber);
299
- if (values.length === 0) return [0, 1];
300
- const min = Math.min(...values);
301
- const max = Math.max(...values);
302
- return min === max ? [min - 1, max + 1] : [min, max];
303
- }
304
- /** The node's normalised position on the axis, or why it has none. */
305
- function position(node, axis, [min, max]) {
306
- const value = node.schemaData[axis.field];
307
- if (value === void 0 || value === null) return `no ${axis.field}`;
308
- if (!isNumber(value)) return `${axis.field} is not a number`;
309
- if (value < min || value > max) return `${axis.field} ${value} is outside ${min}–${max}`;
310
- const t = (value - min) / (max - min);
311
- return axis.direction === "descending" ? 1 - t : t;
312
- }
313
- /** Spread links joining the same pair of nodes (in either direction) into lanes around 0. */
314
- function assignLanes(links) {
315
- const byPair = /* @__PURE__ */ new Map();
316
- for (const link of links) {
317
- const pair = [link.from, link.to].sort().join("\n");
318
- byPair.set(pair, [...byPair.get(pair) ?? [], link]);
319
- }
320
- for (const group of byPair.values()) group.forEach((link, i) => {
321
- link.lane = i - (group.length - 1) / 2;
322
- });
323
- }
324
- //#endregion
325
230
  //#region src/layouts/NodeCard.tsx
326
231
  /**
327
232
  * A clickable node, drawn as a card (title plus selected fields) or a compact shape with a
@@ -330,7 +235,8 @@ function assignLanes(links) {
330
235
  */
331
236
  function NodeCard({ node, repeat, nodes, onSelect }) {
332
237
  const select = () => onSelect?.(node);
333
- const label = `${node.resolvedType}: ${node.title}${repeat ? " (shown in full elsewhere)" : ""}`;
238
+ const annotations = useAnnotations().get(node.id);
239
+ const label = `${node.resolvedType}: ${node.title}${repeat ? " (shown in full elsewhere)" : ""}${annotationsLabel(annotations)}`;
334
240
  const onKeyDown = (e) => {
335
241
  if (e.key === "Enter" || e.key === " ") {
336
242
  e.preventDefault();
@@ -338,18 +244,18 @@ function NodeCard({ node, repeat, nodes, onSelect }) {
338
244
  }
339
245
  };
340
246
  const border = repeat ? "mv:border-dashed mv:border-muted/60 mv:opacity-75" : "mv:border-edge";
341
- if (nodes.style === "shape") return /* @__PURE__ */ jsx("div", {
247
+ if (nodes.style === "shape") return /* @__PURE__ */ jsxs("div", {
342
248
  role: "button",
343
249
  tabIndex: 0,
344
250
  title: label,
345
251
  onClick: select,
346
252
  onKeyDown,
347
253
  "aria-label": label,
348
- className: `mv:flex mv:size-6 mv:cursor-pointer mv:items-center mv:justify-center mv:rounded-full mv:border-2 mv:bg-card mv:text-[0.65rem] mv:font-semibold mv:uppercase mv:text-muted mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
349
- children: /* @__PURE__ */ jsx("span", {
254
+ className: `mv:relative mv:flex mv:size-6 mv:cursor-pointer mv:items-center mv:justify-center mv:rounded-full mv:border-2 mv:bg-card mv:text-[0.65rem] mv:font-semibold mv:uppercase mv:text-muted mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
255
+ children: [/* @__PURE__ */ jsx("span", {
350
256
  "aria-hidden": "true",
351
257
  children: node.resolvedType.charAt(0)
352
- })
258
+ }), /* @__PURE__ */ jsx(AnnotationBadge, { annotations })]
353
259
  });
354
260
  const fields = nodes.fields.map((field) => [field, formatValue(node.schemaData[field])]).filter(([, value]) => value !== void 0);
355
261
  return /* @__PURE__ */ jsxs("div", {
@@ -358,7 +264,7 @@ function NodeCard({ node, repeat, nodes, onSelect }) {
358
264
  title: label,
359
265
  onClick: select,
360
266
  onKeyDown,
361
- className: `mv:w-44 mv:cursor-pointer mv:rounded-md mv:border mv:bg-card mv:px-2.5 mv:py-1.5 mv:text-left mv:shadow-sm mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
267
+ className: `mv:relative mv:w-44 mv:cursor-pointer mv:rounded-md mv:border mv:bg-card mv:px-2.5 mv:py-1.5 mv:text-left mv:shadow-sm mv:hover:border-accent mv:focus-visible:outline-2 mv:focus-visible:outline-accent ${border}`,
362
268
  children: [
363
269
  /* @__PURE__ */ jsxs("span", {
364
270
  className: "mv:block mv:text-[0.65rem] mv:uppercase mv:tracking-wide mv:text-muted",
@@ -380,10 +286,31 @@ function NodeCard({ node, repeat, nodes, onSelect }) {
380
286
  children: value
381
287
  })]
382
288
  }, field))
383
- })
289
+ }),
290
+ /* @__PURE__ */ jsx(AnnotationBadge, { annotations })
384
291
  ]
385
292
  });
386
293
  }
294
+ var BADGE = {
295
+ error: "mv:bg-error",
296
+ warning: "mv:bg-warning",
297
+ info: "mv:bg-info"
298
+ };
299
+ /** A count of the node's validation results in the corner, coloured by the most serious. Decorative: the count is in the card's label. */
300
+ function AnnotationBadge({ annotations }) {
301
+ const worst = annotations && worstSeverity(annotations);
302
+ if (!annotations || !worst) return null;
303
+ return /* @__PURE__ */ jsx("span", {
304
+ "aria-hidden": "true",
305
+ className: `mv:absolute mv:-top-1.5 mv:-right-1.5 mv:flex mv:h-4 mv:min-w-4 mv:items-center mv:justify-center mv:rounded-full mv:px-1 mv:text-[0.6rem] mv:font-semibold mv:text-white ${BADGE[worst]}`,
306
+ children: annotations.length
307
+ });
308
+ }
309
+ function annotationsLabel(annotations) {
310
+ const worst = annotations && worstSeverity(annotations);
311
+ if (!annotations || !worst) return "";
312
+ return ` (${annotations.length} validation ${annotations.length === 1 ? "result" : "results"}, most serious: ${worst})`;
313
+ }
387
314
  function formatValue(value) {
388
315
  if (value === void 0 || value === null || value === "") return void 0;
389
316
  if (Array.isArray(value)) return value.map(String).join(", ");
@@ -513,6 +440,82 @@ function displayValue(value) {
513
440
  return value.replace(/^\[\[([^\]|]+)(\|[^\]]*)?\]\]$/, "$1");
514
441
  }
515
442
  //#endregion
443
+ //#region src/core/tree.ts
444
+ /**
445
+ * Lay a space out as a tree over its hierarchy.
446
+ *
447
+ * Roots are root-type nodes with no hierarchy parent. Traversal is depth-first in space order,
448
+ * so a node's primary occurrence is under the parent reached first. Under its other parents a
449
+ * node is either repeated without its subtree (`repeat`), or left out and linked by a cross-edge
450
+ * (`cross-edge`). Either way traversal stops there, which also keeps cycles from recursing.
451
+ */
452
+ function buildTree(graph, regionId, dag = "repeat") {
453
+ const { hierarchy } = graph;
454
+ const levelTypes = new Set(hierarchy?.levels.map((level) => level.type));
455
+ const rootType = hierarchy?.levels[0]?.type;
456
+ const primary = /* @__PURE__ */ new Map();
457
+ const pendingCross = [];
458
+ const hierarchyParents = (title) => parentEdges(graph, title).filter((edge) => isHierarchyEdge(edge, hierarchy));
459
+ const visit = (node, path) => {
460
+ const nodePath = [...path, node.id];
461
+ const key = occurrenceKey(regionId, nodePath);
462
+ primary.set(node.id, key);
463
+ const childTitles = new Set(childEdges(graph, node.id).filter((edge) => isHierarchyEdge(edge, hierarchy)).map((edge) => edge.child));
464
+ const children = [];
465
+ for (const title of childTitles) {
466
+ const child = graph.nodes.get(title);
467
+ const repeatOf = primary.get(title);
468
+ if (repeatOf === void 0) children.push(visit(child, nodePath));
469
+ else if (dag === "repeat") children.push({
470
+ key: occurrenceKey(regionId, [...nodePath, title]),
471
+ node: child,
472
+ children: [],
473
+ repeatOf
474
+ });
475
+ else pendingCross.push({
476
+ from: key,
477
+ child: title
478
+ });
479
+ }
480
+ return {
481
+ key,
482
+ node,
483
+ children
484
+ };
485
+ };
486
+ const levelNodes = [...graph.nodes.values()].filter((node) => levelTypes.has(node.resolvedType));
487
+ const unparented = levelNodes.filter((node) => hierarchyParents(node.id).length === 0);
488
+ const roots = unparented.filter((node) => node.resolvedType === rootType).map((node) => visit(node, []));
489
+ const orphans = unparented.filter((node) => node.resolvedType !== rootType).map((node) => visit(node, []));
490
+ for (const node of levelNodes) if (!primary.has(node.id)) orphans.push(visit(node, []));
491
+ return {
492
+ roots,
493
+ orphans,
494
+ primary,
495
+ crossLinks: pendingCross.map(({ from, child }) => ({
496
+ from,
497
+ to: primary.get(child),
498
+ kind: "cross"
499
+ }))
500
+ };
501
+ }
502
+ /** Parent → child pairs of occurrence keys: one per rendered parent-child link, then cross-links. */
503
+ function treeLinks(tree) {
504
+ const links = [];
505
+ const walk = (occurrence) => {
506
+ for (const child of occurrence.children) {
507
+ links.push({
508
+ from: occurrence.key,
509
+ to: child.key,
510
+ kind: "tree"
511
+ });
512
+ walk(child);
513
+ }
514
+ };
515
+ for (const root of [...tree.roots, ...tree.orphans]) walk(root);
516
+ return [...links, ...tree.crossLinks];
517
+ }
518
+ //#endregion
516
519
  //#region src/layouts/EdgeOverlay.tsx
517
520
  /**
518
521
  * SVG layer drawing connectors between rendered occurrences.
@@ -655,6 +658,89 @@ function Branches({ orientation, nested = false, children }) {
655
658
  });
656
659
  }
657
660
  //#endregion
661
+ //#region src/core/xy.ts
662
+ /**
663
+ * Position nodes by two numeric fields. Every node of a plotted type appears once: on the plot
664
+ * when both its values are numbers inside the axes' domains, otherwise in `unplaced` with the
665
+ * reason. Positions are normalised; layouts decide the plot's size.
666
+ */
667
+ function buildXYPlot(graph, regionId, layout) {
668
+ const { x, y } = layout;
669
+ const nodes = [...graph.nodes.values()];
670
+ const types = new Set(layout.types.length > 0 ? layout.types : nodes.filter((n) => isNumber(n.schemaData[x.field]) || isNumber(n.schemaData[y.field])).map((n) => n.resolvedType));
671
+ const plotted = nodes.filter((n) => types.has(n.resolvedType));
672
+ const domains = {
673
+ x: domainOf(x, plotted),
674
+ y: domainOf(y, plotted)
675
+ };
676
+ const points = [];
677
+ const unplaced = [];
678
+ for (const node of plotted) {
679
+ const key = occurrenceKey(regionId, [node.id]);
680
+ const px = position(node, x, domains.x);
681
+ const py = position(node, y, domains.y);
682
+ if (typeof px === "number" && typeof py === "number") points.push({
683
+ key,
684
+ node,
685
+ x: px,
686
+ y: py
687
+ });
688
+ else unplaced.push({
689
+ key,
690
+ node,
691
+ reasons: [px, py].filter((r) => typeof r === "string")
692
+ });
693
+ }
694
+ const keys = new Map(points.map((p) => [p.node.id, p.key]));
695
+ const fields = new Set(layout.edgeFields);
696
+ const links = edgesWithin(graph).filter((e) => keys.has(e.parent) && keys.has(e.child) && (fields.size === 0 || fields.has(e.field))).map((e) => ({
697
+ from: keys.get(e.parent),
698
+ to: keys.get(e.child),
699
+ field: e.field,
700
+ hierarchy: isHierarchyEdge(e, graph.hierarchy),
701
+ lane: 0
702
+ }));
703
+ assignLanes(links);
704
+ return {
705
+ points,
706
+ unplaced,
707
+ links,
708
+ domains
709
+ };
710
+ }
711
+ function isNumber(value) {
712
+ return typeof value === "number" && Number.isFinite(value);
713
+ }
714
+ /** The axis's fixed domain, or the range of plotted values (widened when they're all equal). */
715
+ function domainOf(axis, nodes) {
716
+ if (Array.isArray(axis.domain)) return [axis.domain[0], axis.domain[1]];
717
+ const values = nodes.map((n) => n.schemaData[axis.field]).filter(isNumber);
718
+ if (values.length === 0) return [0, 1];
719
+ const min = Math.min(...values);
720
+ const max = Math.max(...values);
721
+ return min === max ? [min - 1, max + 1] : [min, max];
722
+ }
723
+ /** The node's normalised position on the axis, or why it has none. */
724
+ function position(node, axis, [min, max]) {
725
+ const value = node.schemaData[axis.field];
726
+ if (value === void 0 || value === null) return `no ${axis.field}`;
727
+ if (!isNumber(value)) return `${axis.field} is not a number`;
728
+ if (value < min || value > max) return `${axis.field} ${value} is outside ${min}–${max}`;
729
+ const t = (value - min) / (max - min);
730
+ return axis.direction === "descending" ? 1 - t : t;
731
+ }
732
+ /** Spread links joining the same pair of nodes (in either direction) into lanes around 0. */
733
+ function assignLanes(links) {
734
+ const byPair = /* @__PURE__ */ new Map();
735
+ for (const link of links) {
736
+ const pair = [link.from, link.to].sort().join("\n");
737
+ byPair.set(pair, [...byPair.get(pair) ?? [], link]);
738
+ }
739
+ for (const group of byPair.values()) group.forEach((link, i) => {
740
+ link.lane = i - (group.length - 1) / 2;
741
+ });
742
+ }
743
+ //#endregion
658
744
  //#region src/layouts/XYLayout.tsx
659
745
  /**
660
746
  * Nodes positioned by two numeric fields, with straight connectors between them. Positions are
@@ -782,16 +868,27 @@ function format(value) {
782
868
  }
783
869
  //#endregion
784
870
  //#region src/layouts/MapView.tsx
871
+ var EMPTY = /* @__PURE__ */ new Map();
785
872
  /** Renders a map view definition against a space. Sizes to its container and scrolls within it. */
786
- function MapView({ definition, graph, values, onNodeSelect }) {
873
+ function MapView({ definition, graph, values, annotations, onNodeSelect }) {
787
874
  const instanceId = useId();
788
875
  const region = useMemo(() => resolveRegions(definition, values), [definition, values])[0];
789
- return /* @__PURE__ */ jsx("div", {
790
- className: "mv:h-full mv:w-full mv:overflow-auto mv:bg-surface mv:text-ink",
791
- children: layoutFor(region, {
792
- graph,
793
- instanceId,
794
- onNodeSelect
876
+ const props = {
877
+ graph,
878
+ instanceId,
879
+ onNodeSelect
880
+ };
881
+ const { validation, validationSources } = region.nodes;
882
+ const visibleAnnotations = useMemo(() => annotations ? filterAnnotations(annotations, validation, validationSources) : EMPTY, [
883
+ annotations,
884
+ validation,
885
+ validationSources
886
+ ]);
887
+ return /* @__PURE__ */ jsx(AnnotationsContext.Provider, {
888
+ value: visibleAnnotations,
889
+ children: /* @__PURE__ */ jsx("div", {
890
+ className: "mv:h-full mv:w-full mv:overflow-auto mv:bg-surface mv:text-ink",
891
+ children: layoutFor(region, props)
795
892
  })
796
893
  });
797
894
  }
@@ -822,68 +919,127 @@ function layoutFor(region, props) {
822
919
  }
823
920
  }
824
921
  //#endregion
922
+ //#region src/layouts/ParameterInputs.tsx
923
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
924
+ /** A checkbox for a boolean parameter, labelled with the parameter's title. */
925
+ function ToggleParameterInput({ parameter, value, onChange }) {
926
+ const id = useId();
927
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
928
+ id,
929
+ type: "checkbox",
930
+ checked: value === true,
931
+ onChange: (e) => onChange(e.target.checked)
932
+ }), /* @__PURE__ */ jsx("label", {
933
+ htmlFor: id,
934
+ children: parameter.title
935
+ })] });
936
+ }
937
+ /** A dropdown for a parameter with one of several options. */
938
+ function SelectParameterInput({ parameter, value, onChange }) {
939
+ const id = useId();
940
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("label", {
941
+ htmlFor: id,
942
+ className: "mv:text-muted",
943
+ children: parameter.title
944
+ }), /* @__PURE__ */ jsx("select", {
945
+ id,
946
+ value: JSON.stringify(value),
947
+ onChange: (e) => onChange(JSON.parse(e.target.value)),
948
+ children: parameter.control.options.map((option) => /* @__PURE__ */ jsx("option", {
949
+ value: JSON.stringify(option),
950
+ children: String(option)
951
+ }, JSON.stringify(option)))
952
+ })] });
953
+ }
954
+ /** A checkbox group for a parameter taking any subset of its options. */
955
+ function MultiSelectParameterInput({ parameter, value, onChange }) {
956
+ const { options } = parameter.control;
957
+ const selected = Array.isArray(value) ? value : [];
958
+ return /* @__PURE__ */ jsxs("fieldset", {
959
+ className: "mv:m-0 mv:flex mv:flex-wrap mv:items-center mv:gap-2 mv:border-0 mv:p-0",
960
+ children: [/* @__PURE__ */ jsx("legend", {
961
+ className: "mv:float-left mv:mr-1.5 mv:p-0 mv:text-muted",
962
+ children: parameter.title
963
+ }), options.map((option) => {
964
+ const checked = selected.some((v) => same(v, option));
965
+ return /* @__PURE__ */ jsxs("label", {
966
+ className: "mv:flex mv:items-center mv:gap-1",
967
+ children: [/* @__PURE__ */ jsx("input", {
968
+ type: "checkbox",
969
+ checked,
970
+ onChange: () => onChange(options.filter((o) => o === option ? !checked : selected.some((v) => same(v, o))))
971
+ }), String(option)]
972
+ }, JSON.stringify(option));
973
+ })]
974
+ });
975
+ }
976
+ /** The input matching a parameter's control kind. */
977
+ function ParameterInput({ parameter, value, onChange }) {
978
+ const { control } = parameter;
979
+ switch (control.kind) {
980
+ case "toggle": return /* @__PURE__ */ jsx(ToggleParameterInput, {
981
+ parameter: {
982
+ ...parameter,
983
+ control
984
+ },
985
+ value,
986
+ onChange
987
+ });
988
+ case "select": return /* @__PURE__ */ jsx(SelectParameterInput, {
989
+ parameter: {
990
+ ...parameter,
991
+ control
992
+ },
993
+ value,
994
+ onChange
995
+ });
996
+ case "multiselect": return /* @__PURE__ */ jsx(MultiSelectParameterInput, {
997
+ parameter: {
998
+ ...parameter,
999
+ control
1000
+ },
1001
+ value,
1002
+ onChange
1003
+ });
1004
+ }
1005
+ }
1006
+ //#endregion
1007
+ //#region src/layouts/useParameterControls.ts
1008
+ /**
1009
+ * The state behind a map view's parameter controls, with no markup, for hosts that lay the controls
1010
+ * out themselves. Pair each entry with `ParameterInput`, or with one of the typed inputs.
1011
+ */
1012
+ function useParameterControls(definition, values, onChange) {
1013
+ const resolved = useMemo(() => resolveParameterValues(definition, values), [definition, values]);
1014
+ return definition.parameters.map((parameter) => ({
1015
+ parameter,
1016
+ value: resolved[parameter.name],
1017
+ setValue: (value) => onChange(parameter.name, value)
1018
+ }));
1019
+ }
1020
+ //#endregion
825
1021
  //#region src/layouts/ParameterControls.tsx
826
1022
  /**
827
- * Controls generated from a map view's declared parameters. Hosts own the values (session
828
- * state) and decide where the controls go.
1023
+ * Default layout for a map view's parameter controls: a wrapping row. Hosts wanting their own layout
1024
+ * use `useParameterControls` with `ParameterInput` (or the typed inputs) instead.
829
1025
  */
830
1026
  function ParameterControls({ definition, values, onChange }) {
831
- const id = useId();
832
- const resolved = useMemo(() => resolveParameterValues(definition, values), [definition, values]);
833
- const { parameters } = definition;
834
- if (parameters.length === 0) return null;
1027
+ const controls = useParameterControls(definition, values, onChange);
1028
+ if (controls.length === 0) return null;
835
1029
  return /* @__PURE__ */ jsx("div", {
836
1030
  className: "mv:flex mv:flex-wrap mv:items-center mv:gap-x-4 mv:gap-y-2 mv:text-sm",
837
- children: parameters.map((param) => {
838
- const value = resolved[param.name];
839
- const controlId = `${id}-${param.name}`;
840
- const { control } = param;
841
- return /* @__PURE__ */ jsx("div", {
842
- className: "mv:flex mv:items-center mv:gap-1.5",
843
- title: param.description,
844
- children: control.kind === "toggle" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
845
- id: controlId,
846
- type: "checkbox",
847
- checked: value === true,
848
- onChange: (e) => onChange(param.name, e.target.checked)
849
- }), /* @__PURE__ */ jsx("label", {
850
- htmlFor: controlId,
851
- children: param.title
852
- })] }) : control.kind === "select" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("label", {
853
- htmlFor: controlId,
854
- className: "mv:text-muted",
855
- children: param.title
856
- }), /* @__PURE__ */ jsx("select", {
857
- id: controlId,
858
- value: JSON.stringify(value),
859
- onChange: (e) => onChange(param.name, JSON.parse(e.target.value)),
860
- children: control.options.map((option) => /* @__PURE__ */ jsx("option", {
861
- value: JSON.stringify(option),
862
- children: String(option)
863
- }, JSON.stringify(option)))
864
- })] }) : /* @__PURE__ */ jsxs("fieldset", {
865
- className: "mv:m-0 mv:flex mv:flex-wrap mv:items-center mv:gap-2 mv:border-0 mv:p-0",
866
- children: [/* @__PURE__ */ jsx("legend", {
867
- className: "mv:float-left mv:mr-1.5 mv:p-0 mv:text-muted",
868
- children: param.title
869
- }), control.options.map((option) => {
870
- const selected = Array.isArray(value) ? value : [];
871
- const checked = selected.some((v) => JSON.stringify(v) === JSON.stringify(option));
872
- return /* @__PURE__ */ jsxs("label", {
873
- className: "mv:flex mv:items-center mv:gap-1",
874
- children: [/* @__PURE__ */ jsx("input", {
875
- type: "checkbox",
876
- checked,
877
- onChange: () => onChange(param.name, control.options.filter((o) => o === option ? !checked : selected.some((v) => JSON.stringify(v) === JSON.stringify(o))))
878
- }), String(option)]
879
- }, JSON.stringify(option));
880
- })]
881
- })
882
- }, param.name);
883
- })
1031
+ children: controls.map(({ parameter, value, setValue }) => /* @__PURE__ */ jsx("div", {
1032
+ className: "mv:flex mv:items-center mv:gap-1.5",
1033
+ title: parameter.description,
1034
+ children: /* @__PURE__ */ jsx(ParameterInput, {
1035
+ parameter,
1036
+ value,
1037
+ onChange: setValue
1038
+ })
1039
+ }, parameter.name))
884
1040
  });
885
1041
  }
886
1042
  //#endregion
887
- export { MAP_VIEW_SCHEMAS, MapView, MapViewDefinitionError, ParameterControls, mapViewsForSchema, parseMapViewDefinition, resolveParameterValues, selectionFilter };
1043
+ export { MAP_VIEW_SCHEMAS, MapView, MapViewDefinitionError, MultiSelectParameterInput, ParameterControls, ParameterInput, SelectParameterInput, ToggleParameterInput, annotationsFrom, mapViewsForSchema, parseMapViewDefinition, resolveParameterValues, selectionFilter, useParameterControls, viewValidationLevel, worstSeverity };
888
1044
 
889
1045
  //# sourceMappingURL=index.js.map