diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b3429c..8c530bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,3 @@ -This repo welcomes additions from anyone passionate about colourmaps! This is my first Python package so it's a little rough around the edges. +This repo welcomes additions from anyone passionate about colormaps! This is my first Python package so it's a little rough around the edges. If you do make a PR, please check it against the tests first, this will make it easier for me to merge it and push a new release out to PyPI and conda-forge. diff --git a/LICENSE b/LICENSE index 0729b87..fc6cda0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Colourmaps in cmcrameri/cm/cmaps: +Colormaps in cmcrameri/cm/cmaps: Copyright (c) 2020 Fabio Crameri Python scripts and packaging: diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 040c7d0..a15eede 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,14 +1,14 @@ -# To update the colourmaps +# To update the colormaps - Download latest version from Zenodo https://zenodo.org/record/4491293 -- From directory above the colourmaps master folder, create a new directrory called cmaps and do some Bash magic to extract colormap .txt files +- From directory above the colormaps master folder, create a new directrory called cmaps and do some Bash magic to extract colormap .txt files `find . -name '*.txt' -exec cp {} ./cmaps \;` -- Remove the discrete colourmaps (those with numbers at the end) +- Remove the discrete colormaps (those with numbers at the end) -- Paste colourmaps to the cmaps directory in cmcrameri +- Paste colormaps to the cmaps directory in cmcrameri -- Run the `show_cmaps`function and save the figure in teh home directory of the project +- Run the `show_cmaps`function and save the figure in the home directory of the project diff --git a/README.md b/README.md index e94e994..91da5bd 100644 --- a/README.md +++ b/README.md @@ -11,59 +11,72 @@ # cmcrameri -This is a Python wrapper around Fabio Crameri's perceptually uniform colour maps +This is a Python wrapper around Fabio Crameri's perceptually uniform colormaps. -http://www.fabiocrameri.ch/colourmaps.php + -All credit for creating the colourmaps to Fabio. Any errors in the Python implementation of colourmaps are my own. +All credit for creating the colormaps to Fabio. +Any errors in the Python implementation of colormaps are my own. -This version is based on Scientific Colourmaps Version 7.0 (02.02.2021) +This version is based on *Scientific colour maps* [version 7.0](https://zenodo.org/record/4491293) (02.02.2021). ### Install -With pip: - -`pip install cmcrameri` - -With conda: +With `pip`: +``` +pip install cmcrameri +``` +With `conda`: ``` -conda config --add channels conda-forge -conda install cmcrameri +conda install -c conda-forge cmcrameri ``` + ### Usage example ```python -from cmcrameri import cm +import cmcrameri.cm as cmc import matplotlib.pyplot as plt import numpy as np -x = np.linspace(0, 100, 100)[None, :] -plt.imshow(x, aspect='auto', cmap=cm.batlow) # or any of the other colourmaps made by Fabio Crameri + +x = np.linspace(0, 1, 100)[np.newaxis, :] + +plt.imshow(x, aspect='auto', cmap=cmc.batlow) plt.axis('off') plt.show() ``` +Alternatively, the registered name string can be used. +```python +import cmrameri # required in order to register the colormaps with Matplotlib +... +plt.imshow(x, aspect='auto', cmap='cmc.batlow') +``` + ### Extra instructions -You can access all the core colourmaps from Fabio Crameri's list by `cm.` -You can use tab autocompletion on `cm` if your editor supports it +You can access all the core colormaps from Fabio Crameri's list by `cmcrameri.cm.`. -For a reversed colourmap, append `_r` to the colourmap name +You can use tab autocompletion on `cmcrameri.cm` if your editor supports it. -Categorical colormaps have the suffix `S` +For a reversed colormap, append `_r` to the colormap name. -For an image of all the available colourmaps without leaving the comfort of your Python session +Categorical colormaps have the suffix `S`. +For an image of all the available colormaps without leaving the comfort of your Python session: ```python -from cmcrameri.cm import show_cmaps +from cmcrameri import show_cmaps + show_cmaps() ``` +![Figure demonstrating the colormaps](cmcrameri/colormaps.png) -To make the underlying RGB values available, the original text files are shipped as part of the package. Find them on your system with: +The original colormap text files are shipped as part of the package. +Find them on your system with: ```python -from cmcrameri import cm -cm.paths +from cmcrameri.cm import paths + +paths ``` ### License This work is licensed under an [MIT license](https://mit-license.org/). - diff --git a/cmcrameri/__init__.py b/cmcrameri/__init__.py index 6f5d33b..e1a36d4 100644 --- a/cmcrameri/__init__.py +++ b/cmcrameri/__init__.py @@ -1,12 +1,21 @@ """ -cmcrameri is a package of perceptuallt uniform colourmaps for the geosciences -This is mererly a wrapper for previosuly created colour maps. All credit to Fabio Crameri -http://www.fabiocrameri.ch/colourmaps.php -See README.md for an overview and instructions -""" +cmcrameri is a package of perceptually uniform colormaps for the geosciences. +This is merely a wrapper for previously created colormaps, +all credit to Fabio Crameri +https://www.fabiocrameri.ch/colourmaps/ +See README.md for an overview and instructions. +""" from __future__ import absolute_import + from . import cm +from .cm import show_cmaps + + +__all__ = ( + "cm", + "show_cmaps", +) __authors__ = ['Callum Rollo '] diff --git a/cmcrameri/cm.py b/cmcrameri/cm.py index 3f6cda8..85c0ad9 100755 --- a/cmcrameri/cm.py +++ b/cmcrameri/cm.py @@ -1,58 +1,193 @@ """ -Perceptually uniform colourmaps for geosciences - -Packaging of colourmaps created by Fabio Crameri http://www.fabiocrameri.ch/colourmaps.php +Packaging of colormaps created by Fabio Crameri +https://www.fabiocrameri.ch/colourmaps/ Created by Callum Rollo 2020-05-06 """ - -import numpy as np -from pathlib import Path -from matplotlib.colors import LinearSegmentedColormap import matplotlib.pyplot as plt -import os -# Find the colormap text files and make a list of the paths -text_file_folder = os.path.join(os.path.dirname(__file__), 'cmaps') -paths = list(Path(text_file_folder).glob('*.txt')) -crameri_cmaps = dict() -crameri_cmaps_r = dict() -crameri_cmaps_s = dict() -for cmap_path in paths: - # Name of colour map taken from text file - cmap_name = os.path.split(cmap_path)[1][:-4] - cm_data = np.loadtxt(str(cmap_path)) - # Make a linear segmented colour map - if cmap_name[-1] == 'S': - crameri_cmaps_s[cmap_name] = LinearSegmentedColormap.from_list(cmap_name, cm_data) - plt.cm.register_cmap(name='cmc.' + cmap_name, cmap=crameri_cmaps_s[cmap_name]) - continue - crameri_cmaps[cmap_name] = LinearSegmentedColormap.from_list(cmap_name, cm_data) - plt.cm.register_cmap(name='cmc.' + cmap_name, cmap=crameri_cmaps[cmap_name]) - # reverse the colour map and add this to the dictionary crameri_cmaps_r, mpt fpr categorical maps - crameri_cmaps_r[cmap_name + '_r'] = LinearSegmentedColormap.from_list(cmap_name + '_r', cm_data[::-1, :]) - plt.cm.register_cmap(name='cmc.' + cmap_name + '_r', cmap=crameri_cmaps_r[cmap_name + '_r']) - - -def show_cmaps(): +import numpy as np + + +_cmap_names_sequential = ( + "batlow", "batlowW", "batlowK", + "devon", "lajolla", "bamako", + "davos", "bilbao", "nuuk", + "oslo", "grayC", "hawaii", + "lapaz", "tokyo", "buda", + "acton", "turku", "imola", +) + +_cmap_names_diverging = ( + "broc", "cork", "vik", + "lisbon", "tofino", "berlin", + "roma", "bam", "vanimo", +) + +_cmap_names_multi_sequential = ( + "oleron", "bukavu", "fes", +) + +_cmap_base_names_categorical = tuple( + name + for name in _cmap_names_sequential + if name not in {"batlowW", "batlowK"} +) +_cmap_names_categorical = tuple( + f"{name}S" + for name in _cmap_base_names_categorical +) + +_cmap_base_names_cyclic = ( + "roma", "bam", + "broc", "cork", "vik", +) +_cmap_names_cyclic = tuple( + f"{name}O" + for name in _cmap_base_names_cyclic +) + +def _load_cmaps(): + from pathlib import Path + from matplotlib.colors import ListedColormap + + # Prepended to cmap names when registering + cmap_reg_prefix = "cmc." + + cmaps = {} + + def register(cmap): + # Register in Matplotlib + plt.cm.register_cmap(name=f"{cmap_reg_prefix}{cmap.name}", cmap=cmap) + # Add to dict + cmaps[cmap.name] = cmap + + # Find the colormap text files and make a list of the paths + cmap_data_dir = Path(__file__).parent / 'cmaps' + paths = sorted(cmap_data_dir.glob('*.txt')) + + # Load data and generate Colormap objects + for cmap_path in paths: + # Name of colormap is taken from the text file name + cmap_name = cmap_path.stem + + # Categorize + is_categorical = cmap_name.endswith("S") + is_cyclic = cmap_name.endswith("O") + cmap_name_base = cmap_name if not (is_categorical or is_cyclic) else cmap_name[:-1] + if not is_cyclic: + is_sequential = cmap_name_base in _cmap_names_sequential + is_diverging = cmap_name_base in _cmap_names_diverging + is_multi_sequential = cmap_name_base in _cmap_names_multi_sequential + else: + is_sequential = is_diverging = is_multi_sequential = False + + # Check categorization + assert sum( + [is_cyclic, is_sequential, is_diverging, is_multi_sequential] + ) == 1, f"{cmap_name} not categorized properly" + assert not is_categorical or cmap_name_base in _cmap_base_names_categorical, cmap_name + assert not is_cyclic or cmap_name_base in _cmap_base_names_cyclic, cmap_name + + # Load data + data = np.loadtxt(cmap_path) + N = data.shape[0] + N0 = 256 if not is_categorical else 100 + assert N == N0, f"N should be {N0} but is {N}" + + # Create and register colormap + cmap = ListedColormap(colors=data, name=cmap_name) + register(cmap) + + # For non-categorical, also create and register reverse version + if not is_categorical: + register(cmap.reversed()) + + return paths, cmaps + + +paths, cmaps = _load_cmaps() + +# Add all cmaps to the `cmcrameri.cm` namespace +locals().update(cmaps) + + +def show_cmaps(*, ncols=6, figwidth=8): """ - A rough function for a quick plot of the colourmaps. Nowhere near as pretty as the original - see http://www.fabiocrameri.ch/colourmaps.php - :return: + For the original, see + https://www.fabiocrameri.ch/colourmaps/ """ - x = np.linspace(0, 100, 100)[None, :] - fig, axs = plt.subplots(int(np.ceil(len(crameri_cmaps) / 7)), 7, figsize=(22, 10)) - fig.subplots_adjust(hspace=.8, wspace=.08) - axs = axs.ravel() - for ax in axs: - ax.axis('off') - for c, cmap_selected in enumerate(sorted(crameri_cmaps.keys())): - colourmap = crameri_cmaps[cmap_selected] - axs[c].pcolor(x, cmap=colourmap) - axs[c].text(5, -0.3, cmap_selected, fontsize=26) - - -# So colourmaps can be called in other programs -locals().update(crameri_cmaps) -locals().update(crameri_cmaps_r) -locals().update(crameri_cmaps_s) + import math + + x = np.linspace(0, 1, 256)[np.newaxis, :] + + groups = ( + ("Sequential", _cmap_names_sequential), + ("Diverging", _cmap_names_diverging), + ("Multi-sequential", _cmap_names_multi_sequential), + ("Cyclic", _cmap_names_cyclic), + ) + + nrows = 1 + istarts = [] + for group_name, group in groups: + n = len(group) + istarts.append(nrows) + nrows += math.ceil(n / ncols) + if group_name != groups[-1][0]: + nrows += 1 # group spacer row + + nrows_titles = len(groups) + nrows_cmaps = nrows - nrows_titles + + hrel_spacer = 0.3 # spacer height relative cmap row height + hratios = [1 for _ in range(nrows)] + for i in istarts: + hratios[i-1] = hrel_spacer # group spacer row + + hrow = 0.4 # size of cmap row + hspace = 0.7 # hspace, relative to `hrow` + hbottom = 0.2 + htop = 0.05 + figheight = ( + hbottom + + htop + + hrow*nrows_cmaps + + hrow*hrel_spacer*nrows_titles + + hrow*hspace*(nrows - 1) + ) + + fig, axs = plt.subplots(nrows, ncols, + figsize=(figwidth, figheight), + gridspec_kw=dict( + left=0.01, right=0.99, + top=1 - htop/figheight, + bottom=hbottom/figheight, + hspace=hspace/np.mean(hratios), + wspace=0.08, + height_ratios=hratios + ) + ) + fig.set_tight_layout(False) + + for ax in axs.flat: + ax.set_axis_off() + + for istart, (group_name, group) in zip(istarts, groups): + + # Group label + ax0 = axs[istart, 0] + ax0.text(0.01, 1.02, group_name, size=24, c="0.4", style="italic", + va="bottom", ha="left", transform=ax0.transAxes) + + for ax, cmap_name in zip(axs[istart:].flat, group): + + cmap = cmaps[cmap_name] + ax.imshow(x, cmap=cmap, aspect="auto") + ax.text(0.01 * ncols/6, -0.03, cmap_name, size=14, color="0.2", + va="top", transform=ax.transAxes) + + +if __name__ == "__main__": + show_cmaps() + plt.savefig("colormaps.png", dpi=200) diff --git a/cmcrameri/colormaps.png b/cmcrameri/colormaps.png index b691866..6f86819 100644 Binary files a/cmcrameri/colormaps.png and b/cmcrameri/colormaps.png differ diff --git a/setup.py b/setup.py index d96ed0e..df9ecf0 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ license='MIT', long_description=long_description, long_description_content_type='text/markdown', - description = 'Perceptually uniform colourmaps', + description = 'Perceptually uniform colormaps by Fabio Crameri', author = 'Callum Rollo', author_email = 'c.rollo@outlook.com', url = 'https://github.com/callumrollo/cmcrameri', diff --git a/tests/test_cmcrameri.py b/tests/test_cmcrameri.py index a677bda..d7c2d37 100644 --- a/tests/test_cmcrameri.py +++ b/tests/test_cmcrameri.py @@ -1,10 +1,11 @@ """ -Test that the program a) finds the text files and b) creates colourmaps +Test that the program a) finds the text files and b) creates colormaps """ -from matplotlib.colors import LinearSegmentedColormap -from matplotlib.pyplot import get_cmap -from pathlib import Path import sys +from pathlib import Path + +from matplotlib.colors import Colormap +from matplotlib.pyplot import get_cmap library_dir = Path(__file__).parent.parent.absolute() sys.path.append(str(library_dir)) @@ -22,18 +23,18 @@ def test_cmap_import(): for name, cmap in vars(cm).items(): increment = 1 # See if it is a colormap. - if isinstance(cmap, LinearSegmentedColormap): + if isinstance(cmap, Colormap): if name[-1] != 'S': increment = 0.5 no_cmaps += increment cmap_names.append(name) - # Should be as many colour maps as files plus reversed for non categorical ones + # Should be as many colormaps as files plus reversed for non categorical ones assert int(no_cmaps) == len(cm.paths) def test_get_cmap(): for name, cmap in vars(cm).items(): # See if it is a colormap. - if isinstance(cmap, LinearSegmentedColormap): + if isinstance(cmap, Colormap): # if cmap hasn't been correctly registered as # cmc.name, it will raise a ValueError alt_cmap = get_cmap('cmc.' + name) @@ -41,6 +42,7 @@ def test_get_cmap(): assert alt_cmap is cmap -test_find_files() -test_cmap_import() -test_get_cmap() +if __name__ == "__main__": + test_find_files() + test_cmap_import() + test_get_cmap()