@sqlrooms/deck 0.29.0-rc.7 → 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 (108) hide show
  1. package/LICENSE.md +2 -1
  2. package/README.md +31 -6
  3. package/dist/DeckJsonMap.d.ts +2 -2
  4. package/dist/DeckJsonMap.d.ts.map +1 -1
  5. package/dist/DeckJsonMap.js +204 -17
  6. package/dist/DeckJsonMap.js.map +1 -1
  7. package/dist/DeckJsonMapSpec.d.ts +36 -36
  8. package/dist/DeckJsonMapSpec.d.ts.map +1 -1
  9. package/dist/DeckJsonMapSpec.js +1 -0
  10. package/dist/DeckJsonMapSpec.js.map +1 -1
  11. package/dist/MapSettings.d.ts.map +1 -1
  12. package/dist/MapSettings.js +203 -25
  13. package/dist/MapSettings.js.map +1 -1
  14. package/dist/ai.d.ts +55 -8
  15. package/dist/ai.d.ts.map +1 -1
  16. package/dist/ai.js +190 -41
  17. package/dist/ai.js.map +1 -1
  18. package/dist/block.d.ts +20 -0
  19. package/dist/block.d.ts.map +1 -0
  20. package/dist/block.js +119 -0
  21. package/dist/block.js.map +1 -0
  22. package/dist/dashboard.d.ts +2 -0
  23. package/dist/dashboard.d.ts.map +1 -1
  24. package/dist/dashboard.js +194 -285
  25. package/dist/dashboard.js.map +1 -1
  26. package/dist/dashboardConfig.d.ts +11 -3
  27. package/dist/dashboardConfig.d.ts.map +1 -1
  28. package/dist/dashboardConfig.js +82 -7
  29. package/dist/dashboardConfig.js.map +1 -1
  30. package/dist/index.d.ts +4 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/json/aggregateWaypoints.d.ts +14 -0
  35. package/dist/json/aggregateWaypoints.d.ts.map +1 -0
  36. package/dist/json/aggregateWaypoints.js +190 -0
  37. package/dist/json/aggregateWaypoints.js.map +1 -0
  38. package/dist/json/compileColorScale.d.ts +1 -1
  39. package/dist/json/compileColorScale.d.ts.map +1 -1
  40. package/dist/json/compileColorScale.js +42 -20
  41. package/dist/json/compileColorScale.js.map +1 -1
  42. package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
  43. package/dist/json/createDeckJsonConfiguration.js +106 -8
  44. package/dist/json/createDeckJsonConfiguration.js.map +1 -1
  45. package/dist/json/defaultClasses.d.ts +5 -4
  46. package/dist/json/defaultClasses.d.ts.map +1 -1
  47. package/dist/json/defaultClasses.js +9 -4
  48. package/dist/json/defaultClasses.js.map +1 -1
  49. package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
  50. package/dist/json/extractColorScaleLegends.js +43 -5
  51. package/dist/json/extractColorScaleLegends.js.map +1 -1
  52. package/dist/json/heatmapDefaults.d.ts +8 -0
  53. package/dist/json/heatmapDefaults.d.ts.map +1 -0
  54. package/dist/json/heatmapDefaults.js +14 -0
  55. package/dist/json/heatmapDefaults.js.map +1 -0
  56. package/dist/json/layerCompatibility.d.ts.map +1 -1
  57. package/dist/json/layerCompatibility.js +5 -1
  58. package/dist/json/layerCompatibility.js.map +1 -1
  59. package/dist/json/layers/DeckColumnLayer.d.ts +12 -0
  60. package/dist/json/layers/DeckColumnLayer.d.ts.map +1 -0
  61. package/dist/json/layers/DeckColumnLayer.js +26 -0
  62. package/dist/json/layers/DeckColumnLayer.js.map +1 -0
  63. package/dist/json/layers/DeckH3HexagonLayer.d.ts +37 -0
  64. package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -0
  65. package/dist/json/layers/DeckH3HexagonLayer.js +96 -0
  66. package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -0
  67. package/dist/json/layers/DeckTripsLayer.d.ts +45 -0
  68. package/dist/json/layers/DeckTripsLayer.d.ts.map +1 -0
  69. package/dist/json/layers/DeckTripsLayer.js +169 -0
  70. package/dist/json/layers/DeckTripsLayer.js.map +1 -0
  71. package/dist/json/layers/index.d.ts +4 -0
  72. package/dist/json/layers/index.d.ts.map +1 -0
  73. package/dist/json/layers/index.js +4 -0
  74. package/dist/json/layers/index.js.map +1 -0
  75. package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
  76. package/dist/json/rewriteGeoArrowAccessors.js +5 -0
  77. package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
  78. package/dist/mapConfigUtils.d.ts +102 -1
  79. package/dist/mapConfigUtils.d.ts.map +1 -1
  80. package/dist/mapConfigUtils.js +21 -3
  81. package/dist/mapConfigUtils.js.map +1 -1
  82. package/dist/mapLayerConfigUtils.d.ts +15 -0
  83. package/dist/mapLayerConfigUtils.d.ts.map +1 -1
  84. package/dist/mapLayerConfigUtils.js +109 -7
  85. package/dist/mapLayerConfigUtils.js.map +1 -1
  86. package/dist/prepare/detectGeometryColumn.d.ts +10 -1
  87. package/dist/prepare/detectGeometryColumn.d.ts.map +1 -1
  88. package/dist/prepare/detectGeometryColumn.js +48 -6
  89. package/dist/prepare/detectGeometryColumn.js.map +1 -1
  90. package/dist/prepare/wkbDecoder.d.ts.map +1 -1
  91. package/dist/prepare/wkbDecoder.js +88 -1
  92. package/dist/prepare/wkbDecoder.js.map +1 -1
  93. package/dist/prepare/wkbParser.d.ts +5 -0
  94. package/dist/prepare/wkbParser.d.ts.map +1 -1
  95. package/dist/prepare/wkbParser.js +24 -0
  96. package/dist/prepare/wkbParser.js.map +1 -1
  97. package/dist/types.d.ts +14 -4
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js.map +1 -1
  100. package/dist/useDeckMapDatasets.d.ts +17 -0
  101. package/dist/useDeckMapDatasets.d.ts.map +1 -0
  102. package/dist/useDeckMapDatasets.js +69 -0
  103. package/dist/useDeckMapDatasets.js.map +1 -0
  104. package/dist/useDeckMapFitToBounds.d.ts +29 -0
  105. package/dist/useDeckMapFitToBounds.d.ts.map +1 -0
  106. package/dist/useDeckMapFitToBounds.js +333 -0
  107. package/dist/useDeckMapFitToBounds.js.map +1 -0
  108. 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, useStoreWithDuckDb, } from '@sqlrooms/duckdb';
4
- import { column, sql, useMosaicClient, useStoreWithMosaicDashboard, } 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,35 +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
  });
154
+ // Register client for panel reset button
155
+ usePanelClientRegistration(dashboard.id, panel.id, client ? [client] : []);
156
+ const isSampled = Boolean(data && data.numRows >= maxRows);
41
157
  useEffect(() => {
42
158
  onDatasetState(datasetId, {
43
159
  arrowTable: data ?? undefined,
44
160
  error,
45
161
  isLoading,
46
162
  client,
163
+ isSampled,
47
164
  });
48
165
  return () => {
49
166
  onDatasetState(datasetId, undefined);
50
167
  };
51
- }, [client, data, datasetId, error, isLoading, onDatasetState]);
168
+ }, [client, data, datasetId, error, isLoading, isSampled, onDatasetState]);
52
169
  return null;
53
170
  }
54
171
  function createPointRadiusPredicate(interaction, coordinate) {
@@ -70,81 +187,6 @@ function getCoordinate(info) {
70
187
  function isPickedMapFeature(info) {
71
188
  return Boolean(info.picked || info.object || (info.index ?? -1) >= 0);
72
189
  }
73
- function createDeckMapBoundsQuery(options) {
74
- const { source, fitToData } = options;
75
- const baseSourceSql = source.sqlQuery
76
- ? `SELECT * FROM (${source.sqlQuery}) AS "__sqlrooms_dashboard_map_source"`
77
- : `SELECT * FROM ${(source.tableName ?? '')
78
- .split('.')
79
- .map(escapeId)
80
- .join('.')}`;
81
- const longitudeColumn = escapeId(fitToData.longitudeColumn);
82
- const latitudeColumn = escapeId(fitToData.latitudeColumn);
83
- return `
84
- SELECT
85
- ST_XMin(extent) AS min_longitude,
86
- ST_YMin(extent) AS min_latitude,
87
- ST_XMax(extent) AS max_longitude,
88
- ST_YMax(extent) AS max_latitude
89
- FROM (
90
- SELECT ST_Extent_Agg(ST_Point(${longitudeColumn}, ${latitudeColumn})) AS extent
91
- FROM (${baseSourceSql}) AS "__sqlrooms_dashboard_map_points"
92
- WHERE ${longitudeColumn} IS NOT NULL AND ${latitudeColumn} IS NOT NULL
93
- ) AS "__sqlrooms_dashboard_map_extent"
94
- WHERE extent IS NOT NULL
95
- `;
96
- }
97
- function readBoundsFromExtentResult(result) {
98
- const minLongitude = getColValAsNumber(result, 'min_longitude');
99
- const minLatitude = getColValAsNumber(result, 'min_latitude');
100
- const maxLongitude = getColValAsNumber(result, 'max_longitude');
101
- const maxLatitude = getColValAsNumber(result, 'max_latitude');
102
- if (!Number.isFinite(minLongitude) ||
103
- !Number.isFinite(minLatitude) ||
104
- !Number.isFinite(maxLongitude) ||
105
- !Number.isFinite(maxLatitude)) {
106
- return null;
107
- }
108
- return [
109
- [
110
- minLongitude === maxLongitude ? minLongitude - 0.01 : minLongitude,
111
- minLatitude === maxLatitude ? minLatitude - 0.01 : minLatitude,
112
- ],
113
- [
114
- minLongitude === maxLongitude ? maxLongitude + 0.01 : maxLongitude,
115
- minLatitude === maxLatitude ? maxLatitude + 0.01 : maxLatitude,
116
- ],
117
- ];
118
- }
119
- function fitViewStateToBounds(options) {
120
- const { bounds, width, height, padding = 40, maxZoom = 12 } = options;
121
- const viewport = new WebMercatorViewport({
122
- width: Math.max(width, 1),
123
- height: Math.max(height, 1),
124
- });
125
- const fitted = viewport.fitBounds([
126
- [bounds[0][0], bounds[0][1]],
127
- [bounds[1][0], bounds[1][1]],
128
- ], { padding });
129
- return {
130
- longitude: fitted.longitude,
131
- latitude: fitted.latitude,
132
- zoom: Math.min(fitted.zoom, maxZoom),
133
- };
134
- }
135
- const deckMapDashboardFitRequestTarget = new EventTarget();
136
- function emitDeckMapDashboardFitRequest(panelId) {
137
- deckMapDashboardFitRequestTarget.dispatchEvent(new CustomEvent('fit-view', { detail: { panelId } }));
138
- }
139
- function createInitialDeckMapDashboardFitState(key) {
140
- return {
141
- key,
142
- viewState: undefined,
143
- didAutoFit: false,
144
- fitRequestVersion: 0,
145
- handledFitRequestVersion: 0,
146
- };
147
- }
148
190
  function DeckMapDashboardHeaderActions({ dashboardId, panel, }) {
149
191
  const updatePanel = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.updatePanel);
150
192
  const mapConfig = asDeckJsonMapConfig(panel.config);
@@ -169,9 +211,18 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
169
211
  const getSelection = useStoreWithMosaicDashboard((state) => state.mosaic.getSelection);
170
212
  const updatePanel = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.updatePanel);
171
213
  const issue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.getPanelIssue(dashboardId, panel.id));
172
- const reportPanelIssue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.reportPanelIssue);
173
214
  const clearPanelIssue = useStoreWithMosaicDashboard((state) => state.mosaicDashboard.clearPanelIssue);
174
- 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
+ ]);
175
226
  const isSettingsOpen = Boolean(panel.config.settingsOpen);
176
227
  const handleSettingsOpenChange = useCallback((isOpen) => {
177
228
  updatePanel(dashboardId, panel.id, {
@@ -180,33 +231,10 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
180
231
  }, [dashboardId, panel.config, panel.id, updatePanel]);
181
232
  const selection = useMemo(() => getSelection(selectionName, 'crossfilter'), [getSelection, selectionName]);
182
233
  const containerRef = useRef(null);
183
- const [datasetStates, setDatasetStates] = useState({});
234
+ const deckMapRef = useRef(null);
184
235
  const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
185
- const handleDatasetState = useCallback((datasetId, state) => {
186
- setDatasetStates((current) => {
187
- const next = { ...current };
188
- if (state) {
189
- next[datasetId] = state;
190
- }
191
- else {
192
- delete next[datasetId];
193
- }
194
- return next;
195
- });
196
- }, []);
197
- const dataPolicy = useMemo(() => getDeckMapDataPolicy(mapConfig), [mapConfig]);
198
- const runtimeIssueContext = useMemo(() => ({
199
- panelId: panel.id,
200
- chartType: DECK_MAP_DASHBOARD_PANEL_TYPE,
201
- }), [panel.id]);
202
- const runtimeIssueReporter = useMemo(() => ({
203
- reportIssue: (issueToReport) => {
204
- reportPanelIssue(dashboardId, panel.id, issueToReport);
205
- },
206
- clearIssue: () => {
207
- clearPanelIssue(dashboardId, panel.id);
208
- },
209
- }), [clearPanelIssue, dashboardId, panel.id, reportPanelIssue]);
236
+ const [sampledDismissed, setSampledDismissed] = useState(false);
237
+ const { datasetStates, handleDatasetState, runtimeIssueContext, runtimeIssueReporter, handleRenderingError, } = useDeckMapDatasets({ dashboardId, panel });
210
238
  useEffect(() => {
211
239
  const container = containerRef.current;
212
240
  if (!container)
@@ -223,130 +251,14 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
223
251
  return () => {
224
252
  observer.disconnect();
225
253
  };
226
- }, []);
227
- const fitToData = mapConfig?.fitToData ?? null;
228
- const fitToDataSource = useMemo(() => fitToData
229
- ? resolveDeckMapDashboardDatasetSource({
230
- dashboard,
231
- panel,
232
- dataset: mapConfig?.datasets[fitToData.dataset],
233
- })
234
- : undefined, [dashboard, fitToData, mapConfig?.datasets, panel]);
235
- const fitToDataKey = useMemo(() => fitToData && fitToDataSource
236
- ? JSON.stringify({
237
- source: fitToDataSource,
238
- fitToData,
239
- })
240
- : null, [fitToData, fitToDataSource]);
241
- const fitStateKey = useMemo(() => JSON.stringify({ panelId: panel.id, fitToDataKey }), [fitToDataKey, panel.id]);
242
- const [fitState, setFitState] = useState(() => createInitialDeckMapDashboardFitState(fitStateKey));
243
- const activeFitState = fitState.key === fitStateKey
244
- ? fitState
245
- : createInitialDeckMapDashboardFitState(fitStateKey);
246
- const { didAutoFit, fitRequestVersion, handledFitRequestVersion, viewState } = activeFitState;
247
- useEffect(() => {
248
- const handleFitRequest = (event) => {
249
- const detail = event.detail;
250
- if (detail?.panelId === panel.id) {
251
- setFitState((current) => {
252
- const scoped = current.key === fitStateKey
253
- ? current
254
- : createInitialDeckMapDashboardFitState(fitStateKey);
255
- return {
256
- ...scoped,
257
- fitRequestVersion: scoped.fitRequestVersion + 1,
258
- };
259
- });
260
- }
261
- };
262
- deckMapDashboardFitRequestTarget.addEventListener('fit-view', handleFitRequest);
263
- return () => {
264
- deckMapDashboardFitRequestTarget.removeEventListener('fit-view', handleFitRequest);
265
- };
266
- }, [fitStateKey, panel.id]);
267
- useEffect(() => {
268
- const hasManualFitRequest = fitRequestVersion > handledFitRequestVersion;
269
- if (!fitToData ||
270
- !fitToDataSource ||
271
- containerSize.width <= 0 ||
272
- containerSize.height <= 0 ||
273
- (!hasManualFitRequest && didAutoFit)) {
274
- return;
275
- }
276
- let isCancelled = false;
277
- const fitToDataBounds = async () => {
278
- try {
279
- const handle = await executeSql(createDeckMapBoundsQuery({
280
- source: fitToDataSource,
281
- fitToData,
282
- }));
283
- const result = handle ? await handle : null;
284
- if (isCancelled || !result) {
285
- return;
286
- }
287
- const bounds = readBoundsFromExtentResult(result);
288
- if (!bounds) {
289
- setFitState((current) => {
290
- const scoped = current.key === fitStateKey
291
- ? current
292
- : createInitialDeckMapDashboardFitState(fitStateKey);
293
- return {
294
- ...scoped,
295
- didAutoFit: true,
296
- handledFitRequestVersion: fitRequestVersion,
297
- };
298
- });
299
- return;
300
- }
301
- const nextViewState = fitViewStateToBounds({
302
- bounds,
303
- width: containerSize.width,
304
- height: containerSize.height,
305
- padding: fitToData.padding,
306
- maxZoom: fitToData.maxZoom,
307
- });
308
- setFitState((current) => {
309
- const scoped = current.key === fitStateKey
310
- ? current
311
- : createInitialDeckMapDashboardFitState(fitStateKey);
312
- return {
313
- ...scoped,
314
- viewState: nextViewState,
315
- didAutoFit: true,
316
- handledFitRequestVersion: fitRequestVersion,
317
- };
318
- });
319
- }
320
- catch {
321
- if (!isCancelled) {
322
- setFitState((current) => {
323
- const scoped = current.key === fitStateKey
324
- ? current
325
- : createInitialDeckMapDashboardFitState(fitStateKey);
326
- return {
327
- ...scoped,
328
- didAutoFit: true,
329
- handledFitRequestVersion: fitRequestVersion,
330
- };
331
- });
332
- }
333
- }
334
- };
335
- void fitToDataBounds();
336
- return () => {
337
- isCancelled = true;
338
- };
339
- }, [
340
- containerSize.height,
341
- containerSize.width,
342
- didAutoFit,
343
- executeSql,
344
- fitRequestVersion,
345
- fitStateKey,
346
- fitToData,
347
- fitToDataSource,
348
- handledFitRequestVersion,
349
- ]);
254
+ }, [issue]);
255
+ const { fitToData } = useDeckMapFitToBounds({
256
+ panelId: panel.id,
257
+ dashboard,
258
+ panel,
259
+ containerSize,
260
+ deckMapRef,
261
+ });
350
262
  const handleBrushEvent = useCallback((info) => {
351
263
  const interaction = mapConfig?.interaction;
352
264
  if (!interaction) {
@@ -372,41 +284,35 @@ function DeckMapDashboardRenderer({ dashboard, dashboardId, panel, selectionName
372
284
  predicate: createPointRadiusPredicate(interaction, coordinate),
373
285
  });
374
286
  }, [datasetStates, mapConfig?.interaction, selection]);
375
- const handleViewStateChange = useCallback(({ viewState: nextViewState, interactionState, }) => {
376
- const hasUserInteraction = Boolean(interactionState &&
377
- ['isDragging', 'isPanning', 'isRotating', 'isZooming'].some((key) => Boolean(interactionState[key])));
378
- if (fitToData &&
379
- !didAutoFit &&
380
- viewState === null &&
381
- !hasUserInteraction) {
382
- return;
383
- }
384
- setFitState((current) => {
385
- const scoped = current.key === fitStateKey
386
- ? current
387
- : createInitialDeckMapDashboardFitState(fitStateKey);
388
- return {
389
- ...scoped,
390
- viewState: nextViewState,
391
- didAutoFit: hasUserInteraction || scoped.didAutoFit || !fitToData,
392
- };
393
- });
394
- }, [didAutoFit, fitStateKey, fitToData, viewState]);
395
287
  const settingsContent = (_jsx(MapSettingsPanel, { dashboardId: dashboardId, panel: panel, onClose: () => handleSettingsOpenChange(false) }));
396
- 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)
397
- .filter(([, state]) => state.error)
398
- .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
399
- ? createDeckMapDashboardDatasets(mapConfig, datasetStates)
400
- : {}, mapStyle: mapConfig.mapStyle, mapProps: mapConfig.mapProps, showLegends: mapConfig.showLegends, deckProps: {
401
- controller: true,
402
- ...(viewState ? { viewState } : {}),
403
- onViewStateChange: handleViewStateChange,
404
- ...(mapConfig.interaction
405
- ? (mapConfig.interaction.event ?? 'hover') === 'click'
406
- ? { onClick: handleBrushEvent }
407
- : { onHover: handleBrushEvent }
408
- : {}),
409
- } })] }));
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
+ } })] }))] }));
410
316
  return (_jsx("div", { className: "h-full min-h-0", children: _jsx(MosaicDashboardPanelLayout, { isOpen: isSettingsOpen, onIsOpenChange: handleSettingsOpenChange, settings: settingsContent, content: mapContent }) }));
411
317
  }
412
318
  export const deckMapDashboardPanelRenderer = {
@@ -418,15 +324,18 @@ export const deckMapDashboardAddPanelAction = {
418
324
  type: DECK_MAP_DASHBOARD_PANEL_TYPE,
419
325
  label: 'Map',
420
326
  icon: MapIcon,
421
- isEnabled: ({ selectedTable }) => Boolean(findLongitudeLatitudeColumns(selectedTable) ??
422
- findGeometryColumn(selectedTable)),
423
- createPanel: ({ selectedTable }) => selectedTable
424
- ? createDeckMapDashboardPanelConfigForTable({
425
- title: `${selectedTable.tableName} map`,
426
- tableName: selectedTable.tableName,
427
- columns: selectedTable.columns,
428
- tableReference: selectedTable.table,
429
- })
430
- : 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
+ },
431
340
  };
432
341
  //# sourceMappingURL=dashboard.js.map