Skip to content

savefig() renders paths and text differently than show() #786

Closed
karmel opened this Issue Mar 21, 2012 · 28 comments

9 participants

@karmel
karmel commented Mar 21, 2012

After customizing a number of parameters in my matplotlibrc file, I was able to generate figures as desired in with pyplot.show(). However, using pyplot.savefig() with the same parameters resulted in images with thicker lines and bigger fonts, even when the dpi was the same for the default figure and the saved image.

This issue was described by another user here: http://stackoverflow.com/questions/7906365/matplotlib-savefig-plots-different-from-show/9814313

I was able to find a solution for myself as described here: http://stackoverflow.com/questions/7906365/matplotlib-savefig-plots-different-from-show/9814313#9814313

But, it would be really nice if a fix were built in for all the rendering backends, or, alternately, there was at least an easy way to scale paths and fonts so that savefig() and show() resulted in similar images.

@mdboom
Matplotlib Developers member
mdboom commented Mar 21, 2012

I can't reproduce the problem if the dpi's are the same -- I get identical images. Try this:

from matplotlib.pyplot import *
plot([1,2,3])
savefig("test.png", dpi=gcf().dpi)
show()

If you see differences, can you please post both the test.png and a screenshot of the window?

Which backend are you using? I suspect it's one of the non-Agg ones, or the changes you propose would have effect in both the screen and file output.

@karmel
karmel commented Mar 22, 2012

Interesting. That does produce the same image for me. So, I was able to track down the source of the problem, which seems to be having figure.dpi is set to a non-default value in my matplotlibrc file.

So, if I create a matplotlibrc file from the one posted here: http://matplotlib.sourceforge.net/users/customizing.html

And at line 280 set figure.dpi = 160, then the resultant savefig() and show() outputs are different. You can see the difference here: http://i.imgur.com/Ap8NX.png , with the saved image on top of the show() image.

That's promising, because switching that setting back is likely an easier fix than the one I implemented. Is that desired behavior, though? Why would that happen?

Thanks!

@pelson
Matplotlib Developers member
pelson commented Sep 2, 2012

To either @karmel or @mdboom : I think it would be helpful at this point to clarify what each of you thinks the action for this issue now is.

Thanks,

@efiring
Matplotlib Developers member
efiring commented Sep 3, 2012

@karmel is pointing out a genuine bug that I can reproduce with the macosx backend, but not with *agg backends--which is why saving to a png works.

import matplotlib
matplotlib.use("macosx")
matplotlib.rcParams['figure.dpi'] = 240
import matplotlib.pyplot as plt

fig, ax = plt.subplots(1, figsize=(3,2)) # 2 inches high
ax.plot([1,2,3], lw=72) # line 1 inch wide
fig.savefig("testdpi.png", dpi=240)
plt.show()

This should draw a diagonal line of width half the height of the whole figure. With agg it does; with macosx it does not.

@mdboom
Matplotlib Developers member
mdboom commented Sep 4, 2012

Can anyone on a mac look into why this is happening?

@WeatherGod
Matplotlib Developers member

I wonder if this is related to another bug I encountered with a friend who uses a Mac. In that case, the graphics context was handled differently in the macosx and the agg backends. Essentially, the color of the hatches were being handled properly in one, but not the other (I forget which).

@dmcdougall
Matplotlib Developers member

Out of interest. Are you sure lw=72 is correct, and not 72.27? I've seen the number 72.27 appearing in the PGF backend...

@dmcdougall
Matplotlib Developers member

Hmm, so it appears that there are 72 postscript points to an inch, but 72.27 LaTeX points to an inch. I'm on a Mac, I can take a quick look at this.

@dmcdougall
Matplotlib Developers member

So, here's the interesting thing. When using savefig, the resulting figure's line is rather large, much bigger than one inch. However, using show, the line appears to be about an inch thick, which is what it should be. So I conclude that the problem is not with show but with savefig. Unless I'm missing something?

@dmcdougall
Matplotlib Developers member

There also appears to be lots of variables called dpi. There's figure.dpi, figure.canvas.dpi, renderer.dpi and when pushing the save button on the interactive toolbar, a 100 dpi figure is saved as a result of some dpi swapping. I am utterly confused as to why there isn't one single dpi value.

@efiring
Matplotlib Developers member
efiring commented Sep 4, 2012

@dmcdougall: Careful: which backend are you using? And are you measuring the thickness of the line (perpendicular, of course; I should have just made the line horizontal) as a fraction of the height of the figure? It should be 1/2 the height of the figure, since I specified the figure as 2 inches. When I use an agg backend, this is what I see; when I use macosx, it is not, and it is the screen line that is thinner than 1/2 the figure height.

Regarding dpi: yes, it is a confusing mess, and there is more than one way in which dpi operates, depending on backend.

@dmcdougall
Matplotlib Developers member

@efiring I copied and pasted your code, so I'm using the macosx backend. I was measuring the diagonal thickness of the line, since that's the width of the line. Your comment leads me to believe that the diagonal thickness of the line should actually be two inches. Is that correct? I'll check against Agg.

@dmcdougall
Matplotlib Developers member

@efiring Using savefig, I get the same figure using either the agg or macosx backends. Unsurprising, since the macosx backend switches the backend to agg to save the figure anyway. Here is the output using savefig from the agg backend:

agg

And here is the output using savefig from the macosx backend:

osx

I even ran diff between them. The diff is empty.

@efiring
Matplotlib Developers member
efiring commented Sep 5, 2012

@dmcdougall, the confusion here is that one must compare what the macosx backend puts on the screen to what is written to the file. It is not using agg when rendering to the screen, but yes, it is using agg when it writes a png, regardless of whether it is done via savefig or via the file save button. The images you show here are correct (line thickness is half the figure height), but I think that if you estimate the line thickness as shown on your screen you will find it is considerably thinner than half the screen height.

@dmcdougall
Matplotlib Developers member

@efiring Ah, I understand now. Thanks for the explanation.

@efiring
Matplotlib Developers member
efiring commented Sep 5, 2012

src/_macosx.m
That's the objective-C part where the real work is done.

@dmcdougall
Matplotlib Developers member

I'm making progress on this.

@efiring Thanks. I edited the comment once I found it :)

@dmcdougall
Matplotlib Developers member

I've found the problem. The OS X backend just ignores the linewidth parameter; it's never passed to the Core Graphics context.

Here's a screenshot for proof:

osx

I can now sleep! The next step is to fix it, which I need to figure out. But for now it's bedtime. Thanks for the help @efiring!

@mdboom
Matplotlib Developers member
mdboom commented Sep 6, 2012

Thanks for everyone's hard work on this!

@efiring
Matplotlib Developers member
efiring commented Sep 6, 2012

@dmcdougall, that screenshot above: is that what you get with the unmodified macosx backend? It is not the same as what I am seeing; the diagonal line in your version is the correct thickness, the same as in the agg images, whereas I see a line about 1/3 as thick, as a fraction of the figure height. What is the same in your screenshot and on my screen is that the ticks and the surrounding box are rendered differently than in the agg output.

A little more experimentation indicates that the macosx on-screen linewidth is always based on some fixed dpi; it is simply not seeing the figure.dpi setting. For example, if I use rcParams["figure.dpi"]=72, I get a small plot with everything scaled down except the width of the blue line, and the widths of the axis box and ticks.

@dmcdougall
Matplotlib Developers member

@efiring No, it's what I get after fixing the problem.

@mdehoon
mdehoon commented Sep 9, 2012

This doesn't seem the right solution to this problem.
gc.get_linewidth returns the line width previously set by gc.set_linewidth.
But gc.set_linewidth already makes the call to CGContextSetLineWidth, so we should not be needing another call to CGContextSetLineWidth in GraphicsContext_draw_path.

I think the issue lies in the definition of gc.set_linewidth:
Is the line width passed to gc.set_linewidth the line width in pixels? Or the line width in points?
And the same question for gc.get_linewidth: Is the line width returned by gc.get_linewidth the line width in pixels or in points?
I guess it's easiest if get/set_linewidth use units of pixels, and do the conversion between points and pixels uniformly for different backends if possible.
But if gc.set_linewidth must use units of points, then the conversion between points and pixels has to be made within the set_linewidth function of GraphicsContextMac. Which is a bit annoying, since the points_to_pixels function is in the renderer and not in the graphics context.

@dmcdougall
Matplotlib Developers member

@mdehoon Thanks for taking the time to look at the pull request. I appreciate your feedback. I agree with most of what you say. In fact, in light of your comments the solution is much simpler and cleaner, but requires passing the dpi to the graphics context.

For comparison, let's take the agg backend. The dpi is passed in through the constructor and all points are converted to pixels in the c code. The dpi (as far as I am aware) is not currently passed to the objective code for the osx backend. Python passes the linewidths as points to the agg backend, and the conversion is done in c. I think, for the sake of attempting to keep everything uniform we should do the same here.

Since there are also problems with the mac backend not saving bitmaps are the correct dpi (issue ), I think it's best to change the GraphicsContextMac constructor to mimic that of the agg backend. That is, passing the width, height and dpi all to the C code. We could nail two birds with one stone if we do this properly.

I'll look into it more today. Thanks again for your comments.

Edit: The other mac dpi issue I forgot to mention. It is #113.

@dmcdougall
Matplotlib Developers member

On further inspection of the dpi issue, it has transpired that this is not an issue with osx backend in paritcular, see #1223.

@dmcdougall
Matplotlib Developers member

I think this can be closed now. See #1209.

@mdboom mdboom closed this Sep 10, 2012
@gepcel
gepcel commented Jan 15, 2016

I was googling something, and found this closed issue from long time ago. But I've come across this issue multiple times. Can anyone take a look?

The code as following:

mpl.rcParams['font.sans-serif'] = 'SimHei'
d = [19, 14, 8, 21, 17, 10, 16, 11, 8]
cmap=plt.get_cmap('Set1', 6)

fig = plt.figure(dpi=400)
for i, x in enumerate(d):
    plt.bar(i, x, color=cmap(i%3))

ax = gca()
xlim(-0.2, 9)
_ = plt.xticks(range(9), ['合计', '鱼卵', '仔稚鱼']*3, rotation='horizontal')
w = width = ax.patches[0].get_width()

ax.xaxis.set_minor_locator(FixedLocator(np.arange(9)+width/2))
ax.xaxis.set_minor_formatter(FixedFormatter(['合计', '鱼卵', '仔稚鱼']*3))
ax.xaxis.set_major_locator(FixedLocator(np.array([1, 4, 7])+width/2))
ax.xaxis.set_major_formatter(FixedFormatter(['2012年6月', '2013年6月', '2014年6月']))
ax.xaxis.set_tick_params(which='major', pad=17)
ax.xaxis.set_tick_params(which='both', tick1On=False, tick2On=False)
ax.yaxis.set_tick_params(which='both', tick1On=False, tick2On=False, label1On=False)

for r in ax.patches:
    h = r.get_height()
    ax.text(r.get_x()+w/2, h+0.4, int(h), ha='center', va='center')
legend(ax.patches[:3], ['合计', '鱼卵', '仔稚鱼'], loc='upper right')
plt.tight_layout()
fig.savefig('a.png', dpi=fig.dpi)

Since I've turned '%matplotlib inline' on, so I got the fig as following(which I supposed is the result of show()):
image

And the 'a.png' from 'savefig()' is as following:
a

I'm doing some quick plot and formatting, using both 'pyplot' and 'pylab', sorry for the dirty code.
Some backgroud:

Windows 10;
matplotlib  '1.5.1rc1'
python 3.4
ipython 3.2.1
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
@jenshnielsen
Matplotlib Developers member

@gepcel I don't think your issue is related to this one. The inline backend actually calls savefig behind behind the scenes so what you are seeing is a difference between plt.savefig('a.png') and plt.savefig('a.svg') I am not sure why the Agg backend (which saves pngs) don't handle your fonts correctly but that seems to be what happens.

Can you open a new issue coping you post from here so that the issue isn't lost

@gepcel
gepcel commented Jan 15, 2016

OK. Thanks. @jenshnielsen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.