Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph bounding box can’t be calculated #95

Closed
flying-sheep opened this issue Aug 25, 2021 · 9 comments
Closed

Graph bounding box can’t be calculated #95

flying-sheep opened this issue Aug 25, 2021 · 9 comments
Labels

Comments

@flying-sheep
Copy link
Contributor

flying-sheep commented Aug 25, 2021

Since text bounding boxes always contain y0 == -inf, nodes and by extension the whole graph will always have min(ya, -inf) == -inf there. (same for y1, which will always be inf)

return x - 0.5 * (1 + j) * w, -_inf, x + 0.5 * (1 - j) * w, _inf

ya, yb = min(ya, y0), max(yb, y1)

So how is this expected to work …?

@jrfonseca
Copy link
Owner

Yes, it seems the text bounding box is being overestimated. But I don't believe this is being used to calculate the overall bounding box.

This code was added by @iamahuman in 4b00a68 as part of PR #63. IIUC, this is used to speed up drawing (to skip drawing unnecessary elements), and an overestimation of the bounding box along the y-axis should merely mean slightly slower performance (text elements are drawn, even when they are above/below the visible area), not wrong graph bounding box.

The overall graph bounding box is determined by xdot output of dot itself, and is not affected by these values.

@flying-sheep
Copy link
Contributor Author

flying-sheep commented Sep 4, 2021

Graph.bounding is determined like this:

xdot.py/xdot/ui/elements.py

Lines 646 to 649 in a8715f6

self.bounding = Shape._envelope_bounds(
map(_get_bounding, self.shapes),
map(_get_bounding, self.nodes),
map(_get_bounding, self.edges))

and when the Graph is created by XDotParser.parse, its .shapes contains the Text shapes:

xdot.py/xdot/dot/parser.py

Lines 537 to 538 in a8715f6

return elements.Graph(self.width, self.height, self.shapes,
self.nodes, self.edges, self.outputorder)

Am I doing something wrong?


My goal is to use xdot as a library to get the shapes, and then render them into a static canvas (a matplotlib Axes object). I chose xdot as it is a nice pythonic wrapper around a GraphViz graph and its layouting information (shapes, sizes, positions, pens).

Actually xdot is not ideal for this as it depends on GTK while I only need the parts that don’t. Would you be open to split it into two libraries? xdot_core (or so) would contain everything except for the GTK parts, while xdot would depend on xdot_core and contain the UI and Cairo rendering parts.

If yes, we should think about how the rendering works. I think it would make sense to e.g. have a class Renderer(abc.ABC) abstract base class that has a render_<shape> abstract method for each shape’s _draw function. xdot would then contain a class CairoGTKRenderer(Renderer) and I would create a class MatplotlibAxesRenderer(Renderer).

For backwards compat the xdot_core.Graph’s draw method would import xdot inline, instantiate a CairoGTKRenderer and call it’s render_graph method or so.

@iamahuman
Copy link
Contributor

iamahuman commented Sep 4, 2021

You're right. I wasn't able to figure out how to calculate the text height at that time, so I just went for the most conservative option. This was because the bounds were only added as a means to enhance rendering performance, and It was an attempt to prioritize correctness (or at least, not introducing regression) over speed.

@iamahuman
Copy link
Contributor

My goal is to use xdot as a library to get the shapes, and then render them into a static canvas (a matplotlib Axes object). I chose xdot as it is a nice pythonic wrapper around a GraphViz graph and its layouting information (shapes, sizes, positions, pens).

Actually xdot is not ideal for this as it depends on GTK while I only need the parts that don’t. Would you be open to split it into two libraries? xdot_core (or so) would contain everything except for the GTK parts, while xdot would depend on xdot_core and contain the UI and Cairo rendering parts.

If yes, we should think about how the rendering works. I think it would make sense to e.g. have a class Renderer(abc.ABC) abstract base class that has a render_<shape> abstract method for each shape’s _draw function. xdot would then contain a class CairoGTKRenderer(Renderer) and I would create a class MatplotlibAxesRenderer(Renderer).

For backwards compat the xdot_core.Graph’s draw method would import xdot inline, instantiate a CairoGTKRenderer and call it’s render_graph method or so.

I suppose GraphViz itself fits in general for non-interactive use cases.

@jrfonseca
Copy link
Owner

My point was that the bounding property was (and is) not suited for getting accurate bounding boxes. Instead it was designed for a conservative approximation.

For its current use, bounding is perfectly suited. For your use, @flying-sheep , I admit it might not be suited.

You could try to fix it, so bounding is accurate, and never an approximatiom. No objectsion.

Also note that xdot already includes the graph bounding box too:

$ dot -T xdot tests/chars.dot 
digraph G {
	graph [_draw_="c 9 -#fffffe00 C 7 -#ffffff P 4 0 0 0 53.74 342 53.74 342 0 ",
		bb="0,0,342,53.74",
		xdotversion=1.7
	];

Note the bb attribute. Maybe you can use that instead.

Actually xdot is not ideal for this as it depends on GTK while I only need the parts that don’t. Would you be open to split it into two libraries?

I don't oppose in principle. I think there was even a Qt port of it at some point. See #9

But as I explained in https://github.com/jrfonseca/xdot.py#status I barely have time to do anything . I spent a couple hours this morning looking at PRs for the first time in 4-5 months.

In other words: if you want to submit a PR that addresses your needs, and doesn't gives me extra work, I'm for it. Anything that requires my involvement other than reviewing and accepting a PR is unlikely to happen, even if its a good idea.

It's a pity nobody else stepped up to help maintain this. Maybe I did something wrong along the way. But the fact is that sooner or later my other life commitments will force me to completely halt this and other small pet OSS projects of mine completely. If the projects remain useful hopefully they will live as some fork.

@iamahuman
Copy link
Contributor

iamahuman commented Sep 4, 2021

Graph.bounding is determined like this:

xdot.py/xdot/ui/elements.py

Lines 646 to 649 in a8715f6

self.bounding = Shape._envelope_bounds(
map(_get_bounding, self.shapes),
map(_get_bounding, self.nodes),
map(_get_bounding, self.edges))

and when the Graph is created by XDotParser.parse, its .shapes contains the Text shapes:

xdot.py/xdot/dot/parser.py

Lines 537 to 538 in a8715f6

return elements.Graph(self.width, self.height, self.shapes,
self.nodes, self.edges, self.outputorder)

Am I doing something wrong?

Your analysis is correct; any subgraphs containing text nodes will result in their own bounding boxes being overestimated as well. The rendering engine could therefore unnecessarily traverse the (otherwise invisible) graph; however, its child shapes are still tested according to their own bounding boxes, which means some performance benefit is kept.

@iamahuman
Copy link
Contributor

It's a pity nobody else stepped up to help maintain this. Maybe I did something wrong along the way. But the fact is that sooner or later my other life commitments will force me to completely halt this and other small pet OSS projects of mine completely. If the projects remain useful hopefully they will live as some fork.

Three options that come to my mind:

  • Hype it on somewhere like HN. (perhaps already done?)
  • Transfer it to @jazzband, a community of open-source maintainers and a home for some widely-used Python projects.
  • Perhaps I could take the role, but I too can't tell for sure whether I could procure enough time to faithfully carry out the maintenance work, especially given my current state of affairs.

@flying-sheep
Copy link
Contributor Author

there’s also https://adoptoposs.org and some other GitHub org I forgot …

Anything that requires my involvement other than reviewing and accepting a PR is unlikely to happen, even if its a good idea.

yeah, doing releases and so on is also part of it so yeah, we should probably involve one of the maintainer orgs.

@jrfonseca
Copy link
Owner

@iamahuman , @flying-sheep , thanks for your suggestions for maintenance alternatives. I'll look into those.

@flying-sheep, I think I answered your questions. Closing now. Feel free to comment if there's anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants