@sqlrooms/deck 0.29.0-rc.8 → 0.29.0-rc.9

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 (107) hide show
  1. package/README.md +31 -6
  2. package/dist/DeckJsonMap.d.ts +2 -2
  3. package/dist/DeckJsonMap.d.ts.map +1 -1
  4. package/dist/DeckJsonMap.js +202 -19
  5. package/dist/DeckJsonMap.js.map +1 -1
  6. package/dist/DeckJsonMapSpec.d.ts +36 -36
  7. package/dist/DeckJsonMapSpec.d.ts.map +1 -1
  8. package/dist/DeckJsonMapSpec.js +1 -0
  9. package/dist/DeckJsonMapSpec.js.map +1 -1
  10. package/dist/MapSettings.d.ts.map +1 -1
  11. package/dist/MapSettings.js +202 -25
  12. package/dist/MapSettings.js.map +1 -1
  13. package/dist/ai.d.ts +55 -8
  14. package/dist/ai.d.ts.map +1 -1
  15. package/dist/ai.js +190 -41
  16. package/dist/ai.js.map +1 -1
  17. package/dist/block.d.ts +20 -0
  18. package/dist/block.d.ts.map +1 -0
  19. package/dist/block.js +119 -0
  20. package/dist/block.js.map +1 -0
  21. package/dist/dashboard.d.ts +2 -8
  22. package/dist/dashboard.d.ts.map +1 -1
  23. package/dist/dashboard.js +194 -289
  24. package/dist/dashboard.js.map +1 -1
  25. package/dist/dashboardConfig.d.ts +11 -3
  26. package/dist/dashboardConfig.d.ts.map +1 -1
  27. package/dist/dashboardConfig.js +82 -7
  28. package/dist/dashboardConfig.js.map +1 -1
  29. package/dist/index.d.ts +4 -3
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/json/aggregateWaypoints.d.ts +14 -0
  34. package/dist/json/aggregateWaypoints.d.ts.map +1 -0
  35. package/dist/json/aggregateWaypoints.js +190 -0
  36. package/dist/json/aggregateWaypoints.js.map +1 -0
  37. package/dist/json/compileColorScale.d.ts +1 -1
  38. package/dist/json/compileColorScale.d.ts.map +1 -1
  39. package/dist/json/compileColorScale.js +42 -20
  40. package/dist/json/compileColorScale.js.map +1 -1
  41. package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
  42. package/dist/json/createDeckJsonConfiguration.js +106 -8
  43. package/dist/json/createDeckJsonConfiguration.js.map +1 -1
  44. package/dist/json/defaultClasses.d.ts +5 -4
  45. package/dist/json/defaultClasses.d.ts.map +1 -1
  46. package/dist/json/defaultClasses.js +9 -4
  47. package/dist/json/defaultClasses.js.map +1 -1
  48. package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
  49. package/dist/json/extractColorScaleLegends.js +43 -5
  50. package/dist/json/extractColorScaleLegends.js.map +1 -1
  51. package/dist/json/heatmapDefaults.d.ts +8 -0
  52. package/dist/json/heatmapDefaults.d.ts.map +1 -0
  53. package/dist/json/heatmapDefaults.js +14 -0
  54. package/dist/json/heatmapDefaults.js.map +1 -0
  55. package/dist/json/layerCompatibility.d.ts.map +1 -1
  56. package/dist/json/layerCompatibility.js +5 -1
  57. package/dist/json/layerCompatibility.js.map +1 -1
  58. package/dist/json/layers/DeckColumnLayer.d.ts +12 -0
  59. package/dist/json/layers/DeckColumnLayer.d.ts.map +1 -0
  60. package/dist/json/layers/DeckColumnLayer.js +26 -0
  61. package/dist/json/layers/DeckColumnLayer.js.map +1 -0
  62. package/dist/json/layers/DeckH3HexagonLayer.d.ts +37 -0
  63. package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -0
  64. package/dist/json/layers/DeckH3HexagonLayer.js +96 -0
  65. package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -0
  66. package/dist/json/layers/DeckTripsLayer.d.ts +45 -0
  67. package/dist/json/layers/DeckTripsLayer.d.ts.map +1 -0
  68. package/dist/json/layers/DeckTripsLayer.js +169 -0
  69. package/dist/json/layers/DeckTripsLayer.js.map +1 -0
  70. package/dist/json/layers/index.d.ts +4 -0
  71. package/dist/json/layers/index.d.ts.map +1 -0
  72. package/dist/json/layers/index.js +4 -0
  73. package/dist/json/layers/index.js.map +1 -0
  74. package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
  75. package/dist/json/rewriteGeoArrowAccessors.js +5 -0
  76. package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
  77. package/dist/mapConfigUtils.d.ts +102 -1
  78. package/dist/mapConfigUtils.d.ts.map +1 -1
  79. package/dist/mapConfigUtils.js +21 -3
  80. package/dist/mapConfigUtils.js.map +1 -1
  81. package/dist/mapLayerConfigUtils.d.ts +15 -0
  82. package/dist/mapLayerConfigUtils.d.ts.map +1 -1
  83. package/dist/mapLayerConfigUtils.js +109 -7
  84. package/dist/mapLayerConfigUtils.js.map +1 -1
  85. package/dist/prepare/detectGeometryColumn.d.ts +10 -1
  86. package/dist/prepare/detectGeometryColumn.d.ts.map +1 -1
  87. package/dist/prepare/detectGeometryColumn.js +48 -6
  88. package/dist/prepare/detectGeometryColumn.js.map +1 -1
  89. package/dist/prepare/wkbDecoder.d.ts.map +1 -1
  90. package/dist/prepare/wkbDecoder.js +88 -1
  91. package/dist/prepare/wkbDecoder.js.map +1 -1
  92. package/dist/prepare/wkbParser.d.ts +5 -0
  93. package/dist/prepare/wkbParser.d.ts.map +1 -1
  94. package/dist/prepare/wkbParser.js +24 -0
  95. package/dist/prepare/wkbParser.js.map +1 -1
  96. package/dist/types.d.ts +14 -4
  97. package/dist/types.d.ts.map +1 -1
  98. package/dist/types.js.map +1 -1
  99. package/dist/useDeckMapDatasets.d.ts +17 -0
  100. package/dist/useDeckMapDatasets.d.ts.map +1 -0
  101. package/dist/useDeckMapDatasets.js +69 -0
  102. package/dist/useDeckMapDatasets.js.map +1 -0
  103. package/dist/useDeckMapFitToBounds.d.ts +29 -0
  104. package/dist/useDeckMapFitToBounds.d.ts.map +1 -0
  105. package/dist/useDeckMapFitToBounds.js +333 -0
  106. package/dist/useDeckMapFitToBounds.js.map +1 -0
  107. package/package.json +7 -6
package/dist/dashboard.js CHANGED
@@ -1,18 +1,116 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { WebMercatorViewport } from '@deck.gl/core';
3
- import { escapeId, getColValAsNumber, quoteTableReference, useStoreWithDuckDb, } from '@sqlrooms/duckdb';
4
- import { column, sql, useMosaicClient, useStoreWithMosaicDashboard, usePanelClientRegistration, usePanelClients, usePanelResetFilters, ResetFiltersButton, } from '@sqlrooms/mosaic';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { column, sql, useMosaicClient, useStoreWithMosaicDashboard, usePanelClientRegistration, } from '@sqlrooms/mosaic';
5
3
  import { Button, Tooltip, TooltipContent, TooltipTrigger } from '@sqlrooms/ui';
6
- import { FocusIcon, MapIcon, SettingsIcon } from 'lucide-react';
4
+ import { AlertTriangleIcon, FocusIcon, MapIcon, SettingsIcon, } from 'lucide-react';
7
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
6
  import { DeckMapConfigPopoverEditor } from './DeckMapConfigPopoverEditor';
9
7
  import { DeckJsonMap } from './DeckJsonMap';
10
8
  import { MapSettingsPanel } from './MapSettings';
11
9
  import { MosaicDashboardPanelLayout } from '@sqlrooms/mosaic';
12
- import { asDeckJsonMapConfig, createDeckMapDashboardDatasetQuery, createDeckMapDashboardDatasets, DECK_MAP_DASHBOARD_PANEL_TYPE, resolveDeckMapDashboardDatasetSource, } from './dashboardConfig';
13
- import { getDeckMapDataPolicy } from './mapDataPolicy';
10
+ import { asDeckJsonMapConfig, createDeckMapDashboardPanelConfig, createDeckMapDashboardDatasetQuery, createDeckMapDashboardDatasets, DECK_MAP_DASHBOARD_PANEL_TYPE, DEFAULT_DECK_MAP_MAX_DATA_POINTS, resolveDeckMapDashboardDatasetSource, } from './dashboardConfig';
11
+ import { useDeckMapFitToBounds, emitDeckMapDashboardFitRequest, createDeckMapBoundsQuery, } from './useDeckMapFitToBounds';
12
+ export { createDeckMapBoundsQuery };
13
+ import { useDeckMapDatasets } from './useDeckMapDatasets';
14
14
  import { createDeckMapDashboardPanelConfigForTable, findGeometryColumn, findLongitudeLatitudeColumns, } from './mapConfigUtils';
15
+ function createEmptyDeckMapDashboardPanelConfig(title = 'Map') {
16
+ return createDeckMapDashboardPanelConfig({
17
+ title,
18
+ spec: {
19
+ initialViewState: { longitude: 0, latitude: 20, zoom: 1.5 },
20
+ layers: [],
21
+ },
22
+ datasets: {},
23
+ });
24
+ }
25
+ /**
26
+ * Extracts column names from common DuckDB "column not found" error messages
27
+ * and layer geometry incompatibility errors.
28
+ * Returns the column names if detected, or null for unrecognized errors.
29
+ */
30
+ function parseMissingColumnsFromError(message) {
31
+ // "Referenced column "X" not found in FROM clause!" — may appear multiple times
32
+ const refMatches = [
33
+ ...message.matchAll(/Referenced column "([^"]+)" not found/gi),
34
+ ];
35
+ if (refMatches.length > 0)
36
+ return refMatches.map((m) => m[1]);
37
+ // "Binder Error: Column "X" does not exist" or "column X not found"
38
+ const colMatch = message.match(/(?:Column|column)\s+"([^"]+)"\s+(?:does not exist|not found)/i);
39
+ if (colMatch)
40
+ return [colMatch[1]];
41
+ // "Geometry column "X" was not found"
42
+ const geomNotFound = message.match(/Geometry column "([^"]+)" was not found/i);
43
+ if (geomNotFound)
44
+ return [geomNotFound[1]];
45
+ return null;
46
+ }
47
+ /**
48
+ * Detects geometry type mismatch errors (e.g. polygon layer on point data).
49
+ */
50
+ function parseGeometryMismatchError(message) {
51
+ const match = message.match(/requires (\w+) geometry .+ but only (\w+) coordinates were found/i);
52
+ if (match)
53
+ return { required: match[1], found: match[2] };
54
+ return null;
55
+ }
56
+ /**
57
+ * Detects columns referenced in layer config that are missing from the loaded dataset.
58
+ */
59
+ function detectMissingColumns(mapConfig, datasetStates) {
60
+ if (!mapConfig?.spec)
61
+ return [];
62
+ const missing = [];
63
+ const spec = mapConfig.spec;
64
+ const layers = Array.isArray(spec.layers) ? spec.layers : [];
65
+ for (const layer of layers) {
66
+ if (!layer || typeof layer !== 'object')
67
+ continue;
68
+ const layerObj = layer;
69
+ const binding = layerObj._sqlroomsBinding;
70
+ const datasetId = binding?.dataset;
71
+ const arrowTable = datasetId
72
+ ? datasetStates[datasetId]?.arrowTable
73
+ : undefined;
74
+ if (!arrowTable)
75
+ continue;
76
+ const fieldNames = new Set(arrowTable.schema.fields.map((f) => f.name.toLowerCase()));
77
+ const check = (col) => {
78
+ if (col && !fieldNames.has(col.toLowerCase())) {
79
+ missing.push(col);
80
+ }
81
+ };
82
+ // Check @@= accessors
83
+ for (const [propName, propValue] of Object.entries(layerObj)) {
84
+ // Skip elevation references when extrusion is disabled
85
+ if (propName === 'getElevation' && !layerObj.extruded)
86
+ continue;
87
+ if (typeof propValue === 'string' && propValue.startsWith('@@=')) {
88
+ check(propValue.slice(3).trim());
89
+ }
90
+ // Check @@function colorScale/scale field references
91
+ if (propValue &&
92
+ typeof propValue === 'object' &&
93
+ '@@function' in propValue) {
94
+ const fn = propValue;
95
+ if (typeof fn.field === 'string') {
96
+ check(fn.field);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ return [...new Set(missing)];
102
+ }
15
103
  function DeckMapRuntimeIssuePanel({ issue }) {
104
+ if (issue.kind === 'sql-error' || issue.kind === 'render-error') {
105
+ const missingColumns = parseMissingColumnsFromError(issue.message);
106
+ if (missingColumns) {
107
+ return (_jsxs("div", { className: "flex h-full min-h-[200px] flex-col items-center justify-center p-4", children: [_jsx("div", { className: "mb-2 text-center font-semibold", children: "The visualization can't be displayed" }), _jsxs("div", { className: "text-center text-sm", children: [_jsx("span", { children: "Selected columns are missing in the dataset: " }), missingColumns.map((col, idx) => (_jsxs("span", { children: [_jsx("span", { className: "inline-flex items-center rounded-md border border-gray-600 bg-gray-800 px-1 py-0.5 text-xs font-medium text-gray-300", children: col }), ' '] }, idx)))] })] }));
108
+ }
109
+ const geomMismatch = parseGeometryMismatchError(issue.message);
110
+ if (geomMismatch) {
111
+ return (_jsxs("div", { className: "flex h-full min-h-[200px] flex-col items-center justify-center p-4", children: [_jsx("div", { className: "mb-2 text-center font-semibold", children: "The visualization can't be displayed" }), _jsxs("div", { className: "text-center text-sm", children: ["This layer requires ", geomMismatch.required, " geometry, but the current dataset only has ", geomMismatch.found, " coordinates."] })] }));
112
+ }
113
+ }
16
114
  const title = issue.kind === 'too-much-data'
17
115
  ? 'Too much data'
18
116
  : issue.kind === 'sql-error'
@@ -20,37 +118,54 @@ function DeckMapRuntimeIssuePanel({ issue }) {
20
118
  : 'Unable to display map';
21
119
  return (_jsxs("div", { className: "flex h-full min-h-[200px] flex-col items-center justify-center p-4", children: [_jsx("div", { className: "mb-2 text-center font-semibold", children: title }), _jsx("div", { className: "text-center text-sm whitespace-pre-wrap", children: issue.message })] }));
22
120
  }
23
- function DeckMapDashboardDatasetClient({ dashboard, dataset, datasetId, dataPolicy, panel, onDatasetState, runtimeIssueContext, runtimeIssueReporter, selectionName, }) {
121
+ function DeckMapDashboardDatasetClient({ dashboard, dataset, datasetId, fitToData, panel, onDatasetState, runtimeIssueContext, runtimeIssueReporter, selectionName, maxRows, }) {
122
+ const datasetFitToData = useMemo(() => (fitToData?.dataset === datasetId ? fitToData : undefined), [datasetId, fitToData]);
24
123
  const source = useMemo(() => resolveDeckMapDashboardDatasetSource({
25
124
  dashboard,
26
125
  panel,
27
126
  dataset,
28
- }), [dashboard, dataset, panel]);
127
+ fitToData: datasetFitToData,
128
+ }), [dashboard, dataset, datasetFitToData, panel]);
29
129
  const query = useCallback((filter) => source
30
- ? createDeckMapDashboardDatasetQuery(source, filter)
31
- : createDeckMapDashboardDatasetQuery({ tableName: '__missing_dashboard_map_dataset__' }, filter), [source]);
130
+ ? createDeckMapDashboardDatasetQuery(source, filter, {
131
+ sampleRows: maxRows,
132
+ })
133
+ : createDeckMapDashboardDatasetQuery({ tableName: '__missing_dashboard_map_dataset__' }, filter, { sampleRows: maxRows }), [maxRows, source]);
134
+ const queryError = useCallback((err) => {
135
+ onDatasetState(datasetId, {
136
+ arrowTable: undefined,
137
+ error: err,
138
+ isLoading: false,
139
+ client: null,
140
+ isSampled: false,
141
+ });
142
+ }, [datasetId, onDatasetState]);
143
+ const sourceKey = source?.tableName ?? dashboard.selectedTable ?? '';
144
+ const sourceQueryKey = source?.sqlQuery ?? '';
32
145
  const { data, error, isLoading, client } = useMosaicClient({
33
- id: `${panel.id}:${datasetId}`,
146
+ id: `${panel.id}:${datasetId}:${sourceKey}:${sourceQueryKey}`,
34
147
  selectionName,
35
148
  query,
36
- dataPolicy,
149
+ queryError,
37
150
  enabled: Boolean(source),
38
151
  runtimeIssueContext,
39
152
  runtimeIssueReporter,
40
153
  });
41
154
  // Register client for panel reset button
42
155
  usePanelClientRegistration(dashboard.id, panel.id, client ? [client] : []);
156
+ const isSampled = Boolean(data && data.numRows >= maxRows);
43
157
  useEffect(() => {
44
158
  onDatasetState(datasetId, {
45
159
  arrowTable: data ?? undefined,
46
160
  error,
47
161
  isLoading,
48
162
  client,
163
+ isSampled,
49
164
  });
50
165
  return () => {
51
166
  onDatasetState(datasetId, undefined);
52
167
  };
53
- }, [client, data, datasetId, error, isLoading, onDatasetState]);
168
+ }, [client, data, datasetId, error, isLoading, isSampled, onDatasetState]);
54
169
  return null;
55
170
  }
56
171
  function createPointRadiusPredicate(interaction, coordinate) {
@@ -72,85 +187,8 @@ function getCoordinate(info) {
72
187
  function isPickedMapFeature(info) {
73
188
  return Boolean(info.picked || info.object || (info.index ?? -1) >= 0);
74
189
  }
75
- export function createDeckMapBoundsQuery(options) {
76
- const { source, fitToData } = options;
77
- const baseSourceSql = source.sqlQuery
78
- ? `SELECT * FROM (${source.sqlQuery}) AS "__sqlrooms_dashboard_map_source"`
79
- : `SELECT * FROM ${quoteTableReference(source.tableName ?? '')}`;
80
- const longitudeColumn = escapeId(fitToData.longitudeColumn);
81
- const latitudeColumn = escapeId(fitToData.latitudeColumn);
82
- return `
83
- SELECT
84
- ST_XMin(extent) AS min_longitude,
85
- ST_YMin(extent) AS min_latitude,
86
- ST_XMax(extent) AS max_longitude,
87
- ST_YMax(extent) AS max_latitude
88
- FROM (
89
- SELECT ST_Extent_Agg(ST_Point(${longitudeColumn}, ${latitudeColumn})) AS extent
90
- FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_points"
91
- WHERE ${longitudeColumn} IS NOT NULL AND ${latitudeColumn} IS NOT NULL
92
- ) AS "__sqlrooms_dashboard_map_extent"
93
- WHERE extent IS NOT NULL
94
- `;
95
- }
96
- function readBoundsFromExtentResult(result) {
97
- const minLongitude = getColValAsNumber(result, 'min_longitude');
98
- const minLatitude = getColValAsNumber(result, 'min_latitude');
99
- const maxLongitude = getColValAsNumber(result, 'max_longitude');
100
- const maxLatitude = getColValAsNumber(result, 'max_latitude');
101
- if (!Number.isFinite(minLongitude) ||
102
- !Number.isFinite(minLatitude) ||
103
- !Number.isFinite(maxLongitude) ||
104
- !Number.isFinite(maxLatitude)) {
105
- return null;
106
- }
107
- return [
108
- [
109
- minLongitude === maxLongitude ? minLongitude - 0.01 : minLongitude,
110
- minLatitude === maxLatitude ? minLatitude - 0.01 : minLatitude,
111
- ],
112
- [
113
- minLongitude === maxLongitude ? maxLongitude + 0.01 : maxLongitude,
114
- minLatitude === maxLatitude ? maxLatitude + 0.01 : maxLatitude,
115
- ],
116
- ];
117
- }
118
- function fitViewStateToBounds(options) {
119
- const { bounds, width, height, padding = 40, maxZoom = 12 } = options;
120
- const viewport = new WebMercatorViewport({
121
- width: Math.max(width, 1),
122
- height: Math.max(height, 1),
123
- });
124
- const fitted = viewport.fitBounds([
125
- [bounds[0][0], bounds[0][1]],
126
- [bounds[1][0], bounds[1][1]],
127
- ], { padding });
128
- return {
129
- longitude: fitted.longitude,
130
- latitude: fitted.latitude,
131
- zoom: Math.min(fitted.zoom, maxZoom),
132
- };
133
- }
134
- const deckMapDashboardFitRequestTarget = new EventTarget();
135
- function emitDeckMapDashboardFitRequest(panelId) {
136
- deckMapDashboardFitRequestTarget.dispatchEvent(new CustomEvent('fit-view', { detail: { panelId } }));
137
- }
138
- function createInitialDeckMapDashboardFitState(key) {
139
- return {
140
- key,
141
- viewState: undefined,
142
- didAutoFit: false,
143
- fitRequestVersion: 0,
144
- handledFitRequestVersion: 0,
145
- };
146
- }
147
- function DeckMapDashboardHeaderActions({ dashboardId, panel, selectionName, }) {
190
+ function DeckMapDashboardHeaderActions({ dashboardId, panel, }) {
148
191
  const updatePanel = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.updatePanel);
149
- const panelClients = usePanelClients(dashboardId, panel.id);
150
- const { hasActiveFilters, reset } = usePanelResetFilters({
151
- panelClients,
152
- selectionName,
153
- });
154
192
  const mapConfig = asDeckJsonMapConfig(panel.config);
155
193
  const canFitView = Boolean(mapConfig?.fitToData);
156
194
  const isSettingsOpen = Boolean(mapConfig?.settingsOpen);
@@ -164,7 +202,7 @@ function DeckMapDashboardHeaderActions({ dashboardId, panel, selectionName, }) {
164
202
  config: { ...panel.config, settingsOpen: !isSettingsOpen },
165
203
  });
166
204
  }, [dashboardId, isSettingsOpen, panel.config, panel.id, updatePanel]);
167
- return (_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsx(ResetFiltersButton, { disabled: !hasActiveFilters, onClick: reset, tooltip: "Reset panel filters" }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "data-[state=active]:bg-accent h-6 w-6", title: "Map settings", onClick: handleToggleSettings, "data-state": isSettingsOpen ? 'active' : 'inactive', children: _jsx(SettingsIcon, { className: "h-3.5 w-3.5" }) }) }), _jsx(TooltipContent, { children: "Map settings" })] }), _jsx(DeckMapConfigPopoverEditor, { value: panel.config, onApply: handleConfigApply }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6", title: canFitView
205
+ return (_jsxs("div", { className: "flex items-center gap-0.5", children: [_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "data-[state=active]:bg-accent h-6 w-6", title: "Map settings", onClick: handleToggleSettings, "data-state": isSettingsOpen ? 'active' : 'inactive', children: _jsx(SettingsIcon, { className: "h-3.5 w-3.5" }) }) }), _jsx(TooltipContent, { children: "Map settings" })] }), _jsx(DeckMapConfigPopoverEditor, { value: panel.config, onApply: handleConfigApply }), _jsx(Button, { variant: "ghost", size: "icon", className: "h-6 w-6", title: canFitView
168
206
  ? 'Fit map view to data'
169
207
  : 'Fit view unavailable for this map', disabled: !canFitView, onClick: () => emitDeckMapDashboardFitRequest(panel.id), children: _jsx(FocusIcon, { className: "h-3.5 w-3.5" }) })] }));
170
208
  }
@@ -173,9 +211,18 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
173
211
  const getSelection = useStoreWithMosaicDashboard((state) => state.mosaic.getSelection);
174
212
  const updatePanel = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.updatePanel);
175
213
  const issue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.getPanelIssue(dashboardId, panel.id));
176
- const reportPanelIssue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.reportPanelIssue);
177
214
  const clearPanelIssue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.clearPanelIssue);
178
- const executeSql = useStoreWithDuckDb((state) => state.db.executeSql);
215
+ // Clear runtime issues when the active table or panel config changes so
216
+ // the map can recover (e.g., after switching tables or AI updating the map).
217
+ useEffect(() => {
218
+ clearPanelIssue(dashboardId, panel.id);
219
+ }, [
220
+ clearPanelIssue,
221
+ dashboard.selectedTable,
222
+ dashboardId,
223
+ panel.config,
224
+ panel.id,
225
+ ]);
179
226
  const isSettingsOpen = Boolean(panel.config.settingsOpen);
180
227
  const handleSettingsOpenChange = useCallback((isOpen) => {
181
228
  updatePanel(dashboardId, panel.id, {
@@ -184,33 +231,10 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
184
231
  }, [dashboardId, panel.config, panel.id, updatePanel]);
185
232
  const selection = useMemo(() => getSelection(selectionName, 'crossfilter'), [getSelection, selectionName]);
186
233
  const containerRef = useRef(null);
187
- const [datasetStates, setDatasetStates] = useState({});
234
+ const deckMapRef = useRef(null);
188
235
  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
189
- const handleDatasetState = useCallback((datasetId, state) => {
190
- setDatasetStates((current) => {
191
- const next = { ...current };
192
- if (state) {
193
- next[datasetId] = state;
194
- }
195
- else {
196
- delete next[datasetId];
197
- }
198
- return next;
199
- });
200
- }, []);
201
- const dataPolicy = useMemo(() => getDeckMapDataPolicy(mapConfig), [mapConfig]);
202
- const runtimeIssueContext = useMemo(() => ({
203
- panelId: panel.id,
204
- chartType: DECK_MAP_DASHBOARD_PANEL_TYPE,
205
- }), [panel.id]);
206
- const runtimeIssueReporter = useMemo(() => ({
207
- reportIssue: (issueToReport) => {
208
- reportPanelIssue(dashboardId, panel.id, issueToReport);
209
- },
210
- clearIssue: () => {
211
- clearPanelIssue(dashboardId, panel.id);
212
- },
213
- }), [clearPanelIssue, dashboardId, panel.id, reportPanelIssue]);
236
+ const [sampledDismissed, setSampledDismissed] = useState(false);
237
+ const { datasetStates, handleDatasetState, runtimeIssueContext, runtimeIssueReporter, handleRenderingError, } = useDeckMapDatasets({ dashboardId, panel });
214
238
  useEffect(() => {
215
239
  const container = containerRef.current;
216
240
  if (!container)
@@ -227,130 +251,14 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
227
251
  return () => {
228
252
  observer.disconnect();
229
253
  };
230
- }, []);
231
- const fitToData = mapConfig?.fitToData ?? null;
232
- const fitToDataSource = useMemo(() => fitToData
233
- ? resolveDeckMapDashboardDatasetSource({
234
- dashboard,
235
- panel,
236
- dataset: mapConfig?.datasets[fitToData.dataset],
237
- })
238
- : undefined, [dashboard, fitToData, mapConfig?.datasets, panel]);
239
- const fitToDataKey = useMemo(() => fitToData && fitToDataSource
240
- ? JSON.stringify({
241
- source: fitToDataSource,
242
- fitToData,
243
- })
244
- : null, [fitToData, fitToDataSource]);
245
- const fitStateKey = useMemo(() => JSON.stringify({ panelId: panel.id, fitToDataKey }), [fitToDataKey, panel.id]);
246
- const [fitState, setFitState] = useState(() => createInitialDeckMapDashboardFitState(fitStateKey));
247
- const activeFitState = fitState.key === fitStateKey
248
- ? fitState
249
- : createInitialDeckMapDashboardFitState(fitStateKey);
250
- const { didAutoFit, fitRequestVersion, handledFitRequestVersion, viewState } = activeFitState;
251
- useEffect(() => {
252
- const handleFitRequest = (event) => {
253
- const detail = event.detail;
254
- if (detail?.panelId === panel.id) {
255
- setFitState((current) => {
256
- const scoped = current.key === fitStateKey
257
- ? current
258
- : createInitialDeckMapDashboardFitState(fitStateKey);
259
- return {
260
- ...scoped,
261
- fitRequestVersion: scoped.fitRequestVersion + 1,
262
- };
263
- });
264
- }
265
- };
266
- deckMapDashboardFitRequestTarget.addEventListener('fit-view', handleFitRequest);
267
- return () => {
268
- deckMapDashboardFitRequestTarget.removeEventListener('fit-view', handleFitRequest);
269
- };
270
- }, [fitStateKey, panel.id]);
271
- useEffect(() => {
272
- const hasManualFitRequest = fitRequestVersion > handledFitRequestVersion;
273
- if (!fitToData ||
274
- !fitToDataSource ||
275
- containerSize.width <= 0 ||
276
- containerSize.height <= 0 ||
277
- (!hasManualFitRequest && didAutoFit)) {
278
- return;
279
- }
280
- let isCancelled = false;
281
- const fitToDataBounds = async () => {
282
- try {
283
- const handle = await executeSql(createDeckMapBoundsQuery({
284
- source: fitToDataSource,
285
- fitToData,
286
- }));
287
- const result = handle ? await handle : null;
288
- if (isCancelled || !result) {
289
- return;
290
- }
291
- const bounds = readBoundsFromExtentResult(result);
292
- if (!bounds) {
293
- setFitState((current) => {
294
- const scoped = current.key === fitStateKey
295
- ? current
296
- : createInitialDeckMapDashboardFitState(fitStateKey);
297
- return {
298
- ...scoped,
299
- didAutoFit: true,
300
- handledFitRequestVersion: fitRequestVersion,
301
- };
302
- });
303
- return;
304
- }
305
- const nextViewState = fitViewStateToBounds({
306
- bounds,
307
- width: containerSize.width,
308
- height: containerSize.height,
309
- padding: fitToData.padding,
310
- maxZoom: fitToData.maxZoom,
311
- });
312
- setFitState((current) => {
313
- const scoped = current.key === fitStateKey
314
- ? current
315
- : createInitialDeckMapDashboardFitState(fitStateKey);
316
- return {
317
- ...scoped,
318
- viewState: nextViewState,
319
- didAutoFit: true,
320
- handledFitRequestVersion: fitRequestVersion,
321
- };
322
- });
323
- }
324
- catch {
325
- if (!isCancelled) {
326
- setFitState((current) => {
327
- const scoped = current.key === fitStateKey
328
- ? current
329
- : createInitialDeckMapDashboardFitState(fitStateKey);
330
- return {
331
- ...scoped,
332
- didAutoFit: true,
333
- handledFitRequestVersion: fitRequestVersion,
334
- };
335
- });
336
- }
337
- }
338
- };
339
- void fitToDataBounds();
340
- return () => {
341
- isCancelled = true;
342
- };
343
- }, [
344
- containerSize.height,
345
- containerSize.width,
346
- didAutoFit,
347
- executeSql,
348
- fitRequestVersion,
349
- fitStateKey,
350
- fitToData,
351
- fitToDataSource,
352
- handledFitRequestVersion,
353
- ]);
254
+ }, [issue]);
255
+ const { fitToData } = useDeckMapFitToBounds({
256
+ panelId: panel.id,
257
+ dashboard,
258
+ panel,
259
+ containerSize,
260
+ deckMapRef,
261
+ });
354
262
  const handleBrushEvent = useCallback((info) => {
355
263
  const interaction = mapConfig?.interaction;
356
264
  if (!interaction) {
@@ -376,41 +284,35 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
376
284
  predicate: createPointRadiusPredicate(interaction, coordinate),
377
285
  });
378
286
  }, [datasetStates, mapConfig?.interaction, selection]);
379
- const handleViewStateChange = useCallback(({ viewState: nextViewState, interactionState, }) => {
380
- const hasUserInteraction = Boolean(interactionState &&
381
- ['isDragging', 'isPanning', 'isRotating', 'isZooming'].some((key) => Boolean(interactionState[key])));
382
- if (fitToData &&
383
- !didAutoFit &&
384
- viewState === null &&
385
- !hasUserInteraction) {
386
- return;
387
- }
388
- setFitState((current) => {
389
- const scoped = current.key === fitStateKey
390
- ? current
391
- : createInitialDeckMapDashboardFitState(fitStateKey);
392
- return {
393
- ...scoped,
394
- viewState: nextViewState,
395
- didAutoFit: hasUserInteraction || scoped.didAutoFit || !fitToData,
396
- };
397
- });
398
- }, [didAutoFit, fitStateKey, fitToData, viewState]);
399
287
  const settingsContent = (_jsx(MapSettingsPanel, { dashboardId: dashboardId, panel: panel, onClose: () => handleSettingsOpenChange(false) }));
400
- const mapContent = !mapConfig ? (_jsx("div", { className: "text-muted-foreground flex h-full items-center justify-center p-4 text-sm", children: "Invalid map panel config." })) : issue ? (_jsx(DeckMapRuntimeIssuePanel, { issue: issue })) : Object.entries(mapConfig.datasets).length === 0 ? (_jsx("div", { className: "text-muted-foreground flex h-full items-center justify-center p-4 text-sm", children: "Map panels require at least one dataset." })) : (_jsxs("div", { ref: containerRef, className: "relative h-full w-full", children: [Object.entries(mapConfig.datasets).map(([datasetId, dataset]) => (_jsx(DeckMapDashboardDatasetClient, { dashboard: dashboard, dataset: dataset, datasetId: datasetId, dataPolicy: dataPolicy, panel: panel, onDatasetState: handleDatasetState, runtimeIssueContext: runtimeIssueContext, runtimeIssueReporter: runtimeIssueReporter, selectionName: selectionName }, datasetId))), Object.entries(datasetStates)
401
- .filter(([, state]) => state.error)
402
- .map(([datasetId, state]) => (_jsxs("div", { className: "bg-background/90 text-destructive absolute inset-x-4 top-4 z-10 rounded-md border p-3 text-sm shadow", children: ["Failed to load dataset \"", datasetId, "\":", ' ', state.error?.message] }, datasetId))), _jsx(DeckJsonMap, { className: "h-full w-full", spec: mapConfig.spec, datasets: mapConfig
403
- ? createDeckMapDashboardDatasets(mapConfig, datasetStates)
404
- : {}, mapStyle: mapConfig.mapStyle, mapProps: mapConfig.mapProps, showLegends: mapConfig.showLegends, deckProps: {
405
- controller: true,
406
- ...(viewState ? { viewState } : {}),
407
- onViewStateChange: handleViewStateChange,
408
- ...(mapConfig.interaction
409
- ? (mapConfig.interaction.event ?? 'hover') === 'click'
410
- ? { onClick: handleBrushEvent }
411
- : { onHover: handleBrushEvent }
412
- : {}),
413
- } })] }));
288
+ const datasetError = useMemo(() => {
289
+ for (const state of Object.values(datasetStates)) {
290
+ if (state.error)
291
+ return state.error;
292
+ }
293
+ return undefined;
294
+ }, [datasetStates]);
295
+ const missingColumns = useMemo(() => detectMissingColumns(mapConfig ?? undefined, datasetStates), [mapConfig, datasetStates]);
296
+ const mapContent = !mapConfig ? (_jsx("div", { className: "text-muted-foreground flex h-full items-center justify-center p-4 text-sm", children: "Invalid map panel config." })) : Object.entries(mapConfig.datasets).length === 0 ? (_jsx("div", { className: "text-muted-foreground flex h-full items-center justify-center p-4 text-sm", children: "Map panels require at least one dataset." })) : (_jsxs(_Fragment, { children: [Object.entries(mapConfig.datasets).map(([datasetId, dataset]) => (_jsx(DeckMapDashboardDatasetClient, { dashboard: dashboard, dataset: dataset, datasetId: datasetId, fitToData: fitToData ?? undefined, panel: panel, onDatasetState: handleDatasetState, runtimeIssueContext: runtimeIssueContext, runtimeIssueReporter: runtimeIssueReporter, selectionName: selectionName, maxRows: DEFAULT_DECK_MAP_MAX_DATA_POINTS }, datasetId))), issue ? (_jsx(DeckMapRuntimeIssuePanel, { issue: issue })) : datasetError ? (_jsx(DeckMapRuntimeIssuePanel, { issue: {
297
+ kind: 'sql-error',
298
+ panelId: panel.id,
299
+ chartType: DECK_MAP_DASHBOARD_PANEL_TYPE,
300
+ message: datasetError.message,
301
+ recoverable: true,
302
+ } })) : (_jsxs("div", { ref: containerRef, className: "relative h-full w-full", children: [Object.values(datasetStates).some((s) => s.isSampled) &&
303
+ !sampledDismissed && (_jsxs("div", { className: "bg-background/80 text-muted-foreground absolute top-2 left-2 z-10 flex items-center gap-1.5 rounded px-2 py-1 text-xs shadow", children: [_jsxs("span", { children: ["Data sampled to", ' ', DEFAULT_DECK_MAP_MAX_DATA_POINTS.toLocaleString(), " rows"] }), _jsx("button", { className: "text-muted-foreground/60 hover:text-foreground -mr-0.5 ml-0.5", onClick: () => setSampledDismissed(true), children: "\u00D7" })] })), missingColumns.length > 0 && (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { className: `absolute left-2 z-10 flex h-5 w-5 items-center justify-center ${Object.values(datasetStates).some((s) => s.isSampled) &&
304
+ !sampledDismissed
305
+ ? 'top-9'
306
+ : 'top-2'}`, onClick: () => handleSettingsOpenChange(true), children: _jsx(AlertTriangleIcon, { className: "h-4 w-4 text-amber-500 drop-shadow-sm" }) }) }), _jsxs(TooltipContent, { side: "right", children: [_jsx("p", { className: "text-xs font-medium", children: "Missing columns:" }), _jsx("p", { className: "text-xs", children: missingColumns.join(', ') })] })] })), _jsx(DeckJsonMap, { ref: deckMapRef, className: "h-full w-full", spec: mapConfig.spec, datasets: mapConfig
307
+ ? createDeckMapDashboardDatasets(mapConfig, datasetStates)
308
+ : {}, mapStyle: mapConfig.mapStyle, mapProps: mapConfig.mapProps, showLegends: mapConfig.showLegends, onRenderingError: handleRenderingError, deckProps: {
309
+ controller: true,
310
+ ...(mapConfig.interaction
311
+ ? (mapConfig.interaction.event ?? 'hover') === 'click'
312
+ ? { onClick: handleBrushEvent }
313
+ : { onHover: handleBrushEvent }
314
+ : {}),
315
+ } })] }))] }));
414
316
  return (_jsx("div", { className: "h-full min-h-0", children: _jsx(MosaicDashboardPanelLayout, { isOpen: isSettingsOpen, onIsOpenChange: handleSettingsOpenChange, settings: settingsContent, content: mapContent }) }));
415
317
  }
416
318
  export const deckMapDashboardPanelRenderer = {
@@ -422,15 +324,18 @@ export const deckMapDashboardAddPanelAction = {
422
324
  type: DECK_MAP_DASHBOARD_PANEL_TYPE,
423
325
  label: 'Map',
424
326
  icon: MapIcon,
425
- isEnabled: ({ selectedTable }) => Boolean(findLongitudeLatitudeColumns(selectedTable) ??
426
- findGeometryColumn(selectedTable)),
427
- createPanel: ({ selectedTable }) => selectedTable
428
- ? createDeckMapDashboardPanelConfigForTable({
429
- title: `${selectedTable.tableName} map`,
430
- tableName: selectedTable.tableName,
431
- columns: selectedTable.columns,
432
- tableReference: selectedTable.table,
433
- })
434
- : undefined,
327
+ createPanel: ({ selectedTable }) => {
328
+ if (selectedTable &&
329
+ (findLongitudeLatitudeColumns(selectedTable) ??
330
+ findGeometryColumn(selectedTable))) {
331
+ return createDeckMapDashboardPanelConfigForTable({
332
+ title: `${selectedTable.tableName} map`,
333
+ tableName: selectedTable.tableName,
334
+ columns: selectedTable.columns,
335
+ tableReference: selectedTable.table,
336
+ });
337
+ }
338
+ return createEmptyDeckMapDashboardPanelConfig(selectedTable ? `${selectedTable.tableName} map` : 'Map');
339
+ },
435
340
  };
436
341
  //# sourceMappingURL=dashboard.js.map