@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.
- package/README.md +31 -6
- package/dist/DeckJsonMap.d.ts +2 -2
- package/dist/DeckJsonMap.d.ts.map +1 -1
- package/dist/DeckJsonMap.js +202 -19
- package/dist/DeckJsonMap.js.map +1 -1
- package/dist/DeckJsonMapSpec.d.ts +36 -36
- package/dist/DeckJsonMapSpec.d.ts.map +1 -1
- package/dist/DeckJsonMapSpec.js +1 -0
- package/dist/DeckJsonMapSpec.js.map +1 -1
- package/dist/MapSettings.d.ts.map +1 -1
- package/dist/MapSettings.js +202 -25
- package/dist/MapSettings.js.map +1 -1
- package/dist/ai.d.ts +55 -8
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +190 -41
- package/dist/ai.js.map +1 -1
- package/dist/block.d.ts +20 -0
- package/dist/block.d.ts.map +1 -0
- package/dist/block.js +119 -0
- package/dist/block.js.map +1 -0
- package/dist/dashboard.d.ts +2 -8
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +194 -289
- package/dist/dashboard.js.map +1 -1
- package/dist/dashboardConfig.d.ts +11 -3
- package/dist/dashboardConfig.d.ts.map +1 -1
- package/dist/dashboardConfig.js +82 -7
- package/dist/dashboardConfig.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/json/aggregateWaypoints.d.ts +14 -0
- package/dist/json/aggregateWaypoints.d.ts.map +1 -0
- package/dist/json/aggregateWaypoints.js +190 -0
- package/dist/json/aggregateWaypoints.js.map +1 -0
- package/dist/json/compileColorScale.d.ts +1 -1
- package/dist/json/compileColorScale.d.ts.map +1 -1
- package/dist/json/compileColorScale.js +42 -20
- package/dist/json/compileColorScale.js.map +1 -1
- package/dist/json/createDeckJsonConfiguration.d.ts.map +1 -1
- package/dist/json/createDeckJsonConfiguration.js +106 -8
- package/dist/json/createDeckJsonConfiguration.js.map +1 -1
- package/dist/json/defaultClasses.d.ts +5 -4
- package/dist/json/defaultClasses.d.ts.map +1 -1
- package/dist/json/defaultClasses.js +9 -4
- package/dist/json/defaultClasses.js.map +1 -1
- package/dist/json/extractColorScaleLegends.d.ts.map +1 -1
- package/dist/json/extractColorScaleLegends.js +43 -5
- package/dist/json/extractColorScaleLegends.js.map +1 -1
- package/dist/json/heatmapDefaults.d.ts +8 -0
- package/dist/json/heatmapDefaults.d.ts.map +1 -0
- package/dist/json/heatmapDefaults.js +14 -0
- package/dist/json/heatmapDefaults.js.map +1 -0
- package/dist/json/layerCompatibility.d.ts.map +1 -1
- package/dist/json/layerCompatibility.js +5 -1
- package/dist/json/layerCompatibility.js.map +1 -1
- package/dist/json/layers/DeckColumnLayer.d.ts +12 -0
- package/dist/json/layers/DeckColumnLayer.d.ts.map +1 -0
- package/dist/json/layers/DeckColumnLayer.js +26 -0
- package/dist/json/layers/DeckColumnLayer.js.map +1 -0
- package/dist/json/layers/DeckH3HexagonLayer.d.ts +37 -0
- package/dist/json/layers/DeckH3HexagonLayer.d.ts.map +1 -0
- package/dist/json/layers/DeckH3HexagonLayer.js +96 -0
- package/dist/json/layers/DeckH3HexagonLayer.js.map +1 -0
- package/dist/json/layers/DeckTripsLayer.d.ts +45 -0
- package/dist/json/layers/DeckTripsLayer.d.ts.map +1 -0
- package/dist/json/layers/DeckTripsLayer.js +169 -0
- package/dist/json/layers/DeckTripsLayer.js.map +1 -0
- package/dist/json/layers/index.d.ts +4 -0
- package/dist/json/layers/index.d.ts.map +1 -0
- package/dist/json/layers/index.js +4 -0
- package/dist/json/layers/index.js.map +1 -0
- package/dist/json/rewriteGeoArrowAccessors.d.ts.map +1 -1
- package/dist/json/rewriteGeoArrowAccessors.js +5 -0
- package/dist/json/rewriteGeoArrowAccessors.js.map +1 -1
- package/dist/mapConfigUtils.d.ts +102 -1
- package/dist/mapConfigUtils.d.ts.map +1 -1
- package/dist/mapConfigUtils.js +21 -3
- package/dist/mapConfigUtils.js.map +1 -1
- package/dist/mapLayerConfigUtils.d.ts +15 -0
- package/dist/mapLayerConfigUtils.d.ts.map +1 -1
- package/dist/mapLayerConfigUtils.js +109 -7
- package/dist/mapLayerConfigUtils.js.map +1 -1
- package/dist/prepare/detectGeometryColumn.d.ts +10 -1
- package/dist/prepare/detectGeometryColumn.d.ts.map +1 -1
- package/dist/prepare/detectGeometryColumn.js +48 -6
- package/dist/prepare/detectGeometryColumn.js.map +1 -1
- package/dist/prepare/wkbDecoder.d.ts.map +1 -1
- package/dist/prepare/wkbDecoder.js +88 -1
- package/dist/prepare/wkbDecoder.js.map +1 -1
- package/dist/prepare/wkbParser.d.ts +5 -0
- package/dist/prepare/wkbParser.d.ts.map +1 -1
- package/dist/prepare/wkbParser.js +24 -0
- package/dist/prepare/wkbParser.js.map +1 -1
- package/dist/types.d.ts +14 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/useDeckMapDatasets.d.ts +17 -0
- package/dist/useDeckMapDatasets.d.ts.map +1 -0
- package/dist/useDeckMapDatasets.js +69 -0
- package/dist/useDeckMapDatasets.js.map +1 -0
- package/dist/useDeckMapFitToBounds.d.ts +29 -0
- package/dist/useDeckMapFitToBounds.d.ts.map +1 -0
- package/dist/useDeckMapFitToBounds.js +333 -0
- package/dist/useDeckMapFitToBounds.js.map +1 -0
- 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 {
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
127
|
+
fitToData: datasetFitToData,
|
|
128
|
+
}), [dashboard, dataset, datasetFitToData, panel]);
|
|
29
129
|
const query = useCallback((filter) => source
|
|
30
|
-
? createDeckMapDashboardDatasetQuery(source, filter
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
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
|
-
|
|
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
|
|
234
|
+
const deckMapRef = useRef(null);
|
|
188
235
|
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
189
|
-
const
|
|
190
|
-
|
|
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 =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|