Plot LINESTRING Z from GeoDataFrame using pydeck's PathLayer (or TripLayer) - geopandas

I have a geodataframe with LINESTRING Z geometries:
TimeUTC
Latitude
Longitude
AGL
geometry
0
2021-06-16 00:34:04+00:00
42.8354
-70.9196
82.2
LINESTRING Z (42.83541343273769 -70.91961015378617 82.2, 42.83541343273769 -70.91961015378617 82.2)
1
2021-06-14 13:32:18+00:00
42.8467
-70.8192
66.3
LINESTRING Z (42.84674080836037 -70.81919357049679 66.3, 42.84674080836037 -70.81919357049679 66.3)
2
2021-06-18 23:56:05+00:00
43.0788
-70.7541
0.9
LINESTRING Z (43.07882882269921 -70.75414567194126 0.9, 43.07884601143309 -70.75416286067514 0, 43.07885174101104 -70.75416286067514 0, 43.07884028185512 -70.75415713109717 0, 43.07884601143309 -70.75414567194126 0, 43.07884601143309 -70.75414567194126 0)
I can plot the component points using pydeck's ScatterplotLayer using the raw
(not geo) dataframe but I need to also plot the full, smooth, track.
I've tried this:
layers = [
pdk.Layer(
type = "PathLayer",
data=tracks,
get_path="geometry",
width_scale=20,
width_min_pixels=5,
get_width=5,
get_color=[180, 0, 200, 140],
pickable=True,
),
]
view_state = pdk.ViewState(
latitude=gdf_polygon.centroid.x,
longitude=gdf_polygon.centroid.y,
zoom=6,
min_zoom=5,
max_zoom=15,
pitch=40.5,
bearing=-27.36)
r = pdk.Deck(layers=[layers], initial_view_state=view_state)
return(r)
Which silently fails. Try as I might, I cannot find a way to convert the
LINESTRING Z's (and I can do without the Z component if need be) to an object
that pydeck will accept.

I found a way to extract the info needed from GeoPandas and make it work in pydeck. You just need to apply a function that extracts the coordinates from the shapely geometries as a list. Here is a fully reproducible example:
import shapely
import numpy as np
import pandas as pd
import pydeck as pdk
import geopandas as gpd
linestring_a = shapely.geometry.LineString([[0,1,2],
[3,4,5],
[6,7,8]])
linestring_b = shapely.geometry.LineString([[7,15,1],
[8,14,2],
[9,13,3]])
multilinestring = shapely.geometry.MultiLineString([[[10,11,2],
[13,14,5],
[16,17,8]],
[[19,10,11],
[12,15,4],
[10,13,0]]])
gdf = gpd.GeoDataFrame({'id':[1,2,3],
'geometry':[linestring_a,
linestring_b,
multilinestring],
'color_hex':['#ed1c24',
'#faa61a',
'#ffe800']})
# Function that transforms a hex string into an RGB tuple.
def hex_to_rgb(h):
h = h.lstrip("#")
return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4))
# Applying the HEX-to-RGB function above
gdf['color_rgb'] = gdf['color_hex'].apply(hex_to_rgb)
# Function that extracts the 2d list of coordinates from an input geometry
def my_geom_coord_extractor(input_geom):
if (input_geom is None) or (input_geom is np.nan):
return []
else:
if input_geom.type[:len('multi')].lower() == 'multi':
full_coord_list = []
for geom_part in input_geom.geoms:
geom_part_2d_coords = [[coord[0],coord[1]] for coord in list(geom_part.coords)]
full_coord_list.append(geom_part_2d_coords)
else:
full_coord_list = [[coord[0],coord[1]] for coord in list(input_geom.coords)]
return full_coord_list
# Applying the coordinate list extractor to the dataframe
gdf['coord_list'] = gdf['geometry'].apply(my_geom_coord_extractor)
gdf_polygon = gdf.unary_union.convex_hull
# Establishing the default view for the pydeck output
view_state = pdk.ViewState(latitude=gdf_polygon.centroid.coords[0][1],
longitude=gdf_polygon.centroid.coords[0][0],
zoom=4)
# Creating the pydeck layer
layer = pdk.Layer(
type="PathLayer",
data=gdf,
pickable=True,
get_color='color_rgb',
width_scale=20,
width_min_pixels=2,
get_path="coord_list",
get_width=5,
)
# Finalizing the pydeck output
r = pdk.Deck(layers=[layer], initial_view_state=view_state, tooltip={"text": "{id}"})
r.to_html("path_layer.html")
Here's the output it yields:
Big caveat
It seems like pydeck isn't able to deal with MultiLineString geometries. Notice how, in the example above, my original dataframe had 3 geometries, but only 2 lines were drawn in the screenshot.

Related

convert hued displot of X to plot of hue vs mode(X given hue)?

I have a Seaborn displot with a hued variable:
For each hued variable, I want to extract the mode of the density estimate and then plot each hue variable versus its mode, like so:
How do I do this?
You can use scipy.stats.gaussian_kde to create the density estimation function. And then call that function on an array of x-values to calculate its maximum.
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.normal(0.001, 1, 1300).cumsum() + 30,
'hue': np.repeat(np.arange(0.08, 0.20001, 0.01), 100).round(2)})
g = sns.displot(df, x='x', hue='hue', palette='turbo', kind='kde', fill=True, height=6, aspect=1.5)
plt.show()
from scipy.stats import gaussian_kde
from matplotlib.cm import ScalarMappable
fig, ax = plt.subplots(figsize=(10, 6))
hues = df['hue'].unique()
num_hues = len(hues)
colors = sns.color_palette('turbo', num_hues)
xmin, xmax = df['x'].min(), df['x'].max()
xs = np.linspace(xmin, xmax, 500)
for hue, color in zip(hues, colors):
data = df[df['hue'] == hue]['x'].values
kde = gaussian_kde(data)
mode_index = np.argmax(kde(xs))
mode_x = xs[mode_index]
sns.scatterplot(x=[hue], y=[mode_x], color=color, s=50, ax=ax)
cmap = sns.color_palette('turbo', as_cmap=True)
norm = plt.Normalize(hues.min(), hues.max())
plt.colorbar(ScalarMappable(cmap=cmap, norm=norm), ax=ax, ticks=hues)
plt.show()
Here is another approach, extracting the kde curves. It uses the legend of the kde plot to get the correspondence between the curves and the hue values. sns.kdeplot is the axes-level function used by sns.displot(kind='kde'). fill=False creates lines instead of filled polygons for the curves, for which the values are easier to extract. (ax1.fill_between can fill the curves during a second pass). The x and y axes of the second plot are switched to align the x-axes of both plots.
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
df = pd.DataFrame({'x': np.random.normal(0.007, 0.1, 1300).cumsum() + 30,
'hue': np.repeat(np.arange(0.08, 0.20001, 0.01), 100).round(2)})
fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=(12, 10), sharex=True)
sns.kdeplot(data=df, x='x', hue='hue', palette='turbo', fill=False, ax=ax1)
hues = [float(txt.get_text()) for txt in ax1.legend_.get_texts()]
ax2.set_yticks(hues)
ax2.set_ylabel('hue')
for hue, line in zip(hues, ax1.lines[::-1]):
color = line.get_color()
x = line.get_xdata()
y = line.get_ydata()
ax1.fill_between(x, y, color=color, alpha=0.3)
mode_ind = np.argmax(y)
mode_x = x[mode_ind]
sns.scatterplot(x=[mode_x], y=hue, color=color, s=50, ax=ax2)
sns.despine()
plt.tight_layout()
plt.show()

How to add elevation to a buffer polygon in geopandas

How would I add a zero elevation value to each xy component of a buffer polygon, i.e.
Transform POLYGON ((0.20000 0.00000, 0.19904 -0.01960, ...)) into POLYGON ((0.20000 0.00000 0, 0.19904 -0.01960 0, ...))
I think you're asking how you can add a third dimension to your shapely objects and have them set to zero by default.
Here's a way to do so:
import shapely.ops
def add_zero_z(geom):
def _add_zero_z(x, y):
return x, y, [0 for _ in x]
return shapely.ops.transform(_add_zero_z, geom)
Here's an example of how to use it:
import shapely.geometry
x_2D = shapely.geometry.Point((0,0))
print(x_2D.wkt)
# POINT (0 0)
x_3D = add_zero_z(x_2D)
print(x_3D.wkt)
# POINT (0 0 0)
You can also apply the function to a whole GeoDataFrame:
import geopandas as gpd
gdf = gpd.GeoDataFrame({'id':[1,2],
'geometry':[shapely.geometry.Point((0,0)),
shapely.geometry.Point((1,1))]},
geometry='geometry')
# Applying the function to the "geometry" column and replacing it
gdf['geometry'] = gdf['geometry'].apply(lambda geom: add_zero_z(geom))
print(gdf)
# id geometry
# 0 1 POINT Z (0.00000 0.00000 0.00000)
# 1 2 POINT Z (1.00000 1.00000 0.00000)
Credit
My idea for this answer came from this post by user mikewatt.

Numpy custom named datatype slice

I am creating a 3D sphere with 2D grid by selected latitudes and longitudes, which show the cartesian coordinates. This grid represent the key points to draw 3D sphere. Than I am creating X,Y,Z 3D coordinates values from these cartesian coordinates with well known formula. Drawn sphere is shown in the attached picture. I am using a numpy named datatype as
np3d = np.dtype([('X', np.float), ('Y', np.float), ('Z', np.float)])
for 3D coordinates and
np2d = np.dtype([('L', np.float), ('B', np.float)])
for latitude/longitude grid. My python code is
import numpy as np, math
import pandas as pd
from matplotlib import pyplot as plt, ticker, patches, font_manager as fmng
from matplotlib.widgets import Cursor, MultiCursor
from pathlib import Path
from datetime import datetime
fig3d = plt.figure('3D Sphere', figsize=(9.5,9.5))
fig3d.subplots_adjust(left=0.04, bottom=0.07, top=0.97, right=0.97, wspace=0, hspace=0)
prmgraf = dict(axis="both", direction='in',top=True, right=True)
ax3 = fig3d.add_subplot(111, projection='3d')
ax3.grid(False)
ax3.minorticks_on()
ax3.yaxis.set_minor_locator(ticker.AutoMinorLocator(5))
ax3.xaxis.set_minor_locator(ticker.AutoMinorLocator(5))
ax3.zaxis.set_minor_locator(ticker.AutoMinorLocator(5))
ax3.tick_params(which='major', length=4, **prmgraf)
ax3.tick_params(which='minor', length=3, **prmgraf)
ax3.set_xlabel('Axis X')
ax3.set_ylabel('Axis Y')
ax3.set_zlabel('Axis Z')
ax3.set_xlim(-15.0, 15.0)
ax3.set_ylim(-15.0, 15.0)
ax3.set_zlim(-15.0, 15.0)
# azim elev
ax3.view_init(0., 180.)
# ---------------------------- settings ------------------------------
nLat = np.vstack(np.radians(np.arange(-65., 70., 5.)))
nLon = np.radians(np.arange(-180., 185., 5.))
np3d = np.dtype([('X', np.float), ('Y', np.float), ('Z', np.float)])
np2d = np.dtype([('L', np.float), ('B', np.float)])
LatN, LonN = (len(nLat), len(nLon))
SphrRadius = 14.5
#2D cartesian coordinates
pSphr = np.zeros(shape=(LatN, LonN), dtype=np2d)
pSphr['L'] = nLat
pSphr['B'] = nLon
#3D sphere coordinates
Spher = np.zeros(shape=(LatN, LonN), dtype=np3d)
Spher['X'] = SphrRadius*np.cos(pSphr['L'])*np.sin(pSphr['B'])
Spher['Y'] = SphrRadius*np.sin(pSphr['L'])
Spher['Z'] = SphrRadius*np.cos(pSphr['L'])*np.cos(pSphr['B'])
# draw sphere latitudes
for i in range(LatN):
kx = Spher[i,:]['X']
ky = Spher[i,:]['Y']
kz = Spher[i,:]['Z']
ax3.plot3D(kx, ky, kz, c='k',lw=0.5)
plt.show()
Z values of the sphere are changing between -15.0 and 15.0. I want to select ONLY POSITIVE Z VALUES in the "Spher variable". In other words, I want to draw half of the sphere in the Z direction. How can i do that in named datatype? Thanks for now to the friends who will answer.
I found solution as
Spher[(Spher['Z'] < 0.)] = np.nan
But this is not the solution I expected. I want to select all points into a new variable as
ZPositive = Spher[(Spher['Z'] < 0.)]
But in this way, 2D data structure change into 1D data.

Matplotlib animate space vs time plot

I'm currently working on traffic jams analysis and was wondering if there's a way to animate the generation of a plot of such jams.
A plot of this things grow from up to the lower end of the figure, each 'row' is a time instance. The horizontal axis is just the road indicating at each point the position of each vehicle and, with a certain numeric value, the velocity of it. So applying different colors to different velocities, you get a plot that shows how a jam evolves through time in a given road.
My question is, how can I use matplotlib to generate an animation of each instance of the road in time to get such a plot?
The plot is something like this:
I'm simulating a road with vehicles with certain velocities through time, so I wish to animate a plot showing how the traffic jams evolve...
EDIT:
I add some code to make clear what I'm already doing
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation, rc
plt.rcParams['animation.ffmpeg_path'] = u'/usr/bin/ffmpeg'
# model params
vmax = 5
lenroad = 50
prob = 0.4
# sim params
numiters = 10
# traffic model
def nasch():
gaps = np.full(road.shape, -1)
road_r4 = np.full(road.shape, -1)
for n,x in enumerate(road):
if x > -1:
d = 1
while road[(n+d) % len(road)] < 0:
d += 1
d -= 1
gaps[n] = d
road_r1 = np.where(road!=-1, np.minimum(road+1, vmax), -1)
road_r2 = np.where(road_r1!=-1, np.minimum(road_r1, gaps), -1)
road_r3 = np.where(road_r2!=-1, np.where(np.random.rand() < prob, np.maximum(road-1, 0), road), -1)
for n,x in enumerate(road_r3):
if x > -1:
road_r4[(n+x) % len(road_r3)] = x
return road_r4
def plot_nasch(*args):
road = nasch()
plot.set_array([road])
return plot,
# init road
road = np.random.randint(-10, vmax+1, [lenroad])
road = np.where(road>-1, road, -1)
# simulate
fig = plt.figure()
plot = plt.imshow([road], cmap='Pastel2', interpolation='nearest')
for i in range(numiters):
ani = animation.FuncAnimation(fig, plot_nasch, frames=100, interval=500, blit=True)
plt.show()
And I get the following figure, just one road instead of each road painted at the bottom of the previous one:
This is possibly what you want, although I'm not sure why you want to animate the time, since time is already one of the axes in the plot.
The idea here is to store the simulation results of a time-step row by row in an array and replot this array. Thereby previous simulation results are not lost.
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import animation, rc
# model params
vmax = 5
lenroad = 50
prob = 0.4
# sim params
numiters = 25
# traffic model
def nasch():
global road
gaps = np.full(road.shape, -1)
road_r4 = np.full(road.shape, -1)
for n,x in enumerate(road):
if x > -1:
d = 1
while road[(n+d) % len(road)] < 0:
d += 1
d -= 1
gaps[n] = d
road_r1 = np.where(road!=-1, np.minimum(road+1, vmax), -1)
road_r2 = np.where(road_r1!=-1, np.minimum(road_r1, gaps), -1)
road_r3 = np.where(road_r2!=-1, np.where(np.random.rand() < prob, np.maximum(road-1, 0), road), -1)
for n,x in enumerate(road_r3):
if x > -1:
road_r4[(n+x) % len(road_r3)] = x
return road_r4
def plot_nasch(i):
print i
global road
road = nasch()
#store result in array
road_over_time[i+1,:] = road
# plot complete array
plot.set_array(road_over_time)
# init road
road = np.random.randint(-10, vmax+1, [lenroad])
road = np.where(road>-1, road, -1)
# initiate array
road_over_time = np.zeros((numiters+1, lenroad))*np.nan
road_over_time[0,:] = road
fig = plt.figure()
plot = plt.imshow(road_over_time, cmap='Pastel2', interpolation='nearest', vmin=-1.5, vmax=6.5)
plt.colorbar()
ani = animation.FuncAnimation(fig, plot_nasch, frames=numiters, init_func=lambda : 1, interval=400, blit=False, repeat=False)
plt.show()

matplotlib: jpg image special effect/transformation as if its on a page of an open book

I wish to appear a figure (and certain text) as if they are printed on a page of an open book. Is it possible to transform an jpg image programmatically or in matplotlib to have such an effect?
You can use a background axis along with an open source book image to do something like this,
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax2 = fig.add_axes([0.2, 0.3, 0.25, 0.3])
#Plot page from a book
im = plt.imread("./book_page.jpg")
implot = ax1.imshow(im, origin='lower')
# Plot a graph and set background to transparent
x = np.linspace(0,4.*np.pi,40)
y = np.sin(x)
ax2.plot(x,y,'-ro',alpha=0.5)
ax2.set_ylim([-1.1,1.1])
ax2.patch.set_alpha(0.0)
from matplotlib import rc
rc('text', usetex=True)
margin = im.shape[0]*0.075
ytext = im.shape[1]/2.+10
ax1.text(margin, ytext, "The following text is an example")
ax1.text(margin, 90, "Figure 1. Showing a sine function")
plt.show()
Which looks like this,
where I used the following book image.
UPDATE: Added non-affine transformation based on scikit-image warp example, but with Maxwell distribution. The solution saves the matplotlib line as an image in order to apply a pointwise transform. Mapping for vector graphics may be possible but I think this will be more complicated...
import numpy as np
import matplotlib.pyplot as plt
def maxwellian_transform_image(image):
from skimage.transform import PiecewiseAffineTransform, warp
rows, cols = image.shape[0], image.shape[1]
src_cols = np.linspace(0, cols, 20)
src_rows = np.linspace(0, rows, 10)
src_rows, src_cols = np.meshgrid(src_rows, src_cols)
src = np.dstack([src_cols.flat, src_rows.flat])[0]
# add maxwellian to row coordinates
x = np.linspace(0, 3., src.shape[0])
dst_rows = src[:, 1] + (np.sqrt(2/np.pi)*x**2 * np.exp(-x**2/2)) * 50
dst_cols = src[:, 0]
dst_rows *= 1.5
dst_rows -= 1.0 * 50
dst = np.vstack([dst_cols, dst_rows]).T
tform = PiecewiseAffineTransform()
tform.estimate(src, dst)
out_rows = image.shape[0] - 1.5 * 50
out_cols = cols
out = warp(image, tform, output_shape=(out_rows, out_cols))
return out
#Create the new figure
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
#Plot page from a book
im = plt.imread("./book_page.jpg")
implot = ax.imshow(im, origin='lower')
# Plot and save graph as image, will need some manipulation of location
temp, at = plt.subplots()
margin = im.shape[0]*0.1
x = np.linspace(margin,im.shape[0]/2.,40)
y = im.shape[1]/3. + 0.1*im.shape[1]*np.sin(12.*np.pi*x/im.shape[0])
at.plot(x,y,'-ro',alpha=0.5)
temp.savefig("lineplot.png",transparent=True)
#Read in plot as an image and apply transform
plot = plt.imread("./lineplot.png")
out = maxwellian_transform_image(plot)
ax.imshow(out, extent=[0,im.shape[1],0,im.shape[0]])
plt.show()
The figure now looks like,

Resources