Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
7fbd4ef
feat: Add lasso select tool button to map controls
Mar 19, 2026
7a80920
Merge branch 'main' into add-lasso-map-tool-option
Mar 19, 2026
2bc3cbb
feat: Wire up lasso select tool with layer picker dropdown
Mar 19, 2026
09c7a1f
feat: Increase lasso select icon size for better visibility
Mar 20, 2026
ddf6366
Merge branch 'main' into add-lasso-map-tool-option
Mar 20, 2026
badbccf
feat: Add library-based lasso drawing with freehand/polygon modes
Mar 23, 2026
22014f5
refactor: Extract useLassoSelection hook and fix lasso/ruler bugs
Mar 23, 2026
40fd351
feat: Add lasso point-in-polygon selection with CSV/Excel export
Mar 23, 2026
ac9bf8d
feat: Persist lasso polygon display and improve results bar UX
Mar 24, 2026
23d5ac4
docs: Add missing lasso component entries to Development Guide
Mar 24, 2026
ca3f080
feat: Switch lasso to single-layer selection and geometry-aware filte…
Mar 24, 2026
4612985
docs: Update lasso documentation for single-layer selection and geome…
Mar 24, 2026
2714061
feat: Anchor lasso results bar near end of drawn polygon
Mar 24, 2026
08ae5fc
fix: Stabilize lasso memoization, fix broken tests, and simplify code
Mar 25, 2026
45d9ffd
fix: Improve lasso UX with dashed outlines, activation flow, and anch…
Mar 25, 2026
12f4840
docs: Add LassoDropdown to Development Guide key utilities table
Mar 25, 2026
4c8315d
feat: Highlight lasso-selected features and dim unselected layers
Mar 25, 2026
1d1af0e
feat: Add per-layer lasso-selectable option and improve lasso geometr…
Mar 26, 2026
80a1ce9
feat: Add cursor-following hint tooltip for lasso tool
Mar 26, 2026
f53e0bb
fix: Simplify lasso logic, fix bugs, and add test coverage
Mar 26, 2026
362d33b
refactor: Extract shared hooks, icons, and constants from lasso code
Mar 26, 2026
1cd05e4
fix: Fix lasso bugs, extract shared helper, and clean up code
Mar 26, 2026
7c752a2
fix: Fix category normalization bug, replace vulnerable xlsx, and imp…
Mar 26, 2026
9ba0886
feat: Add circle and rectangle lasso draw modes, improve spatial accu…
Mar 27, 2026
b18fcaf
fix: Remove frozen feature collection, disable lasso tooltips, and fi…
Mar 27, 2026
e56805a
fix: Preserve layer selection when toggling lasso off
Mar 27, 2026
696c9df
fix: Fix TypeScript errors in lasso test files
Mar 27, 2026
c5f3af8
fix: Fix pre-existing TS2352 type error in legacy Contour layer
Mar 27, 2026
e23c396
feat: Add toast notifications for lasso CSV and Excel exports
Mar 30, 2026
7de6db8
refactor: Replace exceljs with xlsx for Excel export
Mar 30, 2026
5a380cb
fix: Improve lasso results bar positioning, export filtering, and UX
Mar 30, 2026
e741470
fix: Guard results bar on anchor position and optimize lasso filtering
Mar 31, 2026
3a7d0ea
fix: Filter dynamic color_ prefixed keys and pointSize from lasso exp…
Mar 31, 2026
697a899
feat: Switch lasso export to allowlist of user-configured query columns
Mar 31, 2026
e2c8fe8
fix: Update LassoResultsBar tests for prop-based toasts and allowedCo…
Mar 31, 2026
174c5e8
refactor: Simplify export allowlist to hover and feature info columns
Mar 31, 2026
4d279e1
docs: Add lasso select tool user guide to wiki pages
Mar 31, 2026
bc6c32b
feat: Show lasso polygon area and switch toasts to antd message
Mar 31, 2026
fd14e40
fix: Suppress hover tooltips during lasso draw mode
Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ GeoSet bridges the gap between Superset and GIS tooling. This is accomplished by
- Point size scaling by value (number or percentile bounds)
- Collapsible legend with layer toggling and dynamic iconography
- Native dashboard integration
- Lasso select with CSV/Excel export
- Text Overlay Formatting

<p align="center">
Expand Down
4,107 changes: 2,700 additions & 1,407 deletions superset-frontend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,11 @@
"immer": "^10.1.1",
"interweave": "^13.1.0",
"jquery": "^3.7.1",
"jspdf": "^3.0.1",
"js-levenshtein": "^1.1.6",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"json-stringify-pretty-compact": "^2.0.0",
"jspdf": "^3.0.1",
"lodash": "^4.17.21",
"luxon": "^3.5.0",
"mapbox-gl": "^3.13.0",
Expand Down
8 changes: 8 additions & 0 deletions superset-frontend/plugins/geoset-map-chart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
],
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@deck.gl-community/editable-layers": "^9.2.8",
"@deck.gl/aggregation-layers": "^9.1.14",
"@deck.gl/core": "^9.1.14",
"@deck.gl/extensions": "^9.1.14",
Expand All @@ -33,6 +34,13 @@
"@mapbox/geojson-extent": "^1.0.1",
"@material-ui/icons": "^4.11.3",
"@math.gl/web-mercator": "^4.1.0",
"@turf/area": "^7.2.0",
"@turf/boolean-intersects": "^7.2.0",
"@turf/boolean-point-in-polygon": "^7.2.0",
"@turf/centroid": "^7.2.0",
"@turf/helpers": "^7.2.0",
"@turf/intersect": "^7.2.0",
"@turf/unkink-polygon": "^7.2.0",
"@types/d3-array": "^2.0.0",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-scale-chromatic": "^3.1.0",
Expand Down
153 changes: 130 additions & 23 deletions superset-frontend/plugins/geoset-map-chart/src/DeckGLContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
import { StaticMap, MapRef } from 'react-map-gl';
import DeckGL from '@deck.gl/react';
import type { Deck, Layer } from '@deck.gl/core';
import { GeoJsonLayer } from '@deck.gl/layers';
import { JsonObject, JsonValue, styled } from '@superset-ui/core';
import Tooltip, { TooltipProps } from './components/Tooltip';
import 'mapbox-gl/dist/mapbox-gl.css';
Expand All @@ -42,8 +43,9 @@ import {
isValidViewport,
toNumericViewport,
} from './utils/fitViewport';
import { LayerState } from './types';
import { GeoJsonFeature, LassoDrawMode, LayerState } from './types';
import { MeasureState, useMeasureLayers } from './components/MeasureOverlay';
import { LASSO_CURSOR, useLassoLayer } from './components/useLassoLayer';
import { Coordinate } from './utils/measureDistance';

const TICK = 250; // milliseconds
Expand All @@ -65,6 +67,11 @@ export type DeckGLContainerProps = {
onMeasureDragStart?: (coordinate: Coordinate) => void;
onMeasureDrag?: (coordinate: Coordinate) => void;
onMeasureDragEnd?: (coordinate: Coordinate) => void;
lassoIsActive?: boolean;
lassoDrawMode?: LassoDrawMode;
lassoPolygon?: Coordinate[] | null;
onLassoComplete?: (polygon: Coordinate[]) => void;
selectedFeatures?: GeoJsonFeature[];
onEmptyClick?: () => void;
};

Expand Down Expand Up @@ -106,6 +113,27 @@ const MeasureTooltip = styled.div`
}
`;

const LASSO_HINT_TEXT: Record<LassoDrawMode, string> = {
freehand: 'Click and drag to draw selection',
polygon: 'Double-click or click first point to close',
circle: 'Click and drag to draw circle',
rectangle: 'Click and drag to draw rectangle',
};

const LassoHintTooltip = styled.div`
position: absolute;
background: ${({ theme }) => theme.colorBgElevated};
color: ${({ theme }) => theme.colorTextSecondary};
border: 1px solid ${({ theme }) => theme.colorBorder};
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
pointer-events: none;
z-index: 100;
white-space: nowrap;
transform: translate(16px, 16px);
`;

// Custom ruler cursor as a data URI SVG
// The cursor is a small ruler icon with a crosshair at the click point (top-left)
// Uses white fill with black stroke for visibility on any background
Expand Down Expand Up @@ -186,6 +214,10 @@ export const DeckGLContainer = memo(
const currentViewport = useRef<Viewport>(props.viewport);
const pendingSaveTime = useRef<number | null>(null);
const [tooltip, setTooltip] = useState<TooltipProps['tooltip']>(null);
const [lassoMousePos, setLassoMousePos] = useState<{
x: number;
y: number;
} | null>(null);
const [viewState, setViewState] = useState(() => props.viewport);
const [mapReady, setMapReady] = useState(false);

Expand Down Expand Up @@ -369,16 +401,72 @@ export const DeckGLContainer = memo(
distance,
} = useMeasureLayers(measureState, project);

// Get lasso editable layer (library-based drawing)
const lassoIsActive = props.lassoIsActive ?? false;
const lassoDrawMode = props.lassoDrawMode ?? 'freehand';
const lassoPolygon = props.lassoPolygon ?? null;
const noopLassoComplete = useCallback(() => {}, []);
const { layers: lassoLayers } = useLassoLayer(
lassoIsActive,
props.onLassoComplete ?? noopLassoComplete,
lassoDrawMode,
lassoPolygon,
);

const selectedFeaturesArr = useMemo(
() => props.selectedFeatures ?? [],
[props.selectedFeatures],
);
const hasSelection = selectedFeaturesArr.length > 0;

// Build a highlight layer from lasso-selected features
const highlightLayer = useMemo(() => {
if (!hasSelection) return [];
return [
new GeoJsonLayer({
id: 'lasso-highlight',
data: {
type: 'FeatureCollection' as const,
features: selectedFeaturesArr,
},
getFillColor: (f: any) => {
const c = f.color ?? f.properties?.color;
if (!c) return [255, 200, 0, 200];
return c.length >= 4 ? c : [...c, 200];
},
getLineColor: (f: any) => {
const c = f.strokeColor ?? f.properties?.strokeColor;
if (!c) return [40, 40, 40, 220];
return c.length >= 4 ? c : [...c, 220];
},
getPointRadius: (f: any) => f.sizeValue ?? 4,
pointRadiusMinPixels: 4,
lineWidthMinPixels: 1,
pickable: false,
}),
];
}, [hasSelection, selectedFeaturesArr]);

const allLayers = useMemo(() => {
if (!layerStates || layerStates.length === 0) {
return measureLayers as Layer[];
return [...measureLayers, ...highlightLayer, ...lassoLayers] as Layer[];
}
const layers = layerStates
let layers = layerStates
.map(ls => ls?.layer)
.filter(Boolean) as Layer[];

return [...layers, ...measureLayers] as Layer[];
}, [layerStates, measureLayers]);
// Dim all data layers when there's an active lasso selection
if (hasSelection) {
layers = layers.map(l => l.clone({ opacity: 0.15 }));
}

return [
...layers,
...highlightLayer,
...measureLayers,
...lassoLayers,
] as Layer[];
}, [layerStates, measureLayers, lassoLayers, highlightLayer, hasSelection]);

useEffect(() => {
if (!props.layerStates) return;
Expand Down Expand Up @@ -539,6 +627,7 @@ export const DeckGLContainer = memo(
// Clear tooltip when mouse leaves the map container
const handleMouseLeave = useCallback(() => {
setTooltip(null);
setLassoMousePos(null);
}, []);

// Track drag state for measurement - use refs to avoid stale closure issues
Expand All @@ -551,6 +640,8 @@ export const DeckGLContainer = memo(
(info: any) => {
// Don't handle click if a drag was in progress (threshold was exceeded)
if (measureDragRef.current) return;
// Suppress normal clicks during lasso mode
if (lassoIsActive) return;
if (measureState.isActive && onMeasureClick && info.coordinate) {
onMeasureClick(info.coordinate as Coordinate);
}
Expand All @@ -559,21 +650,25 @@ export const DeckGLContainer = memo(
onEmptyClick();
}
},
[measureState.isActive, onMeasureClick, onEmptyClick],
[measureState.isActive, lassoIsActive, onMeasureClick, onEmptyClick],
);

// Cursor style for measure mode - use custom ruler cursor
// Cursor style for measure/lasso modes
const getCursor = useCallback(
() => (measureState.isActive ? RULER_CURSOR : 'grab'),
[measureState.isActive],
() =>
lassoIsActive
? LASSO_CURSOR
: measureState.isActive
? RULER_CURSOR
: 'grab',
[measureState.isActive, lassoIsActive],
);

// Handle mouse down for drag-to-measure
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
if (!measureState.isActive) return;

// Store initial position - don't start drag yet
const rect = e.currentTarget.getBoundingClientRect();
mouseDownPosRef.current = {
x: e.clientX - rect.left,
Expand All @@ -584,9 +679,18 @@ export const DeckGLContainer = memo(
[measureState.isActive],
);

// Handle mouse move for drag-to-measure
// Handle mouse move for drag-to-measure and lasso hint tooltip
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
// Track cursor for lasso hint tooltip
if (lassoIsActive) {
const rect = e.currentTarget.getBoundingClientRect();
setLassoMousePos({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
}

if (!measureState.isActive || !mouseDownPosRef.current) return;

const map = mapRef.current?.getMap();
Expand All @@ -596,38 +700,32 @@ export const DeckGLContainer = memo(
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

// Check if we've exceeded drag threshold
if (!measureDragRef.current) {
const dx = x - mouseDownPosRef.current.x;
const dy = y - mouseDownPosRef.current.y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance >= DRAG_THRESHOLD) {
// Start drag from the initial mouse down position
if (Math.sqrt(dx * dx + dy * dy) >= DRAG_THRESHOLD) {
measureDragRef.current = true;
const startLngLat = map.unproject([
mouseDownPosRef.current.x,
mouseDownPosRef.current.y,
]);
props.onMeasureDragStart?.([startLngLat.lng, startLngLat.lat]);
} else {
return; // Haven't moved enough yet
return;
}
}

// Continue drag
const lngLat = map.unproject([x, y]);
props.onMeasureDrag?.([lngLat.lng, lngLat.lat]);
},
[measureState.isActive, props.onMeasureDragStart, props.onMeasureDrag],
[measureState.isActive, lassoIsActive, props.onMeasureDragStart, props.onMeasureDrag],
);

// Handle mouse up for drag-to-measure
const handleMouseUp = useCallback(
(e: React.MouseEvent) => {
if (!measureState.isActive) return;

// Only finalize drag if we actually started dragging
if (measureDragRef.current) {
const map = mapRef.current?.getMap();
if (map) {
Expand All @@ -639,15 +737,17 @@ export const DeckGLContainer = memo(
}
}

// Reset refs
measureDragRef.current = false;
mouseDownPosRef.current = null;
},
[measureState.isActive, props.onMeasureDragEnd],
);

// Disable map panning when in measure mode
const controllerOptions = measureState.isActive
// Disable map panning when actively drawing (measure or lasso).
// Re-enable after lasso drawing completes so user can pan/zoom the selection.
const isActivelyDrawing =
measureState.isActive || (lassoIsActive && !lassoPolygon);
const controllerOptions = isActivelyDrawing
? { dragPan: false, dragRotate: false }
: true;

Expand Down Expand Up @@ -740,6 +840,13 @@ export const DeckGLContainer = memo(
{distance}
</MeasureTooltip>
)}
{lassoIsActive && lassoMousePos && !selectedFeaturesArr.length && (
<LassoHintTooltip
style={{ left: lassoMousePos.x, top: lassoMousePos.y }}
>
{LASSO_HINT_TEXT[lassoDrawMode]}
</LassoHintTooltip>
)}
<ScaleControlContainer>
<ScaleBar $width={scaleInfo.width}>{scaleInfo.label}</ScaleBar>
</ScaleControlContainer>
Expand Down
Loading
Loading