From 61b0f7b1385e14851486ec0dc4d0cc43e78697a1 Mon Sep 17 00:00:00 2001 From: Martin Ross Date: Wed, 16 Feb 2022 13:31:48 -0500 Subject: [PATCH] Add support for interactive mouseover/mouseout focus on SVG to focusing on a node and its edges in a complex diagram. This is enabled with !pragma svginteractive. --- .../sourceforge/plantuml/svg/SvgGraphics.java | 26 ++++++ .../plantuml/ugraphic/ImageBuilder.java | 2 +- .../plantuml/ugraphic/svg/UGraphicSvg.java | 15 +++- svg/onmouseinteractivefooter.css | 6 ++ svg/onmouseinteractivefooter.js | 84 +++++++++++++++++++ 5 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 svg/onmouseinteractivefooter.css create mode 100644 svg/onmouseinteractivefooter.js diff --git a/src/net/sourceforge/plantuml/svg/SvgGraphics.java b/src/net/sourceforge/plantuml/svg/SvgGraphics.java index d66bd4dff..c5785d7e7 100644 --- a/src/net/sourceforge/plantuml/svg/SvgGraphics.java +++ b/src/net/sourceforge/plantuml/svg/SvgGraphics.java @@ -988,6 +988,7 @@ public class SvgGraphics { return SignatureUtils.getMD5Hex(comment); } + public void addComment(String comment) { final String signature = getMD5Hex(comment); comment = "MD5=[" + signature + "]\n" + comment; @@ -995,6 +996,31 @@ public class SvgGraphics { getG().appendChild(commentElement); } + public void addScriptTag(String url) { + final Element script = document.createElement("script"); + script.setAttribute("type", "text/javascript"); + script.setAttribute("xlink:href", url); + root.appendChild(script); + } + + public void addScript(String scriptTextPath) { + final Element script = document.createElement("script"); + final String scriptText = getData(scriptTextPath); + final CDATASection cDATAScript = document.createCDATASection(scriptText); + script.appendChild(cDATAScript); + root.appendChild(script); + } + + public void addStyle(String cssStylePath) { + final Element style = simpleElement("style"); + final String text = getData(cssStylePath); + + final CDATASection cdata = document.createCDATASection(text); + style.setAttribute("type", "text/css"); + style.appendChild(cdata); + root.appendChild(style); + } + public void openLink(String url, String title, String target) { Objects.requireNonNull(url); diff --git a/src/net/sourceforge/plantuml/ugraphic/ImageBuilder.java b/src/net/sourceforge/plantuml/ugraphic/ImageBuilder.java index 90e7f6fd1..74fbb5b1e 100644 --- a/src/net/sourceforge/plantuml/ugraphic/ImageBuilder.java +++ b/src/net/sourceforge/plantuml/ugraphic/ImageBuilder.java @@ -255,7 +255,7 @@ public class ImageBuilder { / 96.0; if (scaleFactor <= 0) throw new IllegalStateException("Bad scaleFactor"); - UGraphic ug = createUGraphic(fileFormatOption, dim, animationArg, dx, dy, scaleFactor, titledDiagram.getPragma()); + UGraphic ug = createUGraphic(fileFormatOption, dim, animationArg, dx, dy, scaleFactor, titledDiagram !=null ? titledDiagram.getPragma() : new Pragma()); maybeDrawBorder(ug, dim); if (randomPixel) { drawRandomPoint(ug); diff --git a/src/net/sourceforge/plantuml/ugraphic/svg/UGraphicSvg.java b/src/net/sourceforge/plantuml/ugraphic/svg/UGraphicSvg.java index 4a98bcdd5..aa938242f 100644 --- a/src/net/sourceforge/plantuml/ugraphic/svg/UGraphicSvg.java +++ b/src/net/sourceforge/plantuml/ugraphic/svg/UGraphicSvg.java @@ -71,6 +71,7 @@ public class UGraphicSvg extends AbstractUGraphic implements ClipCo private final boolean textAsPath2; private final String target; + private final Pragma pragma; public double dpiFactor() { return 1; @@ -85,6 +86,7 @@ public class UGraphicSvg extends AbstractUGraphic implements ClipCo super(other); this.textAsPath2 = other.textAsPath2; this.target = other.target; + this.pragma = other.pragma; register(); } @@ -94,7 +96,7 @@ public class UGraphicSvg extends AbstractUGraphic implements ClipCo this(defaultBackground, minDim, colorMapper, new SvgGraphics(colorMapper.toSvg(defaultBackground), svgDimensionStyle, minDim, scale, hover, seed, preserveAspectRatio, lengthAdjust, DarkStrategy.IGNORE_DARK_COLOR, pragma), - textAsPath, linkTarget, stringBounder); + textAsPath, linkTarget, stringBounder, pragma); if (defaultBackground instanceof HColorGradient) { final SvgGraphics svg = getGraphicObject(); svg.paintBackcolorGradient(colorMapper, (HColorGradient) defaultBackground); @@ -117,10 +119,11 @@ public class UGraphicSvg extends AbstractUGraphic implements ClipCo } private UGraphicSvg(HColor defaultBackground, Dimension2D minDim, ColorMapper colorMapper, SvgGraphics svg, - boolean textAsPath, String linkTarget, StringBounder stringBounder) { + boolean textAsPath, String linkTarget, StringBounder stringBounder, Pragma pragma) { super(defaultBackground, colorMapper, stringBounder, svg); this.textAsPath2 = textAsPath; this.target = linkTarget; + this.pragma = pragma; register(); } @@ -152,6 +155,14 @@ public class UGraphicSvg extends AbstractUGraphic implements ClipCo if (metadata != null) getGraphicObject().addComment(metadata); + if (pragma.isDefine("svginteractive") && Boolean.valueOf(pragma.getValue("svginteractive"))) { + // For performance reasons and also because we want the entire graph DOM to be create so we can register + // the event handlers on them we will append to the end of the document + getGraphicObject().addStyle("onmouseinteractivefooter.css"); + getGraphicObject().addScriptTag("https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"); + getGraphicObject().addScript("onmouseinteractivefooter.js"); + } + getGraphicObject().createXml(os); } catch (TransformerException e) { throw new IOException(e.toString()); diff --git a/svg/onmouseinteractivefooter.css b/svg/onmouseinteractivefooter.css new file mode 100644 index 000000000..0cddf4a8c --- /dev/null +++ b/svg/onmouseinteractivefooter.css @@ -0,0 +1,6 @@ +[data-mouse-over-selected="false"] { + opacity: 0.2; +} +[data-mouse-over-selected="true"] { + opacity: 1.0; +} \ No newline at end of file diff --git a/svg/onmouseinteractivefooter.js b/svg/onmouseinteractivefooter.js new file mode 100644 index 000000000..0389c370c --- /dev/null +++ b/svg/onmouseinteractivefooter.js @@ -0,0 +1,84 @@ +(function (){ + /** + * @param {SVG.G} node + * @param {SVG.G} topG + * @return {{node: Set, edges:Set}} + */ + function getEdgesAndDistance1Nodes(node, topG) { + const nodeName = node.attr("id").match(/elem_(.+)/)[1]; + const selector = "[id^=link_]" + const candidates = topG.find(selector) + let edges = new Set(); + let nodes = new Set(); + for (let link of candidates) { + const res = link.attr("id").match(/link_([A-Za-z\d]+)_([A-Za-z\d]+)/); + if (res && res.length==3) { + const N1 = res[1]; + const N2 = res[2]; + if (N1==nodeName) { + const N2selector = `[id^=elem_${N2}]`; + nodes.add(topG.findOne(N2selector)); + edges.add(link); + } else if (N2==nodeName) { + const N1selector = `[id^=elem_${N1}]`; + nodes.add(topG.findOne(N1selector)); + edges.add(link); + } + } + } + return { + "nodes" : nodes, + "edges" : edges + }; + } + + /** + * @param {SVG.G} node + * @param {function(SVG.Dom)} + * @return {{node: Set, edges:Set}} + */ + function walk(node, func) { + let children = node.children(); + for (let child of children) { + walk(child, func) + } + func(node); + } + let s = SVG("svg > g") + /** + * @param {SVGElement} domEl + * @return {{SVGElement}} + */ + function findEnclosingG(domEl) { + let curEl = domEl; + while (curEl.nodeName != "g") { + curEl = curEl.parentElement; + } + return curEl; + } + function onMouseOverElem(domEl) { + let e = SVG(findEnclosingG(domEl.target)); + walk(s, + e => { if (SVG(e)!=s) + SVG(e).attr('data-mouse-over-selected',"false"); + }); + walk(e, e => SVG(e).attr('data-mouse-over-selected',"true")); + let {nodes, edges} = getEdgesAndDistance1Nodes(SVG(e), s); + for (let node of nodes) { + walk(node, e => SVG(e).attr('data-mouse-over-selected',"true")); + } + for (let edge of edges) { + walk(edge, e => SVG(e).attr('data-mouse-over-selected',"true")); + } + } + + function onMouseOutElem(domEl) { + let e = SVG(findEnclosingG(domEl.target)); + walk(s, e => e.attr('data-mouse-over-selected',null)); + } + let gs = s.find("g[id^=elem_]"); + for (let g of gs) { + g.on("mouseover", onMouseOverElem); + g.on("mouseout", onMouseOutElem); + } +})();