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

easy colorbar plotting #437

Closed
maximelucas opened this issue Jul 27, 2023 · 3 comments · Fixed by #441 or #456
Closed

easy colorbar plotting #437

maximelucas opened this issue Jul 27, 2023 · 3 comments · Fixed by #441 or #456
Labels
new feature New feature or request viz

Comments

@maximelucas
Copy link
Collaborator

Add an easy way to plot a colorbar when coloring nodes by a stat.
Right now, in networkx is pretty convoluted to do it, see: https://stackoverflow.com/questions/26739248/how-to-add-a-simple-colorbar-to-a-network-graph-plot-in-python

@maximelucas maximelucas added new feature New feature or request viz labels Jul 27, 2023
@maximelucas
Copy link
Collaborator Author

A bit of theory after some digging to see what's the natural of doing this:

  • a simple plt.colorbar() does not work in networkx because nx.draw() returns None.
  • a simple plt.colorbar(ax=ax) does not work in xgi because xgi.draw() returns an ax but not a mappable (which is needed for a colobar, and is often an AxesImage). For example, plt.imshow() returns an AxesImage, not an axis.
  • Importantly for us, a Collection is a mappable. This includes the output of plt.scatter() (a PathCollection) which we use in xgi.draw_nodes().
  • I think currently our dyads (and hyperedges) are plotted as single Line2D (and Polygon) elements, which are not mappables, but could be made collections to be mappable?

How much do we want from this implementation?

  • Currently, we have 4 cmaps that can be set in xgi.draw(), and each of them could have its own colobar: node_fc_cmap, node_ec_cmap, dyad_color_cmap, edge_fc_cmap.
  • In each case, it would be nice to be able to set a vmin and vmax arguments, e.g. to have a single colorar for different plots that have different mins and maxs (see first arguments of imshow).
  • For comparison, networkx seems to have 2 cmaps (cmap and edge_cmap) with each their own vmin/vmax (vmin,vmax and edge_vmin,edge_vmax).

Potential implementations for the node colours:

  1. Based on stackoverflow, we can do

    H = xgi.random_hypergraph(10, ps=[0.6, 0.05, 0.001], seed=3)
    # --> degrees are [7, 7, 8, 8, 7, 4, 7, 5, 8, 10]
    fig, ax = plt.subplots()
    
    pos = xgi.circular_layout(H)
    cmap = plt.cm.Greens
    ax = xgi.draw(H, pos=pos, ax=ax, node_fc=H.nodes.degree, node_fc_cmap=cmap)
    
    # new code to allow a colorbar
    vmin = 0
    vmax = 10
    sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin = vmin, vmax=vmax))
    plt.colorbar(sm, ax=ax)

    This almost works. The problem is that changing vmax to say a 100 makes the output wrong: node colours are unchanged, but the colorbar now goes until 100, making it look like some nodes have a degree close to 100. This is because xgi.draw() internally compute its own vmax, not aware of the external one.

  2. Using the natural output of plt.scatter. Here is an MWE:

    # create fake nodes and colours
    colors = [7, 7, 8, 8, 7, 4, 7, 5, 8, 10] # degrees from above
    N = len(colors)
    theta = np.linspace(0, 2*np.pi, num=N, endpoint=False)
    x_coords = np.cos(theta)
    y_coords = np.sin(theta)
    
    fig, ax = plt.subplots()
    
    # plot nodes with colors and cmap
    vmin = min(colors)
    vmax = max(colors)
    sc = ax.scatter(x_coords, y_coords, c=colors, cmap="Greens", marker="o", s=100, ec="k", vmin=vmin, vmax=vmax)
    
    # colorbar
    plt.colorbar(sc) # this works because sc is a PathCollection which is a mappable

    Everything works consistently here (changing cmap, vmin, vmax, etc.).

    Screenshot 2023-08-01 at 18 39 29

Solution 2 seems the most natural to me. I've tried this by making scatter and then draw return the mappable. The relative node colours are correct, but there are few problems with respect to our current implementation:

  • the scale of the colorbar is wrong (always in [0, 1])
  • the colorbar always uses the default cmap. But when we specify a different one in xgi.draw(), the nodes use a different one.

Visually, running

fig, ax = plt.subplots()
pos = xgi.circular_layout(H)

ax, im = xgi.draw(H, pos=pos, ax=ax, node_fc=H.nodes.degree, node_fc_cmap=plt.cm.Greens)
plt.colorbar(im)

yields

Screenshot 2023-08-01 at 18 40 15

This is because solution 2 requires to pass cmap and vmin vmax as argument directly to plt.scatter. Right now, the cmap (and its vmin/vmax) is handled in node_fc = _color_arg_to_dict(node_fc, ..., settings["node_fc_cmap"]), and then only the colors are passed to scatter.

So my question is: are we not over-complicating things with _color_arg_to_dict? I feel like we could use the built-in full power of scatter to handle all this. What extra cases are handling that scatter would not, and do we need them?

@maximelucas
Copy link
Collaborator Author

I've started a draft PR #441 so you can see what I tried.

@maximelucas
Copy link
Collaborator Author

Update: networkx seems to only be using matplotlib.scatter directly to handle all this, at least for the node colours.

And nx.draw() returns None, but nx.draw_networkx_nodes() returns a collection so that the following works:

G = nx.gnp_random_graph(n=10, p=0.3)
im = nx.draw_networkx_nodes(G, node_color=np.random.random(len(G)), pos=nx.circular_layout(G))
plt.colorbar(im)

This also allows to use cmap names instead of the actual object: so far for us only cmap=plt.cm.Reds works, but plt.scatter also accepts cmap="Reds".

@maximelucas maximelucas linked a pull request Aug 18, 2023 that will close this issue
@maximelucas maximelucas reopened this Aug 25, 2023
@maximelucas maximelucas linked a pull request Oct 16, 2023 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new feature New feature or request viz
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant