Skip to content

Commit

Permalink
Merge pull request #19 from raphaelquast/dev
Browse files Browse the repository at this point in the history
merge for v0.1.7
  • Loading branch information
raphaelquast authored Oct 15, 2021
2 parents b4ae8f0 + c7a3784 commit c31c559
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 31 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[![tests](https://github.com/raphaelquast/EOmaps/actions/workflows/testMaps.yml/badge.svg?branch=master)](https://github.com/raphaelquast/EOmaps/actions/workflows/testMaps.yml)
[![codecov](https://codecov.io/gh/raphaelquast/EOmaps/branch/dev/graph/badge.svg?token=25M85P7MJG)](https://codecov.io/gh/raphaelquast/MapIt)
[![codecov](https://codecov.io/gh/raphaelquast/EOmaps/branch/dev/graph/badge.svg?token=25M85P7MJG)](https://codecov.io/gh/raphaelquast/EOmaps)
[![pypi](https://img.shields.io/pypi/v/eomaps)](https://pypi.org/project/eomaps/)
# EOmaps

Expand Down
249 changes: 227 additions & 22 deletions eomaps/eomaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ class Maps(object):
or below the map ("vertical"). The default is "vertical"
"""

_shapes = [
"ellipses",
"rectangles",
"trimesh_rectangles",
"delauney_triangulation",
"delauney_triangulation_flat",
"delauney_triangulation_masked",
"delauney_triangulation_flat_masked",
"Voroni",
]

def __init__(
self,
orientation="vertical",
Expand Down Expand Up @@ -336,6 +347,23 @@ def set_plot_specs(self, **kwargs):
- "trimesh_rectangles": rectangles but drawn with a
TriMesh collection so that there are no boundaries between
the pixels. (e.g. useful for contourplots)
NOTE: setting edge- and facecolors afterwards is not possible!
- "delauney_triangulation": plot a delauney-triangulation
for the given set of data. (e.g. a dense triangulation of
irregularly spaced points)
NOTE: setting edge- and facecolors afterwards is not possible!
- "delauney_triangulation_flat": same as the normal delauney-
triangulation, but plotted as a polygon-collection so that
edgecolors etc. can be set.
- "delauney_triangulation_masked" (or "_flat_masked"):
same as "delauney_triangulation" but all triangles that
exhibit side-lengths longer than (2 x radius) are masked
This is particularly useful to triangulate concave areas
that are densely sampled.
-> you can use the radius as a parameter for the max.
interpolation-distance!
The default is "trimesh_rectangles".
"""

Expand Down Expand Up @@ -438,6 +466,9 @@ def on_close(event):

self.BM = BlitManager(self.figure.f.canvas)

# trigger drawing the figure
self.figure.f.canvas.draw()

def _spatial_plot(
self,
data,
Expand Down Expand Up @@ -638,18 +669,47 @@ def _spatial_plot(

f.canvas.draw_idle()

if shape.startswith("delauney_triangulation"):
# set an infinite search-distance if triangulations are used
maxdist = np.inf
else:
maxdist = np.max([np.max(props["w"]), np.max(props["h"])]) * 2
# ------------- add a picker that will be used by the callbacks
# use a cKDTree based picking to speed up picks for large collections
tree = cKDTree(np.stack([props["x0"], props["y0"]], axis=1))
maxdist = np.max([np.max(props["w"]), np.max(props["h"])])
self._attach_picker(coll, maxdist)

self.figure = _Maps_plot(
f=f,
gridspec=gs,
ax=ax,
ax_cb=cb_ax,
ax_cb_plot=cb_plot_ax,
cb=cb,
cb_gridspec=cbgs,
coll=coll,
)

def _attach_picker(self, coll, maxdist):
def picker(artist, event):
if event.inaxes != self.figure.ax:
return False, None

if event.dblclick:
double_click = True
else:
double_click = False

dist, index = tree.query((event.xdata, event.ydata))
# use a cKDTree based picking to speed up picks for large collections
dist, index = self.tree.query((event.xdata, event.ydata))

# always set the point as invalid if it is outside of the bounds
# (for plots like delauney-triangulations that do not require a radius)
bounds = self._bounds
if not (
(bounds[0] < event.xdata < bounds[2])
and (bounds[1] < event.ydata < bounds[3])
):
dist = np.inf

if dist < maxdist:
return True, dict(
ind=index, double_click=double_click, mouse_button=event.button
Expand All @@ -663,18 +723,14 @@ def picker(artist, event):

coll.set_picker(picker)

# trigger drawing the figure
f.canvas.draw()

self.figure = _Maps_plot(
f=f,
gridspec=gs,
ax=ax,
ax_cb=cb_ax,
ax_cb_plot=cb_plot_ax,
cb=cb,
cb_gridspec=cbgs,
coll=coll,
@property
@lru_cache()
def _bounds(self):
return (
self._props["x0"].min(),
self._props["y0"].min(),
self._props["x0"].max(),
self._props["y0"].max(),
)

def _set_cpos(self, x, y, radiusx, radiusy, cpos):
Expand Down Expand Up @@ -710,7 +766,6 @@ def _prepare_data(
shape=None,
buffer=None,
):

# get specifications
if data is None:
data = self.data
Expand Down Expand Up @@ -859,6 +914,10 @@ def _prepare_data(
# calculate rotation angle based on mid-point
theta = np.sign(y3 - y0) * np.rad2deg(np.arcsin(np.abs(y3 - y0) / w))

# use a cKDTree based picking to speed up picks for large collections
if not hasattr(self, "figure"):
self.tree = cKDTree(np.stack([x0, y0], axis=1))

props = dict(
x0=x0,
y0=y0,
Expand All @@ -885,17 +944,17 @@ def _prepare_data(
verts = np.array(list(zip(*[np.array(i).T for i in (p0, p1, p2, p3)])))
x = np.vstack(
[verts[:, 2][:, 0], verts[:, 3][:, 0], verts[:, 1][:, 0]]
).T.ravel()
).T.flat
y = np.vstack(
[verts[:, 2][:, 1], verts[:, 3][:, 1], verts[:, 1][:, 1]]
).T.ravel()
).T.flat

x2 = np.vstack(
[verts[:, 3][:, 0], verts[:, 0][:, 0], verts[:, 1][:, 0]]
).T.ravel()
).T.flat
y2 = np.vstack(
[verts[:, 3][:, 1], verts[:, 0][:, 1], verts[:, 1][:, 1]]
).T.ravel()
).T.flat

x = np.append(x, x2)
y = np.append(y, y2)
Expand All @@ -905,11 +964,95 @@ def _prepare_data(
)

props["tri"] = tri
elif shape.startswith("delauney_triangulation"):
assert (
shape == "delauney_triangulation"
or shape == "delauney_triangulation_masked"
or shape == "delauney_triangulation_flat"
or shape == "delauney_triangulation_flat_masked"
), (
f"the provided delauney-shape suffix '{shape}'"
+ " is not valid ...use one of"
+ " '_masked', '_flat' or ' _flat_masked'"
)
try:
from scipy.spatial import Delaunay
except ImportError:
raise ImportError("'scipy' is required for 'delauney_triangulation'!")

d = Delaunay(np.column_stack((x0, y0)), qhull_options="QJ")

tri = Triangulation(d.points[:, 0], d.points[:, 1], d.simplices)
props["tri"] = tri

if shape.endswith("_masked"):
if radius_crs == "in":
x = xorig[tri.triangles]
y = yorig[tri.triangles]
elif radius_crs == "out":
x = x0[tri.triangles]
y = y0[tri.triangles]
else:
x = x0r[tri.triangles]
y = y0r[tri.triangles]

maxdist = 4 * np.mean(np.sqrt(radiusx ** 2 + radiusy ** 2))

verts = np.stack((x, y), axis=2)
cpos = verts.mean(axis=1)[:, None]
cdist = np.sqrt(np.sum((verts - cpos) ** 2, axis=2))

mask = np.logical_or(
np.any(cdist > maxdist * 2, axis=1), cdist.mean(axis=1) > maxdist
)

tri.set_mask(mask)
elif shape == "Voroni":
try:
from scipy.spatial import Voronoi
from itertools import zip_longest
except ImportError:
raise ImportError("'scipy' is required for 'Voroni'!")

maxdist = 2 * np.mean(np.sqrt(radiusx ** 2 + radiusy ** 2))

xy = np.column_stack((x0, y0))
vor = Voronoi(xy)

rect_regions = np.array(list(zip_longest(*vor.regions, fillvalue=-2))).T
# (use -2 instead of None to make np.take work as expected)

rect_regions = rect_regions[vor.point_region]
# exclude all points at infinity
mask = np.all(np.not_equal(rect_regions, -1), axis=1)
# get the mask for the artificially added vertices
rect_mask = rect_regions == -2

x = np.ma.masked_array(
np.take(vor.vertices[:, 0], rect_regions), mask=rect_mask
)
y = np.ma.masked_array(
np.take(vor.vertices[:, 1], rect_regions), mask=rect_mask
)
rect_verts = np.ma.stack((x, y)).swapaxes(0, 1).swapaxes(1, 2)

# exclude any polygon whose defining point is farther away than maxdist
cdist = np.sqrt(np.sum((rect_verts - vor.points[:, None]) ** 2, axis=2))
polymask = np.all(cdist < maxdist, axis=1)
mask = np.logical_and(mask, polymask)

verts = list(i.compressed().reshape(-1, 2) for i in rect_verts[mask])
props["verts"] = verts

# remember masked points
self._voroni_mask = mask
else:
raise TypeError(
f"'{shape}' is not a valid shape, use one of:\n"
+ " - 'ellipses'\n - 'rectangles'\n - 'trimesh_rectangles'"
+ " - 'ellipses'\n"
+ " - 'rectangles'\n"
+ " - 'trimesh_rectangles'\n"
+ " - 'delauney_triangulation(_flat)(_masked)'"
)

return props
Expand Down Expand Up @@ -1125,6 +1268,23 @@ def _add_collection(
coll.set_norm(norm)
# coll.set_urls(ids)
ax.add_collection(coll)
elif shape == "Voroni":
coll = collections.PolyCollection(
verts=props["verts"],
transOffset=ax.transData,
)
# add centroid positions (used by the picker in self._spatial_plot)
coll._Maps_positions = list(zip(props["x0"], props["y0"]))

if color is not None:
coll.set_color(color)
else:
coll.set_array(np.ma.masked_invalid(z_data)[self._voroni_mask])
coll.set_cmap(cmap)
coll.set_clim(vmin, vmax)
coll.set_norm(norm)
# coll.set_urls(ids)
ax.add_collection(coll)

elif shape == "trimesh_rectangles":
coll = TriMesh(props["tri"])
Expand All @@ -1146,6 +1306,51 @@ def _add_collection(
coll.set_norm(norm)
# coll.set_urls(np.repeat(ids, 3, axis=0))
ax.add_collection(coll)
elif shape.startswith("delauney_triangulation"):
if shape.endswith("_flat") or shape.endswith("_flat_masked"):
shading = "flat"
else:
shading = "gouraud"
if shading == "gouraud":
coll = TriMesh(props["tri"])

if color is not None:
coll.set_facecolors([color] * (len(props["x0"])) * 6)
else:
z = np.ma.masked_invalid(z_data)
# tri-contour meshes need 3 values for each triangle
z = np.tile(z, 3)

coll.set_array(z.ravel())
coll.set_cmap(cmap)
coll.set_clim(vmin, vmax)
coll.set_norm(norm)

else:
tri = props["tri"]
# Vertices of triangles.
maskedTris = tri.get_masked_triangles()
verts = np.stack((tri.x[maskedTris], tri.y[maskedTris]), axis=-1)

coll = collections.PolyCollection(verts, transOffset=ax.transData)
if color is not None:
coll.set_color(color)
else:
z = np.ma.masked_invalid(z_data)
# tri-contour meshes need 3 values for each triangle
z = np.tile(z, 3)
z = z[maskedTris].mean(axis=1)

coll.set_array(z)
coll.set_cmap(cmap)
coll.set_clim(vmin, vmax)
coll.set_norm(norm)

# add centroid positions (used by the picker in self._spatial_plot)
coll._Maps_positions = list(zip(props["x0"], props["y0"]))

# coll.set_urls(np.repeat(ids, 3, axis=0))
ax.add_collection(coll)

return coll

Expand Down
18 changes: 10 additions & 8 deletions tests/test_basic_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class TestBasicPlotting(unittest.TestCase):
def setUp(self):
x, y = np.meshgrid(
np.linspace(-19000000, 19000000, 20), np.linspace(-19000000, 19000000, 20)
np.linspace(-19000000, 19000000, 50), np.linspace(-19000000, 19000000, 50)
)
x, y = x.ravel(), y.ravel()

Expand All @@ -23,14 +23,16 @@ def test_simple_map(self):

plt.close(m.figure.f)

def test_simple_rectangles(self):
m = Maps()
m.data = self.data
m.set_data_specs(xcoord="x", ycoord="y", in_crs=3857)
m.set_plot_specs(plot_epsg=4326, shape="rectangles")
m.plot_map()
def test_simple_plot_shapes(self):
usedata = self.data.sample(500)
for shape in Maps._shapes:
m = Maps()
m.data = usedata
m.set_data_specs(xcoord="x", ycoord="y", in_crs=3857)
m.set_plot_specs(plot_epsg=4326, shape=shape)
m.plot_map()

plt.close(m.figure.f)
plt.close(m.figure.f)

def test_simple_map2(self):
m = Maps()
Expand Down

0 comments on commit c31c559

Please sign in to comment.