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

matplotlib aspect ratio of vertices (e.g. circles) #665

Open
iosonofabio opened this issue May 8, 2023 · 5 comments
Open

matplotlib aspect ratio of vertices (e.g. circles) #665

iosonofabio opened this issue May 8, 2023 · 5 comments
Assignees
Labels
plotting Issues related to plotting graphs in igraph in general todo Triaged for implementation in some unspecified future version

Comments

@iosonofabio
Copy link
Member

Taken from #638 where @alex180500 requests matplotlib plots where vertices (e.g. circles) do not become ellipses upon stretching the x-axis. Here is his example, which reproduces on my machine:

import igraph as ig
import matplotlib.pyplot as plt

g = ig.Graph.Lattice([4, 4], circular=False)
g.es["label"] = [edge.tuple for edge in g.es]
g.es["label_size"] = 8
g.vs["label"] = g.vs.indices
g.vs["label_size"] = 8

# matplotlib
fig, ax = plt.subplots(figsize=(8, 8), dpi=100)
ig.plot(g, target=ax, layout="grid", vertex_size=0.2)
ax.set_aspect(0.5)

The question from the user is whether we can make the matplotlib plot look closer to how it looks in Cairo.

Now the issue is that matplotlib has two ways of drawing e.g. circles:

  • matplotlib.patches.Circle: currently used, uses consistent transform for location and radius - transData
  • ax.scatter: apart from the weird square-root radius quirk, it uses transData for location but other units (dots?) for radius (they call it s)

The current choice works in such a way that when you zoom in the circles become bigger. That is what you would get by literally using a loupe on a raster image with the plot - i.e. what you get with Cairo + manual zoom. However, because the circle is defined in data units, it really dislikes changes of aspect ratio (e.g. zoom in only one axis), in which cases the circles become ellipses. Notice that in Cairo's result (e.g. PNG), if you zoomed in only horizontally you would get the same artifact.

The alternative would be friendly to changes of aspect ratios, but the dot size is independent of zoom, i.e. the radius is fixed in dots/pixels.

The short answer is that unless we create a customized third way of making circles in matplotlib (and triangles, etc.) which is currently out of question because of time constraints, we cannot reproduce Cairo's behaviour exactly within matplotlib.

Proposed solutions
One way to reproduce something close to (but not exactly equal to) Cairo would be to rescale manually the markers for x and y axis separately (e.g. circles would become ellipses). We would need to:

  1. compute the rough x/y limits on the plot based on the vertex layout coordinates, and get the data aspect ratio (e.g. if xlim=(-2, 2) and ylim=(-1, 9), the ratio is 2/5)
  2. compute the axes size in pixels and get that aspect ratio (e.g. if the axes is 300px wide and 100px tall, it would be 3/1)
  3. combine them to compute the composite aspect ratio (e.g. 5/2 * 3/1 = 15/2)
  4. construct artists (e.g. circles -> ellipses) that use data coordinates, but skew the offsets from the center of the shape differently for x and y. In the above example, we would make the ellipsis look like an egg in data coordinates, e.g. the height is 0.15 but the width is 0.02. This way when the ellipsis gets squashed by the horizontal rectangle and then squashed again because the same pixel covers more mileage in y-data coordinates, it ends up roughly round.
  5. We then set the aspect ratio of the Axes and autoscale_view, as we currently do.

The main issue with this - in addition to the fact it's hacky - is that as soon as the user starts changing the aspect ratio or zooming around, it will all fall apart. The other problem is that an Axes does not really have a fixed number of pixels - a Figure does, but our Axes could be a subplot and the padding between subplots can be adjusted post-facto: all of that would change the aspect ratio, sometimes slightly, sometimes not so much, and circles are not circles anymore.

The other way to solve this would be to switch to scale-free ax.scatter for our vertices. That could be done but because we allow per-vertex setting of shapes, we would need to create a PolyCollection for each vertex. Not a problem, just hacky. We would also need to undo the square-root size thing like seaborn has done long ago. Finally, we would be constrained in terms of shapes to the ones covered by ax.scatter, listed in matplotlib.markers. Tbh, I'm having a look right now and there's everything we need there including custom vertices and paths.

Next steps
If we implement this, especially solution 2, this will change quite significantly the way matplotlib + igraph plots behave. I'm generally in favour but it'd be best to hear a few people's feedback including @tacaswell if possible. I did notice that the old project grave (networkx + matplotlib container artist) faced the exact same issue and there did not appear to be a straight solution there.

@iosonofabio iosonofabio added the plotting Issues related to plotting graphs in igraph in general label May 8, 2023
@iosonofabio iosonofabio self-assigned this May 8, 2023
@szhorvat
Copy link
Member

szhorvat commented May 8, 2023

Regarding the stretching of plots: IMO the right approach here is to just use the natural aspect ratio, and simply not stretch. Unfortunately, matplotlib stretches by default, and requires an extra setting to avoid this. People often skip this, which is why so many plots I see in papers are just slightly off, circles being just slightly oval ...

With almost all graph layouts, the horizontal and vertical coordinates are comparable (i.e. effectively have the same unit). Thus stretching will mess up not only the node rendering, but also the layout. Ideally, the default would be a natural aspect ratio. If someone truly needs to stretch the layout, which should be rare, they can still easily rescale the horizontal coordinates while leaving the vertical ones intact.


All that said, the feature to keep the aspect ratio of nodes even the plot aspect ratio is changed is not a bad one.

But please do consider what tradeoffs need to be made to implement this feature. If there's no tradeoff, go for it. However, if there's a drawback, or if it constrains what we can do in the future, then perhaps it's better not to do it.

@ntamas
Copy link
Member

ntamas commented May 8, 2023

Looking at the two proposed solutions, I'm definitely in favour of option 2 instead of option 1. But, I was wondering whether this closes the door for other directions, like adding images, pie charts or other custom artists as nodes.

@stale
Copy link

stale bot commented Sep 18, 2023

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Sep 18, 2023
@ntamas ntamas added todo Triaged for implementation in some unspecified future version and removed stale labels Sep 18, 2023
@alex180500
Copy link

Maybe another answer could be to use a PathCollection instead but I'm not an expert in matplotlib...

@iosonofabio
Copy link
Member Author

Thank you, this is all done in the develop branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plotting Issues related to plotting graphs in igraph in general todo Triaged for implementation in some unspecified future version
Projects
None yet
Development

No branches or pull requests

4 participants