Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP classify to rgba #211

Merged
merged 25 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mapclassify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contextlib
from importlib.metadata import PackageNotFoundError, version

from ._classify_API import classify
from ._classify_API import classify, classify_to_rgba
from .classifiers import (
CLASSIFIERS,
BoxPlot,
Expand Down
61 changes: 61 additions & 0 deletions mapclassify/_classify_API.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import pandas as pd
from matplotlib import cm
from matplotlib.colors import Normalize

from .classifiers import (
BoxPlot,
EqualInterval,
Expand Down Expand Up @@ -214,3 +218,60 @@
classifier = _classifiers[scheme](y, k)

return classifier


def classify_to_rgba(
values, classifier="quantiles", k=6, cmap="viridis", nan_color=[255, 255, 255, 255]
):
"""Convert array of values into RGBA colors using a colormap and classifier.

Parameters
----------
values : list-like
array of input values
classifier : str, optional
string description of a mapclassify classifier, by default "quantiles"
k : int, optional
number of classes to form, by default 6
cmap : str, optional
name of matplotlib colormap to use, by default "viridis"
nan_color : list, optional
RGBA color to fill NaN values, by default [255, 255, 255, 255]

Returns
-------
numpy.array
array of lists with each list containing four values that define a color using
RGBA specification.
"""
if not pd.api.types.is_list_like(nan_color) and not len(nan_color) == 4:
raise ValueError("`nan_color` must be list-like of 4 values: (R,G,B,A)")

Check warning on line 248 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L247-L248

Added lines #L247 - L248 were not covered by tests

# only operate on non-NaN values
v = pd.Series(values)
legit_indices = v[~v.isna()].index.values

Check warning on line 252 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L251-L252

Added lines #L251 - L252 were not covered by tests

# transform (non-NaN) values into class bins
bins = classify(v.dropna().values, scheme=classifier, k=k).yb

Check warning on line 255 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L255

Added line #L255 was not covered by tests

# create a normalizer using the data's range (not strictly 1-k...)
norm = Normalize(min(bins), max(bins))

Check warning on line 258 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L258

Added line #L258 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not clear why there is a classifier called prior to here.
It seems to me this is trying to do a classless choropleth map [1].
If that is true, then the first classifier can be omitted and line 258 could become
norm = Normalize(v.min(), v.max())

But maybe I'm missing something here in my understanding?

[1] Tobler, W. (1973). Choropleth maps without class intervals. Geographical
Analysis, 5:262-265

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the whole idea is to use the classifier to get colors. It might not need that call to Normalize, but we're taking the value, discretizing it to the class bins, then using the bins to get colors from a matplotlib colormap

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
So do I have this right?

This was classified k=8, fisher jenks. So I'm thinking there should only be 8 colors in the figure, but the additional z axis allows for differentiation between units in the same bin but with different values. The color of the hex fill would be whatever class the hex value is placed into.

Copy link
Member Author

@knaaptime knaaptime May 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah exactly. Z and color are disconnnected there. Color is based on Jenks but height is continuous

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(i.e. all this does is take a cmap and translate bins --> colors)

so if you remove the z-dimension, this function gives you the same choropleth geopandas would give you. But then you can also use extrusion to encode the same or a different variable. A bit more at the end of this


# map values to colors
n_cmap = cm.ScalarMappable(norm=norm, cmap=cmap)

Check warning on line 261 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L261

Added line #L261 was not covered by tests

# create array of RGB values (lists of 4) of length n
vals = [n_cmap.to_rgba(i, alpha=None) for i in bins]

Check warning on line 264 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L264

Added line #L264 was not covered by tests

# convert decimals to whole numbers
rgbas = []
for val in vals:

Check warning on line 268 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L267-L268

Added lines #L267 - L268 were not covered by tests
# convert each value in the array of lists
rgbas.append([round(i * 255, 0) for i in val])

Check warning on line 270 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L270

Added line #L270 was not covered by tests

# replace non-nan values with colors
colors = pd.Series(rgbas, index=legit_indices)
v.update(colors)
v = v.fillna(f"{nan_color}").apply(list)

Check warning on line 275 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L273-L275

Added lines #L273 - L275 were not covered by tests

return v.values

Check warning on line 277 in mapclassify/_classify_API.py

View check run for this annotation

Codecov / codecov/patch

mapclassify/_classify_API.py#L277

Added line #L277 was not covered by tests
Loading