From 483804aab2edc5e040cc13eb0dc2a8d7ebec32ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martynas=20Jusevi=C4=8Dius?= Date: Thu, 10 Sep 2026 19:04:36 +0200 Subject: [PATCH] RDFXML2SVG: the force layout gets its asymptotics and its physics fixed The Fruchterman-Reingold positioning was unusable beyond a few dozen nodes: 50 nodes took 8.5s, 100 nodes 506s, 250 nodes extrapolated to hours (SaxonJS 3.0.0-beta2, M-series laptop, default 30 steps). The machinery is rebuilt around the same force model: - positions live in ONE map keyed by node id (O(1) lookups where the sequence-of-maps predicate scans $seq[?node-id eq $id] made every step O(n^3)), threaded through xsl:iterate; adjacency and the circle seed are computed once, not per iteration, and the O(n^2) non-adjacent-ids lists are gone along with the per-edge map rebuild + regroup dance; - hot-path map parameters are declared as="map(*)", NOT the precise map(xs:string, map(*)): SaxonJS validates a declared deep map type entry by entry on every call and parameter binding - on the tunneled positioning pass that alone cost 8.6s of fixed time at n=250 (the single largest measured share; what remains per step is the XPath interpreter's floor); - each step buckets nodes into a spatial grid (0.7 spring lengths per cell, built with for-each-group - map:merge 'combine' re-copies a packed cell's list per node) and repels against a deterministic, spatially stratified sample of the 3x3 neighborhood (at most 3 per cell, 16 total) - the customary ~2k-cutoff grid approximation of FR; - the per-node displacement is a single XPath expression (per-pair XSLT instructions dominated the profile), with sqrt-free delta * k^2 / d^2 repulsion. After: 50 nodes 1.7s, 100 nodes 3.1s, 250 nodes 16.8s, 500 nodes 67.5s wall (incl. ~1.1s npx/compile startup) - about 165x at 100 nodes, and graph sizes that simply never finished become feasible. The physics also had genuine defects, now corrected (layouts therefore differ from the old output beyond the sampling approximation): - repulsion was scaled by node degree: the edge-record regrouping summed a node's repulsion displacement once per incident edge record; - attraction acted twice per adjacent pair (the edge list carried both directions and each record pulled both endpoints); it now acts once, as the cited paper defines; - the canvas clamp mixed the axes (width^2/4 - y^2 is a circle, not the width x height ellipse), took abs() of negative sqrt arguments, and fed the already re-shifted x into the y clamp; it is now a rectangular frame clamp followed by a radial projection onto the inscribed ellipse; - coincident nodes contributed no force and could stick forever; they now separate along a deterministic direction derived from id order; - line clipping divided by zero for horizontally aligned nodes (the acknowledged TO-DO), emitting NaN coordinates - reproduced in the wild on a 100-node synthetic graph - and is now a unit-vector walk-back that needs no case analysis; the literal-line x-diff=0/y-diff=0 branch set y2 from $x2 (typo). The RDF-side patterns are untouched: the converter's contract stays Jena's RDFXML_PLAIN (rdf:Description subjects with rdf:type inlined as an explicit property, no nesting), per the note at the top of the stylesheet. $spring-stiffness was never used by the force model, before or now; it stays for signature compatibility. Verified: identical node/line/viewBox inventory against the old output on 50- and 100-node synthetic graphs, no NaN, byte-identical positions across repeated runs (the hsl() type-coloring was never run-stable under SaxonJS's random-number-generator and still is not), and a side-by-side visual check on a small ontology. The positioning internals (ac:SVGPositioningLoop, ac:force-step, the ac:SVGPositioning tunnel) have no external dependents in Web-Client or LinkedDataHub. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01FJSsKMVPsAKPVvpLYoK7SS --- .../client/xsl/converters/RDFXML2SVG.xsl | 370 ++++++++---------- 1 file changed, 174 insertions(+), 196 deletions(-) diff --git a/src/main/webapp/static/com/atomgraph/client/xsl/converters/RDFXML2SVG.xsl b/src/main/webapp/static/com/atomgraph/client/xsl/converters/RDFXML2SVG.xsl index 6cc424cc..8af2bad4 100644 --- a/src/main/webapp/static/com/atomgraph/client/xsl/converters/RDFXML2SVG.xsl +++ b/src/main/webapp/static/com/atomgraph/client/xsl/converters/RDFXML2SVG.xsl @@ -31,6 +31,7 @@ xmlns:rdfs="&rdfs;" xmlns:xsd="&xsd;" xmlns:owl="&owl;" xmlns:xs="http://www.w3.org/2001/XMLSchema" +xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:math="http://www.w3.org/2005/xpath-functions/math" exclude-result-prefixes="#all"> @@ -39,8 +40,11 @@ exclude-result-prefixes="#all"> - - + + @@ -334,229 +338,204 @@ exclude-result-prefixes="#all"> - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + - + - + - - + + - + - + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - + + - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - + + - + - - + + @@ -622,13 +601,12 @@ exclude-result-prefixes="#all"> - - - - - - - + + + + <xsl:value-of select="@property"/> @@ -655,7 +633,7 @@ exclude-result-prefixes="#all"> - +