"""
Definition of CBS rbg colors. Based on the color rgb definitions from the cbs LaTeX template
"""
import logging
import math
from pathlib import Path
import numpy as np
import matplotlib as mpl
import matplotlib.patches as mpatches
from matplotlib.path import Path as mPath
from matplotlib.image import imread
import matplotlib.transforms as trn
from PIL import Image
from matplotlib import colors as mcolors
logger = logging.getLogger(__name__)
CBS_COLORS_RBG = {
"corporateblauw": (39, 29, 108),
"corporatelichtblauw": (0, 161, 205),
"donkerblauw": (0, 88, 184),
"donkerblauwvergrijsd": (22, 58, 114),
"lichtblauw": (0, 161, 205), # zelfde als corporatelichtblauw
"lichtblauwvergrijsd": (5, 129, 162),
"geel": (255, 204, 0),
"geelvergrijsd": (255, 182, 0),
"oranje": (243, 146, 0),
"oranjevergrijsd": (206, 124, 0),
"rood": (233, 76, 10),
"roodvergrijsd": (178, 61, 2),
"roze": (175, 14, 128),
"rozevergrijsd": (130, 4, 94),
"grasgroen": (83, 163, 29),
"grasgroenvergrijsd": (72, 130, 37),
"appelgroen": (175, 203, 5),
"appelgroenvergrijsd": (137, 157, 12),
"violet": (172, 33, 142),
"lichtgrijs": (224, 224, 224), # 12% zwart
"grijs": (102, 102, 102), # 60% zwart
"logogrijs": (115, 115, 115), # 55% zwart
"codekleur": (88, 88, 88),
}
# prepend 'cbs:' to all color names to prevent collision
CBS_COLORS = {"cbs:" + name: (value[0] / 255, value[1] / 255, value[2] / 255)
for name, value in CBS_COLORS_RBG.items()}
# update the matplotlib colors
mcolors.get_named_colors_mapping().update(CBS_COLORS)
# deze dictionairy bevat meerdere palettes
CBS_PALETS = dict(
koel=[
"cbs:corporatelichtblauw",
"cbs:donkerblauw",
"cbs:appelgroen",
"cbs:grasgroen",
"cbs:oranje",
"cbs:roze",
],
warm=[
"cbs:rood",
"cbs:geel",
"cbs:roze",
"cbs:oranje",
"cbs:grasgroen",
"cbs:appelgroen",
]
)
[docs]def get_color_palette(style="koel"):
"""
Set the color palette
Parameters
----------
style: {"koel", "warm"), optional
Color palette to pick. Default = "koel"
Returns
-------
mpl.cycler:
cbs_color_cycle
Notes
-----
in order to set the cbs color palette default::
import matplotlib as mpl
from cbs_utils.plotting import get_color_palette
mpl.rcParams.update({'axes.prop_cycle': get_color_palette("warm")}
"""
try:
cbs_palette = CBS_PALETS[style]
except KeyError:
raise KeyError(f"Did not recognised style {style}. Should be one of {CBS_PALETS.keys()}")
else:
cbs_color_cycle = mpl.cycler(color=cbs_palette)
return cbs_color_cycle
[docs]def report_colors():
for name, value in CBS_COLORS.items():
logger.info("{:20s}: {}".format(name, value))
RATIO_OPTIONS = {"golden_ratio", "equal", "from_rows"}
[docs]class CBSPlotSettings(object):
"""
Class to hold the figure size for a standard document
Parameters
----------
number_of_figures_rows: int, optional
Number of figure rows, default = 2
number_of_figures_cols: int, optional
Number of figure cols, default = 1
text_width_in_pt: float, optional
Width of the text in pt, default = 392.64
text_height_in_pt: float, optional
Height of the text in pt: default = 693
text_margin_bot_in_inch: float, optional
Space at the bottom in inch. Default = 1 inch
text_height_in_inch: float, optional
Explicitly over rules the calculated text height if not None. Default = None
text_width_in_inch = None,
Explicitly over rules the calculated text height if not None. Default = None
plot_parameters: dict, optional
Dictionary with plot settings. If None (default), take the cbs defaults
color_palette: {"koel", "warm"}, optional
Pick color palette for the plot. Default is "koel"
font_size: int, optional
Size of all fonts. Default = 8
Notes
----------
* The variables are set to make sure that the figure have the exact same size as the document,
such that we do not have to rescale them. In this way the fonts will have the same size
here as in the document
"""
def __init__(self,
fig_width_in_inch: float = None,
fig_height_in_inch: float = None,
number_of_figures_cols: int = 1,
number_of_figures_rows: int = 2,
text_width_in_pt: float = 392.64813,
text_height_in_pt: float = 693,
text_margin_bot_in_inch: float = 1.0, # margin in inch
ratio_option="golden_ratio",
plot_parameters: dict = None,
color_palette: str = "koel",
font_size: int = 8
):
# set scale factor
inches_per_pt = 1 / 72.27
self.number_of_figures_rows = number_of_figures_rows
self.number_of_figures_cols = number_of_figures_cols
self.text_width_in_pt = text_width_in_pt
self.text_height_in_pt = text_height_in_pt
self.text_margin_bot_in_inch = text_margin_bot_in_inch
self.text_height = text_height_in_pt * inches_per_pt,
self.text_width = text_width_in_pt * inches_per_pt
inches_per_pt = 1 / 72.27
text_width_in_pt = 392.64813 # add the line \showthe\columnwidth above you figure in latex
text_height_in_pt = 693 # add the line \showthe\columnwidth above you figure in latex
text_height = text_height_in_pt * inches_per_pt
text_width = text_width_in_pt * inches_per_pt
text_margin_bot = 1.0 # margin in inch
golden_mean = (math.sqrt(5) - 1) / 2
if fig_width_in_inch is not None:
self.fig_width = fig_width_in_inch
else:
self.fig_width = text_width / number_of_figures_cols
if fig_height_in_inch is not None:
self.fig_height = fig_height_in_inch
elif ratio_option == "golden_ratio":
self.fig_height = self.fig_width * golden_mean
elif ratio_option == "equal":
self.fig_height = self.fig_width
elif ratio_option == "from_rows":
self.fig_height = (text_height - text_margin_bot) / number_of_figures_rows
else:
raise ValueError(f"fig height is not given by 'fig_height_in_inch' and 'ratio_option' "
f"= {ratio_option} is not in {RATIO_OPTIONS}")
self.fig_size = (self.fig_width, self.fig_height)
if plot_parameters is not None:
params = plot_parameters
else:
params = {'axes.labelsize': font_size,
'font.size': font_size,
'legend.fontsize': font_size,
'xtick.labelsize': font_size,
'ytick.labelsize': font_size,
'figure.figsize': self.fig_size,
'hatch.color': 'cbs:lichtgrijs',
'axes.prop_cycle': get_color_palette(color_palette),
'axes.edgecolor': "cbs:grijs",
'axes.linewidth': 1.5,
}
mpl.rcParams.update(params)
[docs]def add_values_to_bars(axis, type="bar",
position="c", format="{:.0f}", x_offset=0, y_offset=0, color="k",
horizontalalignment="center", verticalalignment="center"):
"""
Add the values of the bars as number in the center
Parameters
----------
axis : `mpl.pyplot.axes.Axes` object
Axis containing the bar plot
position: {"c", "t", "l", "r", "b"}, optional
Location of the numbers, where "c" is center, "t" is top, "l" is left, "r" is right and "b"
is bottom. Default = "c"
type: {"bar", "barh"}
Direction of the bars. Default = "bar", meaning vertical bars. Alternatively you need to
specify "barh" for horizontal bars.
format: str, optional
Formatter to use for the numbers. Default = "{:.0f}" (remove digits from float)
x_offset: float, optional
x offset in pt. Default = 0
y_offset: float, optional
y offset in pt. Default = 0
color: "str", optional
Color of the characters, Default is black
horizontalalignment: str, optional
Horizontal alignment of the numbers. Default = "center"
verticalalignment: str, optional
Vertical alignment of the numbers Default = "center"
):
"""
# voeg percentage to aan bars
for patch in axis.patches:
b = patch.get_bbox()
cx = (b.x1 + b.x0) / 2
cy = (b.y1 + b.y0) / 2
hh = (b.y1 - b.y0)
ww = (b.x1 - b.x0)
if position == "c":
(px, py) = (cx, cy)
elif position == "t":
(px, py) = (cx, cy + hh / 2)
elif position == "b":
(px, py) = (cx, cy - hh / 2)
elif position == "l":
(px, py) = (cx - ww / 2, cy)
elif position == "r":
(px, py) = (cx + ww / 2, cy)
else:
raise ValueError(f"position = {position} not recognised. Please check")
# add offsets
(px, py) = (px + x_offset, py + y_offset)
if type == "bar":
value = hh
elif type == "barh":
value = ww
else:
raise ValueError(f"type = {type} not recognised. Please check")
# make the value string using the format specifier
value_string = format.format(value)
axis.annotate(value_string, (px, py), color=color,
horizontalalignment=horizontalalignment,
verticalalignment=verticalalignment)
[docs]def add_cbs_pnglogo_to_plot(fig,
axes=None,
image=None,
margin_x=6,
margin_y=6,
loc="lower left",
zorder=10, color="blauw", alpha=1.0,
logo_width_in_mm=3.234,
logo_height_in_mm=4.995,
resample=False,
):
"""
Add a CBS logo to a plot
Parameters
----------
fig : `mpl.pyplot.axes.Axes` object
image: mpl.image or None
To prevent reading the logo many time you can read it once and pass the return image as an
argument in the next call
color: {"blauw", "wit", "grijs"}
Color of the logo. Three colors are available: blauw (blue), wit (white) and grijs (grey).
Default = "blauw"
margin_x, margin_y : int
The *x*/*y* image offset in mm.
alpha : None or float
The alpha blending value.
loc: {"lower left", "upper left", "upper right", "lower right"} or tuple
Location of the logo.
size: int
Size of the icon in pixels
Returns
-------
mpl.image:
The image of the logo
"""
bbox = fig.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
width = bbox.width * fig.dpi
height = bbox.height * fig.dpi
size_x = (logo_width_in_mm / 25.4) * fig.dpi
size_y = (logo_height_in_mm / 25.4) * fig.dpi
if image is None:
# only load the image if it is not already defined. The image is returned by the
# function, so you can use this retrun value for the next call, speeding up the code
image_dir = Path(__file__).parent / "logos"
if color == "blauw":
logo_name = "cbs_logo_tiny.png"
elif color == "wit":
logo_name = "cbs_logo_wit.png"
elif color == "grijs":
logo_name = "cbs_logo_tiny_grijs.pdf"
else:
raise ValueError(f"Color {color} not recognised. Please check")
image_name = image_dir / logo_name
# image = Image.open(str(image_name))
# image = imread(str(image_name))
image = PyPDF2.PdfFileReader(str(image_name))
if resample:
image.thumbnail((size_x, size_y))
# image.resize((int(size_x), int(size_y)), Image.ANTIALIAS)
# concerteer de marge van mm naar pixles
margin_x = (margin_x / 25.4) * fig.dpi
margin_x = (margin_x / 25.4) * fig.dpi
if isinstance(loc, str):
# if loc is a string, set the coordinates based on the value
if loc == "lower left":
xp = margin_x
yp = margin_y
elif loc == "upper left":
xp = margin_x
yp = height - image.size[1] - margin_y
elif loc == "upper right":
xp = width - image.size[0] - margin_x
yp = height - image.size[1] - margin_y
elif loc == "lower right":
xp = width - image.size[0] - margin_x
yp = margin_y
else:
raise ValueError(f"loc {loc} not recognised. Pleas check")
else:
# if it is a tuple, get the values
xp = width * loc[0]
yp = height * loc[1]
# image.shape = [logo_width_in_mm, logo_height_in_mm]
fig.figimage(image, xo=xp, yo=yp, zorder=zorder, alpha=alpha)
return image
[docs]def add_cbs_logo_to_plot(fig,
axes=None,
margin_x_in_mm=6.0,
margin_y_in_mm=6.0,
x0=0,
y0=0,
width=None,
height=None,
zorder_start=1,
):
# maak een box met de coordinaten van de linker onderhoek van het grijze vierkant in axis
# fractie coordinaten
if width is None:
ww = 1
else:
ww = width
if height is None:
hh = 1
else:
hh = height
if axes is not None:
tb = trn.Bbox.from_bounds(x0, y0, ww, hh).transformed(axes.transAxes)
# bereken de linker onderhoek van het figure in Figure coordinaten (pt van linker onderhoek)
x0 = tb.x0 + (margin_x_in_mm / 25.4) * fig.dpi
y0 = tb.y0 + (margin_y_in_mm / 25.4) * fig.dpi
else:
x0 = (margin_x_in_mm / 25.4) * fig.dpi
y0 = (margin_y_in_mm / 25.4) * fig.dpi
all_points = get_cbs_logo_points()
if axes is not None:
trans = axes.transAxes
else:
trans = fig.transFigure
zorder = zorder_start
for points_in_out in all_points:
for ii, points in enumerate(points_in_out):
points[:, :2] *= (fig.dpi / 25.4)
points[:, 0] += x0
points[:, 1] += y0
pl = points[:, :2]
dr = points[:, 2]
tr_path = mPath(pl, dr).transformed(trans.inverted())
if ii == 0:
color = "cbs:logogrijs"
else:
color = "cbs:lichtgrijs"
poly = mpatches.PathPatch(tr_path, fc=color,
linewidth=0,
zorder=zorder,
transform=trans)
poly.set_clip_on(False)
if axes is not None:
axes.add_patch(poly)
else:
fig.patches.append(poly)
zorder += 1
[docs]def get_cbs_logo_points(logo_width_in_mm=3.234, logo_height_in_mm=4.995, rrcor=0.171):
"""
Maak een array met de letters van het CBS logog
Parameters
----------
logo_width_in_mm: float
Breeedte van het logo
logo_height_in_mm: float
Hoogte van het logo
rrcor: float
Radius of corners
Returns
-------
list
List met 3 Nx2 arrays
"""
ww = logo_width_in_mm
# punten C, beginnen links onder, tegen klok in, binnen en buiten kant
points_c = [
np.array(list([
[0.000, 2.663, mPath.MOVETO],
[1.430, 2.663, mPath.LINETO],
[1.430, 3.308, mPath.LINETO],
[0.644, 3.308, mPath.LINETO],
[0.644, 3.577, mPath.LINETO],
[1.430, 3.577, mPath.LINETO],
[1.430, 4.221, mPath.LINETO],
[rrcor, 4.221, mPath.LINETO],
[0.000, 4.221, mPath.CURVE3],
[0.000, 4.221 - rrcor, mPath.CURVE3],
[0.000, 2.663, mPath.CLOSEPOLY],
])),
np.array(list([
[0.188, 2.851, mPath.MOVETO],
[1.242, 2.851, mPath.LINETO],
[1.242, 3.120, mPath.LINETO],
[1.242, 3.120, mPath.LINETO],
[0.457, 3.120, mPath.LINETO],
[0.457, 3.765, mPath.LINETO],
[1.242, 3.765, mPath.LINETO],
[1.242, 4.033, mPath.LINETO],
[0.188, 4.033, mPath.LINETO],
[0.188, 2.851, mPath.CLOSEPOLY],
])),
]
points_b1 = [
np.array(list([
[1.674, 2.663, mPath.MOVETO],
[3.234, 2.663, mPath.LINETO],
[3.234, 4.221 - rrcor, mPath.LINETO],
[3.234, 4.221, mPath.CURVE3],
[3.063, 4.221, mPath.CURVE3],
[2.318, 4.221, mPath.LINETO],
[2.318, 4.996 - rrcor, mPath.LINETO],
[2.318, 4.996, mPath.CURVE3],
[2.147, 4.996, mPath.CURVE3],
[1.674, 4.996, mPath.LINETO],
[1.674, 2.663, mPath.CLOSEPOLY],
])),
np.array(list([
[1.862, 2.851, mPath.MOVETO],
[3.046, 2.851, mPath.LINETO],
[3.046, 4.034, mPath.LINETO],
[2.130, 4.034, mPath.LINETO],
[2.130, 4.808, mPath.LINETO],
[1.862, 4.808, mPath.LINETO],
[1.862, 2.851, mPath.CLOSEPOLY],
])),
]
# in binnen stuk van de b
points_b2 = [
np.array(list([
[2.129, 3.121, mPath.MOVETO],
[2.775, 3.121, mPath.LINETO],
[2.775, 3.766, mPath.LINETO],
[2.129, 3.766, mPath.LINETO],
[2.129, 3.121, mPath.CLOSEPOLY],
])),
np.array(list([
[2.317, 3.309, mPath.MOVETO],
[2.588, 3.309, mPath.LINETO],
[2.588, 3.578, mPath.LINETO],
[2.317, 3.578, mPath.LINETO],
[2.317, 3.309, mPath.CLOSEPOLY],
])),
]
# de punten van de S, beginnende linksboven, tegen de klok in. Eerst array is de
# buitenkant, tweede array is de binnenkant
points_s = [
np.array(list([
[0.000, 2.420, mPath.MOVETO],
[0.000, 0.888, mPath.LINETO],
[2.589, 0.888, mPath.LINETO],
[2.589, 0.645, mPath.LINETO],
[0.000, 0.645, mPath.LINETO],
[0.000, rrcor, mPath.LINETO],
[0.000, 0.000, mPath.CURVE3],
[rrcor, 0, mPath.CURVE3],
[ww - rrcor, 0, mPath.LINETO],
[ww, 0, mPath.CURVE3],
[ww, rrcor, mPath.CURVE3],
[ww, 1.533, mPath.LINETO],
[0.646, 1.533, mPath.LINETO],
[0.646, 1.772, mPath.LINETO],
[3.234, 1.772, mPath.LINETO],
[3.234, 2.420, mPath.LINETO],
[0.000, 2.420, mPath.CLOSEPOLY],
])),
np.array(list([
[0.188, 2.232, mPath.MOVETO],
[0.188, 1.076, mPath.LINETO],
[2.777, 1.076, mPath.LINETO],
[2.777, 0.457, mPath.LINETO],
[0.188, 0.457, mPath.LINETO],
[0.188, 0.188, mPath.LINETO],
[3.045, 0.188, mPath.LINETO],
[3.045, 1.345, mPath.LINETO],
[0.458, 1.345, mPath.LINETO],
[0.458, 1.960, mPath.LINETO],
[3.045, 1.960, mPath.LINETO],
[3.045, 2.232, mPath.LINETO],
[0.188, 2.232, mPath.CLOSEPOLY],
])),
]
return [points_c, points_b1, points_b2, points_s]
[docs]def add_axis_label_background(fig, axes, alpha=1,
margin=0.05,
x0=None,
y0=None,
loc="east",
radius_corner_in_mm=1,
logo_margin_x_in_mm=1,
logo_margin_y_in_mm=1,
add_logo=True,
aspect=None
):
"""
Add a background to the axis label
Parameters
----------
fig : `mpl.figure.Figure` object
The total canvas of the Figure
axes : `mpl.axes.Axes` object
The axes of the plot to add a box
alpha: float, optional
Transparency of the box. Default = 1 (not transparent)
margin: float, optional
The margin between the labels and the side of the gray box
loc: {"east", "south"}
Location of the background. Default = "east" (left to y-axis. Only "east" and "south" are
implemented
add_logo: bool, optional
If true, add the cbs logo. Default = True
radius_corner_in_mm: float, optional
Radius of the corner in mm. Default = 2
logo_margin_x_in_mm: float
Distance from bottom of logo in mm. Default = 2
logo_margin_y_in_mm=2,
Distance from left of logo in mm. Default = 2
"""
# the bounding box with respect to the axis in Figure coordinates
# (0 is bottom left canvas, 1 is top right)
bbox_axis_fig = axes.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
# the bounding box with respect to the axis coordinates
# (0 is bottom left axis, 1 is top right axis)
bbox_axi = axes.get_tightbbox(fig.canvas.get_renderer()).transformed(axes.transAxes.inverted())
if loc == "east":
if x0 is None:
x0 = bbox_axi.x0 - margin * bbox_axi.width
x1 = 0
y0 = 0
y1 = 1
elif loc == "south":
x0 = 0
x1 = 1
if y0 is None:
y0 = bbox_axi.y0 - margin * bbox_axi.height
y1 = 0
else:
raise ValueError(f"Location loc = {loc} is not recognised. Only east and south implemented")
# width and height of the grey box area
width = x1 - x0
height = y1 - y0
logger.debug(f"Adding rectangle with width {width} and height {height}")
# eerste vierkant zorgt voor rechte hoeken aan de rechter kant
if loc == "east":
rec_p = (x0 + width / 2, y0)
rec_w = width / 2
rec_h = height
elif loc == "south":
rec_p = (x0, y0 + height / 2)
rec_w = width
rec_h = height / 2
else:
raise AssertionError(f"This should not happen")
p1 = mpl.patches.Rectangle(rec_p,
width=rec_w,
height=rec_h,
alpha=alpha,
facecolor='cbs:lichtgrijs',
edgecolor='cbs:lichtgrijs',
zorder=0
)
p1.set_transform(axes.transAxes)
p1.set_clip_on(False)
# tweede vierkant zorgt voor ronde hoeken aan de linker kant
radius_in_inch = radius_corner_in_mm / 25.4
xshift = radius_in_inch / bbox_axis_fig.width
yshift = radius_in_inch / bbox_axis_fig.height
pad = radius_in_inch / bbox_axis_fig.width
# we moeten corrigeren voor de ronding van de hoeken als we een aspect ratio hebben
if aspect is None:
aspect = bbox_axis_fig.height / bbox_axis_fig.width
logger.debug(f"Using aspect ratio {aspect}")
p2 = mpl.patches.FancyBboxPatch((x0 + xshift, y0 + yshift),
width=width - 2 * xshift,
height=height - 2 * yshift,
mutation_aspect=1 / aspect,
alpha=alpha,
facecolor='cbs:lichtgrijs',
edgecolor='cbs:lichtgrijs',
transform=fig.transFigure,
zorder=0)
p2.set_boxstyle("round", pad=pad)
p2.set_transform(axes.transAxes)
p2.set_clip_on(False)
axes.add_artist(p1)
axes.add_artist(p2)
if add_logo:
add_cbs_logo_to_plot(fig=fig,
axes=axes,
x0=x0,
y0=y0,
width=width,
height=height,
margin_x_in_mm=logo_margin_x_in_mm,
margin_y_in_mm=logo_margin_y_in_mm)
[docs]def clean_up_artists(axis, artist_list):
"""
try to remove the artists stored in the artist list belonging to the 'axis'.
:param axis: clean artists belonging to these axis
:param artist_list: list of artist to remove
:return: nothing
"""
for artist in artist_list:
try:
# fist attempt: try to remove collection of contours for instance
while artist.collections:
for col in artist.collections:
artist.collections.remove(col)
try:
axis.collections.remove(col)
except ValueError:
pass
artist.collections = []
axis.collections = []
except AttributeError:
pass
# second attempt, try to remove the text
try:
artist.remove()
except (AttributeError, ValueError):
pass