How to write annotation outside the drawing in data coords - python

My graphic goes from y=-1 to y=10
I want to write a small piece of text in an arbitrary position, say at x=2000, y=5:
ax.annotate('MgII', xy=(2000.0, 5.0), xycoords='data')
Now I want the same, but this time the piece of text must be outside the graphic, but in the exact position I mark in data coordinates:
ax.annotate('MgII', xy=(2000.0, 10.5), xycoords='data')
But it then disappears (remember my graphic goes from -1 to 10). There is plenty of space free on top of the graphic.
And, if I specify
xy=(2000.0, 9.999)
then the label appears nearly where I want it, only it is too close to the top border of the picture. I want it at y=10.5, specifically.

ax.annotate('MgII', xy=(2000.0, 10.5), xycoords='data', annotation_clip=False)
By default in data units the annotation is only drawn if it is in axes.
You might be better off using a blended transform:
trans = ax.get_xaxis_transform() # x in data untis, y in axes fraction
ann = ax.annotate('MgII', xy=(2000, 1.05 ), xycoords=trans)

I just had the same problem and found another,very simple solution. Option of the annotate method:
annotation_clip=False
https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.annotate.html

Related

How to make a finer 3D plot with Matplotlib

In Python I have longitude, latitude and height information that I want to plot (3D):
latitudeMeshGrid (100, 100)
longitudeMeshGrid (100, 100)
heightMeshGrid (100, 100)
heightMeshGrid contains 'NaN's for points that are located outside of a specific (long, lat) region (red points in the figure below):
If I try plotting this using:
ax.plot_surface(longitudeMeshGrid, latitudeMeshGrid, heightMeshGrid, cmap=plt.cm.jet, vmin=np.nanmin(heightMeshGrid), vmax=np.nanmax(heightMeshGrid))
I get the following result:
First of all, the colors at the boundaries seem to be incorrect. Secondly, the plot seems rather "coarse" even though I use a fine grid of data. Is there a possibility to reduce the size of the rectangles?
If I remove the NaN data as follows:
longitudeMeshGrid = longitudeMeshGrid[insideBoundary]
latitudeMeshGrid = latitudeMeshGrid[insideBoundary]
heightMeshGrid = heightMeshGrid[insideBoundary]
Then I end up with:
latitudeMeshGrid (7023,)
longitudeMeshGrid (7023,)
heightMeshGrid (7023,)
This I can plot using:
ax.plot_trisurf(longitudeMeshGrid, latitudeMeshGrid, heightMeshGrid, cmap=plt.cm.jet)
With the result:
At least the artifacts at the edges are gone now, but still the plot looks really coarse.
I expect to get something similar as I get in Matlab using:
surf(longitudeMeshGrid, latitudeMeshGrid, heightMeshGrid)
Which ends up as:
which doesn't have any artifacts at the edges and looks much finer/smoother.

How to hack this Bokeh HexTile plot to fix the coords, label placement and axes?

Below is Bokeh 1.4.0 code that tries to draw a HexTile map of the input dataframe, with axes, and tries to place labels on each hex.
I've been stuck on this for two days solid, reading bokeh doc, examples and github known issues, SO, Bokeh Discourse and Red Blob Games's superb tutorial on Hexagonal Grids, and trying code. (I'm less interested in raising Bokeh issues for the future, and far more interested in pragmatic workarounds to known limitations to just get my map code working today.) Plot is below, and code at bottom.
Here are the issues, in rough decreasing order of importance (it's impossible to separate the root-cause and tell which causes which, due to the way Bokeh handles glyphs. If I apply one scale factor or coord transform it fixes one set of issues, but breaks another, 'whack-a-mole' effect):
The label placement is obviously wrong, but I can't seem to hack up any variant of either (x,y) coords or (q,r) coords to work. (I tried combinations of figure(..., match_aspect=True)), I tried 1/sqrt(2) scaling the (x,y)-coords, I tried Hextile(... size, scale) params as per redblobgames, e.g. size = 1/sqrt(3) ~ 0.57735).
Bokeh forces the origin to be top left, and y-coords to increase as you go down, however the default axis labels show y or r as being negative. I found I still had to use p.text(q, -r, .... I suppose I have to manually patch the auto-supplied yaxis labels or TickFormatter to be positive.
I use np.mgrid to generate the coord grid, but I still seem to have to assign q-coords right-to-left: np.mgrid[0:8, (4+1):0:-1]. Still no matter what I do, the hexes are flipped L-to-R
(Note: empty '' counties are placeholders to get the desired shape, hence the boolean mask [counties!=''] on grid coords. This works fine and I want to leave it as-is)
The source (q,r) coords for the hexes are integers, and I use 'odd-r' offset coords (not axial or hexagonal coords). No matter what HexTile(..., size, scale) args I use, one or both dimensions in the plot is wrong or squashed. Or whether I include the 1/sqrt(2) factor in coord transform.
My +q-axis is east and my +r-axis should be 120° SSE
Ideally I'd like to have my origin at bottom-left (math plot style, not computer graphics). But Bokeh apparently doesn't support that, I can live without that. However defaulting the y-axis labels to negative, while requiring a mix of positive and negative coords, is confusing. Anyway, how to hack an automatic fix to that with minimum grief? (manual p.yrange = Range1d(?, ?)?)
Bokeh's approach to attaching (hex) glyphs to plots is a hard idiom to use. Ideally I simply want to reference (q,r)-coords everywhere for hexes, labels, axes. I never want to see (x,y)-coords appearing on axes, label coords, tick-marks, etc. but seems Bokeh won't allow you. I guess you have to manually hack the axes and ticks later. Also, the plot<->glyph interface doesn't allow you to expose a (q,r) <-> (x,y) coord transform function, certainly not a bidirectional one.
The default axes don't seem to have any accessors to automatically find their current extent/limits; p.yaxis.start/end are empty unless you specified them. The result from p.yaxis.major_tick_in,p.yaxis.major_tick_out is also wrong, for this plot it gives (2,6) for both x and y, seems to be clipping those to the interior multiples of 2(?). How to automatically get the axes' extent?
My current plot:
My code:
import pandas as pd
import numpy as np
from math import sqrt
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.models.glyphs import HexTile
from bokeh.io import show
# Data source is a list of county abbreviations, in (q,r) coords...
counties = np.array([
['TE','DY','AM','DN', ''],
['DL','FM','MN','AH', ''],
['SO','LM','CN','LH', ''],
['MO','RN','LD','WH','MH'],
['GA','OY','KE','D', ''],
['', 'CE','LS','WW', ''],
['LC','TA','KK','CW', ''],
['KY','CR','WF','WX', ''],
])
#counties = counties[::-1] # UNUSED: flip so origin is at bottom-left
# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, (4+1):0:-1]
q = q[counties!='']
r = r[counties!='']
sqrt3 = sqrt(3)
# Try to transform odd-r (q,r) offset coords -> (x,y). Per Red Blob Games' tutorial.
x = q - (r//2) # this may be slightly dubious
y = r
counties_df = pd.DataFrame({'q': q, 'r': r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})
p = figure(tools='save,crosshair') # match_aspect=True?
glyph = HexTile(orientation='pointytop', q='x', r='y', size=0.76, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!? size=0.76 is an empirical hack.
p.add_glyph(counties_ds, glyph)
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None
print(f'Axes: x={p.xaxis.major_tick_in}:{p.xaxis.major_tick_out} y={p.yaxis.major_tick_in}:{p.yaxis.major_tick_out}')
# Now can't manage to get the right coords for text labels
p.text(q, -r, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)], text_baseline="middle", text_align="center")
# Ideally I ultimately want to fix this and plot `abbrev` column as the text label
show(p)
There is an axial_to_cartesian function that will just compute the hex centers for you. You can then attach the labels in a variety of orientations and anchoring from these.
Bokeh does not force the origin to be anywhere. There is one axial to cartesian mapping Bokeh uses, exactly what is given by axial_to_cartesian. The position of the Hex tiles (and hence the cartesian coordinates that the axes display) follows from this. If you want different ticks, Bokeh affords lots of control points over both tick location and tick labelling.
There is more than one convention for Axial coords. Bokeh picked the one that has the r-axis tile "up an to the left", i.e. the one explicitly shown here:
https://docs.bokeh.org/en/latest/docs/user_guide/plotting.html#hex-tiles
Bokeh expects up-and-to-the-left axial coords. You will need to convert whatever coordinate system you have to that. For "squishing" you will need to set match_aspect=True to ensure the "data space" aspect ratio matches the "pixel space" aspect ratio 1-1.
Alternatively, if you don't or can't use auto-ranging you will need to set the plot size carefully and also control the border sizes with min_border_left etc to make sure the borders are always big enough to accommodate any tick labels you have (so that the inner region will not be resized)
I don't really understand this question, but you have absolute control over what ticks visually appear, regardless of the underlying tick data. Besides the built-in formatters, there is FuncTickFormatter that lets you format ticks any way you want with a snippet of JS code. [1] (And you also have control of where ticks are located, if you want that.)
[1] Please note the CoffeeScript and from_py_func options are both deprecated and being removed in then next 2.0 release.
Again, you'll want to use axial_to_cartesian to position anything other then Hex tiles. No other glyphs in Bokeh understand axial coordinates (which is why we provide the conversion function).
You misunderstood what major_tick_in and major_tick_out are for. They are literally how far the ticks visually extend inside and outside the plot frame, in pixels.
Auto-ranging (with DataRange1d) is only computed in the browser, in JavaScript, which is why the start/end are not available on the "Python" side. If you need to know the start/end, you will need to explicitly set the start/end, yourself. Note, however that match_aspect=True only function with DataRange1d. If you explicitly set start/end manually, Bokeh will assume you know what you want, and will honor what you ask for, regardless of what it does to aspect.
Below are my solution and plot. Mainly per #bigreddot's advice, but there's still some coordinate hacking needed:
Expecting users to pass input coords as axial instead of offset coords is a major limitation. I work around this. There's no point in creating a offset_to_cartesian() because we need to negate r in two out of three places:
My input is even-r offset coords. I still need to manually apply the offset: q = q + (r+1)//2
I need to manually negate r in both the axial_to_cartesian() call and the datasource creation for the glyph. (But not in the text() call).
The call needs to be: axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')
Need p = figure(match_aspect=True ...) to prevent squishing
I need to manually create my x,y axes to get the range right
Solution:
import pandas as pd
import numpy as np
from math import sqrt
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Range1d
from bokeh.models.glyphs import HexTile
from bokeh.io import curdoc, show
from bokeh.util.hex import cartesian_to_axial, axial_to_cartesian
counties = np.array([
['DL','DY','AM','', ''],
['FM','TE','AH','DN', ''],
['SO','LM','CN','MN', ''],
['MO','RN','LD','MH','LH'],
['GA','OY','WH','D' ,'' ],
['' ,'CE','LS','KE','WW'],
['LC','TA','KK','CW','' ],
['KY','CR','WF','WX','' ]
])
counties = np.flip(counties, (0)) # Flip UD for bokeh
# (q,r) Coordinate system is “odd/even-r” horizontal Offset coords
r, q = np.mgrid[0:8, 0:(4+1)]
q = q[counties!='']
r = r[counties!='']
# Transform for odd-r offset coords; +r-axis goes up
q = q + (r+1)//2
#r = -r # cannot globally negate 'r', see comments
# Transform odd-r offset coords (q,r) -> (x,y)
x, y = axial_to_cartesian(q, -r, size=2/3, orientation='pointytop')
counties_df = pd.DataFrame({'q': q, 'r': -r, 'abbrev': counties[counties!=''], 'x': x, 'y': y })
counties_ds = ColumnDataSource(ColumnDataSource.from_df(counties_df)) # ({'q': q, 'r': r, 'abbrev': counties[counties != '']})
p = figure(match_aspect=True, tools='save,crosshair')
glyph = HexTile(orientation='pointytop', q='q', r='r', size=2/3, fill_color='#f6f699', line_color='black') # q,r,size,scale=??!?!!?
p.add_glyph(counties_ds, glyph)
p.x_range = Range1d(-2,6)
p.y_range = Range1d(-1,8)
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None
p.text(x, y, text=["(%d, %d)" % (q,r) for (q, r) in zip(q, r)],
text_baseline="middle", text_align="center")
show(p)

Appropriately positioning annotations on stacked barplot

I have annotated each bar on my stacked barplot but can't seem to get the annotations to be equivalent to the bar's position.
This is the code I have:
for i in ax_mult.patches:
width,height=i.get_width(),i.get_height()
x,z =i.get_xy()
ax_mult.annotate(str(i.get_height()),(i.get_x()+.30*width,i.get_height()+.1*height))
This is what I am getting
I guess your main problem was that you placed the text in y direction effectively at 1.1 * i.get_height(), without considering the initial offset i.get_y().
Try this:
for i in ax_mult.patches:
ix,iy=i.get_x(),i.get_y() ## gives you the bottom left of each patch
width,height=i.get_width(),i.get_height() ## the width & height of each patch
## to place the annotation at the center (0.5, 0.5):
ax.annotate(str(height),(ix+0.5*width, iy+0.5*height),ha="center",va="center")
## alternatively via ax.text():
# ax.text(ix+.5*width,iy+.5*height,height,ha="center",va="center" )
Note that you may need to "play around" with good offsets, especially in y-direction. The ha="center",va="center" parameters align the text exactly at the chosen coordinate (both horizontally: ha and vertically: va), which comes in handy if you'd like to put the labels e.g. aligned below the top end of the patch:
ax.annotate(str(height),(ix+0.5*width, iy+1.0*height),ha="center",va="top")
Or just above the top end of the patch:
ax.annotate(str(height),(ix+0.5*width, iy+1.0*height),ha="center",va="bottom")

How to view this .fits image without resizing and without modifying colors?

I am trying to open a full-color image from a '.fits' file. But, when comparing it to the corresponding '.gif' image, there appears to be an error regarding its color and size.
How can I view the real-color image in its proper dimensions?
As an example, one can select the '.fits' file and corresponding '.gif' file located at the top of this webpage. My example code, which uses the APLPY module, is below.
def from_fits_to_image(color_scheme, rloc, rname='synop_Ml_0.2104', rext='.fits', cmap=None):
"""
color_scheme : 'rgb', 'grayscale', or 'false color'; color scheme of image to be shown
rloc : type <str>; location of file to be read
rname : type <str>; name of file to be read
rext : type <str>; extension of file to be read
cmap : None or type <str>; colormap
"""
rpath = rloc + rname + rext
if color_scheme == 'rgb':
pic = aplpy.FITSFigure(rpath)
# pic.show_rgb(alt_filename) # what filename is supposed to go here?
else:
pic = aplpy.FITSFigure(rpath)
if color_scheme == 'grayscale':
pic.show_grayscale()
elif color_scheme == 'false color':
if cmap is None:
pic.show_colorscale()
else:
pic.show_colorscale(cmap=cmap)
# plt.savefig(...)
plt.show()
So long as one provides the proper rloc (the location of the downloaded '.fits' file) and color_scheme, the above code will run.
Calling the function below will show an empty plot of the proper dimensions. To make it non-empty, I must supply another existing filename, though I am unclear on what exactly that should be.
from_fits_to_image(color_scheme='rgb', rloc=rloc)
Each of the function calls below reveal a plot that has been resized to small. While color_scheme='grayscale' seems to colorize the plot properly, the other methods do not properly colorize the image.
from_fits_to_image('grayscale', rloc=rloc)
from_fits_to_image('false color', rloc=rloc)
from_fits_to_image('false color', rloc=rloc, cmap='plasma')
For comparison, the '.gif' image is below. Ideally, the output will look exactly like the image below.
EDIT:
I have tried using astropy, PIL, and pyfits unsuccessfully. Any help would be appreciated.
EDIT 2:
Below is the result using fits from astropy.io.
from astropy.io import fits
def reada(rloc, rname='synop_Ml_0.1998', rext='.fits'):
""" """
rpath = rloc + rname + rext
# hdu_list = fits.open(rpath)
# hdu_list.info()
pic = fits.getdata(rpath)
plt.imshow(pic)
plt.show()
reada(rloc=rloc)
I've played with the vmin and vmax kwargs, but to no success. Also, using pyfits to open the file results in the following error, even when using pyfits.open(rpath, uint=True, do_not_scale_image_data=True):
TypeError: Image data can not convert to float
Judging from the gif, it seems that the image is a false-color one, and it's a matter of choosing a right color map. I can't say if there's an equivalent color map in python to the one you've linked to, but we can find something that is pretty close, recreating all the features in the image:
fig = aplpy.FITSFigure('synop_Ml_0.2104.fits')
fig.show_colorscale(vmin=-60, vmax=60, cmap='hot')
which shows the following (note that aplpy does not understand this coordinate system so it plots the figure in pixel coordinates):
The second part of the question is more tricky, and I can't fully answer it. Firstly, you need to convert the Carrington time into longitude, and sine latitude into degrees, and then plot the new values for the axis labels either instead of the old ones or as a parasite axes alongside the old ones (you can refer to an parasite axes example here).
Now it looks like the Carrington time is just degrees rotated since November 9, 1853, and the x-axis spans exactly 360, so I assume the conversion is simply an offset by 757079.95, the the x-axis value at the left edge. We can double-check it on the world coordinates, by looking at how the pixel span of the map corresponds to the coordinate span:
In [88]: fig._wcs.wcs_pix2world(fig._ax1.get_xlim(), fig._ax1.get_ylim(), origin=1)
Out[88]: [array([757439.95, 757079.95]), array([-1., 1.])]
and the difference in values for the xaxis edges, 757079.95 and 757439.95, is exactly 360 degrees.
So then we can use some matplotlib tricks to manually offset the coordinate values to force them to go from zero to 360 and get the xaxis to match your gif image:
# using hidden attributes is non-pythonic but aplpy does not leave us other options
ax = fig._ax1
x_range = ax.get_xlim()
new_ticklabels = np.arange(60, 361, 60)
new_tick_positions = new_ticklabels / 360. * x_range[1] + x_range[0]
ax.set_xticks(new_tick_positions)
ax.set_xticklabels(new_ticklabels)
fig.axis_labels.set_xtext('Carrington Longitude')
Keep in mind that aplpy is a library designed for plotting celestial, not solar, coordinates, so getting the axes transform to work properly could be a rather painful process. An alternative way, and perhaps a better one, would be to plot the fits file with sunpy, a python library for solar physics. However, I have never used it, and it seems to throw an error for this particular fits file. Looks like you need to modify the header of the fits file to get the coordinates properly read. Perhaps you can get in touch with sunpy community if you want to use the library?

Fancy arrow patch not starting where I tell it to start

I've created my own quiver function in python using matplotlib (The provided quiver plot doesn't satisfy some of my needs, thats not important.) My function is below and I've made it plots little green circles where the arrows are meant to start (image below). The arrows always seem to be offset by the same amount in the direction of where they are pointing. The arrow starting position and the circle position is exactly the same.
def my_own_quiver_function(axis, X_pos, Y_pos, X_val, Y_val):
standard_vel = 500000.
scale_factor = 0.0005
vels = np.hypot(X_val, Y_val)
vels = vels/(standard_vel)
widths = vels**2.
widths = widths.clip(max=1.0)
for xp in range(len(X_pos[0])):
for yp in range(len(Y_pos[0])):
xvel = X_val[xp][yp]*scale_factor
yvel = Y_val[xp][yp]*scale_factor
width_val = widths[xp][yp]
axis.add_patch(mpatches.Circle((X_pos[xp][yp], Y_pos[xp][yp]), radius=0.1, lw=0.5, color='m'))
axis.add_patch(mpatches.FancyArrowPatch((X_pos[xp][yp], Y_pos[xp][yp]), (X_pos[xp][yp]+xvel, Y_pos[xp][yp]+yvel), color='w', linewidth=1.*width_val, mutation_scale=30.*width_val,arrowstyle='->'))
Urgh I don't have enough reputation points but the image can be found here: http://imgur.com/S1GywxX
It's really frustrating that they quiver looks all wonky because of this.
EDITS:I've tried removing the mutation because the documentation says its squeezes and stretches the arrow, but it doesn't change anything.
Thanks to #Jezzamon for pointing out the ShrinkA and ShrinkB inputs. The default for these is set to 2.0, probably meant to make sure the arrow doesn't overlap with another annotation such as text or a shape. I set these to 0.0 and it fixed it! I personally think it shouldn't defaulted to that... but there you go!

Categories