Skip to content
Snippets Groups Projects
NodeLinkOptions.tsx 17.1 KiB
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';
import type { CategoricalStats, BooleanStats } from 'ts-common';

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,
  showArrows: false,
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,
      });
  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 [];
  }

        <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>
                      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 w-full justify-between items-center">
                            <span className="font-semibold">Color</span>
                            {!nodeSettings?.colorByAttribute && (
                              <ColorPicker
                                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}
                              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)}
                                  updateLayerSettings({
                                    nodes: {
                                      ...layerSettings.nodes,
                                      [nodeType]: {
                                        ...nodeSettings,
                                        colorAttribute: String(val),
Leonardo's avatar
Leonardo committed
                                        colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].attributeType,
                              {nodeSettings.colorAttributeType === 'number' ? (
                                  <p>Select color scale:</p>
                                  <DropdownColorLegend
                                    value={settings?.colorScale}
                                      updateLayerSettings({
                                        nodes: {
                                          ...layerSettings.nodes,
                                          [nodeType]: { ...nodeSettings, colorScale: val },
                                        },
                              ) : (
                                ['string', 'categorical', 'boolean'].includes(nodeSettings?.colorAttributeType ?? '') &&
                                nodeSettings.colorAttribute && (
                                  <div>
                                    {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)}
                                            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"
                    value={edgeSettings?.hidden ?? false}
                    onChange={val => {
                      updateLayerSettings({
                        edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } },
                      });
                    }}
                  />

                  <Input
                    label="Enable brushing"
                    type="boolean"
                    value={settings?.enableBrushing}
                      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>
                      </AccordionHead>
                      <AccordionBody>
                        <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)}
                              onChange={val =>
                                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) &&
                              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>