Skip to content

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

Open
@iosonofabio

Description

@iosonofabio

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.

Metadata

Metadata

Assignees

Labels

plottingIssues related to plotting graphs in igraph in generaltodoTriaged for implementation in some unspecified future version

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions