This plan outlines the implementation of interactive features for the map preview using a tool-based interaction model:
- Map Control Bar: A vertical toolbar pinned to the right side of the map preview
- Inspect Tool: Hover over data points/labels to see mapped dimension data
- Select Tool: Click labels to edit individual label styling
- Move Tool: Click and drag labels to reposition them
All changes will be persisted with the project file.
- Vertical toolbar pinned to the right side of the map preview pane
- Positioned in the vertical middle
- Overlaps the right edge by 50% of its width
- Styled similarly but distinctly from the main floating toolbar
- Contains tool icons with tooltips
- Supports keyboard shortcuts (I, V, M)
File: components/map-control-bar.tsx (new file)
- Vertical layout (flex-col)
- Positioned absolutely relative to map preview container
- Right edge positioning:
right: -50%(overlaps by 50% width) - Vertical centering:
top: 50%withtransform: translateY(-50%) - Styling:
- Similar rounded corners but different shape (rounded-l-lg instead of rounded-full)
- Similar shadow (
shadow-xl) and border (border border-border) - Different background:
bg-background/95 backdrop-blur(slightly different opacity) - Vertical spacing between buttons (
gap-1orgap-2) - Padding:
p-1orp-1.5 - Width: approximately
w-10orw-12(40-48px)
- Contains three tool buttons:
- Inspect tool (Search icon from lucide-react)
- Select tool (MousePointer or CursorClick icon)
- Move tool (MoveHorizontal icon)
- Each button:
- Size:
h-9 w-9(same as main toolbar buttons) - Variant:
ghost - Shows tooltip on hover (using TooltipProvider)
- Active state:
bg-secondaryorbg-primary/10withtext-primary
- Size:
- Icons from
lucide-react:Search,MousePointer,MoveHorizontal
File: components/map-preview.tsx
- Add state for active tool:
'inspect' | 'select' | 'move' - Default to
'inspect' - Pass tool state and setter to map rendering functions
- Handle keyboard shortcuts (I, V, M) when map preview is focused
File: components/map-preview.tsx
- Render
MapControlBarinside the map container - Position relative to
mapContainerRef - Pass active tool and tool change handler
- Ensure z-index is above map SVG but below modals
- When Inspect tool is active, hovering over symbols shows tooltip with mapped dimension columns
- When Inspect tool is active, hovering over choropleth regions shows tooltip with mapped dimension columns
- When Inspect tool is active, hovering over labels shows tooltip with mapped dimension columns
- Tooltip displays formatted values using existing column formats
- Only active when Inspect tool is selected
File: modules/map-preview/tooltip.ts (new file)
- Create a React portal-based tooltip system (HTML tooltip positioned absolutely)
- Position tooltip near cursor/mouse position
- Style tooltip to match application theme (dark/light mode)
- Show/hide tooltip on mouseenter/mouseleave events
- Accept tooltip content as React node for flexible formatting
File: modules/map-preview/tooltip.ts (new file)
- Create function
getMappedDimensionColumns():- For symbols: Extract columns from
dimensionSettings.symbol(latitude, longitude, sizeBy, colorBy, labelTemplate) - For choropleth: Extract columns from
dimensionSettings.choropleth(stateColumn, colorBy, labelTemplate) - Return array of column names that are mapped to dimensions
- For symbols: Extract columns from
- Create function
formatTooltipData():- Takes data record and mapped columns
- Formats values using
columnFormatsandcolumnTypes - Returns formatted key-value pairs
File: modules/map-preview/symbols.ts
- Modify
renderSymbolsto acceptactiveToolparameter - Only add hover events when
activeTool === 'inspect' - On
mouseenter:- Get data record for the symbol
- Extract mapped dimension columns
- Format and display tooltip via callback
- On
mouseleave: Hide tooltip via callback - Change cursor to
helporcrosshairwhen inspect tool is active
File: modules/map-preview/base-map.ts and modules/map-preview/choropleth.ts
- Modify choropleth rendering to accept
activeToolparameter - Only add hover events when
activeTool === 'inspect' - On
mouseenter:- Identify the feature from the path element
- Match feature to data record using existing
geoDataMaplogic - Extract mapped dimension columns
- Format and display tooltip
- On
mouseleave: Hide tooltip - Change cursor appropriately
File: modules/map-preview/labels.ts
- Modify label rendering to accept
activeToolparameter - Only add hover events when
activeTool === 'inspect' - On
mouseenter:- Get data record associated with the label
- Extract mapped dimension columns
- Format and display tooltip
- On
mouseleave: Hide tooltip - Change cursor appropriately
File: modules/map-preview/base-map.ts
- Ensure custom SVG maps also get hover events when inspect tool is active
- Use existing
extractCandidateFromSVGIdandnormalizeGeoIdentifierfunctions
- When Select tool is active, clicking a label opens the label editor toolbar
- Only works when Select tool is selected
- Clicking outside the label or switching tools closes the editor
File: app/(studio)/types.ts
- Add
IndividualLabelOverridesinterface:interface IndividualLabelOverride { id: string // Unique identifier for the label (e.g., "symbol-0", "choropleth-CA") fontFamily?: string fontStyle?: 'normal' | 'italic' fontWeight?: 'normal' | 'bold' fontSize?: number textAnchor?: 'start' | 'middle' | 'end' dominantBaseline?: 'baseline' | 'middle' | 'hanging' fill?: string stroke?: string strokeWidth?: number x?: number // For drag position y?: number // For drag position }
- Add
individualLabelOverrides: Record<string, IndividualLabelOverride>toStylingSettings
File: components/label-editor-toolbar.tsx (new file)
- Floating toolbar that appears when a label is clicked
- Position near the clicked label
- Include controls for all styling properties
- Include "Reset to Defaults" button
- Include close button
- Use existing UI components (Select, Input, ColorPicker, etc.)
File: modules/map-preview/labels.ts
- Modify label rendering to accept
activeToolparameter - Only enable click events when
activeTool === 'select' - Change
pointer-events: 'none'topointer-events: 'all'when select tool is active - Add unique
data-label-idattribute to each label:- Symbol labels:
symbol-{index}where index is the data record index - Choropleth labels:
choropleth-{featureIdentifier}
- Symbol labels:
- Add click event handler that:
- Prevents default behavior
- Stops propagation
- Only works when
activeTool === 'select' - Calls callback with label ID, data record, and current position
- Opens label editor toolbar
- Change cursor to
pointerwhen select tool is active
File: modules/map-preview/labels.ts
- Modify
renderSymbolLabelsandrenderChoroplethLabels:- Check for override in
stylingSettings.individualLabelOverrides[labelId] - Apply override properties, falling back to default styling settings
- Apply position overrides (x, y) if present
- Check for override in
File: components/label-editor-toolbar.tsx
- On any property change:
- Update
stylingSettings.individualLabelOverrides[labelId] - Call
onUpdateStylingSettingscallback - Trigger map re-render
- Update
- On "Reset to Defaults":
- Remove entry from
individualLabelOverrides - Call
onUpdateStylingSettings
- Remove entry from
- On close:
- Hide toolbar
File: components/map-preview.tsx
- Add state for selected label ID
- Pass
onLabelClickhandler to label rendering functions - Render
LabelEditorToolbarwhen a label is selected - Pass
stylingSettingsandonUpdateStylingSettingsto toolbar
- When Move tool is active, clicking and dragging a label moves it
- Only works when Move tool is selected
- Position is saved with the label override
- Visual feedback during drag (cursor change, maybe slight opacity change)
File: modules/map-preview/labels.ts
- Modify label rendering to accept
activeToolparameter - Only enable drag when
activeTool === 'move' - Use D3 drag behavior (
d3.drag()) on label text elements - On drag start:
- Set cursor to 'grabbing'
- Optionally reduce opacity slightly
- Only if move tool is active
- On drag:
- Update label's x and y attributes based on drag position
- Store position in
stylingSettings.individualLabelOverrides[labelId].xand.y
- On drag end:
- Reset cursor
- Reset opacity
- Save position to styling settings via callback
- Trigger history push for undo/redo
- Change cursor to
moveorgrabwhen move tool is active
File: modules/map-preview/labels.ts
- When rendering labels, check for
xandyin override - If present, use override position instead of calculated position
- For symbol labels: override
projected[0] + position.dxandprojected[1] + position.dy - For choropleth labels: override
centroid[0]andcentroid[1]
File: components/label-editor-toolbar.tsx
- Display current x, y coordinates (read-only or editable)
- Optionally add "Reset Position" button to clear x/y overrides
File: lib/projects.ts
SavedProjecttype already includesstylingSettingsindividualLabelOverrideswill be automatically saved/loaded as part ofstylingSettings- No changes needed to save/load logic
File: state/studio-store.ts
- Ensure
stylingSettingschanges trigger history push - Label drag operations should push history on drag end
- Label editor changes should use debounced history push (already implemented)
components/map-control-bar.tsx- Vertical toolbar with tool selectionmodules/map-preview/tooltip.ts- Tooltip rendering logic and helperscomponents/label-editor-toolbar.tsx- Label styling editor component
app/(studio)/types.ts- AddIndividualLabelOverrideinterface andMapTooltypecomponents/map-preview.tsx- Add tool state, integrate control bar, handle keyboard shortcutsmodules/map-preview/symbols.ts- Add conditional hover events based on active toolmodules/map-preview/choropleth.ts- Add conditional hover events based on active toolmodules/map-preview/base-map.ts- Add conditional hover events to choropleth pathsmodules/map-preview/labels.ts- Add conditional click/drag based on active tool, apply overrides
-
Phase 1: Map Control Bar (Feature 1)
- Create MapControlBar component
- Add tool state management
- Integrate with map preview
- Add keyboard shortcuts (I, V, M)
- Style and position correctly
-
Phase 2: Inspect Tool (Feature 2)
- Create tooltip system
- Create helper functions for extracting mapped columns
- Add conditional hover to symbols (inspect tool only)
- Add conditional hover to choropleth regions (inspect tool only)
- Add conditional hover to labels (inspect tool only)
- Test with various data configurations
-
Phase 3: Select Tool (Feature 3)
- Extend types for label overrides
- Create label editor toolbar
- Make labels conditionally clickable (select tool only)
- Apply overrides when rendering
- Test persistence
-
Phase 4: Move Tool (Feature 4)
- Add conditional drag behavior (move tool only)
- Apply position overrides
- Integrate with label editor
- Test drag and persistence
- SVG elements can't use HTML tooltips directly
- Options:
- Use foreignObject to embed HTML tooltip
- Use SVG elements (rect, text) styled as tooltip
- Use portal to render HTML tooltip outside SVG, positioned absolutely
- Recommendation: Use portal approach for better styling and accessibility
- Symbol labels: Use data index (
symbol-{index}) - Choropleth labels: Use normalized geographic identifier (
choropleth-{featureId}) - Store mapping in data attributes for easy lookup
- Tool state lives in
MapPreviewcomponent - Passed down to rendering functions
- Keyboard shortcuts only active when map preview is focused/expanded
- Tool state doesn't need to persist (always starts with 'inspect')
- Tooltip rendering should be lightweight
- Label editor should only render when label is selected
- Debounce styling updates (already implemented)
- Consider virtualizing if many labels exist
- Add keyboard navigation for label selection
- Add ARIA labels for interactive elements
- Ensure tooltips are accessible to screen readers
- Control bar appears on right side of map preview
- Control bar overlaps right edge by 50% width
- Control bar is vertically centered
- Control bar styling is distinct from main floating toolbar
- Tool buttons show tooltips
- Active tool is visually highlighted
- Keyboard shortcuts work (I, V, M)
- Keyboard shortcuts only work when map is focused
- Inspect tool is default/active on load
- Tooltips appear on symbol hover (inspect tool only)
- Tooltips appear on choropleth region hover (inspect tool only)
- Tooltips appear on label hover (inspect tool only)
- Tooltip content shows mapped dimension columns only
- Tooltip content is formatted correctly
- Tooltips work with custom maps
- Cursor changes appropriately in inspect mode
- Select tool activates on click/keyboard shortcut
- Labels are clickable only when select tool is active
- Label editor toolbar appears on label click
- Label editor controls work correctly
- Individual overrides apply correctly
- Reset to defaults works
- Cursor changes appropriately in select mode
- Move tool activates on click/keyboard shortcut
- Labels can be dragged only when move tool is active
- Visual feedback during drag (cursor, opacity)
- Dragged positions persist
- Cursor changes appropriately in move mode
- All label changes save with project
- All label changes load correctly from project
- Undo/redo works with label changes
- Works in both light and dark themes
- Works with all map types (symbol, choropleth, custom)
- Tool state resets to 'inspect' on map reload (expected behavior)