Skip to content

feat(views): render SVG views from CONSTRUCT queries in OntoSVG - #709

Open
ashleycaselli wants to merge 1 commit into
masterfrom
feat/592-rdf-svg-representations
Open

ashleycaselli wants to merge 1 commit into
masterfrom
feat/592-rdf-svg-representations

Conversation

@ashleycaselli

Copy link
Copy Markdown
Member

Closes #592.

An SVG view's query had to return ready-made markup in an svg column, which means assembling the figure as a string in SPARQL — group_concat, whose ordering docs/svg-views.md already warns is not dependable, plus hand-escaping every data-derived label.

A view query may now instead be a CONSTRUCT describing the image as RDF in the OntoSVG vocabulary. Nothing else about the view declaration changes: the display type stays gen:SvgView.

CONSTRUCT {
  ?figure a svg:Svg ; xml:xmlns "http://www.w3.org/2000/svg" ;
    svg:width "200" ; svg:height "200" ; rdf:_1 ?box .
  ?box a svg:Rect ; svg:x "10" ; svg:y "10" ; svg:fill "#eef" ; rdf:_1 ?caption .
  ?caption a svg:TextElement ; svg:x "20" ; svg:y "35" ; rdf:_1 ?captionText .
  ?captionText a svg:Text ; xml:fragment ?label .
} WHERE { ... }

How it fits together

Most of the plumbing was already there: QueryAccess parses a non-CSV response into an RDF4J Model, ApiCache.retrieveRdfModelAsync caches it, and QueryTemplate.isConstructQuery() says which kind of query this is — all used today by QueryPage. The new parts are:

  • OntoSvg — serializes the graph to SVG markup.
  • QueryResultSvgBuilder — picks the RDF path for a CONSTRUCT query and hands the serialized figures on as the svg column the view already renders, one row per figure. QueryResultSvg itself is unchanged apart from its Javadoc, so headings, entry actions, sanitization and the "(nothing found)" state stay in one place.

The markup still goes through Utils.sanitizeSvg, which remains the only thing deciding what may render. The sanitizer is untouched by this PR.

Details worth a reviewer's attention

Element names come from the vocabulary, not the class names. They do not always agree, and the mismatches are silent ones:

  • svg:TextElement is <text>; svg:Text is character data. Deriving the tag from the class name would have turned every text node into an empty <text> element and dropped every label. The reference smiley in the OntoSVG repo is 11/15 svg:Text whitespace nodes, which is what caught this.
  • svg:ColorProfilecolor-profile, svg:FontFace*font-face*, svg:MissingGlyphmissing-glyph.

The 80-entry map is generated from the xml:tag values in the vocabulary's svg - core.trig, not typed by hand.

Ordering is made deterministic in three places, since a Model is an unordered set of statements and these views are cached and re-rendered: children sort numerically on rdf:_N (so rdf:_10 follows rdf:_9, not rdf:_1), attributes sort by name, and multiple figures sort by root IRI.

xlink:href is written as href. This is the one deliberate deviation from the spec's serialization rules. OntoSVG models SVG 1.1, where links are xlink:href and there is no svg:href; the sanitizer allows only a bare href on a. Emitting xlink:href faithfully would produce links that are silently stripped, and links are a first-class part of this view type (the existing typology diagram links every label to its class IRI). href is the SVG 2 spelling and the one browsers follow. Documented in docs/svg-views.md.

Root detection takes the outermost svg:Svg nodes — those not inside another svg:Svg. "Not anyone's child" is the wrong test, because the vocabulary wraps a document in an svg:Document holding the doctype and the svg:Svg; my first version used it and failed on the reference smiley.

I did not port the reference implementation's approach: it drives a PyShacl rule engine over ~180KB of SHACL vocabulary to build fragments bottom-up. The data model underneath is a plain tree walk.

Known limitation

A CONSTRUCT result has no columns, so a figure rendered this way has no per-figure title heading and no np source link — the view's own title still shows. Both are documented under "What a CONSTRUCT view gives up". Adding them back would mean inventing OntoSVG-adjacent predicates for view metadata, which seemed worth deciding separately.

Testing

12 new tests in OntoSvgTest, including the reference smiley from the OntoSVG repository serialized and checked against its published SVG rendering, plus numeric child ordering, the Text/TextElement distinction, hyphenated tags, escaping of data-derived text and attributes, nested <svg>, stable multi-figure order, unknown classes, and a cyclic graph.

Full suite: 1426 tests, 0 failures, 0 errors — no regressions.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn

An SVG view's query had to return ready-made markup in an svg column, which
means building the figure as a string in SPARQL: group_concat, whose ordering
is not dependable, and hand-escaping every data-derived label.

A view query may now instead be a CONSTRUCT describing the image as RDF in the
OntoSVG vocabulary. The graph is serialized to markup and handed on as the svg
column the view already renders, so headings, actions, sanitization and the
empty state stay in one place, and the display type is unchanged.

The element names come from the vocabulary rather than from the class names,
which do not always agree: svg:TextElement is <text>, while svg:Text is
character data, and the font and colour-profile classes hyphenate. Children
are read from rdf:_1, rdf:_2, ... in numeric order, so rdf:_10 follows rdf:_9
and the order does not depend on how the graph came back. Text and attribute
values are XML-escaped on the way out, and the markup still goes through
Utils.sanitizeSvg, which remains the only thing deciding what may render.

OntoSVG models SVG 1.1, whose links are xlink:href. Those are written as href,
the SVG 2 spelling, which is both what the sanitizer keeps and what browsers
follow; emitting xlink:href would render links that silently do nothing.

Closes #592

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnGpEQkFqCaf4AEQwwhFFn
@ashleycaselli
ashleycaselli force-pushed the feat/592-rdf-svg-representations branch from b469f70 to 4328f50 Compare September 14, 2026 10:42
@ashleycaselli
ashleycaselli requested a review from tkuhn September 15, 2026 16:49
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.

Support for RDF SVG representations for SvgView

1 participant