maidr 0.25.2__py3-none-any.whl → 1.1.0__py3-none-any.whl
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.
- maidr/__init__.py +3 -1
- maidr/api.py +4 -3
- maidr/core/enum/plot_type.py +1 -0
- maidr/core/enum/smooth_keywords.py +10 -0
- maidr/core/maidr.py +40 -15
- maidr/core/plot/boxplot.py +30 -7
- maidr/core/plot/maidr_plot_factory.py +3 -0
- maidr/core/plot/regplot.py +79 -0
- maidr/patch/common.py +2 -1
- maidr/patch/histogram.py +29 -1
- maidr/patch/kdeplot.py +63 -0
- maidr/patch/lineplot.py +1 -0
- maidr/patch/regplot.py +81 -0
- maidr/util/dedup_utils.py +12 -0
- maidr/util/regression_line_utils.py +19 -0
- maidr/util/svg_utils.py +46 -0
- {maidr-0.25.2.dist-info → maidr-1.1.0.dist-info}/METADATA +2 -1
- {maidr-0.25.2.dist-info → maidr-1.1.0.dist-info}/RECORD +20 -13
- {maidr-0.25.2.dist-info → maidr-1.1.0.dist-info}/WHEEL +1 -1
- {maidr-0.25.2.dist-info → maidr-1.1.0.dist-info}/LICENSE +0 -0
maidr/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
__version__ = "
|
|
1
|
+
__version__ = "1.1.0"
|
|
2
2
|
|
|
3
3
|
from .api import close, render, save_html, show, stacked
|
|
4
4
|
from .core import Maidr
|
|
@@ -12,6 +12,8 @@ from .patch import (
|
|
|
12
12
|
histogram,
|
|
13
13
|
lineplot,
|
|
14
14
|
scatterplot,
|
|
15
|
+
regplot,
|
|
16
|
+
kdeplot,
|
|
15
17
|
)
|
|
16
18
|
|
|
17
19
|
__all__ = [
|
maidr/api.py
CHANGED
|
@@ -40,9 +40,10 @@ def save_html(
|
|
|
40
40
|
if isinstance(ax, list):
|
|
41
41
|
for axes in ax:
|
|
42
42
|
maidr = FigureManager.get_maidr(axes.get_figure())
|
|
43
|
-
htmls.append(maidr.
|
|
44
|
-
htmls[-1].save_html(
|
|
45
|
-
|
|
43
|
+
htmls.append(maidr._create_html_doc(use_iframe=False))
|
|
44
|
+
return htmls[-1].save_html(
|
|
45
|
+
file, libdir=lib_dir, include_version=include_version
|
|
46
|
+
)
|
|
46
47
|
else:
|
|
47
48
|
maidr = FigureManager.get_maidr(ax.get_figure())
|
|
48
49
|
return maidr.save_html(file, lib_dir=lib_dir, include_version=include_version)
|
maidr/core/enum/plot_type.py
CHANGED
maidr/core/maidr.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
3
5
|
import io
|
|
4
6
|
import json
|
|
5
7
|
import os
|
|
@@ -12,12 +14,13 @@ import matplotlib.pyplot as plt
|
|
|
12
14
|
from htmltools import HTML, HTMLDocument, Tag, tags
|
|
13
15
|
from lxml import etree
|
|
14
16
|
from matplotlib.figure import Figure
|
|
17
|
+
from maidr.core.enum.plot_type import PlotType
|
|
15
18
|
|
|
16
19
|
from maidr.core.context_manager import HighlightContextManager
|
|
17
20
|
from maidr.core.enum.maidr_key import MaidrKey
|
|
18
|
-
from maidr.core.enum.plot_type import PlotType
|
|
19
21
|
from maidr.core.plot import MaidrPlot
|
|
20
22
|
from maidr.util.environment import Environment
|
|
23
|
+
from maidr.util.dedup_utils import deduplicate_smooth_and_line
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class Maidr:
|
|
@@ -62,7 +65,7 @@ class Maidr:
|
|
|
62
65
|
|
|
63
66
|
def render(self) -> Tag:
|
|
64
67
|
"""Return the maidr plot inside an iframe."""
|
|
65
|
-
return self._create_html_tag()
|
|
68
|
+
return self._create_html_tag(use_iframe=True)
|
|
66
69
|
|
|
67
70
|
def save_html(
|
|
68
71
|
self, file: str, *, lib_dir: str | None = "lib", include_version: bool = True
|
|
@@ -80,7 +83,9 @@ class Maidr:
|
|
|
80
83
|
include_version : bool, default=True
|
|
81
84
|
Whether to include the version number in the dependency folder name.
|
|
82
85
|
"""
|
|
83
|
-
html = self._create_html_doc(
|
|
86
|
+
html = self._create_html_doc(
|
|
87
|
+
use_iframe=False
|
|
88
|
+
) # Always use direct HTML for saving
|
|
84
89
|
return html.save_html(file, libdir=lib_dir, include_version=include_version)
|
|
85
90
|
|
|
86
91
|
def show(
|
|
@@ -96,7 +101,7 @@ class Maidr:
|
|
|
96
101
|
renderer : Literal["auto", "ipython", "browser"], default="auto"
|
|
97
102
|
The renderer to use for the HTML preview.
|
|
98
103
|
"""
|
|
99
|
-
html = self._create_html_tag()
|
|
104
|
+
html = self._create_html_tag(use_iframe=True) # Always use iframe for display
|
|
100
105
|
_renderer = Environment.get_renderer()
|
|
101
106
|
if _renderer == "browser" or (
|
|
102
107
|
Environment.is_interactive_shell() and not Environment.is_notebook()
|
|
@@ -122,10 +127,12 @@ class Maidr:
|
|
|
122
127
|
os.makedirs(static_temp_dir, exist_ok=True)
|
|
123
128
|
|
|
124
129
|
temp_file_path = os.path.join(static_temp_dir, "maidr_plot.html")
|
|
125
|
-
html_file_path = self.save_html(
|
|
130
|
+
html_file_path = self.save_html(
|
|
131
|
+
temp_file_path
|
|
132
|
+
) # This will use use_iframe=False
|
|
126
133
|
webbrowser.open(f"file://{html_file_path}")
|
|
127
134
|
|
|
128
|
-
def _create_html_tag(self) -> Tag:
|
|
135
|
+
def _create_html_tag(self, use_iframe: bool = True) -> Tag:
|
|
129
136
|
"""Create the MAIDR HTML using HTML tags."""
|
|
130
137
|
tagged_elements: list[Any] = [
|
|
131
138
|
element for plot in self._plots for element in plot.elements
|
|
@@ -141,16 +148,32 @@ class Maidr:
|
|
|
141
148
|
maidr = f"\nlet maidr = {json.dumps(self._flatten_maidr(), indent=2)}\n"
|
|
142
149
|
|
|
143
150
|
# Inject plot's svg and MAIDR structure into html tag.
|
|
144
|
-
return Maidr._inject_plot(svg, maidr, self.maidr_id)
|
|
151
|
+
return Maidr._inject_plot(svg, maidr, self.maidr_id, use_iframe)
|
|
145
152
|
|
|
146
|
-
def _create_html_doc(self) -> HTMLDocument:
|
|
153
|
+
def _create_html_doc(self, use_iframe: bool = True) -> HTMLDocument:
|
|
147
154
|
"""Create an HTML document from Tag objects."""
|
|
148
|
-
return HTMLDocument(self._create_html_tag(), lang="en")
|
|
155
|
+
return HTMLDocument(self._create_html_tag(use_iframe), lang="en")
|
|
149
156
|
|
|
150
157
|
def _flatten_maidr(self) -> dict | list[dict]:
|
|
151
158
|
"""Return a single plot schema or a list of schemas from the Maidr instance."""
|
|
152
159
|
if self.plot_type in (PlotType.DODGED, PlotType.STACKED):
|
|
153
160
|
self._plots = [self._plots[0]]
|
|
161
|
+
# Deduplicate: if any SMOOTH plots exist, remove LINE plots
|
|
162
|
+
self._plots = deduplicate_smooth_and_line(self._plots)
|
|
163
|
+
|
|
164
|
+
unique_gids = set()
|
|
165
|
+
deduped_plots = []
|
|
166
|
+
for plot in self._plots:
|
|
167
|
+
if getattr(plot, "type", None) == PlotType.SMOOTH:
|
|
168
|
+
gid = getattr(plot, "_smooth_gid", None)
|
|
169
|
+
if gid and gid in unique_gids:
|
|
170
|
+
continue
|
|
171
|
+
if gid:
|
|
172
|
+
unique_gids.add(gid)
|
|
173
|
+
deduped_plots.append(plot)
|
|
174
|
+
else:
|
|
175
|
+
deduped_plots.append(plot)
|
|
176
|
+
self._plots = deduped_plots
|
|
154
177
|
|
|
155
178
|
plot_schemas = []
|
|
156
179
|
|
|
@@ -243,17 +266,19 @@ class Maidr:
|
|
|
243
266
|
return str(uuid.uuid4())
|
|
244
267
|
|
|
245
268
|
@staticmethod
|
|
246
|
-
def _inject_plot(plot: HTML, maidr: str, maidr_id) -> Tag:
|
|
269
|
+
def _inject_plot(plot: HTML, maidr: str, maidr_id, use_iframe: bool = True) -> Tag:
|
|
247
270
|
"""Embed the plot and associated MAIDR scripts into the HTML structure."""
|
|
248
|
-
# MAIDR_TS_CDN_URL = "http://localhost:
|
|
249
|
-
MAIDR_TS_CDN_URL = "https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"
|
|
271
|
+
# MAIDR_TS_CDN_URL = "http://localhost:8888/tree/maidr/core/maidr.js" # DEMO URL
|
|
272
|
+
MAIDR_TS_CDN_URL = "https://cdn.jsdelivr.net/npm/maidr@latest/dist/maidr.js"
|
|
273
|
+
# Append a query parameter (using TIMESTAMP) to bust the cache (so that the latest (non-cached) version is always loaded).
|
|
274
|
+
TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
250
275
|
|
|
251
276
|
script = f"""
|
|
252
|
-
if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]'))
|
|
277
|
+
if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}?v={TIMESTAMP}"]'))
|
|
253
278
|
{{
|
|
254
279
|
var script = document.createElement('script');
|
|
255
280
|
script.type = 'module';
|
|
256
|
-
script.src = '{MAIDR_TS_CDN_URL}';
|
|
281
|
+
script.src = '{MAIDR_TS_CDN_URL}?v={TIMESTAMP}';
|
|
257
282
|
script.addEventListener('load', function() {{
|
|
258
283
|
window.main();
|
|
259
284
|
}});
|
|
@@ -279,7 +304,7 @@ class Maidr:
|
|
|
279
304
|
# Render the plot inside an iframe if in a Jupyter notebook, Google Colab
|
|
280
305
|
# or VSCode notebook. No need for iframe if this is a Quarto document.
|
|
281
306
|
# For TypeScript we will use iframe by default for now
|
|
282
|
-
if Environment.is_notebook() or Environment.is_shiny():
|
|
307
|
+
if use_iframe and (Environment.is_notebook() or Environment.is_shiny()):
|
|
283
308
|
unique_id = "iframe_" + Maidr._unique_id()
|
|
284
309
|
|
|
285
310
|
def generate_iframe_script(unique_id: str) -> str:
|
maidr/core/plot/boxplot.py
CHANGED
|
@@ -178,19 +178,39 @@ class BoxPlot(
|
|
|
178
178
|
"boxes": [],
|
|
179
179
|
"outliers": [],
|
|
180
180
|
}
|
|
181
|
+
self.lower_outliers_count = []
|
|
181
182
|
|
|
182
183
|
def _get_selector(self) -> list[dict]:
|
|
183
184
|
mins, maxs, medians, boxes, outliers = self.elements_map.values()
|
|
184
185
|
selector = []
|
|
185
|
-
|
|
186
|
+
|
|
187
|
+
for (
|
|
188
|
+
min,
|
|
189
|
+
max,
|
|
190
|
+
median,
|
|
191
|
+
box,
|
|
192
|
+
outlier,
|
|
193
|
+
lower_outliers_count,
|
|
194
|
+
) in zip(
|
|
195
|
+
mins,
|
|
196
|
+
maxs,
|
|
197
|
+
medians,
|
|
198
|
+
boxes,
|
|
199
|
+
outliers,
|
|
200
|
+
self.lower_outliers_count,
|
|
201
|
+
):
|
|
186
202
|
selector.append(
|
|
187
203
|
{
|
|
188
|
-
MaidrKey.LOWER_OUTLIER.value: [
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
MaidrKey.
|
|
192
|
-
MaidrKey.
|
|
193
|
-
MaidrKey.
|
|
204
|
+
MaidrKey.LOWER_OUTLIER.value: [
|
|
205
|
+
f"g[id='{outlier}'] > g > :nth-child(-n+{lower_outliers_count} of use:not([visibility='hidden']))"
|
|
206
|
+
],
|
|
207
|
+
MaidrKey.MIN.value: f"g[id='{min}'] > path",
|
|
208
|
+
MaidrKey.MAX.value: f"g[id='{max}'] > path",
|
|
209
|
+
MaidrKey.Q2.value: f"g[id='{median}'] > path",
|
|
210
|
+
MaidrKey.IQ.value: f"g[id='{box}'] > path",
|
|
211
|
+
MaidrKey.UPPER_OUTLIER.value: [
|
|
212
|
+
f"g[id='{outlier}'] > g > :nth-child(n+{lower_outliers_count + 1} of use:not([visibility='hidden']))"
|
|
213
|
+
],
|
|
194
214
|
}
|
|
195
215
|
)
|
|
196
216
|
return selector if self._orientation == "vert" else list(reversed(selector))
|
|
@@ -217,6 +237,9 @@ class BoxPlot(
|
|
|
217
237
|
medians = self._bxp_extractor.extract_medians(bxp_stats["medians"])
|
|
218
238
|
outliers = self._bxp_extractor.extract_outliers(bxp_stats["fliers"], caps)
|
|
219
239
|
|
|
240
|
+
for outlier in outliers:
|
|
241
|
+
self.lower_outliers_count.append(len(outlier[MaidrKey.LOWER_OUTLIER.value]))
|
|
242
|
+
|
|
220
243
|
caps_elements = self._bxp_elements_extractor.extract_caps(bxp_stats["caps"])
|
|
221
244
|
bxp_maidr = []
|
|
222
245
|
|
|
@@ -11,6 +11,7 @@ from maidr.core.plot.histogram import HistPlot
|
|
|
11
11
|
from maidr.core.plot.lineplot import MultiLinePlot
|
|
12
12
|
from maidr.core.plot.maidr_plot import MaidrPlot
|
|
13
13
|
from maidr.core.plot.scatterplot import ScatterPlot
|
|
14
|
+
from maidr.core.plot.regplot import SmoothPlot
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class MaidrPlotFactory:
|
|
@@ -44,5 +45,7 @@ class MaidrPlotFactory:
|
|
|
44
45
|
return ScatterPlot(ax)
|
|
45
46
|
elif PlotType.DODGED == plot_type or PlotType.STACKED == plot_type:
|
|
46
47
|
return GroupedBarPlot(ax, plot_type, **kwargs)
|
|
48
|
+
elif PlotType.SMOOTH == plot_type:
|
|
49
|
+
return SmoothPlot(ax, **kwargs)
|
|
47
50
|
else:
|
|
48
51
|
raise TypeError(f"Unsupported plot type: {plot_type}.")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from matplotlib.axes import Axes
|
|
4
|
+
from maidr.core.plot.maidr_plot import MaidrPlot
|
|
5
|
+
from maidr.exception.extraction_error import ExtractionError
|
|
6
|
+
import numpy as np
|
|
7
|
+
from maidr.core.enum.plot_type import PlotType
|
|
8
|
+
from maidr.core.enum.maidr_key import MaidrKey
|
|
9
|
+
from maidr.util.regression_line_utils import find_regression_line
|
|
10
|
+
from maidr.util.svg_utils import data_to_svg_coords
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SmoothPlot(MaidrPlot):
|
|
14
|
+
"""
|
|
15
|
+
Extracts and represents a regression line as a smooth plot for MAIDR.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
ax : Axes
|
|
20
|
+
The matplotlib axes object containing the regression line.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, ax: Axes, **kwargs):
|
|
24
|
+
"""
|
|
25
|
+
Initialize a SmoothPlot for a regression line.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
ax : Axes
|
|
30
|
+
The matplotlib axes object containing the regression line.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(ax, PlotType.SMOOTH)
|
|
33
|
+
self._smooth_gid = None
|
|
34
|
+
self._regression_line = kwargs.get("regression_line", None)
|
|
35
|
+
self._poly_gid = kwargs.get("poly_gid", None)
|
|
36
|
+
self._is_polycollection = kwargs.get("is_polycollection", False)
|
|
37
|
+
|
|
38
|
+
def _get_selector(self):
|
|
39
|
+
"""
|
|
40
|
+
Return the CSS selector for highlighting the regression line or PolyCollection in the SVG output.
|
|
41
|
+
"""
|
|
42
|
+
if self._is_polycollection and self._poly_gid:
|
|
43
|
+
return [f"g[id='{self._poly_gid}'] > defs > path"]
|
|
44
|
+
if self._smooth_gid:
|
|
45
|
+
return [f"g[id='{self._smooth_gid}'] path"]
|
|
46
|
+
return ["g[id^='maidr-'] path"]
|
|
47
|
+
|
|
48
|
+
def _extract_plot_data(self) -> list:
|
|
49
|
+
"""
|
|
50
|
+
Extract XY data from the regression line for serialization, including SVG coordinates.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
list
|
|
55
|
+
A list of lists containing dictionaries with X and Y coordinates, and SVG coordinates.
|
|
56
|
+
"""
|
|
57
|
+
regression_line = (
|
|
58
|
+
self._regression_line
|
|
59
|
+
if self._regression_line is not None
|
|
60
|
+
else find_regression_line(self.ax)
|
|
61
|
+
)
|
|
62
|
+
if regression_line is None:
|
|
63
|
+
raise ExtractionError(PlotType.SMOOTH, self.ax)
|
|
64
|
+
self._elements.append(regression_line)
|
|
65
|
+
self._smooth_gid = regression_line.get_gid()
|
|
66
|
+
xydata = np.asarray(regression_line.get_xydata())
|
|
67
|
+
x_data, y_data = xydata[:, 0], xydata[:, 1]
|
|
68
|
+
x_svg, y_svg = data_to_svg_coords(self.ax, x_data, y_data)
|
|
69
|
+
return [
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
MaidrKey.X: float(x),
|
|
73
|
+
MaidrKey.Y: float(y),
|
|
74
|
+
"svg_x": float(sx),
|
|
75
|
+
"svg_y": float(sy),
|
|
76
|
+
}
|
|
77
|
+
for x, y, sx, sy in zip(x_data, y_data, x_svg, y_svg)
|
|
78
|
+
]
|
|
79
|
+
]
|
maidr/patch/common.py
CHANGED
|
@@ -22,6 +22,7 @@ def common(plot_type, wrapped, _, args, kwargs) -> Any:
|
|
|
22
22
|
|
|
23
23
|
# Extract the data points for MAIDR from the plot.
|
|
24
24
|
ax = FigureManager.get_axes(plot)
|
|
25
|
-
|
|
25
|
+
kwargs.pop("ax", None)
|
|
26
|
+
FigureManager.create_maidr(ax, plot_type, **kwargs)
|
|
26
27
|
|
|
27
28
|
return plot
|
maidr/patch/histogram.py
CHANGED
|
@@ -6,6 +6,8 @@ import numpy as np
|
|
|
6
6
|
from matplotlib.axes import Axes
|
|
7
7
|
from matplotlib.container import BarContainer
|
|
8
8
|
from matplotlib.patches import Polygon
|
|
9
|
+
from matplotlib.lines import Line2D
|
|
10
|
+
import uuid
|
|
9
11
|
|
|
10
12
|
from maidr.core.context_manager import ContextManager
|
|
11
13
|
from maidr.core.enum import PlotType
|
|
@@ -21,6 +23,9 @@ def mpl_hist(
|
|
|
21
23
|
np.ndarray,
|
|
22
24
|
BarContainer | Polygon | list[BarContainer | Polygon],
|
|
23
25
|
]:
|
|
26
|
+
"""
|
|
27
|
+
Patch matplotlib Axes.hist to register HIST layer for MAIDR.
|
|
28
|
+
"""
|
|
24
29
|
# Don't proceed if the call is made internally by the patched function.
|
|
25
30
|
if ContextManager.is_internal_context():
|
|
26
31
|
return wrapped(*args, **kwargs)
|
|
@@ -40,4 +45,27 @@ def mpl_hist(
|
|
|
40
45
|
|
|
41
46
|
@wrapt.patch_function_wrapper("seaborn", "histplot")
|
|
42
47
|
def sns_hist(wrapped, instance, args, kwargs) -> Axes:
|
|
43
|
-
|
|
48
|
+
"""
|
|
49
|
+
Patch seaborn.histplot to register HIST and (if kde=True) SMOOTH layers for MAIDR.
|
|
50
|
+
"""
|
|
51
|
+
# Register the histogram as HIST as before
|
|
52
|
+
ax = common(PlotType.HIST, wrapped, instance, args, kwargs)
|
|
53
|
+
# Only register KDE overlay as SMOOTH if kde=True was set
|
|
54
|
+
kde_enabled = kwargs.get("kde", False)
|
|
55
|
+
if kde_enabled:
|
|
56
|
+
# Find the KDE line(s) and register as SMOOTH
|
|
57
|
+
axes = ax if isinstance(ax, Axes) else getattr(ax, "axes", None)
|
|
58
|
+
if axes is not None:
|
|
59
|
+
for line in axes.get_lines():
|
|
60
|
+
if isinstance(line, Line2D):
|
|
61
|
+
if line.get_gid() is None:
|
|
62
|
+
gid = f"maidr-{uuid.uuid4()}"
|
|
63
|
+
line.set_gid(gid)
|
|
64
|
+
common(
|
|
65
|
+
PlotType.SMOOTH,
|
|
66
|
+
lambda *a, **k: axes,
|
|
67
|
+
instance,
|
|
68
|
+
args,
|
|
69
|
+
dict(kwargs, regression_line=line),
|
|
70
|
+
)
|
|
71
|
+
return ax
|
maidr/patch/kdeplot.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import wrapt
|
|
4
|
+
import uuid
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib.axes import Axes
|
|
7
|
+
from matplotlib.lines import Line2D
|
|
8
|
+
from matplotlib.collections import PolyCollection
|
|
9
|
+
from maidr.core.enum import PlotType
|
|
10
|
+
from maidr.patch.common import common
|
|
11
|
+
from maidr.core.context_manager import ContextManager
|
|
12
|
+
from maidr.util.svg_utils import unique_lines_by_xy
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def kde(wrapped, instance, args, kwargs) -> Axes | Line2D | PolyCollection:
|
|
16
|
+
"""
|
|
17
|
+
Patch for seaborn.kdeplot: register all unique lines and/or filled boundaries as SMOOTH.
|
|
18
|
+
"""
|
|
19
|
+
with ContextManager.set_internal_context():
|
|
20
|
+
plot = wrapped(*args, **kwargs)
|
|
21
|
+
ax = plot if isinstance(plot, Axes) else getattr(plot, "axes", None)
|
|
22
|
+
if ax is not None:
|
|
23
|
+
# Register all unique Line2D objects
|
|
24
|
+
lines = [line for line in ax.get_lines() if isinstance(line, Line2D)]
|
|
25
|
+
for kde_line in unique_lines_by_xy(lines):
|
|
26
|
+
if kde_line.get_gid() is None:
|
|
27
|
+
gid = f"maidr-{uuid.uuid4()}"
|
|
28
|
+
kde_line.set_gid(gid)
|
|
29
|
+
common(
|
|
30
|
+
PlotType.SMOOTH,
|
|
31
|
+
lambda *a, **k: ax,
|
|
32
|
+
instance,
|
|
33
|
+
args,
|
|
34
|
+
dict(kwargs, regression_line=kde_line),
|
|
35
|
+
)
|
|
36
|
+
# Register all PolyCollection boundaries as SMOOTH
|
|
37
|
+
for poly in [c for c in ax.collections if isinstance(c, PolyCollection)]:
|
|
38
|
+
if poly.get_paths():
|
|
39
|
+
path = poly.get_paths()[0]
|
|
40
|
+
boundary = path.vertices
|
|
41
|
+
# Defensive: ensure boundary is a numpy array
|
|
42
|
+
boundary = np.asarray(boundary)
|
|
43
|
+
kde_line = Line2D(boundary[:, 0], boundary[:, 1])
|
|
44
|
+
gid = f"maidr-{uuid.uuid4()}"
|
|
45
|
+
kde_line.set_gid(gid)
|
|
46
|
+
poly.set_gid(gid) # Assign gid to PolyCollection group
|
|
47
|
+
common(
|
|
48
|
+
PlotType.SMOOTH,
|
|
49
|
+
lambda *a, **k: ax,
|
|
50
|
+
instance,
|
|
51
|
+
args,
|
|
52
|
+
dict(
|
|
53
|
+
kwargs,
|
|
54
|
+
regression_line=kde_line,
|
|
55
|
+
poly_gid=gid,
|
|
56
|
+
is_polycollection=True,
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
return plot
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# Patch seaborn kdeplot
|
|
63
|
+
wrapt.wrap_function_wrapper("seaborn", "kdeplot", kde)
|
maidr/patch/lineplot.py
CHANGED
maidr/patch/regplot.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import wrapt
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
from matplotlib.lines import Line2D
|
|
6
|
+
from maidr.core.enum import PlotType
|
|
7
|
+
from maidr.patch.common import common
|
|
8
|
+
import numpy as np
|
|
9
|
+
from maidr.core.context_manager import ContextManager
|
|
10
|
+
import uuid
|
|
11
|
+
from maidr.core.enum.smooth_keywords import SMOOTH_KEYWORDS
|
|
12
|
+
from maidr.util.regression_line_utils import find_regression_line
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def regplot(wrapped, instance, args, kwargs) -> Axes:
|
|
16
|
+
"""
|
|
17
|
+
Patch seaborn.regplot to register SCATTER and SMOOTH layers for MAIDR.
|
|
18
|
+
"""
|
|
19
|
+
scatter = kwargs.get("scatter", True)
|
|
20
|
+
if scatter:
|
|
21
|
+
ax = common(PlotType.SCATTER, wrapped, instance, args, kwargs)
|
|
22
|
+
else:
|
|
23
|
+
# Prevent any MAIDR layer registration during plotting when scatter=False
|
|
24
|
+
with ContextManager.set_internal_context():
|
|
25
|
+
ax = wrapped(*args, **kwargs)
|
|
26
|
+
axes = ax if isinstance(ax, Axes) else ax.axes if hasattr(ax, "axes") else None
|
|
27
|
+
if axes is not None:
|
|
28
|
+
regression_line = find_regression_line(axes)
|
|
29
|
+
if regression_line is not None:
|
|
30
|
+
# ---
|
|
31
|
+
# Assign a unique gid to the regression line if not already set.
|
|
32
|
+
# This is necessary because the SVG output may contain many <g> and <path> tags,
|
|
33
|
+
# and only the regression line should be uniquely selectable for accessibility and highlighting.
|
|
34
|
+
# By setting a unique gid, we ensure the backend and frontend can generate a reliable selector
|
|
35
|
+
# (e.g., g[id='maidr-...'] path) that matches only the intended regression line.
|
|
36
|
+
# ---
|
|
37
|
+
if regression_line.get_gid() is None:
|
|
38
|
+
new_gid = f"maidr-{uuid.uuid4()}"
|
|
39
|
+
regression_line.set_gid(new_gid)
|
|
40
|
+
common(
|
|
41
|
+
PlotType.SMOOTH,
|
|
42
|
+
lambda *a, **k: ax,
|
|
43
|
+
instance,
|
|
44
|
+
args,
|
|
45
|
+
dict(kwargs, regression_line=regression_line),
|
|
46
|
+
)
|
|
47
|
+
return ax
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def patched_plot(wrapped, instance, args, kwargs):
|
|
51
|
+
"""
|
|
52
|
+
Patch matplotlib Axes.plot to register SMOOTH layers for MAIDR if the label matches SMOOTH_KEYWORDS.
|
|
53
|
+
"""
|
|
54
|
+
# Call the original plot function
|
|
55
|
+
lines = wrapped(*args, **kwargs)
|
|
56
|
+
# lines can be a list of Line2D objects
|
|
57
|
+
for line in lines:
|
|
58
|
+
if isinstance(line, Line2D):
|
|
59
|
+
label = line.get_label() or ""
|
|
60
|
+
label_str = str(label)
|
|
61
|
+
# Detect if this is a smooth/regression line by label
|
|
62
|
+
if any(key in label_str.lower() for key in SMOOTH_KEYWORDS):
|
|
63
|
+
# Assign a unique gid if not already set
|
|
64
|
+
if line.get_gid() is None:
|
|
65
|
+
new_gid = f"maidr-{uuid.uuid4()}"
|
|
66
|
+
line.set_gid(new_gid)
|
|
67
|
+
# Register as a smooth layer
|
|
68
|
+
common(
|
|
69
|
+
PlotType.SMOOTH,
|
|
70
|
+
lambda *a, **k: instance,
|
|
71
|
+
instance,
|
|
72
|
+
args,
|
|
73
|
+
dict(kwargs, regression_line=line),
|
|
74
|
+
)
|
|
75
|
+
return lines
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Patch seaborn function.
|
|
79
|
+
wrapt.wrap_function_wrapper("seaborn", "regplot", regplot)
|
|
80
|
+
# Patch matplotlib Axes.plot for smooth line detection/registration
|
|
81
|
+
wrapt.wrap_function_wrapper(Axes, "plot", patched_plot)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from maidr.core.enum.plot_type import PlotType
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def deduplicate_smooth_and_line(plots: List[object]) -> List[object]:
|
|
6
|
+
"""
|
|
7
|
+
If any SMOOTH plots exist, remove all LINE plots from the list.
|
|
8
|
+
"""
|
|
9
|
+
has_smooth = any(getattr(plot, "type", None) == PlotType.SMOOTH for plot in plots)
|
|
10
|
+
if has_smooth:
|
|
11
|
+
return [plot for plot in plots if getattr(plot, "type", None) != PlotType.LINE]
|
|
12
|
+
return plots
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from matplotlib.lines import Line2D
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def find_regression_line(axes):
|
|
6
|
+
"""
|
|
7
|
+
Helper to find the regression line (Line2D) in the given axes.
|
|
8
|
+
"""
|
|
9
|
+
return next(
|
|
10
|
+
(
|
|
11
|
+
artist
|
|
12
|
+
for artist in axes.get_children()
|
|
13
|
+
if isinstance(artist, Line2D)
|
|
14
|
+
and artist.get_label() not in (None, "", "_nolegend_")
|
|
15
|
+
and artist.get_xydata() is not None
|
|
16
|
+
and np.asarray(artist.get_xydata()).size > 0
|
|
17
|
+
),
|
|
18
|
+
None,
|
|
19
|
+
)
|
maidr/util/svg_utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from matplotlib.axes import Axes
|
|
3
|
+
from matplotlib.lines import Line2D
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def data_to_svg_coords(
|
|
8
|
+
ax: Axes, x_data: np.ndarray, y_data: np.ndarray
|
|
9
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
10
|
+
"""
|
|
11
|
+
Convert data coordinates to SVG coordinates using matplotlib transforms.
|
|
12
|
+
Returns x_svg, y_svg arrays in SVG points.
|
|
13
|
+
"""
|
|
14
|
+
fig = getattr(ax, "figure", None)
|
|
15
|
+
if fig is None:
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
|
|
18
|
+
fig = plt.gcf()
|
|
19
|
+
try:
|
|
20
|
+
fig.tight_layout()
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
xy_disp = ax.transData.transform(np.column_stack([x_data, y_data]))
|
|
24
|
+
xy_figpix = fig.transFigure.inverted().transform(xy_disp)
|
|
25
|
+
fig_width_pts = fig.get_size_inches()[0] * 72
|
|
26
|
+
fig_height_pts = fig.get_size_inches()[1] * 72
|
|
27
|
+
x_svg = xy_figpix[:, 0] * fig_width_pts
|
|
28
|
+
y_svg = (1 - xy_figpix[:, 1]) * fig_height_pts
|
|
29
|
+
return x_svg, y_svg
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def unique_lines_by_xy(lines: List[Line2D]) -> List[Line2D]:
|
|
33
|
+
"""
|
|
34
|
+
Deduplicate lines by rounded xy data (8 decimals). Only lines with >0 points are kept.
|
|
35
|
+
"""
|
|
36
|
+
seen_xy = set()
|
|
37
|
+
unique_lines = []
|
|
38
|
+
for line in lines:
|
|
39
|
+
xy = np.asarray(line.get_xydata())
|
|
40
|
+
if xy.shape[0] == 0:
|
|
41
|
+
continue
|
|
42
|
+
xy_rounded = tuple(map(tuple, np.round(xy, 8)))
|
|
43
|
+
if xy_rounded not in seen_xy:
|
|
44
|
+
seen_xy.add(xy_rounded)
|
|
45
|
+
unique_lines.append(line)
|
|
46
|
+
return unique_lines
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: maidr
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Multimodal Access and Interactive Data Representations
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Keywords: accessibility,visualization,sonification,braille,tactile,multimodal,data representation,blind,low vision,visual impairments
|
|
@@ -31,6 +31,7 @@ Requires-Dist: lxml (>=5.1.0)
|
|
|
31
31
|
Requires-Dist: matplotlib (>=3.8)
|
|
32
32
|
Requires-Dist: numpy (>=1.26)
|
|
33
33
|
Requires-Dist: seaborn (>=0.12)
|
|
34
|
+
Requires-Dist: statsmodels (>=0.14.4,<0.15.0)
|
|
34
35
|
Requires-Dist: virtualenv (>=20.26.6,<21.0.0)
|
|
35
36
|
Requires-Dist: wrapt (>=1.16.0,<2.0.0)
|
|
36
37
|
Project-URL: Homepage, https://xability.github.io/py-maidr
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
maidr/__init__.py,sha256=
|
|
2
|
-
maidr/api.py,sha256=
|
|
1
|
+
maidr/__init__.py,sha256=62BciXDB6GoklzRkcYKOFyZhj4umsGTIhyV_hB8TbJA,382
|
|
2
|
+
maidr/api.py,sha256=F43mXWsxc7tHdlZqbRlEWkc-RjJVo_zgxCn3NiLBY58,1764
|
|
3
3
|
maidr/core/__init__.py,sha256=WgxLpSEYMc4k3OyEOf1shOxfEq0ASzppEIZYmE91ThQ,25
|
|
4
4
|
maidr/core/context_manager.py,sha256=6cT7ZGOApSpC-SLD2XZWWU_H08i-nfv-JUlzXOtvWYw,3374
|
|
5
5
|
maidr/core/enum/__init__.py,sha256=9ee78L0dlxEx4ulUGVlD-J23UcUZmrGu0rXms54up3c,93
|
|
6
6
|
maidr/core/enum/library.py,sha256=e8ujT_L-McJWfoVJd1ty9K_2bwITnf1j0GPLsnAcHes,104
|
|
7
7
|
maidr/core/enum/maidr_key.py,sha256=ljG0omqzd8K8Yk213N7i7PXGvG-IOlnE5v7o6RoGJzc,795
|
|
8
|
-
maidr/core/enum/plot_type.py,sha256=
|
|
8
|
+
maidr/core/enum/plot_type.py,sha256=CxAHUSpSHw-Fa4QI-yQHyJGPTtaCnWoR92X2ySoX-5I,315
|
|
9
|
+
maidr/core/enum/smooth_keywords.py,sha256=VlpIX1BaoX8efwIrT72GIptxguTpiPtJvvJUPMoaFSQ,194
|
|
9
10
|
maidr/core/figure_manager.py,sha256=e6nI5pGqH0NM3yzt2jeiae4lrBlIOhkDN92GZJ3MNmk,3988
|
|
10
|
-
maidr/core/maidr.py,sha256=
|
|
11
|
+
maidr/core/maidr.py,sha256=t0o8dCoIcj6Oa6HGktPM7l9EN8pplvZX-Xdtp8hg19U,13996
|
|
11
12
|
maidr/core/plot/__init__.py,sha256=xDIpRGM-4DfaSSL3nKcXrjdMecCHJ6en4K4nA_fPefQ,83
|
|
12
13
|
maidr/core/plot/barplot.py,sha256=1HfoqyDGKIXkYQnCHN83Ye_faKpNQ3R4wjlbjD5jUyk,2092
|
|
13
|
-
maidr/core/plot/boxplot.py,sha256=
|
|
14
|
+
maidr/core/plot/boxplot.py,sha256=i11GdNuz_c-hilmhydu3ah-bzyVdFoBkNvRi5lpMrrY,9946
|
|
14
15
|
maidr/core/plot/grouped_barplot.py,sha256=bRcQcvwkF3Q3aZ3PlhbZ6bHI_AfcqdKUMVvlLL94wXM,2078
|
|
15
16
|
maidr/core/plot/heatmap.py,sha256=yMS-31tS2GW4peds9LtZesMxmmTV_YfqYO5M_t5KasQ,2521
|
|
16
17
|
maidr/core/plot/histogram.py,sha256=QV5W-6ZJQQcZsrM91JJBX-ONktJzH7yg_et5_bBPfQQ,1525
|
|
17
18
|
maidr/core/plot/lineplot.py,sha256=_Sg290rk5h1LvtdaY_VKraTLXyiXGi58t-WxLFht5Ds,3331
|
|
18
19
|
maidr/core/plot/maidr_plot.py,sha256=B6hjsu-jSWlevEqJawgwjMOJr51nBBNh7yqJdSTkNhw,3681
|
|
19
|
-
maidr/core/plot/maidr_plot_factory.py,sha256=
|
|
20
|
+
maidr/core/plot/maidr_plot_factory.py,sha256=vhBwozPbNLJyz4EqNmcTGZ87tIUCyHCyiAEZpX_Ztfk,1825
|
|
21
|
+
maidr/core/plot/regplot.py,sha256=b7u6bGTz1IxKahplNUrfwIr_OGSwMJ2BuLgFAVjL0s0,2744
|
|
20
22
|
maidr/core/plot/scatterplot.py,sha256=o0i0uS-wXK9ZrENxneoHbh3-u-2goRONp19Yu9QLsaY,1257
|
|
21
23
|
maidr/exception/__init__.py,sha256=PzaXoYBhyZxMDcJkuxJugDx7jZeseI0El6LpxIwXyG4,46
|
|
22
24
|
maidr/exception/extraction_error.py,sha256=rd37Oxa9gn2OWFWt9AOH5fv0hNd3sAWGvpDMFBuJY2I,607
|
|
@@ -24,20 +26,25 @@ maidr/patch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
24
26
|
maidr/patch/barplot.py,sha256=nxgBWBMUyf3eAW56CN5EKYYy3vsTgEPRlcvNYS3-WiU,2479
|
|
25
27
|
maidr/patch/boxplot.py,sha256=Mcz94pSf7PT3SyRsI8TDrIKVdEmiiUQouXvd05mtnfw,2846
|
|
26
28
|
maidr/patch/clear.py,sha256=2Sc4CIt5jRGkew3TxFsBZm-uowC9yDSxtraEcXZjmGw,396
|
|
27
|
-
maidr/patch/common.py,sha256=
|
|
29
|
+
maidr/patch/common.py,sha256=RV2NayjPmcWJVhZJV7nmBCjcH7MPDhW_fIluTOPAATk,880
|
|
28
30
|
maidr/patch/heatmap.py,sha256=uxLLg530Ql9KVC5rxk8vnwPHXBWWHwYgJRkyHY-tJzs,1048
|
|
29
31
|
maidr/patch/highlight.py,sha256=I1dGFHJAnVd0AHVnMJzk_TE8BC8Uv-I6fTzSrJLU5QM,1155
|
|
30
|
-
maidr/patch/histogram.py,sha256=
|
|
31
|
-
maidr/patch/
|
|
32
|
+
maidr/patch/histogram.py,sha256=k3N0RUf1SQ2402pwbaY5QyS98KnLWvr9glCHQw9NTko,2378
|
|
33
|
+
maidr/patch/kdeplot.py,sha256=qv-OKzuop2aTrkZgUe2OnLxvV-KMyeXt1Td0_uZeHzE,2338
|
|
34
|
+
maidr/patch/lineplot.py,sha256=BCc3tyTrQbdLPUDWIO7hu4h1W71JRPSt2d4tYP54Li4,1085
|
|
35
|
+
maidr/patch/regplot.py,sha256=Ciz43C5XZfWK6wtVWrlV0WNz4R__rcgdqVE9OCaXXRk,3236
|
|
32
36
|
maidr/patch/scatterplot.py,sha256=kln6zZwjVsdQzICalo-RnBOJrx1BnIB2xYUwItHvSNY,525
|
|
33
37
|
maidr/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
|
|
34
39
|
maidr/util/environment.py,sha256=NJzbcFUCIk7OF29eIae8jyHax9p8fQgFLmxM6Af0fUY,4465
|
|
35
40
|
maidr/util/mixin/__init__.py,sha256=aGJZNhtWh77yIVPc7ipIZm1OajigjMtCWYKPuDWTC-c,217
|
|
36
41
|
maidr/util/mixin/extractor_mixin.py,sha256=oHtwpmS5kARvaLrSO3DKTPQxyFUw9nOcKN7rzTj1q4g,5192
|
|
37
42
|
maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
|
|
43
|
+
maidr/util/regression_line_utils.py,sha256=P8RQLixTby2JLz73XZgNiu96C2Ct3pNe4ENRWOjgT8M,509
|
|
44
|
+
maidr/util/svg_utils.py,sha256=2gyzBtNKFHs0utrw1iOlxTmznzivOWQMV2aW8zu2c8E,1442
|
|
38
45
|
maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
46
|
maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
|
|
40
|
-
maidr-
|
|
41
|
-
maidr-
|
|
42
|
-
maidr-
|
|
43
|
-
maidr-
|
|
47
|
+
maidr-1.1.0.dist-info/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
48
|
+
maidr-1.1.0.dist-info/METADATA,sha256=4dAbCUDrTHNIRUmgUuj6_7VI-X1KbzEm6tlmxAyRp1g,2748
|
|
49
|
+
maidr-1.1.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
50
|
+
maidr-1.1.0.dist-info/RECORD,,
|
|
File without changes
|