MatplotLibAPI 3.2.18__tar.gz → 3.2.20__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.
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/.github/workflows/ci.yml +2 -2
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/BoxViolin.py +4 -2
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Wordcloud.py +79 -36
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/__init__.py +7 -1
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/PKG-INFO +1 -1
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/pyproject.toml +1 -1
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/scripts/generate_sample_data.py +1 -8
- matplotlibapi-3.2.20/tests/__init__.py +1 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/conftest.py +3 -5
- matplotlibapi-3.2.20/tests/test_wordcloud.py +77 -0
- matplotlibapi-3.2.18/tests/__init__.py +0 -0
- matplotlibapi-3.2.18/tests/test_wordcloud.py +0 -17
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/.github/workflows/python-publish.yml +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/.gitignore +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/AGENTS.md +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/LICENSE +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Area.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Bar.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Bubble.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Composite.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Heatmap.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Histogram.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Network.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Pie.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Pivot.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Sankey.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/StyleTemplate.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Sunburst.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Table.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Timeserie.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Treemap.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/Waffle.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/_typing.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/_visualization_utils.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/MatplotLibAPI/accessor.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/README.md +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/SECURITY.md +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/SUGGESTIONS.md +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_area.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_bar.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_box_violin.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_bubble.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_dependencies.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_heatmap.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_histogram.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_network.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_pie.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_pivot.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_sankey.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_smoke.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_sunburst.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_table.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_timeseries.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_treemap.py +0 -0
- {matplotlibapi-3.2.18 → matplotlibapi-3.2.20}/tests/test_waffle.py +0 -0
|
@@ -31,12 +31,12 @@ jobs:
|
|
|
31
31
|
|
|
32
32
|
- name: Run style checks
|
|
33
33
|
run: |
|
|
34
|
-
pydocstyle
|
|
34
|
+
pydocstyle .
|
|
35
35
|
black --check .
|
|
36
36
|
|
|
37
37
|
- name: Run static type analysis
|
|
38
38
|
run: |
|
|
39
|
-
pyright
|
|
39
|
+
pyright .
|
|
40
40
|
|
|
41
41
|
- name: Run tests with coverage
|
|
42
42
|
run: |
|
|
@@ -33,10 +33,12 @@ def aplot_box_violin(
|
|
|
33
33
|
validate_dataframe(pd_df, cols=cols)
|
|
34
34
|
plot_ax = _get_axis(ax)
|
|
35
35
|
|
|
36
|
+
common_kwargs = {"data": pd_df, "x": by, "y": column, "palette": style.palette}
|
|
37
|
+
|
|
36
38
|
if violin:
|
|
37
|
-
sns.violinplot(
|
|
39
|
+
sns.violinplot(**common_kwargs, hue=by, legend=False, ax=plot_ax)
|
|
38
40
|
else:
|
|
39
|
-
sns.boxplot(
|
|
41
|
+
sns.boxplot(**common_kwargs, hue=by, legend=False, ax=plot_ax)
|
|
40
42
|
|
|
41
43
|
plot_ax.set_facecolor(style.background_color)
|
|
42
44
|
plot_ax.set_ylabel(string_formatter(column))
|
|
@@ -25,33 +25,6 @@ WORDCLOUD_STYLE_TEMPLATE = StyleTemplate(
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def _normalize_weights(weights: Sequence[float], base_size: int) -> np.ndarray:
|
|
29
|
-
"""Normalize weights to a range of font sizes.
|
|
30
|
-
|
|
31
|
-
Parameters
|
|
32
|
-
----------
|
|
33
|
-
weights : Sequence[float]
|
|
34
|
-
Sequence of weights representing word importance.
|
|
35
|
-
base_size : int
|
|
36
|
-
Base font size used as the lower bound for scaling.
|
|
37
|
-
|
|
38
|
-
Returns
|
|
39
|
-
-------
|
|
40
|
-
numpy.ndarray
|
|
41
|
-
Array of font sizes corresponding to the provided weights.
|
|
42
|
-
"""
|
|
43
|
-
numeric_weights = np.asarray(weights, dtype=float)
|
|
44
|
-
if numeric_weights.size == 0:
|
|
45
|
-
return np.array([], dtype=float)
|
|
46
|
-
min_weight = numeric_weights.min()
|
|
47
|
-
max_weight = numeric_weights.max()
|
|
48
|
-
if min_weight == max_weight:
|
|
49
|
-
return np.full_like(numeric_weights, fill_value=base_size, dtype=float)
|
|
50
|
-
|
|
51
|
-
min_size, max_size = base_size, base_size * 4
|
|
52
|
-
return np.interp(numeric_weights, (min_weight, max_weight), (min_size, max_size))
|
|
53
|
-
|
|
54
|
-
|
|
55
28
|
def _filter_stopwords(
|
|
56
29
|
words: Iterable[str], stopwords: Optional[Iterable[str]]
|
|
57
30
|
) -> np.ndarray:
|
|
@@ -131,6 +104,40 @@ def _prepare_word_frequencies(
|
|
|
131
104
|
return words, weights
|
|
132
105
|
|
|
133
106
|
|
|
107
|
+
def create_circular_mask(size: int = 300, radius: Optional[int] = None) -> np.ndarray:
|
|
108
|
+
"""Construct a binary mask with a circular opening for a word cloud.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
size : int, optional
|
|
113
|
+
Width and height of the mask in pixels. Defaults to ``300``.
|
|
114
|
+
radius : int, optional
|
|
115
|
+
Radius of the circular opening in pixels. Defaults to ``size // 2``.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
numpy.ndarray
|
|
120
|
+
Two-dimensional array suitable for the ``mask`` argument of
|
|
121
|
+
``wordcloud.WordCloud`` where ``0`` values define the drawable region.
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
ValueError
|
|
126
|
+
If ``size`` or ``radius`` are non-positive.
|
|
127
|
+
"""
|
|
128
|
+
if size <= 0:
|
|
129
|
+
raise ValueError("size must be a positive integer.")
|
|
130
|
+
|
|
131
|
+
resolved_radius = radius if radius is not None else size // 2
|
|
132
|
+
if resolved_radius <= 0:
|
|
133
|
+
raise ValueError("radius must be a positive integer.")
|
|
134
|
+
|
|
135
|
+
center = (size - 1) / 2
|
|
136
|
+
x, y = np.ogrid[:size, :size]
|
|
137
|
+
mask_region = (x - center) ** 2 + (y - center) ** 2 > resolved_radius**2
|
|
138
|
+
return 255 * mask_region.astype(np.uint8)
|
|
139
|
+
|
|
140
|
+
|
|
134
141
|
def _plot_words(
|
|
135
142
|
ax: Axes,
|
|
136
143
|
words: Sequence[str],
|
|
@@ -138,6 +145,7 @@ def _plot_words(
|
|
|
138
145
|
style: StyleTemplate,
|
|
139
146
|
title: Optional[str],
|
|
140
147
|
random_state: Optional[int],
|
|
148
|
+
mask: Optional[np.ndarray],
|
|
141
149
|
) -> Axes:
|
|
142
150
|
"""Render words on the provided axes with sizes proportional to weights.
|
|
143
151
|
|
|
@@ -179,25 +187,39 @@ def _plot_words(
|
|
|
179
187
|
|
|
180
188
|
canvas.draw()
|
|
181
189
|
ax_bbox = ax.get_window_extent()
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
resolved_mask = create_circular_mask() if mask is None else np.asarray(mask)
|
|
191
|
+
if resolved_mask is not None and resolved_mask.ndim != 2:
|
|
192
|
+
raise ValueError("mask must be a 2D array.")
|
|
193
|
+
|
|
194
|
+
width = (
|
|
195
|
+
max(int(ax_bbox.width), 1)
|
|
196
|
+
if resolved_mask is None
|
|
197
|
+
else max(int(resolved_mask.shape[1]), 1)
|
|
198
|
+
)
|
|
199
|
+
height = (
|
|
200
|
+
max(int(ax_bbox.height), 1)
|
|
201
|
+
if resolved_mask is None
|
|
202
|
+
else max(int(resolved_mask.shape[0]), 1)
|
|
203
|
+
)
|
|
184
204
|
|
|
185
205
|
frequency_map = {
|
|
186
206
|
string_formatter(word): weight for word, weight in zip(words, weights)
|
|
187
207
|
}
|
|
208
|
+
min_font_size = style.font_mapping[min(style.font_mapping.keys())]
|
|
209
|
+
max_font_size = style.font_mapping[max(style.font_mapping.keys())]
|
|
188
210
|
|
|
189
|
-
font_sizes = _normalize_weights(weights, base_size=style.font_size)
|
|
190
211
|
wc = WordCloud(
|
|
191
212
|
width=width,
|
|
192
213
|
height=height,
|
|
193
214
|
background_color=style.background_color,
|
|
194
215
|
colormap=colormaps.get_cmap(style.palette),
|
|
195
|
-
min_font_size=
|
|
196
|
-
max_font_size=
|
|
216
|
+
min_font_size=min_font_size,
|
|
217
|
+
max_font_size=max_font_size,
|
|
197
218
|
random_state=random_state,
|
|
219
|
+
mask=resolved_mask,
|
|
198
220
|
).generate_from_frequencies(frequency_map)
|
|
199
221
|
|
|
200
|
-
ax.imshow(wc, interpolation="bilinear")
|
|
222
|
+
ax.imshow(wc.to_array(), interpolation="bilinear")
|
|
201
223
|
|
|
202
224
|
if title:
|
|
203
225
|
ax.set_title(title, color=style.font_color, fontsize=style.font_size * 1.5)
|
|
@@ -213,7 +235,8 @@ def aplot_wordcloud(
|
|
|
213
235
|
max_words: int = MAX_RESULTS,
|
|
214
236
|
stopwords: Optional[Iterable[str]] = None,
|
|
215
237
|
random_state: Optional[int] = None,
|
|
216
|
-
ax: Optional[Axes] = None,
|
|
238
|
+
ax: Optional[Axes | np.ndarray[Any, np.dtype[Any]]] = None,
|
|
239
|
+
mask: Optional[np.ndarray] = None,
|
|
217
240
|
) -> Axes:
|
|
218
241
|
"""Plot a word cloud on the provided axes.
|
|
219
242
|
|
|
@@ -235,8 +258,11 @@ def aplot_wordcloud(
|
|
|
235
258
|
Words to exclude from the visualization. Defaults to ``None``.
|
|
236
259
|
random_state : int, optional
|
|
237
260
|
Seed for word placement. Defaults to ``None``.
|
|
238
|
-
ax : matplotlib.axes.Axes, optional
|
|
261
|
+
ax : matplotlib.axes.Axes or numpy.ndarray, optional
|
|
239
262
|
Axes to draw on. Defaults to ``None`` which uses the current axes.
|
|
263
|
+
mask : numpy.ndarray, optional
|
|
264
|
+
Two-dimensional mask array defining the drawable region of the word cloud.
|
|
265
|
+
Defaults to a circular mask generated by :func:`create_circular_mask`.
|
|
240
266
|
|
|
241
267
|
Returns
|
|
242
268
|
-------
|
|
@@ -247,9 +273,15 @@ def aplot_wordcloud(
|
|
|
247
273
|
------
|
|
248
274
|
AttributeError
|
|
249
275
|
If required columns are missing from the DataFrame.
|
|
276
|
+
TypeError
|
|
277
|
+
If ``ax`` is not a ``matplotlib.axes.Axes`` instance.
|
|
250
278
|
"""
|
|
251
279
|
if ax is None:
|
|
252
280
|
ax = cast(Axes, plt.gca())
|
|
281
|
+
elif isinstance(ax, np.ndarray):
|
|
282
|
+
raise TypeError("ax must be a single matplotlib Axes instance, not an array.")
|
|
283
|
+
elif not isinstance(ax, Axes):
|
|
284
|
+
raise TypeError("ax must be a matplotlib Axes instance.")
|
|
253
285
|
|
|
254
286
|
words, weights = _prepare_word_frequencies(
|
|
255
287
|
pd_df=pd_df,
|
|
@@ -259,7 +291,13 @@ def aplot_wordcloud(
|
|
|
259
291
|
stopwords=stopwords,
|
|
260
292
|
)
|
|
261
293
|
return _plot_words(
|
|
262
|
-
ax,
|
|
294
|
+
ax,
|
|
295
|
+
words,
|
|
296
|
+
weights,
|
|
297
|
+
style=style,
|
|
298
|
+
title=title,
|
|
299
|
+
random_state=random_state,
|
|
300
|
+
mask=mask,
|
|
263
301
|
)
|
|
264
302
|
|
|
265
303
|
|
|
@@ -275,6 +313,7 @@ def fplot_wordcloud(
|
|
|
275
313
|
figsize: Tuple[float, float] = FIG_SIZE,
|
|
276
314
|
save_path: Optional[str] = None,
|
|
277
315
|
savefig_kwargs: Optional[Dict[str, Any]] = None,
|
|
316
|
+
mask: Optional[np.ndarray] = None,
|
|
278
317
|
) -> Figure:
|
|
279
318
|
"""Create a new figure with a word cloud.
|
|
280
319
|
|
|
@@ -298,6 +337,9 @@ def fplot_wordcloud(
|
|
|
298
337
|
Seed for word placement. Defaults to ``None``.
|
|
299
338
|
figsize : tuple of float, optional
|
|
300
339
|
Figure size. Defaults to ``FIG_SIZE``.
|
|
340
|
+
mask : numpy.ndarray, optional
|
|
341
|
+
Two-dimensional mask array defining the drawable region of the word cloud.
|
|
342
|
+
Defaults to a circular mask generated by :func:`create_circular_mask`.
|
|
301
343
|
|
|
302
344
|
Returns
|
|
303
345
|
-------
|
|
@@ -325,6 +367,7 @@ def fplot_wordcloud(
|
|
|
325
367
|
style=style,
|
|
326
368
|
title=title,
|
|
327
369
|
random_state=random_state,
|
|
370
|
+
mask=mask,
|
|
328
371
|
)
|
|
329
372
|
fig.patch.set_facecolor(style.background_color)
|
|
330
373
|
fig.tight_layout()
|
|
@@ -35,7 +35,12 @@ from .Timeserie import TIMESERIE_STYLE_TEMPLATE, aplot_timeserie, fplot_timeseri
|
|
|
35
35
|
from .Sunburst import fplot_sunburst
|
|
36
36
|
from .Treemap import TREEMAP_STYLE_TEMPLATE, fplot_treemap
|
|
37
37
|
from .Waffle import aplot_waffle, fplot_waffle
|
|
38
|
-
from .Wordcloud import
|
|
38
|
+
from .Wordcloud import (
|
|
39
|
+
WORDCLOUD_STYLE_TEMPLATE,
|
|
40
|
+
aplot_wordcloud,
|
|
41
|
+
create_circular_mask,
|
|
42
|
+
fplot_wordcloud,
|
|
43
|
+
)
|
|
39
44
|
from .accessor import DataFrameAccessor
|
|
40
45
|
|
|
41
46
|
__all__ = [
|
|
@@ -54,6 +59,7 @@ __all__ = [
|
|
|
54
59
|
"aplot_correlation_matrix",
|
|
55
60
|
"aplot_area",
|
|
56
61
|
"aplot_pie_donut",
|
|
62
|
+
"create_circular_mask",
|
|
57
63
|
"aplot_waffle",
|
|
58
64
|
"fplot_bubble",
|
|
59
65
|
"fplot_network",
|
|
@@ -97,7 +97,7 @@ def generate_treemap_data():
|
|
|
97
97
|
}
|
|
98
98
|
df = pd.DataFrame(data)
|
|
99
99
|
# For treemap, we need a path-like structure. We will create it here.
|
|
100
|
-
df["path"] = df["parent"]
|
|
100
|
+
df["path"] = df["parent"].str.cat(df["location"], sep="/")
|
|
101
101
|
df["path"] = df["path"].str.lstrip("/")
|
|
102
102
|
df.to_csv("data/treemap.csv", index=False)
|
|
103
103
|
|
|
@@ -169,7 +169,6 @@ def generate_wordcloud_data():
|
|
|
169
169
|
|
|
170
170
|
def generate_bar_data():
|
|
171
171
|
"""Generate and save sample data for bar and stacked bar charts."""
|
|
172
|
-
|
|
173
172
|
data = {
|
|
174
173
|
"product": ["Gadget", "Gadget", "Gadget", "Widget", "Widget", "Widget"],
|
|
175
174
|
"region": ["North", "South", "West", "North", "South", "West"],
|
|
@@ -181,7 +180,6 @@ def generate_bar_data():
|
|
|
181
180
|
|
|
182
181
|
def generate_histogram_data():
|
|
183
182
|
"""Generate and save sample data for histogram and KDE plots."""
|
|
184
|
-
|
|
185
183
|
data = {
|
|
186
184
|
"waiting_time_minutes": [
|
|
187
185
|
5,
|
|
@@ -212,7 +210,6 @@ def generate_histogram_data():
|
|
|
212
210
|
|
|
213
211
|
def generate_box_violin_data():
|
|
214
212
|
"""Generate and save sample data for box and violin plots."""
|
|
215
|
-
|
|
216
213
|
data = {
|
|
217
214
|
"department": [
|
|
218
215
|
"Engineering",
|
|
@@ -243,7 +240,6 @@ def generate_box_violin_data():
|
|
|
243
240
|
|
|
244
241
|
def generate_heatmap_and_correlation_data():
|
|
245
242
|
"""Generate and save sample data for heatmaps and correlation matrices."""
|
|
246
|
-
|
|
247
243
|
data = {
|
|
248
244
|
"month": ["Jan", "Jan", "Feb", "Feb", "Mar", "Mar", "Apr", "Apr"],
|
|
249
245
|
"channel": [
|
|
@@ -269,7 +265,6 @@ def generate_heatmap_and_correlation_data():
|
|
|
269
265
|
|
|
270
266
|
def generate_area_data():
|
|
271
267
|
"""Generate and save sample data for area charts."""
|
|
272
|
-
|
|
273
268
|
data = {
|
|
274
269
|
"quarter": ["Q1", "Q2", "Q3", "Q4", "Q1", "Q2", "Q3", "Q4"],
|
|
275
270
|
"segment": [
|
|
@@ -290,7 +285,6 @@ def generate_area_data():
|
|
|
290
285
|
|
|
291
286
|
def generate_pie_waffle_data():
|
|
292
287
|
"""Generate and save sample data for pie, donut, and waffle charts."""
|
|
293
|
-
|
|
294
288
|
data = {
|
|
295
289
|
"device": ["Desktop", "Mobile", "Tablet", "Other"],
|
|
296
290
|
"sessions": [5200, 8900, 1300, 600],
|
|
@@ -302,7 +296,6 @@ def generate_pie_waffle_data():
|
|
|
302
296
|
|
|
303
297
|
def generate_sankey_data():
|
|
304
298
|
"""Generate and save sample data for Sankey diagrams."""
|
|
305
|
-
|
|
306
299
|
data = {
|
|
307
300
|
"source": ["Homepage", "Homepage", "Landing Page", "Landing Page", "Cart"],
|
|
308
301
|
"target": ["Landing Page", "Product", "Product", "Signup", "Checkout"],
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Public test for MatplotLibAPI."""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import Any, Callable
|
|
5
|
+
from typing import Any, Callable, Generator
|
|
6
6
|
|
|
7
7
|
import pandas as pd
|
|
8
8
|
import pytest
|
|
@@ -13,7 +13,6 @@ from scripts import generate_sample_data
|
|
|
13
13
|
@pytest.fixture(scope="session")
|
|
14
14
|
def sample_data_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
15
15
|
"""Generate sample CSV files in an isolated directory for tests."""
|
|
16
|
-
|
|
17
16
|
base_dir = tmp_path_factory.mktemp("sample_data")
|
|
18
17
|
cwd = os.getcwd()
|
|
19
18
|
os.chdir(base_dir)
|
|
@@ -28,7 +27,7 @@ def sample_data_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
|
28
27
|
@pytest.fixture(scope="session")
|
|
29
28
|
def load_sample_df(
|
|
30
29
|
sample_data_dir: Path,
|
|
31
|
-
) -> Callable[
|
|
30
|
+
) -> Callable[..., pd.DataFrame]:
|
|
32
31
|
"""Return a loader that reads generated sample CSVs into dataframes."""
|
|
33
32
|
|
|
34
33
|
def _loader(filename: str, **kwargs: Any) -> pd.DataFrame:
|
|
@@ -38,9 +37,8 @@ def load_sample_df(
|
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
@pytest.fixture(autouse=True)
|
|
41
|
-
def close_matplotlib_figures() -> None:
|
|
40
|
+
def close_matplotlib_figures() -> Generator[None, None, None]:
|
|
42
41
|
"""Close any matplotlib figures created during a test."""
|
|
43
|
-
|
|
44
42
|
yield
|
|
45
43
|
|
|
46
44
|
import matplotlib.pyplot as plt
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tests for word cloud visualizations."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from matplotlib.figure import Figure
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from MatplotLibAPI.Wordcloud import (
|
|
8
|
+
aplot_wordcloud,
|
|
9
|
+
create_circular_mask,
|
|
10
|
+
fplot_wordcloud,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_fplot_wordcloud(load_sample_df):
|
|
15
|
+
"""Render a word cloud figure from sample data."""
|
|
16
|
+
|
|
17
|
+
df = load_sample_df("wordcloud.csv")
|
|
18
|
+
|
|
19
|
+
fig = fplot_wordcloud(
|
|
20
|
+
pd_df=df, text_column="country", weight_column="population", random_state=42
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
assert isinstance(fig, Figure)
|
|
24
|
+
default_mask = create_circular_mask(size=300)
|
|
25
|
+
image = fig.axes[0].images[0].get_array()
|
|
26
|
+
assert image is not None
|
|
27
|
+
assert tuple(image.shape[:2]) == default_mask.shape
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_fplot_wordcloud_with_mask(load_sample_df):
|
|
31
|
+
"""Render a word cloud using a circular mask to constrain placement."""
|
|
32
|
+
|
|
33
|
+
df = load_sample_df("wordcloud.csv")
|
|
34
|
+
mask = create_circular_mask(size=200)
|
|
35
|
+
|
|
36
|
+
fig = fplot_wordcloud(
|
|
37
|
+
pd_df=df,
|
|
38
|
+
text_column="country",
|
|
39
|
+
weight_column="population",
|
|
40
|
+
random_state=0,
|
|
41
|
+
mask=mask,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
image = fig.axes[0].images[0].get_array()
|
|
45
|
+
assert image is not None
|
|
46
|
+
assert tuple(image.shape[:2]) == mask.shape
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_aplot_wordcloud(load_sample_df):
|
|
50
|
+
"""Render a word cloud onto an existing axes object."""
|
|
51
|
+
|
|
52
|
+
df = load_sample_df("wordcloud.csv")
|
|
53
|
+
fig, ax = plt.subplots()
|
|
54
|
+
|
|
55
|
+
result_ax = aplot_wordcloud(
|
|
56
|
+
ax=ax,
|
|
57
|
+
pd_df=df,
|
|
58
|
+
text_column="country",
|
|
59
|
+
weight_column="population",
|
|
60
|
+
random_state=42,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
assert result_ax is not None
|
|
64
|
+
assert ax is result_ax
|
|
65
|
+
image = result_ax.images[0].get_array()
|
|
66
|
+
assert image is not None
|
|
67
|
+
assert image.shape[0] > 0 and image.shape[1] > 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_create_circular_mask():
|
|
71
|
+
"""Verify circular mask generation."""
|
|
72
|
+
|
|
73
|
+
mask = create_circular_mask(size=100, radius=40)
|
|
74
|
+
assert mask.shape == (100, 100)
|
|
75
|
+
assert mask.dtype == np.uint8
|
|
76
|
+
assert np.sum(mask == 0) > 0
|
|
77
|
+
assert np.sum(mask == 255) > 0
|
|
File without changes
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
"""Tests for word cloud visualizations."""
|
|
2
|
-
|
|
3
|
-
from matplotlib.figure import Figure
|
|
4
|
-
|
|
5
|
-
from MatplotLibAPI.Wordcloud import fplot_wordcloud
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_fplot_wordcloud(load_sample_df):
|
|
9
|
-
"""Render a word cloud figure from sample data."""
|
|
10
|
-
|
|
11
|
-
df = load_sample_df("wordcloud.csv")
|
|
12
|
-
|
|
13
|
-
fig = fplot_wordcloud(
|
|
14
|
-
pd_df=df, text_column="country", weight_column="population", random_state=42
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
assert isinstance(fig, Figure)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|