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

Colorbar label position different when executing a block of code #6471

Closed
saintsfan342000 opened this issue May 24, 2016 · 9 comments
Closed

Comments

@saintsfan342000
Copy link

saintsfan342000 commented May 24, 2016

Matplotlib version 1.5.1
Python 3.4.4 on Windows via Anaconda 2.2.0 (64-bit)
Also occured on a Python 3.5.1 install on Lubuntu via Anaconda 4.0
Jupyter QtConsole 4.1.0; IPython verion 4.0.0

I made some functions for positioning and formatting x and y labels and to draw arrows pointing towards these labels. Today I wrote a function that similarly formats the colorbar label. However, the behavior is erroneous if the function is called within a block of code or in a script executed on the command line. I defined a condensed version of the function here as mycolorbar().

If I run the following in a script from the command line, or if I paste everything into a single code block in the Jupyter QtConsole, I get the figure shown below the code. In the QtConsole this occurs regardless of whether I've called %matplotlib

import matplotlib.pyplot as plt
import numpy as np

def mycolorbar(ax,cbar):
    fig = ax.get_figure()
    cax = cbar.ax
    inv = ax.transAxes.inverted()
    R = fig.canvas.get_renderer()
    caxY = cax.yaxis
    ylab = caxY.get_label()
    ticklabelbbox = inv.transform( caxY.get_ticklabel_extents(R)[1].get_points() )
    ylab.set_verticalalignment('center')
    ylab.set_horizontalalignment('center')
    ylab.set_rotation(0)
    xpos, ypos = np.max(ticklabelbbox[:,0]), (3/4)
    caxY.set_label_coords( xpos , ypos , transform = ax.transAxes )

x,y,z = [np.random.rand(1000) for i in range(3)]

fig = plt.figure(facecolor='w')
ax = fig.add_axes([.1,.1,.8,.8])
plt.tricontourf(x,y,z)
plt.xlabel('xlab')
plt.ylabel('ylab')
cbar = plt.colorbar(label='clab')
mycolorbar( ax, cbar)
plt.show()

farout

Observe how far out to the right the colorbar label are. This is "incorrect".

Now, when working interactively in the QtConsole, if I run all of the above code _except_ the last line, let the figure window render, then execute that last line mycolorbar( ax, cbar) on its own, I get the "correct" position for the colorbar label.

closein

I'm asking anyone to debug my code. Just trying to bring a potential issue to light. Thank you.

@saintsfan342000
Copy link
Author

saintsfan342000 commented May 24, 2016

Some further digging:
If I add one line to mycolorbar such that it returns the colorbar labels's final x-position xpos, I get 1.33348253038 when running the whole code block. But if I work interactively by first calling %matplotlib, run everything other than the call to mycolorbar, then call mycolorbar on its own, it returns 1.19285753038. xpos is the right-hand boundary of the colorbar's ticklabels' bounding box in the coordinate system of the main axes .

def mycolorbar(ax,cbar):
    fig = ax.get_figure()
    cax = cbar.ax
    inv = ax.transAxes.inverted()
    R = fig.canvas.get_renderer()
    caxY = cax.yaxis
    ylab = caxY.get_label()
    ticklabelbbox = inv.transform( caxY.get_ticklabel_extents(R)[1].get_points() )
    ylab.set_verticalalignment('center')
    ylab.set_horizontalalignment('center')
    ylab.set_rotation(0)
    xpos, ypos = np.max(ticklabelbbox[:,0]), (3/4)
    caxY.set_label_coords( xpos , ypos , transform = ax.transAxes )
    return xpos

@tacaswell
Copy link
Member

The source of the problem is that you are capturing screen-space position which are figure size / dpi dependent. When the dpi changes (and the default save dpi is currently different than the default screen dpi) your coordinates do not get updated and are wrong.

What exactly are you trying to do that the provide colorbar label does not do?

You probably should be using annotate http://matplotlib.org/users/annotations_guide.html#using-complex-coordinate-with-annotation instead of working directly with transforms.

@saintsfan342000
Copy link
Author

Thanks for the response.

  1. This discrepancy occurs in the figure window that displays, not just when I save the image. Regardless of whether I call savefig. The images I uploaded are exactly what I see in the figure window.
  2. In the interest of succinctness I did not post my entire function. It is here on Github within the larger module figfun. I am trying to satisfy my grad-school adivsor's ridiculous plotting standards, which involves specific orientations and positioning of the axis labels, as well as drawing arrows parallel to the axis that point towards the label. The approach I took was to find the ticklabel bounding boxes and position the axis labels accordingly. This was especially important for the y axis since setting rotation=0 lead to overlap between the axis and tick labels, and I didn't want to have to guess a labelpad every time. The only arrow arrow in the matplotlib arsenal that I can make look like the arrow I need is the FancyArrow, so I couldn't use ax.annotate. There's a tutorial in the repository that demonstrates the use. The issue that has arisen here with the colorbar is the first time I've seen a breakdown in this method. As seen in the tutorial it works perfectly for formatting the x and y axis, and has never been problematic when saving to various formats.

If you were to download and import figfun, you could run the following code in the two scenarios I described above. You'll see that x and y labels format just fine. Only the colorbar label has this peculiar behavior where its position differs depending on whether f.colorbar is called within a larger code block or when called on its own in interactive mode.

import matplotlib.pyplot as plt
import numpy as np
import figfun as f

x,y,z = [np.random.rand(1000) for i in range(3)]

fig = plt.figure(facecolor='w')
ax = fig.add_axes([.1,.1,.8,.8])
plt.tricontourf(x,y,z)
plt.xlabel('xlab')
plt.ylabel('ylab')
cbar = plt.colorbar(label='clab')
f.myax( fig )
f.colorbar( ax, cbar)

plt.show() 

@efiring
Copy link
Member

efiring commented May 26, 2016

Without looking at this in detail, I suspect the problem is related to your use of the colorbar convenience functionality: its ability to resize an Axes and then put itself in the liberated space. This is great for quick plotting and interactive use, but for any finely-tuned plots for publication, it is often better to explicitly size and position all of your Axes, including one that you make to hold the colorbar (via the cax kwarg).
In addition, to avoid surprises, use identical values for figure.dpi and for savefig.dpi, and make sure that the value is small enough so that the entire figure can be displayed on your screen without resizing. Set that figsize when you make the Figure, and don't change it.
To make it easier to lay out Axes in a Figure by specifying their positions in inches, you can use something like this:

def axes_inches(fig, rect, **kw):
    """
    Wrapper for Figure.add_axes in which *rect* is given in inches.
    The translation to normalized coordinates is done immediately
    based on the present figsize.
    *rect* is left, bottom, width, height in inches
    *kw* are passed to Figure.add_axes
    """
    fw = fig.get_figwidth()
    fh = fig.get_figheight()
    l, b, w, h = rect
    relrect = [l / fw, b / fh, w / fw, h / fh]
    ax = fig.add_axes(relrect, **kw)
    return ax

@efiring
Copy link
Member

efiring commented May 26, 2016

On 2016/05/26 8:06 AM, Russle wrote:

The only arrow arrow in the matplotlib arsenal that I can make look like
the arrow I need is the FancyArrow, so I couldn't use |ax.annotate|.

Annotate can make FancyArrow instances. See
http://matplotlib.org/1.5.1/examples/pylab_examples/annotation_demo2.html.

@QuLogic
Copy link
Member

QuLogic commented May 26, 2016

This was especially important for the y axis since setting rotation=0 lead to overlap between the axis and tick labels, and I didn't want to have to guess a labelpad every time.

I think what you want to do there is set the horizontalalignment and verticalalignment differently so that the text is anchored in the right place, and then you don't have to mess with labelpad.

@saintsfan342000
Copy link
Author

Sincerely appreciate everyone's feedback!

@efiring

Without looking at this in detail, I suspect the problem is related to your use of the colorbar convenience functionality

Does that mean you were able to duplicate what I was seeing?

I was under the impression that the "fancy" associated with ax.annotate was not the same as the FancyArrow, mainly because this page, about one-third of the way down, lists only head_length, head_width, tail_width as the attributes that can be passed, and everytime I tried to pass, say, overhang I got an attribute error (perhaps the arrowprops dict isn't the right place to pass it?).

@QuLogic
I had figured out the correct settings for those arguments:

fig2 = plt.figure(2)
ax2 = fig2.add_subplot(111)
ylab = plt.ylabel('ylabel')
ylab.set_rotation(0)
ylab.set_va('bottom')
ylab.set_ha('right')
ylab.set_y(3/4)
plt.draw()

But what I couldn't make sense of after this was that ylab.get_position() returns: (48.75, 0.75). I couldn't figure out what coordinate system these numbers were specified in (looks like y in is axes, but x I wasn't sure).

Ultimately I thought that the most robust (thought perhaps not the smartest or most convenient) method for positioning all these different pieces was to just find their bboxes and work within one coordinate system.


In any case, I thank you all for the assistance and consideration. I moved all of my research codes over to Python about a year ago, and working with Matplotlib is awesome.

@efiring
Copy link
Member

efiring commented May 27, 2016

On 2016/05/26 5:30 PM, Russle wrote:

Sincerely appreciate everyone's feedback!

@efiring https://github.com/efiring

Without looking at this in detail, I suspect the problem is related
to your use of the colorbar convenience functionality

Does that mean you were able to duplicate what I was seeing?

No, it meant I was lazy and just thought about how things might go
haywire instead of trying your example and looking closely.

I was under the impression that the |"fancy"| associated with
|ax.annotate| was not the same as the |FancyArrow|, mainly because this
page
http://matplotlib.org/users/annotations_guide.html#plotting-guide-annotation,
about one-third of the way down, lists only |head_length, head_width,
tail_width| as the attributes that can be passed, and everytime I tried
to pass, say, |overhang| I got an attribute error (perhaps the
|arrowprops| dict isn't the right place to pass it?).

You are correct. I was missing the distinction between FancyArrow and
FancyArrowPatch. The former is more configurable; the latter is what
Annotation uses.

@efiring
Copy link
Member

efiring commented Jul 15, 2016

I'm closing this, as the discussion has died down and I don't think it leads to a clear specification of something we can fix in matplotlib. If this is incorrect--if there is a bug, or a hole in the documentation, or a needed feature--please open a new issue or (better) a PR with a fix. Or, if I have misunderstood the situation, reopen the present issue.

@efiring efiring closed this as completed Jul 15, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants