diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a6b3ff39..56a044b3 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -76,7 +76,8 @@ jobs: -n auto \ --color yes \ --cov mapclassify --cov-report xml --cov-append \ - --doctest-only mapclassify + --doctest-only \ + --mpl mapclassify - name: codecov (${{ matrix.os }}, ${{ matrix.environment-file }}) uses: codecov/codecov-action@v4 diff --git a/ci/310-numba.yaml b/ci/310-numba.yaml index 20cd22f1..10a9e532 100644 --- a/ci/310-numba.yaml +++ b/ci/310-numba.yaml @@ -15,6 +15,7 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov - matplotlib # optional diff --git a/ci/310.yaml b/ci/310.yaml index d1e7f5ef..42a7cb20 100644 --- a/ci/310.yaml +++ b/ci/310.yaml @@ -15,5 +15,6 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov - matplotlib diff --git a/ci/311-numba.yaml b/ci/311-numba.yaml index 3c62a28e..4639be60 100644 --- a/ci/311-numba.yaml +++ b/ci/311-numba.yaml @@ -15,6 +15,7 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov - matplotlib # optional diff --git a/ci/311.yaml b/ci/311.yaml index c8eb4299..2f5d309b 100644 --- a/ci/311.yaml +++ b/ci/311.yaml @@ -15,6 +15,7 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov - matplotlib # docs diff --git a/ci/312-dev.yaml b/ci/312-dev.yaml index 273afe06..e94b49b7 100644 --- a/ci/312-dev.yaml +++ b/ci/312-dev.yaml @@ -7,6 +7,7 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov # optional - pyproj diff --git a/ci/312-numba.yaml b/ci/312-numba.yaml index dd107396..54934992 100644 --- a/ci/312-numba.yaml +++ b/ci/312-numba.yaml @@ -16,6 +16,7 @@ dependencies: - pytest-cov - pytest-xdist - pytest-doctestplus + - pytest-mpl - codecov - matplotlib # optional diff --git a/ci/312.yaml b/ci/312.yaml index d1cd6ad9..5f83b27c 100644 --- a/ci/312.yaml +++ b/ci/312.yaml @@ -15,6 +15,7 @@ dependencies: - pytest - pytest-cov - pytest-xdist + - pytest-mpl - codecov - matplotlib # docs diff --git a/mapclassify/classifiers.py b/mapclassify/classifiers.py index 515a0dc0..8ceae3ac 100644 --- a/mapclassify/classifiers.py +++ b/mapclassify/classifiers.py @@ -1123,6 +1123,66 @@ def plot( plt.savefig(file_name, dpi=dpi) return f, ax + def plot_histogram( + self, + color="dodgerblue", + linecolor="black", + linewidth=None, + ax=None, + despine=True, + **kwargs, + ): + """Plot histogram of `y` with bin values superimposed + + Parameters + ---------- + color : str, optional + hue to color bars of the histogram, by default "dodgerblue". + linecolor : str, optional + color of the lines demarcating each class bin, by default "black" + linewidth : int, optional + change the linewidth demarcating each class bin + ax : matplotlib.Axes, optional + axes object to plot onto, by default None + despine : bool, optional + If True, to use seaborn's despine function to remove top and right axes, + default is True + kwargs : dict, optional + additional keyword arguments passed to matplotlib.axes.Axes.hist, by default + None + + Returns + ------- + matplotlib.Axes + an Axes object with histogram and class bins + + Raises + ------ + ImportError + depends matplotlib and rasies if not installed + """ + try: + import matplotlib.pyplot as plt + + if ax is None: + _, ax = plt.subplots() + except ImportError as e: + raise ImportError from e( + "You must have matplotlib available to use this function" + ) + # plot `y` as a histogram + ax.hist(self.y, color=color, **kwargs) + # get the top of the ax so we know how high to raise each class bar + lim = ax.get_ylim()[1] + # plot upper limit of each bin + for i in self.bins: + ax.vlines(i, 0, lim, color=linecolor, linewidth=linewidth) + # despine if specified + if despine: + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + return ax + class HeadTailBreaks(MapClassifier): """ diff --git a/mapclassify/tests/baseline/test_histogram_plot.png b/mapclassify/tests/baseline/test_histogram_plot.png new file mode 100644 index 00000000..1c1274bc Binary files /dev/null and b/mapclassify/tests/baseline/test_histogram_plot.png differ diff --git a/mapclassify/tests/baseline/test_histogram_plot_despine.png b/mapclassify/tests/baseline/test_histogram_plot_despine.png new file mode 100644 index 00000000..e46eea1c Binary files /dev/null and b/mapclassify/tests/baseline/test_histogram_plot_despine.png differ diff --git a/mapclassify/tests/baseline/test_histogram_plot_linewidth.png b/mapclassify/tests/baseline/test_histogram_plot_linewidth.png new file mode 100644 index 00000000..6e8f16d7 Binary files /dev/null and b/mapclassify/tests/baseline/test_histogram_plot_linewidth.png differ diff --git a/mapclassify/tests/test_mapclassify.py b/mapclassify/tests/test_mapclassify.py index dc06e190..d90ea226 100644 --- a/mapclassify/tests/test_mapclassify.py +++ b/mapclassify/tests/test_mapclassify.py @@ -736,3 +736,26 @@ def test_pooled_bad_classifier(self): message = f"'{classifier}' not a valid classifier." with pytest.raises(ValueError, match=message): Pooled(self.data, classifier=classifier, k=4) + + +class TestPlots: + def setup_method(self): + n = 20 + self.data = numpy.array([numpy.arange(n) + i * n for i in range(1, 4)]).T + + @pytest.mark.mpl_image_compare + def test_histogram_plot(self): + ax = Quantiles(self.data).plot_histogram() + return ax.get_figure() + + @pytest.mark.mpl_image_compare + def test_histogram_plot_despine(self): + ax = Quantiles(self.data).plot_histogram(despine=False) + return ax.get_figure() + + @pytest.mark.mpl_image_compare + def test_histogram_plot_linewidth(self): + ax = Quantiles(self.data).plot_histogram( + linewidth=3, linecolor="red", color="yellow" + ) + return ax.get_figure() diff --git a/pyproject.toml b/pyproject.toml index 2534ba5e..5aba00a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,13 @@ build-backend = "setuptools.build_meta" name = "mapclassify" dynamic = ["version"] maintainers = [ - {name = "Serge Rey", email = "sjsrey@gmail.com"}, - {name = "Wei Kang", email = "weikang9009@gmail.com"}, + { name = "Serge Rey", email = "sjsrey@gmail.com" }, + { name = "Wei Kang", email = "weikang9009@gmail.com" }, ] -license = {text = "BSD 3-Clause"} +license = { text = "BSD 3-Clause" } description = "Classification Schemes for Choropleth Maps." keywords = ["spatial statistics", "geovisualization"] -readme = {text = """\ +readme = { text = """\ `mapclassify` implements a family of classification schemes for choropleth maps. Its focus is on the determination of the number of classes, and the assignment of observations to those classes. It is intended for use with upstream mapping @@ -26,7 +26,7 @@ For further theoretical background see "`Choropleth Mapping`_" in Rey, S.J., D. .. _geopandas: https://geopandas.org/mapping.html .. _geoplot: https://residentmario.github.io/geoplot/user_guide/Customizing_Plots.html .. _Choropleth Mapping: https://geographicdata.science/book/notebooks/05_choropleth.html -""", content-type = "text/x-rst"} +""", content-type = "text/x-rst" } classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", @@ -52,14 +52,8 @@ Home = "https://pysal.org/mapclassify/" Repository = "https://github.com/pysal/mapclassify" [project.optional-dependencies] -speedups = [ - "numba>=0.54", -] -dev = [ - "black", - "ruff", - "pre-commit", -] +speedups = ["numba>=0.54"] +dev = ["black", "ruff", "pre-commit"] docs = [ "nbsphinx", "numpydoc", @@ -76,14 +70,12 @@ tests = [ "pytest-cov", "pytest-xdist", "pytest-doctestplus", + "pytest-mpl" ] all = ["numba[speedups,dev,docs,tests]"] [tool.setuptools.packages.find] -include = [ - "mapclassify", - "mapclassify.*", -] +include = ["mapclassify", "mapclassify.*"] [tool.black] line-length = 88