diff --git a/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts b/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts new file mode 100644 index 00000000..f2cfc45e --- /dev/null +++ b/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts @@ -0,0 +1,34 @@ +import type { GraphEntityShapeVariant } from "./graphTheme"; + +export const ENTITY_SHAPE_ALIASES: Array<[GraphEntityShapeVariant, RegExp]> = [ + ["biomolecule", /\b(gene|protein|enzyme|receptor|target|transcript|rna|dna|mirna|biomolecule|peptide)\b/i], + ["condition", /\b(disease|condition|phenotype|symptom|disorder|syndrome|diagnosis|pathology|trait)\b/i], + ["compound", /\b(drug|chemical|compound|metabolite|molecule|small[_\s-]?molecule|ligand|therapeutic|medication|substance)\b/i], + ["process", /\b(pathway|process|mechanism|function|ontology|biological[_\s-]?process|cellular[_\s-]?process|program|module)\b/i], +]; + +export function classifyEntityShape( + nodeType?: string, + semanticGroup?: string, + content?: string, + properties?: Record, +): GraphEntityShapeVariant { + const values = [ + nodeType, + semanticGroup, + content, + String(properties?.type ?? ""), + String(properties?.category ?? ""), + String(properties?.label ?? ""), + ] + .filter((value) => typeof value === "string" && value.trim().length > 0) + .join(" "); + + for (const [shape, pattern] of ENTITY_SHAPE_ALIASES) { + if (pattern.test(values)) { + return shape; + } + } + + return "entity"; +} diff --git a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts index d2ef696f..698254c7 100644 --- a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +++ b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts @@ -19,6 +19,7 @@ import { withAlpha, zoomTierAtLeast, } from "./graphTheme"; +import { classifyEntityShape } from "./graphEntityShape"; import { computeGraphAnalyticsBase } from "./graphAnalytics"; import type { GraphDisplayMeta, @@ -1090,7 +1091,15 @@ export function resolveEntityShape(attrs: NodeAttributes): GraphEntityShapeVaria return "community"; } - return attrs.entityShape || "entity"; + // Prefer the shape stamped at load time; fall back to classifyEntityShape for + // nodes created programmatically that bypassed useLoadGraph. + return attrs.entityShape + || classifyEntityShape( + attrs.nodeType, + attrs.semanticGroup, + attrs.content, + attrs.properties as Record | undefined, + ); } export function resolveEdgeVariant(state: GraphEdgeVisualState, attrs: EdgeAttributes): GraphEdgeVariant { diff --git a/explorer/src/workspaces/GraphWorkspace/graphTheme.ts b/explorer/src/workspaces/GraphWorkspace/graphTheme.ts index dfa0ebb8..e6677244 100644 --- a/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +++ b/explorer/src/workspaces/GraphWorkspace/graphTheme.ts @@ -832,8 +832,10 @@ export const GRAPH_THEME: GraphTheme = { bridgeMaxSize: 0.7, structureEdgeAlpha: 0.12, inspectionEdgeAlpha: 0.1, - }, - fullGraphStructureLayer: { + }, + // Staged rollout — set mode to "auto" to enable cross-community curve rendering. + // Currently "off" so the canvas overlay layer is inactive in production. + fullGraphStructureLayer: { mode: "off", minimumLiteralEdges: 24, minimumCurves: 8, @@ -843,19 +845,19 @@ export const GRAPH_THEME: GraphTheme = { bridgeLineWidth: 0.9, backboneLineWidth: 0.62, curveStrength: 0.12, - }, - }, - interaction: { - localContextAlpha: 0.32, - hoverContextAlpha: 0.32, - selectedEdgeAlpha: 0.6, - pathEdgeAlpha: 0.76, - localContextMaxSize: 0.6, - selectedEdgeMaxSize: 1.0, - pathEdgeMaxSize: 1.4, - pathOverlayAlpha: 0.16, - }, - overlays: { + }, + }, + interaction: { + localContextAlpha: 0.32, + hoverContextAlpha: 0.32, + selectedEdgeAlpha: 0.6, + pathEdgeAlpha: 0.76, + localContextMaxSize: 0.6, + selectedEdgeMaxSize: 1.0, + pathEdgeMaxSize: 1.4, + pathOverlayAlpha: 0.16, + }, + overlays: { hoverGlowAlpha: 0.18, pathGlowAlpha: 0.16, glowRadiusMultiplier: 4.8, diff --git a/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts b/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts index 02214e85..f23c563a 100644 --- a/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +++ b/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts @@ -14,6 +14,7 @@ import { type GraphLabelVisibilityPolicy, type GraphNodeShapeVariant, } from "./graphTheme"; +import { classifyEntityShape } from "./graphEntityShape"; import { createGraphLoadProgress } from "./graphLoading"; import type { GraphLoadProgress, GraphLoadSummary } from "./types"; @@ -30,12 +31,6 @@ const SEMANTIC_COLOR_FIELDS = [ ] as const; const PROVENANCE_KEYS = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"] as const; -const ENTITY_SHAPE_ALIASES: Array<[GraphEntityShapeVariant, RegExp]> = [ - ["biomolecule", /\b(gene|protein|enzyme|receptor|target|transcript|rna|dna|mirna|biomolecule|peptide)\b/i], - ["condition", /\b(disease|condition|phenotype|symptom|disorder|syndrome|diagnosis|pathology|trait)\b/i], - ["compound", /\b(drug|chemical|compound|metabolite|molecule|small[_\s-]?molecule|ligand|therapeutic|medication|substance)\b/i], - ["process", /\b(pathway|process|mechanism|function|ontology|biological[_\s-]?process|cellular[_\s-]?process|program|module)\b/i], -]; function getSemanticFieldValue(attributes: NodeAttributes, field: (typeof SEMANTIC_COLOR_FIELDS)[number]): string | null { if (field === "nodeType") { @@ -204,24 +199,12 @@ function getProvenanceCount(properties: Record): number { } function resolveEntityShape(attributes: NodeAttributes, semanticGroup: string): GraphEntityShapeVariant { - const values = [ + return classifyEntityShape( attributes.nodeType, semanticGroup, attributes.content, - String(attributes.properties?.type ?? ""), - String(attributes.properties?.category ?? ""), - String(attributes.properties?.label ?? ""), - ] - .filter((value) => typeof value === "string" && value.trim().length > 0) - .join(" "); - - for (const [shape, pattern] of ENTITY_SHAPE_ALIASES) { - if (pattern.test(values)) { - return shape; - } - } - - return "entity"; + attributes.properties as Record | undefined, + ); } function resolveNodeVariantMetadata(