From dcf3d1ad7ad6fee38f898d14f55e6f9e5d51c4cb 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/visualizations/mapvis/MapSettings.tsx | 97 +++++++++++++++++++ .../mapvis/components/layers/index.tsx | 13 +++ .../layers/nodelink-layer/NodeLinkLayer.tsx | 97 +++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx create mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx create mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx diff --git a/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx new file mode 100644 index 000000000..ede7b0ad2 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/MapSettings.tsx @@ -0,0 +1,97 @@ +import React, { useMemo } from 'react'; +import { SettingsContainer } from '../../components/config'; +import { layerTypes } from './components/layers'; +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 || !(Object.keys(graphMetadata.nodes.types).length > 0)) return []; + return Object.entries(graphMetadata.nodes.types[settings.node].attributes).map((kv) => kv[0]); + }, [settings.node]); + + return ( + <SettingsContainer> + <Input + label="Data layer" + type="dropdown" + inline + value={settings.layer} + options={Object.keys(layerTypes)} + onChange={(val) => updateSettings({ layer: val as string })} + /> + + <Input + label="Node Label" + type="dropdown" + inline + value={settings.node} + options={[...Object.keys(graphMetadata.nodes.types)]} + disabled={Object.keys(graphMetadata.nodes.types).length < 1} + onChange={(val) => { + updateSettings({ node: val as string }); + }} + /> + <Input + label="Latitude Location" + type="dropdown" + inline + value={settings.lat} + options={[...spatialAttributes]} + disabled={!settings.node || spatialAttributes.length < 1} + onChange={(val) => updateSettings({ lat: val as string })} + /> + + <Input + inline + label="Longitude Location accessor" + type="dropdown" + value={settings.lon} + options={[...spatialAttributes]} + 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 new file mode 100644 index 000000000..c09ce9458 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx @@ -0,0 +1,13 @@ +import { ChoroplethLayer } from './choropleth-layer/newChoroplethLayer'; +import { HeatLayer } from './heatmap-layer/HeatLayer'; +import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer'; +import { NodeLayer } from './node-layer/NodeLayer'; +import { NodeIconLayer } from './icon-layer/IconLayer'; + +export const layerTypes: Record<string, any> = { + node: NodeLayer, + icon: NodeIconLayer, + 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 new file mode 100644 index 000000000..45362508b --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/nodelink-layer/NodeLinkLayer.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { CompositeLayer } from 'deck.gl'; +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, + nodeShapeDynamic: false, + shapeAccessor: '', + iconMapping: {}, + colorMapping: {}, + edgesOnHover: true, + nodeSizeDynamic: true, + nodeSize: 2, + edgeWidth: 1.5, +}; + +export class NodeLinkLayer extends CompositeLayer<LayerProps> { + static type = 'NodeLink'; + static layerOptions = NodeLinkConfig; + + shouldUpdateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { + return changeFlags.propsChanged; + } + + renderLayers() { + const { graph, config, visible, getNodeLocation, selected } = this.props; + + const layers = []; + + const brushingExtension = new BrushingExtension(); + const collisionFilter = new CollisionFilterExtension(); + + 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, + }), + ); + + 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], + }), + ); + + 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]; + } +} + +NodeLinkLayer.layerName = 'NodeLink'; -- GitLab