From 6332043ecbc9e964aa3b7fc061ec009340797a4b Mon Sep 17 00:00:00 2001 From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl> Date: Mon, 27 May 2024 15:30:38 +0000 Subject: [PATCH] feat(map_layers): fixed flicker error and added layers --- .../choropleth-layer/newChoroplethLayer.tsx | 102 ++++++++++ .../layers/heatmap-layer/HeatLayer.tsx | 187 ++++++++---------- .../layers/heatmap-layer/HeatLayerOptions.tsx | 155 --------------- .../mapvis/components/layers/index.tsx | 6 +- .../visualizations/mapvis/configuration.tsx | 65 ++++++ .../lib/vis/visualizations/mapvis/mapvis.tsx | 133 ++++--------- 6 files changed, 288 insertions(+), 360 deletions(-) create mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/choropleth-layer/newChoroplethLayer.tsx delete mode 100644 libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayerOptions.tsx create mode 100644 libs/shared/lib/vis/visualizations/mapvis/configuration.tsx diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/choropleth-layer/newChoroplethLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/choropleth-layer/newChoroplethLayer.tsx new file mode 100644 index 000000000..33d06d740 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/choropleth-layer/newChoroplethLayer.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { CompositeLayer } from 'deck.gl'; +import { ArcLayer, GeoJsonLayer, ScatterplotLayer } from '@deck.gl/layers'; +import { netherlands } from '../../../../../../mock-data/geo-json'; +import { Edge, Node, LayerProps } from '../../../mapvis.types'; +import { RGBColor, color, geoBounds, geoCentroid, geoContains, interpolateYlOrRd, scaleSequential } from 'd3'; + +export class ChoroplethLayer extends CompositeLayer<LayerProps> { + static type = 'Choropleth'; + + shouldUpdateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { + return changeFlags.dataChanged || changeFlags.propsChanged; + } + + updateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { + if (changeFlags.dataChanged) { + const nodes = props.graph.nodes; + const geojson = netherlands; + const data = this.concatData(geojson, nodes); + this.setState({ data }); + } + } + + concatData(geojson: any, nodes: Node[]) { + const updatedGeojson = { ...geojson }; + + nodes.map((node: Node) => { + const coordinates: [number, number] = this.props.getNodeLocation(node.id); + + if (coordinates) { + updatedGeojson.features.map((feature: any) => { + const isInside = geoContains(feature, coordinates); + if (isInside) { + feature.properties.nodes = feature.properties.nodes ?? []; + feature.properties.nodes.push(node.id); + + feature.properties.townships.map((township: any) => { + const isInside = geoContains(township, coordinates); + + if (isInside) { + township.properties.nodes = township.properties.nodes ?? []; + township.properties.nodes.push(node.id); + } + }); + return; + } + }); + } + }); + + return updatedGeojson; + } + + getColor(polygon: any, min: number, max: number) { + const nodeLength = polygon.properties?.nodes?.length ?? 0; + + if (nodeLength === min) return [128, 128, 128]; + + const colorScale = scaleSequential(interpolateYlOrRd).domain([min, max]); + const _color = color(colorScale(nodeLength)) as RGBColor; + + return _color ? [_color.r, _color.g, _color.b] : [100, 0, 0]; + } + + renderLayers() { + const { data } = this.state; + + const layers: any = []; + + let max = 0; + + (data as any).features.forEach((feature: any) => { + const nodeCount = feature.properties?.nodes ? feature.properties.nodes.length : 0; + if (nodeCount > max) max = nodeCount; + }); + + (data as any).features.forEach((feature: any) => { + layers.push( + new GeoJsonLayer( + this.getSubLayerProps({ + id: `feature-layer-${feature.properties.name}`, + data: feature, + opacity: 0.3, + pickable: true, + stroked: true, + filled: true, + extruded: false, + lineWidthScale: 0.5, + lineWidthMinPixels: 1, + getLineWidth: (d: any) => 1, + getLineColor: (d: any) => [220, 220, 220], + getFillColor: (d: any) => this.getColor(d, 0, max), + }), + ), + ); + }); + + return [...layers]; + } +} + +ChoroplethLayer.layerName = 'Choropleth'; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx index dbc43b33a..48a42c8a4 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayer.tsx @@ -1,46 +1,11 @@ import React from 'react'; import { CompositeLayer, HeatmapLayer } from 'deck.gl'; -import HeatLayerOptions from './HeatLayerOptions'; import * as d3 from 'd3'; import { getDistance, getProperty } from '../../../utlis'; import { Edge, LayerProps } from '../../../mapvis.types'; -/* -Potential use cases: - - Logistics company - Analyze the distribution of departure and arrival locations for shipments - Areas with high concentrations of departures or arrivals can be visualized - Optimize routes, allocate resources efficiently, and identify areas with higher demand for their services - - Marketing company - Analyzing graphs or networks that represent various attributes among entities - Visualize social network interactions, with the color intensity showing the strength or frequency of interactions between users - Identify key influencers, communities, or trends, enabling the marketing company to tailor their strategies based on these insights - - Mobility company - E.g. ride-sharing service or a public transportation provider - Understanding the lengths of outgoing edges (distances) from specific nodes - Highlight areas with longer average distances or higher travel times - Guide decisions like where to position hubs, optimize vehicle placement -*/ - -export const typesMap = ['location', 'distance', 'attribute', 'path']; - -export const locationMap = ['source', 'target']; - -export const HeatLayerConfig = { - type: 'location', - location: 'source', - entity: 'nodeAttributes', - nSegments: 10, - attribute: '', -}; - export class HeatLayer extends CompositeLayer<LayerProps> { static type = 'Heatmap'; - static layerOptions = HeatLayerConfig; - - static generateLayerOptions(layer: any, updatedLayer: void, graphInfo: { [key: string]: any }, deleteLayer: void) { - return <HeatLayerOptions layer={layer} updatedLayer={updatedLayer} graphInfo={graphInfo} deleteLayer={deleteLayer} />; - } updateState({ props, oldProps, context, changeFlags }: { props: any; oldProps: any; context: any; changeFlags: any }) { console.log(props, oldProps, context, changeFlags); @@ -78,80 +43,86 @@ export class HeatLayer extends CompositeLayer<LayerProps> { } renderLayers() { - const { graph, config, visible } = this.props; - - // if (!visible) return; - - const layers = []; - - if (config.type === 'location') { - layers.push( - new HeatmapLayer( - this.getSubLayerProps({ - data: - config.location === 'source' - ? graph.getEdges().map((edge: Edge) => graph.getNode(edge.from)) - : graph.getEdges().map((edge: Edge) => graph.getNode(edge.to)), - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - aggregation: 'SUM', - }), - ), - ); - } else if (config.type === 'distance') { - layers.push( - new HeatmapLayer( - this.getSubLayerProps({ - data: graph.getEdges().map((edge: Edge) => { - const from = graph.getNode(edge.from); - const from_coords: [number, number] = [from.attributes.long, from.attributes.lat]; - const to = graph.getNode(edge.to); - const to_coords: [number, number] = [to.attributes.long, to.attributes.lat]; - const dist = getDistance(from_coords, to_coords); - const node = config.location === 'source' ? from : to; - return { ...node, distance: dist }; - }), - threshold: 0.5, - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getWeight: (d: any) => d.distance, - aggregation: 'MEAN', - }), - ), - ); - } else if (config.type === 'attribute') { - console.log('attribute'); - layers.push( - new HeatmapLayer( - this.getSubLayerProps({ - data: graph.getEdges().map((edge: Edge) => graph.getNode(edge.from)), - getPosition: (d: any) => [d.attributes.long, d.attributes.lat], - getWeight: (d: any) => { - console.log(d, d.attributes[config.attribute]); - return 1; - }, - aggregation: 'SUM', - }), - ), - ); - } else if (config.type === 'path') { - layers.push( - new HeatmapLayer( - this.getSubLayerProps({ - data: this.createSegments( - graph.getEdges().map((edge: Edge) => { - return { - ...edge, - path: [this.props.graph.getNodeLocation(edge.from), this.props.graph.getNodeLocation(edge.to)], - }; - }), - ).flatMap((edge) => edge.path), - getPosition: (d: any) => d, - aggregation: 'SUM', - }), - ), - ); - } + const { graph, config, visible, getNodeLocation } = this.props; + + return new HeatmapLayer({ + data: graph.nodes, + hidden: visible, + getPosition: (d: any) => getNodeLocation(d.id), + getWeight: (d) => 1, + aggregation: 'SUM', + }); - return [...layers]; + // const layers = []; + + // if (config.type === 'location') { + // layers.push( + // new HeatmapLayer( + // this.getSubLayerProps({ + // data: + // config.location === 'source' + // ? graph.getEdges().map((edge: Edge) => graph.getNode(edge.from)) + // : graph.getEdges().map((edge: Edge) => graph.getNode(edge.to)), + // getPosition: (d: any) => [d.attributes.long, d.attributes.lat], + // aggregation: 'SUM', + // }), + // ), + // ); + // } else if (config.type === 'distance') { + // layers.push( + // new HeatmapLayer( + // this.getSubLayerProps({ + // data: graph.getEdges().map((edge: Edge) => { + // const from = graph.getNode(edge.from); + // const from_coords: [number, number] = [from.attributes.long, from.attributes.lat]; + // const to = graph.getNode(edge.to); + // const to_coords: [number, number] = [to.attributes.long, to.attributes.lat]; + // const dist = getDistance(from_coords, to_coords); + // const node = config.location === 'source' ? from : to; + // return { ...node, distance: dist }; + // }), + // threshold: 0.5, + // getPosition: (d: any) => [d.attributes.long, d.attributes.lat], + // getWeight: (d: any) => d.distance, + // aggregation: 'MEAN', + // }), + // ), + // ); + // } else if (config.type === 'attribute') { + // console.log('attribute'); + // layers.push( + // new HeatmapLayer( + // this.getSubLayerProps({ + // data: graph.getEdges().map((edge: Edge) => graph.getNode(edge.from)), + // getPosition: (d: any) => [d.attributes.long, d.attributes.lat], + // getWeight: (d: any) => { + // console.log(d, d.attributes[config.attribute]); + // return 1; + // }, + // aggregation: 'SUM', + // }), + // ), + // ); + // } else if (config.type === 'path') { + // layers.push( + // new HeatmapLayer( + // this.getSubLayerProps({ + // data: this.createSegments( + // graph.getEdges().map((edge: Edge) => { + // return { + // ...edge, + // path: [this.props.graph.getNodeLocation(edge.from), this.props.graph.getNodeLocation(edge.to)], + // }; + // }), + // ).flatMap((edge) => edge.path), + // getPosition: (d: any) => d, + // aggregation: 'SUM', + // }), + // ), + // ); + // } + + // return [...layers]; } } diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayerOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayerOptions.tsx deleted file mode 100644 index 307671269..000000000 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/heatmap-layer/HeatLayerOptions.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React from 'react'; -import { locationMap, typesMap } from './HeatLayer'; - -type Props = { - layer: any; - updatedLayer: any; - graphInfo: any; - deleteLayer: any; -}; - -export default function HeatLayerOptions({ layer, updatedLayer, graphInfo, deleteLayer }: Props) { - console.log(layer.config); - - return ( - <div> - <div className="divider m-0"></div> - - <div> - <select - className="select select-bordered w-full max-w-xs" - value={layer.config.type} - onChange={(e) => - updatedLayer(layer.id, { - config: { - ...layer.config, - type: e.target.value, - }, - }) - } - > - <option disabled selected> - Type - </option> - {typesMap.map((type) => ( - <option key={type} value={type}> - {type} - </option> - ))} - </select> - - {layer.config.type !== 'path' && layer.config.type !== 'attribute' && ( - <select - className="select select-bordered w-full max-w-xs" - value={layer.config.location} - onChange={(e) => - updatedLayer(layer.id, { - config: { - ...layer.config, - location: e.target.value, - }, - }) - } - > - <option disabled selected> - Node - </option> - {locationMap.map((type) => ( - <option key={type} value={type}> - {type} - </option> - ))} - </select> - )} - - {layer.config.type === 'path' && ( - <div className="p-2"> - <p>Number of segments</p> - <div style={{ display: 'flex', justifyContent: 'space-between' }}> - <input - className="range" - type="range" - min={1} - max={20} - value={layer.config.nSegments} - onChange={(e) => - updatedLayer(layer.id, { - config: { - ...layer.config, - nSegments: e.target.value, - }, - }) - } - /> - <p>{layer.config.nSegments}</p> - </div> - </div> - )} - - {layer.config.type === 'attribute' && ( - <> - <select - className="select select-bordered w-full max-w-xs" - value={layer.config.entity} - onChange={(e) => - updatedLayer(layer.id, { - config: { - ...layer.config, - entity: e.target.value, - }, - }) - } - > - <option disabled selected> - Entity - </option> - <option value={'nodeAttributes'}>Node</option> - <option value={'edgeAttributes'}>Edge</option> - </select> - - <select - className="select select-bordered w-full max-w-xs" - value={layer.config.attribute} - onChange={(e) => - updatedLayer(layer.id, { - config: { - ...layer.config, - attribute: e.target.value, - }, - }) - } - > - <option disabled selected> - Attribute - </option> - {Object.keys(graphInfo[layer.config.entity]).map((value) => ( - <option key={value} value={value}> - {value} - </option> - ))} - </select> - </> - )} - </div> - - <div className="divider m-0"></div> - - <div> - <button - className="btn btn-primary w-full" - onClick={() => - updatedLayer(layer.id, { - visible: !layer.visible, - }) - } - > - {layer.visible ? 'Hide layer' : 'Show layer'} - </button> - - <button className="btn btn-outline btn-primary w-full mt-1" onClick={() => deleteLayer(layer.id)}> - Delete layer - </button> - </div> - </div> - ); -} 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 6cb501224..e4f8f75cd 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/layers/index.tsx @@ -1,4 +1,4 @@ -import { ChoroplethLayer } from './choropleth-layer/ChoroplethLayer'; +import { ChoroplethLayer } from './choropleth-layer/newChoroplethLayer'; import { HeatLayer } from './heatmap-layer/HeatLayer'; import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer'; import { NodeLayer } from './node-layer/NodeLayer'; @@ -8,6 +8,6 @@ export const layerTypes: Record<string, any> = { node: NodeLayer, icon: NodeIconLayer, // nodelink: NodeLinkLayer, - // choropleth: ChoroplethLayer, - // heatmap: HeatLayer, + choropleth: ChoroplethLayer, + heatmap: HeatLayer, }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx b/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx new file mode 100644 index 000000000..b295e90df --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/configuration.tsx @@ -0,0 +1,65 @@ +import React, { useMemo } from 'react'; +import { SettingsContainer } from '../../components/config'; +import { layerTypes } from './components/layers'; +import { Input } from '../../..'; +import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; +import { MapProps } from './mapvis'; + +export const MapSettings = ({ + configuration, + graphMetadata, + updateSettings, +}: { + configuration: MapProps; + graphMetadata: GraphMetadata; + updateSettings: (val: any) => void; +}) => { + // const spatialAttributes = useMemo(() => { + // if (!configuration.node) return []; + // return Object.entries(graphMetadata.nodes.types[configuration.node].attributes) + // .filter((kv) => kv[1].dimension === 'spatial') + // .map((kv) => kv[0]); + // }, [configuration.node]); + const spatialAttributes = useMemo(() => { + if (!configuration.node) return []; + return Object.entries(graphMetadata.nodes.types[configuration.node].attributes).map((kv) => kv[0]); + }, [configuration.node]); + + return ( + <SettingsContainer> + <span className="text-xs font-semibold">Data layer</span> + <Input + type="dropdown" + value={configuration.layer} + options={Object.keys(layerTypes)} + onChange={(val) => updateSettings({ layer: val })} + /> + + <span className="text-xs font-semibold">Node Label</span> + <Input + type="dropdown" + value={configuration.node} + options={[...Object.keys(graphMetadata.nodes.types)]} + disabled={Object.keys(graphMetadata.nodes.types).length < 1} + onChange={(val) => updateSettings({ node: val })} + /> + <span className="text-xs font-semibold">Location accessor (lat)</span> + <Input + type="dropdown" + value={configuration.lat} + options={[...spatialAttributes]} + disabled={!configuration.node || spatialAttributes.length < 1} + onChange={(val) => updateSettings({ lat: val })} + /> + + <span className="text-xs font-semibold">Location accessor (lon)</span> + <Input + type="dropdown" + value={configuration.lon} + options={[...spatialAttributes]} + disabled={!configuration.node || spatialAttributes.length < 1} + onChange={(val) => updateSettings({ lon: val })} + /> + </SettingsContainer> + ); +}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 692ba0053..56d182548 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -1,14 +1,12 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useCallback } from 'react'; import DeckGL from '@deck.gl/react'; -import { FlyToInterpolator, MapView, WebMercatorViewport } from '@deck.gl/core'; +import { FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core'; import { SelectionLayer } from '@deck.gl-community/editable-layers'; import { Coordinate, Layer } from './mapvis.types'; import { VISComponentType, VisualizationPropTypes } from '../../common'; -import { SettingsContainer } from '../../components/config'; import { layerTypes } from './components/layers'; import { createBaseMap } from './components/BaseMap'; -import { Input, useML } from '../../..'; -import { GraphMetadata } from '@graphpolaris/shared/lib/data-access/statistics'; +import { MapSettings } from './configuration'; export type MapProps = { layer: undefined | string; @@ -34,6 +32,8 @@ const INITIAL_VIEW_STATE = { const FLY_SPEED = 1000; +const baseLayer = createBaseMap(); + export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: VisualizationPropTypes) => { const [layer, setLayer] = React.useState<Layer | undefined>(undefined); const [viewport, setViewport] = React.useState<Record<string, any>>(INITIAL_VIEW_STATE); @@ -42,28 +42,34 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V const [isSelecting, setIsSelecting] = React.useState<boolean>(false); const [selectingRectangle, setSelectingRectangle] = React.useState<boolean>(false); - const getFittedViewport = (minLat: number, maxLat: number, minLon: number, maxLon: number) => { - const viewportWebMercator = new WebMercatorViewport(viewport).fitBounds( - [ - [minLon, minLat], - [maxLon, maxLat], - ], - { padding: 20 }, - ); - const { zoom, longitude, latitude } = viewportWebMercator; - return { zoom, longitude, latitude }; - }; - - const flyToBoundingBox = (minLat: number, maxLat: number, minLon: number, maxLon: number, options = {}) => { - const fittedViewport = getFittedViewport(minLat, maxLat, minLon, maxLon); - setViewport((prevViewport) => ({ - ...prevViewport, - ...options, - ...fittedViewport, - transitionDuration: FLY_SPEED, - transitionInterpolator: new FlyToInterpolator(), - })); - }; + const getFittedViewport = useCallback( + (minLat: number, maxLat: number, minLon: number, maxLon: number) => { + const viewportWebMercator = new WebMercatorViewport(viewport).fitBounds( + [ + [minLon, minLat], + [maxLon, maxLat], + ], + { padding: 20 }, + ); + const { zoom, longitude, latitude } = viewportWebMercator; + return { zoom, longitude, latitude }; + }, + [viewport], + ); + + const flyToBoundingBox = useCallback( + (minLat: number, maxLat: number, minLon: number, maxLon: number, options = {}) => { + const fittedViewport = getFittedViewport(minLat, maxLat, minLon, maxLon); + setViewport((prevViewport) => ({ + ...prevViewport, + ...options, + ...fittedViewport, + transitionDuration: FLY_SPEED, + transitionInterpolator: new FlyToInterpolator(), + })); + }, + [getFittedViewport], + ); useEffect(() => { if (configuration.layer) { @@ -82,7 +88,7 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V }, [configuration.layer]); useEffect(() => { - if (!graphMetadata.nodes.labels.includes(configuration.node)) { + if (configuration.node != undefined && !graphMetadata.nodes.labels.includes(configuration.node)) { updateSettings({ node: undefined }); } }, [graphMetadata.nodes.types, data, configuration]); @@ -92,8 +98,6 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce( (acc, node) => { - // const latitude = getProperty(node, configuration.lat); - // const longitude = getProperty(node, configuration.lon); const latitude = node?.attributes?.[configuration.lat] as string | undefined; const longitude = node?.attributes?.[configuration.lon] as string | undefined; @@ -135,7 +139,7 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V [selectingRectangle, layer], ); - const handleSelect = (info: any, event: any) => { + const handleSelect = useCallback((info: any, event: any) => { const shiftPressed = event.srcEvent.shiftKey; setIsSelecting(shiftPressed); setSelected((prevSelected) => { @@ -151,12 +155,12 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V return [...prevSelected]; } }); - }; + }, []); return ( - <div className="w-full h-full flex-grow relative"> + <div className="w-full h-full flex-grow relative overflow-hidden"> <DeckGL - layers={[createBaseMap(), dataLayer, selectionLayer]} + layers={[baseLayer, dataLayer]} controller={true} initialViewState={viewport} onViewStateChange={({ viewState }) => setViewport(viewState)} @@ -169,66 +173,7 @@ export const MapVis = ({ data, configuration, updateSettings, graphMetadata }: V ); }; -const MapSettings = ({ - configuration, - graphMetadata, - updateSettings, -}: { - configuration: MapProps; - graphMetadata: GraphMetadata; - updateSettings: (val: any) => void; -}) => { - // const spatialAttributes = useMemo(() => { - // if (!configuration.node) return []; - // return Object.entries(graphMetadata.nodes.types[configuration.node].attributes) - // .filter((kv) => kv[1].dimension === 'spatial') - // .map((kv) => kv[0]); - // }, [configuration.node]); - const spatialAttributes = useMemo(() => { - if (!configuration.node) return []; - return Object.entries(graphMetadata.nodes.types[configuration.node].attributes).map((kv) => kv[0]); - }, [configuration.node]); - - return ( - <SettingsContainer> - <span className="text-xs font-semibold">Data layer</span> - <Input - type="dropdown" - value={configuration.layer} - options={Object.keys(layerTypes)} - onChange={(val) => updateSettings({ layer: val })} - /> - - <span className="text-xs font-semibold">Node Label</span> - <Input - type="dropdown" - value={configuration.node} - options={[...Object.keys(graphMetadata.nodes.types)]} - disabled={Object.keys(graphMetadata.nodes.types).length < 1} - onChange={(val) => updateSettings({ node: val })} - /> - <span className="text-xs font-semibold">Location accessor (lat)</span> - <Input - type="dropdown" - value={configuration.lat} - options={[...spatialAttributes]} - disabled={!configuration.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lat: val })} - /> - - <span className="text-xs font-semibold">Location accessor (lon)</span> - <Input - type="dropdown" - value={configuration.lon} - options={[...spatialAttributes]} - disabled={!configuration.node || spatialAttributes.length < 1} - onChange={(val) => updateSettings({ lon: val })} - /> - </SettingsContainer> - ); -}; - -export const MapComponent: VISComponentType = { +const MapComponent: VISComponentType = { displayName: 'MapVis', component: MapVis, settings: MapSettings, -- GitLab