Repository: geopandas/geopandas

Issue:
Writing to file doesn't respect fiona vsi schemes
Fiona supports writing to different OGR virtual filesystem schemes via the file path, e.g., `zip://`, `s3://`, etc. However, these schemes get overwritten in `gpd.to_file()`, which attempts to take the path and construct an absolute path:
https://github.com/geopandas/geopandas/blob/29add0a735b00dc20c79e0fccc8e6a775c4997b0/geopandas/io/file.py#L127
so you get bad paths like `/home/username/s3:/bucket/object`.

Is this absolute file path important? Is there a reason to not allow VSI-based paths in `gpd.to_file()`? Can we check for a url-like scheme before trying to abs-path it? If so, I'd be happy to submit a PR.

(N.b., the corresponding `gpd.read_file()` handles these URLs correctly).

Discussion:
ian-r-rose: Hrm, looks like the VSI check logic is not stable in all of the versions of fiona that are tested here. Any suggestions on how to proceed?
jorisvandenbossche: > Hrm, looks like the VSI check logic is not stable in all of the versions of fiona that are tested here. Any suggestions on how to proceed?

If it doesn't break other functionality (so it the older version of fiona only prevents this issue being fixed), it is fine to skip the tests for older versions of fiona. Then then new functionality is only available with a recent fiona.
codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=h1) Report
> Merging [#1124](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/29add0a735b00dc20c79e0fccc8e6a775c4997b0?src=pr&el=desc) will **decrease** coverage by `0.79%`.
> The diff coverage is `71.42%`.

[![Impacted file tree graph](https://codecov.io/gh/geopandas/geopandas/pull/1124/graphs/tree.svg?width=650&token=6TOPMcCBhB&height=150&src=pr)](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #1124     +/-   ##
=========================================
- Coverage   89.72%   88.92%   -0.8%     
=========================================
  Files          20       20             
  Lines        1936     1942      +6     
=========================================
- Hits         1737     1727     -10     
- Misses        199      215     +16
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/io/file.py](https://codecov.io/gh/geopandas/geopandas/pull/1124/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL2ZpbGUucHk=) | `83.16% <71.42%> (-11.57%)` | :arrow_down: |
| [geopandas/tools/\_show\_versions.py](https://codecov.io/gh/geopandas/geopandas/pull/1124/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Rvb2xzL19zaG93X3ZlcnNpb25zLnB5) | `85.71% <0%> (-5.2%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=footer). Last update [29add0a...f84adc2](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=h1) Report
> Merging [#1124](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/29add0a735b00dc20c79e0fccc8e6a775c4997b0?src=pr&el=desc) will **decrease** coverage by `0.79%`.
> The diff coverage is `71.42%`.

[![Impacted file tree graph](https://codecov.io/gh/geopandas/geopandas/pull/1124/graphs/tree.svg?width=650&token=6TOPMcCBhB&height=150&src=pr)](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #1124     +/-   ##
=========================================
- Coverage   89.72%   88.92%   -0.8%     
=========================================
  Files          20       20             
  Lines        1936     1942      +6     
=========================================
- Hits         1737     1727     -10     
- Misses        199      215     +16
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/io/file.py](https://codecov.io/gh/geopandas/geopandas/pull/1124/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL2ZpbGUucHk=) | `83.16% <71.42%> (-11.57%)` | :arrow_down: |
| [geopandas/tools/\_show\_versions.py](https://codecov.io/gh/geopandas/geopandas/pull/1124/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Rvb2xzL19zaG93X3ZlcnNpb25zLnB5) | `85.71% <0%> (-5.2%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=footer). Last update [29add0a...f84adc2](https://codecov.io/gh/geopandas/geopandas/pull/1124?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 3c95102e..50f7f50c 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -1,5 +1,4 @@
 from distutils.version import LooseVersion
-import os
 
 import numpy as np
 import six
@@ -120,10 +119,10 @@ def to_file(df, filename, driver="ESRI Shapefile", schema=None, **kwargs):
 
     The *kwargs* are passed to fiona.open and can be used to write
     to multi-layer data, store data within archives (zip files), etc.
+    The path may specify a fiona VSI scheme.
     """
     if schema is None:
         schema = infer_schema(df)
-    filename = os.path.abspath(os.path.expanduser(filename))
     with fiona_env():
         with fiona.open(
             filename, "w", driver=driver, crs=df.crs, schema=schema, **kwargs
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 1d7b194f..476085cf 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -115,7 +115,8 @@ def plot_polygon_collection(
     if values is not None:
         collection.set_array(np.asarray(values))
         collection.set_cmap(cmap)
-        collection.set_clim(vmin, vmax)
+        if "norm" not in kwargs:
+            collection.set_clim(vmin, vmax)
 
     ax.add_collection(collection, autolim=True)
     ax.autoscale_view()
@@ -173,7 +174,8 @@ def plot_linestring_collection(
     if values is not None:
         collection.set_array(np.asarray(values))
         collection.set_cmap(cmap)
-        collection.set_clim(vmin, vmax)
+        if "norm" not in kwargs:
+            collection.set_clim(vmin, vmax)
 
     ax.add_collection(collection, autolim=True)
     ax.autoscale_view()
@@ -233,9 +235,12 @@ def plot_point_collection(
         if pd.api.types.is_list_like(color):
             color = np.take(color, multiindex)
 
-    collection = ax.scatter(
-        x, y, color=color, vmin=vmin, vmax=vmax, cmap=cmap, marker=marker, **kwargs
-    )
+    if "norm" not in kwargs:
+        collection = ax.scatter(
+            x, y, color=color, vmin=vmin, vmax=vmax, cmap=cmap, marker=marker, **kwargs
+        )
+    else:
+        collection = ax.scatter(x, y, color=color, cmap=cmap, marker=marker, **kwargs)
 
     return collection
 


Test patch:
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index 223a486b..ecb2d810 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -176,6 +176,19 @@ class TestPointPlotting:
         # last point == top of colorbar
         np.testing.assert_array_equal(point_colors[-1], cbar_colors[-1])
 
+    def test_subplots_norm(self):
+        # colors of subplots are the same as for plot (norm is applied)
+        cmap = matplotlib.cm.viridis_r
+        norm = matplotlib.colors.Normalize(vmin=0, vmax=20)
+        ax = self.df.plot(column="values", cmap=cmap, norm=norm)
+        actual_colors_orig = ax.collections[0].get_facecolors()
+        exp_colors = cmap(np.arange(10) / (20))
+        np.testing.assert_array_equal(exp_colors, actual_colors_orig)
+        fig, ax = plt.subplots()
+        self.df[1:].plot(column="values", ax=ax, norm=norm, cmap=cmap)
+        actual_colors_sub = ax.collections[0].get_facecolors()
+        np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
+
     def test_empty_plot(self):
         s = GeoSeries([])
         with pytest.warns(UserWarning):
@@ -264,6 +277,19 @@ class TestLineStringPlotting:
             assert ls[0] == exp_ls[0]
             assert ls[1] == exp_ls[1]
 
+    def test_subplots_norm(self):
+        # colors of subplots are the same as for plot (norm is applied)
+        cmap = matplotlib.cm.viridis_r
+        norm = matplotlib.colors.Normalize(vmin=0, vmax=20)
+        ax = self.df.plot(column="values", cmap=cmap, norm=norm)
+        actual_colors_orig = ax.collections[0].get_edgecolors()
+        exp_colors = cmap(np.arange(10) / (20))
+        np.testing.assert_array_equal(exp_colors, actual_colors_orig)
+        fig, ax = plt.subplots()
+        self.df[1:].plot(column="values", ax=ax, norm=norm, cmap=cmap)
+        actual_colors_sub = ax.collections[0].get_edgecolors()
+        np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
+
     def test_multilinestrings(self):
 
         # MultiLineStrings
@@ -411,6 +437,19 @@ class TestPolygonPlotting:
         # colors are repeated for all components within a MultiPolygon
         _check_colors(4, ax.collections[0].get_facecolors(), ["r", "r", "b", "b"])
 
+    def test_subplots_norm(self):
+        # colors of subplots are the same as for plot (norm is applied)
+        cmap = matplotlib.cm.viridis_r
+        norm = matplotlib.colors.Normalize(vmin=0, vmax=10)
+        ax = self.df.plot(column="values", cmap=cmap, norm=norm)
+        actual_colors_orig = ax.collections[0].get_facecolors()
+        exp_colors = cmap(np.arange(2) / (10))
+        np.testing.assert_array_equal(exp_colors, actual_colors_orig)
+        fig, ax = plt.subplots()
+        self.df[1:].plot(column="values", ax=ax, norm=norm, cmap=cmap)
+        actual_colors_sub = ax.collections[0].get_facecolors()
+        np.testing.assert_array_equal(actual_colors_orig[1], actual_colors_sub[0])
+
 
 class TestPolygonZPlotting:
     def setup_method(self):