diff --git a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx index afe153a855b7670ec4acbaa4e05dd21eb1c60f0f..ef0e49e5e4876fb3ee00ec417779ead60f2dd34a 100644 --- a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx +++ b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx @@ -53,7 +53,6 @@ export default function ColorPicker({ value, updateValue }: Props) { triangle="top-right" color={{ r: value[0], g: value[1], b: value[2] }} onChangeComplete={(color) => { - console.log(color); const rgb = color.rgb; const newValue: [number, number, number] = [rgb.r, rgb.g, rgb.b]; updateValue(newValue); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx index 81f943b883c908aadc27263e22273821529e041d..dd407682b8d58002031a3c9d778445accf5ef3f2 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx @@ -1,10 +1,8 @@ import React from 'react'; import { CompositeLayer, Layer } from 'deck.gl'; -import { IconLayer, LineLayer, TextLayer } from '@deck.gl/layers'; +import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers'; import { CompositeLayerType, LayerProps } from '../../mapvis.types'; import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions'; -import { Node } from '@graphpolaris/shared/lib/data-access'; -import { createIcon } from './shapeFactory'; export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { static type = 'nodelink'; @@ -19,27 +17,40 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { } renderLayers() { - const { data, settings, getNodeLocation, ml, graphMetadata } = this.props; + const { data, settings, getNodeLocation, ml, graphMetadata, selected } = this.props; const layerSettings = settings[NodeLinkLayer.type]; const brushingExtension = new BrushingExtension(); const collisionFilter = new CollisionFilterExtension(); + const nodeLocations = data.nodes.reduce((acc: Record<string, [number, number]>, node: any) => { + const pos = getNodeLocation(node._id); + if (pos && (pos[0] !== 0 || pos[1] !== 0)) { + acc[node._id] = pos; + } + return acc; + }, {}); + graphMetadata.edges.labels.forEach((label: string) => { const layerId = `${label}-edges-line`; - const edgeData = data.edges; + + const edgeData = data.edges.filter((edge: any) => { + const from = nodeLocations[edge.from]; + const to = nodeLocations[edge.to]; + return from && to; + }); this._layers[layerId] = new LineLayer({ id: layerId, data: edgeData, - visible: !layerSettings.edges[label].hidden, + visible: !layerSettings?.edges[label]?.hidden, pickable: true, - getWidth: layerSettings.edges[label].width, + getWidth: layerSettings?.edges[label]?.width, getSourcePosition: (d) => getNodeLocation(d.from), getTargetPosition: (d) => getNodeLocation(d.to), - getColor: (d) => layerSettings.edges[d.label].color, + getColor: (d) => layerSettings?.edges[d.label]?.color, extensions: [brushingExtension], - brushingEnabled: layerSettings.enableBrushing, + brushingEnabled: layerSettings?.enableBrushing, }); }); @@ -57,44 +68,49 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { graphMetadata.nodes.labels.forEach((label: string) => { const layerId = `${label}-nodes-scatterplot`; + const textLayerId = `${label}-label-target`; - this._layers[layerId] = new IconLayer({ + const nodes = data.nodes.filter((node: any) => nodeLocations[node._id] && node.label === label); + + this._layers[layerId] = new ScatterplotLayer({ id: layerId, - visible: !layerSettings.nodes[label].hidden, - data: data.nodes.filter((node: Node) => node.label === label), + visible: !layerSettings?.nodes[label]?.hidden, + data: nodes, pickable: true, - getColor: (d) => [200, 140, 0], - getSize: (d) => layerSettings.nodes[label].size, + getFillColor: (d) => layerSettings?.nodes[label]?.color, getPosition: (d) => getNodeLocation(d._id), - getIcon: (d: any) => { - return { - url: createIcon(layerSettings.nodes[label].shape, layerSettings.nodes[label].color), - width: 24, - height: 24, - }; + getRadius: (d) => layerSettings?.nodes[label]?.size, + radiusMinPixels: 5, + getLineWidth: (d: any) => (selected && selected.some((sel) => sel._id === d._id) ? 2 : 1), + lineWidthUnits: 'pixels', + stroked: true, + updateTriggers: { + getIcon: [selected], }, }); - }); - const textLayerId = 'label-target'; - - this._layers[textLayerId] = new TextLayer({ - id: textLayerId, - data: data.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', + this._layers[textLayerId] = new TextLayer({ + id: textLayerId, + data: nodes, + getPosition: (d: any) => getNodeLocation(d._id), + getText: (d: any) => d.label, + getSize: (d: any) => (layerSettings?.nodes[label]?.size * 2) / d.label.length, + getAlignmentBaseline: 'center', + getRadius: 10, + radiusScale: 20, + getColor: [255, 255, 255], + sizeUnits: 'meters', + sizeMaxPixels: 64, + characterSet: 'auto', + fontFamily: 'monospace', + billboard: false, + getAngle: () => 0, + collisionGroup: 'textLabels', + extensions: [collisionFilter], + collisionEnabled: layerSettings?.collisionEnabled ?? true, + getCollisionPriority: () => 100, + collisionTestProps: { sizeScale: 5 }, + }); }); return Object.values(this._layers); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx index 306774b916bb3d26875f68e5f42164826024ee72..5668172943e5401a198876afbbd212c28052b17e 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx @@ -3,6 +3,25 @@ import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/col import { Button, DropdownColorLegend, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components'; import { MapProps } from '../../mapvis'; import { LayerSettingsComponentType } from '../../mapvis.types'; +import { nodeColorHex } from '../../utils'; + +const defaultNodeSettings = (index: number) => ({ + colorByAttribute: false, + colorAttribute: undefined, + colorAttributeType: undefined, + hidden: false, + shape: 'circle', + color: nodeColorHex(index), + size: 40, +}); + +const defaultEdgeSettings = () => ({ + hidden: false, + width: 1, + sizeAttribute: '', + fixed: true, + color: [132, 150, 155], +}); export function NodeLinkOptions({ settings, @@ -12,43 +31,36 @@ export function NodeLinkOptions({ updateSpatialAttribute, }: LayerSettingsComponentType<MapProps>) { const layerType = 'nodelink'; - const layerSettings = settings[layerType]; + const layerSettings = settings[layerType] || { enableBrushing: false, nodes: {}, edges: {} }; useEffect(() => { - if (!layerSettings) { - const initialSettingsObject = { enableBrushing: false, nodes: {}, edges: {} }; + const nodes = layerSettings.nodes || {}; + const edges = layerSettings.edges || {}; - graphMetadata.nodes.labels.forEach((node) => { - initialSettingsObject.nodes = { - ...initialSettingsObject.nodes, - [node]: { - colorByAttribute: false, - colorAttribute: undefined, - colorAttributeType: undefined, - hidden: false, - shape: 'circle', - color: [Math.floor(Math.random() * 251), Math.floor(Math.random() * 251), 0], - size: 10, - }, - }; - }); + const newNodes = graphMetadata.nodes.labels.reduce( + (acc, node, index) => { + acc[node] = nodes[node] || defaultNodeSettings(index); + return acc; + }, + {} as typeof nodes, + ); - graphMetadata.edges.labels.forEach((edge) => { - initialSettingsObject.edges = { - ...initialSettingsObject.edges, - [edge]: { - hidden: false, - width: 1, - sizeAttribute: '', - fixed: true, - color: [0, 0, 0], - }, - }; - }); + const newEdges = graphMetadata.edges.labels.reduce( + (acc, edge) => { + acc[edge] = edges[edge] || defaultEdgeSettings(); + return acc; + }, + {} as typeof edges, + ); - updateLayerSettings({ ...initialSettingsObject }); + if (JSON.stringify(newNodes) !== JSON.stringify(nodes) || JSON.stringify(newEdges) !== JSON.stringify(edges)) { + updateLayerSettings({ + ...layerSettings, + nodes: newNodes, + edges: newEdges, + }); } - }, [graphMetadata, settings, updateLayerSettings]); + }, [graphMetadata]); const handleCollapseToggle = (type: string, itemType: 'nodes' | 'edges') => { if (layerSettings) { @@ -71,266 +83,274 @@ export function NodeLinkOptions({ const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; return ( - <div className="mt-2" key={nodeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(nodeType, 'nodes')} - /> - <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}> - <EntityPill title={nodeType} /> + layerSettings?.nodes?.[nodeType] && ( + <div className="mt-2" key={nodeType}> + <div className="flex items-center"> + <Button + size="2xs" + iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} + variant="ghost" + onClick={() => handleCollapseToggle(nodeType, 'nodes')} + /> + <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}> + <EntityPill title={nodeType} /> + </div> </div> - </div> - {!nodeSettings.collapsed && ( - <div> - <Input - label="Hidden" - type="boolean" - value={nodeSettings.hidden} - onChange={(val) => - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }) - } - /> - <div className="border-t-2 my-2"> - <span className="font-bold mt-2">Location attributes</span> - <Input - inline - label="Latitude" - type="dropdown" - value={settings?.location[nodeType]?.lat} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} - /> + {!nodeSettings.collapsed && ( + <div> <Input - inline - label="Longitude" - type="dropdown" - value={settings?.location[nodeType]?.lon} - options={[...spatialAttributes[nodeType]]} - disabled={spatialAttributes[nodeType].length < 1} - onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} + label="Hidden" + type="boolean" + value={nodeSettings.hidden} + onChange={(val) => + updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }) + } /> - </div> + <div className="border-t-2 my-2"> + <span className="font-bold mt-2">Location attributes</span> + <Input + inline + label="Latitude" + type="dropdown" + value={settings?.location[nodeType]?.lat} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)} + /> + <Input + inline + label="Longitude" + type="dropdown" + value={settings?.location[nodeType]?.lon} + options={[...spatialAttributes[nodeType]]} + disabled={spatialAttributes[nodeType].length < 1} + onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)} + /> + </div> - <div className="border-t-2 my-2"> - <div className="flex justify-between"> - <span className="font-bold">Color</span> - {!nodeSettings.colorByAttribute && ( - <ColorPicker - value={nodeSettings.color} - updateValue={(val) => { - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } }); - }} + <div className="border-t-2 my-2"> + <div className="flex justify-between"> + <span className="font-bold">Color</span> + {!nodeSettings.colorByAttribute && ( + <ColorPicker + value={nodeSettings.color} + updateValue={(val) => { + updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } }); + }} + /> + )} + </div> + + <div> + <Input + label="By attribute" + type="boolean" + value={nodeSettings.colorByAttribute ?? false} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } }, + }) + } /> - )} + {nodeSettings.colorByAttribute && ( + <div> + <Input + inline + label="Color based on" + type="dropdown" + value={nodeSettings.colorAttribute} + options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)} + disabled={!settings.nodes} + onChange={(val) => + updateLayerSettings({ + nodes: { + ...layerSettings.nodes, + [nodeType]: { + ...nodeSettings, + colorAttribute: String(val), + colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension, + }, + }, + }) + } + /> + {nodeSettings.colorAttributeType === 'numerical' ? ( + <div> + <DropdownColorLegend + value={settings?.colorScale} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } }, + }) + } + /> + </div> + ) : ( + <div>Categorical</div> + )} + </div> + )} + </div> </div> - <div> + <div className="border-t-2 my-2"> + <span className="font-bold mt-2">Shape & Size</span> <Input - label="By attribute" - type="boolean" - value={nodeSettings.colorByAttribute ?? false} + inline + label="Shape" + type="dropdown" + value={nodeSettings.shape} + options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']} + disabled={true} onChange={(val) => - updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } } }) + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } }, + }) + } + /> + <Input + label="Size" + type="slider" + min={0} + max={80} + step={5} + value={nodeSettings.size} + onChange={(val) => + updateLayerSettings({ + nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } }, + }) } /> - {nodeSettings.colorByAttribute && ( - <div> - <Input - inline - label="Color based on" - type="dropdown" - value={nodeSettings.colorAttribute} - options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)} - disabled={!settings.nodes} - onChange={(val) => - updateLayerSettings({ - nodes: { - ...layerSettings.nodes, - [nodeType]: { - ...nodeSettings, - colorAttribute: String(val), - colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension, - }, - }, - }) - } - /> - {nodeSettings.colorAttributeType === 'numerical' ? ( - <div> - <DropdownColorLegend - value={settings?.colorScale} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } }, - }) - } - /> - </div> - ) : ( - <div>Categorical</div> - )} - </div> - )} </div> </div> - - <div className="border-t-2 my-2"> - <span className="font-bold mt-2">Shape & Size</span> - <Input - inline - label="Shape" - type="dropdown" - value={nodeSettings.shape} - options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']} - disabled={!settings.shape} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } }, - }) - } - /> - <Input - label="Size" - type="slider" - min={0} - max={40} - step={1} - value={nodeSettings.size} - onChange={(val) => - updateLayerSettings({ - nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } }, - }) - } - /> - </div> - </div> - )} - </div> + )} + </div> + ) ); })} {graphMetadata.edges.labels.map((edgeType) => { const edgeSettings = layerSettings?.edges?.[edgeType] || {}; return ( - <div className="mt-2" key={edgeType}> - <div className="flex items-center"> - <Button - size="2xs" - iconComponent={edgeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} - variant="ghost" - onClick={() => handleCollapseToggle(edgeType, 'edges')} - /> - <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}> - <RelationPill title={edgeType} /> - </div> - </div> - - {!edgeSettings.collapsed && ( - <div> - <Input - label="Hidden" - type="boolean" - value={edgeSettings.hidden ?? false} - onChange={(val) => { - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } }); - }} + layerSettings?.edges?.[edgeType] && ( + <div className="mt-2" key={edgeType}> + <div className="flex items-center"> + <Button + size="2xs" + iconComponent={edgeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'} + variant="ghost" + onClick={() => handleCollapseToggle(edgeType, 'edges')} /> - - <div className="flex justify-between"> - <span className="font-bold">Color</span> - <ColorPicker - value={edgeSettings.color} - updateValue={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) - } - /> + <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}> + <RelationPill title={edgeType} /> </div> + </div> - <Input - label="Enable brushing" - type="boolean" - value={settings.enableBrushing} - onChange={(val) => { - updateLayerSettings({ enableBrushing: val as boolean }); - }} - /> - + {!edgeSettings.collapsed && ( <div> - <div className="flex items-center gap-1"> - <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" /> - <span>Width</span> + <Input + label="Hidden" + type="boolean" + value={edgeSettings.hidden ?? false} + onChange={(val) => { + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } }); + }} + /> + + <div className="flex justify-between"> + <span className="font-bold">Color</span> + <ColorPicker + value={edgeSettings.color} + updateValue={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) + } + /> </div> + <Input - label="Fixed" + label="Enable brushing" type="boolean" - value={edgeSettings.fixed} - onChange={(val) => updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } })} + value={settings.enableBrushing} + onChange={(val) => { + updateLayerSettings({ enableBrushing: val as boolean }); + }} /> - {!edgeSettings.fixed ? ( - <div> - <Input - label="Based on" - type="dropdown" - size="xs" - options={ - graphMetadata.edges.types[edgeType]?.attributes - ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter( - (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical', - ) - : [] - } - value={edgeSettings.sizeAttribute} - onChange={(val) => - updateLayerSettings({ - edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } }, - }) - } - /> - <div className="flex"> + + <div> + <div className="flex items-center gap-1"> + <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" /> + <span>Width</span> + </div> + <Input + label="Fixed" + type="boolean" + value={edgeSettings.fixed} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } }) + } + /> + {!edgeSettings.fixed ? ( + <div> <Input - type="number" - label="min" + label="Based on" + type="dropdown" size="xs" - value={edgeSettings.min} + options={ + graphMetadata.edges.types[edgeType]?.attributes + ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter( + (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical', + ) + : [] + } + value={edgeSettings.sizeAttribute} onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } }) + updateLayerSettings({ + edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } }, + }) } /> + <div className="flex"> + <Input + type="number" + label="min" + size="xs" + value={edgeSettings.min} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } }) + } + /> + <Input + type="number" + label="max" + size="xs" + value={edgeSettings.max} + onChange={(val) => + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } }) + } + /> + </div> + </div> + ) : ( + <div> <Input - type="number" - label="max" - size="xs" - value={edgeSettings.max} + type="slider" + label="Width" + min={0} + max={10} + step={0.2} + value={edgeSettings.width} onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } }) + updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } }) } /> </div> - </div> - ) : ( - <div> - <Input - type="slider" - label="Width" - min={0} - max={10} - step={0.2} - value={edgeSettings.width} - onChange={(val) => - updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } }) - } - /> - </div> - )} + )} + </div> </div> - </div> - )} - </div> + )} + </div> + ) ); })} </div> diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx index 92e78c54d47a31ad0c93c0d312cd532d26b55a38..0bafa0ec657ee18f37acc203ad92b13e4457fc00 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx @@ -2,44 +2,46 @@ function svgToDataURL(svg: string): string { return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; } -export const createIcon = (type: string, color: [number, number, number]) => { +export const createIcon = (type: string, color: [number, number, number], selected: boolean = false) => { + const strokeWidth = selected ? 8 : 2; + switch (type) { case 'diamond': - return svgToDataURL(diamond(color)); + return svgToDataURL(diamond(color, strokeWidth)); case 'star': - return svgToDataURL(star(color)); + return svgToDataURL(star(color, strokeWidth)); case 'circle': - return svgToDataURL(circle(color)); + return svgToDataURL(circle(color, strokeWidth)); case 'square': - return svgToDataURL(square(color)); + return svgToDataURL(square(color, strokeWidth)); case 'triangle': - return svgToDataURL(triangle(color)); + return svgToDataURL(triangle(color, strokeWidth)); case 'location': - return svgToDataURL(location(color)); + return svgToDataURL(location(color, strokeWidth)); default: - return svgToDataURL(diamond(color)); + return svgToDataURL(diamond(color, strokeWidth)); } }; -const diamond = (rgbColor: [number, number, number]) => { +const diamond = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> - <polygon points="12,2 22,12 12,22 2,12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" /> + <polygon points="12,2 22,12 12,22 2,12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" /> </svg>`; }; -const star = (rgbColor: [number, number, number]) => { +const star = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> - <polygon points="12,2 14,8 20,8 15,12 18,18 12,14 6,18 9,12 4,8 10,8" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" /> + <polygon points="12,2 14,8 20,8 15,12 18,18 12,14 6,18 9,12 4,8 10,8" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" /> </svg>`; }; -const triangle = (rgbColor: [number, number, number]) => { +const triangle = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="24" height="24" @@ -47,38 +49,37 @@ const triangle = (rgbColor: [number, number, number]) => { fill-rule="evenodd" clip-rule="evenodd" > - <polygon points="12,2 22,22 2,22" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" /> + <polygon points="12,2 22,22 2,22" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" /> </svg>`; }; -const circle = (rgbColor: [number, number, number]) => { +const circle = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> - <circle cx="12" cy="12" r="12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})"/> + <circle cx="12" cy="12" r="12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" /> </svg>`; }; -const square = (rgbColor: [number, number, number]) => { +const square = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" - xmlns:serif="http://www.serif.com/" fill-rule="evenodd" clip-rule="evenodd" > - <path serif:id="shape 1" d="M0 0h24v24h-24z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" /> + <path d="M0 0h24v24h-24z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" /> </svg>`; }; -const location = (rgbColor: [number, number, number]) => { +const location = (rgbColor: [number, number, number], strokeWidth: number) => { return `<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg"> <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g transform="translate(-125.000000, -643.000000)"> <g transform="translate(37.000000, 169.000000)"> <g transform="translate(78.000000, 468.000000)"> <g transform="translate(10.000000, 6.000000)"> - <path d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z" id="Shape" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})"></path> - <circle id="Oval" fill="#0C0058" fill-rule="nonzero" cx="14" cy="14" r="7"></circle> + <path d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}"></path> + <circle fill="#0C0058" fill-rule="nonzero" cx="14" cy="14" r="7"></circle> </g> </g> </g> diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index c37a2cae69630a4a86176df6fed06790f7817e38..e48b49d6f9c274564aadccee426e4373115bb0c5 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useCallback, useState, useRef, forwardRef, useImperativeHandle } from 'react'; import DeckGL, { DeckGLProps, DeckGLRef } from '@deck.gl/react'; -import { CompositeLayer, FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core'; +import { CompositeLayer, FlyToInterpolator, MapViewState, MapViewState, WebMercatorViewport } from '@deck.gl/core'; import { CompositeLayerType, Coordinate, LayerSettingsType, LocationInfo, SearchResultType } from './mapvis.types'; import { VISComponentType, VisualizationPropTypes } from '../../common'; import { layerTypes, createBaseMap, LayerTypes } from './layers'; @@ -20,7 +20,7 @@ const settings: MapProps = { location: {}, }; -const INITIAL_VIEW_STATE = { +const INITIAL_VIEW_STATE: MapViewState = { latitude: 52.1006, longitude: 5.6464, zoom: 6, diff --git a/libs/shared/lib/vis/visualizations/mapvis/utils.ts b/libs/shared/lib/vis/visualizations/mapvis/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..d45003bd9917c328ef2bbb4493add45c3e0f8c09 --- /dev/null +++ b/libs/shared/lib/vis/visualizations/mapvis/utils.ts @@ -0,0 +1,10 @@ +import { visualizationColors } from 'config'; + +export function nodeColorHex(num: number) { + const colorVal = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length]; + const hex = colorVal.replace(/^#/, ''); + const r = parseInt(hex.substring(0, 2), 16); + const g = parseInt(hex.substring(2, 4), 16); + const b = parseInt(hex.substring(4, 6), 16); + return [r, g, b]; +}