import React, { useEffect } from 'react'; import { ColorPicker } from '@graphpolaris/shared/lib/components/colorComponents/colorPicker'; import { DropdownColorLegend, EntityPill, Input, RelationPill } from '@graphpolaris/shared/lib/components'; import { MapProps } from '../../mapvis'; import { LayerSettingsComponentType } from '../../mapvis.types'; import { nodeColorRGB } from '../../utils'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; import { isEqual } from 'lodash-es'; const defaultNodeSettings = (index: number) => ({ color: nodeColorRGB(index), colorMapping: {}, colorScale: undefined, colorByAttribute: false, colorAttribute: undefined, colorAttributeType: undefined, hidden: false, shape: 'circle', size: 40, }); const defaultEdgeSettings = () => ({ hidden: false, width: 1, sizeAttribute: '', fixed: true, color: [132, 150, 155], }); export function NodeLinkOptions({ settings, graphMetadata, updateLayerSettings, spatialAttributes, updateSpatialAttribute, }: LayerSettingsComponentType<MapProps>) { const layerType = 'nodelink'; const layerSettings = settings[layerType] || { enableBrushing: false, nodes: {}, edges: {} }; useEffect(() => { const nodes = layerSettings.nodes || {}; const edges = layerSettings.edges || {}; const newNodes = graphMetadata.nodes.labels.reduce( (acc, node, index) => { acc[node] = nodes[node] || defaultNodeSettings(index); return acc; }, {} as typeof nodes, ); const newEdges = graphMetadata.edges.labels.reduce( (acc, edge) => { acc[edge] = edges[edge] || defaultEdgeSettings(); return acc; }, {} as typeof edges, ); if (!isEqual(newNodes, nodes) || !isEqual(newEdges, edges)) { updateLayerSettings({ ...layerSettings, nodes: newNodes, edges: newEdges, }); } }, [graphMetadata]); return ( layerSettings && ( <div> <Accordion defaultOpenAll={true}> {graphMetadata.nodes.labels.map((nodeType) => { const nodeSettings = layerSettings?.nodes?.[nodeType] || {}; return ( <AccordionItem className="mt-2" key={nodeType}> <AccordionHead className="flex items-center"> <EntityPill title={nodeType} /> </AccordionHead> <AccordionBody> <div> <Input label="Hidden" type="boolean" value={nodeSettings.hidden} onChange={(val) => updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } }) } /> <Accordion> <AccordionItem> <AccordionHead> <span className="font-semibold">Location attributes</span> </AccordionHead> <AccordionBody> <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)} /> </AccordionBody> </AccordionItem> <AccordionItem> <AccordionHead> <div className="flex justify-between items-center"> <span className="font-semibold">Color</span> {!nodeSettings.colorByAttribute && ( <ColorPicker value={nodeSettings.color} onChange={(val) => { updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } }); }} /> )} </div> </AccordionHead> <AccordionBody> <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)} onChange={(val) => updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorAttribute: String(val), colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension, }, }, }) } /> {nodeSettings.colorAttributeType === 'numerical' ? ( <div> <p>Select color scale:</p> <DropdownColorLegend value={settings?.colorScale} onChange={(val) => updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } }, }) } /> </div> ) : ( nodeSettings.colorAttributeType === 'categorical' && nodeSettings.colorAttribute && ( <div> {( graphMetadata.nodes.types[nodeType] as { attributes: { [key: string]: { values: string[] } } } )?.attributes?.[nodeSettings.colorAttribute]?.values.map((attr: string) => ( <div key={attr} className="flex items-center justify-between"> <p className="truncate w-18">{attr.length > 0 ? attr : 'Empty val'}</p> <ColorPicker value={(nodeSettings?.colorMapping ?? {})[attr] ?? [0, 0, 0]} onChange={(val) => { updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorMapping: { ...nodeSettings.colorMapping, [attr]: val }, }, }, }); }} /> </div> ))} </div> ) )} </div> )} </AccordionBody> </AccordionItem> <AccordionItem> <AccordionHead> <span className="font-semibold">Shape & Size</span> </AccordionHead> <AccordionBody> <Input 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, 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) } }, }) } /> </AccordionBody> </AccordionItem> </Accordion> </div> </AccordionBody> </AccordionItem> ); })} {graphMetadata.edges.labels.map((edgeType) => { const edgeSettings = layerSettings?.edges?.[edgeType] || {}; return ( <AccordionItem className="mt-2" key={edgeType}> <AccordionHead className="flex items-center"> <RelationPill title={edgeType} /> </AccordionHead> <AccordionBody> <Input label="Hidden" type="boolean" value={edgeSettings.hidden ?? false} onChange={(val) => { updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } }); }} /> <Input label="Enable brushing" type="boolean" value={settings.enableBrushing} onChange={(val) => { updateLayerSettings({ enableBrushing: val as boolean }); }} /> <Accordion> <AccordionItem> <AccordionHead> <span className="font-semibold">Color</span> </AccordionHead> <AccordionBody> <ColorPicker value={edgeSettings.color} onChange={(val) => updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } }) } /> </AccordionBody> </AccordionItem> <AccordionItem> <AccordionHead> <span className="font-semibold">Width</span> </AccordionHead> <AccordionBody> <Input label="Fixed" type="boolean" value={edgeSettings.fixed} onChange={(val) => updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } }) } /> {!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"> <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="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> )} </AccordionBody> </AccordionItem> </Accordion> </AccordionBody> </AccordionItem> ); })} </Accordion> </div> ) ); }