Skip to content

Commit

Permalink
Merge pull request #29 from Vizzuality/AF-legend
Browse files Browse the repository at this point in the history
[FE](feat): legend
  • Loading branch information
mluena authored Feb 26, 2024
2 parents c811035 + 590100f commit 4c128a7
Show file tree
Hide file tree
Showing 18 changed files with 260 additions and 221 deletions.
1 change: 1 addition & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ yarn-error.log*

# local env files
.env
.env.local

# vercel
.vercel
Expand Down
2 changes: 1 addition & 1 deletion client/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const nextConfig = {
{
source: '/',
destination: '/projects',
permanent: true,
permanent: false,
},
];
},
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/map/legend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Legend: React.FC<LegendProps> = ({
>
{isChildren && (
<div className="relative flex h-full flex-col overflow-hidden">
<div className="overflow-y-auto overflow-x-hidden">
<div className="flex flex-col space-y-1 overflow-y-auto overflow-x-hidden">
{!!sortable.enabled && !!onChangeOrder && (
<SortableList sortable={sortable} onChangeOrder={onChangeOrder}>
{children}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/map/legend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export interface LegendItemProps extends LegendItemEvents {
info?: string;

// sortable
sortable: Sortable;
sortable?: Sortable;
listeners?: SyntheticListeners;
attributes?: DraggableAttributes;

Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-10 w-72 rounded-md border p-4 shadow-md outline-none',
className
)}
{...props}
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/ui/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ const Slider = React.forwardRef<
className={cn('relative flex w-full touch-none select-none items-center', className)}
{...props}
>
<SliderPrimitive.Track className="bg-secondary relative h-2 w-full grow overflow-hidden rounded-full">
<SliderPrimitive.Range className="bg-primary/50 absolute h-full" />
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-100">
<SliderPrimitive.Range className="absolute h-full bg-gray-400" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="border-primary bg-background ring-offset-background focus-visible:ring-ring block h-5 w-5 rounded-full border-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="border-primary ring-offset-background focus-visible:ring-ring block h-2.5 w-2.5 cursor-pointer rounded-full border-2 bg-black transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border bg-green-400 px-3 py-1.5 text-sm text-green-400 shadow-md',
'animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded border bg-white px-1 py-0.5 text-sm text-black shadow-md',
className
)}
{...props}
Expand Down
13 changes: 12 additions & 1 deletion client/src/containers/datasets/layers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import type { LayerProps } from '@/types/layers';

import { ProjectsLayer } from './projects/layer';
import { ProjectsLayer } from '@/containers/datasets/layers/projects/layer';
import ProjectsLegend from '@/containers/datasets/layers/projects/legend';

type LayersIndex = {
[key: string]: React.ComponentType<LayerProps>;
};

type LegendIndex = {
[key: string]: React.ComponentType;
};

// Define the LAYERS object with the explicit type
export const LAYERS: LayersIndex = {
projects: ProjectsLayer,
'tree-cover': ProjectsLayer,
};

export const LEGENDS: LegendIndex = {
projects: ProjectsLegend,
'tree-cover': ProjectsLegend,
};
3 changes: 1 addition & 2 deletions client/src/containers/datasets/layers/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@ export default function LayersItem({ id, attributes }: Required<LayerListRespons

const handleLayerChange = () => {
if (!id) return;
// Toogle layers if they exist

if (layers.includes(id)) {
return setLayers(layers.filter((l) => l !== id));
}

// Add layers if they don't exist
if (!layers.includes(id)) {
return setLayers([id, ...layers]);
}
Expand Down
48 changes: 48 additions & 0 deletions client/src/containers/datasets/layers/projects/hooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CircleLayer } from 'mapbox-gl';

import { Settings } from '@/components/map/legend/types';

export function useLayers({
settings: { visibility = 'visible', opacity = 1 },
}: {
settings: Settings;
}) {
return [
{
id: 'projects_points_shadow',
type: 'circle',
paint: {
'circle-radius': 32,
'circle-color': '#ccc',
'circle-blur': 1,
'circle-opacity': opacity,
},
layout: {
visibility: visibility,
},
},
{
id: 'projects',
type: 'circle',
paint: {
'circle-stroke-color': '#ffffff',
'circle-stroke-width': ['case', ['boolean', ['feature-state', 'hover'], false], 3, 7],
'circle-radius': ['case', ['boolean', ['feature-state', 'hover'], false], 13, 7],
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#EFB82A',
'#176252',
],
'circle-opacity': opacity,
},
transition: {
duration: 300,
delay: 0,
},
layout: {
visibility: visibility,
},
},
] as CircleLayer[];
}
37 changes: 9 additions & 28 deletions client/src/containers/datasets/layers/projects/layer.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,31 @@
import { Layer, Source } from 'react-map-gl';

import { GeoJSONSourceRaw, GeoJSONSourceOptions, CircleLayer } from 'mapbox-gl';
import { GeoJSONSourceRaw, GeoJSONSourceOptions } from 'mapbox-gl';

import type { LayerProps } from '@/types/layers';

import { useLayers } from './hooks';
import mockData from './mock.json';

const GEOJSON = mockData as GeoJSON.FeatureCollection;

const SOURCE: GeoJSONSourceRaw & GeoJSONSourceOptions = {
const SOURCE: GeoJSONSourceRaw & GeoJSONSourceOptions & { id: string } = {
type: 'geojson',
data: GEOJSON,
id: 'projects',
promoteId: 'ID',
};

export const ProjectsLayer = ({ beforeId, settings }: LayerProps) => {
const LAYERS: CircleLayer[] = [
{
id: 'points_shadow',
type: 'circle',
paint: {
'circle-radius': 32,
'circle-color': '#ccc',
'circle-blur': 1,
},
},
{
id: 'projects',
type: 'circle',
paint: {
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 9,
'circle-radius': 10,
'circle-color': '#176252',
},
layout: {
visibility: settings.visibility ? 'visible' : 'none',
},
},
];

const LAYERS = useLayers({
settings,
});
if (!SOURCE || !LAYERS.length) return null;

return (
<Source {...SOURCE}>
{LAYERS.map((LAYER) => (
<Layer {...LAYER} key={LAYER.id} beforeId={beforeId} />
<Layer {...LAYER} key={LAYER.id} beforeId={beforeId} id={LAYER.id} />
))}
</Source>
);
Expand Down
3 changes: 3 additions & 0 deletions client/src/containers/datasets/layers/projects/legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Legend() {
return <div className="h-3 w-3 rounded-full bg-green-500" />;
}
126 changes: 35 additions & 91 deletions client/src/containers/map/legend/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { useCallback, useMemo } from 'react';
import { useCallback, useState } from 'react';

import { useAtom } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import { ChevronsLeft, ChevronsRight } from 'lucide-react';

import { cn } from '@/lib/classnames';

import { layersSettingsAtom, layersAtom, DEFAULT_SETTINGS } from '@/store';

import { LayerSettings } from '@/types/layers';

import MapLegendItem from '@/containers/map/legend/item';
import { layersSettingsAtom, layersAtom } from '@/store';

import Legend from '@/components/map/legend';
import { Button } from '@/components/ui/button';

import MapLegendItem from './item';

const MapLegends = ({ className = '' }) => {
const [layers, setLayers] = useAtom(layersAtom);
const [layersSettings, setLayersSettings] = useAtom(layersSettingsAtom);
const layersSettings = useAtomValue(layersSettingsAtom);

const [openLegend, setOpenLegend] = useState(true);
const handleOpenLegend = useCallback(() => setOpenLegend((prev) => !prev), []);

const handleChangeOrder = useCallback(
(order: string[]) => {
Expand All @@ -28,95 +31,36 @@ const MapLegends = ({ className = '' }) => {
[layers, setLayers]
);

const handleChangeOpacity = useCallback(
(id: string, opacity: number) =>
setLayersSettings((prev) => ({
...prev,
[id]: {
...DEFAULT_SETTINGS,
...prev[id],
opacity,
},
})),
[setLayersSettings]
);

const handleChangeVisibility = useCallback(
(id: string, visibility: LayerSettings['visibility']) =>
setLayersSettings((prev) => ({
...prev,
[id]: {
...DEFAULT_SETTINGS,
...prev[id],
visibility,
},
})),
[setLayersSettings]
);

const handleChangeExpand = useCallback(
(id: string, expand: boolean) =>
setLayersSettings((prev) => ({
...prev,
[id]: {
...prev[id],
expand,
},
})),
[setLayersSettings]
);

const sortable = layers?.length > 1;

const ITEMS = useMemo(() => {
return layers.map((layer) => {
const settings = layersSettings[layer] ?? { opacity: 1, visibility: 'visible', expand: true };

return (
<MapLegendItem
id={layer}
key={layer}
settings={settings}
onChangeOpacity={(opacity: LayerSettings['opacity']) => {
handleChangeOpacity(layer, opacity);
}}
onChangeVisibility={(visibility: LayerSettings['visibility']) => {
handleChangeVisibility(layer, visibility);
}}
onChangeExpand={(expand: boolean) => {
handleChangeExpand(layer, expand);
}}
return (
<div className="absolute bottom-6 right-6 z-10 flex w-full max-w-[285px] space-x-2">
{openLegend && (
<Legend
className={cn(
'max-h-[calc(100vh_-_theme(space.16)_-_theme(space.6)_-_theme(space.48))]',
className
)}
sortable={{
enabled: sortable,
handle: layers.length > 1,
handle: true,
}}
/>
);
});
}, [
layers,
layersSettings,
sortable,
handleChangeOpacity,
handleChangeVisibility,
handleChangeExpand,
]);

return (
<div className="absolute bottom-16 right-6 z-10 w-full max-w-xs">
<Legend
className={cn(
'max-h-[calc(100vh_-_theme(space.16)_-_theme(space.6)_-_theme(space.48))]',
className
)}
sortable={{
enabled: sortable,
handle: true,
}}
onChangeOrder={handleChangeOrder}
onChangeOrder={handleChangeOrder}
>
{layers.map((layer) => {
const settings = layersSettings[layer] ?? { opacity: 1, visibility: 'visible' };
return <MapLegendItem key={layer} id={layer} settings={settings} />;
})}
</Legend>
)}
<Button
variant="primary"
size="base"
className="ml-auto mt-auto h-8 w-8 rounded-full p-0"
onClick={handleOpenLegend}
>
{ITEMS}
</Legend>
{openLegend ? <ChevronsRight size={12} /> : <ChevronsLeft size={12} />}
</Button>
</div>
);
};
Expand Down
Loading

0 comments on commit 4c128a7

Please sign in to comment.