ppinard/matplotlib-scalebar

Matplotlib-Scalebar does not fully supports geopandas geodataframe plots

PhilipeRLeal opened this issue · 9 comments

Dear all,

I have recently tried the matplotlib-scalebar module over some geopandas geodataframe plot. I noticed that the the given module does not well support the plot. This is due to the "dx" argument. Since the geopandas plot does not have a size of pixel, the "dx" setting is akward, and prone to errors.

Sincerely yours,

Philipe Leal

If I understand you well, you know the distance between two points but not the distance per pixel?

This is what I did :

    x1, x2, y1, y2 = plt.axis()
    bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
    width, height = bbox.width, bbox.height
    px_width = fig.dpi * width
    dist = distance_function_between_points((x1, y1), (x2, y1))
    meter_per_px = dist / px_width
    scalebar = ScaleBar(meter_per_px)
    fig.gca().add_artist(scalebar)

Edit - on second thought this doesn't work. The distance isn't accurately portrayed.

The attached image is fire stations in oklahoma. I set the figure DPI to 1600 and know the image overs (from east to west) 874,494 meters (and the state is actually 749,000 m long - so it's reasonable to assume the plot is accurate). I know the pixels across the plot are 7943 so the meters per pixel is 874,494/7,943 = 110 (approx). Gimp says the bar is 1500pixels wide - so the 200m bar is actually covering 165km

Removing my DPI parameter the result (it is 2km instead of 200m).
fire_stations
fire_stations_def_dpi

I got a work around - the meter_per_px is off by a factor of figure's dpi/2. So I added the lines

meter_per_px = dist / px_width
meter_per_px_adjusted = meter_per_px*fig.dpi/2
scale_bar = ScaleBar(meter_per_px_adjusted, units="m", location="lower left")

It's not perfect, all I needed was to convey a sense of scale to the reader.

Dear Seangrogan,

thank you for your reply. What I am still wondering is the matter of the figure dpi. Assuming that the fig dpi is altered posteriori to the scalebar insertion, wouldn't it suffer from it? In another words, if the figure dpi is altered, would the scalebar precision be also altered? If that is the case, I believe that a fig.transfigure (transform) would be more formal for scalebar construction.

I thank you for your time, and I hope hearing from you soon.

Sincerely yours,

Philipe Leal

No idea, not the author. Only had a similar problem. As soon as I realized the change when changing the DPI changed the scale bar (and had some free time to look again) - it was easy to uncover the workaround.

I also realized in digging into the source code and other issues it really seems to be intended for use with calibrated pictures (MRI scans, microscope images, etc), not scatterplots.

I just needed a quick-and-dirty way to show a sense of scale in a scatterplot, not a (which, in my project, isn't pictures of states but rather city blocks).


Assuming that the fig dpi is altered posteriori to the scalebar insertion, wouldn't it suffer from it?

It didn't seem to bother it. I changed the dpi to 1000 and then changed it back to 100 before saving the plot. It seems to maintain the scale appropriately. Which makes sense as the code immediately applies the scale-bar to the plot (so anything you do to the plot . There might be a problem if you change the dpi between creating the scale bar and and adding the scale bar, i.e.

fig.dpi = 1000
scale_bar = ScaleBar(meter_per_px_adjusted, units="m", location=kwargs.get("location", "lower left"))
fig.dpi = 100  # problem!
fig.gca().add_artist(scale_bar)

1000 DPI
counties

1000 back to 100 after adding scale bar.

counties_100


Speculation: I think it's in how the prefered units are calculated, because the length_px doesn't take into account the dpi. Why dx is off by dpi/2 and not just dpi - I'm not sure.

Thank you for submitting this issue. @seangrogan is absolutely right, the library was design for calibrated images. In other words, the scale bar follows the values/units of the x-axis. I am unfamiliar with geopandas, but from what I can see in the package's documentation, the x-axis corresponds to longitudes and not distances, e.g.

image

I think this is the cause of this issue. Do you know if there is a way to plot the maps differently where the axis would use distances?

Converting WGS (lat/lon) to UTM (eastings/northings) could work. UTM's coordinates are explicitly in meters, so something that is at (10, 10) and something else that is at (16, 18) is 10 meters apart.

Plotting in WGS - following the formula from my original reply [1] I get an initial meters-per-px of 176 and adjusting using [2] I get 88154.

Plotting in UTM, I get 172 (similar order)[1], but the scale comes in on the order of Mm

StationsUTM

So... with a little experimentation - i found the adjustment needed was

k = fig.dpi * (6 / 1_000_000)
meter_per_px_adjusted = meter_per_px * k  
# etc... [2]

and get the 'adjusted value' of 1.03....

StationsUTM

for context, the extent of the UTM image in the x axis is [102_609, 957_664] and y is [3_709_784, 4_122_437] (and these units are meters)


[1]

x1, x2, y1, y2 = plt.axis()
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width, height = bbox.width, bbox.height
px_width = fig.dpi * width
dist = distance_function_between_points((x1, y1), (x2, y1))
meter_per_px = dist / px_width
scalebar = ScaleBar(meter_per_px)
fig.gca().add_artist(scalebar)

[2]

meter_per_px = dist / px_width
meter_per_px_adjusted = meter_per_px*fig.dpi/2
scale_bar = ScaleBar(meter_per_px_adjusted, units="m", location="lower left")

Note: I use the following to convert lat-lon to UTM:

https://github.com/Turbo87/utm/

keep in mind that a UTM coordinate has a cardinality of 4, not 2 i.e. (EASTING, NORTHING, ZONE NUMBER, ZONE LETTER)


Edit: I probably should label the axis for clarity.

StationsUTM

stations

I think if the extent of the axes are in meters, you should just need to set dx to 1.0, regardless of the dpi. The scale bar is drawn in the same reference frame as the plot. The fact that your meter_per_px_adjusted comes close to 1.0 makes me think that dx should indeed be 1.0. It is mentioned in the README, but perhaps it is not very clear:

Set dx to 1.0 if the axes image has already been calibrated by setting its extent.

Could you try with another example to check if my logic works? If it works, I can add a note in the README for other users of geopandas.

It is mentioned in the README, but perhaps it is not very clear:

Set dx to 1.0 if the axes image has already been calibrated by setting its extent.

Ahhhhh! It's not so much it isn't clear as it didn't really occur to me (or seem relevant) as I wasn't using an image (CT scan, whatever). Probably because I've actually never used MatPlotLib in this manner - I didn't quite understand it.

Using UTM coordinates, it does indeed still plot accurately as 150KM

StationsUTM

With this newfound understanding, for WGS (Lat/Lon) I used this modified code:

x1, x2, y1, y2 = ax.axis()
_y = (y1+y2)/2
p1, p2 = (int(x1), _y), (int(x1)+1, _y)
meter_per_deg = great_circle_calculator.distance_between_points(p1, p2)
scale_bar = ScaleBar(meter_per_deg, units="m", location=kwargs.get("location", "lower left"),
                     fixed_value=kwargs.get("fixed_value", None), fixed_units=kwargs.get("fixed_units", None))
fig.gca().add_artist(scale_bar)

It plots a scalebar at about 35.3 North like so:

stations

Distance will vary based on Latitude (hence the _y = (y1+y2)/2). It might be hard to see in the following picture, but the top scale bar is (about) 850 px across and the bottom is 893 px across (Using gimp to measure it)

stations - two scale bars

Theoretically, you could use any two points on the same latitude that are one degree apart in longitude (y), e.g.:(-0.5, 35.3) and (0.5, 35.3)


Also, because math is hard, I never realized that meter_per_px_adjusted was both multiplied by and divided by fig.dpi. Doh moment.


Could you try with another example to check if my logic works? If it works, I can add a note in the README for other users of geopandas.

If you don't mind a proposal for this :

If you're attempting to plot data that are geospatial coordinates (such as using scatterplots to plot the location of structures): if you're using a coordinate system based on UTM where the X and Y are in meters, simply set dx=1. If you are plotting geospatial coordinates based on WGS, NAD where X and Y are in latitude (Y) and longitude (X), you will need to compute the distance between two points at the latitude (Y) you wish to have the scale represent that are also one full degree of longitude (X) apart, in meters. For example dx = great_circle_distance((X, Y), (X+1, Y))


Edit : Also.... Thanks for your help Philippe @ppinard and engaging with us to find a solution. I really appreciate it!