Repository: geopandas/geopandas

Issue:
Pass keywords to colorbar construction
Hey all,
Amazing tool – thank you so much for putting it together and managing it so well.

I'm sure there is an easy fix to this, but I'm having trouble accessing the legend object from a chloropleth plot. I'd imagine ax.get_legend() should return it, but that doesn't seem to be the case.

My endgoal is simply to put the colormap horizontally beneath my figure – so if there's a very easy way to do that let me know. Thank you so much!


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=h1) Report
> Merging [#1089](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/8c42164559173972e7a4bafe705bbb2987d15d47?src=pr&el=desc) will **decrease** coverage by `1.94%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1089      +/-   ##
==========================================
- Coverage    90.2%   88.25%   -1.95%     
==========================================
  Files          19       19              
  Lines        1837     1839       +2     
==========================================
- Hits         1657     1623      -34     
- Misses        180      216      +36
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/plotting.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Bsb3R0aW5nLnB5) | `92.5% <100%> (-0.78%)` | :arrow_down: |
| [geopandas/tools/crs.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Rvb2xzL2Nycy5weQ==) | `48.14% <0%> (-37.04%)` | :arrow_down: |
| [geopandas/io/file.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL2ZpbGUucHk=) | `80% <0%> (-14.74%)` | :arrow_down: |
| [geopandas/io/sql.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL3NxbC5weQ==) | `82.14% <0%> (-14.29%)` | :arrow_down: |
| [geopandas/\_compat.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL19jb21wYXQucHk=) | `85.71% <0%> (-14.29%)` | :arrow_down: |
| [geopandas/geodataframe.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2dlb2RhdGFmcmFtZS5weQ==) | `97.11% <0%> (-0.83%)` | :arrow_down: |
| [geopandas/geoseries.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2dlb3Nlcmllcy5weQ==) | `96.91% <0%> (-0.62%)` | :arrow_down: |
| [geopandas/array.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2FycmF5LnB5) | `94.07% <0%> (-0.52%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1089?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/1089?src=pr&el=footer). Last update [8c42164...24b08a0](https://codecov.io/gh/geopandas/geopandas/pull/1089?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/1089?src=pr&el=h1) Report
> Merging [#1089](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=desc) into [master](https://codecov.io/gh/geopandas/geopandas/commit/8c42164559173972e7a4bafe705bbb2987d15d47?src=pr&el=desc) will **decrease** coverage by `1.94%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1089      +/-   ##
==========================================
- Coverage    90.2%   88.25%   -1.95%     
==========================================
  Files          19       19              
  Lines        1837     1839       +2     
==========================================
- Hits         1657     1623      -34     
- Misses        180      216      +36
```


| [Impacted Files](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [geopandas/plotting.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Bsb3R0aW5nLnB5) | `92.5% <100%> (-0.78%)` | :arrow_down: |
| [geopandas/tools/crs.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL3Rvb2xzL2Nycy5weQ==) | `48.14% <0%> (-37.04%)` | :arrow_down: |
| [geopandas/io/file.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL2ZpbGUucHk=) | `80% <0%> (-14.74%)` | :arrow_down: |
| [geopandas/io/sql.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2lvL3NxbC5weQ==) | `82.14% <0%> (-14.29%)` | :arrow_down: |
| [geopandas/\_compat.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL19jb21wYXQucHk=) | `85.71% <0%> (-14.29%)` | :arrow_down: |
| [geopandas/geodataframe.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2dlb2RhdGFmcmFtZS5weQ==) | `97.11% <0%> (-0.83%)` | :arrow_down: |
| [geopandas/geoseries.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2dlb3Nlcmllcy5weQ==) | `96.91% <0%> (-0.62%)` | :arrow_down: |
| [geopandas/array.py](https://codecov.io/gh/geopandas/geopandas/pull/1089/diff?src=pr&el=tree#diff-Z2VvcGFuZGFzL2FycmF5LnB5) | `94.07% <0%> (-0.52%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/geopandas/geopandas/pull/1089?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/1089?src=pr&el=footer). Last update [8c42164...24b08a0](https://codecov.io/gh/geopandas/geopandas/pull/1089?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

martinfleis: Probably resolves  #393, but that issue is super unclear.
jorisvandenbossche: Fixed the conflict after merging the other PR
jorisvandenbossche: This is giving some errors. It might be that the test needs to be updated now the other PR is merged.

Patch:
diff --git a/geopandas/array.py b/geopandas/array.py
index 230d79bd..d894c9ed 100644
--- a/geopandas/array.py
+++ b/geopandas/array.py
@@ -1029,6 +1029,9 @@ class GeometryArray(ExtensionArray):
         lvalues = self
         rvalues = convert_values(other)
 
+        if len(lvalues) != len(rvalues):
+            raise ValueError("Lengths must match to compare")
+
         # If the operator is not defined for the underlying objects,
         # a TypeError should be raised
         res = [op(a, b) for (a, b) in zip(lvalues, rvalues)]
@@ -1038,3 +1041,6 @@ class GeometryArray(ExtensionArray):
 
     def __eq__(self, other):
         return self._binop(other, operator.eq)
+
+    def __ne__(self, other):
+        return self._binop(other, operator.ne)
diff --git a/geopandas/plotting.py b/geopandas/plotting.py
index 476085cf..3422e49b 100644
--- a/geopandas/plotting.py
+++ b/geopandas/plotting.py
@@ -590,7 +590,9 @@ def plot_dataframe(
         from matplotlib.colors import Normalize
         from matplotlib import cm
 
-        norm = Normalize(vmin=mn, vmax=mx)
+        norm = style_kwds.get("norm", None)
+        if not norm:
+            norm = Normalize(vmin=mn, vmax=mx)
         n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)
         if categorical:
             patches = []


Test patch:
diff --git a/geopandas/tests/test_array.py b/geopandas/tests/test_array.py
index 63ef0eaf..808c9dae 100644
--- a/geopandas/tests/test_array.py
+++ b/geopandas/tests/test_array.py
@@ -625,6 +625,20 @@ def test_getitem():
     assert P5.equals(points[1])
 
 
+def test_equality_ops():
+    with pytest.raises(ValueError):
+        P[:5] == P[:7]
+
+    a1 = from_shapely([points[1], points[2], points[3]])
+    a2 = from_shapely([points[1], points[0], points[3]])
+
+    res = a1 == a2
+    assert res.tolist() == [True, False, True]
+
+    res = a1 != a2
+    assert res.tolist() == [False, True, False]
+
+
 def test_dir():
     assert "contains" in dir(P)
     assert "data" in dir(P)
diff --git a/geopandas/tests/test_extension_array.py b/geopandas/tests/test_extension_array.py
index 7519c3cc..a0d02806 100644
--- a/geopandas/tests/test_extension_array.py
+++ b/geopandas/tests/test_extension_array.py
@@ -13,6 +13,8 @@ A set of fixtures are defined to provide data for the tests (the fixtures
 expected to be available to pytest by the inherited pandas tests).
 
 """
+import operator
+
 import numpy as np
 import pandas as pd
 from pandas.tests.extension import base as extension_tests
@@ -261,7 +263,9 @@ def all_boolean_reductions(request):
     return request.param
 
 
-@pytest.fixture(params=["__eq__", "__ne__", "__le__", "__lt__", "__ge__", "__gt__"])
+# only == and != are support for GeometryArray
+# @pytest.fixture(params=["__eq__", "__ne__", "__le__", "__lt__", "__ge__", "__gt__"])
+@pytest.fixture(params=["__eq__", "__ne__"])
 def all_compare_operators(request):
     """
     Fixture for dunder names for common compare operations
@@ -393,31 +397,25 @@ class TestArithmeticOps(extension_tests.BaseArithmeticOpsTests):
 
 
 class TestComparisonOps(extension_tests.BaseComparisonOpsTests):
-    @not_yet_implemented
+    def _compare_other(self, s, data, op_name, other):
+        op = getattr(operator, op_name.strip("_"))
+        result = op(s, other)
+        expected = s.combine(other, op)
+        self.assert_series_equal(result, expected)
+
+    @skip_pandas_below_024
     def test_compare_scalar(self, data, all_compare_operators):  # noqa
         op_name = all_compare_operators
         s = pd.Series(data)
-        self._compare_other(s, data, op_name, 0)
+        self._compare_other(s, data, op_name, data[0])
 
-    @not_yet_implemented
+    @skip_pandas_below_024
     def test_compare_array(self, data, all_compare_operators):  # noqa
         op_name = all_compare_operators
         s = pd.Series(data)
         other = pd.Series([data[0]] * len(data))
         self._compare_other(s, data, op_name, other)
 
-    def test_direct_arith_with_series_returns_not_implemented(self, data):
-        # EAs should return NotImplemented for ops with Series.
-        # Pandas takes care of unboxing the series and calling the EA's op.
-        other = pd.Series(data)
-        if hasattr(data, "__eq__"):
-            result = data.__eq__(other)
-            assert result is NotImplemented
-        else:
-            raise pytest.skip(
-                "{} does not implement __eq__".format(data.__class__.__name__)
-            )
-
 
 class TestMethods(extension_tests.BaseMethodsTests):
     @no_sorting
diff --git a/geopandas/tests/test_plotting.py b/geopandas/tests/test_plotting.py
index ecb2d810..b9c5e3d0 100644
--- a/geopandas/tests/test_plotting.py
+++ b/geopandas/tests/test_plotting.py
@@ -45,7 +45,9 @@ class TestPointPlotting:
         self.points = GeoSeries(Point(i, i) for i in range(self.N))
 
         values = np.arange(self.N)
+
         self.df = GeoDataFrame({"geometry": self.points, "values": values})
+        self.df["exp"] = (values * 10) ** 3
 
         multipoint1 = MultiPoint(self.points)
         multipoint2 = rotate(multipoint1, 90)
@@ -176,6 +178,21 @@ class TestPointPlotting:
         # last point == top of colorbar
         np.testing.assert_array_equal(point_colors[-1], cbar_colors[-1])
 
+        # # Normalized legend
+        # the colorbar matches the Point colors
+        norm = matplotlib.colors.LogNorm(
+            vmin=self.df[1:].exp.min(), vmax=self.df[1:].exp.max()
+        )
+        ax = self.df[1:].plot(column="exp", cmap="RdYlGn", legend=True, norm=norm)
+        point_colors = ax.collections[0].get_facecolors()
+        cbar_colors = ax.get_figure().axes[1].collections[0].get_facecolors()
+        # first point == bottom of colorbar
+        np.testing.assert_array_equal(point_colors[0], cbar_colors[0])
+        # last point == top of colorbar
+        np.testing.assert_array_equal(point_colors[-1], cbar_colors[-1])
+        # colorbar generated proper long transition
+        assert cbar_colors.shape == (256, 4)
+
     def test_subplots_norm(self):
         # colors of subplots are the same as for plot (norm is applied)
         cmap = matplotlib.cm.viridis_r