From 8c32f7c26fa3a87cf1db8299bb1d27e426305330 Mon Sep 17 00:00:00 2001 From: Ammar Memari Date: Fri, 21 Aug 2026 11:24:37 +0200 Subject: [PATCH] Support per-type label-badge styling for vertices, not just edges Edge types have always been able to style their own label badge (labelColor, labelBackgroundOpacity, labelBorderColor/Width/Style) via the per-type Cytoscape rule in useGraphStyles.ts. Vertex types could not: VertexVisualStyle carried none of these fields, the styling-file import schema had no vertex-side labelColor/etc, and the per-type node Cytoscape rule only ever emitted background/border/shape -- node label appearance was hardcoded to a single canvas-wide default with no way to override it per type. Folds LabelVisualStyle into VertexVisualStyle (mirroring how EdgeVisualStyle already does), adds the matching fields to the styling file's vertexEntrySchema, and extends createGraphStyles's per-type node rule to emit the same text-* Cytoscape properties the edge rule already does. appDefaultNodeLabelStyle (used by preview/legend UI) is now derived from appDefaultVertexStyle instead of duplicating it, since the label fields live there now. --- .../src/core/StateProvider/graphStyles.ts | 53 ++++++++++--------- .../src/core/styling/stylingParser.ts | 5 ++ .../GraphViewer/useGraphStyles.test.tsx | 41 ++++++++++++++ .../src/modules/GraphViewer/useGraphStyles.ts | 6 +++ 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts index 8026c7a37..5e6fc1238 100644 --- a/packages/graph-explorer/src/core/StateProvider/graphStyles.ts +++ b/packages/graph-explorer/src/core/StateProvider/graphStyles.ts @@ -58,12 +58,23 @@ export const ARROW_STYLES = [ ] as const; export type ArrowStyle = (typeof ARROW_STYLES)[number]; +/** The visual appearance of a label badge (shared by edge and vertex labels). */ +export type LabelVisualStyle = { + labelColor: string; + labelBackgroundOpacity: number; + labelBorderColor: string; + labelBorderStyle: LineStyle; + labelBorderWidth: number; +}; + /** * The visual appearance of a vertex — the fields that make sense for both a * per-type style and a type-less global default. Every field is required: this - * is the resolved baseline shape a rendered vertex always has. + * is the resolved baseline shape a rendered vertex always has. Includes + * {@link LabelVisualStyle} so a vertex type can style its own label badge, the + * same way an edge type already can (see {@link EdgeVisualStyle}). */ -export type VertexVisualStyle = { +export type VertexVisualStyle = LabelVisualStyle & { /** Color overwrite for vertex */ color: string; /** Icon overwrite for vertex */ @@ -91,15 +102,6 @@ export type VertexTypeStyle = { longDisplayNameAttribute: string; }; -/** The visual appearance of a label badge (shared by edge and vertex labels). */ -export type LabelVisualStyle = { - labelColor: string; - labelBackgroundOpacity: number; - labelBorderColor: string; - labelBorderStyle: LineStyle; - labelBorderWidth: number; -}; - /** The visual appearance of an edge, shared by per-type styles and defaults. */ export type EdgeVisualStyle = LabelVisualStyle & { lineColor: string; @@ -135,7 +137,13 @@ export type EdgeStyle = Simplify< Readonly >; -/** The default values to use when no user provided value is given. */ +/** + * The default values to use when no user provided value is given. Label-badge + * fields mirror the node label config in + * `components/Graph/styles/defaultNodeStyle.ts` (`text.background` → + * `labelColor`, `text.opacity` → `labelBackgroundOpacity`) — keep in sync if + * that canvas style changes. + */ export const appDefaultVertexStyle = { displayNameAttribute: RESERVED_ID_PROPERTY, longDisplayNameAttribute: RESERVED_TYPES_PROPERTY, @@ -147,23 +155,20 @@ export const appDefaultVertexStyle = { borderWidth: 0, borderColor: "#128EE5", borderStyle: "solid", -} as const satisfies Omit; - -/** - * The default appearance of a node's label badge. Nodes have no per-type label - * styling (see {@link VertexVisualStyle}), so this is the single source for how - * a node label looks. Values mirror the node label config in - * `components/Graph/styles/defaultNodeStyle.ts` (`text.background` → - * `labelColor`, `text.opacity` → `labelBackgroundOpacity`) — keep in sync if - * that canvas style changes. - */ -export const appDefaultNodeLabelStyle = { labelColor: "#1d2531", labelBackgroundOpacity: 0.7, labelBorderColor: "#1d2531", labelBorderStyle: "solid", labelBorderWidth: 0, -} as const satisfies LabelVisualStyle; +} as const satisfies Omit; + +/** + * The default appearance of a node's label badge, for preview/legend UI that + * needs just the label fields (e.g. `VertexPreview.tsx`) — derived from + * {@link appDefaultVertexStyle}, which is now the single source of truth for + * a vertex type's label styling (see {@link VertexVisualStyle}). + */ +export const appDefaultNodeLabelStyle: LabelVisualStyle = appDefaultVertexStyle; /** The default values to use when no user provided value is given. */ export const appDefaultEdgeStyle = { diff --git a/packages/graph-explorer/src/core/styling/stylingParser.ts b/packages/graph-explorer/src/core/styling/stylingParser.ts index b38e2348d..244088fdf 100644 --- a/packages/graph-explorer/src/core/styling/stylingParser.ts +++ b/packages/graph-explorer/src/core/styling/stylingParser.ts @@ -138,6 +138,11 @@ const vertexEntrySchema = z borderWidth: z.number().optional(), borderColor: z.string().optional(), borderStyle: z.enum(LINE_STYLES).optional(), + labelColor: z.string().optional(), + labelBackgroundOpacity: z.number().optional(), + labelBorderColor: z.string().optional(), + labelBorderStyle: z.enum(LINE_STYLES).optional(), + labelBorderWidth: z.number().optional(), }) .transform( ({ icon, ...rest }): Omit => diff --git a/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.test.tsx b/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.test.tsx index 33da0ec8a..871f9adc5 100644 --- a/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.test.tsx +++ b/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.test.tsx @@ -66,6 +66,47 @@ describe("useGraphStyles", () => { shape: "ellipse", width: 24, height: 24, + color: "#FFFFFF", + "text-background-color": "#1d2531", + "text-background-opacity": 0.7, + "text-border-color": "#1d2531", + "text-border-style": "solid", + "text-border-width": 0, + }); + }); + }); + + it("should apply a vertex type's own label-badge style, not just the app default", async () => { + const vertexConfig = { + ...createRandomVertexTypeConfig(), + ...RASTER_ICON, + type: createVertexType("Person"), + color: "#128EE5", + backgroundOpacity: 0.8, + borderColor: "#000000", + borderWidth: 2, + borderStyle: "solid" as const, + shape: "ellipse" as const, + labelColor: "#f8fafc", + labelBackgroundOpacity: 0.94, + labelBorderColor: "#cbd5e1", + labelBorderWidth: 1, + labelBorderStyle: "solid" as const, + }; + dbState.activeSchema.vertices = [vertexConfig]; + dbState.addVertexStyle(vertexConfig.type, vertexConfig); + + const { result } = renderHookWithState(() => useGraphStyles(), dbState); + + await waitFor(() => { + const vertexStyle = getStyles(result)[`node[type="Person"]`] as any; + expect(vertexStyle).toMatchObject({ + color: "#000000", // Black text for a light background + "text-background-color": "#f8fafc", + "text-background-opacity": 0.94, + "text-border-color": "#cbd5e1", + "text-border-width": 1, + "text-border-style": "solid", }); }); }); diff --git a/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.ts b/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.ts index 869ded042..f6b1316b9 100644 --- a/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.ts +++ b/packages/graph-explorer/src/modules/GraphViewer/useGraphStyles.ts @@ -58,6 +58,12 @@ function createGraphStyles( shape: vtConfig.shape, width: 24, height: 24, + color: new Color(vtConfig.labelColor).isDark() ? "#FFFFFF" : "#000000", + "text-background-opacity": vtConfig.labelBackgroundOpacity, + "text-background-color": vtConfig.labelColor, + "text-border-width": vtConfig.labelBorderWidth, + "text-border-color": vtConfig.labelBorderColor, + "text-border-style": vtConfig.labelBorderStyle, }; }