@sqlrooms/deck 0.29.0-rc.10 → 0.29.0-rc.12

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 (173) hide show
  1. package/README.md +88 -16
  2. package/dist/BlockMapSettings.d.ts.map +1 -1
  3. package/dist/BlockMapSettings.js +4 -16
  4. package/dist/BlockMapSettings.js.map +1 -1
  5. package/dist/DashboardMapSettings.d.ts.map +1 -1
  6. package/dist/DashboardMapSettings.js +7 -10
  7. package/dist/DashboardMapSettings.js.map +1 -1
  8. package/dist/DeckJsonMap.d.ts.map +1 -1
  9. package/dist/DeckJsonMap.js +20 -18
  10. package/dist/DeckJsonMap.js.map +1 -1
  11. package/dist/DeckJsonMapSpec.d.ts +916 -53
  12. package/dist/DeckJsonMapSpec.d.ts.map +1 -1
  13. package/dist/DeckJsonMapSpec.js +9 -0
  14. package/dist/DeckJsonMapSpec.js.map +1 -1
  15. package/dist/DeckMapDefaultStylesProvider.d.ts +3 -4
  16. package/dist/DeckMapDefaultStylesProvider.d.ts.map +1 -1
  17. package/dist/DeckMapDefaultStylesProvider.js +13 -7
  18. package/dist/DeckMapDefaultStylesProvider.js.map +1 -1
  19. package/dist/DeckMapSurface.d.ts +1 -1
  20. package/dist/DeckMapSurface.js +1 -1
  21. package/dist/DeckMapSurface.js.map +1 -1
  22. package/dist/MapSettings.d.ts +14 -0
  23. package/dist/MapSettings.d.ts.map +1 -1
  24. package/dist/MapSettings.js +555 -186
  25. package/dist/MapSettings.js.map +1 -1
  26. package/dist/MapSettingsControls.d.ts +8 -1
  27. package/dist/MapSettingsControls.d.ts.map +1 -1
  28. package/dist/MapSettingsControls.js +24 -2
  29. package/dist/MapSettingsControls.js.map +1 -1
  30. package/dist/ai.d.ts +19 -5
  31. package/dist/ai.d.ts.map +1 -1
  32. package/dist/ai.js +63 -112
  33. package/dist/ai.js.map +1 -1
  34. package/dist/aiNormalize.d.ts +31 -0
  35. package/dist/aiNormalize.d.ts.map +1 -0
  36. package/dist/aiNormalize.js +630 -0
  37. package/dist/aiNormalize.js.map +1 -0
  38. package/dist/block.d.ts +1 -1
  39. package/dist/block.d.ts.map +1 -1
  40. package/dist/block.js +1 -1
  41. package/dist/block.js.map +1 -1
  42. package/dist/createOrUpdateDeckMapResource.d.ts +6 -2
  43. package/dist/createOrUpdateDeckMapResource.d.ts.map +1 -1
  44. package/dist/createOrUpdateDeckMapResource.js +57 -4
  45. package/dist/createOrUpdateDeckMapResource.js.map +1 -1
  46. package/dist/dashboard.d.ts.map +1 -1
  47. package/dist/dashboard.js +72 -8
  48. package/dist/dashboard.js.map +1 -1
  49. package/dist/dashboardConfig.d.ts +10 -1
  50. package/dist/dashboardConfig.d.ts.map +1 -1
  51. package/dist/dashboardConfig.js +55 -22
  52. package/dist/dashboardConfig.js.map +1 -1
  53. package/dist/datasets/PreparedDatasetStore.d.ts +3 -4
  54. package/dist/datasets/PreparedDatasetStore.d.ts.map +1 -1
  55. package/dist/datasets/PreparedDatasetStore.js +35 -10
  56. package/dist/datasets/PreparedDatasetStore.js.map +1 -1
  57. package/dist/datasets/helpers.d.ts +39 -2
  58. package/dist/datasets/helpers.d.ts.map +1 -1
  59. package/dist/datasets/helpers.js +86 -3
  60. package/dist/datasets/helpers.js.map +1 -1
  61. package/dist/datasets/usePreparedDatasetStates.d.ts.map +1 -1
  62. package/dist/datasets/usePreparedDatasetStates.js +12 -8
  63. package/dist/datasets/usePreparedDatasetStates.js.map +1 -1
  64. package/dist/datasets/wrapGeometryAsWkb.d.ts +20 -0
  65. package/dist/datasets/wrapGeometryAsWkb.d.ts.map +1 -0
  66. package/dist/datasets/wrapGeometryAsWkb.js +65 -0
  67. package/dist/datasets/wrapGeometryAsWkb.js.map +1 -0
  68. package/dist/index.d.ts +3 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +2 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/json/colorScaleFunction.d.ts +12 -4
  73. package/dist/json/colorScaleFunction.d.ts.map +1 -1
  74. package/dist/json/colorScaleFunction.js.map +1 -1
  75. package/dist/json/compileColorScale.d.ts +3 -2
  76. package/dist/json/compileColorScale.d.ts.map +1 -1
  77. package/dist/json/compileColorScale.js +16 -1
  78. package/dist/json/compileColorScale.js.map +1 -1
  79. package/dist/json/compileGeoArrowAccessor.d.ts +5 -0
  80. package/dist/json/compileGeoArrowAccessor.d.ts.map +1 -1
  81. package/dist/json/compileGeoArrowAccessor.js +17 -3
  82. package/dist/json/compileGeoArrowAccessor.js.map +1 -1
  83. package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
  84. package/dist/json/createDeckJsonConfiguration.js +278 -151
  85. package/dist/json/createDeckJsonConfiguration.js.map +1 -1
  86. package/dist/json/defaultClasses.d.ts.map +1 -1
  87. package/dist/json/defaultClasses.js +3 -3
  88. package/dist/json/defaultClasses.js.map +1 -1
  89. package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
  90. package/dist/json/extractColorScaleLegends.js +43 -14
  91. package/dist/json/extractColorScaleLegends.js.map +1 -1
  92. package/dist/json/heatmapDefaults.d.ts +17 -7
  93. package/dist/json/heatmapDefaults.d.ts.map +1 -1
  94. package/dist/json/heatmapDefaults.js +54 -12
  95. package/dist/json/heatmapDefaults.js.map +1 -1
  96. package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -1
  97. package/dist/json/layers/DeckH3HexagonLayer.js +9 -14
  98. package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -1
  99. package/dist/json/layers/DeckHeatmapLayer.d.ts +32 -0
  100. package/dist/json/layers/DeckHeatmapLayer.d.ts.map +1 -0
  101. package/dist/json/layers/DeckHeatmapLayer.js +69 -0
  102. package/dist/json/layers/DeckHeatmapLayer.js.map +1 -0
  103. package/dist/json/layers/h3IndexFormat.d.ts +6 -0
  104. package/dist/json/layers/h3IndexFormat.d.ts.map +1 -0
  105. package/dist/json/layers/h3IndexFormat.js +17 -0
  106. package/dist/json/layers/h3IndexFormat.js.map +1 -0
  107. package/dist/json/layers/index.d.ts +2 -0
  108. package/dist/json/layers/index.d.ts.map +1 -1
  109. package/dist/json/layers/index.js +2 -0
  110. package/dist/json/layers/index.js.map +1 -1
  111. package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
  112. package/dist/json/rewriteGeoArrowAccessors.js +22 -5
  113. package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
  114. package/dist/json/scaleFunction.d.ts +27 -0
  115. package/dist/json/scaleFunction.d.ts.map +1 -0
  116. package/dist/json/scaleFunction.js +151 -0
  117. package/dist/json/scaleFunction.js.map +1 -0
  118. package/dist/mapAiConfig.d.ts +8 -0
  119. package/dist/mapAiConfig.d.ts.map +1 -1
  120. package/dist/mapAiConfig.js +9 -1
  121. package/dist/mapAiConfig.js.map +1 -1
  122. package/dist/mapAiSharedInstructions.d.ts +3 -0
  123. package/dist/mapAiSharedInstructions.d.ts.map +1 -0
  124. package/dist/mapAiSharedInstructions.js +16 -0
  125. package/dist/mapAiSharedInstructions.js.map +1 -0
  126. package/dist/mapConfig.d.ts +7 -1
  127. package/dist/mapConfig.d.ts.map +1 -1
  128. package/dist/mapConfig.js.map +1 -1
  129. package/dist/mapConfigUtils.d.ts +22 -0
  130. package/dist/mapConfigUtils.d.ts.map +1 -1
  131. package/dist/mapConfigUtils.js +147 -3
  132. package/dist/mapConfigUtils.js.map +1 -1
  133. package/dist/mapFit.d.ts +16 -0
  134. package/dist/mapFit.d.ts.map +1 -1
  135. package/dist/mapFit.js +208 -30
  136. package/dist/mapFit.js.map +1 -1
  137. package/dist/mapLayerConfigUtils.d.ts +64 -1
  138. package/dist/mapLayerConfigUtils.d.ts.map +1 -1
  139. package/dist/mapLayerConfigUtils.js +277 -16
  140. package/dist/mapLayerConfigUtils.js.map +1 -1
  141. package/dist/mapResourceAuthoring.d.ts +3 -1
  142. package/dist/mapResourceAuthoring.d.ts.map +1 -1
  143. package/dist/mapResourceAuthoring.js +413 -20
  144. package/dist/mapResourceAuthoring.js.map +1 -1
  145. package/dist/mosaic.d.ts +1 -1
  146. package/dist/mosaic.d.ts.map +1 -1
  147. package/dist/mosaic.js.map +1 -1
  148. package/dist/prepare/geoarrow.js.map +1 -1
  149. package/dist/prepare/prepareDeckDataset.d.ts.map +1 -1
  150. package/dist/prepare/prepareDeckDataset.js +1 -0
  151. package/dist/prepare/prepareDeckDataset.js.map +1 -1
  152. package/dist/prepare/types.d.ts +3 -0
  153. package/dist/prepare/types.d.ts.map +1 -1
  154. package/dist/prepare/types.js.map +1 -1
  155. package/dist/prepare/wkbDecoder.d.ts +10 -0
  156. package/dist/prepare/wkbDecoder.d.ts.map +1 -1
  157. package/dist/prepare/wkbDecoder.js +141 -20
  158. package/dist/prepare/wkbDecoder.js.map +1 -1
  159. package/dist/prepare/wkbParser.d.ts +9 -0
  160. package/dist/prepare/wkbParser.d.ts.map +1 -1
  161. package/dist/prepare/wkbParser.js +22 -0
  162. package/dist/prepare/wkbParser.js.map +1 -1
  163. package/dist/selectStarAsWkbCollision.d.ts +3 -0
  164. package/dist/selectStarAsWkbCollision.d.ts.map +1 -0
  165. package/dist/selectStarAsWkbCollision.js +78 -0
  166. package/dist/selectStarAsWkbCollision.js.map +1 -0
  167. package/dist/types.d.ts +1 -1
  168. package/dist/types.d.ts.map +1 -1
  169. package/dist/types.js.map +1 -1
  170. package/dist/useDeckMapFitToBounds.d.ts.map +1 -1
  171. package/dist/useDeckMapFitToBounds.js +1 -1
  172. package/dist/useDeckMapFitToBounds.js.map +1 -1
  173. package/package.json +15 -15
@@ -1,5 +1,9 @@
1
+ import { binnedNumericSchemes, categoricalSchemes, continuousDivergingSchemes, continuousSequentialSchemes, formatColorSchemePromptLists, } from '@sqlrooms/color-scales/colorSchemeNames';
1
2
  import { DeckJsonMapSpec } from './DeckJsonMapSpec';
2
3
  import { isDeckMapSqlDatasetSource, isDeckMapTableDatasetSource, } from './mapConfig';
4
+ import { hasSelectStarAsWkbCollision } from './selectStarAsWkbCollision';
5
+ import { getDeckMapSharedAiContractRules } from './mapAiSharedInstructions';
6
+ import { DECK_MAP_LAYER_TYPE_OPTIONS } from './mapLayerConfigUtils';
3
7
  /** Error raised before an invalid map resource can be durably written. */
4
8
  export class DeckMapResourceConfigError extends Error {
5
9
  issues;
@@ -11,12 +15,102 @@ export class DeckMapResourceConfigError extends Error {
11
15
  this.issues = issues;
12
16
  }
13
17
  }
18
+ const BINNED_SCHEME_NAMES = new Set(binnedNumericSchemes);
19
+ const SEQUENTIAL_SCHEME_NAMES = new Set(continuousSequentialSchemes);
20
+ const DIVERGING_SCHEME_NAMES = new Set(continuousDivergingSchemes);
21
+ const CATEGORICAL_SCHEME_NAMES = new Set(categoricalSchemes);
22
+ /** Issue when colorScale `type` and `scheme` disagree or are incomplete. */
23
+ export function getColorScaleTypeSchemeIssue(type, scheme) {
24
+ if (typeof type !== 'string' || !type.trim()) {
25
+ return 'colorScale requires a "type" (sequential, diverging, quantize, quantile, threshold, or categorical)';
26
+ }
27
+ if (typeof scheme !== 'string' || !scheme.trim()) {
28
+ return 'colorScale requires a "scheme" (exact name such as Viridis, YlOrRd, Blues, or Tableau10)';
29
+ }
30
+ if (type !== type.trim()) {
31
+ return 'colorScale type must not have leading or trailing whitespace';
32
+ }
33
+ if (scheme !== scheme.trim()) {
34
+ return 'colorScale scheme must not have leading or trailing whitespace';
35
+ }
36
+ const scaleType = type;
37
+ const schemeName = scheme;
38
+ const allowedForType = (allowed, label) => {
39
+ if (allowed.has(schemeName))
40
+ return undefined;
41
+ if (SEQUENTIAL_SCHEME_NAMES.has(schemeName)) {
42
+ return `scheme "${schemeName}" requires type "sequential" (not "${scaleType}") — use type "sequential", or pick a ${label}`;
43
+ }
44
+ if (DIVERGING_SCHEME_NAMES.has(schemeName)) {
45
+ return `scheme "${schemeName}" requires type "diverging" (not "${scaleType}") — use type "diverging", or pick a ${label}`;
46
+ }
47
+ if (CATEGORICAL_SCHEME_NAMES.has(schemeName)) {
48
+ return `scheme "${schemeName}" requires type "categorical" (not "${scaleType}") — use type "categorical", or pick a ${label}`;
49
+ }
50
+ return `scheme "${schemeName}" is not valid for type "${scaleType}" — use a ${label}`;
51
+ };
52
+ switch (scaleType) {
53
+ case 'quantile':
54
+ case 'quantize':
55
+ case 'threshold':
56
+ return allowedForType(BINNED_SCHEME_NAMES, 'ColorBrewer binned ramp such as YlOrRd, Blues, Greens, or RdYlBu');
57
+ case 'sequential':
58
+ return allowedForType(SEQUENTIAL_SCHEME_NAMES, 'sequential scheme such as Viridis, Plasma, Blues, or YlOrRd');
59
+ case 'diverging':
60
+ return allowedForType(DIVERGING_SCHEME_NAMES, 'diverging scheme such as RdBu, Spectral, or BrBG');
61
+ case 'categorical':
62
+ return allowedForType(CATEGORICAL_SCHEME_NAMES, 'categorical scheme such as Tableau10, Set2, or Category10');
63
+ default:
64
+ return `colorScale type "${scaleType}" is not supported — use sequential, diverging, quantize, quantile, threshold, or categorical`;
65
+ }
66
+ }
67
+ function getColorScaleThresholdsIssue(type, thresholds) {
68
+ if (type !== 'threshold')
69
+ return undefined;
70
+ if (!Array.isArray(thresholds) ||
71
+ thresholds.length === 0 ||
72
+ !thresholds.every((value) => typeof value === 'number' && Number.isFinite(value))) {
73
+ return 'colorScale type "threshold" requires a non-empty numeric "thresholds" array';
74
+ }
75
+ return undefined;
76
+ }
14
77
  function isRecord(value) {
15
78
  return Boolean(value && typeof value === 'object' && !Array.isArray(value));
16
79
  }
17
80
  function hasEntries(value) {
18
81
  return isRecord(value) && Object.keys(value).length > 0;
19
82
  }
83
+ /** True for invalid `ST_MakeLine(ST_Point(...) ORDER BY ...)` (ORDER BY belongs in LIST). */
84
+ function hasBadStMakeLinePointOrderBy(sql) {
85
+ const re = /\bST_MakeLine\s*\(/gi;
86
+ let match;
87
+ while ((match = re.exec(sql)) !== null) {
88
+ let i = match.index + match[0].length;
89
+ while (i < sql.length && /\s/.test(sql[i]))
90
+ i += 1;
91
+ if (/^LIST\s*\(/i.test(sql.slice(i)))
92
+ continue;
93
+ const pointMatch = sql.slice(i).match(/^ST_Point\s*\(/i);
94
+ if (!pointMatch)
95
+ continue;
96
+ i += pointMatch[0].length;
97
+ let depth = 1;
98
+ while (i < sql.length && depth > 0) {
99
+ const ch = sql[i];
100
+ if (ch === '(')
101
+ depth += 1;
102
+ else if (ch === ')')
103
+ depth -= 1;
104
+ i += 1;
105
+ }
106
+ if (depth !== 0)
107
+ continue;
108
+ if (/^\s+ORDER\s+BY\b/i.test(sql.slice(i))) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ }
20
114
  function mergeOptionalRecord(existing, incoming) {
21
115
  if (isRecord(existing) && isRecord(incoming)) {
22
116
  return { ...existing, ...incoming };
@@ -81,6 +175,22 @@ function mergeSpecPatch(existingSpec, incomingSpec, options) {
81
175
  ...(layers ? { layers } : {}),
82
176
  };
83
177
  }
178
+ /** True when a bare `{tableName}` patch would wipe existing geometry SQL. */
179
+ function isSourceDowngrade(existingSource, incomingSource) {
180
+ if (!existingSource || !incomingSource)
181
+ return false;
182
+ const incomingIsBareTableName = isDeckMapTableDatasetSource(incomingSource) &&
183
+ !('transformSql' in incomingSource &&
184
+ typeof incomingSource.transformSql === 'string' &&
185
+ incomingSource.transformSql.trim().length > 0);
186
+ if (!incomingIsBareTableName)
187
+ return false;
188
+ if (isDeckMapSqlDatasetSource(existingSource))
189
+ return true;
190
+ return (isDeckMapTableDatasetSource(existingSource) &&
191
+ typeof existingSource.transformSql === 'string' &&
192
+ existingSource.transformSql.trim().length > 0);
193
+ }
84
194
  function mergeDatasetRegistry(existingDatasets, incomingDatasets, replaceDatasets) {
85
195
  if (replaceDatasets)
86
196
  return incomingDatasets;
@@ -89,13 +199,18 @@ function mergeDatasetRegistry(existingDatasets, incomingDatasets, replaceDataset
89
199
  const datasets = { ...existingDatasets };
90
200
  for (const [datasetId, incomingDataset] of Object.entries(incomingDatasets)) {
91
201
  const existingDataset = existingDatasets[datasetId];
92
- datasets[datasetId] = existingDataset
93
- ? {
94
- ...existingDataset,
95
- ...incomingDataset,
96
- source: incomingDataset.source ?? existingDataset.source,
97
- }
98
- : incomingDataset;
202
+ if (!existingDataset) {
203
+ datasets[datasetId] = incomingDataset;
204
+ continue;
205
+ }
206
+ const resolvedSource = isSourceDowngrade(existingDataset.source, incomingDataset.source)
207
+ ? existingDataset.source
208
+ : (incomingDataset.source ?? existingDataset.source);
209
+ datasets[datasetId] = {
210
+ ...existingDataset,
211
+ ...incomingDataset,
212
+ source: resolvedSource,
213
+ };
99
214
  }
100
215
  return datasets;
101
216
  }
@@ -143,6 +258,27 @@ function parseSpec(config) {
143
258
  issues: [],
144
259
  };
145
260
  }
261
+ const DECK_MAP_LAYER_CLASS_ALIASES = {
262
+ ScatterplotLayer: 'GeoArrowScatterplotLayer',
263
+ HeatmapLayer: 'GeoArrowHeatmapLayer',
264
+ ColumnLayer: 'GeoArrowColumnLayer',
265
+ PathLayer: 'GeoArrowPathLayer',
266
+ PolygonLayer: 'GeoArrowPolygonLayer',
267
+ ArcLayer: 'GeoArrowArcLayer',
268
+ TripsLayer: 'GeoArrowTripsLayer',
269
+ H3HexagonLayer: 'GeoArrowH3HexagonLayer',
270
+ };
271
+ const DECK_MAP_SUPPORTED_LAYER_TYPES = new Set(DECK_MAP_LAYER_TYPE_OPTIONS.map((option) => option.value));
272
+ const COLOR_SCALE_ACCESSOR_PROPS = [
273
+ 'getFillColor',
274
+ 'getLineColor',
275
+ 'getColor',
276
+ 'getSourceColor',
277
+ 'getTargetColor',
278
+ ];
279
+ function isPlainObject(value) {
280
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
281
+ }
146
282
  /**
147
283
  * Validates the post-merge invariants of a renderable, resource-native map.
148
284
  * Patch inputs may be sparse, but the durable result must have supported
@@ -190,6 +326,17 @@ export function getDeckMapResourceConfigIssues(config, options = {}) {
190
326
  : 'must define source.tableName or source.sqlQuery',
191
327
  });
192
328
  }
329
+ const sqlParts = [
330
+ isDeckMapTableDatasetSource(source) ? source.transformSql : undefined,
331
+ isDeckMapSqlDatasetSource(source) ? source.sqlQuery : undefined,
332
+ ].filter((part) => typeof part === 'string' && part.trim().length > 0);
333
+ const sql = sqlParts.join('\n');
334
+ if (sql && hasSelectStarAsWkbCollision(sql)) {
335
+ issues.push({
336
+ path: `datasets.${datasetId}.source`,
337
+ message: 'Do not use SELECT *, ST_AsWKB(col) AS col — DuckDB keeps the original column and the WKB alias collides, producing empty maps. Use SELECT * EXCLUDE (col), ST_AsWKB(col) AS col, or omit transformSql when the geometry column already exists.',
338
+ });
339
+ }
193
340
  }
194
341
  if (parsedSpec.layers.length === 0) {
195
342
  issues.push({
@@ -205,6 +352,64 @@ export function getDeckMapResourceConfigIssues(config, options = {}) {
205
352
  message: 'must name a Deck JSON layer class',
206
353
  });
207
354
  }
355
+ else {
356
+ const aliased = DECK_MAP_LAYER_CLASS_ALIASES[layerType];
357
+ if (aliased) {
358
+ issues.push({
359
+ path: `spec.layers.${index}.@@type`,
360
+ message: `use "${aliased}" — plain "${layerType}" is not a registered Deck JSON layer class`,
361
+ });
362
+ }
363
+ else if (!DECK_MAP_SUPPORTED_LAYER_TYPES.has(layerType)) {
364
+ issues.push({
365
+ path: `spec.layers.${index}.@@type`,
366
+ message: `use a supported Deck map layer type (${[...DECK_MAP_SUPPORTED_LAYER_TYPES].join(', ')}) — "${layerType}" is not allowed`,
367
+ });
368
+ }
369
+ }
370
+ for (const prop of COLOR_SCALE_ACCESSOR_PROPS) {
371
+ const value = layer[prop];
372
+ if (!isPlainObject(value))
373
+ continue;
374
+ if (value['@@type'] === 'ColorScale' &&
375
+ value['@@function'] !== 'colorScale') {
376
+ issues.push({
377
+ path: `spec.layers.${index}.${prop}`,
378
+ message: 'use {"@@function":"colorScale","field":"<column>","type":"...","scheme":"...","domain":"auto"} — {"@@type":"ColorScale","column":"..."} is not valid colorScale syntax',
379
+ });
380
+ continue;
381
+ }
382
+ if (value['@@function'] === 'colorScale') {
383
+ if (typeof value.field !== 'string' || !value.field.trim()) {
384
+ if (typeof value.column === 'string' && value.column.trim()) {
385
+ issues.push({
386
+ path: `spec.layers.${index}.${prop}`,
387
+ message: 'colorScale uses "field" for the column name, not "column" — rename column → field',
388
+ });
389
+ }
390
+ else {
391
+ issues.push({
392
+ path: `spec.layers.${index}.${prop}`,
393
+ message: 'colorScale requires a non-empty string "field" with the exact column name',
394
+ });
395
+ }
396
+ }
397
+ const schemeIssue = getColorScaleTypeSchemeIssue(value.type, value.scheme);
398
+ if (schemeIssue) {
399
+ issues.push({
400
+ path: `spec.layers.${index}.${prop}`,
401
+ message: schemeIssue,
402
+ });
403
+ }
404
+ const thresholdsIssue = getColorScaleThresholdsIssue(value.type, value.thresholds);
405
+ if (thresholdsIssue) {
406
+ issues.push({
407
+ path: `spec.layers.${index}.${prop}`,
408
+ message: thresholdsIssue,
409
+ });
410
+ }
411
+ }
412
+ }
208
413
  const binding = layer._sqlroomsBinding && typeof layer._sqlroomsBinding === 'object'
209
414
  ? layer._sqlroomsBinding
210
415
  : undefined;
@@ -218,12 +423,190 @@ export function getDeckMapResourceConfigIssues(config, options = {}) {
218
423
  message: `references unknown dataset "${boundDataset}"`,
219
424
  });
220
425
  }
221
- return;
222
426
  }
223
- issues.push({
224
- path: `spec.layers.${index}._sqlroomsBinding.dataset`,
225
- message: 'must bind the layer to a config.datasets entry; layer data references and implicit bindings are not durable resource bindings',
226
- });
427
+ else {
428
+ issues.push({
429
+ path: `spec.layers.${index}._sqlroomsBinding.dataset`,
430
+ message: 'must bind the layer to a config.datasets entry; layer data references and implicit bindings are not durable resource bindings',
431
+ });
432
+ }
433
+ if (layerType === 'GeoArrowHeatmapLayer' && 'getWeight' in layer) {
434
+ // Basic: omit getWeight. Custom: numeric constant only.
435
+ const getWeight = layer.getWeight;
436
+ if (config.configMode !== 'custom') {
437
+ issues.push({
438
+ path: `spec.layers.${index}.getWeight`,
439
+ message: 'omit getWeight for default density — the basic settings panel has no weight-column control; do not bind a column',
440
+ });
441
+ }
442
+ else if (isPlainObject(getWeight) ||
443
+ (typeof getWeight === 'string' && getWeight.trim().length > 0)) {
444
+ issues.push({
445
+ path: `spec.layers.${index}.getWeight`,
446
+ message: 'omit getWeight for default density, or use a numeric constant — column / object accessors are not supported',
447
+ });
448
+ }
449
+ }
450
+ // Basic mode: numeric sizes (elevation may use @@=col / scale).
451
+ if (config.configMode !== 'custom') {
452
+ const rejectNonNumericSize = (prop, value, message) => {
453
+ if (value === undefined)
454
+ return;
455
+ if (typeof value === 'number' && Number.isFinite(value))
456
+ return;
457
+ issues.push({
458
+ path: `spec.layers.${index}.${prop}`,
459
+ message,
460
+ });
461
+ };
462
+ if (layerType === 'GeoArrowScatterplotLayer') {
463
+ rejectNonNumericSize('getRadius', layer.getRadius, 'use a positive number (e.g. 4) with radiusUnits "pixels" — string/object size accessors are not supported in basic mode');
464
+ }
465
+ const LINE_LAYER_TYPES = new Set([
466
+ 'GeoArrowPathLayer',
467
+ 'GeoArrowArcLayer',
468
+ 'GeoArrowTripsLayer',
469
+ ]);
470
+ if (typeof layerType === 'string' && LINE_LAYER_TYPES.has(layerType)) {
471
+ rejectNonNumericSize('getWidth', layer.getWidth, 'use a number (e.g. 2) with widthUnits "pixels" — string/object size accessors are not supported in basic mode');
472
+ }
473
+ if (layerType === 'GeoArrowHeatmapLayer') {
474
+ rejectNonNumericSize('radiusPixels', layer.radiusPixels, 'use a positive number (e.g. 30) — string/object size accessors are not supported in basic mode');
475
+ }
476
+ if (layerType === 'GeoArrowColumnLayer') {
477
+ rejectNonNumericSize('radius', layer.radius, 'use a positive number in meters (e.g. 50) — string/object size accessors are not supported in basic mode');
478
+ }
479
+ const ELEVATION_LAYER_TYPES = new Set([
480
+ 'GeoArrowPolygonLayer',
481
+ 'GeoArrowColumnLayer',
482
+ 'GeoArrowH3HexagonLayer',
483
+ ]);
484
+ if (typeof layerType === 'string' &&
485
+ ELEVATION_LAYER_TYPES.has(layerType) &&
486
+ layer.getElevation !== undefined) {
487
+ const elev = layer.getElevation;
488
+ if (typeof elev === 'string') {
489
+ const isColumnAccessor = /^@@=[A-Za-z_][\w]*$/.test(elev.trim());
490
+ if (!isColumnAccessor) {
491
+ issues.push({
492
+ path: `spec.layers.${index}.getElevation`,
493
+ message: 'in basic mode use a number (0 for flat), a column accessor "@@=columnName", or {"@@function":"scale","field":"...","type":"linear","domain":"auto","range":[0,200]} — free-form string expressions require configMode "custom"',
494
+ });
495
+ }
496
+ }
497
+ else if (isPlainObject(elev)) {
498
+ const fn = elev['@@function'];
499
+ const field = typeof elev.field === 'string' ? elev.field.trim() : '';
500
+ const typeOk = elev.type === undefined ||
501
+ elev.type === 'linear' ||
502
+ (typeof elev.type === 'string' && elev.type.trim() === 'linear');
503
+ if ((fn !== 'scale' && fn !== 'scaleLinear') || !field || !typeOk) {
504
+ issues.push({
505
+ path: `spec.layers.${index}.getElevation`,
506
+ message: 'in basic mode use {"@@function":"scale","field":"...","type":"linear","domain":"auto","range":[0,200]} (or a column accessor "@@=columnName")',
507
+ });
508
+ }
509
+ }
510
+ else if (!(typeof elev === 'number' && Number.isFinite(elev))) {
511
+ issues.push({
512
+ path: `spec.layers.${index}.getElevation`,
513
+ message: 'in basic mode use a number (0 for flat), a column accessor "@@=columnName", or {"@@function":"scale","field":"...","type":"linear","domain":"auto","range":[0,200]}',
514
+ });
515
+ }
516
+ }
517
+ }
518
+ if (layerType === 'GeoArrowH3HexagonLayer') {
519
+ const getHexagon = layer.getHexagon;
520
+ if (isPlainObject(getHexagon)) {
521
+ issues.push({
522
+ path: `spec.layers.${index}.getHexagon`,
523
+ message: 'use getHexagon as a deck.gl attribute string "@@=h3_column_name" (or set _sqlroomsBinding.hexagonColumn) — object accessors like {"@@function":"...","column":"..."} are not valid',
524
+ });
525
+ }
526
+ else {
527
+ const hasHexagonBinding = typeof binding?.hexagonColumn === 'string' &&
528
+ binding.hexagonColumn.trim().length > 0;
529
+ const getHexagonAccessor = typeof getHexagon === 'string' ? getHexagon.trim() : '';
530
+ const hasGetHexagonAccessor = /^@@=[A-Za-z_][\w]*$/.test(getHexagonAccessor);
531
+ if (getHexagonAccessor &&
532
+ !hasGetHexagonAccessor &&
533
+ !hasHexagonBinding) {
534
+ issues.push({
535
+ path: `spec.layers.${index}.getHexagon`,
536
+ message: 'use getHexagon as "@@=h3_column_name" (a simple column accessor) or set _sqlroomsBinding.hexagonColumn — bare names and expressions are not valid',
537
+ });
538
+ }
539
+ else if (!hasGetHexagonAccessor && !hasHexagonBinding) {
540
+ issues.push({
541
+ path: `spec.layers.${index}.getHexagon`,
542
+ message: 'GeoArrowH3HexagonLayer requires getHexagon (e.g. "@@=h3_column_name") or _sqlroomsBinding.hexagonColumn set to the H3 index column',
543
+ });
544
+ }
545
+ }
546
+ }
547
+ if (layerType === 'GeoArrowArcLayer') {
548
+ if (layer.getSourcePosition !== undefined) {
549
+ issues.push({
550
+ path: `spec.layers.${index}.getSourcePosition`,
551
+ message: 'do not set getSourcePosition — bind the source geometry via _sqlroomsBinding.sourceGeometryColumn only',
552
+ });
553
+ }
554
+ if (layer.getTargetPosition !== undefined) {
555
+ issues.push({
556
+ path: `spec.layers.${index}.getTargetPosition`,
557
+ message: 'do not set getTargetPosition — bind the target geometry via _sqlroomsBinding.targetGeometryColumn only',
558
+ });
559
+ }
560
+ const hasSource = typeof binding?.sourceGeometryColumn === 'string' &&
561
+ binding.sourceGeometryColumn.trim().length > 0;
562
+ const hasTarget = typeof binding?.targetGeometryColumn === 'string' &&
563
+ binding.targetGeometryColumn.trim().length > 0;
564
+ if (!hasSource) {
565
+ issues.push({
566
+ path: `spec.layers.${index}._sqlroomsBinding.sourceGeometryColumn`,
567
+ message: 'GeoArrowArcLayer requires _sqlroomsBinding.sourceGeometryColumn set to the source geometry column name',
568
+ });
569
+ }
570
+ if (!hasTarget) {
571
+ issues.push({
572
+ path: `spec.layers.${index}._sqlroomsBinding.targetGeometryColumn`,
573
+ message: 'GeoArrowArcLayer requires _sqlroomsBinding.targetGeometryColumn set to the target geometry column name',
574
+ });
575
+ }
576
+ }
577
+ if (layerType === 'GeoArrowTripsLayer') {
578
+ const hasTimestampColumn = typeof binding?.timestampColumn === 'string' &&
579
+ binding.timestampColumn.trim().length > 0;
580
+ if (!hasTimestampColumn) {
581
+ issues.push({
582
+ path: `spec.layers.${index}._sqlroomsBinding.timestampColumn`,
583
+ message: 'GeoArrowTripsLayer requires _sqlroomsBinding.timestampColumn set to the timestamps list column, e.g. "timestamps"',
584
+ });
585
+ }
586
+ }
587
+ if (layerType === 'GeoArrowTripsLayer' ||
588
+ layerType === 'GeoArrowPathLayer') {
589
+ // LIST(...)/ST_MakeLine needs GROUP BY path/trip id.
590
+ if (boundDataset && config.datasets[boundDataset]) {
591
+ const source = config.datasets[boundDataset]?.source;
592
+ const sql = `${source?.transformSql ?? ''} ${source?.sqlQuery ?? ''}`;
593
+ if (hasBadStMakeLinePointOrderBy(sql)) {
594
+ issues.push({
595
+ path: `datasets.${boundDataset}.source`,
596
+ message: 'ST_MakeLine is a scalar that takes a LIST of points. Use ST_MakeLine(LIST(ST_Point(lon, lat) ORDER BY waypoint_order)) — never ST_MakeLine(ST_Point(...) ORDER BY ...). ORDER BY is only valid inside LIST.',
597
+ });
598
+ }
599
+ const usesListAgg = /\bST_MakeLine\s*\(\s*LIST\s*\(/i.test(sql) ||
600
+ /\bLIST\s*\([^)]*ORDER\s+BY/i.test(sql);
601
+ const hasGroupBy = /\bGROUP\s+BY\b/i.test(sql);
602
+ if (usesListAgg && !hasGroupBy) {
603
+ issues.push({
604
+ path: `datasets.${boundDataset}.source`,
605
+ message: `${layerType} waypoint aggregation using LIST(...)/ST_MakeLine must include GROUP BY the trip/path/route id column so each trip becomes one linestring. Without GROUP BY, waypoints are not split per trip.`,
606
+ });
607
+ }
608
+ }
609
+ }
227
610
  });
228
611
  const fitDataset = config.fitToData?.dataset;
229
612
  if (fitDataset && !datasetIdSet.has(fitDataset)) {
@@ -232,6 +615,13 @@ export function getDeckMapResourceConfigIssues(config, options = {}) {
232
615
  message: `references unknown dataset "${fitDataset}"`,
233
616
  });
234
617
  }
618
+ if (typeof config.mapStyle === 'string' &&
619
+ /^mapbox:/i.test(config.mapStyle.trim())) {
620
+ issues.push({
621
+ path: 'mapStyle',
622
+ message: 'mapbox:// styles are not supported (MapLibre cannot fetch that URL scheme). Omit mapStyle to use the host basemap, or use a token-free MapLibre-compatible https:// style URL',
623
+ });
624
+ }
235
625
  return issues;
236
626
  }
237
627
  /** Rejects invalid durable map writes after sparse patches have been merged. */
@@ -241,26 +631,29 @@ export function assertDeckMapResourceConfig(config) {
241
631
  throw new DeckMapResourceConfigError(issues);
242
632
  }
243
633
  /**
244
- * Returns the package-owned authoring contract for hosts with direct worksheet
634
+ * Returns the package-owned authoring contract for hosts with direct document
245
635
  * map capability. Host adapters opt into it; they do not maintain prompt copies.
246
636
  */
247
637
  export function getDeckMapResourceAiInstructions() {
248
- return `## Direct worksheet Deck map resources
638
+ return `## Direct document Deck map resources
249
639
 
250
- When authoring a worksheet map config, use the resource-native Deck JSON contract:
640
+ When authoring a document map config, use the resource-native Deck JSON contract:
251
641
  - A new map must contain at least one config.datasets entry and at least one spec.layers entry.
252
642
  - Every dataset must define source.tableName, source.tableName plus source.transformSql, or source.sqlQuery. Never put sql directly on the dataset object.
253
643
  - Bind every layer to a dataset with _sqlroomsBinding.dataset. Never use data: "@@#datasetId" or an implicit single-dataset binding as a durable resource binding.
254
- - Use supported Deck JSON layer classes such as GeoArrowScatterplotLayer, GeoArrowHeatmapLayer, GeoArrowPolygonLayer, GeoArrowPathLayer, GeoArrowTripsLayer, GeoArrowArcLayer, GeoArrowColumnLayer, GeoArrowH3HexagonLayer, or GeoJsonLayer.
644
+ - Use supported Deck JSON layer classes such as GeoArrowScatterplotLayer, GeoArrowHeatmapLayer, GeoArrowPolygonLayer, GeoArrowPathLayer, GeoArrowTripsLayer, GeoArrowArcLayer, GeoArrowColumnLayer, GeoArrowH3HexagonLayer, or GeoJsonLayer. Prefer typed GeoArrow* layers when geometry type is known; GeoJsonLayer is valid for table-backed WKB/GeoJSON via _sqlroomsBinding.
645
+ - For a standard table-backed longitude/latitude point map, pass the tool's top-level pointBinding with dataset, longitudeColumn, and latitudeColumn. Do not author transformSql, geometryColumn, geometryEncodingHint, or layer geometryColumn for that dataset; the resource writer generates and aligns canonical WKB point geometry. Set geometryColumn in pointBinding only when the default __sqlrooms_geom name is unsuitable.
646
+ ${getDeckMapSharedAiContractRules()}
255
647
  - For table-backed datasets, also pass the same table through the tool's top-level tableName field. A selected table does not replace the required dataset source.
256
648
  - transformSql must be a single SELECT and must read from __sqlrooms_source. Use source.sqlQuery only for a standalone pinned query.
257
- - Use configMode "basic" for a straightforward single-layer map. Use "custom" only for advanced properties the basic settings cannot represent; custom mode does not relax dataset-source or layer-binding requirements.
258
- - For a point geometry column, prefer GeoArrowScatterplotLayer with dataset.geometryColumn and _sqlroomsBinding.geometryColumn set to the exact geometry column. For longitude/latitude columns, use source.transformSql to produce WKB geometry and bind that output column.
649
+ - Use configMode "basic" for a straightforward single-layer map (including extruded column/polygon maps with one elevation column via "@@=col" or a scale accessor). Use "custom" only for advanced properties the basic settings cannot represent; custom mode disables the settings panel and does not relax dataset-source or layer-binding requirements.
650
+ - Prefer data-driven color when a useful varying column exists: getFillColor (or getColor/getSourceColor/getTargetColor for arcs) with {"@@function":"colorScale","field":"<column>","type":"sequential"|"quantile"|"categorical","scheme":"<name>","domain":"auto"}. Use quantile for skewed numeric, sequential for uniform, categorical for strings. Skip numeric columns where min = max (or all zeros) unless the user names that column; otherwise use flat fill. Exact scheme names (case-sensitive): ${formatColorSchemePromptLists()} — do not invent names. Viridis/Plasma/Inferno require type "sequential"; quantile/quantize schemes must be ColorBrewer ramps (YlOrRd, Blues, …).
259
651
  - For updates, sparse config patches are allowed because they are merged with the existing resource. For creates, never send empty datasets or layers.
260
- - To remove existing layers or datasets, set replaceLayers and/or replaceDatasets to true and send the complete desired list or registry. Omit them for additive sparse updates.
652
+ - When updating only visual properties (color scale, scheme, radius, width, elevation scale, visibility, opacity), send datasets as an empty object {} so the tool schema stays valid and merge keeps the existing dataset registry. Do not omit datasets entirely. Never re-send bare source.tableName without transformSql/sqlQuery — that overwrites geometry SQL and breaks the map. Include real dataset entries only when changing a data source.
653
+ - To remove existing layers or datasets, set replaceLayers and/or replaceDatasets to true and send the complete desired list or registry. Omit them for additive sparse updates. For layer-type switches, set replaceLayers: true with only the new layer — do not keep the old layer as visible: false.
261
654
  - If a map write reports an invalid resource config, repair the reported paths and retry the same direct map operation; do not replace it with a dashboard-backed map.
262
655
 
263
656
  Minimal table-backed point map shape:
264
- {"configMode":"basic","datasets":{"places":{"source":{"tableName":"places"},"geometryColumn":"geom","geometryEncodingHint":"wkb"}},"spec":{"layers":[{"@@type":"GeoArrowScatterplotLayer","id":"places","_sqlroomsBinding":{"dataset":"places","geometryColumn":"geom"},"getRadius":4,"radiusUnits":"pixels","pickable":true}]},"fitToData":{"dataset":"places","geometryColumn":"geom"}}`;
657
+ {"tableName":"places","pointBinding":{"dataset":"places","longitudeColumn":"longitude","latitudeColumn":"latitude"},"config":{"configMode":"basic","datasets":{"places":{"source":{"tableName":"places"}}},"spec":{"layers":[{"@@type":"GeoArrowScatterplotLayer","id":"places","_sqlroomsBinding":{"dataset":"places"},"getRadius":4,"radiusUnits":"pixels","pickable":true}]}}}`;
265
658
  }
266
659
  //# sourceMappingURL=mapResourceAuthoring.js.map