aws / aws/graph-explorer

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

Open
#2,134 0 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
481
Forks
108
Avg merge
6d 8h
Merged PRs (30d)
5

Description

## 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.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.

## 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.:
```turtle
@prefix ex: .
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.

Contributor guide

Open the contributing guide

Research direction

Read packages/graph-explorer/src/connector/sparql/mappers/mapQuadToEntities.ts, core/entities/vertex.ts, and core/StateProvider/displayVertex.ts, then trace the existing single-type style resolution described in #2003. Check how Schema Explorer models rdfs:subClassOf relationships. Done means a multi-typed RDF vertex merges styles across its types, with more-specific values winning conflicts, and display-name resolution no longer depends on arbitrary SPARQL result order.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.