Skip to content

Commit 1c79e4b

Browse files
authored
Remove esri.py and datashader from core dependencies (#953)
* Remove esri.py and datashader from core dependencies (#947) Delete esri.py (unused REST client for ArcGIS feature services). Move datashader from install_requires to extras_require[examples]. Replace ds.Canvas coordinate hacks in terrain.py and datasets/__init__.py with np.linspace. Replace tf.Image with PIL.Image in bands_to_img and color_values. Add local hex-to-RGB helper to replace datashader.colors.rgb. Make canvas_like lazy-import datashader with a clear ImportError. Update test_viewshed.py fixture to build coords without datashader. * Make PIL import lazy in bands_to_img and color_values (#947) Pillow is not a declared dependency, so importing it at module level breaks environments that don't have it installed (like CI). Move the import inside the two functions that need it. * Skip test_canvas_like when datashader is not installed (#947) * Remove test_canvas_like (#947)
1 parent 62d3929 commit 1c79e4b

File tree

8 files changed

+55
-116
lines changed

8 files changed

+55
-116
lines changed

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ url = https://github.com/xarray-contrib/xarray-spatial
1919
[options]
2020
include_package_data = True
2121
install_requires =
22-
datashader >= 0.15.0
2322
numba
2423
scipy
2524
xarray
@@ -48,6 +47,7 @@ doc =
4847
sphinx-panels
4948
sphinx_rtd_theme
5049
examples =
50+
datashader >= 0.15.0
5151
optional =
5252
# Optional for polygonize return types.
5353
awkward>=1.4

xrspatial/datasets/__init__.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55
except ImportError:
66
da = None
77

8-
import datashader as ds
98
import numpy as np
10-
import pandas as pd
119
import xarray as xr
1210

1311
__all__ = [
@@ -113,20 +111,19 @@ def _func(arr, block_id=None):
113111
.map_blocks(_func, dtype=np.float32)
114112
)
115113

116-
cvs = ds.Canvas(
117-
x_range=(0, 500),
118-
y_range=(0, 500),
119-
plot_width=shape[1],
120-
plot_height=shape[0],
121-
)
122-
123-
hack_agg = cvs.points(pd.DataFrame({'x': [], 'y': []}), 'x', 'y')
114+
# Build pixel-center coordinates
115+
x_range = (0, 500)
116+
y_range = (0, 500)
117+
dx = (x_range[1] - x_range[0]) / shape[1]
118+
dy = (y_range[1] - y_range[0]) / shape[0]
119+
xs = np.linspace(x_range[0] + dx / 2, x_range[1] - dx / 2, shape[1])
120+
ys = np.linspace(y_range[0] + dy / 2, y_range[1] - dy / 2, shape[0])
124121

125122
agg = xr.DataArray(
126123
data,
127124
name='terrain',
128-
coords=hack_agg.coords,
129-
dims=hack_agg.dims,
125+
coords={'y': ys, 'x': xs},
126+
dims=['y', 'x'],
130127
attrs={'res': 1},
131128
)
132129

xrspatial/esri.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

xrspatial/terrain.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
from functools import partial
55
from typing import Dict, List, Optional, Tuple, Union
66

7-
import datashader as ds
87
# 3rd-party
98
import numpy as np
10-
import pandas as pd
119
import xarray as xr
1210

1311
try:
@@ -633,18 +631,16 @@ def generate_terrain(agg: xr.DataArray,
633631
octaves, lacunarity, persistence, noise_mode,
634632
warp_strength, warp_octaves, worley_blend, worley_seed)
635633

636-
canvas = ds.Canvas(
637-
plot_width=width, plot_height=height, x_range=x_range, y_range=y_range
638-
)
639-
640-
# DataArray coords were coming back different from cvs.points...
641-
hack_agg = canvas.points(pd.DataFrame({'x': [], 'y': []}), 'x', 'y')
642-
res = get_dataarray_resolution(hack_agg)
634+
# Build pixel-center coordinates (matches datashader Canvas convention)
635+
dx = (x_range[1] - x_range[0]) / width
636+
dy = (y_range[1] - y_range[0]) / height
637+
xs = np.linspace(x_range[0] + dx / 2, x_range[1] - dx / 2, width)
638+
ys = np.linspace(y_range[0] + dy / 2, y_range[1] - dy / 2, height)
643639
result = xr.DataArray(out,
644640
name=name,
645-
coords=hack_agg.coords,
646-
dims=hack_agg.dims,
647-
attrs={'res': res})
641+
coords={'y': ys, 'x': xs},
642+
dims=['y', 'x'],
643+
attrs={'res': (dx, dy)})
648644

649645
# --- hydraulic erosion ---
650646
if erode:

xrspatial/tests/test_utils.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,7 @@
44
import warnings
55

66

7-
from xrspatial.datasets import make_terrain
87
from xrspatial import utils
9-
from xrspatial.tests.general_checks import dask_array_available
10-
11-
12-
@dask_array_available
13-
def test_canvas_like():
14-
# aspect ratio is 1:1
15-
terrain_shape = (1000, 1000)
16-
terrain = make_terrain(shape=terrain_shape)
17-
terrain_res = utils.canvas_like(terrain, width=50)
18-
assert terrain_res.shape == (50, 50)
198

209

2110
def test_warn_if_unit_mismatch_degrees_horizontal_elevation_vertical(monkeypatch):

xrspatial/tests/test_viewshed.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import dask.array as da
2-
import datashader as ds
32
import numpy as np
4-
import pandas as pd
53
import pytest
64
import xarray as xa
75

@@ -14,19 +12,23 @@
1412

1513
@pytest.fixture
1614
def empty_agg():
17-
# create an empty image of size 5*5
15+
# create an empty DataArray of size 5x5 with coordinates
1816
H = 5
1917
W = 5
20-
21-
canvas = ds.Canvas(plot_width=W, plot_height=H,
22-
x_range=(-20, 20), y_range=(-20, 20))
23-
24-
empty_df = pd.DataFrame({
25-
'x': np.random.normal(.5, 1, 0),
26-
'y': np.random.normal(.5, 1, 0)
27-
})
28-
agg = canvas.points(empty_df, 'x', 'y')
29-
return agg.astype('i8')
18+
x_range = (-20, 20)
19+
y_range = (-20, 20)
20+
21+
dx = (x_range[1] - x_range[0]) / W
22+
dy = (y_range[1] - y_range[0]) / H
23+
xs = np.linspace(x_range[0] + dx / 2, x_range[1] - dx / 2, W)
24+
ys = np.linspace(y_range[0] + dy / 2, y_range[1] - dy / 2, H)
25+
26+
agg = xa.DataArray(
27+
np.zeros((H, W), dtype='i8'),
28+
coords={'y': ys, 'x': xs},
29+
dims=['y', 'x'],
30+
)
31+
return agg
3032

3133

3234
def test_viewshed_invalid_x_view(empty_agg):

xrspatial/utils.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33
from math import ceil
44
import warnings
55

6-
import datashader as ds
7-
import datashader.transfer_functions as tf
86
import numpy as np
97
import xarray as xr
10-
from datashader.colors import rgb
118
from numba import cuda, jit
129

1310
try:
@@ -558,14 +555,16 @@ def height_implied_by_aspect_ratio(W, X, Y):
558555

559556

560557
def bands_to_img(r, g, b, nodata=1):
558+
from PIL import Image
559+
561560
h, w = r.shape
562561
data = np.zeros((h, w, 4), dtype=np.uint8)
563562
data[:, :, 0] = (r).astype(np.uint8)
564563
data[:, :, 1] = (g).astype(np.uint8)
565564
data[:, :, 2] = (b).astype(np.uint8)
566565
a = np.where(np.logical_or(np.isnan(r), r <= nodata), 0, 255)
567566
data[:, :, 3] = a.astype(np.uint8)
568-
return tf.Image.fromarray(data, "RGBA")
567+
return Image.fromarray(data, "RGBA")
569568

570569

571570
def canvas_like(
@@ -631,6 +630,13 @@ def canvas_like(
631630
# set width and height
632631
height = height_implied_by_aspect_ratio(width, x_range, y_range)
633632

633+
try:
634+
import datashader as ds
635+
except ImportError:
636+
raise ImportError(
637+
"canvas_like requires datashader: pip install datashader"
638+
)
639+
634640
cvs = ds.Canvas(
635641
plot_width=width, plot_height=height, x_range=x_range, y_range=y_range
636642
)
@@ -639,14 +645,23 @@ def canvas_like(
639645
return out
640646

641647

648+
def _hex_to_rgb(c):
649+
"""Convert a hex color string (e.g. '#ff0000' or 'ff0000') to (r, g, b)."""
650+
c = c.lstrip('#')
651+
return int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
652+
653+
642654
def color_values(agg, color_key, alpha=255):
655+
from PIL import Image
656+
643657
def _convert_color(c):
644-
r, g, b = rgb(c)
658+
r, g, b = _hex_to_rgb(c)
645659
return np.array([r, g, b, alpha]).astype(np.uint8).view(np.uint32)[0]
646660

647661
_converted_colors = {k: _convert_color(v) for k, v in color_key.items()}
648662
f = np.vectorize(lambda v: _converted_colors.get(v, 0))
649-
return tf.Image(f(agg.data))
663+
return Image.fromarray(f(agg.data).astype(np.uint32).view(np.uint8).reshape(
664+
agg.data.shape + (4,)), "RGBA")
650665

651666

652667
def _infer_coord_unit_type(coord: xr.DataArray, cellsize: float) -> str:

xrspatial/zonal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1502,7 +1502,7 @@ def suggest_zonal_canvas(
15021502
--------
15031503
.. sourcecode:: python
15041504
1505-
>>> # Imports
1505+
>>> # Imports (datashader is optional: pip install datashader)
15061506
>>> from spatialpandas import GeoDataFrame
15071507
>>> import geopandas as gpd
15081508
>>> import datashader as ds

0 commit comments

Comments
 (0)