Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions packages/graph-explorer/src/modules/GraphViewer/GraphViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Activity,
type ComponentPropsWithRef,
type MouseEvent,
useMemo,
useState,
} from "react";

Expand Down Expand Up @@ -38,9 +39,12 @@ import {
createRenderedVertexId,
getEdgeIdFromRenderedEdgeId,
getVertexIdFromRenderedVertexId,
type DisplayVertex,
type DisplayVertexTypeConfig,
type RenderedEdgeId,
type RenderedVertex,
type RenderedVertexId,
useDisplayVerticesInCanvas,
useDisplayVertexTypeConfigs,
useRenderedEdges,
useRenderedVertices,
Expand All @@ -52,6 +56,7 @@ import { useDefaultNeighborExpansionLimit } from "@/hooks/useExpandNode";
import { cn, isVisible } from "@/utils";

import { ExportGraphButton } from "./ExportGraphButton";
import { filterVertexTypeConfigsForCanvasVertices } from "./filterLegendVertexTypeConfigs";
import { GraphViewerEmptyState } from "./GraphViewerEmptyState";
import { ImportGraphButton } from "./ImportGraphButton";
import ContextMenu from "./internalComponents/ContextMenu";
Expand Down Expand Up @@ -150,6 +155,22 @@ function GraphViewerContent({

const nodes = useRenderedVertices();
const edges = useRenderedEdges();
const allVertexTypeConfigs = useDisplayVertexTypeConfigs().values().toArray();
const displayVerticesInCanvas = useDisplayVerticesInCanvas();

const legendVertexTypeConfigs = useMemo(() => {
const visibleVertices: DisplayVertex[] = [];
for (const node of nodes) {
const vertex = displayVerticesInCanvas.get(node.data.vertexId);
if (vertex != null) {
visibleVertices.push(vertex);
}
}
return filterVertexTypeConfigsForCanvasVertices(
allVertexTypeConfigs,
visibleVertices,
);
}, [allVertexTypeConfigs, displayVerticesInCanvas, nodes]);

const isEmpty = !nodes.length && !edges.length;

Expand Down Expand Up @@ -224,7 +245,10 @@ function GraphViewerContent({
</Activity>
<Activity mode={isVisible(legendOpen)}>
<div className="z-20 col-start-1 row-start-1 grid min-h-0 justify-self-end p-3">
<Legend onClose={() => setLegendOpen(false)} />
<Legend
vertexTypeConfigs={legendVertexTypeConfigs}
onClose={() => setLegendOpen(false)}
/>
</div>
</Activity>
</PanelContent>
Expand All @@ -235,11 +259,13 @@ function GraphViewerContent({

function Legend({
onClose,
vertexTypeConfigs,
className,
...props
}: { onClose: () => void } & ComponentPropsWithRef<typeof Panel>) {
const vtConfigs = useDisplayVertexTypeConfigs().values().toArray();

}: {
onClose: () => void;
vertexTypeConfigs: DisplayVertexTypeConfig[];
} & ComponentPropsWithRef<typeof Panel>) {
return (
<Panel className={cn("max-w-md shadow-md", className)} {...props}>
<PanelHeader>
Expand All @@ -250,7 +276,7 @@ function Legend({
</PanelHeader>
<PanelContent className="p-3">
<ul className="space-y-3">
{vtConfigs.map(vtConfig => (
{vertexTypeConfigs.map(vtConfig => (
<li
key={vtConfig.type}
className="gx-wrap-break-word flex items-center gap-3 text-base font-medium"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";

import type { DisplayVertex, DisplayVertexTypeConfig } from "@/core";

import { createVertexType } from "@/core/entities";

import { filterVertexTypeConfigsForCanvasVertices } from "./filterLegendVertexTypeConfigs";

function configForType(type: string): DisplayVertexTypeConfig {
const vt = createVertexType(type);
return {
type: vt,
displayLabel: type,
attributes: [],
};
}

describe("filterVertexTypeConfigsForCanvasVertices", () => {
it("keeps only configs whose type appears on a canvas vertex", () => {
const allConfigs: DisplayVertexTypeConfig[] = [
configForType("airport"),
configForType("country"),
];
const canvasVertices = [
{ types: [createVertexType("airport")] },
] as DisplayVertex[];

const result = filterVertexTypeConfigsForCanvasVertices(
allConfigs,
canvasVertices,
);

expect(result).toHaveLength(1);
expect(result[0]?.type).toBe(allConfigs[0]?.type);
});

it("includes types from every label on a multi-label vertex", () => {
const allConfigs: DisplayVertexTypeConfig[] = [
configForType("person"),
configForType("worker"),
configForType("company"),
];
const canvasVertices = [
{
types: [createVertexType("person"), createVertexType("worker")],
},
] as DisplayVertex[];

const result = filterVertexTypeConfigsForCanvasVertices(
allConfigs,
canvasVertices,
);

expect(result.map(c => c.type)).toEqual([
allConfigs[0]?.type,
allConfigs[1]?.type,
]);
});

it("returns an empty list when the canvas has no vertices", () => {
const allConfigs: DisplayVertexTypeConfig[] = [configForType("airport")];

const result = filterVertexTypeConfigsForCanvasVertices(allConfigs, []);

expect(result).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type {
DisplayVertex,
DisplayVertexTypeConfig,
VertexType,
} from "@/core";

/**
* Keeps legend rows only for vertex types that appear on at least one canvas vertex
* (including every label on multi-label vertices).
*/
export function filterVertexTypeConfigsForCanvasVertices(
allConfigs: DisplayVertexTypeConfig[],
canvasVertices: DisplayVertex[],
): DisplayVertexTypeConfig[] {
const visibleTypes = new Set<VertexType>();
for (const vertex of canvasVertices) {
for (const type of vertex.types) {
visibleTypes.add(type);
}
}
return allConfigs.filter(config => visibleTypes.has(config.type));
}