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
2 changes: 2 additions & 0 deletions docs/features/graph-view.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ On the **Nodes** tab, each node type can be customized in a variety of ways.
- **Display description attribute** allows you to choose the attribute on the node that is used to describe the node in search
- **Icon** can be picked from the built-in Lucide library via the **Browse** button, or uploaded as a custom SVG/raster image.
- **Colors and borders** can be customized to visually distinguish from other node types
- **Label font size** and **minimum zoomed font size** control how large the node's label renders and how small it can shrink before it hides while zooming out

On the **Edges** tab, each edge type can be customized in a variety of ways.

Expand All @@ -91,6 +92,7 @@ On the **Edges** tab, each edge type can be customized in a variety of ways.
- **Arrow symbol** can be chosen for both source and target variations
- **Colors and borders** can be customized for the edge label and the line
- **Line style** can be solid, dotted, or dashed
- **Label font size** and **minimum zoomed font size** control how large the edge's label renders and how small it can shrink before it hides while zooming out

### Namespace Panel

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { vi } from "vitest";

import type { AutoBoundingBox } from "./types";

import drawBoxWithAdornment from "./drawBoxWithAdornment";

/** A stub 2D context measuring text at a fixed width per character. */
function createFakeContext() {
return {
save: vi.fn(),
restore: vi.fn(),
beginPath: vi.fn(),
closePath: vi.fn(),
moveTo: vi.fn(),
arcTo: vi.fn(),
fill: vi.fn(),
stroke: vi.fn(),
setLineDash: vi.fn(),
fillText: vi.fn(),
measureText: (text: string) => ({ width: text.length * 6 }),
font: "",
fillStyle: "",
strokeStyle: "",
lineWidth: 0,
textAlign: "",
textBaseline: "",
} as unknown as CanvasRenderingContext2D;
}

const boundingBox: AutoBoundingBox = {
x: 0,
y: 0,
width: "auto",
height: "auto",
};

describe("drawBoxWithAdornment", () => {
it("draws the full text without truncation when no maxWidth is given", () => {
const context = createFakeContext();
const longText = "a-very-long-vertex-display-name-value";

drawBoxWithAdornment(context, boundingBox, { text: longText });

expect(context.fillText).toHaveBeenCalledWith(
longText,
expect.any(Number),
expect.any(Number),
);
});

it("truncates the text with an ellipsis once it exceeds maxWidth", () => {
const context = createFakeContext();
const longText = "a-very-long-vertex-display-name-value";

drawBoxWithAdornment(context, boundingBox, {
text: longText,
maxWidth: 20,
});

expect(context.fillText).toHaveBeenCalledWith(
expect.stringMatching(/\.\.\.$/),
expect.any(Number),
expect.any(Number),
);
expect(context.fillText).not.toHaveBeenCalledWith(
longText,
expect.any(Number),
expect.any(Number),
);
});
});
54 changes: 54 additions & 0 deletions packages/graph-explorer/src/core/StateProvider/graphStyles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,33 @@ describe("vertexStyleAtom", () => {
createExpectedVertex({ type: vertexType }),
);
});

it("should default font size fields to 7/6", () => {
const dbState = new DbState();
const vertexType = createVertexType("Person");

const { result } = renderHookWithState(
() => useAtomValue(vertexStyleAtom),
dbState,
);

const style = result.current.get(vertexType);
expect(style.fontSize).toBe(7);
expect(style.minZoomedFontSize).toBe(6);
});

it("should let a user override the font size", () => {
const dbState = new DbState();
const vertexType = createVertexType("Person");
dbState.addVertexStyle(vertexType, { fontSize: 12 });

const { result } = renderHookWithState(
() => useAtomValue(vertexStyleAtom),
dbState,
);

expect(result.current.get(vertexType).fontSize).toBe(12);
});
});

describe("edgeStyleAtom", () => {
Expand Down Expand Up @@ -459,4 +486,31 @@ describe("edgeStyleAtom", () => {
createExpectedEdge({ type: edgeType }),
);
});

it("should default font size fields to 7/6", () => {
const dbState = new DbState();
const edgeType = createEdgeType("KNOWS");

const { result } = renderHookWithState(
() => useAtomValue(edgeStyleAtom),
dbState,
);

const style = result.current.get(edgeType);
expect(style.fontSize).toBe(7);
expect(style.minZoomedFontSize).toBe(6);
});

it("should let a user override the font size", () => {
const dbState = new DbState();
const edgeType = createEdgeType("KNOWS");
dbState.addEdgeStyle(edgeType, { fontSize: 10 });

const { result } = renderHookWithState(
() => useAtomValue(edgeStyleAtom),
dbState,
);

expect(result.current.get(edgeType).fontSize).toBe(10);
});
});
14 changes: 14 additions & 0 deletions packages/graph-explorer/src/core/StateProvider/graphStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export type VertexVisualStyle = {
borderWidth: number;
borderColor: string;
borderStyle: LineStyle;
/** Label font size in pixels. */
fontSize: number;
/** Minimum on-screen font size before labels are hidden while zooming. */
minZoomedFontSize: number;
};

/**
Expand Down Expand Up @@ -108,6 +112,10 @@ export type EdgeVisualStyle = {
lineStyle: LineStyle;
sourceArrowStyle: ArrowStyle;
targetArrowStyle: ArrowStyle;
/** Label font size in pixels. */
fontSize: number;
/** Minimum on-screen font size before labels are hidden while zooming. */
minZoomedFontSize: number;
};

/** The type-specific fields of an edge style. */
Expand Down Expand Up @@ -148,6 +156,9 @@ export const appDefaultVertexStyle = {
borderWidth: 0,
borderColor: "#128EE5",
borderStyle: "solid",
// Keep in sync with `components/Graph/styles/defaultNodeStyle.ts`.
fontSize: 7,
minZoomedFontSize: 6,
} as const satisfies Omit<VertexStyle, "type">;

/** The default values to use when no user provided value is given. */
Expand All @@ -163,6 +174,9 @@ export const appDefaultEdgeStyle = {
lineStyle: "solid",
sourceArrowStyle: "none",
targetArrowStyle: "triangle",
// Keep in sync with `components/Graph/styles/defaultEdgeStyle.ts`.
fontSize: 7,
minZoomedFontSize: 6,
} as const satisfies Omit<EdgeStyle, "type">;

/**
Expand Down
70 changes: 70 additions & 0 deletions packages/graph-explorer/src/core/styling/roundTrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,76 @@ describe("round-trip: export then import", () => {
lineColor: "#0c4a6e",
});
});

test("font size fields survive round-trip", async () => {
const store = getAppStore();
store.set(
userVertexStylesAtom,
new Map<VertexType, VertexStyleStorage>([
[
createVertexType("airport"),
{
type: createVertexType("airport"),
fontSize: 14,
minZoomedFontSize: 8,
},
],
]),
);
store.set(
userEdgeStylesAtom,
new Map<EdgeType, EdgeStyleStorage>([
[
createEdgeType("route"),
{
type: createEdgeType("route"),
fontSize: 11,
minZoomedFontSize: 5,
},
],
]),
);

const { result: exportResult } = renderHookWithJotai(() =>
useExportStylingFile(),
);
const payload = exportResult.current.getExportPayload();

expect(payload.vertices["airport"]).toStrictEqual({
fontSize: 14,
minZoomedFontSize: 8,
});
expect(payload.edges["route"]).toStrictEqual({
fontSize: 11,
minZoomedFontSize: 5,
});

const file = envelopeToFile(payload);

store.set(userVertexStylesAtom, new Map());
store.set(userEdgeStylesAtom, new Map());

const { result: importResult } = renderHookWithJotai(() =>
useApplyStylingImport(),
);
const parseOut = await parseStylingFile(file);
importResult.current(parseOut);

expect(
store.get(sharedVertexStylesAtom).get(createVertexType("airport")),
).toStrictEqual({
type: createVertexType("airport"),
fontSize: 14,
minZoomedFontSize: 8,
});
expect(
store.get(sharedEdgeStylesAtom).get(createEdgeType("route")),
).toStrictEqual({
type: createEdgeType("route"),
fontSize: 11,
minZoomedFontSize: 5,
});
});
});

function envelopeToFile(payload: unknown): File {
Expand Down
4 changes: 4 additions & 0 deletions packages/graph-explorer/src/core/styling/stylingParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ const vertexEntrySchema = z
borderWidth: z.number().optional(),
borderColor: z.string().optional(),
borderStyle: z.enum(LINE_STYLES).optional(),
fontSize: z.number().optional(),
minZoomedFontSize: z.number().optional(),
})
.transform(
({ icon, ...rest }): Omit<VertexStyleStorage, "type"> =>
Expand All @@ -157,6 +159,8 @@ const edgeEntrySchema = z.object({
lineStyle: z.enum(LINE_STYLES).optional(),
sourceArrowStyle: z.enum(ARROW_STYLES).optional(),
targetArrowStyle: z.enum(ARROW_STYLES).optional(),
fontSize: z.number().optional(),
minZoomedFontSize: z.number().optional(),
});

// --- File-format types ---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@ function Content({ edgeType }: { edgeType: EdgeType }) {
</Select>
</Field>
</FieldGroup>
<FieldGroup className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel>Label Font Size</FieldLabel>
<NumberInput
min={1}
step={1}
value={edgeStyle.fontSize}
onValueChange={fontSize => setEdgeStyle({ fontSize })}
/>
</Field>
<Field>
<FieldLabel>Min Zoomed Font Size</FieldLabel>
<NumberInput
min={1}
step={1}
value={edgeStyle.minZoomedFontSize}
onValueChange={minZoomedFontSize =>
setEdgeStyle({ minZoomedFontSize })
}
/>
</Field>
</FieldGroup>
</FieldSet>
<FieldSet>
<FieldLegend>Line Styling</FieldLegend>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ describe("useGraphStyles", () => {
"border-opacity": 1,
"border-style": "solid",
shape: "ellipse",
"font-size": 7,
"min-zoomed-font-size": 6,
width: 24,
height: 24,
});
Expand All @@ -86,6 +88,8 @@ describe("useGraphStyles", () => {
labelBorderWidth: 1,
labelBorderColor: "#000000",
labelBorderStyle: "solid" as const,
fontSize: 12,
minZoomedFontSize: 7,
};
dbState.activeSchema.edges = [edgeConfig];
dbState.addEdgeStyle(edgeConfig.type, edgeConfig);
Expand All @@ -107,6 +111,8 @@ describe("useGraphStyles", () => {
"text-border-width": 1,
"text-border-color": "#000000",
"text-border-style": "solid",
"font-size": 12,
"min-zoomed-font-size": 7,
width: 2,
"source-distance-from-node": 0,
"target-distance-from-node": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function createGraphStyles(
"border-opacity": vtConfig.borderWidth > 0 ? 1 : 0,
"border-style": vtConfig.borderStyle,
shape: vtConfig.shape,
"font-size": vtConfig.fontSize,
"min-zoomed-font-size": vtConfig.minZoomedFontSize,
width: 24,
height: 24,
};
Expand Down Expand Up @@ -84,6 +86,8 @@ function createGraphStyles(
"text-border-width": etConfig?.labelBorderWidth,
"text-border-color": etConfig?.labelBorderColor,
"text-border-style": etConfig?.labelBorderStyle,
"font-size": etConfig.fontSize,
"min-zoomed-font-size": etConfig.minZoomedFontSize,
width: etConfig.lineThickness,
"source-distance-from-node": 0,
"target-distance-from-node": 0,
Expand Down
Loading