1
0
mirror of https://github.com/octoleo/plantuml.git synced 2025-01-03 07:12:29 +00:00

Merge pull request #930 from blipper/master

Add a new !pragma svginterface <true|false> to control INTERACTIVE mode on SVG output and 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.
This commit is contained in:
PlantUML 2022-02-16 22:43:35 +01:00 committed by GitHub
commit db67c75919
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 25 deletions

View File

@ -90,7 +90,7 @@ public class GraphicsSudoku {
public ImageData writeImageSvg(OutputStream os) throws IOException { public ImageData writeImageSvg(OutputStream os) throws IOException {
final UGraphicSvg ug = new UGraphicSvg(HColorUtils.WHITE, true, new Dimension2DDouble(0, 0), final UGraphicSvg ug = new UGraphicSvg(HColorUtils.WHITE, true, new Dimension2DDouble(0, 0),
new ColorMapperIdentity(), false, 1.0, null, null, 0, "none", FileFormat.SVG.getDefaultStringBounder(), new ColorMapperIdentity(), false, 1.0, null, null, 0, "none", FileFormat.SVG.getDefaultStringBounder(),
LengthAdjust.defaultValue()); LengthAdjust.defaultValue(), false);
drawInternal(ug); drawInternal(ug);
ug.writeToStream(os, null, -1); // dpi param is not used ug.writeToStream(os, null, -1); // dpi param is not used
return ImageDataSimple.ok(); return ImageDataSimple.ok();

View File

@ -129,7 +129,7 @@ public class SvgGraphics {
private final boolean svgDimensionStyle; private final boolean svgDimensionStyle;
private final LengthAdjust lengthAdjust; private final LengthAdjust lengthAdjust;
private final boolean INTERACTIVE = false; private final boolean INTERACTIVE;
final protected void ensureVisible(double x, double y) { final protected void ensureVisible(double x, double y) {
if (x > maxX) { if (x > maxX) {
@ -141,7 +141,7 @@ public class SvgGraphics {
} }
public SvgGraphics(String backcolor, boolean svgDimensionStyle, Dimension2D minDim, double scale, String hover, public SvgGraphics(String backcolor, boolean svgDimensionStyle, Dimension2D minDim, double scale, String hover,
long seed, String preserveAspectRatio, LengthAdjust lengthAdjust, DarkStrategy darkStrategy) { long seed, String preserveAspectRatio, LengthAdjust lengthAdjust, DarkStrategy darkStrategy, boolean interactive) {
try { try {
this.lengthAdjust = lengthAdjust; this.lengthAdjust = lengthAdjust;
this.svgDimensionStyle = svgDimensionStyle; this.svgDimensionStyle = svgDimensionStyle;
@ -149,6 +149,7 @@ public class SvgGraphics {
this.document = getDocument(); this.document = getDocument();
this.backcolor = backcolor; this.backcolor = backcolor;
this.preserveAspectRatio = preserveAspectRatio; this.preserveAspectRatio = preserveAspectRatio;
this.INTERACTIVE = interactive;
ensureVisible(minDim.getWidth(), minDim.getHeight()); ensureVisible(minDim.getWidth(), minDim.getHeight());
this.root = getRootNode(); this.root = getRootNode();
@ -979,6 +980,7 @@ public class SvgGraphics {
return SignatureUtils.getMD5Hex(comment); return SignatureUtils.getMD5Hex(comment);
} }
public void addComment(String comment) { public void addComment(String comment) {
final String signature = getMD5Hex(comment); final String signature = getMD5Hex(comment);
comment = "MD5=[" + signature + "]\n" + comment; comment = "MD5=[" + signature + "]\n" + comment;
@ -986,6 +988,31 @@ public class SvgGraphics {
getG().appendChild(commentElement); 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) { public void openLink(String url, String title, String target) {
Objects.requireNonNull(url); Objects.requireNonNull(url);

View File

@ -159,7 +159,7 @@ public class FontChecker {
private String getSvgImage(char c) throws IOException, TransformerException { private String getSvgImage(char c) throws IOException, TransformerException {
final SvgGraphics svg = new SvgGraphics(null, true, new Dimension2DDouble(0, 0), 1.0, null, 42, "none", final SvgGraphics svg = new SvgGraphics(null, true, new Dimension2DDouble(0, 0), 1.0, null, 42, "none",
LengthAdjust.defaultValue(), DarkStrategy.IGNORE_DARK_COLOR); LengthAdjust.defaultValue(), DarkStrategy.IGNORE_DARK_COLOR, false);
svg.setStrokeColor("black"); svg.setStrokeColor("black");
svg.svgImage(getBufferedImage(c), 0, 0); svg.svgImage(getBufferedImage(c), 0, 0);
final ByteArrayOutputStream os = new ByteArrayOutputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream();

View File

@ -66,6 +66,7 @@ import net.sourceforge.plantuml.LineParam;
import net.sourceforge.plantuml.OptionFlags; import net.sourceforge.plantuml.OptionFlags;
import net.sourceforge.plantuml.Scale; import net.sourceforge.plantuml.Scale;
import net.sourceforge.plantuml.SvgCharSizeHack; import net.sourceforge.plantuml.SvgCharSizeHack;
import net.sourceforge.plantuml.Pragma;
import net.sourceforge.plantuml.TitledDiagram; import net.sourceforge.plantuml.TitledDiagram;
import net.sourceforge.plantuml.Url; import net.sourceforge.plantuml.Url;
import net.sourceforge.plantuml.UseStyle; import net.sourceforge.plantuml.UseStyle;
@ -272,7 +273,7 @@ public class ImageBuilder {
/ 96.0; / 96.0;
if (scaleFactor <= 0) if (scaleFactor <= 0)
throw new IllegalStateException("Bad scaleFactor"); throw new IllegalStateException("Bad scaleFactor");
UGraphic ug = createUGraphic(fileFormatOption, dim, animationArg, dx, dy, scaleFactor); UGraphic ug = createUGraphic(fileFormatOption, dim, animationArg, dx, dy, scaleFactor, titledDiagram !=null ? titledDiagram.getPragma() : new Pragma());
maybeDrawBorder(ug, dim); maybeDrawBorder(ug, dim);
if (randomPixel) { if (randomPixel) {
drawRandomPoint(ug); drawRandomPoint(ug);
@ -401,12 +402,18 @@ public class ImageBuilder {
} }
private UGraphic createUGraphic(FileFormatOption option, final Dimension2D dim, Animation animationArg, double dx, private UGraphic createUGraphic(FileFormatOption option, final Dimension2D dim, Animation animationArg, double dx,
double dy, double scaleFactor) { double dy, double scaleFactor, Pragma pragma) {
switch (option.getFileFormat()) { switch (option.getFileFormat()) {
case PNG: case PNG:
return createUGraphicPNG(scaleFactor, dim, animationArg, dx, dy, option.getWatermark()); return createUGraphicPNG(scaleFactor, dim, animationArg, dx, dy, option.getWatermark());
case SVG: case SVG:
return createUGraphicSVG(scaleFactor, dim); final boolean interactive;
if (!pragma.isDefine("svginteractive"))
interactive = false;
else {
interactive = Boolean.valueOf(pragma.getValue("svginteractive"));
}
return createUGraphicSVG(scaleFactor, dim, interactive);
case EPS: case EPS:
return new UGraphicEps(backcolor, colorMapper, stringBounder, EpsStrategy.getDefault2()); return new UGraphicEps(backcolor, colorMapper, stringBounder, EpsStrategy.getDefault2());
case EPS_TEXT: case EPS_TEXT:
@ -432,14 +439,14 @@ public class ImageBuilder {
} }
} }
private UGraphic createUGraphicSVG(double scaleFactor, Dimension2D dim) { private UGraphic createUGraphicSVG(double scaleFactor, Dimension2D dim, boolean interactive) {
final String hoverPathColorRGB = getHoverPathColorRGB(); final String hoverPathColorRGB = getHoverPathColorRGB();
final LengthAdjust lengthAdjust = skinParam == null ? LengthAdjust.defaultValue() : skinParam.getlengthAdjust(); final LengthAdjust lengthAdjust = skinParam == null ? LengthAdjust.defaultValue() : skinParam.getlengthAdjust();
final String preserveAspectRatio = getPreserveAspectRatio(); final String preserveAspectRatio = getPreserveAspectRatio();
final boolean svgDimensionStyle = skinParam == null || skinParam.svgDimensionStyle(); final boolean svgDimensionStyle = skinParam == null || skinParam.svgDimensionStyle();
final String svgLinkTarget = getSvgLinkTarget(); final String svgLinkTarget = getSvgLinkTarget();
final UGraphicSvg ug = new UGraphicSvg(backcolor, svgDimensionStyle, dim, colorMapper, false, scaleFactor, final UGraphicSvg ug = new UGraphicSvg(backcolor, svgDimensionStyle, dim, colorMapper, false, scaleFactor,
svgLinkTarget, hoverPathColorRGB, seed, preserveAspectRatio, stringBounder, lengthAdjust); svgLinkTarget, hoverPathColorRGB, seed, preserveAspectRatio, stringBounder, lengthAdjust, interactive);
return ug; return ug;
} }

View File

@ -70,6 +70,7 @@ public class UGraphicSvg extends AbstractUGraphic<SvgGraphics> implements ClipCo
private final boolean textAsPath2; private final boolean textAsPath2;
private final String target; private final String target;
private final boolean interactive;
public double dpiFactor() { public double dpiFactor() {
return 1; return 1;
@ -84,16 +85,17 @@ public class UGraphicSvg extends AbstractUGraphic<SvgGraphics> implements ClipCo
super(other); super(other);
this.textAsPath2 = other.textAsPath2; this.textAsPath2 = other.textAsPath2;
this.target = other.target; this.target = other.target;
this.interactive = other.interactive;
register(); register();
} }
public UGraphicSvg(HColor defaultBackground, boolean svgDimensionStyle, Dimension2D minDim, ColorMapper colorMapper, public UGraphicSvg(HColor defaultBackground, boolean svgDimensionStyle, Dimension2D minDim, ColorMapper colorMapper,
boolean textAsPath, double scale, String linkTarget, String hover, long seed, String preserveAspectRatio, boolean textAsPath, double scale, String linkTarget, String hover, long seed, String preserveAspectRatio,
StringBounder stringBounder, LengthAdjust lengthAdjust) { StringBounder stringBounder, LengthAdjust lengthAdjust, boolean interactive) {
this(defaultBackground, minDim, colorMapper, this(defaultBackground, minDim, colorMapper,
new SvgGraphics(colorMapper.toSvg(defaultBackground), svgDimensionStyle, minDim, scale, hover, seed, new SvgGraphics(colorMapper.toSvg(defaultBackground), svgDimensionStyle, minDim, scale, hover, seed,
preserveAspectRatio, lengthAdjust, DarkStrategy.IGNORE_DARK_COLOR), preserveAspectRatio, lengthAdjust, DarkStrategy.IGNORE_DARK_COLOR, interactive),
textAsPath, linkTarget, stringBounder); textAsPath, linkTarget, stringBounder, interactive);
if (defaultBackground instanceof HColorGradient) { if (defaultBackground instanceof HColorGradient) {
final SvgGraphics svg = getGraphicObject(); final SvgGraphics svg = getGraphicObject();
svg.paintBackcolorGradient(colorMapper, (HColorGradient) defaultBackground); svg.paintBackcolorGradient(colorMapper, (HColorGradient) defaultBackground);
@ -116,10 +118,11 @@ public class UGraphicSvg extends AbstractUGraphic<SvgGraphics> implements ClipCo
} }
private UGraphicSvg(HColor defaultBackground, Dimension2D minDim, ColorMapper colorMapper, SvgGraphics svg, private UGraphicSvg(HColor defaultBackground, Dimension2D minDim, ColorMapper colorMapper, SvgGraphics svg,
boolean textAsPath, String linkTarget, StringBounder stringBounder) { boolean textAsPath, String linkTarget, StringBounder stringBounder, boolean interactive) {
super(defaultBackground, colorMapper, stringBounder, svg); super(defaultBackground, colorMapper, stringBounder, svg);
this.textAsPath2 = textAsPath; this.textAsPath2 = textAsPath;
this.target = linkTarget; this.target = linkTarget;
this.interactive = interactive;
register(); register();
} }
@ -151,6 +154,14 @@ public class UGraphicSvg extends AbstractUGraphic<SvgGraphics> implements ClipCo
if (metadata != null) if (metadata != null)
getGraphicObject().addComment(metadata); getGraphicObject().addComment(metadata);
if (interactive) {
// 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); getGraphicObject().createXml(os);
} catch (TransformerException e) { } catch (TransformerException e) {
throw new IOException(e.toString()); throw new IOException(e.toString());

View File

@ -0,0 +1,6 @@
[data-mouse-over-selected="false"] {
opacity: 0.2;
}
[data-mouse-over-selected="true"] {
opacity: 1.0;
}

View File

@ -0,0 +1,84 @@
(function (){
/**
* @param {SVG.G} node
* @param {SVG.G} topG
* @return {{node: Set<SVG.G>, edges:Set<SVG.G>}}
*/
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<SVG.G>, edges:Set<SVG.G>}}
*/
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);
}
})();