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).

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
 

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).
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):

pip install -e .
parse_log_pytest
requirements.txt
matplotlib descartes mapclassify pytest
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==25.3.0
certifi==2025.1.31
click==8.1.8
click-plugins==1.1.1
cligj==0.7.2
contourpy==1.3.0
cycler==0.12.1
Cython==3.0.12
descartes==1.1.0
exceptiongroup==1.2.2
fiona==1.10.1
fonttools==4.56.0
-e git+https://github.com/geopandas/geopandas.git@b71ab0bf3a0a88151a827189dba5c97284460b5d#egg=geopandas
importlib_metadata==8.6.1
importlib_resources==6.5.2
iniconfig==2.1.0
joblib==1.4.2
kiwisolver==1.4.7
mapclassify==2.8.1
matplotlib==3.9.4
networkx==3.2.1
numpy==2.0.2
packaging==24.2
pandas==2.2.3
pillow==11.1.0
pluggy==1.5.0
pyparsing==3.2.3
pyproj==3.6.1
pytest==8.3.5
python-dateutil==2.9.0.post0
pytz==2025.2
scikit-learn==1.6.1
scipy==1.13.1
shapely==2.0.7
six==1.17.0
threadpoolctl==3.6.0
tomli==2.2.1
tzdata==2025.2
zipp==3.21.0

name: geopandas
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==25.3.0
      - certifi==2025.1.31
      - click==8.1.8
      - click-plugins==1.1.1
      - cligj==0.7.2
      - contourpy==1.3.0
      - cycler==0.12.1
      - cython==3.0.12
      - descartes==1.1.0
      - exceptiongroup==1.2.2
      - fiona==1.10.1
      - fonttools==4.56.0
      - importlib-metadata==8.6.1
      - importlib-resources==6.5.2
      - iniconfig==2.1.0
      - joblib==1.4.2
      - kiwisolver==1.4.7
      - mapclassify==2.8.1
      - matplotlib==3.9.4
      - networkx==3.2.1
      - numpy==2.0.2
      - packaging==24.2
      - pandas==2.2.3
      - pillow==11.1.0
      - pluggy==1.5.0
      - pyparsing==3.2.3
      - pyproj==3.6.1
      - pytest==8.3.5
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - scikit-learn==1.6.1
      - scipy==1.13.1
      - shapely==2.0.7
      - six==1.17.0
      - threadpoolctl==3.6.0
      - tomli==2.2.1
      - tzdata==2025.2
      - zipp==3.21.0
prefix: /opt/conda/envs/geopandas

geopandas/tests/test_plotting.py::TestPointPlotting::test_subplots_norm
geopandas/tests/test_plotting.py::TestPointPlotting::test_legend geopandas/tests/test_plotting.py::TestPointPlotting::test_multipoints geopandas/tests/test_plotting.py::TestLineStringPlotting::test_single_color geopandas/tests/test_plotting.py::TestLineStringPlotting::test_style_kwargs geopandas/tests/test_plotting.py::TestLineStringPlotting::test_subplots_norm geopandas/tests/test_plotting.py::TestLineStringPlotting::test_multilinestrings geopandas/tests/test_plotting.py::TestPolygonZPlotting::test_plot geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_colors geopandas/tests/test_plotting.py::TestNonuniformGeometryPlotting::test_style_kwargs geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_legend geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_negative_legend geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_scheme_name_compat[FISHER_JENKS] geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_scheme_name_compat[FISHERJENKS] geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_classification_kwds geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_cax_legend_height geopandas/tests/test_plotting.py::TestPlotCollections::test_linestrings geopandas/tests/test_plotting.py::TestPlotCollections::test_linestrings_values geopandas/tests/test_plotting.py::TestPlotCollections::test_polygons geopandas/tests/test_plotting.py::TestPlotCollections::test_polygons_values geopandas/tests/test_plotting.py::test_column_values
geopandas/tests/test_plotting.py::TestPointPlotting::test_figsize geopandas/tests/test_plotting.py::TestPointPlotting::test_default_colors geopandas/tests/test_plotting.py::TestPointPlotting::test_colormap geopandas/tests/test_plotting.py::TestPointPlotting::test_single_color geopandas/tests/test_plotting.py::TestPointPlotting::test_markersize geopandas/tests/test_plotting.py::TestPointPlotting::test_style_kwargs geopandas/tests/test_plotting.py::TestPointPlotting::test_empty_plot geopandas/tests/test_plotting.py::TestPointZPlotting::test_plot geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_invalid_scheme geopandas/tests/test_plotting.py::TestMapclassifyPlotting::test_cax_legend_passing geopandas/tests/test_plotting.py::TestPlotCollections::test_points geopandas/tests/test_plotting.py::TestPlotCollections::test_points_values
geopandas/io/file.py geopandas/plotting.py
geopandas/io/file.py geopandas/plotting.py