Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Showing
with 855 additions and 776 deletions
......@@ -4,7 +4,7 @@ import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/com
import { MapProps } from '../../mapvis';
import { EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components';
export default function ChoroplethOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
export function ChoroplethOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => {
......@@ -59,22 +59,21 @@ export default function ChoroplethOptions({ settings, graphMetadata, updateSetti
<div>
<Input
inline
label="Longitude"
label="Latitude"
type="dropdown"
value={settings?.[nodeType]?.lon}
value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
/>
<Input
inline
label="Latitude"
label="Longitude"
type="dropdown"
value={settings?.[nodeType]?.lat}
value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
/>
</div>
)}
......
......@@ -2,10 +2,10 @@ import React from 'react';
import { CompositeLayer, HeatmapLayer } from 'deck.gl';
import * as d3 from 'd3';
import { getDistance } from '../../utlis';
import { Edge, LayerProps } from '../../mapvis.types';
import { CompositeLayerType, Edge, LayerProps } from '../../mapvis.types';
import { Node } from '@graphpolaris/shared/lib/data-access';
export class HeatLayer extends CompositeLayer<LayerProps> {
export class HeatLayer extends CompositeLayer<CompositeLayerType> {
static type = 'Heatmap';
updateState({ changeFlags }: { changeFlags: any }) {
......@@ -16,7 +16,7 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
// Generates a path between source and target nodes
return edges.map((edge: Edge, index) => {
const length = getDistance(edge.path[0], edge.path[1]);
const nSegments = length * this.props.config.nSegments;
const nSegments = length * this.props.settings.nSegments;
let xscale = d3
.scaleLinear()
......@@ -44,22 +44,24 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
}
renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props;
const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const layers: any[] = [];
const layerIds: string[] = [];
metaData.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-iconlayer`;
graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-heatmaplayer`;
layerIds.push(layerId);
console.log(settings.nodes[label].size);
layers.push(
new HeatmapLayer({
id: `${label}-nodes-iconlayer`,
data: graph.nodes.filter((node: Node) => node.label === label),
visible: !config[label]?.hidden,
new HeatmapLayer<Node>({
id: layerId,
data: data.nodes.filter((node: Node) => node.label === label),
visible: !settings.nodes[label].hidden,
getPosition: (d: any) => getNodeLocation(d.id),
getWeight: (d) => 1,
getWeight: (d: any) => settings.nodes[label].size,
radiusPixels: settings.nodes[label].size,
aggregation: 'SUM',
}),
);
......@@ -68,77 +70,6 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
setLayerIds(layerIds);
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];
}
}
......
......@@ -2,87 +2,76 @@ import React, { useState, useMemo, useEffect } from 'react';
import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common';
import { MapProps } from '../../mapvis';
import { Button, EntityPill, Input } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function HeatLayerOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => {
graphMetadata.nodes.labels.forEach((node) => {
updateSettings({
[node]: {
color: [0, 0, 0],
hidden: false,
fixed: true,
min: 0,
max: 10,
radius: 1,
sizeAttribute: '',
lon: '',
lat: '',
...settings?.[node],
},
});
});
}, [graphMetadata]);
const spatialAttributes: { [id: string]: string[] } = {};
graphMetadata.nodes.labels.forEach((node) => {
spatialAttributes[node] = Object.entries(graphMetadata.nodes.types[node].attributes)
.filter(([, value]) => value.dimension === 'numerical')
.map(([key]) => key);
});
export function HeatLayerOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({
...prevCollapsed,
[nodeType]: !prevCollapsed[nodeType],
}));
settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
updateSettings({ nodes: settings.nodes });
};
return (
<div>
{graphMetadata.nodes.labels.map((nodeType) => (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="w-1/2">
<Button
iconComponent={settings?.[nodeType].hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
variant="ghost"
onClick={() => {
updateSettings({ [nodeType]: { ...settings?.[nodeType], hidden: !settings?.[nodeType].hidden as boolean } });
}}
/>
</div>
</div>
{!collapsed[nodeType] && (
<div>
<Input
inline
label="Longitude"
type="dropdown"
value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
/>
{settings?.nodes &&
Object.keys(settings.nodes).map((nodeType) => {
const nodeSettings = settings.nodes[nodeType];
return (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="w-1/2">
<Button
iconComponent={nodeSettings.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
variant="ghost"
onClick={() =>
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: !nodeSettings.hidden } } })
}
/>
</div>
</div>
<Input
inline
label="Latitude"
type="dropdown"
value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
/>
{!nodeSettings.collapsed && (
<div>
<Input
inline
label="Latitude"
type="dropdown"
value={nodeSettings.lat}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lat: String(val) } } });
}}
/>
<Input
inline
label="Longitude"
type="dropdown"
value={nodeSettings.lon}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lon: String(val) } } });
}}
/>
<Input
label="Size"
type="slider"
min={0}
max={40}
step={1}
value={nodeSettings.size}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } } });
}}
/>
</div>
)}
</div>
)}
</div>
))}
);
})}
</div>
);
}
import React from 'react';
import { CompositeLayer } from 'deck.gl';
import { IconLayer } from '@deck.gl/layers';
import { LayerProps } from '../../mapvis.types';
import { CompositeLayerType, LayerProps } from '../../mapvis.types';
import { Node } from '@graphpolaris/shared/lib/data-access';
const ICON_MAPPING = {
marker: { x: 0, y: 0, width: 128, height: 128, mask: false },
};
export class NodeIconLayer extends CompositeLayer<LayerProps> {
export class NodeIconLayer extends CompositeLayer<CompositeLayerType> {
static type = 'Icon';
updateState({ changeFlags }: { changeFlags: any }) {
......@@ -16,26 +16,26 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> {
}
renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props;
const { data, settings, getNodeLocation, setLayerIds, graphMetadata } = this.props;
const layers: any[] = [];
const layerIds: string[] = [];
metaData.nodes.labels.forEach((label: string) => {
graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-iconlayer`;
layerIds.push(layerId);
layers.push(
new IconLayer({
id: layerId,
data: graph.nodes.filter((node: Node) => node.label === label),
visible: !config[label].hidden,
data: data.nodes.filter((node: Node) => node.label === label),
visible: !settings.nodes[label].hidden,
iconAtlas: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png',
iconMapping: ICON_MAPPING,
sizeScale: 10,
pickable: true,
getIcon: (d: any) => 'marker',
getColor: (d: any) => config[label].color,
getColor: (d: any) => settings.nodes[label].color,
getPosition: (d: any) => getNodeLocation(d._id),
getSize: (d: any) => 3,
}),
......
......@@ -4,7 +4,7 @@ import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/com
import { MapProps } from '../../mapvis';
import { EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components';
export default function IconOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
export function IconOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => {
......@@ -59,23 +59,23 @@ export default function IconOptions({ settings, graphMetadata, updateSettings }:
<div>
<Input
inline
label="Longitude"
label="Latitude"
type="dropdown"
value={settings?.[nodeType]?.lon}
value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
/>
<Input
inline
label="Latitude"
label="Longitude"
type="dropdown"
value={settings?.[nodeType]?.lat}
value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
/>
<div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<Input
......
......@@ -3,12 +3,13 @@ import { HeatLayer } from './heatmap-layer/HeatLayer';
import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer';
import { NodeLayer } from './node-layer/NodeLayer';
import { NodeIconLayer } from './icon-layer/IconLayer';
import NodeOptions from './node-layer/NodeOptions';
import NodeLinkOptions from './nodelink-layer/NodeLinkOptions';
import IconOptions from './icon-layer/IconOptions';
import HeatLayerOptions from './heatmap-layer/HeatLayerOptions';
import ChoroplethOptions from './choropleth-layer/ChoroplethOptions';
import { NodeLinkOptions } from './nodelink-layer/NodeLinkOptions';
import { IconOptions } from './icon-layer/IconOptions';
import { HeatLayerOptions } from './heatmap-layer/HeatLayerOptions';
import { ChoroplethOptions } from './choropleth-layer/ChoroplethOptions';
import { TileLayer, BitmapLayer } from 'deck.gl';
import { VisualizationSettingsPropTypes } from '../../../common';
import { MapProps } from '../mapvis';
export const layerTypes: Record<string, any> = {
// node: NodeLayer,
......@@ -18,7 +19,11 @@ export const layerTypes: Record<string, any> = {
// choropleth: ChoroplethLayer,
};
export const layerSettings: Record<string, any> = {
export type MapLayerSettingsPropTypes = VisualizationSettingsPropTypes<MapProps> & {
spatialAttributes: { [id: string]: string[] };
};
export const layerSettings: Record<string, React.FC<MapLayerSettingsPropTypes>> = {
nodelink: NodeLinkOptions,
heatmap: HeatLayerOptions,
// node: NodeOptions,
......
import React from 'react';
import { CompositeLayer } from 'deck.gl';
import { ScatterplotLayer } from '@deck.gl/layers';
import { LayerProps } from '../../mapvis.types';
import { CompositeLayerType, LayerProps } from '../../mapvis.types';
import { Node } from '@graphpolaris/shared/lib/data-access';
export class NodeLayer extends CompositeLayer<LayerProps> {
export class NodeLayer extends CompositeLayer<CompositeLayerType> {
static type = 'Node';
updateState({ changeFlags }: { changeFlags: any }) {
......@@ -28,20 +28,20 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
}
renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props;
const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const layers: any[] = [];
const layerIds: any[] = [];
metaData.nodes.labels.forEach((label: string) => {
graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-scatterplot`;
layerIds.push(layerId);
layers.push(
new ScatterplotLayer({
id: layerId,
visible: !config[label].hidden,
data: graph.nodes.filter((node: Node) => node.label === label),
visible: !settings.nodes[label].hidden,
data: data.nodes.filter((node: Node) => node.label === label),
pickable: true,
filled: true,
radiusScale: 6,
......@@ -49,8 +49,8 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
radiusMaxPixels: 100,
lineWidthMinPixels: 1,
getPosition: (d: any) => getNodeLocation(d._id),
getFillColor: (d: any) => config[label].color,
getRadius: (d: any) => this.getRadius(d, config),
getFillColor: (d: any) => settings.nodes[label].color,
getRadius: (d: any) => settings.nodes[label].radius,
}),
);
});
......
import React, { useEffect, useMemo, useState } from 'react';
import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker';
import { MapProps } from '../../mapvis';
import { MapNodeOrEdgeData, MapProps } from '../../mapvis';
import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common';
import { EntityPill, Icon, Input } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function NodeOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => {
graphMetadata.nodes.labels.forEach((node) => {
updateSettings({
[node]: {
color: [0, 0, 0],
hidden: false,
fixed: true,
min: 0,
max: 10,
radius: 1,
sizeAttribute: '',
lon: '',
lat: '',
...settings?.[node],
},
});
});
}, [graphMetadata]);
const spatialAttributes: { [id: string]: string[] } = {};
graphMetadata.nodes.labels.forEach((node) => {
spatialAttributes[node] = Object.entries(graphMetadata.nodes.types[node].attributes)
.filter(([, value]) => value.dimension === 'numerical')
.map(([key]) => key);
});
export default function NodeOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({
...prevCollapsed,
[nodeType]: !prevCollapsed[nodeType],
}));
settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
updateSettings({ nodes: settings.nodes });
};
return (
<div>
{graphMetadata.nodes.labels.map((nodeType) => (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="w-1/2">
<ColorPicker
value={settings?.[nodeType]?.['color'] ? settings?.[nodeType]?.['color'] : [0, 0, 0]}
updateValue={(val: number[]) => updateSettings({ [nodeType]: { ...settings?.[nodeType], color: val } })}
/>
</div>
</div>
{!collapsed[nodeType] && (
<div>
<Input
inline
label="Longitude"
type="dropdown"
value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
/>
<Input
inline
label="Latitude"
type="dropdown"
value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
/>
<div className="ml-2">
<div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<Input
label="Hidden"
type="boolean"
value={settings?.[nodeType]?.hidden ?? false}
onChange={(val: boolean) => updateSettings({ [nodeType]: { ...settings?.[nodeType], hidden: val } })}
{settings?.nodes &&
Object.keys(settings.nodes).map((nodeType) => {
const nodeSettings = settings.nodes[nodeType];
return (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="w-1/2">
<ColorPicker
value={nodeSettings.color}
updateValue={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
}}
/>
</div>
</div>
{!nodeSettings.collapsed && (
<div>
<div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<span>Radius</span>
</div>
<Input
label="Fixed"
type="boolean"
value={settings?.[nodeType]?.fixed ?? false}
onChange={(val: boolean) => updateSettings({ [nodeType]: { ...settings?.[nodeType], fixed: val } })}
inline
label="Latitude"
type="dropdown"
value={nodeSettings.lat}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lat: String(val) } } });
}}
/>
{!settings?.[nodeType]?.fixed ? (
<div>
<Input
inline
label="Longitude"
type="dropdown"
value={nodeSettings.lon}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lon: String(val) } } });
}}
/>
<div className="ml-2">
<div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<Input
label="Based on"
type="dropdown"
size="xs"
options={Object.keys(graphMetadata.nodes.types[nodeType].attributes).filter(
(key) => graphMetadata.nodes.types[nodeType].attributes[key].dimension === 'numerical',
)}
value={settings?.[nodeType]?.sizeAttribute ?? ''}
onChange={(val: string | number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], sizeAttribute: val } })}
label="Hidden"
type="boolean"
value={nodeSettings.hidden}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } });
}}
/>
<div className="flex">
<Input
type="number"
label="min"
size="xs"
value={settings?.[nodeType]?.min ?? 0}
onChange={(val: number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], min: val } })}
/>
<Input
type="number"
label="max"
size="xs"
value={settings?.[nodeType]?.max ?? 10}
onChange={(val: number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], max: val } })}
/>
</div>
</div>
) : (
<div>
<div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<span>Radius</span>
</div>
<Input
type="slider"
label="Width"
min={0}
max={10}
step={0.5}
value={settings?.[nodeType]?.radius ?? 1}
onChange={(val: number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], radius: val } })}
label="Fixed"
type="boolean"
value={nodeSettings.fixed}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, fixed: val } } });
}}
/>
{!settings?.[nodeType]?.fixed ? (
<div>
<Input
label="Based on"
type="dropdown"
size="xs"
options={spatialAttributes[nodeType]}
value={nodeSettings.sizeAttribute}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, sizeAttribute: String(val) } } });
}}
/>
<div className="flex">
<Input
type="number"
label="min"
size="xs"
value={nodeSettings.min}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, min: Number(val) } } });
}}
/>
<Input
type="number"
label="max"
size="xs"
value={nodeSettings.max}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, max: Number(val) } } });
}}
/>
</div>
</div>
) : (
<div>
<Input
type="slider"
label="Width"
min={0}
max={10}
step={0.5}
value={nodeSettings.radius}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, radius: Number(val) } } });
}}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
</div>
)}
</div>
))}
);
})}
</div>
);
}
import React from 'react';
import { CompositeLayer } from 'deck.gl';
import { CompositeLayer, Layer } from 'deck.gl';
import { IconLayer, LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
import { LayerProps } from '../../mapvis.types';
import { CompositeLayerType, LayerProps } from '../../mapvis.types';
import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions';
import { Edge, Node } from '@graphpolaris/shared/lib/data-access';
import { createIcon } from './shapeFactory';
export class NodeLinkLayer extends CompositeLayer<LayerProps> {
export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
static type = 'NodeLink';
private _layers: Record<string, Layer> = {};
constructor(props: LayerProps) {
super(props);
}
updateState({ changeFlags }: { changeFlags: any }) {
return changeFlags.propsOrDataChanged || changeFlags.somethingChanged;
}
renderLayers() {
const { graph, config, getNodeLocation, selected, setLayerIds, metaData } = this.props;
const layers = [];
const layerIds = [];
const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const brushingExtension = new BrushingExtension();
const collisionFilter = new CollisionFilterExtension();
metaData.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-scatterplot`;
layerIds.push(layerId);
layers.push(
new IconLayer({
id: layerId,
visible: !config[label].hidden,
data: graph.nodes.filter((node: Node) => node.label === label),
pickable: true,
getColor: (d) => [200, 140, 0],
getSize: (d: any) => config[label].size,
getPosition: (d: any) => getNodeLocation(d._id),
getIcon: (d: any) => {
return {
url: createIcon(config[label].shape ?? 'circle', config[label].color),
width: 24,
height: 24,
};
},
mask: true,
}),
);
});
metaData.edges.labels.forEach((label: string) => {
graphMetadata.edges.labels.forEach((label: string) => {
const layerId = `${label}-edges-line`;
layerIds.push(layerId);
const edgeData =
selected.length > 0 ? graph.edges.filter((edge: Edge) => selected.includes(edge.from) || selected.includes(edge.to)) : graph.edges;
selected.length > 0 ? data.edges.filter((edge: Edge) => selected.includes(edge.from) || selected.includes(edge.to)) : data.edges;
layers.push(
new LineLayer({
id: layerId,
data: edgeData,
visible: !config[label]?.hidden,
pickable: true,
getWidth: (d: any) => config[label].width,
getSourcePosition: (d: any) => getNodeLocation(d.from),
getTargetPosition: (d: any) => getNodeLocation(d.to),
getColor: (d: any) => config[d.label].color,
radiusScale: 3000,
brushingEnabled: config.enableBrushing,
extensions: [brushingExtension],
}),
);
this._layers[layerId] = new LineLayer<Edge>({
id: layerId,
data: edgeData,
visible: !settings.edges[label].hidden,
pickable: true,
getWidth: settings.edges[label].width,
getSourcePosition: (d) => getNodeLocation(d.from),
getTargetPosition: (d) => getNodeLocation(d.to),
getColor: (d) => settings.edges[d.label].color,
extensions: [brushingExtension],
});
});
graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-scatterplot`;
const textLayerId = 'label-target';
layerIds.push(textLayerId);
this._layers[layerId] = new IconLayer<Node>({
id: layerId,
visible: !settings.nodes[label].hidden,
data: data.nodes.filter((node: Node) => node.label === label),
pickable: true,
getColor: (d) => [200, 140, 0],
getSize: (d) => settings.nodes[label].size,
getPosition: (d) => getNodeLocation(d._id),
getIcon: (d: any) => {
return {
url: createIcon(settings.nodes[label].shape, settings.nodes[label].color),
width: 24,
height: 24,
};
},
});
});
layers.push(
new TextLayer({
id: textLayerId,
data: graph.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',
}),
);
const textLayerId = 'label-target';
setLayerIds(layerIds);
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',
});
return layers;
return Object.values(this._layers);
}
}
......
import React, { useState, useEffect } from 'react';
import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker';
import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common';
import { MapProps } from '../../mapvis';
import { Button, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function NodeLinkOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
export function NodeLinkOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
const handleCollapseToggle = (nodeType: string) => {
settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
updateSettings({ nodes: settings.nodes });
};
useEffect(() => {
graphMetadata.nodes.labels.forEach((node) => {
updateSettings({
[node]: {
color: [0, 0, 0],
hidden: false,
shape: '',
size: 10,
fixed: true,
min: 0,
max: 10,
sizeAttribute: '',
lon: '',
lat: '',
...settings?.[node],
},
});
});
graphMetadata.edges.labels.forEach((edge) => {
updateSettings({
[edge]: {
color: [0, 0, 0],
hidden: false,
fixed: true,
min: 0,
max: 10,
width: 1,
widthAttribute: '',
...settings?.[edge],
},
});
graphMetadata.nodes.labels.map((nodeType) => {
if (settings?.[nodeType]?.lat) {
}
});
}, [graphMetadata]);
const spatialAttributes: { [id: string]: string[] } = {};
graphMetadata.nodes.labels.forEach((node) => {
spatialAttributes[node] = Object.entries(graphMetadata.nodes.types[node].attributes)
.filter(([, value]) => value.dimension === 'numerical')
.map(([key]) => key);
});
const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({
...prevCollapsed,
[nodeType]: !prevCollapsed[nodeType],
}));
};
}, [settings.node, graphMetadata]);
return (
<div>
{graphMetadata.nodes.labels.map((nodeType) => (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="flex items-center space-x-2">
<ColorPicker
value={settings?.[nodeType]?.['color'] ? settings?.[nodeType]?.['color'] : [0, 0, 0]}
updateValue={(val: number[]) => updateSettings({ [nodeType]: { ...settings?.[nodeType], color: val } })}
/>
<Button
iconComponent={settings?.[nodeType]?.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
variant="ghost"
onClick={() => {
updateSettings({ [nodeType]: { ...settings?.[nodeType], hidden: !settings?.[nodeType]?.hidden as boolean } });
}}
/>
</div>
</div>
{!collapsed[nodeType] && (
<div>
<Input
inline
label="Longitude"
type="dropdown"
value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lon: val as string } })}
/>
<Input
inline
label="Latitude"
type="dropdown"
value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })}
/>
<Input
inline
label="Shape"
type="dropdown"
value={settings?.[nodeType]?.shape}
options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']}
disabled={!settings.shape}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], shape: val as string } })}
/>
<Input
label="Size"
type="slider"
min={0}
max={20}
step={1}
value={settings?.[nodeType]?.size}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], size: val as number } })}
/>
</div>
)}
</div>
))}
{settings?.nodes &&
Object.keys(settings.nodes).map((nodeType) => {
const nodeSettings = settings.nodes[nodeType];
return (
<div className="mt-2" key={nodeType}>
<div className="flex items-center">
<div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<EntityPill title={nodeType} />
</div>
<div className="flex items-center space-x-2">
<ColorPicker
value={nodeSettings.color}
updateValue={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
}}
/>
<Button
iconComponent={nodeSettings.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
variant="ghost"
onClick={() => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: !nodeSettings.hidden } } });
}}
/>
</div>
</div>
{graphMetadata.edges.labels.map((edgeType) => (
<div className="mt-2" key={edgeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType)}>
<RelationPill title={edgeType} />
</div>
<div className="w-1/2 flex">
<ColorPicker
value={settings?.[edgeType]?.['color'] ? settings?.[edgeType]?.['color'] : [0, 0, 0]}
updateValue={(val: number[]) => updateSettings({ [edgeType]: { ...settings?.[edgeType], color: val } })}
/>
<Button
iconComponent={settings?.[edgeType]?.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
variant="ghost"
onClick={() => {
updateSettings({ [edgeType]: { ...settings?.[edgeType], hidden: !settings?.[edgeType]?.hidden as boolean } });
}}
/>
{!nodeSettings.collapsed && (
<div>
<Input
inline
label="Latitude"
type="dropdown"
value={nodeSettings.lat}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lat: String(val) } } });
}}
/>
<Input
inline
label="Longitude"
type="dropdown"
value={nodeSettings.lon}
options={[...(spatialAttributes[nodeType] || [])]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lon: String(val) } } });
}}
/>
<Input
inline
label="Shape"
type="dropdown"
value={nodeSettings.shape}
options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']}
disabled={!settings.shape}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) as any } } });
}}
/>
<Input
label="Size"
type="slider"
min={0}
max={40}
step={1}
value={nodeSettings.size}
onChange={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } } });
}}
/>
</div>
)}
</div>
</div>
);
})}
{!collapsed[edgeType] && (
<div>
<Input
label="Enable brushing"
type="boolean"
value={settings.enableBrushing}
onChange={(val) => {
updateSettings({ enableBrushing: val as boolean });
}}
/>
{settings?.edges &&
Object.keys(settings.edges).map((edgeType) => {
const edgeSettings = settings.edges[edgeType];
<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>
return (
<div className="mt-2" key={edgeType}>
<div className="flex items-center">
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType)}>
<RelationPill title={edgeType} />
</div>
<Input
label="Fixed"
type="boolean"
value={settings?.[edgeType]?.fixed ?? false}
onChange={(val: boolean) => updateSettings({ [edgeType]: { ...settings?.[edgeType], fixed: val } })}
/>
{!settings?.[edgeType]?.fixed ? (
<div className="w-1/2 flex">
<ColorPicker
value={settings.edges[edgeType].color}
updateValue={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } })}
/>
<Button
iconComponent={
settings.edges[edgeType].hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'
}
variant="ghost"
onClick={() =>
updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: !edgeSettings.hidden } } })
}
/>
</div>
</div>
{!edgeSettings.collapsed && (
<div>
<Input
label="Enable brushing"
type="boolean"
value={settings.enableBrushing}
onChange={(val) => {
updateSettings({ enableBrushing: val as boolean });
}}
/>
<div>
<Input
label="Based on"
type="dropdown"
size="xs"
options={
graphMetadata.nodes.types[edgeType]?.attributes
? Object.keys(graphMetadata.nodes.types[edgeType].attributes).filter(
(key) => graphMetadata.nodes.types[edgeType].attributes[key].dimension === 'numerical',
)
: []
}
value={settings?.[edgeType]?.sizeAttribute ?? ''}
onChange={(val: string | number) => updateSettings({ [edgeType]: { ...settings?.[edgeType], sizeAttribute: val } })}
/>
<div className="flex">
<Input
type="number"
label="min"
size="xs"
value={settings?.[edgeType]?.min ?? 0}
onChange={(val: number) => updateSettings({ [edgeType]: { ...settings?.[edgeType], min: val } })}
/>
<Input
type="number"
label="max"
size="xs"
value={settings?.[edgeType]?.max ?? 10}
onChange={(val: number) => updateSettings({ [edgeType]: { ...settings?.[edgeType], max: val } })}
/>
<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>
</div>
) : (
<div>
<Input
type="slider"
label="Width"
min={0}
max={10}
step={0.5}
value={settings?.[edgeType]?.width ?? 1}
onChange={(val: number) => updateSettings({ [edgeType]: { ...settings?.[edgeType], width: val } })}
label="Fixed"
type="boolean"
value={edgeSettings.fixed}
onChange={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } })}
/>
{!edgeSettings.fixed ? (
<div>
<Input
label="Based on"
type="dropdown"
size="xs"
options={
graphMetadata.nodes.types[edgeType]?.attributes
? Object.keys(graphMetadata.nodes.types[edgeType].attributes).filter(
(key) => graphMetadata.nodes.types[edgeType].attributes[key].dimension === 'numerical',
)
: []
}
value={edgeSettings.sizeAttribute ?? ''}
onChange={(val) =>
updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } } })
}
/>
<div className="flex">
<Input
type="number"
label="min"
size="xs"
value={edgeSettings.min}
onChange={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } })}
/>
<Input
type="number"
label="max"
size="xs"
value={edgeSettings.max}
onChange={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } })}
/>
</div>
</div>
) : (
<div>
<Input
type="slider"
label="Width"
min={0}
max={10}
step={0.2}
value={settings.edges[edgeType].width}
onChange={(val) =>
updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } })
}
/>
</div>
)}
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
))}
);
})}
</div>
);
}
import React, { useEffect, useMemo, useCallback, useState } from 'react';
import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
import DeckGL from '@deck.gl/react';
import { FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core';
import { CompositeLayer, FlyToInterpolator, Position, WebMercatorViewport } from '@deck.gl/core';
import { SelectionLayer } from '@deck.gl-community/editable-layers';
import { Coordinate, Layer } from './mapvis.types';
import { CompositeLayerType, Coordinate, Layer } from './mapvis.types';
import { VISComponentType, VisualizationPropTypes } from '../../common';
import { layerTypes, createBaseMap } from './layers';
import { MapSettings } from './settings';
import { layerTypes, createBaseMap, MapLayerSettingsPropTypes } from './layers';
import { MapSettings } from './MapSettings';
import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
import SearchBar from './search';
import { SearchBar } from './SearchBar';
import { Icon } from '@graphpolaris/shared/lib/components';
export type MapProps = { layer: string; enableBrushing: boolean };
export type MapNodeOrEdgeData = MapNodeData | MapEdgeData;
export type MapNodeData = {
color: [number, number, number];
hidden: boolean;
fixed: boolean;
min: number;
max: number;
radius: number;
collapsed: boolean;
lat?: string;
lon?: string;
shape: 'circle' | 'square' | 'triangle' | 'diamond' | 'location' | 'star';
size: number;
sizeAttribute?: string;
};
export type MapEdgeData = {
color: [number, number, number];
hidden: boolean;
fixed: boolean;
min: number;
max: number;
radius: number;
collapsed: boolean;
size: number;
width: number;
sizeAttribute?: string;
};
export type MapProps = {
layer: string;
enableBrushing: boolean;
nodes: Record<string, MapNodeData>;
edges: Record<string, MapEdgeData>;
};
const settings: MapProps = { layer: 'nodelink', enableBrushing: false };
const settings: MapProps = { layer: 'nodelink', enableBrushing: false, nodes: {}, edges: {} };
const INITIAL_VIEW_STATE = {
latitude: 52.1006,
......@@ -24,10 +59,8 @@ const INITIAL_VIEW_STATE = {
const FLY_SPEED = 1000;
const baseLayer = createBaseMap();
export const MapVis = ({ data, settings, handleSelect, graphMetadata }: VisualizationPropTypes<MapProps>) => {
const [layer, setLayer] = useState<Layer | undefined>(undefined);
export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
const baseLayer = useRef(createBaseMap());
const [viewport, setViewport] = useState<Record<string, any>>(INITIAL_VIEW_STATE);
const [hoverObject, setHoverObject] = useState<Node | null>(null);
const [selected, setSelected] = useState<any[]>([]);
......@@ -64,20 +97,28 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
[getFittedViewport],
);
useEffect(() => {
setLayer({
type: settings.layer ? layerTypes?.[settings.layer] : layerTypes.nodelink,
config: settings,
});
}, [settings.layer]);
const dataLayer = useMemo(() => {
if (!layer || !settings.layer) return null;
const [dataLayer, setDataLayer] = useState<{ component: CompositeLayer<CompositeLayerType>; id: string } | null>(null);
const coordinateLookup: { [id: string]: Coordinate } = data.nodes.reduce(
useEffect(() => {
if (!props.settings.layer) {
setDataLayer(null);
return;
}
const layer = {
component: props.settings.layer ? layerTypes?.[props.settings.layer] : layerTypes.nodelink,
settings: props.settings,
id: props.settings.layer,
};
const coordinateLookup: { [id: string]: Position } = props.data.nodes.reduce(
(acc, node) => {
const latitude = settings?.[node.label]?.lat ? (node?.attributes?.[settings[node.label].lat] as string) : undefined;
const longitude = settings?.[node.label]?.lon ? (node?.attributes?.[settings[node.label].lon] as string) : undefined;
const latitude = props.settings.nodes?.[node.label]?.lat
? (node?.attributes?.[props.settings.nodes[node.label].lat as any] as string)
: undefined;
const longitude = props.settings.nodes?.[node.label]?.lon
? (node?.attributes?.[props.settings.nodes[node.label].lon as any] as string)
: undefined;
if (latitude !== undefined && longitude !== undefined) {
acc[node._id] = [parseFloat(longitude), parseFloat(latitude)];
......@@ -85,20 +126,26 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
return acc;
},
{} as { [id: string]: Coordinate },
{} as { [id: string]: Position },
);
return new layer.type({
graph: data,
metaData: graphMetadata,
config: settings,
const layerProps: CompositeLayerType = {
...props,
selected: selected,
hoverObject: hoverObject,
getNodeLocation: (d: string) => coordinateLookup[d],
flyToBoundingBox: flyToBoundingBox,
setLayerIds: (val: string[]) => setLayerIds(val),
});
}, [layer, data, selected, hoverObject, settings]);
};
if (dataLayer && dataLayer.id === layer.id) {
// dataLayer.updateState;
setDataLayer({ component: dataLayer.component.clone(layerProps), id: props.settings.layer });
} else {
// @ts-ignore
setDataLayer({ component: new layer.component(layerProps), id: props.settings.layer });
}
}, [props.settings.layer, props.data, selected, hoverObject, props.settings]);
const selectionLayer = useMemo(
() =>
......@@ -122,16 +169,16 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
}
}
setSelected(nodes.map((node) => node._id));
handleSelect({ nodes, edges });
props.handleSelect({ nodes, edges });
} else {
handleSelect();
props.handleSelect();
}
setSelectingRectangle(false);
},
layerIds: layerIds,
getTentativeFillColor: () => [22, 37, 67, 100],
}),
[selectingRectangle, layer],
[selectingRectangle],
);
return (
......@@ -146,35 +193,35 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
</div>
{isSearching && (
<SearchBar
onSearch={(boundingbox: [number, number, number, number]) => {
flyToBoundingBox(...boundingbox);
onSearch={(boundingBox: [number, number, number, number]) => {
flyToBoundingBox(...boundingBox);
setIsSearching(false);
}}
/>
)}
<DeckGL
layers={[baseLayer, dataLayer, selectionLayer]}
layers={[baseLayer.current, dataLayer?.component, selectionLayer]}
controller={true}
initialViewState={viewport}
onViewStateChange={({ viewState }) => setViewport(viewState)}
onClick={({ object }) => {
if (data) {
if (props.data) {
if (!object) {
handleSelect();
props.handleSelect();
setSelected([]);
return;
}
if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) {
handleSelect({ nodes: [object] });
props.handleSelect({ nodes: [object] });
setSelected([object.id]);
}
if (object.type === 'Feature') {
const ids = object.properties.nodes;
if (ids.length > 0) {
const nodes = data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id));
handleSelect({ nodes: [...nodes] });
const nodes = props.data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id));
props.handleSelect({ nodes: [...nodes] });
} else {
handleSelect();
props.handleSelect();
setSelected([]);
return;
}
......
import { CompositeLayer, Position } from 'deck.gl';
import { MapLayerSettingsPropTypes } from './layers';
import { VisualizationPropTypes, VisualizationSettingsType } from '../../common';
import { MapProps } from './mapvis';
import { Node as QueryNode } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice';
export type Coordinate = [number, number] | [];
export interface LayerProps {
[key: string]: any;
}
export type CompositeLayerType = VisualizationPropTypes<MapProps> & {
selected: any[];
hoverObject: QueryNode | null;
getNodeLocation: (d: string) => Position;
flyToBoundingBox: (minLat: number, maxLat: number, minLon: number, maxLon: number, options?: { [key: string]: any }) => void;
setLayerIds: (val: string[]) => void;
};
export type Layer = {
type: any;
config: any;
id: string;
component: CompositeLayer<CompositeLayerType>;
settings: MapProps & VisualizationSettingsType;
};
export type Node = {
......
import React from 'react';
import { SettingsContainer } from '../../components/config';
import { layerSettings, layerTypes } from './layers';
import { Input } from '../../..';
import { VisualizationSettingsPropTypes } from '../../common';
import { MapProps } from './mapvis';
export const MapSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) => {
const DataLayerSettings = settings.layer && layerSettings?.[settings.layer];
return (
<SettingsContainer>
<Input
label="Data layer"
type="dropdown"
inline
value={settings.layer}
options={Object.keys(layerTypes)}
onChange={(val) => updateSettings({ layer: val as string })}
/>
{DataLayerSettings && <DataLayerSettings settings={settings} graphMetadata={graphMetadata} updateSettings={updateSettings} />}
</SettingsContainer>
);
};
......@@ -3,17 +3,16 @@ import { dataColors, visualizationColors } from 'config';
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import {
Application,
AssetsBundle,
Color,
Container,
FederatedPointerEvent,
Graphics,
IPointData,
Sprite,
Assets,
Text,
Texture,
Resource,
RenderTexture
} from 'pixi.js';
import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access';
import { NLPopup } from './NLPopup';
......@@ -24,6 +23,8 @@ import { Viewport } from 'pixi-viewport';
import { NodelinkVisProps } from '../nodelinkvis';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { MovedEvent } from 'pixi-viewport/dist/types';
import { Theme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { useConfig } from '@graphpolaris/shared/lib/data-access/store';
type Props = {
onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void;
......@@ -47,7 +48,12 @@ type LayoutState = 'reset' | 'running' | 'paused';
export const NLPixi = (props: Props) => {
const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]);
const [assetsLoaded, setAssetsLoaded] = useState(false);
const globalConfig = useConfig();
useEffect(() => {
update();
}, [globalConfig.currentTheme]);
const app = useMemo(
() =>
......@@ -61,7 +67,13 @@ export const NLPixi = (props: Props) => {
[],
);
const nodeLayer = useMemo(() => new Container(), []);
const labelLayer = useMemo(() => {
const linkLabelLayer = useMemo(() => {
const container = new Container();
container.alpha = 0;
container.renderable = false;
return container;
}, []);
const nodeLabelLayer = useMemo(() => {
const container = new Container();
container.alpha = 0;
container.renderable = false;
......@@ -70,7 +82,8 @@ export const NLPixi = (props: Props) => {
const nodeMap = useRef(new Map<string, Sprite>());
const linkGfx = new Graphics();
const labelMap = useRef(new Map<string, Text>());
const linkLabelMap = useRef(new Map<string, Text>());
const nodeLabelMap = useRef(new Map<string, Text>());
const viewport = useRef<Viewport>();
const layoutState = useRef<LayoutState>('reset');
const layoutStoppedCount = useRef(0);
......@@ -87,11 +100,71 @@ export const NLPixi = (props: Props) => {
// const cull = new Cull();
// let cullDirty = useRef(true);
const textureId = (selected: boolean = false) => {
const selectionSuffix = selected ? '_selected' : '';
const shapeSuffix = props.configuration.shapes.shape == 'rectangle' ? '_square' : '';
return `texture${selectionSuffix}${shapeSuffix}`;
};
const _glyphTexture = RenderTexture.create();
const _selectedTexture = RenderTexture.create();
const getTexture = (renderTexture: RenderTexture, selected: boolean = false): RenderTexture => {
let size = config.NODE_RADIUS * (1 / responsiveScale);
let lineWidth = selected ? 12 : 6;
renderTexture.resize(size + lineWidth, size + lineWidth);
const graphics = new Graphics();
graphics.lineStyle(lineWidth, 0x4e586a);
graphics.beginFill(0xffffff, 1);
if (props.configuration.shapes?.shape == "circle") {
graphics.drawCircle((size / 2) + (lineWidth / 2), (size / 2) + (lineWidth/2), (size / 2));
} else {
graphics.drawRect(lineWidth, lineWidth, size - lineWidth, size - lineWidth);
}
graphics.endFill();
app.renderer.render(graphics, { renderTexture });
return renderTexture;
}
// Pixi viewport zoom scale, but discretized to single decimal.
const [responsiveScale, setResponsiveScale] = useState(1);
useEffect(() => {
if (nodeMap.current.size === 0) return;
graph.current.nodes.forEach((node) => {
const sprite = nodeMap.current.get(node._id) as Sprite;
const nodeMeta = props.graph.nodes[node._id];
sprite.texture = (sprite as any).selected ? selectedTexture : glyphTexture;
// To calculate the scale, we:
// 1) Determine the node radius, with a minimum of 5. If not available, we default to 5.
// 2) Get the ratio with respect to the typical size of the node (divide by NODE_RADIUS).
// 3) Scale this ratio by the current scale factor.
let scale = (Math.max(nodeMeta.radius || 5, 5) / config.NODE_RADIUS) * 2;
scale *= responsiveScale;
sprite.scale.set(scale, scale);
});
if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
// Change font size at specific scale intervals
const fontSize = (responsiveScale <= 0.1) ? 15 : (responsiveScale <= 0.2) ? 22.5 : (responsiveScale <= 0.4) ? 30 : (responsiveScale <= 0.6) ? 37.5 : 45;
const strokeWidth = fontSize / 2;
linkLabelMap.current.forEach((text) => {
text.style.fontSize = fontSize;
text.style.strokeThickness = strokeWidth;
text.resolution = Math.ceil(0.5 / responsiveScale);
});
nodeLabelMap.current.forEach((text) => {
text.style.fontSize = fontSize * (2 / 3);
text.resolution = Math.ceil(1 / responsiveScale);
});
graph.current.nodes.forEach((node: any) => {
updateNodeLabel(node);
});
}, [responsiveScale, props.configuration.shapes?.shape]);
const [config, setConfig] = useState({
width: 1000,
......@@ -101,7 +174,7 @@ export const NLPixi = (props: Props) => {
LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
NODE_RADIUS: 5,
NODE_RADIUS: 70,
// NODE_BORDER_LINE_WIDTH: 1.0,
// NODE_BORDER_LINE_WIDTH_SELECTED: 5.0, // if selected and normal width are different the thicker line will be still in the gfx
// NODE_BORDER_COLOR_DEFAULT: dataColors.neutral[70],
......@@ -113,6 +186,14 @@ export const NLPixi = (props: Props) => {
LINE_WIDTH_DEFAULT: 0.8,
});
const glyphTexture = useMemo(() => {
return getTexture(_glyphTexture);
}, [responsiveScale, props.configuration.shapes?.shape]);
const selectedTexture = useMemo(() => {
return getTexture(_selectedTexture, true);
}, [responsiveScale, props.configuration.shapes?.shape]);
useEffect(() => {
setConfig((lastConfig) => {
return {
......@@ -122,15 +203,6 @@ export const NLPixi = (props: Props) => {
});
}, [props.layoutAlgorithm, props.configuration]);
useEffect(() => {
if (nodeMap.current.size === 0) return;
const texture = Assets.get(textureId());
for (const sprite of nodeMap.current.values()) {
sprite.texture = texture;
}
}, [props.configuration.shapes?.shape]);
const imperative = useRef<any>(null);
const mouseClickThreshold = 200; // Time between mouse up and down events that is considered a click, and not a drag.
......@@ -161,11 +233,13 @@ export const NLPixi = (props: Props) => {
setPopups([{ node: node, pos: toGlobal(node) }]);
for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite;
sprite.texture = Assets.get(textureId(false));
sprite.texture = glyphTexture;
(sprite as any).selected = false;
}
}
sprite.texture = Assets.get(textureId(true));
sprite.texture = selectedTexture;
(sprite as any).selected = true;
props.onClick({ node: node, pos: toGlobal(node) });
......@@ -180,7 +254,8 @@ export const NLPixi = (props: Props) => {
if (holdDownTime < mouseClickThreshold) {
for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite;
sprite.texture = Assets.get(textureId(false));
sprite.texture = glyphTexture;
(sprite as any).selected = false;
}
setPopups([]);
props.onClick();
......@@ -220,26 +295,39 @@ export const NLPixi = (props: Props) => {
onZoom(event: FederatedPointerEvent) {
const scale = viewport.current!.transform.scale.x;
if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
labelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0;
if (scale > 2) {
const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0.
setResponsiveScale((scale < 0.05) ? 0.1 : (scale < 0.1) ? 0.2 : (scale < 0.2) ? 0.4 : (scale < 0.3) ? 0.6 : 0.8);
} else {
setResponsiveScale(1);
}
if (labelLayer.alpha > 0) {
labelLayer.renderable = true;
if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
linkLabelLayer.alpha = (scale > 2) ? Math.min(1, (scale - 2) * 3) : 0;
const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0.
if (linkLabelLayer.alpha > 0) {
linkLabelLayer.renderable = true;
} else {
linkLabelLayer.renderable = false;
}
// Only change the fontSize for specific intervals, continuous change has too big of an impact on performance
const fontSize = scale < 0.1 ? 30 : scale < 0.2 ? 40 : scale < 0.3 ? 50 : 60;
const strokeWidth = fontSize / 2;
labelMap.current.forEach((text) => {
text.style.fontSize = fontSize;
text.style.strokeThickness = strokeWidth;
});
nodeLabelLayer.alpha = (scale > 5) ? Math.min(1, (scale - 5) * 3) : 0;
if (nodeLabelLayer.alpha > 0) {
nodeLabelLayer.renderable = true;
} else {
labelLayer.renderable = false;
nodeLabelLayer.renderable = false;
}
}
},
getLinkWidth() {
return props.configuration.edges.width.width || config.LINE_WIDTH_DEFAULT;
},
getBackgroundColor() {
// Colors corresponding to .bg-light class
return globalConfig.currentTheme === Theme.dark ? 0x121621 : 0xffffff;
},
}));
function resize() {
......@@ -286,8 +374,9 @@ export const NLPixi = (props: Props) => {
// Update texture when selected
const nodeMeta = props.graph.nodes[node._id];
const texture = Assets.get(textureId(nodeMeta.selected));
const texture = (gfx as any).selected ? selectedTexture : glyphTexture;
gfx.texture = texture;
// Cluster colors
if (nodeMeta?.cluster) {
......@@ -314,6 +403,10 @@ export const NLPixi = (props: Props) => {
// }
};
const getNodeLabel = (nodeMeta: NodeType) => {
return nodeMeta.label
}
const createNode = (node: NodeTypeD3, selected?: boolean) => {
const nodeMeta = props.graph.nodes[node._id];
......@@ -325,11 +418,11 @@ export const NLPixi = (props: Props) => {
if (node.x === undefined || node.y === undefined) return;
let sprite: Sprite;
const texture = Assets.get(textureId());
const texture = glyphTexture;
sprite = new Sprite(texture);
sprite.tint = nodeColor(nodeMeta.type);
const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2;
const scale = (Math.max(nodeMeta.radius || 5, 5) / config.NODE_RADIUS) * 2;
sprite.scale.set(scale, scale);
sprite.anchor.set(0.5, 0.5);
sprite.cullable = true;
......@@ -346,13 +439,31 @@ export const NLPixi = (props: Props) => {
updateNode(node);
(sprite as any).node = node;
// Node label
const attribute = getNodeLabel(nodeMeta);
const text = new Text(attribute, {
fontSize: 20,
fill: 0xffffff,
wordWrap: true,
wordWrapWidth: 65,
align: 'center'
});
text.eventMode = 'none';
text.cullable = true;
text.anchor.set(0.5, 0.5);
text.scale.set(0.1, 0.1);
nodeLabelMap.current.set(node._id, text);
nodeLabelLayer.addChild(text);
updateNodeLabel(node);
return sprite;
};
const createLinkLabel = (link: LinkTypeD3) => {
// check if link is already drawn, and if so, delete it
if (link && link?._id && labelMap.current.has(link._id)) {
labelMap.current.delete(link._id);
if (link && link?._id && linkLabelMap.current.has(link._id)) {
linkLabelMap.current.delete(link._id);
}
const linkMeta = props.graph.links[link._id];
......@@ -360,14 +471,14 @@ export const NLPixi = (props: Props) => {
const text = new Text(linkMeta.name, {
fontSize: 60,
fill: config.LINE_COLOR_DEFAULT,
stroke: 0xffffff,
stroke: imperative.current.getBackgroundColor(),
strokeThickness: 30,
});
text.cullable = true;
text.anchor.set(0.5, 0.5);
text.scale.set(0.1, 0.1);
labelMap.current.set(link._id, text);
labelLayer.addChild(text);
linkLabelMap.current.set(link._id, text);
linkLabelLayer.addChild(text);
updateLinkLabel(link);
......@@ -408,7 +519,7 @@ export const NLPixi = (props: Props) => {
// let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT;
let style = imperative.current.getLinkWidth();
let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML;
......@@ -450,7 +561,9 @@ export const NLPixi = (props: Props) => {
};
const updateLinkLabel = (link: LinkTypeD3) => {
const text = labelMap.current.get(link._id);
if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
const text = linkLabelMap.current.get(link._id);
if (!text) return;
const _source = link.source;
......@@ -488,9 +601,36 @@ export const NLPixi = (props: Props) => {
} else {
text.rotation = rads;
}
text.style.stroke = imperative.current.getBackgroundColor();
};
// const text = labelMap.current.get(link._id);
const updateNodeLabel = (node: NodeTypeD3) => {
if (graph.current.nodes.length > config.LABEL_MAX_NODES) return;
const text = nodeLabelMap.current.get(node._id) as Text | undefined;
if (text == null) return;
if (node.x) text.x = node.x;
if (node.y) text.y = node.y;
const nodeMeta = props.graph.nodes[node._id];
const originalText = getNodeLabel(nodeMeta);
text.text = originalText; // This is required to ensure the text size check (next line) works
if ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) {
text.text = originalText;
} else {
// Change character limit at specific scale intervals
const charLimit = (responsiveScale > 0.2) ? 15 : (responsiveScale > 0.1) ? 30 : 75;
text.text = `${ originalText.slice(0, charLimit)}…`;
}
text.alpha = ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) ? 1 : 0;
}
// const text = linkLabelMap.current.get(link._id);
// if (!text) return;
// const source = link.source as NodeTypeD3;
......@@ -511,27 +651,14 @@ export const NLPixi = (props: Props) => {
// text.rotation = rads;
// }
async function loadAssets() {
if (!Assets.cache.has('texture')) {
Assets.addBundle('glyphs', {
texture: 'assets/sprite.png',
texture_square: 'assets/sprite_square.png',
texture_selected: 'assets/sprite_selected.png',
texture_selected_square: 'assets/sprite_selected_square.png',
});
await Assets.loadBundle('glyphs');
}
setAssetsLoaded(true);
}
useEffect(() => {
loadAssets();
return () => {
nodeMap.current.clear();
labelMap.current.clear();
linkLabelMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
linkLabelLayer.removeChildren();
nodeLabelLayer.removeChildren();
const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker;
if(layout?.cleanup != null) layout.cleanup();
......@@ -539,11 +666,11 @@ export const NLPixi = (props: Props) => {
}, []);
useEffect(() => {
if (assetsLoaded && props.graph && ref.current && ref.current.children.length > 0 && imperative.current) {
if (props.graph && ref.current && ref.current.children.length > 0 && imperative.current) {
if (isSetup.current === false) setup();
else update(false);
}
}, [config, assetsLoaded]);
}, [config]);
useEffect(() => {
if (props.graph) {
......@@ -589,6 +716,8 @@ export const NLPixi = (props: Props) => {
}
gfx.position.copyFrom(node as IPointData);
updateNodeLabel(node);
});
if (stopped === graph.current.nodes.length) {
......@@ -623,7 +752,8 @@ export const NLPixi = (props: Props) => {
nodeMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
linkLabelLayer.removeChildren();
nodeLabelLayer.removeChildren();
}
nodeMap.current.forEach((gfx, id) => {
......@@ -634,11 +764,11 @@ export const NLPixi = (props: Props) => {
}
});
labelMap.current.forEach((text, id) => {
linkLabelMap.current.forEach((text, id) => {
if (!graph.current.links.find((link) => link._id === id)) {
labelLayer.removeChild(text);
linkLabelLayer.removeChild(text);
text.destroy();
labelMap.current.delete(id);
linkLabelMap.current.delete(id);
}
});
......@@ -651,6 +781,7 @@ export const NLPixi = (props: Props) => {
node.x = old?.x || node.x;
node.y = old?.y || node.y;
updateNode(node);
updateNodeLabel(node);
} else {
createNode(node);
}
......@@ -658,7 +789,7 @@ export const NLPixi = (props: Props) => {
if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
graph.current.links.forEach((link) => {
if (!forceClear && labelMap.current.has(link._id)) {
if (!forceClear && linkLabelMap.current.has(link._id)) {
updateLinkLabel(link);
} else {
createLinkLabel(link);
......@@ -694,7 +825,7 @@ export const NLPixi = (props: Props) => {
*/
const setup = async () => {
nodeLayer.removeChildren();
labelLayer.removeChildren();
linkLabelLayer.removeChildren();
app.stage.removeChildren();
if (!props.graph) throw Error('Graph is undefined');
......@@ -723,8 +854,9 @@ export const NLPixi = (props: Props) => {
viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 });
viewport.current.addChild(linkGfx);
viewport.current.addChild(labelLayer);
viewport.current.addChild(linkLabelLayer);
viewport.current.addChild(nodeLayer);
viewport.current.addChild(nodeLabelLayer);
viewport.current.on('moved', (event) => {
imperative.current.onMoved(event);
});
......
......@@ -208,7 +208,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
value={settings.edges.width.width}
onChange={(val) => updateSettings({ edges: { ...settings.edges, width: { ...settings.edges.width, width: val } } })}
min={0.1}
max={2}
max={4}
step={0.1}
/>
</div>
......
......@@ -5,6 +5,7 @@ import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { useSearchResultData } from '@graphpolaris/shared/lib/data-access';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
export type TableProps = {
id: string;
......@@ -144,51 +145,74 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
return (
<SettingsContainer>
<Input
type="dropdown"
label="Select entity"
value={settings.displayEntity}
onChange={(val) => updateSettings({ displayEntity: val as string })}
options={graphMetadata.nodes.labels}
/>
<Input type="boolean" label="Show barplot" value={settings.showBarplot} onChange={(val) => updateSettings({ showBarplot: val })} />
<Input
type="dropdown"
label="Items per page"
value={settings.itemsPerPage}
onChange={(val) => updateSettings({ itemsPerPage: val as number })}
options={[10, 25, 50, 100]}
/>
<Input
type="number"
label="Max Bars in Bar Plots"
value={settings.maxBarsCount}
onChange={(val) => updateSettings({ maxBarsCount: val })}
/>
<div>
<span className="text-sm">Attributes to display:</span>
<Button
className="w-full text-justify justify-start"
variantType="secondary"
variant="ghost"
size="sm"
onClick={toggleCollapseAttr}
iconComponent="icon-[ic--baseline-arrow-drop-down]"
>
attributes:{' '}
</Button>
<div className="">
{!areCollapsedAttr && (
<Input
type="checkbox"
value={settings.displayAttributes}
options={selectedNodeAttributes}
onChange={(val: string[] | string) => {
const updatedVal = Array.isArray(val) ? val : [val];
updateSettings({ displayAttributes: updatedVal });
}}
<div className="my-2">
<Input
className="w-full text-justify justify-center"
type="dropdown"
value={settings.displayEntity}
options={graphMetadata.nodes.labels}
onChange={(val) => updateSettings({ displayEntity: val as string })}
overrideRender={
<EntityPill
title={
<div className="flex flex-row justify-between items-center cursor-pointer">
<span>{settings.displayEntity || ''}</span>
<Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
</div>
}
/>
)}
}
></Input>
<div className="my-2">
<Input
type="boolean"
label="Show barplot"
value={settings.showBarplot}
onChange={(val) => updateSettings({ showBarplot: val })}
/>
</div>
<div className="my-2">
<Input
type="dropdown"
label="Items per page"
value={settings.itemsPerPage}
onChange={(val) => updateSettings({ itemsPerPage: val as number })}
options={[10, 25, 50, 100]}
/>
</div>
<div className="my-2">
<Input
type="number"
label="Max Bars in Bar Plots"
value={settings.maxBarsCount}
onChange={(val) => updateSettings({ maxBarsCount: val })}
/>
</div>
<div className="flex flex-col items-start space-y-2">
<span className="text-sm">Attributes to display:</span>
<Button
className="w-full text-justify justify-start"
variantType="secondary"
variant="ghost"
size="sm"
onClick={toggleCollapseAttr}
iconComponent={areCollapsedAttr ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'}
>
attributes:{' '}
</Button>
<div className="">
{!areCollapsedAttr && (
<Input
type="checkbox"
value={settings.displayAttributes}
options={selectedNodeAttributes}
onChange={(val: string[] | string) => {
const updatedVal = Array.isArray(val) ? val : [val];
updateSettings({ displayAttributes: updatedVal });
}}
/>
)}
</div>
</div>
</div>
</SettingsContainer>
......