Matplotlib newline character in xtick label - python

I've seen a bunch of solutions (including matplotlib example code) that to have an x-tick label be multi-line you can just introduce a newline character. Which I did (below is a code excerpt that adds this newline):
subplot.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d\n%H:%M", tz=startTime.tzinfo))
However, I noticed this introduces a weird quirk in which when I mouse-over the plots it kind of causes all the plots to 'jump' up and down (shift slightly up and then back down when I mouse over again). Note: if there is just one plot then the bottom matplotlib toolbar (with the save button etc..) shifts up and down only. This makes it unpleasant to look at when you are trying to move the mouse around and interact with the plots. I noticed when I take out the new-line character this quirk disappears. Anyone else run into this and solve it (as in keeping multiline label without this weird 'jump' quirk)?
I'm using Python 3.6 and matplotlib 1.5.3. using TKAgg backend.

By default, the same formatter is used for the values shown in the NavigationToolbar as on the axes. I suppose that you want to use the format "%m-%d\n%H:%M" in question just for the ticklabel formatting and are happy to use a single-line format for the values shown when moving the mouse.
This can be achieved by using a different formatter for those two cases.
# Format tick labels
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d\n%H:%M"))
# Format toolbar coordinates
ax.fmt_xdata = mdates.DateFormatter('%m-%d %H:%M')
Example picture:
Complete code for reproduction:
import matplotlib
matplotlib.use("TkAgg")
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import numpy as np
dates = pd.date_range("2016-06-01 09:00", "2016-06-01 16:00", freq="H" )
y = np.cumsum(np.random.normal(size=len(dates)))
df = pd.DataFrame({"Dates" : dates, "y": y})
fig, ax = plt.subplots()
ax.plot_date(df["Dates"], df.y, '-')
ax.xaxis.set_major_locator(mdates.HourLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d\n%H:%M"))
ax.fmt_xdata = mdates.DateFormatter('%m-%d %H:%M')
plt.show()

Related

Python plt.yticks does not work when specifying font on a logscale

I have a piece of code for producing a plot that suddenly stopped working. The issue comes when I try to define the font for the yticks on a plot with a logscale.
Example:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as font_manager
fig, ax = plt.subplots(figsize=(9,6))
ax.plot(np.arange(10),np.arange(10),color='green',linestyle='-',marker='.')
plt.xticks(font='Nimbus Roman',size=22)
plt.yticks(font='Nimbus Roman',size=22)
ax.set_yscale('log')
this produces an error:
ValueError: None is not a valid value for fontset; supported values are 'cm', 'dejavuserif', 'dejavusans', 'stix', 'stixsans', 'custom'
However, if I remove "font='Nimbus Roman'" fom the yticks part, or remove the logscale, the plot is plotted as expected. I also tried different fonts to no avail. The same error appears for the x-axis if I instead set that one to logscale.
I am using python3.9. This error does not occur in python3.8.6

Image hidden from a matplotlib plot when shifted

I lose my image from a subplot when I shift the image.
(The code is run in Jupyter Lab):
from mpl_toolkits.axes_grid1 import host_subplot
from mpl_toolkits import axisartist
hostImage = host_subplot(221, axes_class=axisartist.Axes)
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (-0.0014, 0), box_alignment=(1, 0))
hostImage.add_artist(ab)
The image can still be seen with the above configuration.
Next, when I change parameters the image vanishes:
Shifting the image to the left changing line 7
ab = AnnotationBbox(imagebox, (-0.0025, 0), box_alignment=(1, 0))
to
ab = AnnotationBbox(imagebox, (-0.5, 0), box_alignment=(1, 0))
Changing the matrix layout of the subplots changing line
hostImage = host_subplot(221, axes_class=axisartist.Axes)
to
hostImage = host_subplot(111, axes_class=axisartist.Axes)
-> How can I show everything I add to a subplot (more or less) regardless how far off it may be from the axes 'central part' (the area spanned by the two axes, 'axes' in the sense of a plot)?
Using the plt.tight_layout() method did not help.
Here is the test image I used (the red rhomboid).
%%%%%%%%%%%
To make it clearer what I really want to achieve (practical background of the question):
I have line plots showing measurement data of about 30 sensors which are positioned in the real world in a rather geometrically complex 3D measurement setup. The position of the sensors is essential for anybody trying to understand the chart. So the image serves as a kind of 3D legend for the chart. In a single plot I show data of about 5-6 sensors (more sensors in single chart would make it unreadable).
See this real example (work in progress where I stopped to post my question):
image of the real case
This example I established by creating a second subplot below the subplot with the curves. This second suplot has hidden axes (in the sense of plural of axis). It already is a workable solution and my current baseline.
By the way, for this reason I want the image to be rather below the plot in order not to 'waste' horizontal space for the chart where I plot curves.
So the '3D image legend' is integral part of the finally exported 'all-in-one' plot (.png)
The .pngs go into my written report which is my ultimate goal.
In the report I could also add each image corresponding to a plot by hand, but having all info (plot and image) included in one-in-all matplotlib figures makes it more convenient to establish the report and also less error-prone (pairing wrong images and plots, since I have many sensors and many configurations thus creating quite a number of such plots).
What triggered my question beyond my above solution already established:
I want to finally place labels (matplotlib annotations) as 'overlay' on the image with the sensor names on top of the image.
And then connect these labels via arrow lines with the corresponding curves of the plot. This would make it very clear and convenient to the reader to understand which plot curve corresponds to which sensor position in the image -> kind of '3D legend'.
I had found ConnectionPatch as a solution for drawing lines between subplots but I got an error message which I ultimately did not want to try to resolve but choose the approach:
Have the image as part of the very same subplot of the curves because connecting labels within a subplot is easy (actually you can see in the image I uploaded already such sensor name labels placed along the right y-axis).
Why do I use host_subplot?
I have up to five y-axes in my plot (I am aware that this high number of y-axis may be questionable but it is please not what I want to discuss in this post) and I understood having more than 2 additional y-axis is possible only with host_subplot using .twinx().
P.S.:
After all I think I should for now lower my high expectations and stick with my workable solution of two subplots and just renounce on the possibility of connecting labels in the second subplot with curves in the first subplot.
Matplotlib 3.5 (or presumably better)
If you are using Matplotlib 3.5 (or presumably better), this works for what you want, I think (or close):
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as axisartist
hostImage = host_subplot(221, axes_class=axisartist.Axes)
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (-0.0025, 0), box_alignment=(1, 0))
hostImage.add_artist(ab)
hostImage.figure.subplots_adjust(left=0.69) # based on https://matplotlib.org/stable/tutorials/intermediate/tight_layout_guide.html saying how to manually adjust
hostImage.figure.set_size_inches((18, 10)) # from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/figure.py; also see drevicko's comment https://stackoverflow.com/a/638443/8508004
hostImage.figure.savefig("my_image_test.png") # fix for `hostImage.savefig("my_image_test.png")`, based on https://forum.freecodecamp.org/t/attribute-error-axessubplot-object-has-no-attribute-savefig/46025
This will show the same view of the produced plot in both the direct in JupyterLab output and in the image file produced. (The actual size will probably be slightly different, with the image file displaying better resolution.) **If you don't want to produce an image file, then you can remove the last two lines and just include the adjustment **,figure.subplots_adjust(left=0.69) , to account for the Annotation box being added.
I put pertinent sources in the comments for each line.
My test image was wide and short so you may need to adjust figure.subplots_adjust(left=0.69) to what works for you. (Now I don't like that I had to stumble around trying very high and low versions of the left value for figure.subplots_adjust(), and then hone in on a just-right setting but it worked. I will say that usually I set the figure size before making the subplots, such as here, and maybe doing it that way makes it seem less experimenting is necessary to get it working. But the fact the manual adjustment is mentioned in discussion of tight_layout in Matplotlib's documentation, in regards to elements going outside the figure area, makes me think it happens that you need to do some adjusting now and then.)
Here I use hostImage.figure.set_size_inches((18, 10)). Maybe you don't need yours as wide?
Code for checking Matplotlib version:
import matplotlib
print (matplotlib.__version__ )
Matplotlib versions prior to 3.5 (or maybe specifically 3.2.1?)
The code above wasn't working with Matplotlib 3.2.1 with all else the same. (In launches of Jupyter sessions served via MyBinder from here before running %pip install matplotlib --upgrade in a cell and restarting the kernel.) The image produced was good but the output directly in the Jupyter notebook was cutoff and only showing a fragment.
This code block below works for what you want, I think (or close), if using Matplotlib 3.2.1. Since I couldn't get the direct output in the Jupyter cell where I was using Matplotplib 3.2.1 to display correctly, this just displays the plot from the associated image file produced.
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as axisartist
hostImage = host_subplot(221, axes_class=axisartist.Axes)
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (-0.0025, 0), box_alignment=(1, 0))
hostImage.add_artist(ab)
hostImage.figure.subplots_adjust(left=0.69) # based on https://matplotlib.org/stable/tutorials/intermediate/tight_layout_guide.html saying how to manually adjust
hostImage.figure.set_size_inches((18, 10)) # from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/figure.py; also see drevicko's comment https://stackoverflow.com/a/638443/8508004
hostImage.figure.savefig("my_image_test.png") # fix for `hostImage.savefig("my_image_test.png")`, based on https://forum.freecodecamp.org/t/attribute-error-axessubplot-object-has-no-attribute-savefig/460255
hostImage.figure.clf() # using this so, Jupyter won't display the Matplotlib plot object; instead we'll show the image file
from IPython.display import Image
Image(filename="my_image_test.png")
How things are working for the shared lines I added is covered above.
Optionally when using Matplotlib 3.2.1 with code like here, to not also show the matplotlib cruft, such as something like <Figure size 1296x720 with 0 Axes>, you can split running this between two cells.
First cell's code:
%%capture
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as axisartist
hostImage = host_subplot(221, axes_class=axisartist.Axes)
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (-0.0025, 0), box_alignment=(1, 0))
hostImage.add_artist(ab)
hostImage.figure.subplots_adjust(left=0.69) # based on https://matplotlib.org/stable/tutorials/intermediate/tight_layout_guide.html saying how to manually adjust
hostImage.figure.set_size_inches((18, 10)) # from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/figure.py; also see drevicko's comment https://stackoverflow.com/a/638443/8508004
hostImage.figure.savefig("my_image_test.png") # fix for `hostImage.savefig("my_image_test.png")`, based on https://forum.freecodecamp.org/t/attribute-error-axessubplot-object-has-no-attribute-savefig/460255
hostImage.figure.clf() # using this so, Jupyter won't display the Matplotlib plot object; instead we'll show the image file
Second cell's code:
from IPython.display import Image
Image(filename="my_image_test.png")
The first cell will show no output of any kind now due to the %%capture cell magic.
UPDATE:
(code below only tested with Matplotlib 3.5.)
Some options based on addition of sample figure OP is using and additional information in comment here, I suggest starting over with simpler subplot use for arranging the two elements. (If it was much more complex, I'd suggest other methods for compositing the two elements. Options would include: If just for presenting in Jupyter, ipywidgets can be used for layout. Pillow and ReportLab can be useful if making a publication-quality figure is the goal.)
!curl -o testImage.png https://owncloud.tuwien.ac.at/index.php/s/3caJsb2PcwN7HdU/download
#based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplots_demo.html
# and https://www.moonbooks.org/Articles/How-to-insert-an-image-a-picture-or-a-photo-in-a-matplotlib-figure/
# and https://nbviewer.org/gist/fomightez/4c2116e50f080b1305c41b9ac70df124#Solution
# axis off for lower plot based on https://stackoverflow.com/a/10035974/8508004
import matplotlib.pyplot as plt
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
fig, axs = plt.subplots(2,1,figsize=(4, 8))
#fig.suptitle('Vertically stacked subplots')
axs[0].grid()
axs[1].grid()
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (0.5,0.5))
axs[1].add_artist(ab)
axs[1].axis('off');
Or:
!curl -o testImage.png https://owncloud.tuwien.ac.at/index.php/s/3caJsb2PcwN7HdU/download
#based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplots_demo.html
# and https://www.moonbooks.org/Articles/How-to-insert-an-image-a-picture-or-a-photo-in-a-matplotlib-figure/
# and https://nbviewer.org/gist/fomightez/4c2116e50f080b1305c41b9ac70df124#Solution
# axis turned off for lower plot based on https://stackoverflow.com/a/10035974/8508004
import matplotlib.pyplot as plt
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
# data to plot based on https://stackoverflow.com/a/17996099/8508004 and converting it
# to work with subplot method
fig, axs = plt.subplots(2,1)
plt.subplots_adjust(hspace=1.8) # to move the bottom plot down some so not covering the top small one
#fig.suptitle('Vertically stacked subplots')
axs[0].plot(range(15))
axs[0].set_xlim(-7, 7)
axs[0].set_ylim(-7, 7)
axs[0].set_aspect('equal')
axs[1].grid()
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (0.5,0.5))
axs[1].add_artist(ab)
axs[1].axis('off');
Or if want to save the figure something like:
!curl -o testImage.png https://owncloud.tuwien.ac.at/index.php/s/3caJsb2PcwN7HdU/download
#based on https://matplotlib.org/stable/gallery/subplots_axes_and_figures/subplots_demo.html
# and https://www.moonbooks.org/Articles/How-to-insert-an-image-a-picture-or-a-photo-in-a-matplotlib-figure/
# and https://nbviewer.org/gist/fomightez/4c2116e50f080b1305c41b9ac70df124#Solution
# axis turned off for lower plot based on https://stackoverflow.com/a/10035974/8508004
import matplotlib.pyplot as plt
from matplotlib.offsetbox import TextArea, DrawingArea, OffsetImage, AnnotationBbox
import matplotlib.image as mpimg
# data to plot based on https://stackoverflow.com/a/17996099/8508004 and converting it
# to work with subplot method
fig, axs = plt.subplots(2,1)
plt.subplots_adjust(hspace=0.3) # to move the bottom plot down some so not covering the top small one
#fig.suptitle('Vertically stacked subplots')
axs[0].plot(range(15))
axs[0].set_xlim(-7, 7)
axs[0].set_ylim(-7, 7)
axs[0].set_aspect('equal')
axs[1].grid()
test_image = mpimg.imread('testImage.png')
imagebox = OffsetImage(test_image, zoom=1)
ab = AnnotationBbox(imagebox, (0.5,0.5))
axs[1].add_artist(ab)
axs[1].axis('off')
# to accomodate this adjustment in the figure that gets saved via `plt.savefig()`, increase figure size
fig.set_size_inches((4, 7)) # from https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/figure.py; also see drevicko's comment
plt.savefig("stacked.png");
I'm not sure while the size changes on the top plot if you set the size so you can accomodate them but there's some honing on the right numbers needed there.
Edit on 2022-09-28:
I have found a solution for my case by browsing the help/py-code of matplotlib.offsetbox.AnnotationBbox:
The desired effect can be achieved by modifying the argument xybox of AnnotationBbox like so, for example
ab = AnnotationBbox(imagebox, xy = (1, 0), xybox = (2.0, 1.0), box_alignment=(1, 0))
Setting xybox = (2.0, 1.0), hence the x-value to 2.0 shifts the image far to the right of the plot area.

Odd behavior of plotting in Pandas

I would like to know if the behavior of the following code is expected.
The first figure (Series) is saved as I would expect. The second (DataFrame) is not.
If this is not a bug, how can I achieve my (obvious) goal?
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
fig = plt.figure()
pd.Series(np.random.randn(100)).plot()
fig.savefig('c:\\temp\\plt_series.png')
fig = plt.figure()
pd.DataFrame(np.random.randn(100,2)).plot()
fig.savefig('c:\\temp\\plt_df.png')
After saving the figure, close the current plot using plt.close() to close the current figure, otherwise the old one is still active even if the next plot is being generated. You can also use plt.close('all') to be sure all open figures are closed.

Axis spikes not showing up on Matplotlib plots

I am trying to plot a couple of line graphs using Matplotlib and the small dashes which marks the center of the xticks are not showing up. Here is a sample plot I found online which has the marks (I circled them).
Now below is my code and the graph. I know it's not related to spines.
Code:
from sklearn.externals import joblib
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import math
sns.set()
sns.set_style("dark")
sns.set_style("white")
plt.figure()
plt.plot([1,2,3,4,5],[10,5,69,38,52],label='test')
plt.xticks([1,2,3,4,5],['apple','orange','grapes','lemon','pear'],ha='right')
plt.xticks(rotation=45)
plt.savefig("test.png", dpi=300)
OS: MacOX High Sierra 10.13, Python: 3.6.0 and no Virtual Environments
You can customise the seaborn styles to add back ticks (there is also a style for ticks). See here for the full details.
Just change your set_style line to have a second parameter which is a dictionary of overrides. In this case it is setting the size of the xticks. That link gives the full details of built-in styles and all the options for overriding.
sns.set_style("white", {"xtick.major.size": 5})

Detailed date in cursor pos on pyplot charts

Let's say there's a time series that I want to plot in matplotlib:
dates = pd.date_range(start='2011-01-01', end='2012-01-01')
s = pd.Series(np.random.rand(1, len(dates))[0], index=dates)
The GUI backends in matplotlib have this nice feature that they show the cursor coordinates in the window. When I plot pandas series using its plot() method like this:
fig = plt.figure()
s.plot()
fig.show()
the cursor's x coords are shown in full yyyy-mm-dd at the bottom of the window as you can see on pic 1.
However when I plot the same series s with pyplot:
fig = plt.figure()
plt.plot(s.index, s.values)
fig.show()
full dates are only shown when I zoom in and in the default view I can only see Mon-yyyy (see pic 2) and I would see just the year if the series were longer.
In my project there are functions for drawing complex, multi-series graphs from time series data using plt.plot(), so when I view the results in GUI I only see the full dates in the close-ups. I'm using ipython3 v. 4.0 and I'm mostly working with the MacOSX backend, but I tried TK, Qt and GTK backends on Linux with no difference in the behavior.
So far I've got 2 ideas on how to get the full dates displayed in GUI at any zoom level:
rewrite plt.plot() to pd.Series.plot()
use canvas event handler to get the x-coord from the cursor pos and print it somewhere
However before I attempt any of the above I need to know for sure if there is a better quicker way to get the full dates printed in the graph window. I guess there is, because pandas is using it, but I couldn't find it in pyplot docs or examples or elsewhere online and it's none of these 2 calls:
ax.xaxis_date()
fig.autofmt_xdate()
Somebody please advise.
Hooks for formatting the info are Axes.format_coord or Axes.fmt_xdata. Standard formatters are defined in matplotlib.dates (plus some additions from pandas). A basic solution could be:
import matplotlib.dates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
dates = pd.date_range(start='2011-01-01', end='2012-01-01')
series = pd.Series(np.random.rand(len(dates)), index=dates)
plt.plot(series.index, series.values)
plt.gca().fmt_xdata = matplotlib.dates.DateFormatter('%Y-%m-%d')
plt.show()

Categories