Skip to content

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

Description

@giantamoeba

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 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.tscreateVertexDraft 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.tscreateVertex: "The primary type (used for styling) will be the first type in the array."
  • packages/graph-explorer/src/core/StateProvider/displayVertex.tsvertexStyleByTypeAtom(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.

Environment

  • OS: Linux (CachyOS, kernel 7.1.4)
  • Browser: Google Chrome
  • Graph Explorer Version: 3.2.2
  • Graph Database & Version: Apache Jena Fuseki 5.1.0 (SPARQL 1.1), stain/jena-fuseki Docker image

Steps to Reproduce

  1. 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 .
  2. In Settings → Styles, style ex:Specific distinctively (a non-default icon and color).
  3. In the Search sidebar, search/add ex:thing1 to the canvas.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions