From a61f3cbbaf13831baf5dc0a796bf1fa220d83eae Mon Sep 17 00:00:00 2001 From: Milho001 <l.milhomemfrancochristino@uu.nl> Date: Tue, 18 Jun 2024 16:05:42 +0000 Subject: [PATCH] feat(nl): nodelink performance to pixi rendering and layouting does optimizations for large graphs while keeping the normal process for smaller graphs also reinstates nl shapes in settings --- apps/web/public/assets/sprite.png | Bin 0 -> 687 bytes apps/web/public/assets/sprite_selected.png | Bin 0 -> 646 bytes .../public/assets/sprite_selected_square.png | Bin 0 -> 100 bytes apps/web/public/assets/sprite_square.png | Bin 0 -> 344 bytes .../shared/lib/components/dropdowns/index.tsx | 2 +- libs/shared/lib/components/inputs/index.tsx | 26 ++- .../lib/graph-layout/graphology-layouts.ts | 27 ++- .../nodelinkvis/components/NLPixi.tsx | 184 +++++++++--------- .../nodelinkvis/nodelinkvis.tsx | 2 +- 9 files changed, 133 insertions(+), 108 deletions(-) create mode 100644 apps/web/public/assets/sprite.png create mode 100644 apps/web/public/assets/sprite_selected.png create mode 100644 apps/web/public/assets/sprite_selected_square.png create mode 100644 apps/web/public/assets/sprite_square.png diff --git a/apps/web/public/assets/sprite.png b/apps/web/public/assets/sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..a9d77b0520b6b4945bbe38e04553e4e349f1794e GIT binary patch literal 687 zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT3?e@WZ39vj0X`wFK-w=N%P%|&L?WS}=&YdV zY>-GKL>$P@_6yJUjm+^5&+zoj{r~?zP&PO=f6?-dKY#xE@#E+2{l^lss(%0e{ps_U zH*ep+eD&tWt$WvQ+&y>k+M%OoCQe_}HDOL=V_#uqM@B)znOKo*pxweHL4LsuZ!i5= zvogU$ivRwNLsQx!f_2!h<z3j$z`%If)5S3);_#fQoYR^N1YDdsG*)PoDJWQX-2315 zIn64ONy+;k<FC@trB*487kDMU>fYW~bBf7a<@d*!<Bx=Oj(<HN$Jci@edAo_o2RCJ zVtrA1TTOl6q0HLF%8#WJ+&7)@{cif|@fN+$I;T%t_5SuK|GmxNN!`V?5??0eNy{#) z#lDMowktkr?VHf1Qd@lV=I^A_0v8syuHVwHxasj5k@%CHtU3xws%PiVd>pvdi&Z-$ zOYv3hj@31lq8q$E2L1D?4U@EG&2g{^jhp=a&_oF{q2ecIo32mVeIV_L-LyR8$@@5N z^i23?u(sD|F;iK<i9P?SyXN+3-Eb96yx;!YXk+>Ev#<UIOi@WtXG*;|(OCD*va5Fs zH(X9&_;&ZFws72g5gw0^=l+MCJ~~6lK;htqJC4EZZY9lHZycH3Om1*EIr&cBxGEs( za<k%I6LpFG37e<O2rN6AD`Pp?cg4X8QQsK<Hw*D-e(ax6`8>fxPx!c2N`gmQ>5t5# zei;l6jS_7i({qK?3^L>OGo#dUwPXu-efaX{Qo2XHG3(LJwIYi&FI{f1e5}WNznO{C zhfhs4!<u7e!?y=t8~-kmb8qRJChKW)s)}XTi$9v;URMLVrpGY<4blotVK_ET^}&|e S-<5%}&*16m=d#Wzp$P!Bu{<>Z literal 0 HcmV?d00001 diff --git a/apps/web/public/assets/sprite_selected.png b/apps/web/public/assets/sprite_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..bd57dcc236de77693873365fdf8d007f2349d2ba GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT3?e@WZ39v{0X`wFK-w=N3xUF+?4anZpy+JB zh)l2)kdf^dp6wf%;~k#i>6!cg|9_zPpFe+IynJ){>aF!#_7qljq!%=N`}Y0Q=P$3{ zyt{Du#*yP^x9&PPb@sBHlIG}?(z@13^3zZD0__tj3GxeOSpD|Sk4p(2Qv4SVO=*h= z)?s&Nb9l$Vz_{Jh#W5t}@RTd3r!^}Gumm_3F6!)j$tHSV?f?J&;xt8$$Ibh+&5O32 zGO`L>#(Fw=o{G)6$cfVgW2YJQ$(_5Tv*<@6=R2<({vxk5KeSY|*?RVfKYFdc%aG?l z-o=&T#})pF%2wMO+<06Y@ky5R-AzXwCfllcY*#&Ru1uU|T6HA%yU6;5%ibNlc)j=f zjdPYW3;0(ixIdLs*;N11YUc|<zMfkrkDtH!a>cX)k86JM_r5n>Tiz6Wm3{4WVSV@H zzye3zj1ReIczW}>r{B_-%GvB@GEIzW3%69+@{MO*9R*T9cb@&v+RMchnz81;eB$i3 zgl**y)fcO>Oun`3@l_M1gD<8{RZVkuNcuMQkFWOE&r6rj$-BRjaZ%duusiEJ<HY(G z35DAVeBGgyYa(@G3-9s>{d9%d*Q>c_e_%<OeAXgfpIt%vT6OX6Jl2#`Ywmoz%NUt1 zZ5}+qLG7$eUGzp_DTV}vw!}Y?vs*ZQZe91DHO+Ijm-`$&8{7R~Brjd}aV$*IZq4ZW zVv=xfhxg&{3mljxH+yp4;&W14U|Ug}V88OX;KIeJ?n3>W<Qny?>QA)>l}>r2dfj3F c6faT9^1EVNHYho!03(>e)78&qol`;+0MfrA!vFvP literal 0 HcmV?d00001 diff --git a/apps/web/public/assets/sprite_selected_square.png b/apps/web/public/assets/sprite_selected_square.png new file mode 100644 index 0000000000000000000000000000000000000000..eb7a57e4f70d60c5096d07f0cd53803454285099 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^wjj*N3?x<7d|m~l*aCb)T>t<7?-!Bv;jqp+popZW wi(`n!`Q#t`jP@F71`Q0qHIf4iR|xN9C|rJH`@fU+{6Iwvp00i_>zopr01F8n;Q#;t literal 0 HcmV?d00001 diff --git a/apps/web/public/assets/sprite_square.png b/apps/web/public/assets/sprite_square.png new file mode 100644 index 0000000000000000000000000000000000000000..38a4cd208703a2e481ca4337430f631026c6bed0 GIT binary patch literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^wjj*G1SBUieA@@47>k44ofy`glX(f`xTHpSruq6Z zXaU(A42<oW3@jieKr98s3=GT*7#Wy>G$TlC0TW!-b^$Yj4N_<*$-N#(9rAQ>49Q@9 zdut=_AqNqbz@8QSL3fzewM%up6qwjMm;aIX<BYc{;qUdn6z#S5W1j!{z}fpJ-~M}h z(D&;;nHl%iKYw_7{+@f+dcBubZJ)jT#q&M2$!1S{dF`LSmENqFpY_8s|Nac#%HYH2 zD}Nu$IWb$R?%ZwjH0OI)J`~^EXDIz?S=;>2-+FIOFgN`(XM6scwmmB!;x}T(tG!?T mEA$7R4}R~t`|IkB+CLZ^8CYlcMn~QU`O4GP&t;ucLK6V{;DuBG literal 0 HcmV?d00001 diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index 9fea3c4c9..be780115d 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -109,7 +109,7 @@ export function DropdownItem({ value, disabled, className, onClick, submenu, sel onMouseEnter={() => setIsSubmenuOpen(true)} onMouseLeave={() => setIsSubmenuOpen(false)} > - {value} + { value } {submenu && isSubmenuOpen && <DropdownSubmenuContainer ref={submenuRef}>{submenu}</DropdownSubmenuContainer>} </li> ); diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index c3a3e9838..df18203eb 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -96,7 +96,7 @@ type DropdownProps = { disabled?: boolean; className?: string; value?: number | string; - options: number[] | string[]; + options: number[] | string[] | {[key: string]: string}[]; onChange?: (value: number | string) => void; }; @@ -371,6 +371,23 @@ export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) ); }; +function parsedValue(item: number | string | {[key: string]: string}, key: boolean = false) { + switch (typeof item) { + case 'number': return item.toString(); + case 'object': return key ? Object.keys(item)[0] : Object.values(item)[0]; + } + + return item; +} + +function currentValue(value: string | number | undefined, options?: {[key: string]: string}[]) { + if (options != null && typeof options[0] == 'object') { + return parsedValue(options.find(x => x[value as string]) as {[key: string]: string}); + } + + return value; +} + export const DropdownInput = ({ label, value, @@ -411,7 +428,7 @@ export const DropdownInput = ({ > <DropdownTrigger variant={buttonVariant} - title={overrideRender || value} + title={overrideRender || currentValue(value, options as {[key: string]: string}[]) } size={size} className="cursor-pointer" disabled={disabled} @@ -427,12 +444,11 @@ export const DropdownInput = ({ options.map((item: any, index: number) => ( <DropdownItem key={index} - value={item.toString()} + value={parsedValue(item)} selected={value === item} onClick={() => { - const parsedValue = typeof item === 'number' ? item.toString() : item; if (onChange) { - onChange(parsedValue); + onChange(parsedValue(item, true)); } setIsDropdownOpen(false); }} diff --git a/libs/shared/lib/graph-layout/graphology-layouts.ts b/libs/shared/lib/graph-layout/graphology-layouts.ts index dd82b7093..67419cbb7 100644 --- a/libs/shared/lib/graph-layout/graphology-layouts.ts +++ b/libs/shared/lib/graph-layout/graphology-layouts.ts @@ -198,18 +198,27 @@ export class GraphologyForceAtlas2Webworker extends GraphologyLayout { const sensibleSettings = forceAtlas2.inferSettings(graph); - const layout = new FA2Layout(graph, { - settings: { - ...this.defaultLayoutSettings, - ...sensibleSettings, - adjustSizes: graph.order < 300 ? true : false, - }, - }); + let settings = { + ...this.defaultLayoutSettings, + ...sensibleSettings, + adjustSizes: graph.order < 300 ? true : false + }; + + if (graph.order > 5000) { + settings = { + ...settings, + barnesHutOptimize: true, + barnesHutTheta: 0.75, + slowDown: 0.75 + }; + } + + const layout = new FA2Layout(graph, { settings }); layout.start(); - // stop the layout after 5 seconds + // stop the layout after 10 seconds setTimeout(() => { layout.stop(); - }, 20000); + }, 10000); } } diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 0b14261d0..2f41de1a8 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -1,7 +1,7 @@ import { GraphType, LinkType, NodeType } from '../types'; import { dataColors, visualizationColors } from 'config'; import { ReactEventHandler, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import { Application, Circle, Color, Container, FederatedPointerEvent, Graphics, IPointData } from 'pixi.js'; +import { Application, AssetsBundle, Color, Container, FederatedPointerEvent, Graphics, IPointData, Sprite, Assets, Texture, Resource } from 'pixi.js'; import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access'; import { NLPopup } from './NLPopup'; import { hslStringToHex, nodeColor } from './utils'; @@ -30,6 +30,16 @@ type LayoutState = 'reset' | 'running' | 'paused'; // MAIN COMPONENT ////////////////// +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'); +} + export const NLPixi = (props: Props) => { const [quickPopup, setQuickPopup] = useState<{ node: NodeType; pos: IPointData } | undefined>(); const [popups, setPopups] = useState<{ node: NodeType; pos: IPointData }[]>([]); @@ -46,10 +56,9 @@ export const NLPixi = (props: Props) => { [], ); const nodeLayer = useMemo(() => new Container(), []); - const linkLayer = useMemo(() => new Container(), []); - const nodeMap = useRef(new Map<string, Graphics>()); - const linkMap = useRef(new Map<string, Graphics>()); + const nodeMap = useRef(new Map<string, Sprite>()); + const linkGfx = new Graphics(); const viewport = useRef<Viewport>(); const layoutState = useRef<LayoutState>('reset'); const layoutStoppedCount = useRef(0); @@ -57,7 +66,7 @@ export const NLPixi = (props: Props) => { const mouseInCanvas = useRef<boolean>(false); const isSetup = useRef(false); const ml = useML(); - const dragging = useRef<{ node: NodeType; gfx: Graphics } | null>(null); + const dragging = useRef<{ node: NodeType; gfx: Sprite } | null>(null); const onlyClicked = useRef(false); const searchResults = useSearchResultData(); @@ -66,6 +75,12 @@ export const NLPixi = (props: Props) => { // const cull = new Cull(); // let cullDirty = useRef(true); + const textureId = (selected: boolean = false) => { + const selectionSuffix = selected ? '_selected' : ''; + const shapeSuffix = (props.configuration.shapes.shape == 'rectangle') ? '_square' : ''; + return `texture${selectionSuffix}${shapeSuffix}`; + }; + const [config, setConfig] = useState({ width: 1000, height: 1000, @@ -73,10 +88,10 @@ export const NLPixi = (props: Props) => { LAYOUT_ALGORITHM: Layouts.FORCEATLAS2WEBWORKER, NODE_RADIUS: 5, - NODE_BORDER_LINE_WIDTH: 1.0, - NODE_BORDER_LINE_WIDTH_SELECTED: 5.0, // if selected and normal width are different the thicker line will be still in the gfx - NODE_BORDER_COLOR_DEFAULT: dataColors.neutral[70], - NODE_BORDER_COLOR_SELECTED: dataColors.orange[60], + // NODE_BORDER_LINE_WIDTH: 1.0, + // NODE_BORDER_LINE_WIDTH_SELECTED: 5.0, // if selected and normal width are different the thicker line will be still in the gfx + // NODE_BORDER_COLOR_DEFAULT: dataColors.neutral[70], + // NODE_BORDER_COLOR_SELECTED: dataColors.orange[60], LINE_COLOR_DEFAULT: dataColors.neutral[40], LINE_COLOR_SELECTED: visualizationColors.GPSelected.colors[1], @@ -93,10 +108,19 @@ export const NLPixi = (props: Props) => { }); }, [props.layoutAlgorithm, props.configuration]); + useEffect(() => { + if (nodeMap.current.size === 0) return; + + const texture = Assets.get(textureId()); + for (const sprite of nodeMap.current.values()) { + sprite.texture = texture; + } + }, [props.configuration.shapes?.shape]); + const imperative = useRef<any>(null); useImperativeHandle(imperative, () => ({ - onDragStart(node: NodeType, gfx: Graphics) { + onDragStart(node: NodeType, gfx: Sprite) { dragging.current = { node, gfx }; onlyClicked.current = true; @@ -208,7 +232,7 @@ export const NLPixi = (props: Props) => { } else return { x: 0, y: 0 }; } - function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Graphics) { + function onDragStart(event: FederatedPointerEvent, node: NodeType, gfx: Sprite) { event.stopPropagation(); if (imperative.current) imperative.current.onDragStart(node, gfx); } @@ -227,22 +251,16 @@ export const NLPixi = (props: Props) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; - const lineColor = node.isShortestPathSource - ? dataColors.orange['95'] - : node.isShortestPathTarget - ? dataColors.blue['95'] - : node.selected - ? config.NODE_BORDER_COLOR_SELECTED - : config.NODE_BORDER_COLOR_DEFAULT; - const lineWidth = node.selected ? config.NODE_BORDER_LINE_WIDTH_SELECTED : config.NODE_BORDER_LINE_WIDTH; - gfx.lineStyle(lineWidth, new Color(hslStringToHex(lineColor))); + // Update texture when selected + const texture = Assets.get(textureId(node.selected)); + gfx.texture = texture; + // Cluster colors if (node?.cluster) { - gfx.beginFill(node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000); - } else gfx.beginFill(nodeColor(node.type)); - - gfx.drawCircle(0, 0, Math.max(node.radius || 5, 5)); - gfx.endFill(); + gfx.tint = node.cluster >= 0 ? nodeColor(node.cluster) : 0x000000; + } else { + gfx.tint = nodeColor(node.type); + } gfx.position.set(node.x, node.y); @@ -278,12 +296,21 @@ export const NLPixi = (props: Props) => { const createNode = (node: NodeType, selected?: boolean) => { // check if node is already drawn, and if so, delete it - if (node && node?._id && nodeMap.current?.has(node._id)) { + if (node && node?._id && nodeMap.current.has(node._id)) { nodeMap.current.delete(node._id); } // Do not draw node if it has no position if (node.x === undefined || node.y === undefined) return; - const gfx = new Graphics(); + + let gfx: Sprite; + const texture = Assets.get(textureId()); + gfx = new Sprite(texture); + + gfx.tint = nodeColor(node.type); + const scale = (Math.max(node.radius || 5, 5) / 70) * 2; + gfx.scale.set(scale, scale); + gfx.anchor.set(0.5, 0.5); + nodeMap.current.set(node._id, gfx); nodeLayer.addChild(gfx); node.selected = selected; @@ -303,8 +330,8 @@ export const NLPixi = (props: Props) => { // }); // }; - const updateLink = (link: LinkType, gfx: Graphics) => { - if (!props.graph) return; + const updateLink = (link: LinkType) => { + if (!props.graph || nodeMap.current.size === 0) return; const _source = link.source; const _target = link.target; @@ -335,7 +362,7 @@ export const NLPixi = (props: Props) => { return; } - if (gfx) { + if (linkGfx) { // let color = link.color || 0x000000; let color = config.LINE_COLOR_DEFAULT; let style = config.LINE_WIDTH_DEFAULT; @@ -366,36 +393,27 @@ export const NLPixi = (props: Props) => { style = 3.0; } - gfx.clear(); - gfx.beginFill(); - gfx + // 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); - gfx.endFill(); } else { throw Error('Link not found'); } }; - const createLink = (link: LinkType) => { - if (linkMap.current.has(link.id)) { - linkMap.current.delete(link.id); - } - const gfx = new Graphics(); - gfx.name = 'link_' + link.id; - linkMap.current.set(link.id, gfx); - updateLink(link, gfx); - linkLayer.addChild(gfx); - return gfx; - }; - useEffect(() => { return () => { nodeMap.current.clear(); - linkMap.current.clear(); + linkGfx.clear(); nodeLayer.removeChildren(); - linkLayer.removeChildren(); }; }, []); @@ -412,15 +430,9 @@ export const NLPixi = (props: Props) => { const gfx = nodeMap.current.get(node._id); if (!gfx) return; const isNodeInSearchResults = searchResults.nodes.some((resultNode) => resultNode.id === node._id); + gfx.alpha = isNodeInSearchResults || searchResults.nodes.length === 0 ? 1 : 0.05; }); - - props.graph.links.forEach((link: LinkType) => { - const gfx = linkMap.current.get(link.id); - if (!gfx) return; - const isLinkInSearchResults = searchResults.edges.some((resultEdge) => resultEdge.id === link.id); - gfx.alpha = isLinkInSearchResults || searchResults.edges.length === 0 ? 1 : 0.05; - }); } }, [searchResults]); @@ -432,6 +444,9 @@ export const NLPixi = (props: Props) => { if (!layoutAlgorithm.current) return; let stopped = 0; + + const widthHalf = app.renderer.width / 2; + const heightHalf = app.renderer.height / 2; props.graph.nodes.forEach((node: NodeType, i) => { if (!layoutAlgorithm.current) return; const gfx = nodeMap.current.get(node._id); @@ -441,7 +456,7 @@ export const NLPixi = (props: Props) => { if ( !position || - Math.abs(node.x - position.x - app.renderer.width / 2) + Math.abs(node.y - position.y - app.renderer.height / 2) < 1 + Math.abs(node.x - position.x - widthHalf) + Math.abs(node.y - position.y - heightHalf) < 1 ) { stopped += 1; return; @@ -449,8 +464,8 @@ export const NLPixi = (props: Props) => { try { if (layoutAlgorithm.current.provider === 'Graphology') { // this is a dirty hack to fix the graphology layout being out of bounds - node.x = position.x + app.renderer.width / 2; - node.y = position.y + app.renderer.height / 2; + node.x = position.x + widthHalf; + node.y = position.y + heightHalf; } else { node.x = position.x; node.y = position.y; @@ -460,14 +475,7 @@ export const NLPixi = (props: Props) => { layoutState.current = 'paused'; } - if (layoutState.current === 'running') { - gfx.position.copyFrom({ - x: node.fx || gfx.position.x + ((node.x || 0) - gfx.position.x) * 0.1 * delta, - y: node.fy || gfx.position.y + ((node.y || 0) - gfx.position.y) * 0.1 * delta, - }); - } else { - gfx.position.copyFrom(node as IPointData); - } + gfx.position.copyFrom(node as IPointData); }); if (stopped === props.graph.nodes.length) { @@ -483,12 +491,13 @@ export const NLPixi = (props: Props) => { layoutState.current = 'running'; } - // Update forces of the links + // Draw the links + linkGfx.clear(); + linkGfx.beginFill(); props.graph.links.forEach((link: any) => { - if (linkMap.current && !!linkMap.current.has(link.id)) { - updateLink(link, linkMap.current.get(link.id) as Graphics); - } + updateLink(link); }); + linkGfx.endFill(); } }; @@ -499,9 +508,8 @@ export const NLPixi = (props: Props) => { if (props.graph) { if (forceClear) { nodeMap.current.clear(); - linkMap.current.clear(); + linkGfx.clear(); nodeLayer.removeChildren(); - linkLayer.removeChildren(); } nodeMap.current.forEach((gfx, id) => { @@ -512,30 +520,23 @@ export const NLPixi = (props: Props) => { } }); - linkMap.current.forEach((gfx, id) => { - if (!props.graph?.links?.find((link) => link.id === id)) { - linkLayer.removeChild(gfx); - gfx.destroy(); - linkMap.current.delete(id); - } - }); + linkGfx.clear(); props.graph.nodes.forEach((node: NodeType) => { if (!forceClear && nodeMap.current.has(node._id)) { const old = nodeMap.current.get(node._id); - node.x = old?.x || node.x; - node.y = old?.y || node.y; - updateNode(node); + + try { + node.x = old?.x || node.x; + node.y = old?.y || node.y; + updateNode(node); + } catch (e) { + // node.x and .y become read-only when some layout algorithms are finished + } } else { createNode(node); } }); - props.graph.links.forEach((link: LinkType) => { - if (!forceClear && linkMap.current.has(link.id)) { - const gfx = linkMap.current.get(link.id); - if (gfx) updateLink(link, gfx); - } else createLink(link); - }); // // update text colour (written after nodes so that text appears on top of nodes) // nodes.forEach((node: NodeType) => { @@ -563,9 +564,8 @@ export const NLPixi = (props: Props) => { * It creates graphic objects and adds these to the PIXI containers. It also clears both of these of previous nodes and links. * @param graph The graph returned from the database and that is parsed into a nodelist and edgelist. */ - const setup = () => { + const setup = async () => { nodeLayer.removeChildren(); - linkLayer.removeChildren(); app.stage.removeChildren(); if (!props.graph) throw Error('Graph is undefined'); @@ -583,7 +583,7 @@ export const NLPixi = (props: Props) => { // activate plugins viewport.current.drag().pinch().wheel({ smooth: 2 }).animate({}).decelerate({ friction: 0.75 }); - viewport.current.addChild(linkLayer); + viewport.current.addChild(linkGfx); viewport.current.addChild(nodeLayer); viewport.current.on('drag-start', (event) => { imperative.current.onPan(); @@ -595,7 +595,7 @@ export const NLPixi = (props: Props) => { app.stage.on('mouseup', onDragEnd); nodeMap.current.clear(); - linkMap.current.clear(); + linkGfx.clear(); update(true); isSetup.current = true; }; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index f5c06cf4f..70c7867ba 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -177,7 +177,7 @@ const NodelinkSettings = ({ settings, graphMetadata, updateSettings }: Visualiza type="dropdown" label="Shape" value={settings.shapes.shape} - options={['Circle', 'Square']} + options={[{circle: 'Circle'}, {rectangle: 'Square'}]} onChange={(val) => updateSettings({ shapes: { ...settings.shapes, shape: val as any } })} /> ) : ( -- GitLab