Part of the graph outside of the image in SVG because of default DPI attribute value
Hi and thanks for this sweet library.
I noticed part of graphs usually end-up outside of the image when generating a SVG (generating in PNG works fine).
Reproduction with GraphViz version 12.1.2-1 on Arch Linux which must be the one used as I haven't installed the maven dependency.
(gv/render-graph
{:nodes ["a"]}
{:filename "/tmp/out.svg"})
Result:
The content of the SVG – notice how the <svg width="83pt" height="72pt" … don't match the viewBox="0.00 0.00 62.00 53.74" …:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Generated by graphviz version 12.1.2 (0)
-->
<!-- Pages: 1 -->
<svg width="83pt" height="72pt"
viewBox="0.00 0.00 62.00 53.74" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1.33333 1.33333) rotate(0) translate(4 49.74)">
<polygon fill="white" stroke="none" points="-4,4 -4,-49.74 58,-49.74 58,4 -4,4"/>
<!-- a -->
<g id="node1" class="node">
<title>a</title>
<ellipse fill="none" stroke="black" cx="27" cy="-22.87" rx="27" ry="22.87"/>
<text text-anchor="middle" x="27" y="-17.82" font-family="Times,serif" font-size="14.00">a</text>
</g>
</g>
</svg>
Manually setting viewBox="0.00 0.00 83.00 72.00" … in the SVG file produces the expected result:
In my code example, I'm able to fix this by setting the dpi to "":
(gv/render-graph
{:default-attributes {:graph {:dpi ""}}
:nodes ["a"]}
{:filename "/tmp/out.svg"})
For the record, I notice that GraphViz attribute dpi is supposed to be a Double rather than a String.
Is this expected behavior or maybe there's something I'm doing wrong?
In a broader way: I was a bit surprised to see the user passed default-attributes being merged on top of gspec/default-attributes – which in the end, is the culprit of my problem. Wouldn't it be more straightforward to not merge with gspec/default-attributes and let user get the result of generation on their default-attributes? After all, digraph G { "a" } uses no default – unless I'm mistaken – and works fine.
Thanks for the detailed report!
It seems like part of the problem is that for some output targets, the dpi attribute is ignored and 72dpi is used (ie. 96 dpi is not actually the default sometimes):
https://gitlab.com/graphviz/graphviz/-/issues/1649 https://gitlab.com/graphviz/graphviz/-/issues/2597
A workaround is to set the default dpi to 72:
(gv/render-graph
{:nodes ["a"]
:default-attributes {:graph {:dpi "72"}}}
{:filename "out.svg"})
In a broader way: I was a bit surprised to see the user passed default-attributes being merged on top of gspec/default-attributes – which in the end, is the culprit of my problem. Wouldn't it be more straightforward to not merge with gspec/default-attributes and let user get the result of generation on their default-attributes? After all, digraph G { "a" } uses no default – unless I'm mistaken – and works fine.
The reason default attributes are set is because all attributes set on nodes must have default values set or they are ignored. This is a requirement that comes from the underlying c API.
For example:
(gv/render-graph
{:nodes [{:id "red"
:color "red"}
{:id "green"
:color "green"}]})
Without default attributes set, the :color attribute for both nodes would be ignored. This is true even for cases where every node sets a value for an attribute and the default value is effectively ignored. This wasn't my idea. It's just how the c API works.
For the record, I notice that GraphViz attribute dpi is supposed to be a Double rather than a String.
This is also another quirk from the underlying c API. All values are passed as strings, which the graphviz library parses based on the type.
Generally speaking, I try to keep the clj-graphviz library fairly thin so that graphviz examples can be ported straightforwardly. I try to avoid including too much logic or cleverness in the wrapper to avoid the case where someone ports a graphviz example, it doesn't work, and the reason is because of an arbitrary decision made by clj-graphviz about how things "should" work. I wouldn't be opposed to adding some coercion for obvious cases, but I just want to make sure that it doesn't get in the way of anyone who is trying to port complicated examples. I'm also open to the idea of an even higher level API that tries harder to make the API more clojure friendly and is less concerned about matching the behavior of the graphviz cli.
There's a couple changes that might help with this particular issue. I don't think all of these options are good, but I'll list all of them for the sake of comparison.
- No code changes. Update documentation to include guidance.
- Don't merge gspec/default-attributes. This is a breaking change and makes setting attributes that have defaults more annoying.
- Potentially, defaults could be set on demand only for attributes that are specified somewhere in the graph. Ideally, this would make clj-graphviz's behavior more closely match graphviz's cli behavior. More investigation would be needed to make sure this wouldn't cause any regressions or cause problems only relevant to the c API.
- Omit the dpi attribute from the defaults.
- Omit all graph attributes from the defaults.
- Change the dpi attribute default to 72.
I'm leaning towards option 3 or 5, but I'd like to think about it a bit more. It's not totally clear what the practical differences are between 3 and 5.
Thanks for the swift answer.
Ok : it's good to know the origins of the default and format of the attributes.
I agree on the philosophy of keeping libs as thin (transparent) as possible.
Obviously solution 2. could be problematic re breaking changes.
I don't know enough about the underlying C API to comment on 4., 5. nor 6. thought they all look like good low hanging fruit solutions to me.
In any case, the workaround of setting :dpi "72" is enough in my case. Feel free to close this report if you wish to.
Thanks again ! 😃