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 ...@@ -4,7 +4,7 @@ import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/com
import { MapProps } from '../../mapvis'; import { MapProps } from '../../mapvis';
import { EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components'; 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>>({}); const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
...@@ -59,22 +59,21 @@ export default function ChoroplethOptions({ settings, graphMetadata, updateSetti ...@@ -59,22 +59,21 @@ export default function ChoroplethOptions({ settings, graphMetadata, updateSetti
<div> <div>
<Input <Input
inline inline
label="Longitude" label="Latitude"
type="dropdown" type="dropdown"
value={settings?.[nodeType]?.lon} value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]} options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1} 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 <Input
inline inline
label="Latitude" label="Longitude"
type="dropdown" type="dropdown"
value={settings?.[nodeType]?.lat} value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]} options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1} 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> </div>
)} )}
......
...@@ -2,10 +2,10 @@ import React from 'react'; ...@@ -2,10 +2,10 @@ import React from 'react';
import { CompositeLayer, HeatmapLayer } from 'deck.gl'; import { CompositeLayer, HeatmapLayer } from 'deck.gl';
import * as d3 from 'd3'; import * as d3 from 'd3';
import { getDistance } from '../../utlis'; 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'; import { Node } from '@graphpolaris/shared/lib/data-access';
export class HeatLayer extends CompositeLayer<LayerProps> { export class HeatLayer extends CompositeLayer<CompositeLayerType> {
static type = 'Heatmap'; static type = 'Heatmap';
updateState({ changeFlags }: { changeFlags: any }) { updateState({ changeFlags }: { changeFlags: any }) {
...@@ -16,7 +16,7 @@ export class HeatLayer extends CompositeLayer<LayerProps> { ...@@ -16,7 +16,7 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
// Generates a path between source and target nodes // Generates a path between source and target nodes
return edges.map((edge: Edge, index) => { return edges.map((edge: Edge, index) => {
const length = getDistance(edge.path[0], edge.path[1]); 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 let xscale = d3
.scaleLinear() .scaleLinear()
...@@ -44,22 +44,24 @@ export class HeatLayer extends CompositeLayer<LayerProps> { ...@@ -44,22 +44,24 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
} }
renderLayers() { renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props; const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const layers: any[] = []; const layers: any[] = [];
const layerIds: string[] = []; const layerIds: string[] = [];
metaData.nodes.labels.forEach((label: string) => { graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-iconlayer`; const layerId = `${label}-nodes-heatmaplayer`;
layerIds.push(layerId); layerIds.push(layerId);
console.log(settings.nodes[label].size);
layers.push( layers.push(
new HeatmapLayer({ new HeatmapLayer<Node>({
id: `${label}-nodes-iconlayer`, id: layerId,
data: graph.nodes.filter((node: Node) => node.label === label), data: data.nodes.filter((node: Node) => node.label === label),
visible: !config[label]?.hidden, visible: !settings.nodes[label].hidden,
getPosition: (d: any) => getNodeLocation(d.id), getPosition: (d: any) => getNodeLocation(d.id),
getWeight: (d) => 1, getWeight: (d: any) => settings.nodes[label].size,
radiusPixels: settings.nodes[label].size,
aggregation: 'SUM', aggregation: 'SUM',
}), }),
); );
...@@ -68,77 +70,6 @@ export class HeatLayer extends CompositeLayer<LayerProps> { ...@@ -68,77 +70,6 @@ export class HeatLayer extends CompositeLayer<LayerProps> {
setLayerIds(layerIds); setLayerIds(layerIds);
return layers; 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'; ...@@ -2,87 +2,76 @@ import React, { useState, useMemo, useEffect } from 'react';
import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common'; import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common';
import { MapProps } from '../../mapvis'; import { MapProps } from '../../mapvis';
import { Button, EntityPill, Input } from '@graphpolaris/shared/lib/components'; import { Button, EntityPill, Input } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function HeatLayerOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) { export function HeatLayerOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
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);
});
const handleCollapseToggle = (nodeType: string) => { const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({ settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
...prevCollapsed, updateSettings({ nodes: settings.nodes });
[nodeType]: !prevCollapsed[nodeType],
}));
}; };
return ( return (
<div> <div>
{graphMetadata.nodes.labels.map((nodeType) => ( {settings?.nodes &&
<div className="mt-2" key={nodeType}> Object.keys(settings.nodes).map((nodeType) => {
<div className="flex items-center"> const nodeSettings = settings.nodes[nodeType];
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}> return (
<EntityPill title={nodeType} /> <div className="mt-2" key={nodeType}>
</div> <div className="flex items-center">
<div className="w-1/2"> <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<Button <EntityPill title={nodeType} />
iconComponent={settings?.[nodeType].hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'} </div>
variant="ghost" <div className="w-1/2">
onClick={() => { <Button
updateSettings({ [nodeType]: { ...settings?.[nodeType], hidden: !settings?.[nodeType].hidden as boolean } }); iconComponent={nodeSettings.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
}} variant="ghost"
/> onClick={() =>
</div> updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: !nodeSettings.hidden } } })
</div> }
/>
{!collapsed[nodeType] && ( </div>
<div> </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 {!nodeSettings.collapsed && (
inline <div>
label="Latitude" <Input
type="dropdown" inline
value={settings?.[nodeType]?.lat} label="Latitude"
options={[...spatialAttributes[nodeType]]} type="dropdown"
disabled={!settings.node || spatialAttributes[nodeType].length < 1} value={nodeSettings.lat}
onChange={(val) => updateSettings({ [nodeType]: { ...settings?.[nodeType], lat: val as string } })} 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> })}
))}
</div> </div>
); );
} }
import React from 'react'; import React from 'react';
import { CompositeLayer } from 'deck.gl'; import { CompositeLayer } from 'deck.gl';
import { IconLayer } from '@deck.gl/layers'; 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'; import { Node } from '@graphpolaris/shared/lib/data-access';
const ICON_MAPPING = { const ICON_MAPPING = {
marker: { x: 0, y: 0, width: 128, height: 128, mask: false }, 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'; static type = 'Icon';
updateState({ changeFlags }: { changeFlags: any }) { updateState({ changeFlags }: { changeFlags: any }) {
...@@ -16,26 +16,26 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> { ...@@ -16,26 +16,26 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> {
} }
renderLayers() { renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props; const { data, settings, getNodeLocation, setLayerIds, graphMetadata } = this.props;
const layers: any[] = []; const layers: any[] = [];
const layerIds: string[] = []; const layerIds: string[] = [];
metaData.nodes.labels.forEach((label: string) => { graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-iconlayer`; const layerId = `${label}-nodes-iconlayer`;
layerIds.push(layerId); layerIds.push(layerId);
layers.push( layers.push(
new IconLayer({ new IconLayer({
id: layerId, id: layerId,
data: graph.nodes.filter((node: Node) => node.label === label), data: data.nodes.filter((node: Node) => node.label === label),
visible: !config[label].hidden, visible: !settings.nodes[label].hidden,
iconAtlas: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png', iconAtlas: 'https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/icon-atlas.png',
iconMapping: ICON_MAPPING, iconMapping: ICON_MAPPING,
sizeScale: 10, sizeScale: 10,
pickable: true, pickable: true,
getIcon: (d: any) => 'marker', getIcon: (d: any) => 'marker',
getColor: (d: any) => config[label].color, getColor: (d: any) => settings.nodes[label].color,
getPosition: (d: any) => getNodeLocation(d._id), getPosition: (d: any) => getNodeLocation(d._id),
getSize: (d: any) => 3, getSize: (d: any) => 3,
}), }),
......
...@@ -4,7 +4,7 @@ import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/com ...@@ -4,7 +4,7 @@ import { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/com
import { MapProps } from '../../mapvis'; import { MapProps } from '../../mapvis';
import { EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components'; 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>>({}); const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
useEffect(() => { useEffect(() => {
...@@ -59,23 +59,23 @@ export default function IconOptions({ settings, graphMetadata, updateSettings }: ...@@ -59,23 +59,23 @@ export default function IconOptions({ settings, graphMetadata, updateSettings }:
<div> <div>
<Input <Input
inline inline
label="Longitude" label="Latitude"
type="dropdown" type="dropdown"
value={settings?.[nodeType]?.lon} value={settings?.[nodeType]?.lat}
options={[...spatialAttributes[nodeType]]} options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1} 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 <Input
inline inline
label="Latitude" label="Longitude"
type="dropdown" type="dropdown"
value={settings?.[nodeType]?.lat} value={settings?.[nodeType]?.lon}
options={[...spatialAttributes[nodeType]]} options={[...spatialAttributes[nodeType]]}
disabled={!settings.node || spatialAttributes[nodeType].length < 1} 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"> <div className="flex items-center gap-1">
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" /> <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
<Input <Input
......
...@@ -3,12 +3,13 @@ import { HeatLayer } from './heatmap-layer/HeatLayer'; ...@@ -3,12 +3,13 @@ import { HeatLayer } from './heatmap-layer/HeatLayer';
import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer'; import { NodeLinkLayer } from './nodelink-layer/NodeLinkLayer';
import { NodeLayer } from './node-layer/NodeLayer'; import { NodeLayer } from './node-layer/NodeLayer';
import { NodeIconLayer } from './icon-layer/IconLayer'; import { NodeIconLayer } from './icon-layer/IconLayer';
import NodeOptions from './node-layer/NodeOptions'; import { NodeLinkOptions } from './nodelink-layer/NodeLinkOptions';
import NodeLinkOptions from './nodelink-layer/NodeLinkOptions'; import { IconOptions } from './icon-layer/IconOptions';
import IconOptions from './icon-layer/IconOptions'; import { HeatLayerOptions } from './heatmap-layer/HeatLayerOptions';
import HeatLayerOptions from './heatmap-layer/HeatLayerOptions'; import { ChoroplethOptions } from './choropleth-layer/ChoroplethOptions';
import ChoroplethOptions from './choropleth-layer/ChoroplethOptions';
import { TileLayer, BitmapLayer } from 'deck.gl'; import { TileLayer, BitmapLayer } from 'deck.gl';
import { VisualizationSettingsPropTypes } from '../../../common';
import { MapProps } from '../mapvis';
export const layerTypes: Record<string, any> = { export const layerTypes: Record<string, any> = {
// node: NodeLayer, // node: NodeLayer,
...@@ -18,7 +19,11 @@ export const layerTypes: Record<string, any> = { ...@@ -18,7 +19,11 @@ export const layerTypes: Record<string, any> = {
// choropleth: ChoroplethLayer, // 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, nodelink: NodeLinkOptions,
heatmap: HeatLayerOptions, heatmap: HeatLayerOptions,
// node: NodeOptions, // node: NodeOptions,
......
import React from 'react'; import React from 'react';
import { CompositeLayer } from 'deck.gl'; import { CompositeLayer } from 'deck.gl';
import { ScatterplotLayer } from '@deck.gl/layers'; 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'; import { Node } from '@graphpolaris/shared/lib/data-access';
export class NodeLayer extends CompositeLayer<LayerProps> { export class NodeLayer extends CompositeLayer<CompositeLayerType> {
static type = 'Node'; static type = 'Node';
updateState({ changeFlags }: { changeFlags: any }) { updateState({ changeFlags }: { changeFlags: any }) {
...@@ -28,20 +28,20 @@ export class NodeLayer extends CompositeLayer<LayerProps> { ...@@ -28,20 +28,20 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
} }
renderLayers() { renderLayers() {
const { graph, config, getNodeLocation, setLayerIds, metaData } = this.props; const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const layers: any[] = []; const layers: any[] = [];
const layerIds: any[] = []; const layerIds: any[] = [];
metaData.nodes.labels.forEach((label: string) => { graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-scatterplot`; const layerId = `${label}-nodes-scatterplot`;
layerIds.push(layerId); layerIds.push(layerId);
layers.push( layers.push(
new ScatterplotLayer({ new ScatterplotLayer({
id: layerId, id: layerId,
visible: !config[label].hidden, visible: !settings.nodes[label].hidden,
data: graph.nodes.filter((node: Node) => node.label === label), data: data.nodes.filter((node: Node) => node.label === label),
pickable: true, pickable: true,
filled: true, filled: true,
radiusScale: 6, radiusScale: 6,
...@@ -49,8 +49,8 @@ export class NodeLayer extends CompositeLayer<LayerProps> { ...@@ -49,8 +49,8 @@ export class NodeLayer extends CompositeLayer<LayerProps> {
radiusMaxPixels: 100, radiusMaxPixels: 100,
lineWidthMinPixels: 1, lineWidthMinPixels: 1,
getPosition: (d: any) => getNodeLocation(d._id), getPosition: (d: any) => getNodeLocation(d._id),
getFillColor: (d: any) => config[label].color, getFillColor: (d: any) => settings.nodes[label].color,
getRadius: (d: any) => this.getRadius(d, config), getRadius: (d: any) => settings.nodes[label].radius,
}), }),
); );
}); });
......
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker'; 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 { VisualizationSettingsPropTypes } from '@graphpolaris/shared/lib/vis/common';
import { EntityPill, Icon, Input } from '@graphpolaris/shared/lib/components'; import { EntityPill, Icon, Input } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function NodeOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) { export default function NodeOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
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);
});
const handleCollapseToggle = (nodeType: string) => { const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({ settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
...prevCollapsed, updateSettings({ nodes: settings.nodes });
[nodeType]: !prevCollapsed[nodeType],
}));
}; };
return ( return (
<div> <div>
{graphMetadata.nodes.labels.map((nodeType) => ( {settings?.nodes &&
<div className="mt-2" key={nodeType}> Object.keys(settings.nodes).map((nodeType) => {
<div className="flex items-center"> const nodeSettings = settings.nodes[nodeType];
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}> return (
<EntityPill title={nodeType} /> <div className="mt-2" key={nodeType}>
</div> <div className="flex items-center">
<div className="w-1/2"> <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<ColorPicker <EntityPill title={nodeType} />
value={settings?.[nodeType]?.['color'] ? settings?.[nodeType]?.['color'] : [0, 0, 0]} </div>
updateValue={(val: number[]) => updateSettings({ [nodeType]: { ...settings?.[nodeType], color: val } })} <div className="w-1/2">
/> <ColorPicker
</div> value={nodeSettings.color}
</div> updateValue={(val) => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
{!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 } })}
/> />
</div> </div>
</div>
{!nodeSettings.collapsed && (
<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 <Input
label="Fixed" inline
type="boolean" label="Latitude"
value={settings?.[nodeType]?.fixed ?? false} type="dropdown"
onChange={(val: boolean) => updateSettings({ [nodeType]: { ...settings?.[nodeType], fixed: val } })} 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 ? ( <Input
<div> 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 <Input
label="Based on" label="Hidden"
type="dropdown" type="boolean"
size="xs" value={nodeSettings.hidden}
options={Object.keys(graphMetadata.nodes.types[nodeType].attributes).filter( onChange={(val) => {
(key) => graphMetadata.nodes.types[nodeType].attributes[key].dimension === 'numerical', updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } });
)} }}
value={settings?.[nodeType]?.sizeAttribute ?? ''}
onChange={(val: string | number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], sizeAttribute: 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> <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 <Input
type="slider" label="Fixed"
label="Width" type="boolean"
min={0} value={nodeSettings.fixed}
max={10} onChange={(val) => {
step={0.5} updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, fixed: val } } });
value={settings?.[nodeType]?.radius ?? 1} }}
onChange={(val: number) => updateSettings({ [nodeType]: { ...settings?.[nodeType], radius: 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> </div>
)} );
</div> })}
))}
</div> </div>
); );
} }
import React from 'react'; 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 { 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 { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions';
import { Edge, Node } from '@graphpolaris/shared/lib/data-access'; import { Edge, Node } from '@graphpolaris/shared/lib/data-access';
import { createIcon } from './shapeFactory'; import { createIcon } from './shapeFactory';
export class NodeLinkLayer extends CompositeLayer<LayerProps> { export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
static type = 'NodeLink'; static type = 'NodeLink';
private _layers: Record<string, Layer> = {};
constructor(props: LayerProps) {
super(props);
}
updateState({ changeFlags }: { changeFlags: any }) { updateState({ changeFlags }: { changeFlags: any }) {
return changeFlags.propsOrDataChanged || changeFlags.somethingChanged; return changeFlags.propsOrDataChanged || changeFlags.somethingChanged;
} }
renderLayers() { renderLayers() {
const { graph, config, getNodeLocation, selected, setLayerIds, metaData } = this.props; const { data, settings, getNodeLocation, selected, setLayerIds, graphMetadata } = this.props;
const layers = [];
const layerIds = [];
const brushingExtension = new BrushingExtension(); const brushingExtension = new BrushingExtension();
const collisionFilter = new CollisionFilterExtension(); const collisionFilter = new CollisionFilterExtension();
metaData.nodes.labels.forEach((label: string) => { graphMetadata.edges.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) => {
const layerId = `${label}-edges-line`; const layerId = `${label}-edges-line`;
layerIds.push(layerId);
const edgeData = 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( this._layers[layerId] = new LineLayer<Edge>({
new LineLayer({ id: layerId,
id: layerId, data: edgeData,
data: edgeData, visible: !settings.edges[label].hidden,
visible: !config[label]?.hidden, pickable: true,
pickable: true, getWidth: settings.edges[label].width,
getWidth: (d: any) => config[label].width, getSourcePosition: (d) => getNodeLocation(d.from),
getSourcePosition: (d: any) => getNodeLocation(d.from), getTargetPosition: (d) => getNodeLocation(d.to),
getTargetPosition: (d: any) => getNodeLocation(d.to), getColor: (d) => settings.edges[d.label].color,
getColor: (d: any) => config[d.label].color, extensions: [brushingExtension],
radiusScale: 3000, });
brushingEnabled: config.enableBrushing,
extensions: [brushingExtension],
}),
);
}); });
graphMetadata.nodes.labels.forEach((label: string) => {
const layerId = `${label}-nodes-scatterplot`;
const textLayerId = 'label-target'; this._layers[layerId] = new IconLayer<Node>({
layerIds.push(textLayerId); 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( const textLayerId = 'label-target';
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',
}),
);
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 React, { useState, useEffect } from 'react';
import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/colorPicker'; 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 { Button, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components';
import { MapLayerSettingsPropTypes } from '..';
export default function NodeLinkOptions({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) { export function NodeLinkOptions({ settings, graphMetadata, updateSettings, spatialAttributes }: MapLayerSettingsPropTypes) {
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({}); const handleCollapseToggle = (nodeType: string) => {
settings.nodes[nodeType].collapsed = !settings.nodes[nodeType].collapsed;
updateSettings({ nodes: settings.nodes });
};
useEffect(() => { useEffect(() => {
graphMetadata.nodes.labels.forEach((node) => { graphMetadata.nodes.labels.map((nodeType) => {
updateSettings({ if (settings?.[nodeType]?.lat) {
[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]); }, [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);
});
const handleCollapseToggle = (nodeType: string) => {
setCollapsed((prevCollapsed) => ({
...prevCollapsed,
[nodeType]: !prevCollapsed[nodeType],
}));
};
return ( return (
<div> <div>
{graphMetadata.nodes.labels.map((nodeType) => ( {settings?.nodes &&
<div className="mt-2" key={nodeType}> Object.keys(settings.nodes).map((nodeType) => {
<div className="flex items-center"> const nodeSettings = settings.nodes[nodeType];
<div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}> return (
<EntityPill title={nodeType} /> <div className="mt-2" key={nodeType}>
</div> <div className="flex items-center">
<div className="flex items-center space-x-2"> <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType)}>
<ColorPicker <EntityPill title={nodeType} />
value={settings?.[nodeType]?.['color'] ? settings?.[nodeType]?.['color'] : [0, 0, 0]} </div>
updateValue={(val: number[]) => updateSettings({ [nodeType]: { ...settings?.[nodeType], color: val } })} <div className="flex items-center space-x-2">
/> <ColorPicker
<Button value={nodeSettings.color}
iconComponent={settings?.[nodeType]?.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'} updateValue={(val) => {
variant="ghost" updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
onClick={() => { }}
updateSettings({ [nodeType]: { ...settings?.[nodeType], hidden: !settings?.[nodeType]?.hidden as boolean } }); />
}} <Button
/> iconComponent={nodeSettings.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'}
</div> variant="ghost"
</div> onClick={() => {
updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, hidden: !nodeSettings.hidden } } });
{!collapsed[nodeType] && ( }}
<div> />
<Input </div>
inline </div>
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>
))}
{graphMetadata.edges.labels.map((edgeType) => ( {!nodeSettings.collapsed && (
<div className="mt-2" key={edgeType}> <div>
<div className="flex items-center"> <Input
<div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType)}> inline
<RelationPill title={edgeType} /> label="Latitude"
</div> type="dropdown"
<div className="w-1/2 flex"> value={nodeSettings.lat}
<ColorPicker options={[...(spatialAttributes[nodeType] || [])]}
value={settings?.[edgeType]?.['color'] ? settings?.[edgeType]?.['color'] : [0, 0, 0]} disabled={!settings.node || spatialAttributes[nodeType].length < 1}
updateValue={(val: number[]) => updateSettings({ [edgeType]: { ...settings?.[edgeType], color: val } })} onChange={(val) => {
/> updateSettings({ nodes: { ...settings.nodes, [nodeType]: { ...nodeSettings, lat: String(val) } } });
<Button }}
iconComponent={settings?.[edgeType]?.hidden ? 'icon-[ic--baseline-visibility-off]' : 'icon-[ic--baseline-visibility]'} />
variant="ghost" <Input
onClick={() => { inline
updateSettings({ [edgeType]: { ...settings?.[edgeType], hidden: !settings?.[edgeType]?.hidden as boolean } }); 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>
</div> );
})}
{!collapsed[edgeType] && ( {settings?.edges &&
<div> Object.keys(settings.edges).map((edgeType) => {
<Input const edgeSettings = settings.edges[edgeType];
label="Enable brushing"
type="boolean"
value={settings.enableBrushing}
onChange={(val) => {
updateSettings({ enableBrushing: val as boolean });
}}
/>
<div> return (
<div className="flex items-center gap-1"> <div className="mt-2" key={edgeType}>
<Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" /> <div className="flex items-center">
<span>Width</span> <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType)}>
<RelationPill title={edgeType} />
</div> </div>
<Input <div className="w-1/2 flex">
label="Fixed" <ColorPicker
type="boolean" value={settings.edges[edgeType].color}
value={settings?.[edgeType]?.fixed ?? false} updateValue={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } })}
onChange={(val: boolean) => updateSettings({ [edgeType]: { ...settings?.[edgeType], fixed: val } })} />
/> <Button
{!settings?.[edgeType]?.fixed ? ( 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> <div>
<Input <div className="flex items-center gap-1">
label="Based on" <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
type="dropdown" <span>Width</span>
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> </div>
</div>
) : (
<div>
<Input <Input
type="slider" label="Fixed"
label="Width" type="boolean"
min={0} value={edgeSettings.fixed}
max={10} onChange={(val) => updateSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } })}
step={0.5}
value={settings?.[edgeType]?.width ?? 1}
onChange={(val: number) => updateSettings({ [edgeType]: { ...settings?.[edgeType], width: 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>
)} );
</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 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 { 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 { VISComponentType, VisualizationPropTypes } from '../../common';
import { layerTypes, createBaseMap } from './layers'; import { layerTypes, createBaseMap, MapLayerSettingsPropTypes } from './layers';
import { MapSettings } from './settings'; import { MapSettings } from './MapSettings';
import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; 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'; 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 = { const INITIAL_VIEW_STATE = {
latitude: 52.1006, latitude: 52.1006,
...@@ -24,10 +59,8 @@ const INITIAL_VIEW_STATE = { ...@@ -24,10 +59,8 @@ const INITIAL_VIEW_STATE = {
const FLY_SPEED = 1000; const FLY_SPEED = 1000;
const baseLayer = createBaseMap(); export const MapVis = (props: VisualizationPropTypes<MapProps>) => {
const baseLayer = useRef(createBaseMap());
export const MapVis = ({ data, settings, handleSelect, graphMetadata }: VisualizationPropTypes<MapProps>) => {
const [layer, setLayer] = useState<Layer | undefined>(undefined);
const [viewport, setViewport] = useState<Record<string, any>>(INITIAL_VIEW_STATE); const [viewport, setViewport] = useState<Record<string, any>>(INITIAL_VIEW_STATE);
const [hoverObject, setHoverObject] = useState<Node | null>(null); const [hoverObject, setHoverObject] = useState<Node | null>(null);
const [selected, setSelected] = useState<any[]>([]); const [selected, setSelected] = useState<any[]>([]);
...@@ -64,20 +97,28 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz ...@@ -64,20 +97,28 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
[getFittedViewport], [getFittedViewport],
); );
useEffect(() => { const [dataLayer, setDataLayer] = useState<{ component: CompositeLayer<CompositeLayerType>; id: string } | null>(null);
setLayer({
type: settings.layer ? layerTypes?.[settings.layer] : layerTypes.nodelink,
config: settings,
});
}, [settings.layer]);
const dataLayer = useMemo(() => {
if (!layer || !settings.layer) return 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) => { (acc, node) => {
const latitude = settings?.[node.label]?.lat ? (node?.attributes?.[settings[node.label].lat] as string) : undefined; const latitude = props.settings.nodes?.[node.label]?.lat
const longitude = settings?.[node.label]?.lon ? (node?.attributes?.[settings[node.label].lon] as string) : undefined; ? (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) { if (latitude !== undefined && longitude !== undefined) {
acc[node._id] = [parseFloat(longitude), parseFloat(latitude)]; acc[node._id] = [parseFloat(longitude), parseFloat(latitude)];
...@@ -85,20 +126,26 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz ...@@ -85,20 +126,26 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
return acc; return acc;
}, },
{} as { [id: string]: Coordinate }, {} as { [id: string]: Position },
); );
return new layer.type({ const layerProps: CompositeLayerType = {
graph: data, ...props,
metaData: graphMetadata,
config: settings,
selected: selected, selected: selected,
hoverObject: hoverObject, hoverObject: hoverObject,
getNodeLocation: (d: string) => coordinateLookup[d], getNodeLocation: (d: string) => coordinateLookup[d],
flyToBoundingBox: flyToBoundingBox, flyToBoundingBox: flyToBoundingBox,
setLayerIds: (val: string[]) => setLayerIds(val), 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( const selectionLayer = useMemo(
() => () =>
...@@ -122,16 +169,16 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz ...@@ -122,16 +169,16 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
} }
} }
setSelected(nodes.map((node) => node._id)); setSelected(nodes.map((node) => node._id));
handleSelect({ nodes, edges }); props.handleSelect({ nodes, edges });
} else { } else {
handleSelect(); props.handleSelect();
} }
setSelectingRectangle(false); setSelectingRectangle(false);
}, },
layerIds: layerIds, layerIds: layerIds,
getTentativeFillColor: () => [22, 37, 67, 100], getTentativeFillColor: () => [22, 37, 67, 100],
}), }),
[selectingRectangle, layer], [selectingRectangle],
); );
return ( return (
...@@ -146,35 +193,35 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz ...@@ -146,35 +193,35 @@ export const MapVis = ({ data, settings, handleSelect, graphMetadata }: Visualiz
</div> </div>
{isSearching && ( {isSearching && (
<SearchBar <SearchBar
onSearch={(boundingbox: [number, number, number, number]) => { onSearch={(boundingBox: [number, number, number, number]) => {
flyToBoundingBox(...boundingbox); flyToBoundingBox(...boundingBox);
setIsSearching(false); setIsSearching(false);
}} }}
/> />
)} )}
<DeckGL <DeckGL
layers={[baseLayer, dataLayer, selectionLayer]} layers={[baseLayer.current, dataLayer?.component, selectionLayer]}
controller={true} controller={true}
initialViewState={viewport} initialViewState={viewport}
onViewStateChange={({ viewState }) => setViewport(viewState)} onViewStateChange={({ viewState }) => setViewport(viewState)}
onClick={({ object }) => { onClick={({ object }) => {
if (data) { if (props.data) {
if (!object) { if (!object) {
handleSelect(); props.handleSelect();
setSelected([]); setSelected([]);
return; return;
} }
if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) { if (object.hasOwnProperty('attributes') && object.hasOwnProperty('id') && object.hasOwnProperty('label')) {
handleSelect({ nodes: [object] }); props.handleSelect({ nodes: [object] });
setSelected([object.id]); setSelected([object.id]);
} }
if (object.type === 'Feature') { if (object.type === 'Feature') {
const ids = object.properties.nodes; const ids = object.properties.nodes;
if (ids.length > 0) { if (ids.length > 0) {
const nodes = data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id)); const nodes = props.data.nodes.filter((node) => ids.includes((node as unknown as { id: string }).id));
handleSelect({ nodes: [...nodes] }); props.handleSelect({ nodes: [...nodes] });
} else { } else {
handleSelect(); props.handleSelect();
setSelected([]); setSelected([]);
return; 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 type Coordinate = [number, number] | [];
export interface LayerProps { export interface LayerProps {
[key: string]: any; [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 = { export type Layer = {
type: any; id: string;
config: any; component: CompositeLayer<CompositeLayerType>;
settings: MapProps & VisualizationSettingsType;
}; };
export type Node = { 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'; ...@@ -3,17 +3,16 @@ import { dataColors, visualizationColors } from 'config';
import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { import {
Application, Application,
AssetsBundle,
Color, Color,
Container, Container,
FederatedPointerEvent, FederatedPointerEvent,
Graphics, Graphics,
IPointData, IPointData,
Sprite, Sprite,
Assets,
Text, Text,
Texture, Texture,
Resource, Resource,
RenderTexture
} from 'pixi.js'; } from 'pixi.js';
import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access'; import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access';
import { NLPopup } from './NLPopup'; import { NLPopup } from './NLPopup';
...@@ -24,6 +23,8 @@ import { Viewport } from 'pixi-viewport'; ...@@ -24,6 +23,8 @@ import { Viewport } from 'pixi-viewport';
import { NodelinkVisProps } from '../nodelinkvis'; import { NodelinkVisProps } from '../nodelinkvis';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { MovedEvent } from 'pixi-viewport/dist/types'; 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 = { type Props = {
onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void; onClick: (event?: { node: NodeTypeD3; pos: IPointData }) => void;
...@@ -47,7 +48,12 @@ type LayoutState = 'reset' | 'running' | 'paused'; ...@@ -47,7 +48,12 @@ type LayoutState = 'reset' | 'running' | 'paused';
export const NLPixi = (props: Props) => { export const NLPixi = (props: Props) => {
const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>();
const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]); const [popups, setPopups] = useState<{ node: NodeTypeD3; pos: IPointData }[]>([]);
const [assetsLoaded, setAssetsLoaded] = useState(false);
const globalConfig = useConfig();
useEffect(() => {
update();
}, [globalConfig.currentTheme]);
const app = useMemo( const app = useMemo(
() => () =>
...@@ -61,7 +67,13 @@ export const NLPixi = (props: Props) => { ...@@ -61,7 +67,13 @@ export const NLPixi = (props: Props) => {
[], [],
); );
const nodeLayer = useMemo(() => new Container(), []); 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(); const container = new Container();
container.alpha = 0; container.alpha = 0;
container.renderable = false; container.renderable = false;
...@@ -70,7 +82,8 @@ export const NLPixi = (props: Props) => { ...@@ -70,7 +82,8 @@ export const NLPixi = (props: Props) => {
const nodeMap = useRef(new Map<string, Sprite>()); const nodeMap = useRef(new Map<string, Sprite>());
const linkGfx = new Graphics(); 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 viewport = useRef<Viewport>();
const layoutState = useRef<LayoutState>('reset'); const layoutState = useRef<LayoutState>('reset');
const layoutStoppedCount = useRef(0); const layoutStoppedCount = useRef(0);
...@@ -87,11 +100,71 @@ export const NLPixi = (props: Props) => { ...@@ -87,11 +100,71 @@ export const NLPixi = (props: Props) => {
// const cull = new Cull(); // const cull = new Cull();
// let cullDirty = useRef(true); // let cullDirty = useRef(true);
const textureId = (selected: boolean = false) => { const _glyphTexture = RenderTexture.create();
const selectionSuffix = selected ? '_selected' : ''; const _selectedTexture = RenderTexture.create();
const shapeSuffix = props.configuration.shapes.shape == 'rectangle' ? '_square' : '';
return `texture${selectionSuffix}${shapeSuffix}`; 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({ const [config, setConfig] = useState({
width: 1000, width: 1000,
...@@ -101,7 +174,7 @@ export const NLPixi = (props: Props) => { ...@@ -101,7 +174,7 @@ export const NLPixi = (props: Props) => {
LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER, LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
NODE_RADIUS: 5, NODE_RADIUS: 70,
// NODE_BORDER_LINE_WIDTH: 1.0, // 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_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], // NODE_BORDER_COLOR_DEFAULT: dataColors.neutral[70],
...@@ -113,6 +186,14 @@ export const NLPixi = (props: Props) => { ...@@ -113,6 +186,14 @@ export const NLPixi = (props: Props) => {
LINE_WIDTH_DEFAULT: 0.8, 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(() => { useEffect(() => {
setConfig((lastConfig) => { setConfig((lastConfig) => {
return { return {
...@@ -122,15 +203,6 @@ export const NLPixi = (props: Props) => { ...@@ -122,15 +203,6 @@ export const NLPixi = (props: Props) => {
}); });
}, [props.layoutAlgorithm, props.configuration]); }, [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 imperative = useRef<any>(null);
const mouseClickThreshold = 200; // Time between mouse up and down events that is considered a click, and not a drag. 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) => { ...@@ -161,11 +233,13 @@ export const NLPixi = (props: Props) => {
setPopups([{ node: node, pos: toGlobal(node) }]); setPopups([{ node: node, pos: toGlobal(node) }]);
for (const popup of popups) { for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite; 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) }); props.onClick({ node: node, pos: toGlobal(node) });
...@@ -180,7 +254,8 @@ export const NLPixi = (props: Props) => { ...@@ -180,7 +254,8 @@ export const NLPixi = (props: Props) => {
if (holdDownTime < mouseClickThreshold) { if (holdDownTime < mouseClickThreshold) {
for (const popup of popups) { for (const popup of popups) {
const sprite = nodeMap.current.get(popup.node._id) as Sprite; 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([]); setPopups([]);
props.onClick(); props.onClick();
...@@ -220,26 +295,39 @@ export const NLPixi = (props: Props) => { ...@@ -220,26 +295,39 @@ export const NLPixi = (props: Props) => {
onZoom(event: FederatedPointerEvent) { onZoom(event: FederatedPointerEvent) {
const scale = viewport.current!.transform.scale.x; const scale = viewport.current!.transform.scale.x;
if (graph.current.nodes.length < config.LABEL_MAX_NODES) { if (scale > 2) {
labelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0; 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) { if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
labelLayer.renderable = true; 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 nodeLabelLayer.alpha = (scale > 5) ? Math.min(1, (scale - 5) * 3) : 0;
const fontSize = scale < 0.1 ? 30 : scale < 0.2 ? 40 : scale < 0.3 ? 50 : 60; if (nodeLabelLayer.alpha > 0) {
const strokeWidth = fontSize / 2; nodeLabelLayer.renderable = true;
labelMap.current.forEach((text) => {
text.style.fontSize = fontSize;
text.style.strokeThickness = strokeWidth;
});
} else { } 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() { function resize() {
...@@ -286,8 +374,9 @@ export const NLPixi = (props: Props) => { ...@@ -286,8 +374,9 @@ export const NLPixi = (props: Props) => {
// Update texture when selected // Update texture when selected
const nodeMeta = props.graph.nodes[node._id]; 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; gfx.texture = texture;
// Cluster colors // Cluster colors
if (nodeMeta?.cluster) { if (nodeMeta?.cluster) {
...@@ -314,6 +403,10 @@ export const NLPixi = (props: Props) => { ...@@ -314,6 +403,10 @@ export const NLPixi = (props: Props) => {
// } // }
}; };
const getNodeLabel = (nodeMeta: NodeType) => {
return nodeMeta.label
}
const createNode = (node: NodeTypeD3, selected?: boolean) => { const createNode = (node: NodeTypeD3, selected?: boolean) => {
const nodeMeta = props.graph.nodes[node._id]; const nodeMeta = props.graph.nodes[node._id];
...@@ -325,11 +418,11 @@ export const NLPixi = (props: Props) => { ...@@ -325,11 +418,11 @@ export const NLPixi = (props: Props) => {
if (node.x === undefined || node.y === undefined) return; if (node.x === undefined || node.y === undefined) return;
let sprite: Sprite; let sprite: Sprite;
const texture = Assets.get(textureId()); const texture = glyphTexture;
sprite = new Sprite(texture); sprite = new Sprite(texture);
sprite.tint = nodeColor(nodeMeta.type); 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.scale.set(scale, scale);
sprite.anchor.set(0.5, 0.5); sprite.anchor.set(0.5, 0.5);
sprite.cullable = true; sprite.cullable = true;
...@@ -346,13 +439,31 @@ export const NLPixi = (props: Props) => { ...@@ -346,13 +439,31 @@ export const NLPixi = (props: Props) => {
updateNode(node); updateNode(node);
(sprite as any).node = 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; return sprite;
}; };
const createLinkLabel = (link: LinkTypeD3) => { const createLinkLabel = (link: LinkTypeD3) => {
// check if link is already drawn, and if so, delete it // check if link is already drawn, and if so, delete it
if (link && link?._id && labelMap.current.has(link._id)) { if (link && link?._id && linkLabelMap.current.has(link._id)) {
labelMap.current.delete(link._id); linkLabelMap.current.delete(link._id);
} }
const linkMeta = props.graph.links[link._id]; const linkMeta = props.graph.links[link._id];
...@@ -360,14 +471,14 @@ export const NLPixi = (props: Props) => { ...@@ -360,14 +471,14 @@ export const NLPixi = (props: Props) => {
const text = new Text(linkMeta.name, { const text = new Text(linkMeta.name, {
fontSize: 60, fontSize: 60,
fill: config.LINE_COLOR_DEFAULT, fill: config.LINE_COLOR_DEFAULT,
stroke: 0xffffff, stroke: imperative.current.getBackgroundColor(),
strokeThickness: 30, strokeThickness: 30,
}); });
text.cullable = true; text.cullable = true;
text.anchor.set(0.5, 0.5); text.anchor.set(0.5, 0.5);
text.scale.set(0.1, 0.1); text.scale.set(0.1, 0.1);
labelMap.current.set(link._id, text); linkLabelMap.current.set(link._id, text);
labelLayer.addChild(text); linkLabelLayer.addChild(text);
updateLinkLabel(link); updateLinkLabel(link);
...@@ -408,7 +519,7 @@ export const NLPixi = (props: Props) => { ...@@ -408,7 +519,7 @@ export const NLPixi = (props: Props) => {
// let color = link.color || 0x000000; // let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT; let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT; let style = imperative.current.getLinkWidth();
let alpha = linkMeta.alpha || 1; let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) { if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML; color = config.LINE_COLOR_ML;
...@@ -450,7 +561,9 @@ export const NLPixi = (props: Props) => { ...@@ -450,7 +561,9 @@ export const NLPixi = (props: Props) => {
}; };
const updateLinkLabel = (link: LinkTypeD3) => { 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; if (!text) return;
const _source = link.source; const _source = link.source;
...@@ -488,9 +601,36 @@ export const NLPixi = (props: Props) => { ...@@ -488,9 +601,36 @@ export const NLPixi = (props: Props) => {
} else { } else {
text.rotation = rads; 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; // if (!text) return;
// const source = link.source as NodeTypeD3; // const source = link.source as NodeTypeD3;
...@@ -511,27 +651,14 @@ export const NLPixi = (props: Props) => { ...@@ -511,27 +651,14 @@ export const NLPixi = (props: Props) => {
// text.rotation = rads; // 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(() => { useEffect(() => {
loadAssets();
return () => { return () => {
nodeMap.current.clear(); nodeMap.current.clear();
labelMap.current.clear(); linkLabelMap.current.clear();
linkGfx.clear(); linkGfx.clear();
nodeLayer.removeChildren(); nodeLayer.removeChildren();
labelLayer.removeChildren(); linkLabelLayer.removeChildren();
nodeLabelLayer.removeChildren();
const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker; const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker;
if(layout?.cleanup != null) layout.cleanup(); if(layout?.cleanup != null) layout.cleanup();
...@@ -539,11 +666,11 @@ export const NLPixi = (props: Props) => { ...@@ -539,11 +666,11 @@ export const NLPixi = (props: Props) => {
}, []); }, []);
useEffect(() => { 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(); if (isSetup.current === false) setup();
else update(false); else update(false);
} }
}, [config, assetsLoaded]); }, [config]);
useEffect(() => { useEffect(() => {
if (props.graph) { if (props.graph) {
...@@ -589,6 +716,8 @@ export const NLPixi = (props: Props) => { ...@@ -589,6 +716,8 @@ export const NLPixi = (props: Props) => {
} }
gfx.position.copyFrom(node as IPointData); gfx.position.copyFrom(node as IPointData);
updateNodeLabel(node);
}); });
if (stopped === graph.current.nodes.length) { if (stopped === graph.current.nodes.length) {
...@@ -623,7 +752,8 @@ export const NLPixi = (props: Props) => { ...@@ -623,7 +752,8 @@ export const NLPixi = (props: Props) => {
nodeMap.current.clear(); nodeMap.current.clear();
linkGfx.clear(); linkGfx.clear();
nodeLayer.removeChildren(); nodeLayer.removeChildren();
labelLayer.removeChildren(); linkLabelLayer.removeChildren();
nodeLabelLayer.removeChildren();
} }
nodeMap.current.forEach((gfx, id) => { nodeMap.current.forEach((gfx, id) => {
...@@ -634,11 +764,11 @@ export const NLPixi = (props: Props) => { ...@@ -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)) { if (!graph.current.links.find((link) => link._id === id)) {
labelLayer.removeChild(text); linkLabelLayer.removeChild(text);
text.destroy(); text.destroy();
labelMap.current.delete(id); linkLabelMap.current.delete(id);
} }
}); });
...@@ -651,6 +781,7 @@ export const NLPixi = (props: Props) => { ...@@ -651,6 +781,7 @@ export const NLPixi = (props: Props) => {
node.x = old?.x || node.x; node.x = old?.x || node.x;
node.y = old?.y || node.y; node.y = old?.y || node.y;
updateNode(node); updateNode(node);
updateNodeLabel(node);
} else { } else {
createNode(node); createNode(node);
} }
...@@ -658,7 +789,7 @@ export const NLPixi = (props: Props) => { ...@@ -658,7 +789,7 @@ export const NLPixi = (props: Props) => {
if (graph.current.nodes.length < config.LABEL_MAX_NODES) { if (graph.current.nodes.length < config.LABEL_MAX_NODES) {
graph.current.links.forEach((link) => { graph.current.links.forEach((link) => {
if (!forceClear && labelMap.current.has(link._id)) { if (!forceClear && linkLabelMap.current.has(link._id)) {
updateLinkLabel(link); updateLinkLabel(link);
} else { } else {
createLinkLabel(link); createLinkLabel(link);
...@@ -694,7 +825,7 @@ export const NLPixi = (props: Props) => { ...@@ -694,7 +825,7 @@ export const NLPixi = (props: Props) => {
*/ */
const setup = async () => { const setup = async () => {
nodeLayer.removeChildren(); nodeLayer.removeChildren();
labelLayer.removeChildren(); linkLabelLayer.removeChildren();
app.stage.removeChildren(); app.stage.removeChildren();
if (!props.graph) throw Error('Graph is undefined'); if (!props.graph) throw Error('Graph is undefined');
...@@ -723,8 +854,9 @@ export const NLPixi = (props: Props) => { ...@@ -723,8 +854,9 @@ export const NLPixi = (props: Props) => {
viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 }); viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 });
viewport.current.addChild(linkGfx); viewport.current.addChild(linkGfx);
viewport.current.addChild(labelLayer); viewport.current.addChild(linkLabelLayer);
viewport.current.addChild(nodeLayer); viewport.current.addChild(nodeLayer);
viewport.current.addChild(nodeLabelLayer);
viewport.current.on('moved', (event) => { viewport.current.on('moved', (event) => {
imperative.current.onMoved(event); imperative.current.onMoved(event);
}); });
......
...@@ -208,7 +208,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza ...@@ -208,7 +208,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza
value={settings.edges.width.width} value={settings.edges.width.width}
onChange={(val) => updateSettings({ edges: { ...settings.edges, width: { ...settings.edges.width, width: val } } })} onChange={(val) => updateSettings({ edges: { ...settings.edges, width: { ...settings.edges.width, width: val } } })}
min={0.1} min={0.1}
max={2} max={4}
step={0.1} step={0.1}
/> />
</div> </div>
......
...@@ -5,6 +5,7 @@ import { Input } from '@graphpolaris/shared/lib/components/inputs'; ...@@ -5,6 +5,7 @@ import { Input } from '@graphpolaris/shared/lib/components/inputs';
import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { Button } from '@graphpolaris/shared/lib/components/buttons';
import { useSearchResultData } from '@graphpolaris/shared/lib/data-access'; import { useSearchResultData } from '@graphpolaris/shared/lib/data-access';
import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
export type TableProps = { export type TableProps = {
id: string; id: string;
...@@ -144,51 +145,74 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio ...@@ -144,51 +145,74 @@ const TableSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
return ( return (
<SettingsContainer> <SettingsContainer>
<Input <div className="my-2">
type="dropdown" <Input
label="Select entity" className="w-full text-justify justify-center"
value={settings.displayEntity} type="dropdown"
onChange={(val) => updateSettings({ displayEntity: val as string })} value={settings.displayEntity}
options={graphMetadata.nodes.labels} options={graphMetadata.nodes.labels}
/> onChange={(val) => updateSettings({ displayEntity: val as string })}
<Input type="boolean" label="Show barplot" value={settings.showBarplot} onChange={(val) => updateSettings({ showBarplot: val })} /> overrideRender={
<Input <EntityPill
type="dropdown" title={
label="Items per page" <div className="flex flex-row justify-between items-center cursor-pointer">
value={settings.itemsPerPage} <span>{settings.displayEntity || ''}</span>
onChange={(val) => updateSettings({ itemsPerPage: val as number })} <Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
options={[10, 25, 50, 100]} </div>
/> }
<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 });
}}
/> />
)} }
></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>
</div> </div>
</SettingsContainer> </SettingsContainer>
......