Skip to content

Merge vertex styles across all of a vertex's types instead of picking one arbitrarily - #2138

Open
giantamoeba wants to merge 1 commit into
aws:mainfrom
giantamoeba:fix/merge-styles-across-vertex-types
Open

Merge vertex styles across all of a vertex's types instead of picking one arbitrarily#2138
giantamoeba wants to merge 1 commit into
aws:mainfrom
giantamoeba:fix/merge-styles-across-vertex-types

Conversation

@giantamoeba

Copy link
Copy Markdown

Description

Fixes the multi-type vertex styling bug described in #2134.

Today, when a vertex has more than one type — routine for SPARQL/RDF connections under RDFS/OWL inference, where every superclass gets asserted as a peer rdf:type — styling (icon, color, shape, border, etc.) and the displayNameAttribute/longDisplayNameAttribute lookup are resolved from a single "primary type," which createVertex sets to types[0]. For Gremlin/openCypher that's a reasonable choice (labels are already an ordered list from the graph engine); for RDF, rdf:type is an unordered set, so "first" is really "whatever order the SPARQL engine happened to return," which is not guaranteed by SPARQL and in practice is often the most generic ancestor class — so a user's styling for the specific class they actually care about silently never applies.

This PR replaces the single-type lookup with a merge across every one of a vertex's types, generalizing the resolution model already described in #2003 ({ ...appDefault, ...userCustom.get(type) } for one type) to fold over multiple types:

  • core/StateProvider/graphStyles.ts: adds vertexTypeSetKey (a stable, order-independent identity for a vertex's full type set), mergeVertexStyleFields (folds each type's stored style into one, field by field, so a field unset by one type falls through to another that sets it — e.g. a borderColor styled once on a shared ancestor class applies to every subtype automatically), and resolveVertexStyleForTypes (the merged result overlaid on appDefaultVertexStyle, same shape as the existing resolveVertexStyle). Also adds vertexStyleAtom.getForTypes, vertexStyleByTypesAtom, and the useVertexStyleForTypes hook.
  • core/StateProvider/displayVertex.ts: displayVertexSelector now resolves displayName/displayDescription via resolveVertexStyleForTypes(vertexTypes, ...) instead of vertexStyleByTypeAtom(vertex.type).
  • core/StateProvider/renderedEntities.ts: a rendered vertex's Cytoscape data.type (used for stylesheet selector matching) is now vertexTypeSetKey(vertex.types) instead of vertex.primaryType.
  • modules/GraphViewer/useGraphStyles.ts: builds stylesheet rules for the distinct type-sets actually present among rendered vertices, in addition to (not instead of) the existing one-rule-per-schema-type generation — so single-typed vertices are unaffected (vertexTypeSetKey of one type is that type itself) and multi-typed vertices get their own correctly-merged rule instead of colliding with one arbitrary member type's rule.
  • components/VertexRow.tsx: search-result rows now resolve style via useVertexStyleForTypes(vertex.types) instead of useVertexStyle(vertex.primaryType).

Not changed: anywhere a single, real schema type is genuinely what's wanted — the Legend panel, the Schema view's per-type Styles editor, the filter sidebar, VertexIconByType/VertexSymbolByType — all still use useVertexStyle(type)/vertexStyleByTypeAtom unmodified, since those show or edit one type's own style, not a specific multi-typed vertex instance.

Conflict resolution — an honest tradeoff

When two of a vertex's types both set the same field (e.g. both set color), something has to win. I resolve this by folding the vertex's types in ascending lexicographic order, so the lexicographically-last type wins conflicts. This is a stable, reproducible tiebreak, not a specificity judgement — nothing in the app currently tracks rdfs:subClassOf (or any other) class hierarchy; schema sync only samples instance data (types + attribute names) for every connector. There's no signal available today to determine which of a resource's asserted types is actually "more specific." Sorting at least makes the outcome a deterministic property of the type names themselves instead of an accident of query result order, which is what motivated this fix in the first place.

A hierarchy-aware tiebreak (prefer the type that is not an rdfs:subClassOf ancestor of any other asserted type) would be a strictly better conflict resolution and a natural follow-up once class-hierarchy data is tracked anywhere in the app — happy to discuss whether that's in scope for a first pass or worth its own follow-up issue.

Validation

  • pnpm run check:types — passes
  • pnpm run check:lint — 0 warnings, 0 errors
  • pnpm run check:format — passes
  • pnpm run test — full suite passes (one pre-existing, unrelated failure in safeSessionStorage.test.ts reproduces identically on main with no changes — a vi.mocked(logger.warn) assertion that appears environment-dependent, not something this PR touches)
  • New unit tests in graphStyles.test.ts cover vertexTypeSetKey (order-independence, dedup, distinctness), mergeVertexStyleFields (non-conflicting merge, conflict tiebreak order-independence, missing styles, empty result), and vertexStyleAtom.getForTypes (merge-overlaid-on-defaults, order-independence)
  • An earlier version of this fix rebuilt every rendered vertex's style on every render, because useAllRenderedVertexStyles fed an unstable array reference (from useDisplayVerticesInCanvas()) into useMemo, defeating memoization — on a graph with many vertices sharing type combinations this showed up as a pathological re-render storm during session restore. Fixed by reading nodesAtom directly and memoizing on a content-based string key over the distinct type-set combinations actually present, rather than on the vertex list's object identity. Covered by two new regression tests in useGraphStyles.test.tsx: one asserting the returned array is referentially stable (===) across a no-op re-render, and one asserting it stays stable when a new vertex reuses an existing type combination but correctly rebuilds when a genuinely new combination appears.
  • Manually verified against a real SPARQL/RDF dataset (CGMES power-grid model materialized with RDFS inference — every equipment resource ends up asserting both its specific CIM class and every ancestor class as separate rdf:type triples): before this fix, styled classes rendered with the app's default icon/color depending on triple-store query order; after, they render with the styling set on their specific class.
  • Also verified side by side on a minimal, isolated repro — same resource (two types: ex:Specific sets icon+color, ex:General sets shape+border), same styles file, loaded into an unmodified v3.2.2 build and this branch's dev build, both pointed at the same Fuseki dataset:
    • Before
before-unfixed renders `ex:General`'s diamond shape/border, but `ex:Specific`'s icon and color are silently dropped — falls back to the default blue snowflake icon. - **After** after-fixed renders the merge of both — `ex:General`'s diamond shape _and_ `ex:Specific`'s red zap icon/color on the same node.

Related Issues

Check List

  • I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • I have verified pnpm checks passes with no errors.
  • I have verified pnpm test passes with no failures. — 2677/2678 pass; the one failure (safeSessionStorage.test.ts, resolveSessionStorage > warns and falls back to in-memory storage when sessionStorage is unavailable) reproduces identically on unmodified main, so it's pre-existing and unrelated to this change, not something this branch introduced.
  • I have covered new added functionality with unit tests if necessary. — new tests in graphStyles.test.ts (vertexTypeSetKey, mergeVertexStyleFields, vertexStyleAtom.getForTypes) and two regression tests in useGraphStyles.test.tsx for the memoization fix.
  • I have updated documentation if necessary. — checked docs/adr/ for anything describing primaryType/single-type style resolution; nothing references it, so nothing needed updating.

… one arbitrarily

For a SPARQL/RDF connection, a resource with more than one rdf:type
(common under RDFS/OWL inference, where every ancestor class is
asserted as a peer rdf:type triple) had its styling/displayNameAttribute
resolved from whichever type happened to come first in SPARQL result
order -- unspecified by the SPARQL spec, and in practice often the most
generic ancestor class rather than the specific one a user actually
styled.

Adds resolveVertexStyleForTypes/mergeVertexStyleFields, which fold a
vertex's full type set (sorted for a stable, reproducible tiebreak) into
one merged style instead of keying off a single primaryType. Wires this
through useGraphStyles.ts (Cytoscape stylesheet generation),
displayVertex.ts (displayNameAttribute resolution), renderedEntities.ts
(the type key used as the Cytoscape selector), and VertexRow.tsx (search
result styling).

See PR_DRAFT.md for the full writeup and reproduction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SPARQL: vertex styling/primary type picks an arbitrary rdf:type when a resource has multiple types (not a merge of all matching styles)

2 participants