matplotlib and apect ratio of geographical-data plots - python

I process geographical information and present the results using
matplotlib. All input is lattitude/longitude [degree]. I convert into
x/y [meter] for my calculations. And I present my results in
lattitude/longitude. The problem is to get the graphs aspect-ratio
right: All graphs are too wide. Is there a standard procedure to set the
correct aspect-ratio so I can simply draw my scatter and other diagrams
using lat/lon and the result has the correct shape? On screen and on
paper (png)?
[added this part later]
This is a bare-bone stripped version of my problem. I need actual lat/lon values
around the axes and an accurate shape (square). Right now it appears wide (2x).
import math
import matplotlib.pyplot as plt
import numpy as np
from pylab import *
w=1/math.cos(math.radians(60.0))
plt_area=[0,w,59.5,60.5] #60deg North, adjacent to the prime meridian
a=np.zeros(shape=(300,300))
matshow(a, extent=plt_area)
plt.grid(False)
plt.axis(plt_area)
fig = plt.gcf()
fig.set_size_inches(8,8)
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
plt.show()

It seems I found the solution.
And I found it here: How can I set the aspect ratio in matplotlib?
import math
import matplotlib.pyplot as plt
import numpy as np
w=1/math.cos(math.radians(60.0))
plt_area=[0,w,59.5,60.5] #square area
a=np.zeros(shape=(300,300))
fig = plt.figure()
ax = fig.add_subplot(111)
ax.imshow(a)
plt.grid(False)
ax.axis(plt_area)
fig = plt.gcf()
fig.set_size_inches(8,8)
ax.set_aspect(w)
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
plt.show()

In matplotlib I usually change the figure size like this:
import matplotlib.pyplot as plt
plt.clf()
fig = plt.figure()
fig_p = plt.gcf()
fig_p.set_size_inches(8, 8) # x, y
However this sets the dimensions for the figure outer dimensions, not the plot area. You can change the plot area relative to the figure size given in ratios of the total figure size lengths of x and y respectively:
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
As long as the the relative ratios stay symmetrically the aspect ratio should be the same for the plot are.
Example 1:
plt.clf()
fig = plt.figure()
fig_p = plt.gcf()
fig_p.set_size_inches(5, 5) # x, y for figure canvas
# Relative distance ratio between origin of the figure and max extend of canvas
fig.subplots_adjust(left=0.2, right=0.8, bottom=0.2, top=0.8)
ax1 = fig.add_subplot(111)
xdata = [rand()*10 for i in xrange(100)]
ydata = [rand()*1 for i in xrange(100)]
ax1.plot(xdata, ydata, '.b', )
ax1.set_xlabel('Very Large X-Label', size=30)
plt.savefig('squareplot.png', dpi=96)
Example 2:
fig.subplots_adjust(left=0.0, right=1.0, bottom=0.0, top=1.0)
Plot area fills the figure size completely:

Don't try to fix this by fiddling fig.set_size_inches() or fig.subplots_adjust() or by changing your data; instead use a Mercator projection.
You can get a quick and dirty Mercator projection by using an aspect ratio of the reciprocal of the cosine of the mean latitude of your data. This is "pretty good" for data contained in about 1 degree of latitude, which is about 100 km. (You have to decide if, for your application, this is "good enough". If it isn't, you really have to consider some serious geographical projection libraries...)
Example:
from math import cos, radians
import matplotlib.pyplot as plt
import numpy as np
# Helsinki 60.1708 N, 24.9375 E
# Helsinki (lng, lat)
hels = [24.9375, 60.1708]
# a point 100 km directly north of Helsinki
pt_N = [24.9375, 61.0701]
# a point 100 km east of Helsinki along its parallel
pt_E = [26.7455, 60.1708]
coords = np.array([pt_N, hels, pt_E])
plt.figure()
plt.plot(coords[:,0], coords[:,1])
# either of these will estimate the "central latitude" of your data
# 1) do the plot, then average the limits of the y-axis
central_latitude = sum(plt.axes().get_ylim())/2.
# 2) actually average the latitudes in your data
central_latitude = np.average(coords, 0)[1]
# calculate the aspect ratio that will approximate a
# Mercator projection at this central latitude
mercator_aspect_ratio = 1/cos(radians(central_latitude))
# set the aspect ratio of the axes to that
plt.axes().set_aspect(mercator_aspect_ratio)
plt.show()
I picked Helsinki for the example since at that latitude the aspect ratio is almost 2... because two degrees of longitude is the about same distance as one degree of latitude.
To really see this work: a) run the above, b) resize the window. Then comment out the call to set_aspect() and do the same. In the first case, the correct aspect ratio is maintained, in the latter you get nonsensical stretching.
The points 100km north and east of Helsinki were calculated/confirmed by the EXCELLENT page calculating distances between lat/lng points at Movable Type Scripts

Related

How to evenly spread annotation imageboxes around a scatterplot?

I would like to annotate a scatterplot with images corresponding to each datapoint. With standard parameters the images end up clashing with each other and other important features such as legend axis, etc. Thus, I would like the images to form a circle or a rectangle around the main scatter plot.
My code looks like this for now and I am struggling to modify it to organise the images around the center point of the plot.
import matplotlib.cbook as cbook
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import seaborn as sns
#Generate n points around a 2d circle
def generate_circle_points(n, centre_x, center_y, radius=1):
"""Generate n points around a circle.
Args:
n (int): Number of points to generate.
centre_x (float): x-coordinate of circle centre.
center_y (float): y-coordinate of circle centre.
radius (float): Radius of circle.
Returns:
list: List of points.
"""
points = []
for i in range(n):
angle = 2 * np.pi * i / n
x = centre_x + radius * np.cos(angle)
y = center_y + radius * np.sin(angle)
points.append([x, y])
return points
fig, ax = plt.subplots(1, 1, figsize=(7.5, 7.5))
data = pd.DataFrame(data={'x': np.random.uniform(0.5, 2.5, 20),
'y': np.random.uniform(10000, 50000, 20)})
with cbook.get_sample_data('grace_hopper.jpg') as image_file:
image = plt.imread(image_file)
# Set logarithmic scale for x and y axis
ax.set(xscale="log", yscale='log')
# Add grid
ax.grid(True, which='major', ls="--", c='gray')
coordianates = generate_circle_points(n=len(data),
centre_x=0, center_y=0, radius=10)
# Plot the scatter plot
scatter = sns.scatterplot(data=data, x='x', y='y', ax=ax)
for index, row in data.iterrows():
imagebox = OffsetImage(image, zoom=0.05)
imagebox.image.axes = ax
xy = np.array([row['x'], row['y']])
xybox = np.array(coordianates[index])
ab = AnnotationBbox(imagebox, xy,
xycoords='data',
boxcoords="offset points",
xybox=xybox,
pad=0)
ax.add_artist(ab)
for the moment the output looks like this:enter image description here
Ideally I would like the output to look to something like this:
enter image description here
Many thanks in advance for your help
Not an answer but a long comment:
You can control the location of the arrows, but sometimes it is easier to export figures as SVGs and edit them in Adobe Illustrator or Inkscape.
R has a dodge argument which is really nice, but even then is not always perfect. Solutions in Python exist but are laborious.
The major issue is that this needs to be done last as alternations to the plot would make it problematic. A few points need mentioning.
Your figures will have to have a fixed size (57mm / 121mm / 184mm for Science, 83mm / 171mm for RSC, 83mm / 178mm for ACS etc.), if you need to scale the figure in Illustrator keep note of the scaling factor, adding it as a textbox outside of the canvas —as the underlying plot will need to be replaced at least once due to Murphy's law. Exporting at the right size the SVG is ideal. Sounds silly, but it helps. Likewise, make sure the font size does not go under the minimum spec (7-9 points).

Matplotlib using Wedge() in polar plots

TL/DR: How to use Wedge() in polar coordinates?
I'm generating a 2D histogram plot in polar coordinates (r, theta). At various values of r there can be different numbers of theta values (to preserve equal area sized bins). To draw the color coded bins I'm currently using pcolormesh() calls for each radial ring. This works ok, but near the center of the plot where there may be only 3 bins (each 120 degrees "wide" in theta space), pcolormesh() draws triangles that don't "sweep" out full arc (just connecting the two outer arc points with a straight line).
I've found a workaround using ax.bar() call, one for each radial ring and passing in arrays of theta values (each bin rendering as an individual bar). But when doing 90 rings with 3 to 360 theta bins in each, it's incredibly slow (minutes).
I tried using Wedge() patches, but can't get them to render correctly in the polar projection. Here is sample code showing both approaches:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Wedge
from matplotlib.collections import PatchCollection
# Theta coordinates in degrees
theta1=45
theta2=80
# Radius coordinates
r1 = 0.4
r2 = 0.5
# Plot using bar()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
theta_mid = np.deg2rad((theta1 + theta2)/2)
theta_width = np.deg2rad(theta2 - theta1)
height = r2 - r1
ax.bar(x=theta_mid, height = height, width=theta_width, bottom=r1)
ax.set_rlim(0, 1)
plt.savefig('bar.png')
# Plot using Wedge()
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
patches = []
patches.append( Wedge(center=(0, 0), r = r1, theta1=theta1, theta2=theta2, width = r2-r1, color='blue'))
p = PatchCollection(patches)
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')
The outputs of each are:
Bar
Wedge
I've tried using radians for the wedge (because polar plots usually want their angle values in radians). That didn't help.
Am I missing something in how I'm using the Wedge? If I add thousands of Wedges to my Patch collection should I have any expectation it will be faster than bar()?
Thinking this was an actual bug, I opened this issue https://github.com/matplotlib/matplotlib/issues/22717 on matplotlib where one of the maintainers nicely pointed out that I should be using Rectangle() instead of Wedge().
The solution they provided is
from matplotlib.patches import Rectangle
fig, ax = plt.subplots(figsize=[6,6], subplot_kw={'projection': 'polar'})
p = PatchCollection([Rectangle((np.deg2rad(theta1), r1), theta_width, height, color='blue')])
ax.add_collection(p)
ax.set_rlim(0, 1)
plt.savefig('wedge.png')

Fine-tuning of pcolor() polar plot

I have a 64x360 Matrix of values belonging to radial and azimuthal coordinates. I want to visualize them in two plots: a cartesian and a polar plot.
I visualized the heatmap in cartesian coordinates using imshow():
import numpy as np
import matplotlib.pyplot as plt
P=np.loadtxt('Pdata.csv')
print np.shape(P)
plt.imshow(P)
plt.xlabel('radius')
plt.ylabel('theta')
plt.show()
This gives me the desired plot:
The same plot in polar coordinates was also pretty straigh forward using pcolor():
r=np.arange(0,np.shape(P)[1],1)
t=np.arange(0,np.shape(P)[0],1)
R,T = np.meshgrid(r,t)
fig = plt.figure()
ax = fig.add_subplot(111, polar = True)
ax.pcolor(T,R,P)
plt.show()
However, I am not really satisfied with the result:
The resolution of the plot seems to be pretty limited so that it's not possible to distinguish between angles with higher intensity and lower intensity, as it is in the cartesian plot. The whole solid angle seems to be divided into six or seven "cake wedges" only. Is there an easy and pythonic way to enhance the angular resolution?
Ok, I found out something. It works with:
t = np.radians(np.linspace(0, np.shape(P)[0],np.shape(P)[0]))
r = np.arange(0, np.shape(P)[1], 1)
Just as seen here: Polar contour plot in matplotlib - best (modern) way to do it?

How do I force scatter points real pixel values when plotting in pyplot/python?

I've taken an image and extracted some features from it using OpenCv. I'd like to replot those points and their respective areas (which are real pixel values) into a scatter window and then save it. Unfortunately, when I plot the points, they resize to stay more visible. If I zoom in they resize. I'd like to save the whole figure retaining the actual ratio of pixel (x,y) coordinates to size of points plotted.
For instance:
import matplotlib.pyplot as plt
x=[5000,10000,20000]
y=[20000,10000,5000]
area_in_pixels=[100,200,100]
scatter(x,y,s=area_in_pixels)
I would like this to produce tiny dots on the image. They should span like 10 xy units. However, the dots it produces are large, and appear to span 1000 xy units.
I've tried resizing the image with:
plt.figure(figsize=(10,10))
Which seems to resize the points relative to their position a little. But I'm not sure what scale I would select to make this accurate. DPI settings on plt.figsave seem to make the saved image larger but don't appear to alter relative spot sizes.
Asked another way, is there another way to relate the s which is in points^2 to a real number or to the units of the x-y axis?
You can use patches to create markers sized relative to the data coordinates.
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
xData=[5000,10000,20000, 15000]
yData=[20000,10000,5000, 15000]
radius_in_pixels=[100,200,100, 1000] # Circle takes radius as an argument. You could convert from area.
fig = plt.figure()
ax = fig.add_subplot(111, aspect='equal')
for x, y, r in zip(xData, yData, radius_in_pixels):
ax.add_artist(Circle(xy=(x, y), radius = r))
plt.xlim(0, max(xData) + 200)
plt.ylim(0, max(yData) + 200)
plt.show()

plot circle on unequal axes with pyplot

I would like to plot a circle on an auto-scaled pyplot-generated graphic. When I run
ax.get_aspect()
hoping for a value with which I could manipulate the axes of a ellipse, pyplot returns:
auto
which is less than useful. What methods would you suggest for plotting a circle on a pyplot plot with unequal axes?
This question is more than one year old, but I too just had this question. I needed to add circles to a matplotlib plot and I wanted to be able to specify the circle's location in the plot using data coordinates, and I didn't want the circle radius to change with panning/zooming (or worse the circle turning into an ellipse).
The best and most simple solution that I've found is simply plot a curve with a single point and include a circle marker:
ax.plot(center_x,center_y,'bo',fillstyle='none',markersize=5)
which gives a nice, fixed-size blue circle with no fill!
It really does depend what you want it for.
The problem with defining a circle in data coordinates when aspect ratio is auto, is that you will be able to resize the figure (or its window), and the data scales will stretch nicely. Unfortunately, this would also mean that your circle is no longer a circle, but an ellipse.
There are several ways of addressing this. Firstly, and most simply, you could fix your aspect ratio and then put a circle on the plot in data coordinates:
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = plt.axes()
ax.set_aspect(1)
theta = np.linspace(-np.pi, np.pi, 200)
plt.plot(np.sin(theta), np.cos(theta))
plt.show()
With this, you will be able to zoom and pan around as per usual, but the shape will always be a circle.
If you just want to put a circle on a figure, independent of the data coordinates, such that panning and zooming of an axes did not effect the position and zoom on the circle, then you could do something like:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = plt.axes()
patch = mpatches.Circle((325, 245), 180, alpha=0.5, transform=None)
fig.artists.append(patch)
plt.show()
This is fairly advanced mpl, but even so, I think it is fairly readable.
HTH,
Building on #user3208430, if you want the circle to always appear at the same place in the axes (regardless of data ranges), you can position it using axes coordinates via transform:
ax.plot(.94, .94, 'ro', fillstyle='full', markersize=5, transform=ax.transAxes)
Where x and y are between [0 and 1]. This example places the marker in the upper right-hand corner of the axes.

Categories

Resources