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 416 additions and 215 deletions
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge';
import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow';
import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState, MiniMap } from 'reactflow';
import 'reactflow/dist/style.css';
import { Button } from '../../components/buttons';
import { useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access';
......@@ -10,8 +10,7 @@ import { SelfEdge } from '../pills/edges/self-edge';
import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill';
import { SchemaRelationPill } from '../pills/nodes/relation/SchemaRelationPill';
import { SchemaSettings } from './SchemaSettings';
import { Settings } from '@mui/icons-material';
import { ContentCopy, FitScreen, Fullscreen, KeyboardArrowDown, KeyboardArrowRight, Remove } from '@mui/icons-material';
import { Settings, ContentCopy, Fullscreen, Remove } from '@mui/icons-material';
import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout';
import { ConnectionLine, ConnectionDragLine } from '../../querybuilder';
import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils';
......@@ -97,7 +96,13 @@ export const Schema = (props: Props) => {
const xy = bounds ? { x1: 50, x2: bounds.width - 50, y1: 50, y2: bounds.height - 200 } : { x1: 0, x2: 500, y1: 0, y2: 1000 };
await layout.current?.layout(expandedSchema, xy);
const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType, settings.animatedEdges);
setNodes(schemaFlow.nodes);
const nodesWithRef = schemaFlow.nodes.map((node) => ({
...node,
data: { ...node.data, reactFlowRef },
}));
setNodes(nodesWithRef);
setEdges(schemaFlow.edges);
setTimeout(() => fitView(), 100);
}
......@@ -115,6 +120,17 @@ export const Schema = (props: Props) => {
);
}, [searchResults]);
const nodeColor = (node: any) => {
switch (node.type) {
case 'entity':
return '#fb7b04';
case 'relation':
return '#0676C1';
default:
return '#ff0072';
}
};
return (
<Panel
title="Schema"
......@@ -132,7 +148,7 @@ export const Schema = (props: Props) => {
}}
/>
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Hide</p>
</TooltipContent>
</Tooltip>
......@@ -149,15 +165,23 @@ export const Schema = (props: Props) => {
}}
/>
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Copy Schema to Clipboard</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent={<FitScreen />} onClick={() => {}} />
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent={<Fullscreen />}
onClick={() => {
fitView();
}}
/>
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Fit to screen</p>
</TooltipContent>
</Tooltip>
......@@ -167,7 +191,7 @@ export const Schema = (props: Props) => {
<TooltipTrigger>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent={<Settings />} className="schema-settings" />
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Schema settings</p>
</TooltipContent>
</Tooltip>
......@@ -202,7 +226,9 @@ export const Schema = (props: Props) => {
onInit(reactFlowInstance);
}}
proOptions={{ hideAttribution: true }}
></ReactFlow>
>
<MiniMap nodeColor={nodeColor} />
</ReactFlow>
</ReactFlowProvider>
)}
{/* <div>
......
import React, { useState } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow';
import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder';
import { SchemaEntityPopup } from './SchemaEntityPopup';
import { Popup } from '@graphpolaris/shared/lib/components/Popup';
import { SchemaNode } from '../../../model';
import { EntityPill } from '@graphpolaris/shared/lib/components';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis';
export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => {
const [openPopup, setOpenPopup] = useState(false);
const ref = useRef<HTMLDivElement>(null);
/**
* adds drag functionality in order to be able to drag the entityNode to the schema
* @param event React Mouse drag event
......@@ -40,12 +40,6 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc
return (
<>
{openPopup && (
<Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem">
<SchemaEntityPopup data={data} onClose={() => setOpenPopup(false)} />
</Popup>
)}
<div
className="w-fit h-fit"
onDragStart={(event) => onDragStart(event)}
......@@ -57,7 +51,34 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc
setOpenPopup(!openPopup);
}}
draggable
ref={ref}
>
{openPopup && (
<Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}>
<TooltipTrigger />
<TooltipContent side="right">
<div>
<VisualizationTooltip
type="schema"
typeOfSchema="node"
name={data.name}
colorHeader="#fb7b04"
numberOfElements={1000}
data={data.attributes.reduce(
(acc, attr) => {
if (attr.name && attr.type) {
acc[attr.name] = attr.type;
}
return acc;
},
{} as Record<string, string>,
)}
/>
</div>
</TooltipContent>
</Tooltip>
)}
<EntityPill
draggable
title={id}
......
/**
* This program has been developed by students from the bachelor Computer Science at
* Utrecht University within the Software Project course.
* © Copyright Utrecht University (Department of Information and Computing Sciences)
*/
/* istanbul ignore file */
/* The comment above was added so the code coverage wouldn't count this file towards code coverage.
* We do not test components/renderfunctions/styling files.
* See testing plan for more details.*/
import { FormBody, FormCard, FormControl, FormHBar, FormTitle } from '@graphpolaris/shared/lib/components/forms';
import { SchemaReactflowEntity } from '@graphpolaris/shared/lib/schema/model';
import { FormEvent } from 'react';
export type SchemaEntityPopupProps = {
data: SchemaReactflowEntity;
onClose: () => void;
};
/**
* NodeQualityEntityPopupNode is the node that represents the popup that shows the node quality for an entity
* @param data Input data of type NodeQualityDataForEntities, which is for the popup.
*/
export const SchemaEntityPopup = (props: SchemaEntityPopupProps) => {
function submit() {
// dispatch(setSchemaSettings(state));
props.onClose();
}
return (
// <FormDiv hAnchor="left">
<>
<FormCard>
<FormBody
onSubmit={(e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
submit();
}}
>
<FormTitle
title="Node Statistics"
// title={props.data.name}
onClose={props.onClose}
/>
<FormHBar />
<span className="px-5 pt-2">
<span>Name</span>
<span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.name}</span>
</span>
<FormHBar />
<span className="px-5 pt-2">
<span>Attributes</span>
<span className="float-right font-light font-mono">{props.data.attributes.length}</span>
</span>
{props.data.attributes.map((attribute: any) => {
return (
<div key={attribute.name} className="px-5 pt-1">
<span>{attribute.name}</span>
<span className="float-right font-light font-mono">{attribute.type}</span>
</div>
);
})}
<FormHBar />
<FormControl>
<button
className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5"
onClick={() => {
submit();
}}
>
Close
</button>
</FormControl>
</FormBody>
</FormCard>
</>
);
};
import React, { useState } from 'react';
import React, { useState, useRef } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow';
import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder';
......@@ -7,8 +7,12 @@ import { SchemaRelationshipPopup } from './SchemaRelationshipPopup';
import { SchemaEdge } from '../../../model';
import { RelationPill } from '@graphpolaris/shared/lib/components';
import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis';
export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => {
const [openPopup, setOpenPopup] = useState(false);
const ref = useRef<HTMLDivElement>(null);
/**
* Adds drag functionality in order to be able to drag the relationNode to the schema.
......@@ -43,14 +47,8 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }:
const onClickToggleAttributeAnalyticsPopupMenu = (): void => {
data.toggleAttributeAnalyticsPopupMenu(data.collection);
};
return (
<>
{openPopup && (
<Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem">
<SchemaRelationshipPopup data={data} onClose={() => setOpenPopup(false)} />
</Popup>
)}
<div
className="w-fit h-fit"
onDragStart={(event) => onDragStart(event)}
......@@ -63,6 +61,37 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }:
}}
draggable
>
{openPopup && (
<Tooltip key={data.name} open={true} boundaryElement={ref} showArrow={true}>
<TooltipTrigger />
<TooltipContent side="top">
<div>
<VisualizationTooltip
type="schema"
typeOfSchema="relationship"
name={data.collection}
colorHeader="#0676C1"
numberOfElements={1000}
connectedFrom={data.from}
connectedTo={data.to}
data={
data.attributes.length > 0
? data.attributes.reduce(
(acc, attr) => {
if (attr.name && attr.type) {
acc[attr.name] = attr.type;
}
return acc;
},
{} as Record<string, string>,
)
: {}
}
/>
</div>
</TooltipContent>
</Tooltip>
)}
<RelationPill
draggable
title={data.collection}
......
......@@ -21,7 +21,7 @@ export function Sidebar({ onTab, tab }: { onTab: (tab: SideNavTab) => void; tab:
<TooltipProvider delayDuration={100}>
<div className="w-11 flex flex-col items-center">
{tabs.map((t) => (
<Tooltip key={t.name}>
<Tooltip key={t.name} placement={'right'}>
<TooltipTrigger asChild>
<Button
variantType="secondary"
......@@ -38,7 +38,7 @@ export function Sidebar({ onTab, tab }: { onTab: (tab: SideNavTab) => void; tab:
className={tab === t.name ? 'bg-secondary-100' : ''}
/>
</TooltipTrigger>
<TooltipContent side={'right'}>{t.name}</TooltipContent>
<TooltipContent>{t.name}</TooltipContent>
</Tooltip>
))}
<div className="mt-auto mb-2">
......
import React from 'react';
type NestedItemProps = {
keyName: string;
value: Record<string, any>;
};
export const NestedItem = ({ keyName, value }: NestedItemProps) => {
const numberOfMaxElementsShow = 9;
return (
<div>
<span className="font-bold">{keyName}:</span>
{Object.keys(value).map((keyInside, indexInside) => (
<div key={`tooltipItem_${keyName}_${keyInside}`} className="mx-2">
<span className="font-bold">{keyInside}:</span>
{typeof value[keyInside] === 'object' ? (
<div>
{Object.keys(value[keyInside])
.slice(0, numberOfMaxElementsShow)
.map((keyInside2) => (
<div key={`tooltipItem_${keyName}_${keyInside}_${keyInside2}`} className="mx-2">
<span className="font-bold">{keyInside2}:</span>
<span className="truncate"> {JSON.stringify(value[keyInside][keyInside2])}</span>
</div>
))}
</div>
) : (
<span> {value[keyInside]}</span>
)}
</div>
))}
</div>
);
};
......@@ -19,6 +19,7 @@ import {
import { filterData } from './similarity';
import { Button, Panel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components';
import { Remove, Fullscreen } from '@mui/icons-material';
import { NestedItem } from './NestedItem';
const SIMILARITY_THRESHOLD = 0.7;
......@@ -125,7 +126,7 @@ export function SearchBar(props: { onRemove?: () => void }) {
}}
/>
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Hide</p>
</TooltipContent>
</Tooltip>
......@@ -133,15 +134,15 @@ export function SearchBar(props: { onRemove?: () => void }) {
<TooltipTrigger asChild>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={() => {}} />
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Mock icon</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
}
>
<div className="flex flex-col w-full p-2">
<div className="w-full">
<div className="flex flex-col w-full h-full">
<div className="w-full mb-2">
<input
type="text"
ref={inputRef}
......@@ -150,21 +151,21 @@ export function SearchBar(props: { onRemove?: () => void }) {
id="input-group-search"
className="block w-full p-2 ps-2 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0"
placeholder="Search database"
></input>
/>
</div>
<div>
{recentSearches.length !== 0 && (
<div className="px-3 pb-3">
<p className="text-sm">Recent searches</p>
{recentSearches.slice(0, 3).map((term) => (
<p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}>
{term}
</p>
))}
</div>
)}
{recentSearches.length !== 0 && (
<div className="px-3 pb-3">
<p className="text-sm">Recent searches</p>
{recentSearches.slice(0, 3).map((term, index) => (
<p key={`recent-${term}-${index}`} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}>
{term}
</p>
))}
</div>
)}
<div className="flex-grow overflow-y-auto">
{search !== '' && (
<div className="z-10 w-full overflow-y-auto scroll h-full px-2 pb-2">
<div className="z-10 w-full h-full overflow-y-auto px-2 pb-2">
{SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? (
<div className="ml-1 text-sm">
<p className="text-secondary-500">Found no matches...</p>
......@@ -173,38 +174,63 @@ export function SearchBar(props: { onRemove?: () => void }) {
SEARCH_CATEGORIES.map((category, index) => {
if (results[category].nodes.length > 0 || results[category].edges.length > 0) {
return (
<div key={index}>
<div key={`results_${index}`} className="mt-4">
<div className="flex justify-between p-2 text-lg">
<p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p>
<p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p>
</div>
<div className="h-[1px] w-full bg-secondary-200"></div>
{Object.values(Object.values(results[category]))
.flat()
.map((item, index) => (
<div
key={index}
className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2"
title={JSON.stringify(item)}
onClick={() => {
CATEGORY_ACTIONS[category](
{
nodes: results[category].nodes.includes(item) ? [item] : [],
edges: results[category].edges.includes(item) ? [item] : [],
},
dispatch,
);
}}
>
<div className="font-bold text-sm">
{item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)}
<TooltipProvider delayDuration={300}>
{Object.values(Object.values(results[category]))
.flat()
.map((item, idx) => (
<div
key={`${category}-${item.id}-${idx}`}
className="flex flex-col hover:bg-secondary-200 px-2 py-1 cursor-pointer rounded ml-2"
onClick={() => {
CATEGORY_ACTIONS[category](
{
nodes: results[category].nodes.includes(item) ? [item] : [],
edges: results[category].edges.includes(item) ? [item] : [],
},
dispatch,
);
}}
>
<div className="font-bold text-sm">
{item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)}
</div>
<Tooltip key={`tooltip_${category}-${item.id}-${idx}`} placement={'bottom'}>
<TooltipTrigger asChild>
<div className="font-light text-secondary-800 text-xs truncate">{JSON.stringify(item)}</div>
</TooltipTrigger>
<TooltipContent>
<div className="truncate">
{Object.keys(item).map((key, idx) => (
<div key={`tooltipItem_${idx}`} className="mx-2">
{!(typeof item[key] === 'object' && item[key] !== null) ? (
<div>
<span className="font-bold">{key}:</span>
<span className="truncate">
{' '}
{key === 'similarity' ? Math.round(item[key] * 1000) / 1000 : item[key]}
</span>
</div>
) : (
<NestedItem keyName={key} value={item[key]} />
)}
</div>
))}
</div>
</TooltipContent>
</Tooltip>
</div>
<div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div>
</div>
))}
))}
</TooltipProvider>
</div>
);
} else return <></>;
}
return null;
})
)}
</div>
......
......@@ -71,7 +71,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void }) {
);
})}
</Tabs>
<div className="items-center shrink-0 px-0.5">
<div className="flex items-center shrink-0 px-0.5">
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>
......@@ -96,7 +96,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void }) {
</DropdownItemContainer>
</DropdownContainer>
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Add visualization</p>
</TooltipContent>
</Tooltip>
......@@ -109,7 +109,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void }) {
<TooltipTrigger asChild>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={props.fullSize} />
</TooltipTrigger>
<TooltipContent side={'top'}>
<TooltipContent>
<p>Full screen</p>
</TooltipContent>
</Tooltip>
......
......@@ -10,7 +10,7 @@ export function Recommender() {
return (
<div className="p-4">
<span className="text-md">Select a visualization</span>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-3 gap-4 my-2">
{Object.keys(Visualizations).map((name) => (
<div
key={name}
......@@ -22,8 +22,8 @@ export function Recommender() {
}}
>
<div className="flex items-center justify-between">
<span className="text-sm font-semibold">{name}</span>
<Info tooltip="Here an explanation" side="top" />
<span className="text-sm font-semibold truncate">{name}</span>
<Info tooltip="Here an explanation" placement="top" />
</div>
{/* <image src={image} /> */}
</div>
......
......@@ -11,6 +11,7 @@ export function processLinkPrediction(ml: ML, graph: GraphType): GraphType {
if (allNodeIds.has(link.from) && allNodeIds.has(link.to)) {
const toAdd: LinkType = {
id: link.from + ':LP:' + link.to, // TODO: this only supports one link between two nodes
name: 'Link Prediction',
source: link.from,
target: link.to,
value: link.attributes.jaccard_coefficient as number,
......
......@@ -11,6 +11,7 @@ import {
IPointData,
Sprite,
Assets,
Text,
Texture,
Resource,
} from 'pixi.js';
......@@ -56,19 +57,27 @@ export const NLPixi = (props: Props) => {
antialias: true,
autoDensity: true,
eventMode: 'auto',
resolution: window.devicePixelRatio || 1,
resolution: window.devicePixelRatio || 2,
}),
[],
);
const nodeLayer = useMemo(() => new Container(), []);
const labelLayer = useMemo(() => {
const container = new Container();
container.alpha = 0;
container.renderable = false;
return container;
}, []);
const nodeMap = useRef(new Map<string, Sprite>());
const linkGfx = new Graphics();
const labelMap = useRef(new Map<string, Text>());
const viewport = useRef<Viewport>();
const layoutState = useRef<LayoutState>('reset');
const layoutStoppedCount = useRef(0);
const ref = useRef<HTMLDivElement>(null);
const mouseInCanvas = useRef<boolean>(false);
const [dragging, setDragging] = useState<boolean>(false);
const isSetup = useRef(false);
const ml = useML();
const searchResults = useSearchResultData();
......@@ -89,6 +98,8 @@ export const NLPixi = (props: Props) => {
width: 1000,
height: 1000,
LABEL_MAX_NODES: 1000,
LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER,
NODE_RADIUS: 5,
......@@ -130,6 +141,7 @@ export const NLPixi = (props: Props) => {
if (props.configuration.showPopUpOnHover) return;
(event as any).mouseDownTimeStamp = event.timeStamp;
setDragging(true);
},
onMouseUpNode(event: FederatedPointerEvent) {
......@@ -163,7 +175,7 @@ export const NLPixi = (props: Props) => {
onMouseUpStage(event: FederatedPointerEvent) {
if (props.configuration.showPopUpOnHover) return;
// If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups.
const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp;
if (holdDownTime < mouseClickThreshold) {
......@@ -198,7 +210,7 @@ export const NLPixi = (props: Props) => {
},
onMoved(event: MovedEvent) {
if (props.configuration.showPopUpOnHover) return;
for (const popup of popups) {
if (popup.node.x == null || popup.node.y == null) continue;
popup.pos.x = event.viewport.transform.position.x + popup.node.x * event.viewport.scale.x;
......@@ -206,6 +218,29 @@ export const NLPixi = (props: Props) => {
}
setPopups([...popups]);
},
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 (labelLayer.alpha > 0) {
labelLayer.renderable = true;
const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0.
// 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;
});
} else {
labelLayer.renderable = false;
}
}
},
}));
function resize() {
......@@ -298,6 +333,7 @@ export const NLPixi = (props: Props) => {
const scale = (Math.max(nodeMeta.radius || 5, 5) / 70) * 2;
sprite.scale.set(scale, scale);
sprite.anchor.set(0.5, 0.5);
sprite.cullable = true;
sprite.eventMode = 'static';
sprite.on('mousedown', (e) => imperative.current.onMouseDown(e));
......@@ -314,13 +350,30 @@ export const NLPixi = (props: Props) => {
return sprite;
};
// /** UpdateRadius works just like UpdateColors, but also applies radius*/
// const UpdateRadius = (graph: GraphType, radius: number) => {
// // update for each node in graph
// graph.nodes.forEach((node: NodeType) => {
// createNode(node);
// });
// };
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);
}
const linkMeta = props.graph.links[link._id];
const text = new Text(linkMeta.name, {
fontSize: 60,
fill: config.LINE_COLOR_DEFAULT,
stroke: 0xffffff,
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);
updateLinkLabel(link);
return text;
};
const updateLink = (link: LinkTypeD3) => {
if (!props.graph || nodeMap.current.size === 0) return;
......@@ -354,53 +407,111 @@ export const NLPixi = (props: Props) => {
return;
}
if (linkGfx) {
// let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT;
let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML;
// let color = link.color || 0x000000;
let color = config.LINE_COLOR_DEFAULT;
let style = config.LINE_WIDTH_DEFAULT;
let alpha = linkMeta.alpha || 1;
if (linkMeta.mlEdge) {
color = config.LINE_COLOR_ML;
if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
style = linkMeta.value * 1.8;
} else {
style = 0;
alpha = 0.2;
}
} else if (props.highlightedLinks && props.highlightedLinks.includes(linkMeta)) {
if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
color = dataColors.magenta[50];
// 0xaa00ff;
style = linkMeta.value * 1.8;
} else {
style = 0;
alpha = 0.2;
}
} else if (props.highlightedLinks && props.highlightedLinks.includes(linkMeta)) {
if (linkMeta.mlEdge && ml.communityDetection.jaccard_threshold) {
if (linkMeta.value > ml.communityDetection.jaccard_threshold) {
color = dataColors.magenta[50];
// 0xaa00ff;
style = linkMeta.value * 1.8;
}
} else {
color = dataColors.red[70];
// color = 0xff0000;
style = 1.0;
}
} else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) {
color = dataColors.green[50];
// color = 0x00ff00;
style = 3.0;
} else {
color = dataColors.red[70];
// color = 0xff0000;
style = 1.0;
}
} else if (props.currentShortestPathEdges && props.currentShortestPathEdges.includes(linkMeta)) {
color = dataColors.green[50];
// color = 0x00ff00;
style = 3.0;
}
// Conditional alpha for search results
if (searchResults.nodes.length > 0 || searchResults.edges.length > 0) {
// FIXME: searchResults.edges should be a hashmap to improve performance.
const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link._id);
alpha = isLinkInSearchResults ? 1 : 0.05;
}
// Conditional alpha for search results
if (searchResults.nodes.length > 0 || searchResults.edges.length > 0) {
// FIXME: searchResults.edges should be a hashmap to improve performance.
const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link._id);
alpha = isLinkInSearchResults ? 1 : 0.05;
}
linkGfx
.lineStyle(style, hslStringToHex(color), alpha)
.moveTo(source.x || 0, source.y || 0)
.lineTo(target.x || 0, target.y || 0);
};
linkGfx
.lineStyle(style, hslStringToHex(color), alpha)
.moveTo(source.x || 0, source.y || 0)
.lineTo(target.x || 0, target.y || 0);
const updateLinkLabel = (link: LinkTypeD3) => {
const text = labelMap.current.get(link._id);
if (!text) return;
const _source = link.source;
const _target = link.target;
if (!_source || !_target) {
return;
}
const source = nodeMap.current.get(link.source as string) as Sprite;
const target = nodeMap.current.get(link.target as string) as Sprite;
text.x = (source.x + target.x) / 2;
text.y = (source.y + target.y) / 2;
const length = Math.hypot(target.x - source.x, target.y - source.y);
// Skip rendering labels on very short edges
if (length < text.width + 10) {
// 10 to account for size of node
text.alpha = 0;
return;
} else {
text.alpha = 1;
}
const rads = Math.atan2(target.y - source.y, target.x - source.x);
text.rotation = rads;
const degrees = Math.abs(text.angle % 360);
// Rotate edge labels to always be legible
if (degrees > 90 && degrees < 270) {
text.rotation = rads + Math.PI;
} else {
throw Error('Link not found');
text.rotation = rads;
}
};
// const text = labelMap.current.get(link._id);
// if (!text) return;
// const source = link.source as NodeTypeD3;
// const target = link.target as NodeTypeD3;
// if (source.x == null || source.y == null || target.x == null || target.y == null) return;
// text.x = (source.x + target.x) / 2;
// text.y = (source.y + target.y) / 2;
// const rads = Math.atan2(target.y - source.y, target.x - source.x);
// const degrees = Math.abs(text.angle % 360);
// // Rotate edge labels to always be legible
// if (degrees > 90 && degrees < 270) {
// text.rotation = rads + Math.PI;
// } else {
// text.rotation = rads;
// }
async function loadAssets() {
if (!Assets.cache.has('texture')) {
Assets.addBundle('glyphs', {
......@@ -418,8 +529,10 @@ export const NLPixi = (props: Props) => {
loadAssets();
return () => {
nodeMap.current.clear();
labelMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
};
}, []);
......@@ -494,6 +607,7 @@ export const NLPixi = (props: Props) => {
linkGfx.beginFill();
graph.current.links.forEach((link: any) => {
updateLink(link);
updateLinkLabel(link);
});
linkGfx.endFill();
}
......@@ -507,6 +621,7 @@ export const NLPixi = (props: Props) => {
nodeMap.current.clear();
linkGfx.clear();
nodeLayer.removeChildren();
labelLayer.removeChildren();
}
nodeMap.current.forEach((gfx, id) => {
......@@ -517,6 +632,14 @@ export const NLPixi = (props: Props) => {
}
});
labelMap.current.forEach((text, id) => {
if (!graph.current.links.find((link) => link._id === id)) {
labelLayer.removeChild(text);
text.destroy();
labelMap.current.delete(id);
}
});
linkGfx.clear();
graph.current.nodes.forEach((node) => {
......@@ -531,6 +654,16 @@ 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)) {
updateLinkLabel(link);
} else {
createLinkLabel(link);
}
});
}
// // update text colour (written after nodes so that text appears on top of nodes)
// nodes.forEach((node: NodeType) => {
// if (node.gfxAttributes !== undefined) {
......@@ -559,6 +692,7 @@ export const NLPixi = (props: Props) => {
*/
const setup = async () => {
nodeLayer.removeChildren();
labelLayer.removeChildren();
app.stage.removeChildren();
if (!props.graph) throw Error('Graph is undefined');
......@@ -587,10 +721,17 @@ 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(nodeLayer);
viewport.current.on('moved', (event) => {
imperative.current.onMoved(event);
});
viewport.current.on('drag-end', (event) => {
setDragging(false);
});
viewport.current.on('zoomed', (event) => {
imperative.current.onZoom(event);
});
app.stage.eventMode = 'dynamic';
app.stage.on('mousedown', (e) => imperative.current.onMouseDown(e));
......@@ -629,21 +770,25 @@ export const NLPixi = (props: Props) => {
return (
<>
{popups.map((popup) => (
<Tooltip key={popup.node._id} open={true} boundaryElement={ref} showArrow={true}>
<Tooltip key={popup.node._id} open={true} interactive={!dragging} boundaryElement={ref} showArrow={true}>
<TooltipTrigger x={popup.pos.x} y={popup.pos.y} />
<TooltipContent>
<NLPopup onClose={() => {}} data={{node: props.graph.nodes[popup.node._id], pos: popup.pos}} key={popup.node._id} />
<NLPopup onClose={() => {}} data={{ node: props.graph.nodes[popup.node._id], pos: popup.pos }} key={popup.node._id} />
</TooltipContent>
</Tooltip>
))}
{quickPopup != null &&
{quickPopup != null && (
<Tooltip key={quickPopup.node._id} open={true} boundaryElement={ref} showArrow={true}>
<TooltipTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} />
<TooltipContent>
<NLPopup onClose={() => {}} data={{node: props.graph.nodes[quickPopup.node._id], pos: quickPopup.pos}} key={quickPopup.node._id} />
<NLPopup
onClose={() => {}}
data={{ node: props.graph.nodes[quickPopup.node._id], pos: quickPopup.pos }}
key={quickPopup.node._id}
/>
</TooltipContent>
</Tooltip>
}
)}
<div
className="h-full w-full overflow-hidden"
ref={ref}
......
......@@ -247,6 +247,7 @@ export function parseQueryResult(queryResult: GraphQueryResult, ml: ML, options:
source: uniqueEdges[i].from,
target: uniqueEdges[i].to,
value: uniqueEdges[i].count,
name: uniqueEdges[i].attributes.Type,
mlEdge: false,
color: 0x000000,
};
......
......@@ -9,7 +9,7 @@ import { GraphType, LinkType, NodeType } from '../types';
export function nodeColor(num: number) {
// num = num % 4;
// const col = '#000000';
let entityColors = Object.values(visualizationColors.GPSeq.colors[9]);
//let entityColors = Object.values(visualizationColors.GPSeq.colors[9]);
const col = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length];
return binaryColor(col);
}
......@@ -18,7 +18,7 @@ export function nodeColorHex(num: number) {
// num = num % 4;
// const col = '#000000';
let entityColors = Object.values(visualizationColors.GPSeq.colors[9]);
//let entityColors = Object.values(visualizationColors.GPSeq.colors[9]);
const col = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length];
return col;
}
......
......@@ -57,6 +57,7 @@ export interface LinkType {
// The thickness of a line
id: string;
value: number;
name: string;
// To check if an edge is calculated based on a ML algorithm
mlEdge: boolean;
color: number;
......
import { Meta, Unstyled } from '@storybook/blocks';
<Meta title="Visualizations/Implementation" />
<Meta title="Visualizations/SemanticSubstrates" />
# Variables
......