datavizlib 0.1.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.
- datavizlib-0.1.0/LICENSE +21 -0
- datavizlib-0.1.0/PKG-INFO +82 -0
- datavizlib-0.1.0/README.md +43 -0
- datavizlib-0.1.0/pyproject.toml +34 -0
- datavizlib-0.1.0/setup.cfg +4 -0
- datavizlib-0.1.0/src/datavizlib/__init__.py +6 -0
- datavizlib-0.1.0/src/datavizlib/charts/__init__.py +4 -0
- datavizlib-0.1.0/src/datavizlib/charts/base.py +329 -0
- datavizlib-0.1.0/src/datavizlib/charts/line.py +121 -0
- datavizlib-0.1.0/src/datavizlib.egg-info/PKG-INFO +82 -0
- datavizlib-0.1.0/src/datavizlib.egg-info/SOURCES.txt +14 -0
- datavizlib-0.1.0/src/datavizlib.egg-info/dependency_links.txt +1 -0
- datavizlib-0.1.0/src/datavizlib.egg-info/requires.txt +2 -0
- datavizlib-0.1.0/src/datavizlib.egg-info/top_level.txt +1 -0
- datavizlib-0.1.0/tests/test_base.py +50 -0
- datavizlib-0.1.0/tests/test_line.py +75 -0
datavizlib-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philipp Melzer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datavizlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opinionated matplotlib-based library for clean, presentation-ready charts and visual storytelling.
|
|
5
|
+
Author-email: Philipp Melzer <philipp.melzer@icloud.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Philipp Melzer
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/phmelzer/datavizlib
|
|
29
|
+
Keywords: data visualization,matplotlib,charting,plotting
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.8
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: matplotlib>=3.8
|
|
37
|
+
Requires-Dist: pandas>=2.0
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# 📊 datavizlib
|
|
41
|
+
|
|
42
|
+
Opinionated matplotlib-based Python tooling for building clean, consistent, presentation-ready charts.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Install from PyPI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install datavizlib
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or install locally for development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from datavizlib import LineChart
|
|
62
|
+
|
|
63
|
+
chart = LineChart(
|
|
64
|
+
title="Monthly revenue",
|
|
65
|
+
subtitle="Actual vs forecast",
|
|
66
|
+
x_label="Month",
|
|
67
|
+
y_label="Revenue"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
chart.build()
|
|
71
|
+
chart.add_data(df, x="month", y="revenue", linestyle="solid")
|
|
72
|
+
chart.add_data(df, x="month", y="forecast", linestyle="dotted")
|
|
73
|
+
chart.render()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
Contributions are welcome.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
`datavizlib` is released under the MIT License. See `LICENSE` for details.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# 📊 datavizlib
|
|
2
|
+
|
|
3
|
+
Opinionated matplotlib-based Python tooling for building clean, consistent, presentation-ready charts.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install from PyPI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install datavizlib
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install locally for development:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from datavizlib import LineChart
|
|
23
|
+
|
|
24
|
+
chart = LineChart(
|
|
25
|
+
title="Monthly revenue",
|
|
26
|
+
subtitle="Actual vs forecast",
|
|
27
|
+
x_label="Month",
|
|
28
|
+
y_label="Revenue"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
chart.build()
|
|
32
|
+
chart.add_data(df, x="month", y="revenue", linestyle="solid")
|
|
33
|
+
chart.add_data(df, x="month", y="forecast", linestyle="dotted")
|
|
34
|
+
chart.render()
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
39
|
+
Contributions are welcome.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
`datavizlib` is released under the MIT License. See `LICENSE` for details.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "datavizlib"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{name = "Philipp Melzer", email = "philipp.melzer@icloud.com"}
|
|
6
|
+
]
|
|
7
|
+
description = "Opinionated matplotlib-based library for clean, presentation-ready charts and visual storytelling."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
keywords = ["data visualization", "matplotlib", "charting", "plotting"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Operating System :: OS Independent"
|
|
15
|
+
]
|
|
16
|
+
dependencies = [
|
|
17
|
+
"matplotlib>=3.8",
|
|
18
|
+
"pandas>=2.0"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
license = {file = "LICENSE"}
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Repository = "https://github.com/phmelzer/datavizlib"
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BaseChart - Base class for building consistent charts.
|
|
3
|
+
|
|
4
|
+
A minimal, opinionated layer on top of matplotlib that helps create
|
|
5
|
+
charts that are easy to read and quick to understand.
|
|
6
|
+
|
|
7
|
+
It enforces a consistent layout and visual structure, focusing on alignment, hierarchy,
|
|
8
|
+
and clarity.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseChart:
|
|
15
|
+
"""
|
|
16
|
+
Base chart class enforcing a clear and consistent layout for data visualizations.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# ALDI color palette
|
|
20
|
+
ALDI_PALETTE = [
|
|
21
|
+
"#00005F", # Navy
|
|
22
|
+
"#FF7800", # Orange
|
|
23
|
+
"#FFC800", # Yellow
|
|
24
|
+
"#55C3F0", # Light blue
|
|
25
|
+
"#D70000", # Red
|
|
26
|
+
"#439538", # Green
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
FONT_SIZES = {
|
|
30
|
+
"title": 14,
|
|
31
|
+
"subtitle": 10,
|
|
32
|
+
"axis_label": 10,
|
|
33
|
+
"tick_label": 10,
|
|
34
|
+
"footnote": 8,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __init__(self, title, x_label, y_label, footnote, subtitle=None):
|
|
38
|
+
"""
|
|
39
|
+
Initialize chart with required elements.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
title (str)
|
|
43
|
+
subtitle (str, optional)
|
|
44
|
+
x_label (str)
|
|
45
|
+
y_label (str)
|
|
46
|
+
footnote (str)
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If required parameters are missing.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
self._validate_inputs(title, x_label, y_label, footnote)
|
|
53
|
+
|
|
54
|
+
self.fig, self.ax = plt.subplots()
|
|
55
|
+
|
|
56
|
+
self.title = title
|
|
57
|
+
self.subtitle = subtitle
|
|
58
|
+
self.x_label = x_label
|
|
59
|
+
self.y_label = y_label
|
|
60
|
+
self.footnote = footnote
|
|
61
|
+
|
|
62
|
+
self._configure_layout()
|
|
63
|
+
|
|
64
|
+
self.color_palette = self.ALDI_PALETTE.copy()
|
|
65
|
+
|
|
66
|
+
# Grey as background color leaving black and other colors for stronger contrast
|
|
67
|
+
self.background_color = "#808080"
|
|
68
|
+
|
|
69
|
+
self.black_color = "#000000"
|
|
70
|
+
|
|
71
|
+
def _validate_inputs(self, title, x_label, y_label, footnote):
|
|
72
|
+
"""Ensure required elements are provided."""
|
|
73
|
+
if not title:
|
|
74
|
+
raise ValueError("title is required")
|
|
75
|
+
if not x_label or not y_label:
|
|
76
|
+
raise ValueError("x_label and y_label are required")
|
|
77
|
+
if not footnote:
|
|
78
|
+
raise ValueError("footnote is required")
|
|
79
|
+
|
|
80
|
+
def _configure_layout(self):
|
|
81
|
+
"""
|
|
82
|
+
Configure layout with intentional white space.
|
|
83
|
+
|
|
84
|
+
Ensures that titles and subtitles have sufficient space and do not
|
|
85
|
+
overlap with the chart area.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if self.subtitle:
|
|
89
|
+
top_margin = 0.80 # More space for subtitle
|
|
90
|
+
else:
|
|
91
|
+
top_margin = 0.88 # Less space needed
|
|
92
|
+
|
|
93
|
+
self.fig.subplots_adjust(
|
|
94
|
+
left=0.0,
|
|
95
|
+
right=1.0,
|
|
96
|
+
top=top_margin,
|
|
97
|
+
bottom=0.15,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _get_color_palette(self):
|
|
101
|
+
"""Return the chart color palette."""
|
|
102
|
+
return self.color_palette
|
|
103
|
+
|
|
104
|
+
def _get_next_color(self, index=0):
|
|
105
|
+
"""Return a color from the palette."""
|
|
106
|
+
return self.color_palette[index % len(self.color_palette)]
|
|
107
|
+
|
|
108
|
+
def add_data(self, df, **kwargs):
|
|
109
|
+
"""
|
|
110
|
+
Add data to the chart.
|
|
111
|
+
|
|
112
|
+
This is a standard interface for all chart types. Subclasses must
|
|
113
|
+
override this method to implement their specific data handling logic.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
df: pandas DataFrame containing the data
|
|
117
|
+
**kwargs: Chart-specific arguments (e.g., x, y for LineChart)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
self for method chaining
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
NotImplementedError: Always (must be overridden by subclass)
|
|
124
|
+
"""
|
|
125
|
+
raise NotImplementedError("Subclasses must implement add_data()")
|
|
126
|
+
|
|
127
|
+
def build(self):
|
|
128
|
+
"""
|
|
129
|
+
Assemble chart following best practices.
|
|
130
|
+
|
|
131
|
+
Steps:
|
|
132
|
+
1. Remove visual clutter
|
|
133
|
+
2. Add and style axis labels
|
|
134
|
+
3. Align labels for readability
|
|
135
|
+
4. Add title, subtitle, and footnote
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
tuple: (Figure, Axes)
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Remove unnecessary spines for better readability
|
|
142
|
+
self._clean_spines()
|
|
143
|
+
|
|
144
|
+
# Add axis labels with proper styling
|
|
145
|
+
self._add_axis_labels()
|
|
146
|
+
|
|
147
|
+
# Style tick labels
|
|
148
|
+
self.ax.tick_params(
|
|
149
|
+
axis="both",
|
|
150
|
+
colors=self.background_color,
|
|
151
|
+
labelsize=self.FONT_SIZES["tick_label"],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Force layout calculation
|
|
155
|
+
self.fig.canvas.draw()
|
|
156
|
+
|
|
157
|
+
# Align labels to axis boundaries
|
|
158
|
+
self._align_y_label_to_tick()
|
|
159
|
+
self._align_x_label_to_tick()
|
|
160
|
+
|
|
161
|
+
# Re-draw after label movement
|
|
162
|
+
self.fig.canvas.draw()
|
|
163
|
+
|
|
164
|
+
# Add title elements
|
|
165
|
+
self._add_title()
|
|
166
|
+
self._add_subtitle()
|
|
167
|
+
self._add_footnote()
|
|
168
|
+
|
|
169
|
+
return self.fig, self.ax
|
|
170
|
+
|
|
171
|
+
def _clean_spines(self):
|
|
172
|
+
"""
|
|
173
|
+
Remove unnecessary spines.
|
|
174
|
+
|
|
175
|
+
Reduces cognitive load and keeps the focus on the data.
|
|
176
|
+
"""
|
|
177
|
+
self.ax.spines["top"].set_visible(False)
|
|
178
|
+
self.ax.spines["right"].set_visible(False)
|
|
179
|
+
self.ax.spines["left"].set_color(self.background_color)
|
|
180
|
+
self.ax.spines["bottom"].set_color(self.background_color)
|
|
181
|
+
|
|
182
|
+
def _add_axis_labels(self):
|
|
183
|
+
"""
|
|
184
|
+
Add axis labels.
|
|
185
|
+
|
|
186
|
+
Labels are styled and positioned to be easy to locate and read quickly.
|
|
187
|
+
"""
|
|
188
|
+
self.ax.set_xlabel(
|
|
189
|
+
self.x_label.upper(),
|
|
190
|
+
rotation=0,
|
|
191
|
+
ha="left",
|
|
192
|
+
va="top",
|
|
193
|
+
fontsize=self.FONT_SIZES["axis_label"],
|
|
194
|
+
color=self.background_color,
|
|
195
|
+
)
|
|
196
|
+
self.ax.xaxis.label.set_color(self.background_color)
|
|
197
|
+
|
|
198
|
+
self.ax.set_ylabel(
|
|
199
|
+
self.y_label.upper(),
|
|
200
|
+
rotation=90,
|
|
201
|
+
ha="left",
|
|
202
|
+
va="top",
|
|
203
|
+
fontsize=self.FONT_SIZES["axis_label"],
|
|
204
|
+
color=self.background_color,
|
|
205
|
+
)
|
|
206
|
+
self.ax.yaxis.label.set_color(self.background_color)
|
|
207
|
+
|
|
208
|
+
def _align_y_label_to_tick(self):
|
|
209
|
+
"""
|
|
210
|
+
Align y-label top with the top of the y-axis.
|
|
211
|
+
|
|
212
|
+
The label's top edge aligns with where the axis line ends,
|
|
213
|
+
improving scanability and reducing unnecessary eye movement.
|
|
214
|
+
"""
|
|
215
|
+
renderer = self.fig.canvas.get_renderer()
|
|
216
|
+
|
|
217
|
+
# Place at a known reference point to measure offset
|
|
218
|
+
ref_y = 0.5
|
|
219
|
+
self.ax.yaxis.set_label_coords(-0.10, ref_y)
|
|
220
|
+
self.fig.canvas.draw()
|
|
221
|
+
|
|
222
|
+
# Measure where the top of the label ended up in axes coords
|
|
223
|
+
bbox = self.ax.yaxis.label.get_window_extent(renderer=renderer)
|
|
224
|
+
bbox_axes = bbox.transformed(self.ax.transAxes.inverted())
|
|
225
|
+
|
|
226
|
+
# Shift so the label's top (y1) aligns with axis top (1.0)
|
|
227
|
+
shift = 1.0 - bbox_axes.y1
|
|
228
|
+
self.ax.yaxis.set_label_coords(-0.10, ref_y + shift)
|
|
229
|
+
|
|
230
|
+
def _align_x_label_to_tick(self):
|
|
231
|
+
"""
|
|
232
|
+
Align x-label left with the start of the x-axis.
|
|
233
|
+
|
|
234
|
+
The label's left edge aligns with where the axis line begins,
|
|
235
|
+
supporting natural left-to-right reading flow.
|
|
236
|
+
"""
|
|
237
|
+
renderer = self.fig.canvas.get_renderer()
|
|
238
|
+
|
|
239
|
+
# Place at a known reference point to measure offset
|
|
240
|
+
ref_x = 0.5
|
|
241
|
+
self.ax.xaxis.set_label_coords(ref_x, -0.10)
|
|
242
|
+
self.fig.canvas.draw()
|
|
243
|
+
|
|
244
|
+
# Measure where the left of the label ended up in axes coords
|
|
245
|
+
bbox = self.ax.xaxis.label.get_window_extent(renderer=renderer)
|
|
246
|
+
bbox_axes = bbox.transformed(self.ax.transAxes.inverted())
|
|
247
|
+
|
|
248
|
+
# Shift so the label's left (x0) aligns with axis start (0.0)
|
|
249
|
+
shift = 0.0 - bbox_axes.x0
|
|
250
|
+
self.ax.xaxis.set_label_coords(ref_x + shift, -0.10)
|
|
251
|
+
|
|
252
|
+
def _get_ylabel_bbox(self):
|
|
253
|
+
"""Get y-label bounding box in figure coordinates."""
|
|
254
|
+
renderer = self.fig.canvas.get_renderer()
|
|
255
|
+
bbox = self.ax.yaxis.label.get_window_extent(renderer=renderer)
|
|
256
|
+
bbox_fig = bbox.transformed(self.fig.transFigure.inverted())
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
"x0": bbox_fig.x0,
|
|
260
|
+
"x1": bbox_fig.x1,
|
|
261
|
+
"y0": bbox_fig.y0,
|
|
262
|
+
"y1": bbox_fig.y1,
|
|
263
|
+
"width": bbox_fig.width,
|
|
264
|
+
"height": bbox_fig.height,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
def _add_title(self):
|
|
268
|
+
"""
|
|
269
|
+
Add main title.
|
|
270
|
+
|
|
271
|
+
The title should communicate the key message or insight,
|
|
272
|
+
not just describe the data.
|
|
273
|
+
"""
|
|
274
|
+
ylabel_bbox = self._get_ylabel_bbox()
|
|
275
|
+
|
|
276
|
+
# Position based on whether subtitle exists
|
|
277
|
+
y_pos = 0.91 if self.subtitle else 0.94
|
|
278
|
+
|
|
279
|
+
self.title_bbox = self.fig.text(
|
|
280
|
+
ylabel_bbox["x0"],
|
|
281
|
+
y_pos,
|
|
282
|
+
self.title,
|
|
283
|
+
fontsize=self.FONT_SIZES["title"],
|
|
284
|
+
ha="left",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def _add_subtitle(self):
|
|
288
|
+
"""
|
|
289
|
+
Add subtitle below title.
|
|
290
|
+
|
|
291
|
+
Provides context without overloading the main message.
|
|
292
|
+
"""
|
|
293
|
+
if not self.subtitle:
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
ylabel_bbox = self._get_ylabel_bbox()
|
|
297
|
+
|
|
298
|
+
# Fixed offset below title
|
|
299
|
+
self.fig.text(
|
|
300
|
+
ylabel_bbox["x0"],
|
|
301
|
+
0.86,
|
|
302
|
+
self.subtitle,
|
|
303
|
+
fontsize=self.FONT_SIZES["subtitle"],
|
|
304
|
+
ha="left",
|
|
305
|
+
color=self.background_color,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
def _add_footnote(self):
|
|
309
|
+
"""
|
|
310
|
+
Add footnote below chart.
|
|
311
|
+
|
|
312
|
+
Aligns with the start of the x-axis for visual consistency.
|
|
313
|
+
Should include data source and assumptions.
|
|
314
|
+
"""
|
|
315
|
+
if not self.footnote:
|
|
316
|
+
return
|
|
317
|
+
|
|
318
|
+
# Get the x-axis start (x=0 in axes coords) in figure coordinates
|
|
319
|
+
x_display, _ = self.ax.transAxes.transform((0, 0))
|
|
320
|
+
x_fig, _ = self.fig.transFigure.inverted().transform((x_display, 0))
|
|
321
|
+
|
|
322
|
+
self.fig.text(
|
|
323
|
+
x_fig,
|
|
324
|
+
0.00,
|
|
325
|
+
self.footnote,
|
|
326
|
+
fontsize=self.FONT_SIZES["footnote"],
|
|
327
|
+
ha="left",
|
|
328
|
+
color=self.background_color,
|
|
329
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from datavizlib.charts.base import BaseChart
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LineChart(BaseChart):
|
|
6
|
+
"""Line chart implementation for visualizing time series or continuous data."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, title, x_label, y_label, footnote, subtitle=None):
|
|
9
|
+
super().__init__(title, x_label, y_label, footnote, subtitle)
|
|
10
|
+
|
|
11
|
+
self._lines = []
|
|
12
|
+
|
|
13
|
+
def add_data(
|
|
14
|
+
self,
|
|
15
|
+
df,
|
|
16
|
+
x,
|
|
17
|
+
y,
|
|
18
|
+
linestyle="solid",
|
|
19
|
+
color=None,
|
|
20
|
+
linewidth=2,
|
|
21
|
+
marker=None,
|
|
22
|
+
markersize=6,
|
|
23
|
+
alpha=1.0,
|
|
24
|
+
label=None,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Add a line to the chart.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
df: pandas DataFrame containing the data
|
|
31
|
+
x: column name for x-axis
|
|
32
|
+
y: column name for y-axis
|
|
33
|
+
linestyle: line style - "solid", "dashed", "dotted", "dashdot" (default: "solid")
|
|
34
|
+
color: hex color code (e.g., "#FF0000") or None to use palette color (default: None)
|
|
35
|
+
linewidth: line thickness in points (default: 2)
|
|
36
|
+
marker: marker style - "o" (circle), "s" (square), "^" (triangle), "D" (diamond),
|
|
37
|
+
"*" (star), "x" (X), "+" (plus), None (no marker) (default: None)
|
|
38
|
+
markersize: size of markers in points (default: 6)
|
|
39
|
+
alpha: transparency from 0.0 (invisible) to 1.0 (opaque) (default: 1.0)
|
|
40
|
+
label: label for legend (default: None, no legend entry)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
self for method chaining
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: if df is not a DataFrame or missing required columns
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if not isinstance(df, pd.DataFrame):
|
|
50
|
+
raise ValueError("df must be a pandas DataFrame")
|
|
51
|
+
|
|
52
|
+
if x not in df.columns:
|
|
53
|
+
raise ValueError(f"Column '{x}' not found in DataFrame")
|
|
54
|
+
if y not in df.columns:
|
|
55
|
+
raise ValueError(f"Column '{y}' not found in DataFrame")
|
|
56
|
+
|
|
57
|
+
if color is None:
|
|
58
|
+
color = self._get_next_color(len(self._lines))
|
|
59
|
+
|
|
60
|
+
self._lines.append(
|
|
61
|
+
{
|
|
62
|
+
"df": df,
|
|
63
|
+
"x": x,
|
|
64
|
+
"y": y,
|
|
65
|
+
"linestyle": linestyle,
|
|
66
|
+
"color": color,
|
|
67
|
+
"linewidth": linewidth,
|
|
68
|
+
"marker": marker,
|
|
69
|
+
"markersize": markersize,
|
|
70
|
+
"alpha": alpha,
|
|
71
|
+
"label": label,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
def render(self):
|
|
78
|
+
"""
|
|
79
|
+
Render the line chart with all added lines.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
tuple: (Figure, Axes)
|
|
83
|
+
"""
|
|
84
|
+
if not self._lines:
|
|
85
|
+
raise ValueError("No data added. Call add_data() first.")
|
|
86
|
+
|
|
87
|
+
# Track if any lines have labels for legend
|
|
88
|
+
has_labels = any(line.get("label") for line in self._lines)
|
|
89
|
+
|
|
90
|
+
for line in self._lines:
|
|
91
|
+
df = line["df"]
|
|
92
|
+
x_col = line["x"]
|
|
93
|
+
y_col = line["y"]
|
|
94
|
+
linestyle = line["linestyle"]
|
|
95
|
+
color = line["color"]
|
|
96
|
+
linewidth = line["linewidth"]
|
|
97
|
+
marker = line["marker"]
|
|
98
|
+
markersize = line["markersize"]
|
|
99
|
+
alpha = line["alpha"]
|
|
100
|
+
label = line.get("label")
|
|
101
|
+
|
|
102
|
+
x_data = df[x_col]
|
|
103
|
+
y_data = df[y_col]
|
|
104
|
+
|
|
105
|
+
self.ax.plot(
|
|
106
|
+
x_data,
|
|
107
|
+
y_data,
|
|
108
|
+
color=color,
|
|
109
|
+
linestyle=linestyle,
|
|
110
|
+
linewidth=linewidth,
|
|
111
|
+
marker=marker,
|
|
112
|
+
markersize=markersize,
|
|
113
|
+
alpha=alpha,
|
|
114
|
+
label=label,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Add legend if any lines have labels
|
|
118
|
+
if has_labels:
|
|
119
|
+
self.ax.legend(loc="best", frameon=False)
|
|
120
|
+
|
|
121
|
+
return self.build()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: datavizlib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Opinionated matplotlib-based library for clean, presentation-ready charts and visual storytelling.
|
|
5
|
+
Author-email: Philipp Melzer <philipp.melzer@icloud.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Philipp Melzer
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Repository, https://github.com/phmelzer/datavizlib
|
|
29
|
+
Keywords: data visualization,matplotlib,charting,plotting
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: Operating System :: OS Independent
|
|
33
|
+
Requires-Python: >=3.8
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: matplotlib>=3.8
|
|
37
|
+
Requires-Dist: pandas>=2.0
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# 📊 datavizlib
|
|
41
|
+
|
|
42
|
+
Opinionated matplotlib-based Python tooling for building clean, consistent, presentation-ready charts.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
Install from PyPI:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install datavizlib
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or install locally for development:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from datavizlib import LineChart
|
|
62
|
+
|
|
63
|
+
chart = LineChart(
|
|
64
|
+
title="Monthly revenue",
|
|
65
|
+
subtitle="Actual vs forecast",
|
|
66
|
+
x_label="Month",
|
|
67
|
+
y_label="Revenue"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
chart.build()
|
|
71
|
+
chart.add_data(df, x="month", y="revenue", linestyle="solid")
|
|
72
|
+
chart.add_data(df, x="month", y="forecast", linestyle="dotted")
|
|
73
|
+
chart.render()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
Contributions are welcome.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
`datavizlib` is released under the MIT License. See `LICENSE` for details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/datavizlib/__init__.py
|
|
5
|
+
src/datavizlib.egg-info/PKG-INFO
|
|
6
|
+
src/datavizlib.egg-info/SOURCES.txt
|
|
7
|
+
src/datavizlib.egg-info/dependency_links.txt
|
|
8
|
+
src/datavizlib.egg-info/requires.txt
|
|
9
|
+
src/datavizlib.egg-info/top_level.txt
|
|
10
|
+
src/datavizlib/charts/__init__.py
|
|
11
|
+
src/datavizlib/charts/base.py
|
|
12
|
+
src/datavizlib/charts/line.py
|
|
13
|
+
tests/test_base.py
|
|
14
|
+
tests/test_line.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
datavizlib
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# filepath: tests/test_base.py
|
|
2
|
+
"""Lean tests for BaseChart class."""
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from datavizlib.charts.base import BaseChart
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def base_chart():
|
|
11
|
+
return BaseChart(title="Test", x_label="X", y_label="Y", footnote="Source")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_constructor_and_attrs(base_chart):
|
|
15
|
+
assert base_chart.title == "Test"
|
|
16
|
+
assert base_chart.x_label == "X"
|
|
17
|
+
assert base_chart.y_label == "Y"
|
|
18
|
+
assert base_chart.footnote == "Source"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_optional_subtitle():
|
|
22
|
+
chart = BaseChart(title="T", x_label="X", y_label="Y", footnote="S", subtitle="Sub")
|
|
23
|
+
assert chart.subtitle == "Sub"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_validation_errors():
|
|
27
|
+
with pytest.raises(ValueError, match="title"):
|
|
28
|
+
BaseChart(title="", x_label="X", y_label="Y", footnote="S")
|
|
29
|
+
|
|
30
|
+
with pytest.raises(ValueError, match="x_label"):
|
|
31
|
+
BaseChart(title="T", x_label="", y_label="Y", footnote="S")
|
|
32
|
+
|
|
33
|
+
with pytest.raises(ValueError, match="y_label"):
|
|
34
|
+
BaseChart(title="T", x_label="X", y_label="", footnote="S")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_color_palette_and_helpers(base_chart):
|
|
38
|
+
assert getattr(base_chart, "color_palette")
|
|
39
|
+
assert len(base_chart.color_palette) >= 1
|
|
40
|
+
assert base_chart._get_color_palette() == base_chart.color_palette
|
|
41
|
+
assert base_chart._get_next_color(0) == base_chart.color_palette[0]
|
|
42
|
+
assert (
|
|
43
|
+
base_chart._get_next_color(len(base_chart.color_palette))
|
|
44
|
+
== base_chart.color_palette[0]
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_build_returns_fig_and_ax(base_chart):
|
|
49
|
+
fig, ax = base_chart.build()
|
|
50
|
+
assert fig is not None and ax is not None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# filepath: tests/test_line.py
|
|
2
|
+
"""Lean tests for LineChart class."""
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from datavizlib.charts.line import LineChart
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def sample_data():
|
|
12
|
+
return pd.DataFrame({"year": [2020, 2021, 2022], "sales": [100, 150, 130]})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def line_chart():
|
|
17
|
+
return LineChart(title="Test", x_label="X", y_label="Y", footnote="Source")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_constructor(line_chart):
|
|
21
|
+
assert line_chart.title == "Test"
|
|
22
|
+
assert getattr(line_chart, "_lines", []) == []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_add_data_basic(line_chart, sample_data):
|
|
26
|
+
result = line_chart.add_data(sample_data, x="year", y="sales")
|
|
27
|
+
assert result is line_chart
|
|
28
|
+
entry = line_chart._lines[0]
|
|
29
|
+
assert entry["df"] is sample_data
|
|
30
|
+
assert entry["x"] == "year" and entry["y"] == "sales"
|
|
31
|
+
assert entry["linestyle"] == "solid"
|
|
32
|
+
assert entry["color"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("linestyle,color", [("dotted", None), ("solid", "#FF0000")])
|
|
36
|
+
def test_add_data_styles(line_chart, sample_data, linestyle, color):
|
|
37
|
+
line_chart.add_data(
|
|
38
|
+
sample_data, x="year", y="sales", linestyle=linestyle, color=color
|
|
39
|
+
)
|
|
40
|
+
entry = line_chart._lines[-1]
|
|
41
|
+
assert entry["linestyle"] == linestyle
|
|
42
|
+
if color:
|
|
43
|
+
assert entry["color"] == color
|
|
44
|
+
else:
|
|
45
|
+
assert entry["color"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_add_data_validation(line_chart):
|
|
49
|
+
with pytest.raises(ValueError):
|
|
50
|
+
line_chart.add_data([1, 2, 3], x="a", y="b")
|
|
51
|
+
|
|
52
|
+
with pytest.raises(ValueError):
|
|
53
|
+
line_chart.add_data(pd.DataFrame({"a": [1]}), x="invalid", y="a")
|
|
54
|
+
|
|
55
|
+
with pytest.raises(ValueError):
|
|
56
|
+
line_chart.add_data(pd.DataFrame({"a": [1]}), x="a", y="invalid")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_render_flow(line_chart, sample_data):
|
|
60
|
+
with pytest.raises(ValueError):
|
|
61
|
+
line_chart.render()
|
|
62
|
+
|
|
63
|
+
line_chart.build()
|
|
64
|
+
line_chart.add_data(sample_data, x="year", y="sales")
|
|
65
|
+
fig, ax = line_chart.render()
|
|
66
|
+
assert fig is not None and ax is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_multiple_lines_render(line_chart, sample_data):
|
|
70
|
+
line_chart.add_data(sample_data, x="year", y="sales")
|
|
71
|
+
line_chart.add_data(
|
|
72
|
+
sample_data, x="year", y="sales", linestyle="dotted", color="#FF0000"
|
|
73
|
+
)
|
|
74
|
+
fig, ax = line_chart.render()
|
|
75
|
+
assert len(ax.lines) == 2
|