Newer
Older
import React, { useEffect } from 'react';
import { ColorPicker } from '@/lib/components/colorComponents/colorPicker';
import { DropdownColorLegend, EntityPill, Input, RelationPill } from '@/lib/components';
import { MapProps } from '../../mapvis';
import { LayerSettingsComponentType, EDGE_COLOR_DEFAULT } from '../../mapvis.types';
import { nodeColorRGB } from '../../utils';
import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@/lib/components/accordion';
import { isEqual } from 'lodash-es';
const defaultNodeSettings = (index: number) => ({
colorMapping: {},
colorScale: undefined,
colorByAttribute: false,
colorAttribute: undefined,
colorAttributeType: undefined,
hidden: false,
shape: 'circle',
size: 40,
});
const defaultEdgeSettings = () => ({
hidden: false,
width: 1,
sizeAttribute: '',
fixed: true,
colorMapping: {},
colorScale: undefined,
colorByAttribute: false,
colorAttribute: undefined,
colorAttributeType: undefined,
export function NodeLinkOptions({
settings,
graphMetadata,
updateLayerSettings,
spatialAttributes,
updateSpatialAttribute,
}: LayerSettingsComponentType<MapProps>) {
const layerType = 'nodelink';
const layerSettings = settings[layerType] || { enableBrushing: false, nodes: {}, edges: {} };
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]);
function getCategories(object: 'nodes' | 'edges', type: string, colorAttribute: string) {
const stats = graphMetadata[object].types[type]?.attributes?.[colorAttribute]?.statistics;
if ('uniqueItems' in stats && 'values' in stats) {
// CategoricalStats
if (stats.values.length < 50) {
return stats.values;
} else {
return [];
}
}
if (isEqual(Object.keys(stats), ['true', 'false'])) {
// BooleanStats
return ['true', 'false'];
}
return [];
}
layerSettings && (
<div>
{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>
label="Hidden"
type="boolean"
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>
<div className="flex w-full justify-between items-center">
<span className="font-semibold">Color</span>
value={nodeColorRGB(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}
nodes: {
...layerSettings.nodes,
[nodeType]: { ...nodeSettings, colorByAttribute: val },
},
})
}
/>
{nodeSettings.colorByAttribute && (
<div>
<Input
inline
label="Color based on"
type="dropdown"
options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)}
updateLayerSettings({
nodes: {
...layerSettings.nodes,
[nodeType]: {
...nodeSettings,
colorAttribute: String(val),
colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].attributeType,
{nodeSettings.colorAttributeType === 'number' ? (
<p>Select color scale:</p>
<DropdownColorLegend
value={settings?.colorScale}
nodes: {
...layerSettings.nodes,
[nodeType]: { ...nodeSettings, colorScale: val },
},
['string', 'categorical', 'boolean'].includes(nodeSettings?.colorAttributeType ?? '') &&
{getCategories('nodes', nodeType, nodeSettings.colorAttribute).map((attr: string, i: number) => (
<div key={attr} className="flex items-center justify-between">
<p className="truncate w-18">{attr.length > 0 ? attr : 'Empty val'}</p>
<ColorPicker
value={nodeColorRGB((nodeSettings?.colorMapping ?? {})[attr] ?? i)}
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
updateLayerSettings({
nodes: {
...layerSettings.nodes,
[nodeType]: {
...nodeSettings,
colorMapping: { ...nodeSettings.colorMapping, [attr]: val },
},
},
});
}}
/>
</div>
))}
</div>
)
)}
</div>
)}
</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"
onChange={val => {
updateLayerSettings({
...layerSettings,
edges: { ...layerSettings.edges, [edgeType]: { ...edgeSettings, hidden: val } },
}}
/>
<Input
label="Enable brushing"
type="boolean"
updateLayerSettings({ enableBrushing: val as boolean });
}}
/>
<Input
label="Show arrows"
type="boolean"
value={settings?.showArrows}
onChange={val => {
updateLayerSettings({ showArrows: val as boolean });
}}
/>
<Accordion>
<AccordionItem>
<AccordionHead>
<div className="flex w-full justify-between items-center">
<span className="font-semibold">Color</span>
{!edgeSettings?.colorByAttribute && (
<div className="w-4 h-4 rounded-sm" style={{ backgroundColor: `rgb(${EDGE_COLOR_DEFAULT})` }} />
)}
</div>
<Input
label="By attribute"
type="boolean"
value={edgeSettings?.colorByAttribute ?? false}
onChange={val =>
updateLayerSettings({
edges: { ...layerSettings.edges, [edgeType]: { ...edgeSettings, colorByAttribute: val } },
{edgeSettings.colorByAttribute && (
<div>
<Input
inline
label="Color based on"
type="dropdown"
value={edgeSettings?.colorAttribute}
options={Object.keys(graphMetadata.edges.types[edgeType]?.attributes)}
updateLayerSettings({
edges: {
...layerSettings.edges,
[edgeType]: {
...edgeSettings,
colorAttribute: String(val),
colorAttributeType: graphMetadata.edges.types[edgeType].attributes[val].attributeType,
},
},
/>
{edgeSettings.colorAttributeType === 'number' ? (
<div>
<p>Select color scale:</p>
<DropdownColorLegend
value={settings?.colorScale}
onChange={val =>
updateLayerSettings({
edges: { ...layerSettings.edges, [edgeType]: { ...edgeSettings, colorScale: val } },
})
}
/>
</div>
) : (
['string', 'categorical', 'boolean'].includes(edgeSettings?.colorAttributeType ?? '') &&
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
edgeSettings.colorAttribute && (
<div>
{getCategories('edges', edgeType, edgeSettings.colorAttribute).map((attr: string, i: number) => (
<div key={attr} className="flex items-center justify-between">
<p className="truncate w-18">{attr.length > 0 ? attr : 'Empty val'}</p>
<ColorPicker
value={nodeColorRGB((edgeSettings?.colorMapping ?? {})[attr] ?? i)}
onChange={val => {
updateLayerSettings({
edges: {
...layerSettings.edges,
[edgeType]: {
...edgeSettings,
colorMapping: { ...edgeSettings.colorMapping, [attr]: val },
},
},
});
}}
/>
</div>
))}
</div>
)
)}
</div>
)}
</AccordionBody>
</AccordionItem>
<AccordionItem>
<AccordionHead>
<span className="font-semibold">Width</span>
</AccordionHead>
<AccordionBody>
<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) } },
})
}
/>
</AccordionBody>
</AccordionItem>
</Accordion>
</AccordionBody>
</AccordionItem>
);
})}
</Accordion>
</div>
)