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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """datavizlib - An opinionated Python library for building clean, consistent data visualizations."""
2
+
3
+ from datavizlib.charts.base import BaseChart
4
+ from datavizlib.charts.line import LineChart
5
+
6
+ __all__ = ["BaseChart", "LineChart"]
@@ -0,0 +1,4 @@
1
+ from .base import BaseChart
2
+ from .line import LineChart
3
+
4
+ __all__ = ["BaseChart", "LineChart"]
@@ -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,2 @@
1
+ matplotlib>=3.8
2
+ pandas>=2.0
@@ -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