You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
For a SPARQL/RDF connection, when a resource has more than one rdf:type (common with RDFS/OWL-materialized data, where every ancestor class is asserted as a peer rdf:type triple), the vertex's primaryType — used to resolve icon, color, shape, and the displayNameAttribute — is whichever type happens to come first in the SPARQL query's result order. That order isn't guaranteed by SPARQL, and in practice is often the most generic ancestor class, not the specific one a user actually styled.
Relevant code:
packages/graph-explorer/src/connector/sparql/mappers/mapQuadToEntities.ts — createVertexDraft builds types: [binding.object.value] per rdf:type binding; mergeVertexDrafts merges via Set, so the final order is whatever order the query returned the rdf:type triples in.
packages/graph-explorer/src/core/entities/vertex.ts — createVertex: "The primary type (used for styling) will be the first type in the array."
packages/graph-explorer/src/core/StateProvider/displayVertex.ts — vertexStyleByTypeAtom(vertex.type) keys both styling and displayNameAttribute resolution off that single primaryType.
The "ordered array, first = primary" model fits Gremlin/openCypher, where a vertex's labels are already an ordered list from the graph engine. RDF's rdf:type is a set, not an ordered list — there's no principled "first," and whatever a given SPARQL engine happens to return first for ?s a ?o is effectively arbitrary and can change across engines, query plans, or triple-store versions/upgrades.
In our case (an RDFS-reasoned CGMES/CIM power-grid dataset materialized via Apache Jena before export to Fuseki), generic ancestor classes consistently won over the specific equipment class we'd styled, on every resource in the dataset — silently defeating per-class styling entirely.
Suggested fix
Not "pick one type and use it exclusively" (still arbitrary, just differently) — merge styles across all of a vertex's types, CSS-cascade style:
Resolve a vertex's computed style by folding over every type in vertex.types (not just the single primaryType), merging each type's stored style object field by field. A field left unset by one type falls through to another type that does set it — e.g. a shared borderColor/shape styled once on a common ancestor class (cim:ConductingEquipment) applies to every subtype automatically, without repeating it on every leaf class.
For a genuine conflict (two of the vertex's types both set the same field, e.g. both set color), prefer the more specific type — ordering the fold from most-generic to most-specific using the rdfs:subClassOf hierarchy the Schema Explorer already models, so the leaf-most type's value wins on direct conflicts, the same way a more specific CSS selector wins.
This isn't a new mechanism — it's a small extension of the existing style-resolution architecture described in #2003 ("Kickoff: Styling System Overhaul"), which already layers { ...appDefault, ...userCustom.get(type) } for a single type. Generalizing that fold to run over every type a vertex has, instead of just one, gets correct behavior for multi-typed RDF resources without a new style model, and composes with (rather than replaces) the ongoing styling overhaul.
Related: #1312 / #1194 (multi-label schema entries / neighbor counts) established multi-label/multi-type support elsewhere in the app; this is the remaining gap in styling resolution specifically.
Load RDF data where one resource has two rdf:type values — a specific class and one of its rdfs:subClassOf ancestors, e.g.:
@prefix ex: <http://example.org/> .
ex:thing1 a ex:Specific, ex:General .
In Settings → Styles, style ex:Specific distinctively (a non-default icon and color).
In the Search sidebar, search/add ex:thing1 to the canvas.
Observe the node's rendered icon and color.
Expected Behavior
The node renders using the style set on ex:Specific (or, ideally, a merge of every type's style, with ex:Specific's fields winning on conflicts). Instead, it renders with whichever type the SPARQL query happened to return first — in practice, often ex:General's default (unstyled) appearance — with no way to predict or control which type wins.
I am interested in working on this issue, and will soon post a PR
Important
If you are interested in working on this issue, please leave a comment.
Tip
Please use a 👍 reaction to provide a +1/vote. This helps the community and maintainers prioritize this request.
Description
For a SPARQL/RDF connection, when a resource has more than one
rdf:type(common with RDFS/OWL-materialized data, where every ancestor class is asserted as a peerrdf:typetriple), the vertex'sprimaryType— used to resolve icon, color, shape, and thedisplayNameAttribute— is whichever type happens to come first in the SPARQL query's result order. That order isn't guaranteed by SPARQL, and in practice is often the most generic ancestor class, not the specific one a user actually styled.Relevant code:
packages/graph-explorer/src/connector/sparql/mappers/mapQuadToEntities.ts—createVertexDraftbuildstypes: [binding.object.value]perrdf:typebinding;mergeVertexDraftsmerges viaSet, so the final order is whatever order the query returned therdf:typetriples in.packages/graph-explorer/src/core/entities/vertex.ts—createVertex: "The primary type (used for styling) will be the first type in the array."packages/graph-explorer/src/core/StateProvider/displayVertex.ts—vertexStyleByTypeAtom(vertex.type)keys both styling anddisplayNameAttributeresolution off that singleprimaryType.The "ordered array, first = primary" model fits Gremlin/openCypher, where a vertex's labels are already an ordered list from the graph engine. RDF's
rdf:typeis a set, not an ordered list — there's no principled "first," and whatever a given SPARQL engine happens to return first for?s a ?ois effectively arbitrary and can change across engines, query plans, or triple-store versions/upgrades.In our case (an RDFS-reasoned CGMES/CIM power-grid dataset materialized via Apache Jena before export to Fuseki), generic ancestor classes consistently won over the specific equipment class we'd styled, on every resource in the dataset — silently defeating per-class styling entirely.
Suggested fix
Not "pick one type and use it exclusively" (still arbitrary, just differently) — merge styles across all of a vertex's types, CSS-cascade style:
vertex.types(not just the singleprimaryType), merging each type's stored style object field by field. A field left unset by one type falls through to another type that does set it — e.g. a sharedborderColor/shapestyled once on a common ancestor class (cim:ConductingEquipment) applies to every subtype automatically, without repeating it on every leaf class.color), prefer the more specific type — ordering the fold from most-generic to most-specific using therdfs:subClassOfhierarchy the Schema Explorer already models, so the leaf-most type's value wins on direct conflicts, the same way a more specific CSS selector wins.This isn't a new mechanism — it's a small extension of the existing style-resolution architecture described in #2003 ("Kickoff: Styling System Overhaul"), which already layers
{ ...appDefault, ...userCustom.get(type) }for a single type. Generalizing that fold to run over every type a vertex has, instead of just one, gets correct behavior for multi-typed RDF resources without a new style model, and composes with (rather than replaces) the ongoing styling overhaul.Related: #1312 / #1194 (multi-label schema entries / neighbor counts) established multi-label/multi-type support elsewhere in the app; this is the remaining gap in styling resolution specifically.
Environment
stain/jena-fusekiDocker imageSteps to Reproduce
rdf:typevalues — a specific class and one of itsrdfs:subClassOfancestors, e.g.:ex:Specificdistinctively (a non-default icon and color).ex:thing1to the canvas.Expected Behavior
The node renders using the style set on
ex:Specific(or, ideally, a merge of every type's style, withex:Specific's fields winning on conflicts). Instead, it renders with whichever type the SPARQL query happened to return first — in practice, oftenex:General's default (unstyled) appearance — with no way to predict or control which type wins.I am interested in working on this issue, and will soon post a PR
Important
If you are interested in working on this issue, please leave a comment.
Tip
Please use a 👍 reaction to provide a +1/vote. This helps the community and maintainers prioritize this request.