diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 245167da3..4dc1cccd8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -47,17 +47,21 @@ ProPlot v0.4.0 (2020-##-##)
===========================
.. rubric:: Deprecated
-- Remove redundant `~proplot.rctools.use_fonts`, use ``rcParams['sans-serif']``
- precedence instead (:pr:`95`).
-- `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualx` no longer accept "scale-spec" arguments.
- Must be a function, two functions, or an axis scale instance (:pr:`96`).
- Remove ``subplots.innerspace``, ``subplots.titlespace``,
``subplots.xlabspace``, and ``subplots.ylabspace`` spacing arguments,
automatically calculate default non-tight spacing using `~proplot.subplots._get_space`
based on current tick lengths, label sizes, etc.
+- Remove redundant `~proplot.rctools.use_fonts`, use ``rcParams['sans-serif']``
+ precedence instead (:pr:`95`).
+- `~proplot.axes.Axes.dualx` and `~proplot.axes.Axes.dualx` no longer accept "scale-spec" arguments.
+ Must be a function, two functions, or an axis scale instance (:pr:`96`).
+- Rename `~proplot.styletools.Cycle` ``samples`` to ``N``, rename
+ `~proplot.styletools.show_colors` ``nbreak`` to ``nhues`` (:pr:`98`).
.. rubric:: Features
+- Add `~proplot.styletools.LinearSegmentedColormap.from_file` static methods (:pr:`98`).
+ You can now load files by passing a name to `~proplot.styletools.Colormap`.
- Add Fira Math as DejaVu Sans-alternative (:pr:`95`). Has complete set of math characters.
- Add TeX Gyre Heros as Helvetica-alternative (:pr:`95`). This is the new open-source default font.
- Add `xlinewidth`, `ylinewidth`, `xgridcolor`, `ygridcolor` keyword
@@ -81,7 +85,7 @@ ProPlot v0.4.0 (2020-##-##)
- Imperative mood for docstring summaries (:pr:`92`).
- Fix `~proplot.styletools.show_cycles` bug (:pr:`90`) and show cycles using colorbars
- rather than lines.
+ rather than lines (:pr:`98`).
.. rubric:: Internals
diff --git a/docs/1dplots.ipynb b/docs/1dplots.ipynb
index 46a2152b3..4ad0451ff 100644
--- a/docs/1dplots.ipynb
+++ b/docs/1dplots.ipynb
@@ -88,11 +88,13 @@
"axs.format(suptitle='Automatic subplot formatting')\n",
"\n",
"# Plot DataArray\n",
- "cycle = plot.Cycle(plot.shade('light blue', 0.4), fade=90, space='hpl')\n",
+ "color = plot.shade('light blue', 0.4)\n",
+ "cycle = plot.Cycle(color, fade=90, space='hpl')\n",
"axs[0].plot(da, cycle=cycle, lw=3, colorbar='ul', colorbar_kw={'locator': 20})\n",
"\n",
"# Plot Dataframe\n",
- "cycle = plot.Cycle(plot.shade('jade', 0.4), fade=90, space='hpl')\n",
+ "color = plot.shade('jade', 0.4)\n",
+ "cycle = plot.Cycle(color, fade=90, space='hpl')\n",
"axs[1].plot(df, cycle=cycle, lw=3, legend='uc')"
]
},
diff --git a/docs/2dplots.ipynb b/docs/2dplots.ipynb
index 91e753a2f..88b8ad927 100644
--- a/docs/2dplots.ipynb
+++ b/docs/2dplots.ipynb
@@ -95,7 +95,9 @@
"axs[0].format(yreverse=True)\n",
"\n",
"# Plot DataFrame\n",
- "axs[1].contourf(df, cmap='Blues', colorbar='r', linewidth=0.7, color='gray7')\n",
+ "axs[1].contourf(\n",
+ " df, cmap='Blues', colorbar='r', linewidth=0.7, color='gray7'\n",
+ ")\n",
"axs[1].format(xtickminor=False)"
]
},
diff --git a/docs/environment.yml b/docs/environment.yml
index 05593c12e..76e9ee41b 100644
--- a/docs/environment.yml
+++ b/docs/environment.yml
@@ -18,7 +18,6 @@ dependencies:
- pip
- pip:
- ..
- - lxml
- pyyaml
- pyqt5
- nbsphinx
diff --git a/proplot/colors/open.txt b/proplot/colors/opencolor.txt
similarity index 100%
rename from proplot/colors/open.txt
rename to proplot/colors/opencolor.txt
diff --git a/proplot/cycles/538.hex b/proplot/cycles/538.hex
new file mode 100644
index 000000000..52f7cec0b
--- /dev/null
+++ b/proplot/cycles/538.hex
@@ -0,0 +1 @@
+'#008fd5', '#fc4f30', '#e5ae38', '#6d904f', '#8b8b8b', '#810f7c',
diff --git a/proplot/cycles/Contrast.hex b/proplot/cycles/Contrast.hex
new file mode 100644
index 000000000..11232d974
--- /dev/null
+++ b/proplot/cycles/Contrast.hex
@@ -0,0 +1 @@
+"#2B4162", "#FA9F42", "#E0E0E2", "#A21817", "#0B6E4F",
diff --git a/proplot/cycles/Cool.hex b/proplot/cycles/Cool.hex
new file mode 100644
index 000000000..cbafcd43c
--- /dev/null
+++ b/proplot/cycles/Cool.hex
@@ -0,0 +1 @@
+"#6C464F", "#9E768F", "#9FA4C4", "#B3CDD1", "#C7F0BD",
diff --git a/proplot/cycles/FlatUI.hex b/proplot/cycles/FlatUI.hex
new file mode 100644
index 000000000..d35ce690a
--- /dev/null
+++ b/proplot/cycles/FlatUI.hex
@@ -0,0 +1 @@
+"#3498db", "#e74c3c", "#95a5a6", "#34495e", "#2ecc71", "#9b59b6",
diff --git a/proplot/cycles/Floral.hex b/proplot/cycles/Floral.hex
new file mode 100644
index 000000000..731923aca
--- /dev/null
+++ b/proplot/cycles/Floral.hex
@@ -0,0 +1 @@
+"#23395B", "#D81E5B", "#FFFD98", "#B9E3C6", "#59C9A5",
diff --git a/proplot/cycles/Hot.hex b/proplot/cycles/Hot.hex
new file mode 100644
index 000000000..b7f06cd14
--- /dev/null
+++ b/proplot/cycles/Hot.hex
@@ -0,0 +1 @@
+"#0D3B66", "#F95738", "#F4D35E", "#FAF0CA", "#EE964B",
diff --git a/proplot/cycles/Sharp.hex b/proplot/cycles/Sharp.hex
new file mode 100644
index 000000000..9b71dfd6d
--- /dev/null
+++ b/proplot/cycles/Sharp.hex
@@ -0,0 +1 @@
+"#007EA7", "#D81159", "#B3CDD1", "#FFBC42", "#0496FF",
diff --git a/proplot/cycles/Warm.hex b/proplot/cycles/Warm.hex
new file mode 100644
index 000000000..5aabebf86
--- /dev/null
+++ b/proplot/cycles/Warm.hex
@@ -0,0 +1 @@
+'#335c67', '#9e2a2b', '#fff3b0', '#e09f3e', '#540b0e'
diff --git a/proplot/cycles/colorblind.hex b/proplot/cycles/colorblind.hex
new file mode 100644
index 000000000..7d7218dd4
--- /dev/null
+++ b/proplot/cycles/colorblind.hex
@@ -0,0 +1 @@
+'#0072B2', '#D55E00', '#009E73', '#CC79A7', '#F0E442', '#56B4E9',
diff --git a/proplot/cycles/colorblind10.hex b/proplot/cycles/colorblind10.hex
new file mode 100644
index 000000000..7d356f95b
--- /dev/null
+++ b/proplot/cycles/colorblind10.hex
@@ -0,0 +1 @@
+"#0173B2", "#DE8F05", "#029E73", "#D55E00", "#CC78BC", "#CA9161", "#FBAFE4", "#949494", "#ECE133", "#56B4E9"
diff --git a/proplot/cycles/default.hex b/proplot/cycles/default.hex
new file mode 100644
index 000000000..9d8b2e450
--- /dev/null
+++ b/proplot/cycles/default.hex
@@ -0,0 +1 @@
+'#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf',
diff --git a/proplot/cycles/ggplot.hex b/proplot/cycles/ggplot.hex
new file mode 100644
index 000000000..61a5f1fe3
--- /dev/null
+++ b/proplot/cycles/ggplot.hex
@@ -0,0 +1 @@
+'#E24A33', '#348ABD', '#988ED5', '#777777', '#FBC15E', '#8EBA42', '#FFB5B8',
diff --git a/proplot/styletools.py b/proplot/styletools.py
index 43b8bb7f7..5f7ff581d 100644
--- a/proplot/styletools.py
+++ b/proplot/styletools.py
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""
Tools for registering and visualizing colormaps, color cycles, color string
-names, and fonts. Defines new colormap classes, new colormap normalizer
+names, and fonts. New colormap classes, new colormap normalizer
classes, and new constructor functions for generating instances of these
-classes. Includes related utilities for manipulating colors. See
+classes. Related utilities for manipulating colors. See
:ref:`Colormaps`, :ref:`Color cycles`, and :ref:`Colors and fonts`
for details.
"""
@@ -14,9 +14,9 @@
import json
import glob
import cycler
-from lxml import etree
+from xml.etree import ElementTree
from numbers import Number, Integral
-from matplotlib import docstring, rcParams
+from matplotlib import rcParams
import numpy as np
import numpy.ma as ma
import matplotlib.colors as mcolors
@@ -39,7 +39,7 @@
]
# Colormap stuff
-CMAPS_CATEGORIES = {
+CMAPS_TABLE = {
# Assorted origin, but these belong together
'Grayscale': (
'Grays', 'Mono', 'GrayCycle',
@@ -124,21 +124,13 @@
'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar',
'gist_rainbow', 'gist_stern', 'gist_yarg',
),
- 'Miscellaneous': (
+ 'Other': (
'binary', 'bwr', 'brg', # appear to be custom matplotlib
'cubehelix', 'wistia', 'CMRmap', # individually released
'seismic', 'terrain', 'nipy_spectral', # origin ambiguous
- ),
+ 'tab10', 'tab20', 'tab20b', 'tab20c', # merged colormap cycles
+ )
}
-CMAPS_DELETE = (
- 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink',
- 'spring', 'summer', 'autumn', 'winter', 'cool', 'wistia',
- 'afmhot', 'gist_heat', 'copper',
- 'seismic', 'bwr', 'brg',
- 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
- 'gnuplot', 'gnuplot2', 'cmrmap', 'hsv', 'hot', 'rainbow',
- 'gist_rainbow', 'jet', 'nipy_spectral', 'gist_ncar', 'cubehelix',
-)
CMAPS_DIVERGING = tuple(
(key1.lower(), key2.lower()) for key1, key2 in (
('PiYG', 'GYPi'),
@@ -156,47 +148,8 @@
('DryWet', 'WetDry')
))
-# Color cycle stuff
-CYCLES_PRESET = {
- # Default matplotlib v2
- 'default': [
- '#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
- '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
- # From stylesheets
- '538': [
- '#008fd5', '#fc4f30', '#e5ae38', '#6d904f', '#8b8b8b', '#810f7c'],
- 'ggplot': [
- '#E24A33', '#348ABD', '#988ED5', '#777777', '#FBC15E', '#8EBA42',
- '#FFB5B8'],
- # The two nice-looking seaborn color cycles
- 'ColorBlind': [
- '#0072B2', '#D55E00', '#009E73', '#CC79A7', '#F0E442', '#56B4E9'],
- # versions with more colors
- 'ColorBlind10': [
- '#0173B2', '#DE8F05', '#029E73', '#D55E00', '#CC78BC', '#CA9161',
- '#FBAFE4', '#949494', '#ECE133', '#56B4E9'],
- # Created with iwanthue and coolers
- 'FlatUI': [
- '#3498db', '#e74c3c', '#95a5a6', '#34495e', '#2ecc71', '#9b59b6'],
- 'Warm': [
- (51, 92, 103), (158, 42, 43), (255, 243, 176),
- (224, 159, 62), (84, 11, 14)],
- 'Cool': ['#6C464F', '#9E768F', '#9FA4C4', '#B3CDD1', '#C7F0BD'],
- 'Sharp': ['#007EA7', '#D81159', '#B3CDD1', '#FFBC42', '#0496FF'],
- 'Hot': ['#0D3B66', '#F95738', '#F4D35E', '#FAF0CA', '#EE964B'],
- 'Contrast': ['#2B4162', '#FA9F42', '#E0E0E2', '#A21817', '#0B6E4F'],
- 'Floral': ['#23395B', '#D81E5B', '#FFFD98', '#B9E3C6', '#59C9A5'],
-}
-CYCLES_DELETE = (
- 'tab10', 'tab20', 'tab20b', 'tab20c',
- 'paired', 'pastel1', 'pastel2', 'dark2',
-) # unappealing cycles, and cycles that are just merged monochrome colormaps
-CYCLES_RENAME = (
- ('Accent', 'Set1'),
-) # rename existing cycles
-
# Named color filter props
-COLORS_SPACE = 'hcl' # dist 'distinct-ness' of colors using this colorspace
+COLORS_SPACE = 'hcl' # color "distincness" is defined with this space
COLORS_THRESH = 0.10 # bigger number equals fewer colors
COLORS_TRANSLATIONS = tuple((re.compile(regex), sub) for regex, sub in (
('/', ' '),
@@ -270,30 +223,6 @@
'Verdana',
]
-# Docstring fragments
-cyclic_doc = """
-cyclic : bool, optional
- Whether the colormap is cyclic. If ``True``, this changes how the
- leftmost and rightmost color levels are selected, and `extend` can only
- be ``'neither'`` (a warning will be issued otherwise).
-"""
-gamma_doc = """
-gamma1 : float, optional
- If >1, makes low saturation colors more prominent. If <1,
- makes high saturation colors more prominent. Similar to the
- `HCLWizard `__ option.
- See `make_mapping_array` for details.
-gamma2 : float, optional
- If >1, makes high luminance colors more prominent. If <1,
- makes low luminance colors more prominent. Similar to the
- `HCLWizard `__ option.
- See `make_mapping_array` for details.
-gamma : float, optional
- Use this to identically set `gamma1` and `gamma2` at once.
-"""
-docstring.interpd.update(gamma_doc=gamma_doc)
-docstring.interpd.update(cyclic_doc=cyclic_doc)
-
def _get_channel(color, channel, space='hcl'):
"""
@@ -387,7 +316,7 @@ def saturate(color, scale=0.5):
def to_rgb(color, space='rgb', cycle=None, alpha=False):
"""
- Translate color in *any* format and from *any* colorspace to an RGB
+ Translate the color in *any* format and from *any* colorspace to an RGB
tuple. This is a generalization of `matplotlib.colors.to_rgb` and the
inverse of `to_xyz`.
@@ -521,9 +450,9 @@ def to_xyz(color, space='hcl', alpha=False):
def _clip_colors(colors, clip=True, gray=0.2):
"""
- Clips impossible colors rendered in an HSl-to-RGB colorspace conversion.
+ Clip impossible colors rendered in an HSL-to-RGB colorspace conversion.
Used by `PerceptuallyUniformColormap`. If `mask` is ``True``, impossible
- colors are masked out
+ colors are masked out.
Parameters
----------
@@ -613,9 +542,9 @@ def _make_segmentdata_array(values, coords=None, ratios=None):
def make_mapping_array(N, data, gamma=1.0, inverse=False):
r"""
- Mostly a copy of `~matplotlib.colors.makeMappingArray`, but allows
+ Similar to `~matplotlib.colors.makeMappingArray` but permits
*circular* hue gradations along 0-360, disables clipping of
- out-of-bounds channel values, and with fancier "gamma" scaling.
+ out-of-bounds channel values, and uses fancier "gamma" scaling.
Parameters
----------
@@ -737,11 +666,11 @@ def make_mapping_array(N, data, gamma=1.0, inverse=False):
return lut
-class _Colormap():
+class _Colormap(object):
"""Mixin class used to add some helper methods."""
def _get_data(self, ext):
"""
- Returns a string containing the colormap colors for saving.
+ Return a string containing the colormap colors for saving.
Parameters
----------
@@ -764,14 +693,14 @@ def _get_data(self, ext):
for line in data)
else:
raise ValueError(
- f'Invalid extension {ext!r}. Options are "hex", "txt", '
- f'"rgb", or "rgba".'
+ f'Invalid extension {ext!r}. Options are: '
+ "'hex', 'txt', 'rgb', 'rgba'."
)
return data
def _parse_path(self, path, dirname='.', ext=''):
"""
- Parses user input path.
+ Parse the user input path.
Parameters
----------
@@ -812,12 +741,14 @@ def __repr__(self):
f'..., {data[-1][1]:.3f}],\n')
return type(self).__name__ + '({\n' + string + '})'
- @docstring.dedent_interpd
- def __init__(self, *args, alpha=None, cyclic=False, **kwargs):
+ def __init__(self, *args, cyclic=False, alpha=None, **kwargs):
"""
Parameters
----------
- %(cyclic_doc)s
+ cyclic : bool, optional
+ Whether the colormap is cyclic. If ``True``, this changes how the
+ leftmost and rightmost color levels are selected, and `extend` can
+ only be ``'neither'`` (a warning will be issued otherwise).
alpha : float, optional
The opacity for the entire colormap. Overrides the input
segment data.
@@ -835,7 +766,8 @@ def _resample(self, N):
def concatenate(self, *args, ratios=1, name=None, **kwargs):
"""
- Appends arbitrary colormaps onto this one.
+ Return the concatenation of this colormap with the
+ input colormaps.
Parameters
----------
@@ -857,6 +789,11 @@ def concatenate(self, *args, ratios=1, name=None, **kwargs):
**kwargs
Passed to `LinearSegmentedColormap.updated`
or `PerceptuallyUniformColormap.updated`.
+
+ Returns
+ -------
+ `LinearSegmentedColormap`
+ The colormap.
"""
# Try making a simple copy
if not args:
@@ -957,54 +894,6 @@ def xyy(ix, funcs=funcs):
# Return copy
return self.updated(name=name, segmentdata=segmentdata, **kwargs)
- @staticmethod
- def from_list(name, colors, ratios=None, **kwargs):
- """
- Make a `LinearSegmentedColormap` from a list of colors.
-
- Parameters
- ----------
- name : str
- The colormap name.
- colors : list of color-spec or (float, color-spec) tuples, optional
- If list of RGB[A] tuples or color strings, the colormap transitions
- evenly from ``colors[0]`` at the left-hand side to
- ``colors[-1]`` at the right-hand side.
-
- If list of (float, color-spec) tuples, the float values are the
- coordinate of each transition and must range from 0 to 1. This
- can be used to divide the colormap range unevenly.
- ratios : list of float, optional
- Relative extents of each color transition. Must have length
- ``len(colors) - 1``. Larger numbers indicate a slower
- transition, smaller numbers indicate a faster transition.
-
- Other parameters
- ----------------
- **kwargs
- Passed to `LinearSegmentedColormap`.
-
- Returns
- -------
- `LinearSegmentedColormap`
- The colormap.
- """
- # Get coordinates
- coords = None
- if not np.iterable(colors):
- raise ValueError(f'Colors must be iterable, got colors={colors!r}')
- if (np.iterable(colors[0]) and len(colors[0]) == 2
- and not isinstance(colors[0], str)):
- coords, colors = zip(*colors)
- colors = [to_rgb(color, alpha=True) for color in colors]
-
- # Build segmentdata
- keys = ('red', 'green', 'blue', 'alpha')
- cdict = {}
- for key, values in zip(keys, zip(*colors)):
- cdict[key] = _make_segmentdata_array(values, coords, ratios)
- return LinearSegmentedColormap(name, cdict, **kwargs)
-
def punched(self, cut=None, name=None, **kwargs):
"""
Return a version of the colormap with the center "punched out".
@@ -1022,6 +911,11 @@ def punched(self, cut=None, name=None, **kwargs):
**kwargs
Passed to `LinearSegmentedColormap.updated`
or `PerceptuallyUniformColormap.updated`.
+
+ Returns
+ -------
+ `LinearSegmentedColormap`
+ The colormap.
"""
cut = _notNone(cut, 0)
if cut == 0:
@@ -1038,7 +932,7 @@ def punched(self, cut=None, name=None, **kwargs):
def reversed(self, name=None, **kwargs):
"""
- Returns a reversed copy of the colormap, as in
+ Return a reversed copy of the colormap, as in
`~matplotlib.colors.LinearSegmentedColormap`.
Parameters
@@ -1070,7 +964,7 @@ def func_r(x):
def save(self, path=None):
"""
- Saves the colormap data to a file.
+ Save the colormap data to a file.
Parameters
----------
@@ -1184,7 +1078,7 @@ def shifted(self, shift=None, name=None, **kwargs):
def truncated(self, left=None, right=None, name=None, **kwargs):
"""
- Returns a truncated version of the colormap.
+ Return a truncated version of the colormap.
Parameters
----------
@@ -1268,7 +1162,7 @@ def updated(
alpha=None, gamma=None, cyclic=None
):
"""
- Returns a new colormap, with relevant properties copied from this one
+ Return a new colormap, with relevant properties copied from this one
if they were not provided as keyword arguments.
Parameters
@@ -1296,6 +1190,80 @@ def updated(
cmap._rgba_over = self._rgba_over
return cmap
+ @staticmethod
+ def from_file(path, warn_on_failure=False):
+ """
+ Load colormap from a file.
+ Valid file extensions are described in the below table.
+
+ ===================== =============================================================================================================================================================================================================
+ Extension Description
+ ===================== =============================================================================================================================================================================================================
+ ``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).'ColorBlind10':
+ ``.xml`` XML files with ```` entries specifying ``x``, ``r``, ``g``, ``b``, and optionally, ``a`` values, where ``x`` is the colormap coordinate and the rest are the RGB and opacity (or "alpha") values.
+ ``.rgb`` 3-column table delimited by commas or consecutive spaces, each column indicating red, blue and green color values.
+ ``.xrgb`` As with ``.rgb``, but with 4 columns. The first column indicates the colormap coordinate.
+ ``.rgba``, ``.xrgba`` As with ``.rgb``, ``.xrgb``, but with a trailing opacity (or "alpha") column.
+ ===================== =============================================================================================================================================================================================================
+
+ Parameters
+ ----------
+ path : str
+ The file path.
+ warn_on_failure : bool, optional
+ If ``True``, issue a warning when loading fails rather than
+ raising an error.
+ """ # noqa
+ return _from_file(path, listed=False, warn_on_failure=warn_on_failure)
+
+ @staticmethod
+ def from_list(name, colors, ratios=None, **kwargs):
+ """
+ Make a `LinearSegmentedColormap` from a list of colors.
+
+ Parameters
+ ----------
+ name : str
+ The colormap name.
+ colors : list of color-spec or (float, color-spec) tuples, optional
+ If list of RGB[A] tuples or color strings, the colormap transitions
+ evenly from ``colors[0]`` at the left-hand side to
+ ``colors[-1]`` at the right-hand side.
+
+ If list of (float, color-spec) tuples, the float values are the
+ coordinate of each transition and must range from 0 to 1. This
+ can be used to divide the colormap range unevenly.
+ ratios : list of float, optional
+ Relative extents of each color transition. Must have length
+ ``len(colors) - 1``. Larger numbers indicate a slower
+ transition, smaller numbers indicate a faster transition.
+
+ Other parameters
+ ----------------
+ **kwargs
+ Passed to `LinearSegmentedColormap`.
+
+ Returns
+ -------
+ `LinearSegmentedColormap`
+ The colormap.
+ """
+ # Get coordinates
+ coords = None
+ if not np.iterable(colors):
+ raise ValueError(f'Colors must be iterable, got colors={colors!r}')
+ if (np.iterable(colors[0]) and len(colors[0]) == 2
+ and not isinstance(colors[0], str)):
+ coords, colors = zip(*colors)
+ colors = [to_rgb(color, alpha=True) for color in colors]
+
+ # Build segmentdata
+ keys = ('red', 'green', 'blue', 'alpha')
+ cdict = {}
+ for key, values in zip(keys, zip(*colors)):
+ cdict[key] = _make_segmentdata_array(values, coords, ratios)
+ return LinearSegmentedColormap(name, cdict, **kwargs)
+
class ListedColormap(mcolors.ListedColormap, _Colormap):
r"""
@@ -1327,7 +1295,7 @@ def __init__(self, *args, alpha=None, **kwargs):
def concatenate(self, *args, name=None, N=None, **kwargs):
"""
- Appends arbitrary colormaps onto this colormap.
+ Append arbitrary colormaps onto this colormap.
Parameters
----------
@@ -1356,7 +1324,7 @@ def concatenate(self, *args, name=None, N=None, **kwargs):
def save(self, path=None):
"""
- Saves the colormap data to a file.
+ Save the colormap data to a file.
Parameters
----------
@@ -1445,7 +1413,8 @@ def truncated(self, left=None, right=None, name=None):
def updated(self, colors=None, name=None, N=None, *, alpha=None):
"""
- Creates copy of the colormap.
+ Return a new colormap with relevant properties copied from this one
+ if they were not provided as keyword arguments.
Parameters
----------
@@ -1467,12 +1436,37 @@ def updated(self, colors=None, name=None, N=None, *, alpha=None):
cmap._rgba_over = self._rgba_over
return cmap
+ @staticmethod
+ def from_file(path, warn_on_failure=False):
+ """
+ Load color cycle from a file.
+ Valid file extensions are described in the below table.
+
+ ===================== =============================================================================================================================================================================================================
+ Extension Description
+ ===================== =============================================================================================================================================================================================================
+ ``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).'ColorBlind10':
+ ``.xml`` XML files with ```` entries specifying ``x``, ``r``, ``g``, ``b``, and optionally, ``a`` values, where ``x`` is the colormap coordinate and the rest are the RGB and opacity (or "alpha") values.
+ ``.rgb`` 3-column table delimited by commas or consecutive spaces, each column indicating red, blue and green color values.
+ ``.xrgb`` As with ``.rgb``, but with 4 columns. The first column indicates the colormap coordinate.
+ ``.rgba``, ``.xrgba`` As with ``.rgb``, ``.xrgb``, but with a trailing opacity (or "alpha") column.
+ ===================== =============================================================================================================================================================================================================
+
+ Parameters
+ ----------
+ path : str
+ The file path.
+ warn_on_failure : bool, optional
+ If ``True``, issue a warning when loading fails rather than
+ raising an error.
+ """ # noqa
+ return _from_file(path, listed=True, warn_on_failure=warn_on_failure)
+
class PerceptuallyUniformColormap(LinearSegmentedColormap, _Colormap):
"""Similar to `~matplotlib.colors.LinearSegmentedColormap`, but instead
of varying the RGB channels, we vary hue, saturation, and luminance in
either the HCL colorspace or the HSL or HPL scalings of HCL."""
- @docstring.dedent_interpd
def __init__(
self, name, segmentdata, N=None, space=None, clip=True,
gamma=None, gamma1=None, gamma2=None,
@@ -1506,7 +1500,22 @@ def __init__(
clip : bool, optional
Whether to "clip" impossible colors, i.e. truncate HCL colors
with RGB channels with values >1, or mask them out as gray.
- %(gamma_doc)s
+ cyclic : bool, optional
+ Whether the colormap is cyclic. If ``True``, this changes how the
+ leftmost and rightmost color levels are selected, and `extend` can
+ only be ``'neither'`` (a warning will be issued otherwise).
+ gamma : float, optional
+ Sets `gamma1` and `gamma2` to this identical value.
+ gamma1 : float, optional
+ If >1, makes low saturation colors more prominent. If <1,
+ makes high saturation colors more prominent. Similar to the
+ `HCLWizard `_ option.
+ See `make_mapping_array` for details.
+ gamma2 : float, optional
+ If >1, makes high luminance colors more prominent. If <1,
+ makes low luminance colors more prominent. Similar to the
+ `HCLWizard `_ option.
+ See `make_mapping_array` for details.
**kwargs
Passed to `LinearSegmentedColormap`.
@@ -1521,7 +1530,7 @@ def __init__(
... 'hue': [[0, 'red', 'red'], [1, 'blue', 'blue']],
... 'saturation': [[0, 100, 100], [1, 100, 100]],
... 'luminance': [[0, 100, 100], [1, 20, 20]],
- ... }
+ ... }
>>> cmap = plot.PerceptuallyUniformColormap(data)
"""
@@ -1554,8 +1563,8 @@ def __init__(
self._clip = clip
def _init(self):
- """As with `~matplotlib.colors.LinearSegmentedColormap`, but converts
- each value in the lookup table from 'input' to RGB."""
+ """As with `~matplotlib.colors.LinearSegmentedColormap`, but convert
+ each value in the lookup table from ``self._space`` to RGB."""
# First generate the lookup table
channels = ('hue', 'saturation', 'luminance')
# gamma weights *low chroma* and *high luminance*
@@ -1580,13 +1589,13 @@ def _init(self):
self._lut[:, :3] = _clip_colors(self._lut[:, :3], self._clip)
def _resample(self, N):
- """Returns a new colormap with *N* entries."""
+ """Return a new colormap with *N* entries."""
return self.updated(N=N)
@staticmethod
def from_color(name, color, fade=None, space='hsl', **kwargs):
"""
- Returns a monochromatic "sequential" colormap that blends from white
+ Return a monochromatic "sequential" colormap that blends from white
or near-white to the input color.
Parameters
@@ -1635,7 +1644,7 @@ def from_hsl(
ratios=None, **kwargs
):
"""
- Makes a `~PerceptuallyUniformColormap` by specifying the hue,
+ Make a `~PerceptuallyUniformColormap` by specifying the hue,
saturation, and luminance transitions individually.
Parameters
@@ -1738,14 +1747,24 @@ def from_list(name, colors, ratios=None, **kwargs):
cdict[key] = _make_segmentdata_array(values, coords, ratios)
return PerceptuallyUniformColormap(name, cdict, **kwargs)
- @docstring.dedent_interpd
def set_gamma(self, gamma=None, gamma1=None, gamma2=None):
"""
- Set new gamma value(s) and regenerates the colormap.
+ Modify the gamma value(s) and refresh the lookup table.
Parameters
----------
- %(gamma_doc)s
+ gamma : float, optional
+ Sets `gamma1` and `gamma2` to this identical value.
+ gamma1 : float, optional
+ If >1, makes low saturation colors more prominent. If <1,
+ makes high saturation colors more prominent. Similar to the
+ `HCLWizard `_ option.
+ See `make_mapping_array` for details.
+ gamma2 : float, optional
+ If >1, makes high luminance colors more prominent. If <1,
+ makes low luminance colors more prominent. Similar to the
+ `HCLWizard `_ option.
+ See `make_mapping_array` for details.
"""
gamma1 = _notNone(gamma1, gamma)
gamma2 = _notNone(gamma2, gamma)
@@ -1761,7 +1780,7 @@ def updated(
clip=None, gamma1=None, gamma2=None, space=None
):
"""
- Returns a new colormap, with relevant properties copied from this one
+ Return a new colormap with relevant properties copied from this one
if they were not provided as keyword arguments.
Parameters
@@ -1832,7 +1851,7 @@ def __getitem__(self, key):
"""Retrieve the colormap associated with the sanitized key name. The
key name is case insensitive. If it ends in ``'_r'``, the result of
``cmap.reversed()`` is returned for the colormap registered under
- the name ``cmap[:-2]``. If it ends in ``'_shifted'``, the result of
+ the name ``key[:-2]``. If it ends in ``'_shifted'``, the result of
``cmap.shifted(180)`` is returned for the colormap registered under
the name ``cmap[:-8]``. Reversed diverging colormaps can be requested
with their "reversed" name -- for example, ``'BuRd'`` is equivalent
@@ -2010,8 +2029,8 @@ def __getitem__(self, key):
def Colors(*args, **kwargs):
- """Identical to `Cycle`, but returns a list of colors instead of
- a `~cycler.Cycler` object."""
+ """Pass all arguments to `Cycle` and return the list of colors from
+ the cycler object."""
cycle = Cycle(*args, **kwargs)
return [dict_['color'] for dict_ in cycle]
@@ -2113,28 +2132,36 @@ def Colormap(
`~matplotlib.colors.ListedColormap` instance.
"""
# Initial stuff
+ # TODO: Play with using "qualitative" colormaps in realistic examples,
+ # how to make colormaps cyclic.
if not args:
raise ValueError(
f'Colormap() requires at least one positional argument.'
)
if listmode not in ('listed', 'linear', 'perceptual'):
raise ValueError(
+ f'Invalid listmode={listmode!r}. Options are: '
+ "'listed', 'linear', 'perceptual'."
f'Invalid listmode={listmode!r}. Options are '
'"listed", "linear", and "perceptual".'
)
tmp = '_no_name'
cmaps = []
for i, cmap in enumerate(args):
- # First load data
+ # Load registered colormaps and maps on file
# TODO: Document how 'listmode' also affects loaded files
if isinstance(cmap, str):
if '.' in cmap:
- if os.path.isfile(os.path.expanduser(cmap)):
- tmp, cmap = _load_cmap_cycle(
- cmap, cmap=(listmode != 'listed'))
- else:
+ isfile = os.path.isfile(os.path.expanduser(cmap))
+ if isfile:
+ if listmode == 'listed':
+ cmap = ListedColormap.from_file(cmap)
+ else:
+ cmap = LinearSegmentedColormap.from_file(cmap)
+ if not isfile or not cmap:
raise FileNotFoundError(
- f'Colormap or cycle file {cmap!r} not found.'
+ f'Colormap or cycle file {cmap!r} not found '
+ 'or failed to load.'
)
else:
try:
@@ -2179,11 +2206,13 @@ def Colormap(
msg = f'Invalid cmap, cycle, or color {cmap!r}.'
if isinstance(cmap, str):
msg += (
- '\nValid cmap and cycle names: '
- + ', '.join(sorted(mcm.cmap_d)) + '.'
- '\nValid color names: '
- + ', '.join(sorted(mcolors.colorConverter.colors))
- + '.')
+ f'\nValid cmap and cycle names: '
+ + ', '.join(map(repr, sorted(mcm.cmap_d))) + '.'
+ f'\nValid color names: '
+ + ', '.join(map(repr, sorted(
+ mcolors.colorConverter.colors))
+ ) + '.'
+ )
raise ValueError(msg)
cmap = PerceptuallyUniformColormap.from_color(tmp, color, fade)
if ireverse:
@@ -2225,7 +2254,7 @@ def Colormap(
def Cycle(
- *args, samples=None, name=None,
+ *args, N=None, name=None,
marker=None, alpha=None, dashes=None, linestyle=None, linewidth=None,
markersize=None, markeredgewidth=None,
markeredgecolor=None, markerfacecolor=None,
@@ -2259,11 +2288,11 @@ def Cycle(
is looked up and its ``colors`` attribute is used. See `cycles`.
* Otherwise, the argument is passed to `Colormap`, and colors
from the resulting `~matplotlib.colors.LinearSegmentedColormap`
- are used. See the `samples` argument.
+ are used. See the `N` argument.
- If the last positional argument is numeric, it is used for the
- `samples` keyword argument.
- samples : float or list of float, optional
+ If the last positional argument is numeric, it is used for the `N`
+ keyword argument.
+ N : float or list of float, optional
For `~matplotlib.colors.ListedColormap`\ s, this is the number of
colors to select. For example, ``Cycle('538', 4)`` returns the first 4
colors of the ``'538'`` color cycle.
@@ -2326,8 +2355,7 @@ def Cycle(
)
nprops = max(nprops, len(value))
props[key] = [*value] # ensure mutable list
- # If args is non-empty, means we want color cycle; otherwise is always
- # black
+ # If args is non-empty, means we want color cycle; otherwise is black
if not args:
props['color'] = ['k'] # ensures property cycler is non empty
if kwargs:
@@ -2351,24 +2379,23 @@ def Cycle(
# Collect samples
if args and isinstance(args[-1], Number):
# means we want to sample existing colormaps or cycles
- args, samples = args[:-1], args[-1]
+ args, N = args[:-1], args[-1]
kwargs.setdefault('fade', 90)
kwargs.setdefault('listmode', 'listed')
cmap = Colormap(*args, **kwargs) # the cmap object itself
if isinstance(cmap, ListedColormap):
- N = samples
- colors = cmap.colors[:N] # if samples is None, does nothing
+ colors = cmap.colors[:N] # if N is None, does nothing
else:
- samples = _notNone(samples, 10)
- if isinstance(samples, Integral):
- samples = np.linspace(0, 1, samples) # from edge to edge
- elif np.iterable(samples) and all(
- isinstance(item, Number) for item in samples):
- samples = np.array(samples)
+ N = _notNone(N, 10)
+ if isinstance(N, Integral):
+ x = np.linspace(0, 1, N) # from edge to edge
+ elif np.iterable(N) and all(
+ isinstance(item, Number) for item in N):
+ x = np.array(N)
else:
- raise ValueError(f'Invalid samples {samples!r}.')
- N = len(samples)
- colors = cmap(samples)
+ raise ValueError(f'Invalid samples {N!r}.')
+ N = len(x)
+ colors = cmap(x)
# Register and save the samples as a ListedColormap
name = name or '_no_name'
@@ -2380,13 +2407,17 @@ def Cycle(
# Add to property dict
nprops = max(nprops, len(colors))
- props['color'] = [tuple(color) if not isinstance(color, str) else color
- for color in cmap.colors] # save the tupled version!
+ props['color'] = [
+ tuple(color) if not isinstance(color, str) else color
+ for color in cmap.colors
+ ] # save the tupled version!
+
# Build cycler, make sure lengths are the same
for key, value in props.items():
if len(value) < nprops:
- value[:] = [value[i % len(value)] for i in range(
- nprops)] # make loop double back
+ value[:] = [
+ value[i % len(value)] for i in range(nprops)
+ ] # make loop double back
cycle = cycler.cycler(**props)
cycle.name = name
return cycle
@@ -2394,9 +2425,9 @@ def Cycle(
def Norm(norm, levels=None, **kwargs):
"""
- Returns an arbitrary `~matplotlib.colors.Normalize` instance, used to
- interpret the `norm` and `norm_kw` arguments when passed to any plotting
- method wrapped by `~proplot.wrappers.cmap_changer`.
+ Return an arbitrary `~matplotlib.colors.Normalize` instance.
+ Used to interpret the `norm` and `norm_kw` arguments when passed to any
+ plotting method wrapped by `~proplot.wrappers.cmap_changer`.
Parameters
----------
@@ -2437,7 +2468,7 @@ def Norm(norm, levels=None, **kwargs):
norm_out = normalizers.get(norm, None)
if norm_out is None:
raise ValueError(
- f'Unknown normalizer {norm!r}. Options are '
+ f'Unknown normalizer {norm!r}. Options are: '
+ ', '.join(map(repr, normalizers.keys())) + '.'
)
# Instantiate class
@@ -2477,8 +2508,8 @@ class BinNorm(mcolors.BoundaryNorm):
`extend` keyword argument. For `extend` equal to ``'neither'``,
the coordinates including out-of-bounds values are
``[0, 0, 0.25, 0.5, 0.75, 1, 1]`` -- out-of-bounds values have the same
- color as the nearest in-bounds values. For `extend` equal to
- ``'both'``, the bins are ``[0, 0.16, 0.33, 0.5, 0.66, 0.83, 1]`` --
+ color as the nearest in-bounds values. For `extend` equal to ``'both'``,
+ the bins are ``[0, 0.16, 0.33, 0.5, 0.66, 0.83, 1]`` --
out-of-bounds values are given distinct colors. This makes sure your
colorbar always shows the **full range of colors** in the colormap.
4. Whenever `BinNorm.__call__` is invoked, the input value normalized by
@@ -2600,7 +2631,7 @@ def __init__(
self.N = levels.size
def __call__(self, xq, clip=None):
- """Normalizes data values to the range 0-1."""
+ """Normalize data values to 0-1."""
# Follow example of LinearSegmentedNorm, but perform no interpolation,
# just use searchsorted to bin the data.
norm_clip = self._norm_clip
@@ -2613,21 +2644,21 @@ def __call__(self, xq, clip=None):
return ma.array(yq, mask=mask)
def inverse(self, yq):
- """Raises error. Inversion after discretization is impossible."""
- raise ValueError('BinNorm is not invertible.')
+ """Raise an error. Inversion after discretization is impossible."""
+ raise RuntimeError('BinNorm is not invertible.')
class LinearSegmentedNorm(mcolors.Normalize):
"""
This is the default normalizer paired with `BinNorm` whenever `levels`
are non-linearly spaced. The normalized value is linear with respect to
- its *average index* in the `levels` vector, allowing uniform color
- transitions across *arbitrarily spaced* monotonically increasing values.
+ its average index in the `levels` vector, allowing uniform color
+ transitions across arbitrarily spaced monotonically increasing values.
It accomplishes this following the example of the
`~matplotlib.colors.LinearSegmentedColormap` source code, by performing
- efficient, vectorized linear interpolation between the provided
- boundary levels.
+ efficient, vectorized linear interpolation between the provided boundary
+ levels.
Can be used by passing ``norm='segmented'`` or ``norm='segments'`` to any
command accepting ``cmap``. The default midpoint is zero.
@@ -2658,7 +2689,7 @@ def __init__(self, levels, vmin=None, vmax=None, **kwargs):
self._y = np.linspace(0, 1, len(levels))
def __call__(self, xq, clip=None):
- """Normalizes data values to the range 0-1. Inverse operation
+ """Normalize the data values to 0-1. Inverse
of `~LinearSegmentedNorm.inverse`."""
# Follow example of make_mapping_array for efficient, vectorized
# linear interpolation across multiple segments.
@@ -2714,8 +2745,7 @@ def __init__(self, midpoint=0, vmin=None, vmax=None, clip=None):
self._midpoint = midpoint
def __call__(self, xq, clip=None):
- """Normalizes data values to the range 0-1. Inverse operation of
- `~MidpointNorm.inverse`."""
+ """Normalize data values to 0-1. Inverse of `~MidpointNorm.inverse`."""
# Get middle point in 0-1 coords, and value
# Notes:
# * Look up these three values in case vmin/vmax changed; this is
@@ -2770,19 +2800,26 @@ def _get_data_paths(dirname):
]
-def _load_cmap_cycle(filename, cmap=False):
- """
- Helper function that reads generalized colormap and color cycle files.
- """
- N = rcParams['image.lut'] # query this when register function is called
+def _from_file(filename, listed=False, warn_on_failure=False):
+ """Read generalized colormap and color cycle files."""
filename = os.path.expanduser(filename)
if os.path.isdir(filename): # no warning
- return None, None
+ return
+
+ # Warn if loading failed during `register_cmaps` or `register_cycles`
+ # but raise error if user tries to load a file.
+ def _warn_or_raise(msg):
+ if warn_on_failure:
+ _warn_proplot(msg)
+ else:
+ raise RuntimeError(msg)
# Directly read segmentdata json file
# NOTE: This is special case! Immediately return name and cmap
+ N = rcParams['image.lut']
name, ext = os.path.splitext(os.path.basename(filename))
ext = ext[1:]
+ cmap = None
if ext == 'json':
with open(filename, 'r') as f:
data = json.load(f)
@@ -2790,11 +2827,11 @@ def _load_cmap_cycle(filename, cmap=False):
for key in ('cyclic', 'gamma', 'gamma1', 'gamma2', 'space'):
kw[key] = data.pop(key, None)
if 'red' in data:
- data = LinearSegmentedColormap(name, data, N=N)
+ cmap = LinearSegmentedColormap(name, data, N=N)
else:
- data = PerceptuallyUniformColormap(name, data, N=N, **kw)
+ cmap = PerceptuallyUniformColormap(name, data, N=N, **kw)
if name[-2:] == '_r':
- data = data.reversed(name[:-2])
+ cmap = cmap.reversed(name[:-2])
# Read .rgb, .rgba, .xrgb, and .xrgba files
elif ext in ('txt', 'rgb', 'xrgb', 'rgba', 'xrgba'):
@@ -2807,19 +2844,19 @@ def _load_cmap_cycle(filename, cmap=False):
try:
data = [[float(num) for num in line] for line in data]
except ValueError:
- _warn_proplot(
+ _warn_or_raise(
f'Failed to load {filename!r}. Expected a table of comma '
'or space-separated values.'
)
- return None, None
+ return
# Build x-coordinates and standardize shape
data = np.array(data)
if data.shape[1] != len(ext):
- _warn_proplot(
+ _warn_or_raise(
f'Failed to load {filename!r}. Got {data.shape[1]} columns, '
f'but expected {len(ext)}.'
)
- return None, None
+ return
if ext[0] != 'x': # i.e. no x-coordinates specified explicitly
x = np.linspace(0, 1, data.shape[0])
else:
@@ -2830,25 +2867,21 @@ def _load_cmap_cycle(filename, cmap=False):
# https://sciviscolor.org/matlab-matplotlib-pv44/
elif ext == 'xml':
try:
- xmldoc = etree.parse(filename)
+ doc = ElementTree.parse(filename)
except IOError:
- _warn_proplot(f'Failed to load {filename!r}.')
- return None, None
+ _warn_or_raise(
+ f'Failed to load {filename!r}.'
+ )
+ return
x, data = [], []
- for s in xmldoc.getroot().findall('.//Point'):
+ for s in doc.getroot().findall('.//Point'):
# Verify keys
if any(key not in s.attrib for key in 'xrgb'):
- _warn_proplot(
+ _warn_or_raise(
f'Failed to load {filename!r}. Missing an x, r, g, or b '
'specification inside one or more tags.'
)
- return None, None
- if 'o' in s.attrib and 'a' in s.attrib:
- _warn_proplot(
- f'Failed to load {filename!r}. Contains '
- 'ambiguous opacity key.'
- )
- return None, None
+ return
# Get data
color = []
for key in 'rgbao': # o for opacity
@@ -2858,12 +2891,13 @@ def _load_cmap_cycle(filename, cmap=False):
x.append(float(s.attrib['x']))
data.append(color)
# Convert to array
- if not all(len(data[0]) == len(color) for color in data):
- _warn_proplot(
- f'File {filename!r} has some points with alpha channel '
- 'specified, some without.'
+ if not all(len(data[0]) == len(color)
+ and len(color) in (3, 4) for color in data):
+ _warn_or_raise(
+ f'Failed to load {filename!r}. Unexpected number of channels '
+ 'or mixed channels across tags.'
)
- return None, None
+ return
# Read hex strings
elif ext == 'hex':
@@ -2871,27 +2905,23 @@ def _load_cmap_cycle(filename, cmap=False):
string = open(filename).read() # into single string
data = re.findall('#[0-9a-fA-F]{6}', string) # list of strings
if len(data) < 2:
- _warn_proplot(
+ _warn_or_raise(
f'Failed to load {filename!r}. Hex strings not found.'
)
- return None, None
+ return
# Convert to array
x = np.linspace(0, 1, len(data))
data = [to_rgb(color) for color in data]
else:
- _warn_proplot(
+ _warn_or_raise(
f'Colormap or cycle file {filename!r} has unknown extension.'
)
- return None, None
+ return
# Standardize and reverse if necessary to cmap
# TODO: Document the fact that filenames ending in _r return a reversed
# version of the colormap stored in that file.
- if isinstance(data, LinearSegmentedColormap):
- if not cmap:
- _warn_proplot(f'Failed to load {filename!r} as color cycle.')
- return None, None
- else:
+ if not cmap:
x, data = np.array(x), np.array(data)
# for some reason, some aren't in 0-1 range
x = (x - x.min()) / (x.max() - x.min())
@@ -2901,108 +2931,62 @@ def _load_cmap_cycle(filename, cmap=False):
name = name[:-2]
data = data[::-1, :]
x = 1 - x[::-1]
- if cmap:
+ if listed:
+ cmap = ListedColormap(data, name, N=len(data))
+ else:
data = [(x, color) for x, color in zip(x, data)]
- data = LinearSegmentedColormap.from_list(name, data, N=N)
+ cmap = LinearSegmentedColormap.from_list(name, data, N=N)
# Return colormap or data
- return name, data
+ return cmap
@_timer
def register_cmaps():
"""
- Add colormaps packaged with ProPlot or saved to the ``~/.proplot/cmaps``
- folder. This is called on import. Maps are registered according to their
- filenames -- for example, ``name.xyz`` will be registered as ``'name'``.
-
- This is called on import. Use `show_cmaps` to generate a table of the
- registered colormaps. Valid extensions are described in the below table.
-
- ===================== =============================================================================================================================================================================================================
- Extension Description
- ===================== =============================================================================================================================================================================================================
- ``.hex`` List of HEX strings in any format (comma-separated, separate lines, with double quotes... anything goes).
- ``.xml`` XML files with ```` entries specifying ``x``, ``r``, ``g``, ``b``, and optionally, ``a`` values, where ``x`` is the colormap coordinate and the rest are the RGB and opacity (or "alpha") values.
- ``.rgb`` 3-column table delimited by commas or consecutive spaces, each column indicating red, blue and green color values.
- ``.xrgb`` As with ``.rgb``, but with 4 columns. The first column indicates the colormap coordinate.
- ``.rgba``, ``.xrgba`` As with ``.rgb``, ``.xrgb``, but with a trailing opacity (or "alpha") column.
- ===================== =============================================================================================================================================================================================================
- """ # noqa
- # Turn original matplotlib maps from ListedColormaps
- # to LinearSegmentedColormaps. It makes zero sense to me that they are
- # stored as ListedColormaps.
- for name in CMAPS_CATEGORIES['Matplotlib originals']:
- if name == 'twilight_shifted': # CmapDict does this automatically
- mcm.cmap_d.pop(name, None)
- else:
- cmap = mcm.cmap_d.get(name, None)
- if isinstance(cmap, ListedColormap):
- mcm.cmap_d.pop(name, None)
- mcm.cmap_d[name] = LinearSegmentedColormap.from_list(
- name, cmap.colors, cyclic=(name == 'twilight'))
-
- # Misc tasks
- # to be consistent with registered color names (also 'Murica)
- cmap = mcm.cmap_d.pop('Greys', None)
- if cmap is not None:
- mcm.cmap_d['Grays'] = cmap
- for name in ('Spectral',):
- mcm.cmap_d[name] = mcm.cmap_d[name].reversed(
- name=name) # make spectral go from 'cold' to 'hot'
-
- # Remove gross cmaps (strong-arm user into using the better ones)
- for name in CMAPS_DELETE:
- mcm.cmap_d.pop(name, None)
-
- # Add colormaps from ProPlot and user directories
- for path in _get_data_paths('cmaps'):
+ Register colormaps packaged with ProPlot or saved to the
+ ``~/.proplot/cmaps`` folder. This is called on import. Maps are registered
+ according to their filenames -- for example, ``name.xyz`` will be
+ registered as ``'name'``.
+
+ For a table of valid extensions, see `LinearSegmentedColormap.from_file`.
+ To visualize the registered colormaps, use `show_cmaps`.
+ """
+ for i, path in enumerate(_get_data_paths('cmaps')):
for filename in sorted(glob.glob(os.path.join(path, '*'))):
- name, cmap = _load_cmap_cycle(filename, cmap=True)
- if name is None:
+ cmap = LinearSegmentedColormap.from_file(
+ filename, warn_on_failure=True
+ )
+ if not cmap:
continue
- mcm.cmap_d[name] = cmap
- # Add cyclic attribute
- for name, cmap in mcm.cmap_d.items():
- # add hidden attribute used by BinNorm
- cmap._cyclic = (name.lower() in (
- 'twilight', 'twilight_shifted', 'phase', 'graycycle'))
+ if i == 0 and cmap.name.lower() in ('phase', 'graycycle'):
+ cmap._cyclic = True
+ mcm.cmap_d[cmap.name] = cmap
@_timer
def register_cycles():
"""
- Add color cycles packaged with ProPlot or saved to the
+ Register color cycles packaged with ProPlot or saved to the
``~/.proplot/cycles`` folder. This is called on import. Cycles are
registered according to their filenames -- for example, ``name.hex`` will
be registered under the name ``'name'`` as a
`~matplotlib.colors.ListedColormap` map (see `Cycle` for details).
- This is called on import. Use `show_cycles` to generate a table of the
- registered cycles. For valid file formats, see `register_cmaps`.
+ For a table of valid extensions, see `ListedColormap.from_file`.
+ To visualize the registered colormaps, use `show_cmaps`.
"""
- # Remove gross cycles, change the names of some others
- for name in CYCLES_DELETE:
- mcm.cmap_d.pop(name, None)
- for (name1, name2) in CYCLES_RENAME:
- cycle = mcm.cmap_d.pop(name1, None)
- if cycle:
- mcm.cmap_d[name2] = cycle
-
- # Read cycles from directories
- cycles_load = {}
for path in _get_data_paths('cycles'):
for filename in sorted(glob.glob(os.path.join(path, '*'))):
- name, data = _load_cmap_cycle(filename, cmap=False)
- if name is None:
+ cmap = ListedColormap.from_file(
+ filename, warn_on_failure=True
+ )
+ if not cmap:
continue
- cycles_load[name] = data
-
- # Register cycles as ListedColormaps
- for name, colors in {**CYCLES_PRESET, **cycles_load}.items():
- cmap = ListedColormap(colors, name=name)
- cmap.colors = [to_rgb(color, alpha=True) for color in cmap.colors]
- mcm.cmap_d[name] = cmap
+ if isinstance(cmap, LinearSegmentedColormap):
+ cmap = ListedColormap(colors(cmap), name=cmap.name)
+ mcm.cmap_d[cmap.name] = cmap
+ cycles.append(cmap.name)
@_timer
@@ -3024,7 +3008,7 @@ def register_colors(nmax=np.inf):
colors.clear()
base = {}
base.update(mcolors.BASE_COLORS)
- base.update(COLORS_BASE)
+ base.update(COLORS_BASE) # full names
mcolors.colorConverter.colors.clear() # clean out!
mcolors.colorConverter.cache.clear() # clean out!
for name, dict_ in (('base', base), ('css', mcolors.CSS4_COLORS)):
@@ -3040,15 +3024,18 @@ def register_colors(nmax=np.inf):
if i == 0:
paths = [ # be explicit because categories matter!
os.path.join(path, base)
- for base in ('xkcd.txt', 'crayola.txt', 'open.txt')
+ for base in ('xkcd.txt', 'crayola.txt', 'opencolor.txt')
]
else:
paths = sorted(glob.glob(os.path.join(path, '*.txt')))
for file in paths:
cat, _ = os.path.splitext(os.path.basename(file))
with open(file, 'r') as f:
- pairs = [tuple(item.strip() for item in line.split(':'))
- for line in f.readlines() if line.strip()]
+ pairs = [
+ tuple(item.strip() for item in line.split(':'))
+ for line in f.readlines()
+ if line.strip() and line.strip()[0] != '#'
+ ]
if not all(len(pair) == 2 for pair in pairs):
raise RuntimeError(
f'Invalid color names file {file!r}. '
@@ -3056,7 +3043,7 @@ def register_colors(nmax=np.inf):
)
# Categories for which we add *all* colors
- if cat == 'open' or i == 1:
+ if cat == 'opencolor' or i == 1:
dict_ = {name: color for name, color in pairs}
mcolors.colorConverter.colors.update(dict_)
colors[cat] = sorted(dict_)
@@ -3096,9 +3083,9 @@ def register_colors(nmax=np.inf):
@_timer
def register_fonts():
- """Adds fonts packaged with ProPlot or saved to the ``~/.proplot/fonts``
- folder. Also deletes the font cache, which may cause delays.
- Detects ``.ttf`` and ``.otf`` files -- see `this link \
+ """Add fonts packaged with ProPlot or saved to the ``~/.proplot/fonts``
+ folder, if they are not already added. Detects ``.ttf`` and ``.otf`` files
+ -- see `this link \
`__
for a guide on converting various other font file types to ``.ttf`` and
``.otf`` for use with matplotlib."""
@@ -3165,9 +3152,53 @@ def register_fonts():
fonts[:] = [*fonts_proplot, *fonts_system]
+def _draw_bars(cmapdict, length=4.0, width=0.2):
+ """
+ Draw colorbars for "colormaps" and "color cycles". This is called by
+ `show_cycles` and `show_cmaps`.
+ """
+ # Figure
+ from . import subplots
+ naxs = len(cmapdict) + sum(map(len, cmapdict.values()))
+ fig, axs = subplots(
+ nrows=naxs, axwidth=length, axheight=width,
+ share=0, hspace=0.03,
+ )
+ iax = -1
+ nheads = nbars = 0 # for deciding which axes to plot in
+ a = np.linspace(0, 1, 257).reshape(1, -1)
+ a = np.vstack((a, a))
+ for cat, names in cmapdict.items():
+ if not names:
+ continue
+ nheads += 1
+ for imap, name in enumerate(names):
+ iax += 1
+ if imap + nheads + nbars > naxs:
+ break
+ ax = axs[iax]
+ if imap == 0: # allocate this axes for title
+ iax += 1
+ ax.set_visible(False)
+ ax = axs[iax]
+ cmap = mcm.cmap_d[name]
+ ax.imshow(
+ a, cmap=name, origin='lower', aspect='auto',
+ levels=cmap.N
+ )
+ ax.format(
+ ylabel=name,
+ ylabel_kw={'rotation': 0, 'ha': 'right', 'va': 'center'},
+ xticks='none', yticks='none', # no ticks
+ xloc='neither', yloc='neither', # no spines
+ title=(cat if imap == 0 else None)
+ )
+ nbars += len(names)
+
+
def show_channels(
- *args, N=100, rgb=True, saturation=True,
- minhue=0, maxsat=1000, axwidth=None, width=100
+ *args, N=100, rgb=True, saturation=True, minhue=0,
+ maxsat=500, width=100, axwidth=1.7
):
"""
Show how arbitrary colormap(s) vary with respect to the hue, chroma,
@@ -3200,12 +3231,12 @@ def show_channels(
Returns
-------
`~proplot.subplots.Figure`
- The figure instance.
- """ # noqa
+ The figure.
+ """
# Figure and plot
from . import subplots
if not args:
- args = (rcParams['image.cmap'],)
+ raise ValueError(f'At least one positional argument required.')
array = [[1, 1, 2, 2, 3, 3]]
labels = ('Hue', 'Chroma', 'Luminance')
if saturation:
@@ -3216,7 +3247,7 @@ def show_channels(
labels += ('Red', 'Green', 'Blue')
fig, axs = subplots(
array=array, span=False, share=1,
- aspect=1, axwidth=axwidth, axpad='1em',
+ axwidth=axwidth, axpad='1em',
)
# Iterate through colormaps
mc, ms, mp = 0, 0, 0
@@ -3311,7 +3342,7 @@ def show_colorspaces(luminance=None, saturation=None, hue=None, axwidth=2):
Returns
-------
`~proplot.subplots.Figure`
- The figure instance.
+ The figure.
"""
# Get colorspace properties
hues = np.linspace(0, 360, 361)
@@ -3370,14 +3401,14 @@ def show_colorspaces(luminance=None, saturation=None, hue=None, axwidth=2):
return fig
-def show_colors(nbreak=17, minsat=20):
+def show_colors(nhues=17, minsat=20):
"""
- Visualize the registered color names in two figures. Adapted from
- `this example `_.
+ Generate tables of the registered color names. Adapted from
+ `this example `__.
Parameters
----------
- nbreak : int, optional
+ nhues : int, optional
The number of breaks between hues for grouping "like colors" in the
color table.
minsat : float, optional
@@ -3387,16 +3418,16 @@ def show_colors(nbreak=17, minsat=20):
Returns
-------
figs : list of `~proplot.subplots.Figure`
- The figure instances.
+ The figure.
"""
# Test used to "categories" colors
- breakpoints = np.linspace(0, 360, nbreak)
+ breakpoints = np.linspace(0, 360, nhues)
def _color_filter(i, hcl): # noqa: E306
gray = hcl[1] <= minsat
if i == 0:
return gray
color = breakpoints[i - 1] <= hcl[0] < breakpoints[i]
- if i == nbreak - 1:
+ if i == nhues - 1:
color = color or color == breakpoints[i] # endpoint inclusive
return not gray and color
@@ -3404,8 +3435,8 @@ def _color_filter(i, hcl): # noqa: E306
figs = []
from . import subplots
for cats in (
- ('open',),
- tuple(name for name in colors if name not in ('css', 'open'))
+ ('opencolor',),
+ tuple(name for name in colors if name not in ('css', 'opencolor'))
):
# Dictionary of colors for that category
data = {}
@@ -3415,7 +3446,7 @@ def _color_filter(i, hcl): # noqa: E306
# Group colors together by discrete range of hue, then sort by value
# For opencolors this is not necessary
- if cats == ('open',):
+ if cats == ('opencolor',):
wscale = 0.5
swatch = 1.5
nrows, ncols = 10, len(COLORS_OPEN) # rows and columns
@@ -3426,8 +3457,6 @@ def _color_filter(i, hcl): # noqa: E306
nrows = nrows * 2
ncols = (ncols + 1) // 2
names.resize((ncols, nrows))
- names = names.tolist()
- # names = names.reshape((ncols, nrows)).tolist()
# Get colors in perceptally uniform space, then group based on hue
# thresholds
@@ -3444,7 +3473,7 @@ def _color_filter(i, hcl): # noqa: E306
[pair for pair in hclpairs if _color_filter(i, pair[1])],
key=lambda x: x[1][2]
)
- for i in range(nbreak)
+ for i in range(nhues)
]
names = np.array([
name for ipairs in hclpairs for name, _ in ipairs
@@ -3485,158 +3514,94 @@ def _color_filter(i, hcl): # noqa: E306
return figs
-def show_cmaps(*args, N=256, length=4.0, width=0.2, unknown='User'):
+def show_cmaps(*args, N=None, unknown='User', **kwargs):
"""
- Visualize all registered colormaps, or the list of colormap names if
- positional arguments are passed. Adapted from `this example \
+ Generate a table of the registered colormaps or the input colormaps.
+ Adapted from `this example \
`__.
Parameters
----------
*args : colormap-spec, optional
- Positional arguments are colormap names or objects. Default is
- all of the registered colormaps.
+ Colormap names or objects.
N : int, optional
- The number of levels in each colorbar.
+ The number of levels in each colorbar. Default is
+ :rc:`image.lut`.
+ unknown : str, optional
+ Category name for colormaps that are unknown to ProPlot. The
+ default is ``'User'``.
length : float or str, optional
- The length of each colorbar. Units are interpreted by
+ The length of the colorbars. Units are interpreted by
`~proplot.utils.units`.
width : float or str, optional
- The width of each colorbar. Units are interpreted by
+ The width of the colorbars. Units are interpreted by
`~proplot.utils.units`.
- unknown : str, optional
- Category name for colormaps that are unknown to ProPlot. The
- default is ``'User'``.
Returns
-------
`~proplot.subplots.Figure`
- The figure instance.
+ The figure.
"""
# Have colormaps separated into categories
+ N = _notNone(N, rcParams['image.lut'])
if args:
- imaps = [Colormap(cmap, N=N).name for cmap in args]
+ names = [Colormap(cmap, N=N).name for cmap in args]
else:
- imaps = [
- name for name in mcm.cmap_d.keys() if name not in ('vega', 'greys')
- and name[0] != '_'
- and isinstance(mcm.cmap_d[name], LinearSegmentedColormap)
+ names = [
+ name for name in mcm.cmap_d.keys() if
+ isinstance(mcm.cmap_d[name], LinearSegmentedColormap)
]
# Get dictionary of registered colormaps and their categories
- imaps = [name.lower() for name in imaps]
- cats = {cat: names for cat, names in CMAPS_CATEGORIES.items()}
- cats_plot = {cat: [name for name in names if name.lower() in imaps]
- for cat, names in cats.items()}
- # Distinguish known from unknown (i.e. user) maps, add as a new category
- imaps_known = [name.lower() for cat, names in cats.items()
- for name in names if name.lower() in imaps]
- imaps_unknown = [name for name in imaps if name not in imaps_known]
- # Remove categories with no known maps and put user at start
- cats_plot = {unknown: imaps_unknown, **cats_plot}
- cats_plot = {cat: maps for cat, maps in cats_plot.items() if maps}
+ cmapdict = {}
+ names_all = list(map(str.lower, names))
+ names_known = sum(map(list, CMAPS_TABLE.values()), [])
+ cmapdict[unknown] = [name for name in names if name not in names_known]
+ for cat, names in CMAPS_TABLE.items():
+ cmapdict[cat] = [name for name in names if name.lower() in names_all]
- # Figure
- from . import subplots
- naxs = len(imaps_known) + len(imaps_unknown) + len(cats_plot)
- fig, axs = subplots(
- nrows=naxs, axwidth=length, axheight=width,
- share=0, hspace=0.03,
- )
- iax = -1
- ntitles = nplots = 0 # for deciding which axes to plot in
- a = np.linspace(0, 1, 257).reshape(1, -1)
- a = np.vstack((a, a))
- for cat, names in cats_plot.items():
- # Space for title
- if not names:
- continue
- ntitles += 1
- for imap, name in enumerate(names):
- # Draw colorbar
- iax += 1
- if imap + ntitles + nplots > naxs:
- break
- ax = axs[iax]
- if imap == 0: # allocate this axes for title
- iax += 1
- ax.set_visible(False)
- ax = axs[iax]
- if name not in mcm.cmap_d or name.lower(
- ) not in imaps: # i.e. the expected builtin colormap is missing
- ax.set_visible(False) # empty space
- continue
- ax.imshow(a, cmap=name, origin='lower', aspect='auto', levels=N)
- ax.format(ylabel=name,
- ylabel_kw={'rotation': 0, 'ha': 'right', 'va': 'center'},
- xticks='none', yticks='none', # no ticks
- xloc='neither', yloc='neither', # no spines
- title=(cat if imap == 0 else None))
- # Space for plots
- nplots += len(names)
- return fig
+ # Return figure of colorbars
+ return _draw_bars(cmapdict, **kwargs)
-def show_cycles(*args, axwidth=1.5):
+def show_cycles(*args, **kwargs):
"""
- Visualize all registered color cycles, or the list of cycle names if
- positional arguments are passed.
+ Generate a table of registered color cycles or the input color cycles.
Parameters
----------
*args : colormap-spec, optional
- Positional arguments are cycle names or objects. Default is
- all of the registered colormaps.
- axwidth : str or float, optional
- Average width of each subplot. Units are interpreted by
+ Cycle names or objects.
+ length : float or str, optional
+ The length of the colorbars. Units are interpreted by
+ `~proplot.utils.units`.
+ width : float or str, optional
+ The width of the colorbars. Units are interpreted by
`~proplot.utils.units`.
Returns
-------
`~proplot.subplots.Figure`
- The figure instance.
+ The figure.
"""
# Get the list of cycles
if args:
- icycles = {
- getattr(cycle, 'name', '_no_name'): Colors(cycle)
- for cycle in args}
+ names = [cmap.name for cmap in args]
else:
- # use global cycles variable
- icycles = {key: mcm.cmap_d[key].colors for key in cycles}
- nrows = len(icycles) // 3 + len(icycles) % 3
+ names = [
+ name for name in mcm.cmap_d.keys() if
+ isinstance(mcm.cmap_d[name], ListedColormap)
+ ]
- # Create plot
- from . import subplots
- state = np.random.RandomState(51423)
- fig, axs = subplots(
- ncols=3, nrows=nrows, aspect=1, axwidth=axwidth,
- sharey=False, sharex=False, axpad=0.05
- )
- for i, (ax, (key, cycle)) in enumerate(zip(axs, icycles.items())):
- key = key.lower()
- array = state.rand(20, len(cycle)) - 0.5
- array = array[:, :1] + array.cumsum(axis=0) + np.arange(0, len(cycle))
- for j, color in enumerate(cycle):
- l, = ax.plot(array[:, j], lw=5, ls='-', color=color)
- # make first lines have big zorder
- l.set_zorder(10 + len(cycle) - j)
- title = f'{key}: {len(cycle)} colors'
- ax.set_title(title)
- ax.grid(True)
- for axis in 'xy':
- ax.tick_params(axis=axis,
- which='both', labelbottom=False, labelleft=False,
- bottom=False, top=False, left=False, right=False)
- if axs[i + 1:]:
- axs[i + 1:].set_visible(False)
- return fig
+ # Return figure of colorbars
+ cmapdict = {'Color cycles': names}
+ return _draw_bars(cmapdict, **kwargs)
def show_fonts(*args, size=12, text=None):
"""
- Visualize the available sans-serif fonts. If a glyph is unavailable,
- it is replaced by the "¤" dummy character.
+ Generate a table of fonts. If a glyph for a particular font is unavailable,
+ it is replaced with the "¤" dummy character.
Parameters
----------
@@ -3691,6 +3656,32 @@ def show_fonts(*args, size=12, text=None):
return f
+# Apply custom changes
+mcm.cmap_d['Grays'] = mcm.cmap_d.pop('Greys', None) # 'Murica (and consistency with registered colors) # noqa
+mcm.cmap_d['Spectral'] = mcm.cmap_d['Spectral'].reversed(
+ name='Spectral') # make spectral go from 'cold' to 'hot'
+for _name in CMAPS_TABLE['Matplotlib originals']: # initialize as empty lists
+ if _name == 'twilight_shifted':
+ mcm.cmap_d.pop(_name, None)
+ else:
+ _cmap = mcm.cmap_d.get(_name, None)
+ if _cmap and isinstance(_cmap, mcolors.ListedColormap):
+ mcm.cmap_d.pop(_name, None) # removes the map from cycles list!
+ mcm.cmap_d[_name] = LinearSegmentedColormap.from_list(
+ _name, _cmap.colors, cyclic=('twilight' in _name))
+for _cat in ('MATLAB', 'GNUplot', 'GIST', 'Other'):
+ for _name in CMAPS_TABLE[_cat]:
+ mcm.cmap_d.pop(_name, None)
+
+# Initialize customization folders and files
+_rc_folder = os.path.join(os.path.expanduser('~'), '.proplot')
+if not os.path.isdir(_rc_folder):
+ os.mkdir(_rc_folder)
+for _rc_sub in ('cmaps', 'cycles', 'colors', 'fonts'):
+ _rc_sub = os.path.join(_rc_folder, _rc_sub)
+ if not os.path.isdir(_rc_sub):
+ os.mkdir(_rc_sub)
+
#: List of registered colormap names.
cmaps = [] # track *downloaded* colormaps
diff --git a/proplot/wrappers.py b/proplot/wrappers.py
index 2cf72684a..505da7091 100644
--- a/proplot/wrappers.py
+++ b/proplot/wrappers.py
@@ -1512,7 +1512,7 @@ def cycle_changer(
# Get the new cycler
cycle_args = () if cycle is None else (cycle,)
if not is1d and y.shape[1] > 1: # default samples count
- cycle_kw.setdefault('samples', y.shape[1])
+ cycle_kw.setdefault('N', y.shape[1])
cycle = styletools.Cycle(*cycle_args, **cycle_kw)
# Get the original property cycle
# NOTE: Matplotlib saves itertools.cycle(cycler), not the original
@@ -1525,6 +1525,8 @@ def cycle_changer(
for key, value in prop.items():
if key not in by_key:
by_key[key] = {*()} # set
+ if isinstance(value, (list, np.ndarray)):
+ value = tuple(value)
by_key[key].add(value)
# Reset property cycler if it differs
reset = ({*by_key} != {*cycle.by_key()}) # reset if keys are different
diff --git a/requirements.txt b/requirements.txt
index c6290c8b1..eb5f0e2e9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,4 @@
# Hard requirements
-# TODO: Remove lxml after #50 merged
# TODO: Remove pyyaml and copy matplotlib's manual parsing approach?
matplotlib
pyyaml
-lxml