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:
commit
db67c75919
@ -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();
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
6
svg/onmouseinteractivefooter.css
Normal file
6
svg/onmouseinteractivefooter.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[data-mouse-over-selected="false"] {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
[data-mouse-over-selected="true"] {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
84
svg/onmouseinteractivefooter.js
Normal file
84
svg/onmouseinteractivefooter.js
Normal 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);
|
||||||
|
}
|
||||||
|
})();
|
Loading…
Reference in New Issue
Block a user