-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Display layers and their legend
- Loading branch information
Showing
44 changed files
with
3,215 additions
and
761 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import Legend from "@/components/map/legend"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; | ||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; | ||
import useMapLayers from "@/hooks/use-map-layers"; | ||
import { Media, MediaContextProvider } from "@/media"; | ||
import ChevronDownIcon from "@/svgs/chevron-down.svg"; | ||
import ListBulletIcon from "@/svgs/list-bullet.svg"; | ||
|
||
const LegendControls = () => { | ||
const [layers] = useMapLayers(); | ||
|
||
if (layers.length === 0) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<MediaContextProvider> | ||
<Media lessThan="xl" className="leading-none"> | ||
<Popover> | ||
<PopoverTrigger asChild> | ||
<Button type="button" variant="yellow" className="w-8 font-sans"> | ||
<span className="sr-only">Legend</span> | ||
<ListBulletIcon aria-hidden /> | ||
</Button> | ||
</PopoverTrigger> | ||
<PopoverContent | ||
side="left" | ||
align="end" | ||
className="max-h-[500px] w-[300px] max-w-[calc(100vw_-_theme(spacing.8)_-_2_*_theme(spacing.5)_-_24px)] overflow-y-auto" | ||
> | ||
<Legend /> | ||
</PopoverContent> | ||
</Popover> | ||
</Media> | ||
<Media greaterThanOrEqual="xl" className="relative"> | ||
<Collapsible className="absolute bottom-0 right-0" defaultOpen> | ||
<CollapsibleTrigger asChild> | ||
<Button | ||
type="button" | ||
variant="yellow-alt" | ||
className="group w-[300px] justify-between px-2.5 font-sans" | ||
> | ||
<span>Legend</span> | ||
<ChevronDownIcon aria-hidden className="group-data-[state=closed]:rotate-180" /> | ||
</Button> | ||
</CollapsibleTrigger> | ||
<CollapsibleContent className="max-h-[500px] w-[300px] overflow-y-auto border-t border-t-casper-blue-400 bg-white"> | ||
<Legend /> | ||
</CollapsibleContent> | ||
</Collapsible> | ||
</Media> | ||
</MediaContextProvider> | ||
); | ||
}; | ||
|
||
export default LegendControls; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { useMemo } from "react"; | ||
import { Layer } from "react-map-gl"; | ||
|
||
import useMapLayers from "@/hooks/use-map-layers"; | ||
|
||
import LayerManagerItem from "./item"; | ||
|
||
const LayerManager = () => { | ||
const [layers] = useMapLayers(); | ||
|
||
/** | ||
* These layers are here to aid with positioning the real data layers between themselves. | ||
* See more: https://github.com/visgl/react-map-gl/issues/939#issuecomment-625290200 | ||
*/ | ||
const positioningLayers = useMemo(() => { | ||
return layers.map((layer, index) => { | ||
const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`; | ||
return ( | ||
<Layer | ||
key={`layer-position-${layer.id}`} | ||
id={`layer-position-${layer.id}`} | ||
type="background" | ||
layout={{ visibility: "none" }} | ||
beforeId={beforeId} | ||
/> | ||
); | ||
}); | ||
}, [layers]); | ||
|
||
const layerManagerItems = useMemo(() => { | ||
return layers.map((layer, index) => { | ||
const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`; | ||
const { id, ...settings } = layer; | ||
return ( | ||
<LayerManagerItem key={`layer-${id}`} id={id} settings={settings} beforeId={beforeId} /> | ||
); | ||
}); | ||
}, [layers]); | ||
|
||
return ( | ||
<> | ||
{positioningLayers} | ||
{layerManagerItems} | ||
</> | ||
); | ||
}; | ||
|
||
export default LayerManager; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { Layer, Source } from "react-map-gl"; | ||
|
||
import useLayerConfig from "@/hooks/use-layer-config"; | ||
import { LayerSettings } from "@/types/layer"; | ||
|
||
interface LayerManagerItemProps { | ||
id: number; | ||
beforeId: string; | ||
settings: LayerSettings; | ||
} | ||
|
||
const LayerManagerItem = ({ id, beforeId, settings }: LayerManagerItemProps) => { | ||
const config = useLayerConfig(id, settings); | ||
|
||
if (!config) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Source {...config.source}> | ||
{config.styles.map((style) => ( | ||
<Layer key={style.id} {...style} beforeId={beforeId} /> | ||
))} | ||
</Source> | ||
); | ||
}; | ||
|
||
export default LayerManagerItem; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
"use client"; | ||
|
||
import { | ||
Announcements, | ||
closestCenter, | ||
DndContext, | ||
DragEndEvent, | ||
DragOverlay, | ||
DragStartEvent, | ||
ScreenReaderInstructions, | ||
} from "@dnd-kit/core"; | ||
import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; | ||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; | ||
import { useCallback, useMemo, useState } from "react"; | ||
|
||
import SortableItem from "@/components/map/legend/sortable-item"; | ||
import useMapLayers from "@/hooks/use-map-layers"; | ||
|
||
import LegendItem from "./item"; | ||
|
||
const Legend = () => { | ||
const [layers, { updateLayerOrder }] = useMapLayers(); | ||
const [draggedLayerId, setDraggedLayerId] = useState<number | null>(null); | ||
|
||
const legendItems = useMemo(() => { | ||
return layers.map(({ id, ...settings }) => ( | ||
<SortableItem key={id} id={id} settings={settings} /> | ||
)); | ||
}, [layers]); | ||
|
||
const draggedLegendItem = useMemo(() => { | ||
if (draggedLayerId === null) { | ||
return null; | ||
} | ||
|
||
const layer = layers.find(({ id }) => id === draggedLayerId); | ||
if (!layer) { | ||
return null; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { id, ...settings } = layer; | ||
|
||
return ( | ||
<div className="shadow"> | ||
<LegendItem id={draggedLayerId} settings={settings} /> | ||
</div> | ||
); | ||
}, [layers, draggedLayerId]); | ||
|
||
const layerIds = useMemo(() => layers.map(({ id }) => id), [layers]); | ||
|
||
const accessibility: { | ||
announcements: Announcements; | ||
screenReaderInstructions: ScreenReaderInstructions; | ||
} = useMemo( | ||
() => ({ | ||
announcements: { | ||
onDragStart({ active }) { | ||
const position = layers.findIndex(({ id }) => id === active.id); | ||
return `Picked up layer in position ${position} of ${layers.length}`; | ||
}, | ||
onDragOver({ over }) { | ||
if (over) { | ||
const position = layers.findIndex(({ id }) => id === over.id); | ||
return `Layer was moved into position ${position} of ${layers.length}`; | ||
} | ||
}, | ||
onDragEnd({ over }) { | ||
if (over) { | ||
const position = layers.findIndex(({ id }) => id === over.id); | ||
return `Layer was dropped at position ${position} of ${layers.length}`; | ||
} | ||
}, | ||
onDragCancel() { | ||
return `Dragging was cancelled.`; | ||
}, | ||
}, | ||
screenReaderInstructions: { | ||
draggable: | ||
"Press space or enter to grab the layer. Use the arrow keys to move the layer up or down. Press space or enter again to drop the layer. Press escape to cancel.", | ||
}, | ||
}), | ||
[layers], | ||
); | ||
|
||
const onDragStart = useCallback( | ||
({ active }: DragStartEvent) => { | ||
setDraggedLayerId(active.id as number); | ||
}, | ||
[setDraggedLayerId], | ||
); | ||
|
||
const onDragEnd = useCallback( | ||
({ active, over }: DragEndEvent) => { | ||
if (over === null || active.id === over.id) { | ||
return; | ||
} | ||
|
||
const id = active.id as number; | ||
const index = layerIds.indexOf(over.id as number); | ||
|
||
updateLayerOrder(id, index); | ||
setDraggedLayerId(null); | ||
}, | ||
[layerIds, updateLayerOrder, setDraggedLayerId], | ||
); | ||
|
||
return ( | ||
<DndContext | ||
collisionDetection={closestCenter} | ||
modifiers={[restrictToVerticalAxis, restrictToParentElement]} | ||
accessibility={accessibility} | ||
onDragStart={onDragStart} | ||
onDragEnd={onDragEnd} | ||
> | ||
<SortableContext strategy={verticalListSortingStrategy} items={layerIds}> | ||
{legendItems} | ||
</SortableContext> | ||
<DragOverlay>{draggedLegendItem}</DragOverlay> | ||
</DndContext> | ||
); | ||
}; | ||
|
||
export default Legend; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Square from "@/components/map/legend/item/square"; | ||
import useLayerLegend from "@/hooks/use-layer-legend"; | ||
|
||
type BasicLegendProps = ReturnType<typeof useLayerLegend>["data"]; | ||
|
||
const BasicLegend = (data: BasicLegendProps) => { | ||
if (!data.items || data.items.length === 0) { | ||
return null; | ||
} | ||
|
||
if (data.items.length === 1) { | ||
return <Square item={data.items[0]} className="h-3" />; | ||
} | ||
|
||
return ( | ||
<div className="columns-2"> | ||
{data.items.map((item) => ( | ||
<div key={item.color} className="flex items-start gap-2"> | ||
<Square item={item} className="relative top-0.5 h-3 w-5 shrink-0" /> | ||
<div className="text-2xs text-gray-500">{item.value}</div> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default BasicLegend; |
26 changes: 26 additions & 0 deletions
26
client/src/components/map/legend/item/choropleth-legend.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import Square from "@/components/map/legend/item/square"; | ||
import Unit from "@/components/map/legend/item/unit"; | ||
import Values from "@/components/map/legend/item/values"; | ||
import useLayerLegend from "@/hooks/use-layer-legend"; | ||
|
||
type ChoroplethLegendProps = ReturnType<typeof useLayerLegend>["data"]; | ||
|
||
const ChoroplethLegend = (data: ChoroplethLegendProps) => { | ||
if (!data.items || data.items.length === 0) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<> | ||
<Unit {...data} /> | ||
<div className="flex h-3"> | ||
{data.items.map((item) => ( | ||
<Square key={item.color} item={item} className="flex-1" /> | ||
))} | ||
</div> | ||
<Values {...data} /> | ||
</> | ||
); | ||
}; | ||
|
||
export default ChoroplethLegend; |
Oops, something went wrong.