diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/MapTooltip.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/MapTooltip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..668aedcf6cc8b2ef196e58c11c342588a0ad5cd5 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/components/MapTooltip.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { NodeType } from '../../nodelinkvis/types'; +import { GeoJsonType } from '../mapvis.types'; +import { SearchResultType } from '../mapvis.types'; +import { TooltipProvider } from '@graphpolaris/shared/lib/components'; + +export type NodelinkPopupProps = { + type: 'node' | 'area' | 'location'; + data: NodeType | GeoJsonType | SearchResultType; +}; + +const isGeoJsonType = (data: NodeType | GeoJsonType | SearchResultType): data is GeoJsonType => { + return (data as GeoJsonType).properties !== undefined; +}; + +const MapTooltipNode = (node: NodeType) => ( + <div> + {Object.keys(node.attributes).length === 0 ? ( + <div className="flex justify-center items-center h-full"> + <span>No attributes</span> + </div> + ) : ( + Object.entries(node.attributes).map(([k, v]) => ( + <div key={k} className="flex flex-row gap-1 items-center min-h-5"> + <span className="font-semibold truncate min-w-[40%]">{k}</span> + <span className="ml-auto text-right truncate grow-1 flex items-center"> + {v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? ( + <span className="ml-auto text-right truncate">{typeof v === 'number' ? v.toLocaleString('de-DE') : v.toString()}</span> + ) : ( + <div + className={`ml-auto mt-auto h-4 w-12 border-[1px] solid border-gray`} + style={{ + background: + 'repeating-linear-gradient(-45deg, transparent, transparent 6px, #eaeaea 6px, #eaeaea 8px), linear-gradient(to bottom, transparent, transparent)', + }} + ></div> + )} + </span> + </div> + )) + )} + </div> +); +const MapTooltipChoropleth = (geoJson: GeoJsonType) => ( + <div> + <div className="flex flex-row gap-3"> + <span>Area id: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span>{geoJson.properties?.regioFacetId ?? 'N/A'}</span> + </span> + </div> + <div className="flex flex-row gap-3"> + <span>Nodes in area: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span>{geoJson.properties?.nodes?.length ?? 0}</span> + </span> + </div> + <div className="flex flex-row gap-3"> + <span>Townships in area: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span>{geoJson.properties?.townships?.length ?? 0}</span> + </span> + </div> + </div> +); + +const renderLocationDetails = (location: SearchResultType) => ( + <div> + <div className="flex flex-row gap-3"> + <span>Name: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span>{location?.display_name}</span> + </span> + </div> + <div className="flex flex-row gap-3"> + <span>Type: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span>{location?.addresstype}</span> + </span> + </div> + <div className="flex flex-row gap-3"> + <span>Coordinate: </span> + <span className="ml-auto max-w-[10rem] text-right truncate"> + <span> + [{location?.lon}, {location?.lat}] + </span> + </span> + </div> + </div> +); + +export const MapTooltip = (props: NodelinkPopupProps) => { + const { type, data } = props; + + return ( + <TooltipProvider delayDuration={100}> + <div className="text-[0.9rem] min-w-[10rem]"> + <div className="card-body p-0"> + <div className="h-[1px] w-full bg-secondary-200"></div> + <div className="px-2.5 text-[0.8rem]"> + {type === 'node' + ? data && 'attributes' in data + ? MapTooltipNode(data as NodeType) + : null + : data && isGeoJsonType(data) + ? MapTooltipChoropleth(data as GeoJsonType) + : renderLocationDetails(data as SearchResultType)} + </div> + <div className="h-[1px] w-full"></div> + </div> + </div> + </TooltipProvider> + ); +}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx deleted file mode 100644 index ea5bc92fc41140633a1698955284d7d6a8bc96a1..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/components/Tooltip.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React from 'react'; -import { NodeType } from '../../nodelinkvis/types'; -import { GeoJsonType } from '../mapvis.types'; -import { SearchResultType } from '../mapvis.types'; -import { TooltipProvider } from '@graphpolaris/shared/lib/components'; - -export type NodelinkPopupProps = { - type: 'node' | 'area' | 'location'; - data: NodeType | GeoJsonType | SearchResultType; -}; - -const isGeoJsonType = (data: NodeType | GeoJsonType | SearchResultType): data is GeoJsonType => { - return (data as GeoJsonType).properties !== undefined; -}; - -export const MapTooltip = (props: NodelinkPopupProps) => { - const { type, data } = props; - - const renderNodeDetails = (node: NodeType) => ( - <div> - {Object.keys(node.attributes).length === 0 ? ( - <div className="flex justify-center items-center h-full"> - <span>No attributes</span> - </div> - ) : ( - Object.entries(node.attributes).map(([k, v]) => ( - <div key={k} className="flex flex-row gap-1 items-center min-h-5"> - <span className="font-semibold truncate min-w-[40%]">{k}</span> - <span className="ml-auto text-right truncate grow-1 flex items-center"> - {v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? ( - <span className="ml-auto text-right truncate">{typeof v === 'number' ? v.toLocaleString('de-DE') : v.toString()}</span> - ) : ( - <div - className={`ml-auto mt-auto h-4 w-12 border-[1px] solid border-gray`} - style={{ - background: - 'repeating-linear-gradient(-45deg, transparent, transparent 6px, #eaeaea 6px, #eaeaea 8px), linear-gradient(to bottom, transparent, transparent)', - }} - ></div> - )} - </span> - </div> - )) - )} - </div> - ); - - const renderAreaDetails = (geoJson: GeoJsonType) => ( - <div> - <div className="flex flex-row gap-3"> - <span>Area id: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span>{geoJson.properties?.regioFacetId ?? 'N/A'}</span> - </span> - </div> - <div className="flex flex-row gap-3"> - <span>Nodes in area: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span>{geoJson.properties?.nodes?.length ?? 0}</span> - </span> - </div> - <div className="flex flex-row gap-3"> - <span>Townships in area: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span>{geoJson.properties?.townships?.length ?? 0}</span> - </span> - </div> - </div> - ); - - const renderLocationDetails = (location: SearchResultType) => ( - <div> - <div className="flex flex-row gap-3"> - <span>Name: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span>{location?.display_name}</span> - </span> - </div> - <div className="flex flex-row gap-3"> - <span>Type: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span>{location?.addresstype}</span> - </span> - </div> - <div className="flex flex-row gap-3"> - <span>Coordinate: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span> - [{location?.lon}, {location?.lat}] - </span> - </span> - </div> - </div> - ); - - return ( - <TooltipProvider delayDuration={100}> - <div className="text-[0.9rem] min-w-[10rem]"> - <div className="card-body p-0"> - <div className="h-[1px] w-full bg-secondary-200"></div> - <div className="px-2.5 text-[0.8rem]"> - {type === 'node' - ? data && 'attributes' in data - ? renderNodeDetails(data as NodeType) - : null - : data && isGeoJsonType(data) - ? renderAreaDetails(data as GeoJsonType) - : renderLocationDetails(data as SearchResultType)} - </div> - <div className="h-[1px] w-full"></div> - </div> - </div> - </TooltipProvider> - ); -}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/index.ts b/libs/shared/lib/vis/visualizations/mapvis/components/index.ts index 9b32285c3da9231d2de7c72905792579458184c1..6d9b032707c9167ebf01592cce0912e4f163a47f 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/index.ts +++ b/libs/shared/lib/vis/visualizations/mapvis/components/index.ts @@ -1,4 +1,4 @@ export * from './ActionBar'; export * from './Attribution'; export * from './MapSettings'; -export * from './Tooltip'; +export * from './MapTooltip'; diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 9ee3fed2f4d87a7a81bf00a670f301a6f9d94e2c..b53acfa01d9417cb83c10e9f67592874a5e42d9b 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -12,6 +12,7 @@ import { VisualizationTooltip } from '@graphpolaris/shared/lib/components/Visual import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; import { isGeoJsonType, rgbToHex } from './utils'; import { NodeType } from '../nodelinkvis/types'; +import { ChoroplethLayer } from './layers/choropleth-layer/ChoroplethLayer'; export type MapProps = { layer: LayerTypes; @@ -140,12 +141,12 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx flyToBoundingBox: flyToBoundingBox, setLayerIds: (val: string[]) => setLayerIds(val), }; - if (dataLayer && dataLayer.id === layer.id) { setDataLayer({ component: dataLayer.component.clone(layerProps), id: props.settings.layer }); } else { // @ts-ignore setDataLayer({ component: new layer.component(layerProps), id: props.settings.layer }); + setSelected([]); } }, [props.settings.layer, props.data, selected, hoverObject, props.settings]); @@ -196,35 +197,67 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx const handleClick = useCallback( (object: any) => { if (props.data) { + // Deselect everything if no object is clicked if (!object) { props.handleSelect(); setSelected([]); setSearchResult(undefined); return; } + + // Handle clicking on a node (when it's not a feature in the choropleth layer) if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) { const objectLocation: Coordinate = coordinateLookup[object.id]; props.handleSelect({ nodes: [object] }); + if (objectLocation) { const [x, y] = coordinateToXY(objectLocation); - setSelected([{ ...object, x, y, lon: objectLocation[0], lat: objectLocation[1], selectedType: 'node' }]); + setSelected([ + { + ...object, + x, + y, + lon: objectLocation[0], + lat: objectLocation[1], + selectedType: 'node', + }, + ]); } } - if (object.type === 'Feature') { - const centroid = geoCentroid(object); - if (centroid) { - const [x, y] = coordinateToXY(centroid); - setSelected([{ ...object, x, y, selectedType: 'area' }]); - } - const ids = object.properties.nodes; - if (ids && ids.length > 0) { - const nodes = props.data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id)); - props.handleSelect({ nodes: [...nodes] }); + + // Handle clicking on a geographic feature (Choropleth layer) + if (object.type === 'Feature' && props.settings.layer === 'choropleth') { + // Ensure the current dataLayer is of ChoroplethLayer type + if (dataLayer?.component instanceof ChoroplethLayer) { + const choroplethLayer = dataLayer.component; + + // Get the color of the selected area using the ChoroplethLayer's getColor method + const selectedColor = choroplethLayer.getColor(object); + + const centroid = geoCentroid(object); + if (centroid) { + const [x, y] = coordinateToXY(centroid); + setSelected([ + { + ...object, + x, + y, + selectedType: 'area', + color: selectedColor, + }, + ]); + } + // If the feature contains node IDs, handle selecting the corresponding nodes + const ids = object.properties.nodes; + if (ids && ids.length > 0) { + const nodes = props.data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id)); + props.handleSelect({ nodes: [...nodes] }); + } } } } }, - [props, coordinateLookup, coordinateToXY], + [props, coordinateLookup, coordinateToXY, dataLayer], ); useEffect(() => { @@ -270,11 +303,17 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx ? (node as SearchResultType)?.name : 'N/A' } - colorHeader={rgbToHex( - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[0] ?? 0, - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[1] ?? 0, - props?.settings?.nodelink?.nodes?.[node.label]?.color?.[2] ?? 0, - )} + colorHeader={ + node.selectedType === 'node' + ? rgbToHex( + props?.settings?.nodelink?.nodes?.[node.label]?.color?.[0] ?? 0, + props?.settings?.nodelink?.nodes?.[node.label]?.color?.[1] ?? 0, + props?.settings?.nodelink?.nodes?.[node.label]?.color?.[2] ?? 0, + ) + : node.selectedType === 'area' + ? rgbToHex(node.color[0], node.color[1], node.color[2]) + : 'hsl(var(--clr-node))' + } > <MapTooltip type={node.selectedType} data={{ ...node }} key={node._id} /> </VisualizationTooltip> @@ -285,7 +324,7 @@ export const MapVis = forwardRef((props: VisualizationPropTypes<MapProps>, refEx <Tooltip open={true} interactive={false} boundaryElement={ref} showArrow={true}> <TooltipTrigger x={searchResult.x} y={searchResult.y} /> <TooltipContent> - <VisualizationTooltip name={searchResult.name} colorHeader="#FB9637"> + <VisualizationTooltip name={searchResult.name} colorHeader="hsl(var(--clr-node))"> <MapTooltip type="location" data={{ ...searchResult }} key={searchResult.name} /> </VisualizationTooltip> </TooltipContent>