"""
Palette helpers ported from ggpubr.
The user-facing entry point is :func:`get_palette`. The
``color_palette`` / ``fill_palette`` and ``gradient_color`` /
``gradient_fill`` helpers return ready-to-add plotnine scales
that draw their colours from the named palettes.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from ._ggsci import BREWER_PALETTES, GGSCI_PALETTES
from ._ggthemes import GGTHEMES_PALETTES, ggthemes_palette
if TYPE_CHECKING:
from typing import Sequence
__all__ = (
"get_palette",
"color_palette",
"fill_palette",
"set_palette",
"change_palette",
"gradient_color",
"gradient_fill",
"show_point_shapes",
"show_line_types",
)
_DEFAULT_GREY_GRADIENT = ("#FFFFFF", "#000000")
_DEFAULT_VIRIDIS = (
"#440154",
"#482878",
"#3E4A89",
"#31688E",
"#26828E",
"#1F9E89",
"#35B779",
"#6DCD59",
"#B4DE2C",
"#FDE725",
)
def _resolve_palette(
palette: "str | Sequence[str]",
) -> tuple[str, ...]:
"""Return a tuple of hex colours for the given name."""
if not isinstance(palette, str):
return tuple(palette)
name = palette
if name in GGSCI_PALETTES:
return GGSCI_PALETTES[name]
if name in BREWER_PALETTES:
return BREWER_PALETTES[name]
if name in GGTHEMES_PALETTES:
return tuple(ggthemes_palette(name))
if name.lower() in {"grey", "gray"}:
return _DEFAULT_GREY_GRADIENT
if name.lower() == "viridis":
return _DEFAULT_VIRIDIS
# Try matplotlib qualitative / sequential cmaps
try:
import matplotlib as mpl
cmap = mpl.colormaps[name]
n = getattr(cmap, "N", 256)
return tuple(
mpl.colors.to_hex(cmap(i / max(1, n - 1)))
for i in range(min(n, 12))
)
except (KeyError, ValueError) as exc:
raise ValueError(
f"Unknown palette {palette!r}. Known names: "
f"ggsci ({sorted(GGSCI_PALETTES)}), "
f"brewer ({sorted(BREWER_PALETTES)}), "
f"or any matplotlib colormap."
) from exc
[docs]
def get_palette(
palette: "str | Sequence[str]" = "default",
k: int = 8,
) -> list[str]:
"""
Return ``k`` colours sampled from a named palette.
Parameters
----------
palette : str or sequence
Either one of the names in
:mod:`plotnine_extra.palettes._ggsci`, the strings
``"default"`` / ``"grey"`` / ``"viridis"``, a matplotlib
colormap name, or a custom sequence of colours.
k : int, default 8
Number of colours to return. If ``k`` is larger than the
underlying palette, colours are linearly interpolated.
Returns
-------
list of str
Hex colour strings of length ``k``.
"""
if palette == "default":
cols = GGSCI_PALETTES["npg"]
else:
cols = _resolve_palette(palette)
if len(cols) >= k:
return list(cols[:k])
# Interpolate to reach k colours
return _interpolate_colors(cols, k)
def _interpolate_colors(cols: "Sequence[str]", k: int) -> list[str]:
import matplotlib as mpl
rgb = np.array([mpl.colors.to_rgb(c) for c in cols])
xs_old = np.linspace(0, 1, len(cols))
xs_new = np.linspace(0, 1, k)
out = np.column_stack(
[np.interp(xs_new, xs_old, rgb[:, i]) for i in range(3)]
)
return [mpl.colors.to_hex(row) for row in out]
[docs]
def color_palette(
palette: "str | Sequence[str]" = "default",
**kwargs,
):
"""
Return a discrete plotnine ``scale_color_manual`` using
the named palette.
"""
from plotnine import scale_color_manual
cols = get_palette(palette, k=kwargs.pop("k", 12))
return scale_color_manual(values=cols, **kwargs)
[docs]
def fill_palette(
palette: "str | Sequence[str]" = "default",
**kwargs,
):
"""
Return a discrete plotnine ``scale_fill_manual`` using
the named palette.
"""
from plotnine import scale_fill_manual
cols = get_palette(palette, k=kwargs.pop("k", 12))
return scale_fill_manual(values=cols, **kwargs)
[docs]
def gradient_color(
palette: "str | Sequence[str]" = "viridis",
**kwargs,
):
"""
Return a continuous plotnine ``scale_color_gradientn`` using
the named palette.
"""
from plotnine import scale_color_gradientn
cols = list(_resolve_palette(palette))
return scale_color_gradientn(colors=cols, **kwargs)
[docs]
def gradient_fill(
palette: "str | Sequence[str]" = "viridis",
**kwargs,
):
"""
Return a continuous plotnine ``scale_fill_gradientn`` using
the named palette.
"""
from plotnine import scale_fill_gradientn
cols = list(_resolve_palette(palette))
return scale_fill_gradientn(colors=cols, **kwargs)
[docs]
def set_palette(plot, palette: "str | Sequence[str]"):
"""
Add ``color_palette`` and ``fill_palette`` layers to ``plot``.
"""
return plot + color_palette(palette) + fill_palette(palette)
[docs]
def change_palette(plot, palette: "str | Sequence[str]"):
"""Alias of :func:`set_palette`."""
return set_palette(plot, palette)
[docs]
def show_point_shapes():
"""
Return a small ``ggplot`` showing a set of matplotlib
point shapes (mirrors ``ggpubr::show_point_shapes``).
Uses matplotlib's string marker codes rather than R's
integer shape codes. Integer shapes are not valid
matplotlib markers and would crash the renderer.
"""
import pandas as pd
from plotnine import (
aes,
element_blank,
geom_point,
geom_text,
ggplot,
labs,
scale_shape_identity,
theme_minimal,
)
# Common matplotlib markers with a short descriptive label.
shapes = [
("o", "circle"),
("s", "square"),
("^", "triangle_up"),
("v", "triangle_down"),
("D", "diamond"),
("P", "plus_filled"),
("X", "x_filled"),
("*", "star"),
("p", "pentagon"),
("h", "hexagon"),
("<", "triangle_left"),
(">", "triangle_right"),
]
n = len(shapes)
rows = 4
df = pd.DataFrame(
{
"shape": [s for s, _ in shapes],
"x": [i % rows for i in range(n)],
"y": [-(i // rows) for i in range(n)],
"label": [f"{s!r}\n{name}" for s, name in shapes],
}
)
return (
ggplot(df, aes("x", "y"))
+ geom_point(aes(shape="shape"), size=6, fill="#7F7F7F")
+ geom_text(aes(label="label"), nudge_y=-0.4, size=8)
+ scale_shape_identity()
+ labs(title="Point shapes", x=None, y=None)
+ theme_minimal()
+ _no_axes_theme(element_blank)
)
[docs]
def show_line_types():
"""
Return a small ``ggplot`` showing the available matplotlib
line types (mirrors ``ggpubr::show_line_types``).
"""
import pandas as pd
from plotnine import (
aes,
element_blank,
geom_segment,
geom_text,
ggplot,
labs,
scale_linetype_identity,
theme_minimal,
)
types = ("solid", "dashed", "dotted", "dashdot")
df = pd.DataFrame(
{
"linetype": types,
"x": [0] * len(types),
"xend": [1] * len(types),
"y": list(range(len(types))),
"yend": list(range(len(types))),
}
)
return (
ggplot(df)
+ geom_segment(
aes(
x="x",
xend="xend",
y="y",
yend="yend",
linetype="linetype",
),
size=1,
)
+ geom_text(
aes(x="xend", y="y", label="linetype"),
nudge_x=0.05,
ha="left",
)
+ scale_linetype_identity()
+ labs(title="Line types", x=None, y=None)
+ theme_minimal()
+ _no_axes_theme(element_blank)
)
def _no_axes_theme(element_blank):
from plotnine import theme
return theme(
axis_text=element_blank(),
axis_ticks=element_blank(),
panel_grid=element_blank(),
)