diff --git a/.coverage b/.coverage new file mode 100644 index 0000000..09adc1e Binary files /dev/null and b/.coverage differ diff --git a/ridge_map.egg-info/PKG-INFO b/ridge_map.egg-info/PKG-INFO new file mode 100644 index 0000000..bf81cb4 --- /dev/null +++ b/ridge_map.egg-info/PKG-INFO @@ -0,0 +1,365 @@ +Metadata-Version: 2.2 +Name: ridge_map +Version: 0.0.6 +Summary: 1d lines, 3d maps +Home-page: https://github.com/ColCarroll/ridge_map +Author: Colin Carroll +Author-email: colcarroll@gmail.com +License: MIT +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: SRTM.py +Requires-Dist: numpy +Requires-Dist: matplotlib +Requires-Dist: scikit-image>=0.14.2 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: license +Dynamic: requires-dist +Dynamic: summary + +ridge_map +========= +![Build status](https://github.com/ColCarroll/ridge_map/actions/workflows/build.yml/badge.svg) + +*Ridge plots of ridges* +----------------------- + +A library for making ridge plots of... ridges. Choose a location, get an elevation map, and tinker with it to make something beautiful. Heavily inspired from [Zach Cole's beautiful art](https://twitter.com/ZachACole/status/1121554541101477889), [Jake Vanderplas' examples](https://github.com/jakevdp/altair-examples/blob/master/notebooks/PulsarPlot.ipynb), and Joy Division's [1979 album "Unknown Pleasures"](https://gist.github.com/ColCarroll/68e29c92b766418b0a4497b4eb2ecba4). + +Uses [matplotlib](https://matplotlib.org/), [SRTM.py](https://github.com/tkrajina/srtm.py), [numpy](https://www.numpy.org/), and [scikit-image](https://scikit-image.org/) (for lake detection). + +Installation +------------ + +Available on [PyPI](https://pypi.org/project/ridge-map/): + +```bash +pip install ridge_map +``` + +Or live on the edge and install from github with + +```bash +pip install git+https://github.com/colcarroll/ridge_map.git +``` + +You can also make a copy of [this colab](https://colab.research.google.com/drive/1ntwd73haePt3OS5ysz4yGSlhmUecY24O?usp=sharing). + +Want to help? +------------- + +- I feel like I am missing something easy or obvious with lake/road/river/ocean detection, but what I've got gets me most of the way there. If you hack on the `RidgeMap.preprocessor` method and find something nice, I would love to hear about it! +- Did you make a cool map? Open an issue with the code and I will add it to the examples. + +Examples +-------- + +The API allows you to download the data once, then edit the plot yourself, +or allow the default processor to help you. + +### New Hampshire by default + +Plotting with all the defaults should give you a map of my favorite mountains. + +```python +from ridge_map import RidgeMap + +RidgeMap().plot_map() +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/white_mountains.png?raw=true) + +### Download once and tweak settings + +First you download the elevation data to get an array with shape +`(num_lines, elevation_pts)`, then you can use the preprocessor +to automatically detect lakes, rivers, and oceans, and scale the elevations. +Finally, there are options to style the plot + +```python +rm = RidgeMap((11.098251,47.264786,11.695633,47.453630)) +values = rm.get_elevation_data(num_lines=150) +values=rm.preprocess( + values=values, + lake_flatness=2, + water_ntile=10, + vertical_ratio=240) +rm.plot_map(values=values, + label='Karwendelgebirge', + label_y=0.1, + label_x=0.55, + label_size=40, + linewidth=1) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/karwendelgebirge.png?raw=true) + +### Plot with colors! + +If you are plotting a town that is super into burnt orange for whatever +reason, you can respect that choice. + +```python +rm = RidgeMap((-97.794285,30.232226,-97.710171,30.334509)) +values = rm.get_elevation_data(num_lines=80) +rm.plot_map(values=rm.preprocess(values=values, water_ntile=12, vertical_ratio=40), + label='Austin\nTexas', + label_x=0.75, + linewidth=6, + line_color='orange') +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/austin.png?raw=true) + +### Plot with even more colors! + +The line color accepts a [matplotlib colormap](https://matplotlib.org/gallery/color/colormap_reference.html#sphx-glr-gallery-color-colormap-reference-py), so really feel free to go to town. + +```python +rm = RidgeMap((-123.107300,36.820279,-121.519775,38.210130)) +values = rm.get_elevation_data(num_lines=150) +rm.plot_map(values=rm.preprocess(values=values, lake_flatness=3, water_ntile=50, vertical_ratio=30), + label='The Bay\nArea', + label_x=0.1, + line_color = plt.get_cmap('spring')) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/san_francisco.png?raw=true) + +### Plot with custom fonts and elevation colors! + +You can find a good font [from Google](https://fonts.google.com/), and then get the path to the ttf file [in the github repo](https://github.com/google/fonts/tree/master/ofl). + +If you pass a matplotlib colormap, you can specify `kind="elevation"` to color tops of mountains different from bottoms. `ocean`, `gnuplot`, and `bone` look nice. + +```python +from ridge_map import FontManager + +font = FontManager('https://github.com/google/fonts/blob/main/ofl/uncialantiqua/UncialAntiqua-Regular.ttf?raw=true') +rm = RidgeMap((-156.250305,18.890695,-154.714966,20.275080), font=font.prop) + +values = rm.get_elevation_data(num_lines=100) +rm.plot_map(values=rm.preprocess(values=values, lake_flatness=2, water_ntile=10, vertical_ratio=240), + label="Hawai'i", + label_y=0.85, + label_x=0.7, + label_size=60, + linewidth=2, + line_color=plt.get_cmap('ocean'), + kind='elevation') +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/hawaii.png?raw=true) + +### How do I find a bounding box? + +I have been using [this website](http://bboxfinder.com). I find an area I like, draw a rectangle, then copy and paste the coordinates into the `RidgeMap` constructor. + +```python +rm = RidgeMap((-73.509693,41.678682,-73.342838,41.761581)) +values = rm.get_elevation_data() +rm.plot_map(values=rm.preprocess(values=values, lake_flatness=2, water_ntile=2, vertical_ratio=60), + label='Kent\nConnecticut', + label_y=0.7, + label_x=0.65, + label_size=40) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/kent.png?raw=true) + +### What about really flat areas? + +You might really have to tune the `water_ntile` and `lake_flatness` to get the water right. You can set them to 0 if you do not want any water marked. + +```python +rm = RidgeMap((-71.167374,42.324286,-70.952454, 42.402672)) +values = rm.get_elevation_data(num_lines=50) +rm.plot_map(values=rm.preprocess(values=values, lake_flatness=4, water_ntile=30, vertical_ratio=20), + label='Cambridge\nand Boston', + label_x=0.75, + label_size=40, + linewidth=1) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/boston.png?raw=true) + +### Can I change the angle? + +Yes, you can change the angle at which you look at the map. South to North is 0 degrees, East to West is 90 degrees and so forth with the rest of the compass. I really recommend playing around with this setting because of the really cool maps it can generate. + +Play around with `interpolation`, `lock_rotation`, and `crop` to polish out the map. + +```python +rm = RidgeMap((-124.848974,46.292035,-116.463262,49.345786)) +values = rm.get_elevation_data(elevation_pts=300, num_lines=300, viewpoint_angle=11) +values=rm.preprocess( + values=values, + lake_flatness=2, + water_ntile=10, + vertical_ratio=240 +) +rm.plot_map(values=values, + label='Washington', + label_y=0.8, + label_x=0.05, + label_size=40, + linewidth=2 +) +``` + +![png](/examples/washington.png?raw=true) +![gif](/examples/washington.gif?raw=true) + +### What about Walden Pond? + +It is that pleasant kettle pond in the bottom right of this map, looking entirely comfortable with its place in Western writing and thought. + +```python +rm = RidgeMap((-71.418858,42.427511,-71.310024,42.481719)) +values = rm.get_elevation_data(num_lines=100) +rm.plot_map(values=rm.preprocess(values=values, water_ntile=15, vertical_ratio=30), + label='Concord\nMassachusetts', + label_x=0.1, + label_size=30) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/concord.png?raw=true) + +### Do you play nicely with other matplotlib figures? + +Of course! If you really want to put a stylized elevation map in a scientific plot you are making, I am not going to stop you, and will actually make it easier for you. Just pass an argument for `ax` to `RidgeMap.plot_map`. + +```python +import numpy as np +fig, axes = plt.subplots(ncols=2, figsize=(20, 5)) +x = np.linspace(-2, 2) +y = x * x + +axes[0].plot(x, y, 'o') + +rm = RidgeMap() +rm.plot_map(label_size=24, background_color=(1, 1, 1), ax=axes[1]) +``` + +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/multiaxis.png?raw=true) + +User Examples +------------- + +### Annotating, changing background color, custom text + +This example shows how to annotate a lat/long on the map, and updates the color of the label text to allow for a dark background. Thanks to [kratsg](https://github.com/kratsg) for contributing. + +```python +import matplotlib +import matplotlib.pyplot as plt +import numpy as np + +bgcolor = np.array([65,74,76])/255. + +scipp = (-122.060510, 36.998776) +rm = RidgeMap((-122.087116,36.945365,-121.999226,37.023250)) +scipp_coords = ((scipp[0] - rm.longs[0])/(rm.longs[1] - rm.longs[0]),(scipp[1] - rm.lats[0])/(rm.lats[1] - rm.lats[0])) + +values = rm.get_elevation_data(num_lines=150) +ridges = rm.plot_map(values=rm.preprocess(values=values, + lake_flatness=1, + water_ntile=0, + vertical_ratio=240), + label='Santa Cruz\nMountains', + label_x=0.75, + label_y=0.05, + label_size=36, + kind='elevation', + linewidth=1, + background_color=bgcolor, + line_color = plt.get_cmap('cool')) + +# Bit of a hack to update the text label color +for child in ridges.get_children(): + if isinstance(child, matplotlib.text.Text) and 'Santa Cruz' in child._text: + label_artist = child + break +label_artist.set_color('white') + +ridges.text(scipp_coords[0]+0.005, scipp_coords[1]+0.005, 'SCIPP', + fontproperties=rm.font, + size=20, + color="white", + transform=ridges.transAxes, + verticalalignment="bottom", + zorder=len(values)+10) + +ridges.plot(*scipp_coords, 'o', + color='white', + transform=ridges.transAxes, + ms=6, + zorder=len(values)+10) +``` + +#### Updated Annotation and Custom Text Color +The above code still works, but now there is a simplified method (Shown Below) that will produce the same image. + +```python +import matplotlib.pyplot as plt +import numpy as np + +from ridge_map import RidgeMap + +bgcolor = np.array([65,74,76])/255. + +rm = RidgeMap((-122.087116,36.945365,-121.999226,37.023250)) +values = rm.get_elevation_data(num_lines=150) +values = rm.preprocess( + values=values, + lake_flatness=1, + water_ntile=0, + vertical_ratio=240 +) + +rm.plot_map( + values=values, + label='Santa Cruz\nMountains', + label_x=0.75, + label_y=0.05, + label_size=36, + label_color='white', + kind='elevation', + linewidth=1, + background_color=bgcolor, + line_color = plt.get_cmap('cool') +) + +rm.plot_annotation( + label='SCIPP', + coordinates=(-122.060510, 36.998776), + x_offset=0.005, + y_offset=0.005, + label_size=20, + annotation_size=6, + color='white', + background=False +) +``` +![png](https://github.com/ColCarroll/ridge_map/blob/main/examples/santa_cruz.png?raw=true) + +Elevation Data +-------------- + +Elevation data used by `ridge_map` comes from NASA's [Shuttle Radar Topography Mission](https://www2.jpl.nasa.gov/srtm/) (SRTM), high resolution topographic data collected in 2000, and released in 2015. SRTM data are sampled at a resolution of 1 arc-second (about 30 meters). SRTM data is provided to `ridge_map` via the python package `SRTM.py` ([link](https://github.com/tkrajina/srtm.py)). SRTM data is not available for latitudes greater than N 60° or less than S 60°: + + + +![gif](https://www2.jpl.nasa.gov/srtm/images/SRTM_2-24-2016.gif) diff --git a/ridge_map.egg-info/SOURCES.txt b/ridge_map.egg-info/SOURCES.txt new file mode 100644 index 0000000..36b067a --- /dev/null +++ b/ridge_map.egg-info/SOURCES.txt @@ -0,0 +1,11 @@ +LICENSE +README.md +setup.py +ridge_map/__init__.py +ridge_map/ridge_map.py +ridge_map.egg-info/PKG-INFO +ridge_map.egg-info/SOURCES.txt +ridge_map.egg-info/dependency_links.txt +ridge_map.egg-info/requires.txt +ridge_map.egg-info/top_level.txt +test/test_ridge_map.py \ No newline at end of file diff --git a/ridge_map.egg-info/dependency_links.txt b/ridge_map.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ridge_map.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/ridge_map.egg-info/requires.txt b/ridge_map.egg-info/requires.txt new file mode 100644 index 0000000..6578952 --- /dev/null +++ b/ridge_map.egg-info/requires.txt @@ -0,0 +1,4 @@ +SRTM.py +numpy +matplotlib +scikit-image>=0.14.2 diff --git a/ridge_map.egg-info/top_level.txt b/ridge_map.egg-info/top_level.txt new file mode 100644 index 0000000..9071f8e --- /dev/null +++ b/ridge_map.egg-info/top_level.txt @@ -0,0 +1 @@ +ridge_map diff --git a/ridge_map/ridge_map.py b/ridge_map/ridge_map.py index 87a9c05..4e7f68b 100644 --- a/ridge_map/ridge_map.py +++ b/ridge_map/ridge_map.py @@ -8,7 +8,7 @@ import matplotlib.pyplot as plt import numpy as np from skimage.filters import rank -from skimage.morphology import square +from skimage.morphology import footprint_rectangle from skimage.util import img_as_ubyte from scipy.ndimage import rotate @@ -91,14 +91,15 @@ def longs(self): """Bottom and top longitude of bounding box.""" return (self.bbox[0], self.bbox[2]) + # pylint: disable=too-many-arguments,too-many-positional-arguments def get_elevation_data( - self, - num_lines=80, - elevation_pts=300, - viewpoint_angle=0, + self, + num_lines=80, + elevation_pts=300, + viewpoint_angle=0, crop=False, - interpolation=0, - lock_resolution=False + interpolation=0, + lock_resolution=False, ): """Fetch elevation data and return a numpy array. @@ -114,25 +115,29 @@ def get_elevation_data( crop : bool If the corners are cropped when rotating interpolation : int in [0, 5] - The level of interpolation. Can smooth out sharp edges, especially + The level of interpolation. Can smooth out sharp edges, especially when rotating. Above 1 tends to lead to an all NaN graph. lock_resolution : bool - Locks the resolution during rotation, ensuring consistent rotation - deltas but producing potential scaling artifacts. These artifacts + Locks the resolution during rotation, ensuring consistent rotation + deltas but producing potential scaling artifacts. These artifacts can be reduced by setting num_lines = elevation_pts. Returns ------- np.ndarray """ - if (45 < (viewpoint_angle % 360) < 135 or 225 < (viewpoint_angle % 360) < 315) and not lock_resolution: + if ( + 45 < (viewpoint_angle % 360) < 135 or 225 < (viewpoint_angle % 360) < 315 + ) and not lock_resolution: num_lines, elevation_pts = elevation_pts, num_lines - + values = self._srtm_data.get_image( (elevation_pts, num_lines), self.lats, self.longs, 5280, mode="array" ) - values = rotate(values, angle=viewpoint_angle, reshape=not crop, order=interpolation) - + values = rotate( + values, angle=viewpoint_angle, reshape=not crop, order=interpolation + ) + return values def preprocess( @@ -173,7 +178,10 @@ def preprocess( values = (values - np.min(values)) / (np.max(values) - np.min(values)) is_water = values < np.percentile(values, water_ntile) - is_lake = rank.gradient(img_as_ubyte(values), square(3)) < lake_flatness + is_lake = ( + rank.gradient(img_as_ubyte(values), footprint_rectangle((3, 3))) + < lake_flatness + ) values[nan_vals] = np.nan values[np.logical_or(is_water, is_lake)] = np.nan @@ -193,7 +201,7 @@ def plot_annotation( background=True, ax=None, ): - """Plot an annotation to an existing map + """Plot an annotation to an existing map. It is recommended to call this function only after calling map_plot() @@ -217,23 +225,26 @@ def plot_annotation( If there is a background or not ax : matplotlib Axes You can pass your own axes, but probably best not to - + Returns ------- matplotlib.Axes - """ + """ if ax is None and self.ax is None: - raise ValueError("No axes found: Either plot_map() beforehand or pass an matplotlib.Axes value through") - elif ax is None: + raise ValueError( + "No axes found: Either plot_map() beforehand or pass an matplotlib.Axes value " + "to the function." + ) + if ax is None: ax = self.ax - + highest_zorder = max(text.zorder for text in ax.texts) if ax.texts else 1 - + rel_coordinates = ( (coordinates[0] - self.longs[0]) / (self.longs[1] - self.longs[0]), (coordinates[1] - self.lats[0]) / (self.lats[1] - self.lats[0]), ) - + annotation_color = "black" if color: annotation_color = color @@ -265,10 +276,10 @@ def plot_annotation( ms=annotation_size, zorder=highest_zorder, ) - + self.ax = ax return ax - + # pylint: disable=too-many-arguments,too-many-locals def plot_map( self, @@ -326,7 +337,6 @@ def plot_map( ------- matplotlib.Axes """ - if kind not in {"gradient", "elevation"}: raise TypeError("Argument `kind` must be one of 'gradient' or 'elevation'") if values is None: @@ -383,6 +393,6 @@ def plot_map( for spine in ax.spines.values(): spine.set_visible(False) ax.set_facecolor(background_color) - + self.ax = ax return ax