charite-plot 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {charite_plot-0.1.1 → charite_plot-0.2.0}/CHANGELOG.md +11 -0
  2. charite_plot-0.2.0/CITATION.cff +19 -0
  3. {charite_plot-0.1.1 → charite_plot-0.2.0}/LICENSE.md +4 -0
  4. {charite_plot-0.1.1 → charite_plot-0.2.0}/PKG-INFO +26 -1
  5. {charite_plot-0.1.1 → charite_plot-0.2.0}/README.md +21 -0
  6. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/__init__.py +1 -1
  7. charite_plot-0.2.0/charite_plot/_theme_common.py +55 -0
  8. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/altair_themes.py +61 -38
  9. charite_plot-0.2.0/charite_plot/mpl_themes.py +188 -0
  10. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/altair.md +14 -0
  11. charite_plot-0.2.0/docs/assets/theme_example.png +0 -0
  12. charite_plot-0.2.0/docs/citing.md +20 -0
  13. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/matplotlib.md +14 -0
  14. {charite_plot-0.1.1 → charite_plot-0.2.0}/examples/mpl_showcase.py +3 -1
  15. charite_plot-0.2.0/examples/palette_preview.py +61 -0
  16. charite_plot-0.2.0/examples/poly_showcase.py +107 -0
  17. {charite_plot-0.1.1 → charite_plot-0.2.0}/mkdocs.yml +1 -0
  18. charite_plot-0.1.1/charite_plot/mpl_themes.py +0 -129
  19. charite_plot-0.1.1/docs/assets/theme_example.png +0 -0
  20. charite_plot-0.1.1/examples/palette_preview.py +0 -47
  21. charite_plot-0.1.1/examples/poly_showcase.py +0 -108
  22. {charite_plot-0.1.1 → charite_plot-0.2.0}/.github/workflows/ci.yml +0 -0
  23. {charite_plot-0.1.1 → charite_plot-0.2.0}/.gitignore +0 -0
  24. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/colors.py +0 -0
  25. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/fonts.py +0 -0
  26. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/palettes.py +0 -0
  27. {charite_plot-0.1.1 → charite_plot-0.2.0}/charite_plot/py.typed +0 -0
  28. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/api/altair_themes.md +0 -0
  29. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/api/colors.md +0 -0
  30. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/api/fonts.md +0 -0
  31. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/api/mpl_themes.md +0 -0
  32. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/api/palettes.md +0 -0
  33. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/assets/palette_preview.png +0 -0
  34. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/colors.md +0 -0
  35. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/index.md +0 -0
  36. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/installation.md +0 -0
  37. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/palettes.md +0 -0
  38. {charite_plot-0.1.1 → charite_plot-0.2.0}/docs/stylesheets/extra.css +0 -0
  39. {charite_plot-0.1.1 → charite_plot-0.2.0}/examples/altair_showcase.py +0 -0
  40. {charite_plot-0.1.1 → charite_plot-0.2.0}/pyproject.toml +0 -0
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.0] - 2026-07-01
9
+
10
+ ### Added
11
+
12
+ - `theme_charite` (matplotlib) gained `interactive` and `tiny_margins` parameters.
13
+ - `theme_charite` (Altair) gained `thickness` and `tiny_margins` parameters, bringing it to parity with the matplotlib backend.
14
+
15
+ ### Changed
16
+
17
+ - Font-size ramp is now shared between both backends: labels scale at 1× and legend text at 0.8× the base size. Altair legend labels are slightly smaller and axis labels slightly larger as a result.
18
+
8
19
  ## [0.1.1] - 2026-06-13
9
20
 
10
21
  ### Fixed
@@ -0,0 +1,19 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: "charite-plot: Matplotlib and Altair Themes for Charité – Universitätsmedizin Berlin"
5
+ authors:
6
+ - family-names: Ramezani
7
+ given-names: Pedram
8
+ email: pedram.ramezani@charite.de
9
+ version: 0.2.0
10
+ date-released: 2026-07-01
11
+ license: MIT
12
+ url: "https://github.com/pedramezani/charite-plot"
13
+ repository-code: "https://github.com/pedramezani/charite-plot"
14
+ keywords:
15
+ - matplotlib
16
+ - altair
17
+ - theme
18
+ - visualization
19
+ - charite
@@ -7,3 +7,7 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
7
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
8
 
9
9
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
+
11
+ ---
12
+
13
+ This package is based on the original `charite` R package developed by Johannes Julius Mohn (https://github.com/johannesjuliusm/charite), which is also distributed under the MIT License.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: charite-plot
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: Matplotlib and Altair themes for Charité – Universitätsmedizin Berlin
5
5
  Project-URL: Homepage, https://github.com/pedramezani/charite-plot
6
6
  Project-URL: Documentation, https://pedramezani.github.io/charite-plot
@@ -17,6 +17,10 @@ License: # MIT License
17
17
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18
18
 
19
19
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
+
21
+ ---
22
+
23
+ This package is based on the original `charite` R package developed by Johannes Julius Mohn (https://github.com/johannesjuliusm/charite), which is also distributed under the MIT License.
20
24
  License-File: LICENSE.md
21
25
  Keywords: altair,charite,matplotlib,theme,visualization
22
26
  Classifier: Development Status :: 3 - Alpha
@@ -140,6 +144,27 @@ The fallback chain follows the official Charité brand guidelines:
140
144
  apply_theme(theme_charite(font="Arial"))
141
145
  ```
142
146
 
147
+ ## How to cite
148
+
149
+ If you use charite-plot in your work, please cite it as:
150
+
151
+ ```bibtex
152
+ @software{ramezani2026chariteplot,
153
+ author = {Ramezani, Pedram},
154
+ title = {charite-plot: Matplotlib and Altair Themes for Charité – Universitätsmedizin Berlin},
155
+ year = {2026},
156
+ version = {0.2.0},
157
+ url = {https://github.com/pedramezani/charite-plot},
158
+ license = {MIT},
159
+ }
160
+ ```
161
+
162
+ ## Acknowledgements
163
+
164
+ This package is based on the original [`charite` R package](https://github.com/johannesjuliusm/charite) developed by Johannes Julius Mohn.
165
+
143
166
  ## License
144
167
 
145
168
  MIT © 2026 Pedram Ramezani
169
+
170
+ The original `charite` R package by Johannes Julius Mohn is likewise MIT licensed.
@@ -91,6 +91,27 @@ The fallback chain follows the official Charité brand guidelines:
91
91
  apply_theme(theme_charite(font="Arial"))
92
92
  ```
93
93
 
94
+ ## How to cite
95
+
96
+ If you use charite-plot in your work, please cite it as:
97
+
98
+ ```bibtex
99
+ @software{ramezani2026chariteplot,
100
+ author = {Ramezani, Pedram},
101
+ title = {charite-plot: Matplotlib and Altair Themes for Charité – Universitätsmedizin Berlin},
102
+ year = {2026},
103
+ version = {0.2.0},
104
+ url = {https://github.com/pedramezani/charite-plot},
105
+ license = {MIT},
106
+ }
107
+ ```
108
+
109
+ ## Acknowledgements
110
+
111
+ This package is based on the original [`charite` R package](https://github.com/johannesjuliusm/charite) developed by Johannes Julius Mohn.
112
+
94
113
  ## License
95
114
 
96
115
  MIT © 2026 Pedram Ramezani
116
+
117
+ The original `charite` R package by Johannes Julius Mohn is likewise MIT licensed.
@@ -26,5 +26,5 @@ from .palettes import PALETTES, make_palette # noqa: F401
26
26
  from . import mpl_themes # noqa: F401
27
27
  from . import altair_themes # noqa: F401
28
28
 
29
- __version__ = "0.1.1"
29
+ __version__ = "0.2.0"
30
30
  __author__ = "Pedram Ramezani"
@@ -0,0 +1,55 @@
1
+ """Shared theme primitives for the matplotlib and Altair backends.
2
+
3
+ These small ``NamedTuple`` structures keep the per-backend theme builders
4
+ readable by giving names to the derived size/margin values. The font-size
5
+ ratios are shared so both backends scale identically from their base size
6
+ (matplotlib works in points, Altair in pixels, but the ratios are the same).
7
+ """
8
+
9
+ from typing import NamedTuple
10
+
11
+
12
+ class FontSizes(NamedTuple):
13
+ """Derived font sizes, all scaled from the base ``font_size``."""
14
+ title: float
15
+ subtitle: float
16
+ label: float
17
+ legend: float
18
+
19
+
20
+ class Margins(NamedTuple):
21
+ """Derived margins/paddings around the plot panel.
22
+
23
+ ``wspace`` / ``hspace`` are subplot-spacing fractions used only by the
24
+ matplotlib backend; they default to ``0`` so the Altair backend can build
25
+ a ``Margins`` without supplying them.
26
+ """
27
+ title: float
28
+ label: float
29
+ legend: float
30
+ wspace: float = 0.0
31
+ hspace: float = 0.0
32
+
33
+
34
+ class Thickness(NamedTuple):
35
+ """Derived line widths, scaled from the base ``thickness``.
36
+
37
+ ``lines`` / ``patches`` are matplotlib-specific defaults; they carry
38
+ defaults so the Altair backend can build a ``Thickness`` from just the
39
+ axis-related widths.
40
+ """
41
+ axes: float
42
+ grid: float
43
+ ticks: float
44
+ lines: float = 1.5
45
+ patches: float = 0.0
46
+
47
+
48
+ def resolve_font_sizes(font_size: float) -> FontSizes:
49
+ """Return the standard Charité font-size ramp derived from *font_size*."""
50
+ return FontSizes(
51
+ title=round(font_size * 1.2),
52
+ subtitle=font_size,
53
+ label=font_size,
54
+ legend=round(font_size * 0.8),
55
+ )
@@ -8,17 +8,20 @@ Register and enable with::
8
8
  enable(font="Arial", font_size=13) # font override
9
9
  """
10
10
 
11
+ from __future__ import annotations
12
+
11
13
  from .colors import (
12
14
  BLACK, WHITE, TEXT_GREY, PRIME_BLUE, PRIME_LGREY,
13
15
  SECOND_DBLUE, KORALL,
14
16
  )
15
17
  from .palettes import PALETTES
16
18
  from .fonts import build_font_stack
19
+ from ._theme_common import Margins, Thickness, resolve_font_sizes
17
20
 
18
21
  from typing import TYPE_CHECKING
19
22
 
20
23
  if TYPE_CHECKING:
21
- from altair.theme import ThemeConfig
24
+ from altair.theme import AxisConfigKwds, ThemeConfig
22
25
 
23
26
 
24
27
  def _css_font_stack(preferred: str | None) -> str:
@@ -32,10 +35,12 @@ def _css_font_stack(preferred: str | None) -> str:
32
35
  def theme_charite(
33
36
  font: str | None = None,
34
37
  font_size: int = 12,
38
+ thickness: float = 0.5,
35
39
  grid: bool = False,
36
40
  palette: str | list[str] = "primary",
37
41
  background: str = "white",
38
- ) -> "ThemeConfig":
42
+ tiny_margins: bool = False,
43
+ ) -> ThemeConfig:
39
44
  """Return an Altair theme config dict for the Charité corporate theme.
40
45
 
41
46
  Parameters
@@ -44,78 +49,95 @@ def theme_charite(
44
49
  Preferred font. Falls back through Charité Text Office → Charit? Text Office → Calibri → DejaVu Sans → sans-serif.
45
50
  font_size:
46
51
  Base font size in pixels.
52
+ thickness:
53
+ Axis line and tick width.
47
54
  grid:
48
55
  Show axis grid lines.
49
56
  palette:
50
57
  Built-in palette name or list of hex colors for the categorical range.
51
58
  background:
52
59
  Chart background color.
60
+ tiny_margins:
61
+ Minimise all paddings around the chart.
53
62
  """
54
63
  colors = PALETTES[palette] if isinstance(palette, str) else list(palette)
55
- font_str = _css_font_stack(preferred=font)
56
- label_size = font_size - 1
57
- title_size = round(font_size * 1.2)
58
-
59
- axis = {
60
- "labelFont": font_str,
61
- "titleFont": font_str,
62
- "labelFontSize": label_size,
63
- "titleFontSize": font_size,
64
+ font_stack = _css_font_stack(preferred=font)
65
+
66
+ FONT_SIZES = resolve_font_sizes(font_size)
67
+ MARGINS = Margins(
68
+ title=8 if tiny_margins else 12, # chart title offset
69
+ label=3 if tiny_margins else 6, # axis / legend title padding
70
+ legend=2 if tiny_margins else 4, # legend inner padding
71
+ )
72
+ THICKNESS = Thickness(
73
+ axes=thickness, # domain line width
74
+ grid=thickness * 0.8, # grid line width
75
+ ticks=thickness, # tick mark width
76
+ lines=2, # line-mark stroke width (independent of thickness)
77
+ patches=0.4, # geoshape stroke width
78
+ )
79
+ CHART_PAD = 6 if tiny_margins else 10
80
+
81
+ axis: AxisConfigKwds = {
82
+ "labelFont": font_stack,
83
+ "titleFont": font_stack,
84
+ "labelFontSize": FONT_SIZES.label,
85
+ "titleFontSize": FONT_SIZES.subtitle,
64
86
  "labelColor": TEXT_GREY,
65
87
  "titleColor": TEXT_GREY,
66
- "titlePadding": 6,
88
+ "titlePadding": MARGINS.label,
67
89
  "domain": True,
68
90
  "domainColor": BLACK,
69
- "domainWidth": 0.5,
91
+ "domainWidth": THICKNESS.axes,
70
92
  "ticks": True,
71
93
  "tickColor": BLACK,
72
- "tickWidth": 0.5,
94
+ "tickWidth": THICKNESS.ticks,
73
95
  "tickSize": 4,
74
96
  "grid": grid,
75
97
  "gridColor": PRIME_LGREY,
76
- "gridWidth": 0.4,
77
- "gridOpacity": 0.8,
98
+ "gridWidth": THICKNESS.grid,
99
+ "gridOpacity": 0.85,
78
100
  }
79
101
 
80
102
  return {
81
103
  "config": {
82
104
  "background": background,
83
- "font": font_str,
84
- "padding": {"top": 10, "bottom": 10, "left": 10, "right": 10},
105
+ "font": font_stack,
106
+ "padding": {"top": CHART_PAD, "bottom": CHART_PAD, "left": CHART_PAD, "right": CHART_PAD},
85
107
  "view": {"stroke": None},
86
108
  "axis": axis,
87
109
  "axisX": axis,
88
110
  "axisY": axis,
89
111
  "legend": {
90
- "labelFont": font_str,
91
- "titleFont": font_str,
92
- "labelFontSize": label_size,
93
- "titleFontSize": font_size,
112
+ "labelFont": font_stack,
113
+ "titleFont": font_stack,
114
+ "labelFontSize": FONT_SIZES.legend,
115
+ "titleFontSize": FONT_SIZES.subtitle,
94
116
  "labelColor": TEXT_GREY,
95
117
  "titleColor": TEXT_GREY,
96
- "titlePadding": 6,
97
- "padding": 4,
98
- "rowPadding": 3,
118
+ "titlePadding": MARGINS.label,
119
+ "padding": MARGINS.legend,
120
+ "rowPadding": FONT_SIZES.legend / 4,
99
121
  "symbolSize": 100,
100
122
  },
101
123
  "header": {
102
- "labelFont": font_str,
103
- "titleFont": font_str,
104
- "labelFontSize": label_size,
105
- "titleFontSize": font_size,
124
+ "labelFont": font_stack,
125
+ "titleFont": font_stack,
126
+ "labelFontSize": FONT_SIZES.label,
127
+ "titleFontSize": FONT_SIZES.subtitle,
106
128
  "labelColor": WHITE,
107
129
  "titleColor": PRIME_BLUE,
108
130
  "labelBackground": PRIME_BLUE,
109
131
  },
110
132
  "title": {
111
- "font": font_str,
112
- "subtitleFont": font_str,
113
- "fontSize": title_size,
114
- "subtitleFontSize": font_size,
133
+ "font": font_stack,
134
+ "subtitleFont": font_stack,
135
+ "fontSize": FONT_SIZES.title,
136
+ "subtitleFontSize": FONT_SIZES.subtitle,
115
137
  "color": PRIME_BLUE,
116
138
  "subtitleColor": TEXT_GREY,
117
139
  "anchor": "middle",
118
- "offset": 12,
140
+ "offset": MARGINS.title,
119
141
  },
120
142
  "range": {
121
143
  "category": colors,
@@ -125,13 +147,13 @@ def theme_charite(
125
147
  "ramp": ["#ffffff", PRIME_BLUE],
126
148
  },
127
149
  "bar": {"color": colors[0], "binSpacing": 1},
128
- "line": {"color": colors[0], "strokeWidth": 2},
150
+ "line": {"color": colors[0], "strokeWidth": THICKNESS.lines},
129
151
  "point": {"color": colors[0], "size": 60, "opacity": 0.85, "filled": True},
130
152
  "area": {"color": colors[0], "opacity": 0.8},
131
153
  "arc": {"color": colors[0]},
132
154
  "rect": {"color": colors[0]},
133
- "geoshape": {"stroke": WHITE, "strokeWidth": 0.4},
134
- "text": {"font": font_str, "fontSize": label_size, "color": TEXT_GREY},
155
+ "geoshape": {"stroke": WHITE, "strokeWidth": THICKNESS.patches},
156
+ "text": {"font": font_stack, "fontSize": FONT_SIZES.label, "color": TEXT_GREY},
135
157
  }
136
158
  }
137
159
 
@@ -149,7 +171,8 @@ def enable(**kwargs) -> None:
149
171
  ----------
150
172
  **kwargs:
151
173
  Any parameter accepted by ``theme_charite``
152
- (``font``, ``font_size``, ``grid``, ``palette``, ``background``).
174
+ (``font``, ``font_size``, ``thickness``, ``grid``, ``palette``,
175
+ ``background``, ``tiny_margins``).
153
176
 
154
177
  Examples
155
178
  --------
@@ -0,0 +1,188 @@
1
+ """Matplotlib theme for Charité – Universitätsmedizin Berlin."""
2
+
3
+ from contextlib import contextmanager
4
+
5
+ from cycler import cycler
6
+
7
+ from .colors import TEXT_GREY, PRIME_BLUE, PRIME_LGREY, BLACK, WHITE
8
+ from .palettes import PALETTES
9
+ from .fonts import build_font_stack
10
+ from ._theme_common import Margins, Thickness, resolve_font_sizes
11
+
12
+
13
+ def theme_charite(
14
+ font: str | None = None,
15
+ font_size: int = 10,
16
+ thickness: float = 0.5,
17
+ grid: bool = False,
18
+ palette: str | list[str] = "primary",
19
+ interactive: bool = False,
20
+ tiny_margins: bool = False,
21
+ ) -> dict:
22
+ """Return matplotlib rcParams dict for the Charité corporate identity theme.
23
+
24
+ Parameters
25
+ ----------
26
+ font:
27
+ Preferred font family. Falls back through Charité Text Office →
28
+ Charit? Text Office → Calibri → DejaVu Sans → sans-serif if the requested font is not installed.
29
+ font_size:
30
+ Base font size in points. Defaults to 10 for screen; use 8 for print.
31
+ thickness:
32
+ Axis line and tick width. Mirrors the R ``thickness`` parameter.
33
+ grid:
34
+ Show major grid lines. Disabled by default.
35
+ palette:
36
+ Name of a built-in palette or a list of hex color strings used for the
37
+ ``axes.prop_cycle``.
38
+ interactive:
39
+ Enable matplotlib interactive mode (equivalent to ``plt.ion()``).
40
+ Useful in notebooks or scripts where you want plots to display
41
+ without blocking.
42
+ tiny_margins:
43
+ Minimise all margins around the plot panel. Useful for dense layouts or
44
+ when saving figures with little surrounding whitespace.
45
+ """
46
+ sans_font_stack = build_font_stack(preferred=font)
47
+ monospace_font_stack = ["Andale Mono", "Nimbus Mono L", "Courier New", "Courier", "Fixed", "Terminal", "monospace"]
48
+ colors = PALETTES[palette] if isinstance(palette, str) else list(palette)
49
+
50
+ FONT_SIZES = resolve_font_sizes(font_size)
51
+ MARGINS = Margins(
52
+ title=4 if tiny_margins else 8,
53
+ label=2 if tiny_margins else 4,
54
+ legend=2 if tiny_margins else 4,
55
+ wspace=0.1 if tiny_margins else 0.2,
56
+ hspace=0.125 if tiny_margins else 0.25
57
+ )
58
+ THICKNESS = Thickness(
59
+ axes=thickness,
60
+ grid=thickness * 0.6,
61
+ ticks=thickness,
62
+ lines=1.5,
63
+ patches=0
64
+ )
65
+
66
+ return {
67
+ # Font — monospace is a fallback for latex mathtt, code blocks, etc.
68
+ "font.family": "sans-serif",
69
+ "font.sans-serif": sans_font_stack,
70
+ "font.monospace": monospace_font_stack,
71
+ "font.size": FONT_SIZES.label,
72
+
73
+ # Axes
74
+ "axes.linewidth": THICKNESS.axes,
75
+ "axes.titlelocation": "center",
76
+ "xaxis.labellocation": "center",
77
+ "yaxis.labellocation": "center",
78
+ "axes.titlesize": FONT_SIZES.title,
79
+ "axes.titlecolor": PRIME_BLUE,
80
+ "axes.titlepad": MARGINS.title,
81
+ "axes.labelsize": FONT_SIZES.label,
82
+ "axes.labelcolor": TEXT_GREY,
83
+ "axes.labelpad": MARGINS.label,
84
+ "axes.edgecolor": BLACK,
85
+ "axes.facecolor": WHITE,
86
+ "axes.grid": grid,
87
+ "axes.axisbelow": True,
88
+ "axes.spines.top": False,
89
+ "axes.spines.right": False,
90
+
91
+ # Axes 3D (only visible when projection='3d')
92
+ "axes3d.grid": grid,
93
+ "axes3d.xaxis.panecolor": WHITE,
94
+ "axes3d.yaxis.panecolor": WHITE,
95
+ "axes3d.zaxis.panecolor": WHITE,
96
+
97
+ # Polar axes (only visible when projection='polar')
98
+ "polaraxes.grid": grid,
99
+
100
+ # Grid (only visible when grid=True)
101
+ "grid.color": PRIME_LGREY,
102
+ "grid.linewidth": THICKNESS.grid,
103
+ # "grid.minor.alpha": 0.48,
104
+
105
+ # Ticks
106
+ "xtick.color": BLACK,
107
+ "ytick.color": BLACK,
108
+ "xtick.labelsize": FONT_SIZES.label,
109
+ "ytick.labelsize": FONT_SIZES.label,
110
+ "xtick.labelcolor": TEXT_GREY,
111
+ "ytick.labelcolor": TEXT_GREY,
112
+ "xtick.major.size": 2,
113
+ "ytick.major.size": 2,
114
+ "xtick.major.pad": 2,
115
+ "ytick.major.pad": 2,
116
+ "xtick.major.width": THICKNESS.ticks,
117
+ "ytick.major.width": THICKNESS.ticks,
118
+ "xtick.minor.visible": False,
119
+ "ytick.minor.visible": False,
120
+
121
+ # Text
122
+ "text.color": TEXT_GREY,
123
+ "text.antialiased": True,
124
+
125
+ # Figure
126
+ "figure.facecolor": WHITE,
127
+ "figure.edgecolor": WHITE,
128
+ "figure.dpi": 100,
129
+ "figure.figsize": (11, 8),
130
+ "figure.subplot.hspace": MARGINS.hspace,
131
+ "figure.subplot.wspace": MARGINS.wspace,
132
+ "figure.titlesize": FONT_SIZES.title,
133
+ "figure.labelsize": FONT_SIZES.label,
134
+
135
+ # Lines / markers
136
+ "lines.linewidth": THICKNESS.lines,
137
+ "lines.markersize": 6,
138
+ "lines.antialiased": True,
139
+
140
+ # Patches (bars, etc.)
141
+ "patch.linewidth": THICKNESS.patches,
142
+ "patch.force_edgecolor": False,
143
+ "patch.antialiased": True,
144
+
145
+ # Histograms
146
+ "hist.bins": "auto",
147
+
148
+ # Legend
149
+ "legend.fancybox": True,
150
+ "legend.frameon": False,
151
+ "legend.fontsize": FONT_SIZES.legend,
152
+ "legend.title_fontsize": FONT_SIZES.title,
153
+ "legend.labelcolor": TEXT_GREY,
154
+
155
+ # Interactive mode
156
+ "interactive": interactive,
157
+
158
+ # Save
159
+ "savefig.dpi": 300,
160
+ "savefig.bbox": "tight",
161
+ "savefig.transparent": True,
162
+
163
+ # Color cycle
164
+ "axes.prop_cycle": cycler("color", colors),
165
+ }
166
+
167
+
168
+ def apply_theme(params: dict) -> None:
169
+ """Apply a theme dict as the active matplotlib rcParams (permanent until reset)."""
170
+ import matplotlib as mpl
171
+ mpl.rcParams.update(params)
172
+
173
+
174
+ @contextmanager
175
+ def using(params: dict):
176
+ """Context manager: temporarily apply *params* then restore previous rcParams.
177
+
178
+ Examples
179
+ --------
180
+ ```python
181
+ with using(theme_charite(palette="goldelse")):
182
+ fig, ax = plt.subplots()
183
+ ax.plot([1, 2, 3])
184
+ ```
185
+ """
186
+ import matplotlib as mpl
187
+ with mpl.rc_context(params):
188
+ yield
@@ -44,9 +44,11 @@ alt.theme.enable("charite") # enable later
44
44
  |-----------|------|---------|-------------|
45
45
  | `font` | `str | None` | `None` | Preferred font. CSS fallback: Charité Text Office → Charit? Text Office → Calibri → DejaVu Sans → sans-serif. |
46
46
  | `font_size` | `int` | `12` | Base font size in pixels. |
47
+ | `thickness` | `float` | `0.5` | Axis line and tick width. |
47
48
  | `grid` | `bool` | `False` | Show axis grid lines. |
48
49
  | `palette` | `str | list[str]` | `"primary"` | Named palette or list of hex strings for the categorical color range. |
49
50
  | `background` | `str` | `"white"` | Chart background color. |
51
+ | `tiny_margins` | `bool` | `False` | Minimise all paddings around the chart. Useful for dense layouts. |
50
52
 
51
53
  ---
52
54
 
@@ -95,3 +97,15 @@ alt.Chart(df).mark_point().encode(
95
97
  ```python
96
98
  enable(background="transparent")
97
99
  ```
100
+
101
+ ### Dense layout with minimal margins
102
+
103
+ ```python
104
+ enable(tiny_margins=True)
105
+ ```
106
+
107
+ ### Thicker axis lines
108
+
109
+ ```python
110
+ enable(thickness=1.0)
111
+ ```
@@ -0,0 +1,20 @@
1
+ # Citing charite-plot
2
+
3
+ If you use charite-plot in your research or work, please cite it. A `CITATION.cff` file is included in the repository and GitHub will surface a **Cite this repository** button on the sidebar automatically.
4
+
5
+ ## BibTeX
6
+
7
+ ```bibtex
8
+ @software{ramezani2026chariteplot,
9
+ author = {Ramezani, Pedram},
10
+ title = {charite-plot: Matplotlib and Altair Themes for Charité – Universitätsmedizin Berlin},
11
+ year = {2026},
12
+ version = {0.2.0},
13
+ url = {https://github.com/pedramezani/charite-plot},
14
+ license = {MIT},
15
+ }
16
+ ```
17
+
18
+ ## Acknowledgements
19
+
20
+ charite-plot is based on the original [`charite` R package](https://github.com/johannesjuliusm/charite) developed by Johannes Julius Mohn.
@@ -57,6 +57,8 @@ mpl.rcParams.update(params)
57
57
  | `thickness` | `float` | `0.5` | Axis line and tick width. |
58
58
  | `grid` | `bool` | `False` | Show major grid lines. |
59
59
  | `palette` | `str | list[str]` | `"primary"` | Named palette or list of hex strings for `axes.prop_cycle`. |
60
+ | `interactive` | `bool` | `False` | Enable interactive mode (equivalent to `plt.ion()`). Useful in notebooks or scripts where plots should display without blocking. |
61
+ | `tiny_margins` | `bool` | `False` | Minimise all margins and paddings around the plot panel. Useful for dense layouts. |
60
62
 
61
63
  ---
62
64
 
@@ -87,3 +89,15 @@ apply_theme(theme_charite(palette=["#004d9b", "#ea5451", "#fab600"]))
87
89
  ```python
88
90
  apply_theme(theme_charite(grid=True))
89
91
  ```
92
+
93
+ ### Dense layout with minimal margins
94
+
95
+ ```python
96
+ apply_theme(theme_charite(tiny_margins=True))
97
+ ```
98
+
99
+ ### Interactive mode (notebooks / REPL)
100
+
101
+ ```python
102
+ apply_theme(theme_charite(interactive=True)) # equivalent to plt.ion()
103
+ ```
@@ -41,7 +41,7 @@ fig.suptitle(
41
41
  fontsize=16,
42
42
  y=1.01,
43
43
  )
44
- sub = fig.add_gridspec(3, 3, hspace=0.45, wspace=0.35)
44
+ sub = fig.add_gridspec(3, 3, hspace=0.125, wspace=0.1)
45
45
 
46
46
  cats = ["A", "B", "C"]
47
47
 
@@ -178,7 +178,9 @@ ax9.set_yticks([n_rows - i - 0.6 for i in range(n_rows)])
178
178
  ax9.set_yticklabels(palette_names, fontsize=7)
179
179
  ax9.set_xticks([])
180
180
  ax9.spines["left"].set_visible(False)
181
+ ax9.spines["bottom"].set_visible(False)
181
182
  ax9.tick_params(left=False)
183
+ ax9.tick_params(bottom=False)
182
184
  ax9.set_title("Color Palettes")
183
185
 
184
186
 
@@ -0,0 +1,61 @@
1
+ """Palette preview — generates swatch strips for every built-in palette.
2
+
3
+ Produces one asset in docs/assets/:
4
+ palette_preview.png
5
+
6
+ Run with:
7
+ python examples/palette_preview.py
8
+ """
9
+
10
+ import sys
11
+ import pathlib
12
+ sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
13
+
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.patches as mpatches
16
+
17
+ from charite_plot.mpl_themes import theme_charite, apply_theme
18
+ from charite_plot.palettes import PALETTES
19
+
20
+ apply_theme(theme_charite())
21
+
22
+ MAX_COLS = max(len(p) for p in PALETTES.values())
23
+ names = list(PALETTES.keys())
24
+ LABEL_PAD = 0.4
25
+ ROW_H = 0.58
26
+ FIG_W = 7.5
27
+
28
+ ASSETS = pathlib.Path(__file__).parent.parent / "docs" / "assets"
29
+ ASSETS.mkdir(parents=True, exist_ok=True)
30
+ OUT = ASSETS / "palette_preview.png"
31
+ SAVE_KW = dict(dpi=300, bbox_inches="tight", facecolor="white", transparent=False)
32
+
33
+
34
+ def _draw_row(ax: plt.Axes, name: str) -> None:
35
+ for i, color in enumerate(PALETTES[name]):
36
+ ax.add_patch(
37
+ mpatches.Rectangle(
38
+ (i, 0),
39
+ 1.0,
40
+ 0.82,
41
+ facecolor=color,
42
+ edgecolor="none",
43
+ )
44
+ )
45
+ ax.set_xlim(-LABEL_PAD, MAX_COLS)
46
+ ax.set_ylim(-0.1, 1.0)
47
+ ax.text(-LABEL_PAD, 0.41, name, ha="right", va="center", fontsize=12)
48
+ ax.axis("off")
49
+
50
+
51
+ fig, axes = plt.subplots(
52
+ len(names),
53
+ 1,
54
+ figsize=(FIG_W, len(names) * ROW_H + 0.3),
55
+ gridspec_kw={"hspace": 0},
56
+ )
57
+ for ax, name in zip(axes, names):
58
+ _draw_row(ax, name)
59
+ fig.savefig(OUT, **SAVE_KW)
60
+ print(f"Saved → {OUT}")
61
+ plt.show()
@@ -0,0 +1,107 @@
1
+ """Synthetic time-series showcase — generates the charite-plot theme example image.
2
+
3
+ Replicates the curves from ``tools/data/simulate_timeseries_data.R`` in the
4
+ charite R package: three conditions (wiggly normal, cubic polynomial, square
5
+ root) z-scored and plotted with uniform ±SE bands.
6
+
7
+ Produces one asset in docs/assets/:
8
+ theme_example.png
9
+
10
+ Run with:
11
+ python examples/poly_showcase.py
12
+ """
13
+
14
+ import sys
15
+ import pathlib
16
+ sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
17
+
18
+ import numpy as np
19
+ import matplotlib.pyplot as plt
20
+
21
+ from charite_plot.mpl_themes import theme_charite, apply_theme
22
+ from charite_plot.colors import PRIME_BLUE, PRIME_DGREY, KORALL, TEXT_GREY
23
+
24
+ COLORS = [KORALL, PRIME_BLUE, PRIME_DGREY] # C1, C2, C3
25
+ ASSETS = pathlib.Path(__file__).parent.parent / "docs" / "assets"
26
+ ASSETS.mkdir(parents=True, exist_ok=True)
27
+ OUT = ASSETS / "theme_example.png"
28
+ SAVE_KW = dict(dpi=300, bbox_inches="tight", facecolor="white")
29
+
30
+
31
+ # ── data helpers ────────────────────────────────────────────────────────────
32
+
33
+ def _dnorm(x: np.ndarray, mean: float, sd: float) -> np.ndarray:
34
+ return np.exp(-0.5 * ((x - mean) / sd) ** 2) / (sd * np.sqrt(2 * np.pi))
35
+
36
+
37
+ def _scale(x: np.ndarray) -> np.ndarray:
38
+ return (x - x.mean()) / x.std(ddof=1)
39
+
40
+
41
+ def _generate_data():
42
+ # R and numpy use different RNGs so the tiny noise terms differ slightly,
43
+ # but they are visually negligible after z-scoring (sd ≤ 0.01).
44
+ rng = np.random.default_rng(42)
45
+ n = 100
46
+ time = np.linspace(0, 1, n)
47
+ t_axis = np.arange(1, n + 1)
48
+
49
+ c1_core = _dnorm(time, mean=0.5, sd=0.15)
50
+ c1 = (c1_core
51
+ + 0.1 * np.sin(25 * time)
52
+ + 0.05 * np.cos(40 * time)
53
+ + rng.normal(0, 0.005, n))
54
+ c2 = 3.9 * time**3 - 6 * time**2 + 3 * time + rng.normal(0, 0.01, n)
55
+ c3 = time**0.5 + rng.normal(0, 0.005, n)
56
+
57
+ means = {"C1": _scale(c1) + 2.2, "C2": _scale(c2) + 2.8, "C3": _scale(c3) + 2.8}
58
+ se_all = rng.uniform(0.4, 0.6, n * 3)
59
+ ses = {"C1": se_all[:n], "C2": se_all[n:2*n], "C3": se_all[2*n:]}
60
+ return t_axis, means, ses
61
+
62
+
63
+ # ── plot helpers ────────────────────────────────────────────────────────────
64
+
65
+ def _plot_curves(ax: plt.Axes, t_axis, means, ses, frameon: bool = False) -> None:
66
+ for (label, mean), se, color in zip(means.items(), ses.values(), COLORS):
67
+ ax.fill_between(t_axis, mean - se, mean + se, alpha=0.25, color=color, linewidth=0)
68
+ ax.plot(t_axis, mean, lw=2, label=label, color=color)
69
+ ax.set_xlim(left=0)
70
+ ax.set_ylim(bottom=0)
71
+ ax.set_xlabel("Time")
72
+ ax.set_ylabel("Signal")
73
+ ax.legend(frameon=frameon)
74
+
75
+
76
+ def _despine(ax: plt.Axes) -> None:
77
+ """Seaborn-style despine: trim spines to tick extent."""
78
+ for side in ("top", "right"):
79
+ ax.spines[side].set_visible(False)
80
+ for side in ("left", "bottom"):
81
+ ax.spines[side].set_position(("outward", 0))
82
+ xt = np.asarray(ax.get_xticks())
83
+ xt = xt[(xt >= ax.get_xlim()[0]) & (xt <= ax.get_xlim()[1])]
84
+ if xt.size:
85
+ ax.spines["bottom"].set_bounds(xt[0], xt[-1])
86
+ yt = np.asarray(ax.get_yticks())
87
+ yt = yt[(yt >= ax.get_ylim()[0]) & (yt <= ax.get_ylim()[1])]
88
+ if yt.size:
89
+ ax.spines["left"].set_bounds(yt[0], yt[-1])
90
+
91
+
92
+ # ── generate data once ──────────────────────────────────────────────────────
93
+ t_axis, means, ses = _generate_data()
94
+
95
+
96
+ # ── single: Charité theme ───────────────────────────────────────────────────
97
+ apply_theme(theme_charite(font_size=14, thickness=1))
98
+
99
+ fig, ax = plt.subplots(figsize=(6, 4.5))
100
+ fig.subplots_adjust(top=0.88)
101
+ fig.suptitle("Charité", fontsize=16, y=0.95, color=PRIME_BLUE)
102
+ fig.text(0.5, 0.90, "mpl Theme", ha="center", va="top", fontsize=9.5, color=TEXT_GREY)
103
+ _plot_curves(ax, t_axis, means, ses)
104
+ _despine(ax)
105
+ fig.savefig(OUT, **SAVE_KW)
106
+ print(f"Saved → {OUT}")
107
+ plt.show()
@@ -50,6 +50,7 @@ nav:
50
50
  - Palettes: palettes.md
51
51
  - Matplotlib: matplotlib.md
52
52
  - Altair: altair.md
53
+ - Citing: citing.md
53
54
  - API Reference:
54
55
  - colors: api/colors.md
55
56
  - palettes: api/palettes.md
@@ -1,129 +0,0 @@
1
- """Matplotlib theme for Charité – Universitätsmedizin Berlin."""
2
-
3
- from contextlib import contextmanager
4
-
5
- from cycler import cycler
6
-
7
- from .colors import TEXT_GREY, PRIME_BLUE, PRIME_LGREY, BLACK
8
- from .palettes import PALETTES
9
- from .fonts import build_font_stack
10
-
11
-
12
- def theme_charite(
13
- font: str | None = None,
14
- font_size: float = 10,
15
- thickness: float = 0.5,
16
- grid: bool = False,
17
- palette: str | list[str] = "primary",
18
- ) -> dict:
19
- """Return matplotlib rcParams dict for the Charité corporate identity theme.
20
-
21
- Parameters
22
- ----------
23
- font:
24
- Preferred font family. Falls back through Charité Text Office →
25
- Charit? Text Office → Calibri → DejaVu Sans → sans-serif if the requested font is not installed.
26
- font_size:
27
- Base font size in points. Defaults to 10 for screen; use 8 for print.
28
- thickness:
29
- Axis line and tick width. Mirrors the R ``thickness`` parameter.
30
- grid:
31
- Show major grid lines. Disabled by default.
32
- palette:
33
- Name of a built-in palette or a list of hex color strings used for the
34
- ``axes.prop_cycle``.
35
- """
36
- colors = PALETTES[palette] if isinstance(palette, str) else list(palette)
37
-
38
- return {
39
- # Font
40
- "font.family": "sans-serif",
41
- "font.sans-serif": build_font_stack(preferred=font),
42
- "font.size": font_size,
43
-
44
- # Spines — only bottom and left visible (mirrors theme_classic in R)
45
- "axes.spines.top": False,
46
- "axes.spines.right": False,
47
-
48
- # Axes
49
- "axes.linewidth": thickness,
50
- "axes.titlesize": round(font_size * 1.2),
51
- "axes.titlecolor": PRIME_BLUE,
52
- "axes.titlepad": 8,
53
- "axes.labelsize": font_size,
54
- "axes.labelcolor": TEXT_GREY,
55
- "axes.edgecolor": BLACK,
56
- "axes.facecolor": "white",
57
- "axes.grid": grid,
58
-
59
- # Grid (only visible when grid=True)
60
- "grid.color": PRIME_LGREY,
61
- "grid.linewidth": thickness * 0.6,
62
- "grid.alpha": 0.8,
63
-
64
- # Ticks
65
- "xtick.color": BLACK,
66
- "ytick.color": BLACK,
67
- "xtick.labelsize": font_size - 1,
68
- "ytick.labelsize": font_size - 1,
69
- "xtick.labelcolor": TEXT_GREY,
70
- "ytick.labelcolor": TEXT_GREY,
71
- "xtick.major.size": 3,
72
- "ytick.major.size": 3,
73
- "xtick.major.width": thickness,
74
- "ytick.major.width": thickness,
75
- "xtick.minor.visible": False,
76
- "ytick.minor.visible": False,
77
-
78
- # Text
79
- "text.color": TEXT_GREY,
80
-
81
- # Figure
82
- "figure.facecolor": "white",
83
- "figure.edgecolor": "white",
84
- "figure.dpi": 100,
85
-
86
- # Lines / markers
87
- "lines.linewidth": 1.5,
88
- "lines.markersize": 6,
89
-
90
- # Patches (bars, etc.)
91
- "patch.linewidth": 0,
92
-
93
- # Legend
94
- "legend.frameon": False,
95
- "legend.fontsize": round(font_size * 0.9),
96
- "legend.title_fontsize": font_size,
97
- "legend.labelcolor": TEXT_GREY,
98
-
99
- # Save
100
- "savefig.dpi": 300,
101
- "savefig.bbox": "tight",
102
- "savefig.transparent": True,
103
-
104
- # Color cycle
105
- "axes.prop_cycle": cycler("color", colors),
106
- }
107
-
108
-
109
- def apply_theme(params: dict) -> None:
110
- """Apply a theme dict as the active matplotlib rcParams (permanent until reset)."""
111
- import matplotlib as mpl
112
- mpl.rcParams.update(params)
113
-
114
-
115
- @contextmanager
116
- def using(params: dict):
117
- """Context manager: temporarily apply *params* then restore previous rcParams.
118
-
119
- Examples
120
- --------
121
- ```python
122
- with using(theme_charite(palette="goldelse")):
123
- fig, ax = plt.subplots()
124
- ax.plot([1, 2, 3])
125
- ```
126
- """
127
- import matplotlib as mpl
128
- with mpl.rc_context(params):
129
- yield
@@ -1,47 +0,0 @@
1
- """Palette preview — generates a swatch strip for every built-in palette.
2
-
3
- Run with:
4
- python examples/palette_preview.py
5
- """
6
-
7
- import sys
8
- import pathlib
9
- sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
10
-
11
- import matplotlib.pyplot as plt
12
- import matplotlib.patches as mpatches
13
-
14
- from charite_plot.mpl_themes import theme_charite, apply_theme
15
- from charite_plot.palettes import PALETTES
16
-
17
- apply_theme(theme_charite())
18
-
19
- MAX_COLS = max(len(p) for p in PALETTES.values())
20
- names = list(PALETTES.keys())
21
-
22
- LABEL_PAD = 0.4 # data-units reserved to the left for labels at font size 12
23
-
24
- fig, axes = plt.subplots(
25
- len(names), 1,
26
- figsize=(7.5, len(names) * 0.58 + 0.3),
27
- gridspec_kw={"hspace": 0},
28
- )
29
-
30
- for ax, name in zip(axes, names):
31
- palette = PALETTES[name]
32
- for i, color in enumerate(palette):
33
- ax.add_patch(mpatches.Rectangle(
34
- (i, 0), 1.0, 0.82,
35
- facecolor=color, edgecolor="none",
36
- ))
37
- ax.set_xlim(-LABEL_PAD, MAX_COLS)
38
- ax.set_ylim(-0.1, 1.0)
39
- ax.text(-LABEL_PAD, 0.41, name, ha="right", va="center", fontsize=12)
40
- ax.axis("off")
41
-
42
-
43
- out = pathlib.Path(__file__).parent.parent / "docs" / "assets" / "palette_preview.png"
44
- out.parent.mkdir(parents=True, exist_ok=True)
45
- fig.savefig(out, dpi=300, bbox_inches="tight", facecolor="white")
46
- print(f"Saved → {out}")
47
- plt.show()
@@ -1,108 +0,0 @@
1
- """Synthetic time-series showcase — generates the charite-plot theme example image.
2
-
3
- Replicates the curves from ``tools/data/simulate_timeseries_data.R`` in the
4
- charite R package: three conditions (wiggly normal, cubic polynomial, square
5
- root) z-scored and plotted with uniform ±SE bands.
6
-
7
- Run with:
8
- python examples/poly_showcase.py
9
- """
10
-
11
- import sys
12
- import pathlib
13
- sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
14
-
15
- import numpy as np
16
- import matplotlib.pyplot as plt
17
-
18
- from charite_plot.mpl_themes import theme_charite, apply_theme
19
- from charite_plot.colors import PRIME_BLUE, PRIME_DGREY, KORALL, TEXT_GREY
20
-
21
- apply_theme(theme_charite(font_size=10))
22
-
23
- # ── reproduce R's simulate_timeseries_data.R (set.seed(42)) ────────────────
24
- # R and numpy use different RNGs so the tiny noise terms differ slightly,
25
- # but they are visually negligible after z-scoring (sd ≤ 0.01).
26
- rng = np.random.default_rng(42)
27
-
28
- n_time = 100
29
- time = np.linspace(0, 1, n_time) # seq(0, 1, length.out = 100)
30
- t_axis = np.arange(1, n_time + 1) # seq(1, 100) — x-axis
31
-
32
-
33
- def _dnorm(x: np.ndarray, mean: float, sd: float) -> np.ndarray:
34
- """Normal PDF — equivalent to R's dnorm()."""
35
- return np.exp(-0.5 * ((x - mean) / sd) ** 2) / (sd * np.sqrt(2 * np.pi))
36
-
37
-
38
- def _scale(x: np.ndarray) -> np.ndarray:
39
- """Center and unit-scale — equivalent to R's scale()."""
40
- return (x - x.mean()) / x.std(ddof=1)
41
-
42
-
43
- # C1: wiggly normal
44
- c1_core = _dnorm(time, mean=0.5, sd=0.15)
45
- c1 = (c1_core
46
- + 0.1 * np.sin(25 * time)
47
- + 0.05 * np.cos(40 * time)
48
- + rng.normal(0, 0.005, n_time))
49
-
50
- # C2: 3rd-order polynomial
51
- c2 = 3.9 * time**3 - 6 * time**2 + 3 * time + rng.normal(0, 0.01, n_time)
52
-
53
- # C3: square root
54
- c3 = time**0.5 + rng.normal(0, 0.005, n_time)
55
-
56
- # z-score and shift (matching the R data frame construction)
57
- means = {
58
- "C1": _scale(c1) + 2.2,
59
- "C2": _scale(c2) + 2.8,
60
- "C3": _scale(c3) + 2.8,
61
- }
62
-
63
- # SE: runif(n_time * 3, min = 0.4, max = 0.6), sliced per condition
64
- se_all = rng.uniform(0.4, 0.6, n_time * 3)
65
- ses = {
66
- "C1": se_all[0 : n_time],
67
- "C2": se_all[n_time : 2 * n_time],
68
- "C3": se_all[2 * n_time :],
69
- }
70
-
71
- # ── plot ───────────────────────────────────────────────────────────────────
72
- colors = [KORALL, PRIME_BLUE, PRIME_DGREY] # C1, C2, C3
73
-
74
- fig, ax = plt.subplots(figsize=(6, 4.5)) # 4:3
75
- fig.subplots_adjust(top=0.88)
76
- fig.suptitle("Charité", fontsize=16, y=0.95, color=PRIME_BLUE)
77
- fig.text(0.5, 0.90, "mpl Theme", ha="center", va="top", fontsize=9.5, color=TEXT_GREY)
78
-
79
- for (label, mean), se, color in zip(means.items(), ses.values(), colors):
80
- ax.fill_between(t_axis, mean - se, mean + se, alpha=0.25, color=color, linewidth=0)
81
- ax.plot(t_axis, mean, lw=2, label=label, color=color)
82
-
83
- ax.set_xlim(left=0)
84
- ax.set_ylim(bottom=0)
85
- ax.set_xlabel("Time")
86
- ax.set_ylabel("Signal")
87
- ax.legend(frameon=False)
88
-
89
- # despine: offset spines outward and trim to tick extent (seaborn-style)
90
- for side in ("left", "bottom"):
91
- ax.spines[side].set_position(("outward", 0))
92
- for side in ("top", "right"):
93
- ax.spines[side].set_visible(False)
94
- xt = np.asarray(ax.get_xticks())
95
- xt = xt[(xt >= ax.get_xlim()[0]) & (xt <= ax.get_xlim()[1])]
96
- if xt.size:
97
- ax.spines["bottom"].set_bounds(xt[0], xt[-1])
98
- yt = np.asarray(ax.get_yticks())
99
- yt = yt[(yt >= ax.get_ylim()[0]) & (yt <= ax.get_ylim()[1])]
100
- if yt.size:
101
- ax.spines["left"].set_bounds(yt[0], yt[-1])
102
-
103
- # ── save ───────────────────────────────────────────────────────────────────
104
- out = pathlib.Path(__file__).parent.parent / "docs" / "assets" / "theme_example.png"
105
- out.parent.mkdir(parents=True, exist_ok=True)
106
- fig.savefig(out, dpi=300, bbox_inches="tight", facecolor="white")
107
- print(f"Saved → {out}")
108
- plt.show()
File without changes
File without changes