From cb54eda73af75c2e03c88e7f99589dd6276ed4a7 Mon Sep 17 00:00:00 2001 From: Leonardo <leomilho@gmail.com> Date: Tue, 18 Jun 2024 17:39:09 +0200 Subject: [PATCH] feat(map_nodelink): added nodelink layer --- .../vis/components/config/SelectionConfig.tsx | 4 +- .../vis/visualizations/mapvis/MapSettings.tsx | 50 ++++- .../mapvis/components/layers/index.tsx | 2 +- .../layers/nodelink-layer/NodeLinkLayer.tsx | 206 +++++------------- .../lib/vis/visualizations/mapvis/mapvis.tsx | 51 +++-- 5 files changed, 132 insertions(+), 181 deletions(-) diff --git a/libs/shared/lib/vis/components/config/SelectionConfig.tsx b/libs/shared/lib/vis/components/config/SelectionConfig.tsx index 7d41cf4a1..8fc395725 100644 --- a/libs/shared/lib/vis/components/config/SelectionConfig.tsx +++ b/libs/shared/lib/vis/components/config/SelectionConfig.tsx @@ -25,7 +25,7 @@ export const SelectionConfig = () => { /> </div> {selection.content.map((item, index) => ( - <> + <div key={index}> <div key={index + 'id'} className="flex justify-between items-center px-4 py-1 gap-1"> <span className="text-xs font-normal">ID</span> <span className="text-xs">{item._id}</span> @@ -43,7 +43,7 @@ export const SelectionConfig = () => { </div> ); })} - </> + </div> ))} </div> ); diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx index 5e411a08c..ede7b0ad2 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx @@ -1,19 +1,51 @@ import React, { useMemo } from 'react'; import { SettingsContainer } from '../../components/config'; import { layerTypes } from './components/layers'; -import { Input } from '../../..'; +import { EntityPill, Input } from '../../..'; import { VisualizationSettingsPropTypes } from '../../common'; import { MapProps } from './mapvis'; +import { nodeColorHex } from '../nodelinkvis/components/utils'; + +const DataLayerSettings = ({ + layer, + settings, + graphMetadata, + updateSettings, +}: VisualizationSettingsPropTypes<MapProps> & { layer: keyof typeof layerTypes }) => { + switch (layer) { + case 'nodelink': + return ( + <> + {graphMetadata.nodes.labels.map((item, index) => ( + <div className="flex m-1 items-center" key={item}> + <div className="w-3/4 mr-6"> + <EntityPill title={item} /> + </div> + <div className="w-1/2"> + <div className={`h-5 w-5 border-2 border-sec-300`} style={{ backgroundColor: nodeColorHex(index + 1) }}></div> + </div> + </div> + ))} + + <Input + label="Enable brushing" + type="boolean" + value={settings.enableBrushing} + onChange={(val) => { + console.log('update brush', val); + updateSettings({ enableBrushing: val as boolean }); + }} + /> + </> + ); + default: + return; + } +}; export const MapSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) => { - // const spatialAttributes = useMemo(() => { - // if (!settings.node) return []; - // return Object.entries(graphMetadata.nodes.types[settings.node].attributes) - // .filter((kv) => kv[1].dimension === 'spatial') - // .map((kv) => kv[0]); - // }, [settings.node]); const spatialAttributes = useMemo(() => { - if (!settings.node) return []; + if (!settings.node || !(Object.keys(graphMetadata.nodes.types).length > 0)) return []; return Object.entries(graphMetadata.nodes.types[settings.node].attributes).map((kv) => kv[0]); }, [settings.node]); @@ -58,6 +90,8 @@ export const MapSettings = ({ settings, graphMetadata, updateSettings }: Visuali disabled={!settings.node || spatialAttributes.length < 1} onChange={(val) => updateSettings({ lon: val as string })} /> + + <DataLayerSettings layer={settings.layer} settings={settings} graphMetadata={graphMetadata} updateSettings={updateSettings} /> </SettingsContainer> ); }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx index e4f8f75cd..c09ce9458 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx @@ -7,7 +7,7 @@ import { NodeIconLayer } from './icon-layer/IconLayer'; export const layerTypes: Record<string, any> = { node: NodeLayer, icon: NodeIconLayer, - // nodelink: NodeLinkLayer, + nodelink: NodeLinkLayer, choropleth: ChoroplethLayer, heatmap: HeatLayer, }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx index 81a8c1a0b..45362508b 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { CompositeLayer } from 'deck.gl'; -import { IconLayer, LineLayer, TextLayer } from '@deck.gl/layers'; -import NodeLinkOptions from './NodeLinkOptions'; -import { createIcon } from './shapeFactory'; -import { getProperty } from '../../../utlis'; -import { Edge, Node, LayerProps } from '../../../mapvis.types'; +import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers'; +import { LayerProps } from '../../../mapvis.types'; +import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions'; export const NodeLinkConfig = { showLabels: false, @@ -22,159 +20,75 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { static type = 'NodeLink'; static layerOptions = NodeLinkConfig; - static generateLayerOptions(layer: any, updatedLayer: void, graphInfo: { [key: string]: any }, deleteLayer: void) { - return <NodeLinkOptions layer={layer} updatedLayer={updatedLayer} graphInfo={graphInfo} deleteLayer={deleteLayer} />; - } - shouldUpdateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { return changeFlags.propsChanged; } - updateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { - console.log(props, oldProps, context, changeFlags); - } - renderLayers() { - const { graph, config, visible, isSelecting, hoverObject } = this.props; - - // if (!visible) return; + const { graph, config, visible, getNodeLocation, selected } = this.props; const layers = []; - layers.push( - new IconLayer( - this.getSubLayerProps({ - id: 'all-nodes', - data: graph.getNodes().filter((node: Node) => !this.props.selected.includes(node)), - pickable: true, - sizeScale: 5, - getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize), - opacity: this.props.selected.length > 0 ? 0.05 : 1, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getIcon: (d: any) => { - const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`); - const shape = this.props.config.iconMapping[shapeProperty]; - const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0]; + const brushingExtension = new BrushingExtension(); + const collisionFilter = new CollisionFilterExtension(); - return { - url: createIcon(shape, color), - width: 24, - height: 24, - }; - }, - mask: true, - }), - ), + layers.push( + new ScatterplotLayer({ + hidden: visible, + data: graph.nodes, + pickable: true, + radiusScale: 6, + radiusMinPixels: 7, + radiusMaxPixels: 100, + lineWidthMinPixels: 1, + getPosition: (d: any) => getNodeLocation(d.id), + getFillColor: (d: any) => { + if (d.label === 'PERSON') { + return [182, 154, 239]; + } else if (d.label === 'INCIDENT') { + return [169, 25, 25]; + } + return [0, 0, 0]; + }, + getRadius: (d: any) => 5, + }), ); - if (this.props.selected.length > 0) { - const edges = graph.getEdges().filter((edge: Edge) => new Set(this.props.selected.map((node: Node) => node.id)).has(edge.from)); - - const nodes = edges.map((edge: Edge) => graph.getNode(edge.to)); - - layers.push([ - new IconLayer( - this.getSubLayerProps({ - id: 'selected-nodes', - data: this.props.selected, - pickable: true, - sizeScale: 5, - getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize), - opacity: 1, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getIcon: (d: any) => { - const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`); - const shape = this.props.config.iconMapping[shapeProperty]; - const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0]; - return { - url: createIcon(shape, color), - width: 24, - height: 24, - }; - }, - mask: true, - getColor: (d: any) => [200, 140, 0], - }), - ), - new IconLayer( - this.getSubLayerProps({ - id: 'target-nodes', - data: nodes, - pickable: true, - sizeScale: 5, - getSize: (d: any) => (config.nodeSizeDynamic ? Math.min(d.connectedEdges.length, 5) : config.nodeSize), - opacity: 1, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getIcon: (d: any) => { - const shapeProperty = getProperty(d, `attributes.${this.props.config.shapeAccessor}`); - const shape = this.props.config.iconMapping[shapeProperty]; - const color = this.props.config.colorMapping[shapeProperty] || [255, 125, 0]; - return { - url: createIcon(shape, color), - width: 24, - height: 24, - }; - }, - mask: true, - getColor: (d: any) => [200, 140, 0], - }), - ), - new LineLayer( - this.getSubLayerProps({ - id: 'edges', - data: edges, - pickable: true, - getWidth: (d: any) => config.edgeWidth, - getSourcePosition: (d: any) => graph.getNodeLocation(d.from), - getTargetPosition: (d: any) => graph.getNodeLocation(d.to), - getColor: (d: any) => [0, 0, 0], - }), - ), - new TextLayer( - this.getSubLayerProps({ - id: 'label-selected', - data: this.props.selected, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getText: (d: any) => d.id, - getSize: 15, - visible: config.showLabels, - getAlignmentBaseline: 'top', - background: true, - getBackgroundColor: [255, 125, 0], - getPixelOffset: [10, 10], - }), - ), - new TextLayer( - this.getSubLayerProps({ - id: 'label-target', - data: nodes, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getText: (d: any) => d.id, - getSize: 15, - visible: config.showLabels, - getAlignmentBaseline: 'top', - background: true, - getPixelOffset: [10, 10], - }), - ), - ]); - } + layers.push( + new LineLayer({ + id: 'edges', + data: graph.edges, + pickable: true, + getWidth: (d: any) => 2, + getSourcePosition: (d: any) => getNodeLocation(d.from), + getTargetPosition: (d: any) => getNodeLocation(d.to), + getColor: (d: any) => [145, 168, 208], + radiusScale: 3000, + brushingEnabled: config.enableBrushing, + extensions: [brushingExtension], + }), + ); - if (hoverObject && config.edgesOnHover) { - layers.push( - new LineLayer( - this.getSubLayerProps({ - id: 'edges-hover', - data: graph.getEdges().filter((edge: Edge) => edge.from === hoverObject.id), - pickable: true, - getWidth: (d: any) => config.edgeWidth, - getSourcePosition: (d: any) => graph.getNodeLocation(d.from), - getTargetPosition: (d: any) => graph.getNodeLocation(d.to), - getColor: (d: any) => [0, 0, 0], - }), - ), - ); - } + layers.push( + new TextLayer({ + id: 'label-target', + data: graph.nodes, + getPosition: (d: any) => getNodeLocation(d.id), + getText: (d: any) => d.id, + getSize: 15, + visible: true, + getAlignmentBaseline: 'top', + background: true, + getPixelOffset: [10, 10], + extensions: [collisionFilter], + collisionEnabled: true, + getCollisionPriority: (d: any) => d.id, + collisionTestProps: { sizeScale: 10 }, + getRadius: 10, + radiusUnits: 'pixels', + collisionGroup: 'text', + }), + ); return [...layers]; } diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 9a254d9de..47eba03d3 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -14,6 +14,7 @@ export type MapProps = { node: undefined | string; lat: string; lon: string; + enableBrushing: boolean; }; const settings: MapProps = { @@ -21,6 +22,7 @@ const settings: MapProps = { node: undefined, lat: 'gp_latitude', lon: 'gp_longitude', + enableBrushing: false, }; const INITIAL_VIEW_STATE = { @@ -35,7 +37,7 @@ const FLY_SPEED = 1000; const baseLayer = createBaseMap(); -export const MapVis = ({ data, settings, updateSettings, graphMetadata }: VisualizationPropTypes<MapProps>) => { +export const MapVis = ({ data, settings, updateSettings, graphMetadata, handleSelect }: VisualizationPropTypes<MapProps>) => { const [layer, setLayer] = React.useState<Layer | undefined>(undefined); const [viewport, setViewport] = React.useState<Record<string, any>>(INITIAL_VIEW_STATE); const [hoverObject, setHoverObject] = React.useState<Node | null>(null); @@ -79,9 +81,7 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual id: Date.now(), name: 'New layer', type: layerType, - config: { - ...layerType.layerOptions, - }, + config: settings, visible: true, }); }, [settings.layer]); @@ -112,7 +112,6 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual }, {} as { [id: string]: Coordinate }, ); - // console.log('coordinateLookup', coordinateLookup); return new layer.type({ id: `${layer.id}`, @@ -145,24 +144,6 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual [selectingRectangle, layer], ); - const handleSelect = useCallback((info: any, event: any) => { - const shiftPressed = event.srcEvent.shiftKey; - setIsSelecting(shiftPressed); - setSelected((prevSelected) => { - if (!shiftPressed) { - return info.object !== undefined ? [info.object] : []; - } else { - const selectedIndex = prevSelected.findIndex((obj) => obj === info.object); - if (selectedIndex !== -1) { - prevSelected.splice(selectedIndex, 1); - } else { - prevSelected.push(info.object); - } - return [...prevSelected]; - } - }); - }, []); - return ( <div className="w-full h-full flex-grow relative overflow-hidden"> <DeckGL @@ -170,7 +151,29 @@ export const MapVis = ({ data, settings, updateSettings, graphMetadata }: Visual controller={true} initialViewState={viewport} onViewStateChange={({ viewState }) => setViewport(viewState)} - onClick={handleSelect} + onClick={({ object }) => { + if (data) { + if (!object) { + handleSelect(); + return; + } + if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) { + handleSelect({ nodes: [object] }); + setSelected([object.id]); + } + if (object.type === 'Feature') { + const ids = object.properties.nodes; + if (ids.length > 0) { + const nodes = data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id)); + handleSelect({ nodes: [...nodes] }); + } else { + handleSelect(); + setSelected([]); + return; + } + } + } + }} onHover={({ object }) => { setHoverObject(object !== undefined ? object : null); }} -- GitLab