maidr 1.4.1__tar.gz → 1.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. {maidr-1.4.1 → maidr-1.4.3}/PKG-INFO +1 -1
  2. {maidr-1.4.1 → maidr-1.4.3}/maidr/__init__.py +1 -1
  3. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/enum/smooth_keywords.py +3 -0
  4. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/grouped_barplot.py +3 -1
  5. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/lineplot.py +15 -2
  6. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/mplfinance_lineplot.py +26 -102
  7. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/regplot.py +29 -21
  8. maidr-1.4.3/maidr/util/regression_line_utils.py +69 -0
  9. {maidr-1.4.1 → maidr-1.4.3}/pyproject.toml +1 -1
  10. maidr-1.4.1/maidr/util/regression_line_utils.py +0 -19
  11. {maidr-1.4.1 → maidr-1.4.3}/LICENSE +0 -0
  12. {maidr-1.4.1 → maidr-1.4.3}/README.md +0 -0
  13. {maidr-1.4.1 → maidr-1.4.3}/maidr/api.py +0 -0
  14. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/__init__.py +0 -0
  15. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/context_manager.py +0 -0
  16. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/enum/__init__.py +0 -0
  17. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/enum/library.py +0 -0
  18. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/enum/maidr_key.py +0 -0
  19. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/enum/plot_type.py +0 -0
  20. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/figure_manager.py +0 -0
  21. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/maidr.py +0 -0
  22. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/__init__.py +0 -0
  23. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/barplot.py +0 -0
  24. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/boxplot.py +0 -0
  25. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/candlestick.py +0 -0
  26. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/heatmap.py +0 -0
  27. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/histogram.py +0 -0
  28. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/maidr_plot.py +0 -0
  29. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/maidr_plot_factory.py +0 -0
  30. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/mplfinance_barplot.py +0 -0
  31. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/regplot.py +0 -0
  32. {maidr-1.4.1 → maidr-1.4.3}/maidr/core/plot/scatterplot.py +0 -0
  33. {maidr-1.4.1 → maidr-1.4.3}/maidr/exception/__init__.py +0 -0
  34. {maidr-1.4.1 → maidr-1.4.3}/maidr/exception/extraction_error.py +0 -0
  35. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/__init__.py +0 -0
  36. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/barplot.py +0 -0
  37. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/boxplot.py +0 -0
  38. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/candlestick.py +0 -0
  39. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/clear.py +0 -0
  40. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/common.py +0 -0
  41. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/heatmap.py +0 -0
  42. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/highlight.py +0 -0
  43. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/histogram.py +0 -0
  44. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/kdeplot.py +0 -0
  45. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/lineplot.py +0 -0
  46. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/mplfinance.py +0 -0
  47. {maidr-1.4.1 → maidr-1.4.3}/maidr/patch/scatterplot.py +0 -0
  48. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/__init__.py +0 -0
  49. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/dedup_utils.py +0 -0
  50. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/environment.py +0 -0
  51. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/mixin/__init__.py +0 -0
  52. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/mixin/extractor_mixin.py +0 -0
  53. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/mixin/merger_mixin.py +0 -0
  54. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/mplfinance_utils.py +0 -0
  55. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/plot_detection.py +0 -0
  56. {maidr-1.4.1 → maidr-1.4.3}/maidr/util/svg_utils.py +0 -0
  57. {maidr-1.4.1 → maidr-1.4.3}/maidr/widget/__init__.py +0 -0
  58. {maidr-1.4.1 → maidr-1.4.3}/maidr/widget/shiny.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: maidr
3
- Version: 1.4.1
3
+ Version: 1.4.3
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
@@ -1,4 +1,4 @@
1
- __version__ = "1.4.1"
1
+ __version__ = "1.4.3"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -2,8 +2,11 @@
2
2
  SMOOTH_KEYWORDS = [
3
3
  "smooth",
4
4
  "lowess",
5
+ "loess",
5
6
  "regression",
6
7
  "linear regression",
8
+ "linear fit",
9
+ "fit",
7
10
  "kde",
8
11
  "density",
9
12
  "gaussian",
@@ -24,7 +24,6 @@ class GroupedBarPlot(
24
24
  grouped_ax_schema = {
25
25
  MaidrKey.X.value: self.ax.get_xlabel(),
26
26
  MaidrKey.Y.value: self.ax.get_ylabel(),
27
- MaidrKey.FILL.value: self.ax.get_title(),
28
27
  }
29
28
  return self.merge_dict(base_ax_schema, grouped_ax_schema)
30
29
 
@@ -44,6 +43,9 @@ class GroupedBarPlot(
44
43
  return None
45
44
 
46
45
  x_level = self.extract_level(self.ax)
46
+ if x_level is None:
47
+ return None
48
+
47
49
  data = []
48
50
 
49
51
  self._elements.extend(
@@ -87,9 +87,14 @@ class MultiLinePlot(MaidrPlot, LineExtractorMixin):
87
87
  if not all_lines:
88
88
  return None
89
89
 
90
+ # Try to get series names from legend
91
+ legend_labels = []
92
+ if self.ax.legend_ is not None:
93
+ legend_labels = [text.get_text() for text in self.ax.legend_.get_texts()]
94
+
90
95
  all_lines_data = []
91
96
 
92
- for line in all_lines:
97
+ for i, line in enumerate(all_lines):
93
98
  xydata = line.get_xydata()
94
99
  if xydata is None or not xydata.size: # type: ignore
95
100
  continue
@@ -102,11 +107,19 @@ class MultiLinePlot(MaidrPlot, LineExtractorMixin):
102
107
  line.set_gid(unique_gid)
103
108
 
104
109
  label: str = line.get_label() # type: ignore
110
+
111
+ # Try to get the series name from legend labels
112
+ line_type = ""
113
+ if legend_labels and i < len(legend_labels):
114
+ line_type = legend_labels[i]
115
+ elif not label.startswith("_child"):
116
+ line_type = label
117
+
105
118
  line_data = [
106
119
  {
107
120
  MaidrKey.X: float(x),
108
121
  MaidrKey.Y: float(y),
109
- MaidrKey.FILL: (label if not label.startswith("_child") else ""),
122
+ **({MaidrKey.FILL: line_type} if line_type else {}),
110
123
  }
111
124
  for x, y in line.get_xydata() # type: ignore
112
125
  ]
@@ -23,7 +23,6 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
23
23
 
24
24
  def __init__(self, ax: Axes, **kwargs):
25
25
  super().__init__(ax, PlotType.LINE)
26
- self._line_titles = [] # Store line titles separately
27
26
 
28
27
  def _get_selector(self) -> Union[str, List[str]]:
29
28
  """Return selectors for all lines that have data."""
@@ -48,65 +47,6 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
48
47
 
49
48
  return selectors
50
49
 
51
- def _extract_axes_data(self) -> dict:
52
- """
53
- Extract axis labels for the plot.
54
-
55
- Returns
56
- -------
57
- dict
58
- Dictionary containing x and y axis labels with custom y-label for moving averages.
59
- """
60
- x_labels = self.ax.get_xlabel()
61
- if not x_labels:
62
- x_labels = self.extract_shared_xlabel(self.ax)
63
- if not x_labels:
64
- x_labels = "Date"
65
-
66
- # Get the period from the first line for y-axis label
67
- ma_period = self._extract_moving_average_period()
68
- y_label = (
69
- f"{ma_period}-day mav price ($)"
70
- if ma_period
71
- else "Moving Average Price ($)"
72
- )
73
-
74
- return {MaidrKey.X: x_labels, MaidrKey.Y: y_label}
75
-
76
- def _extract_moving_average_periods(self) -> List[str]:
77
- """
78
- Extract all moving average periods from the _maidr_ma_period attributes set by the mplfinance patch.
79
-
80
- Returns
81
- -------
82
- List[str]
83
- List of moving average periods (e.g., ["3", "6", "30"]).
84
- """
85
- all_lines = self.ax.get_lines()
86
- periods = []
87
- for line in all_lines:
88
- # Get the period that was stored by the mplfinance patch
89
- ma_period = getattr(line, "_maidr_ma_period", None)
90
- if ma_period is not None:
91
- periods.append(str(ma_period))
92
-
93
- # Remove duplicates and sort
94
- periods = sorted(list(set(periods)))
95
-
96
- return periods
97
-
98
- def _extract_moving_average_period(self) -> str:
99
- """
100
- Extract the moving average period from the _maidr_ma_period attribute set by the mplfinance patch.
101
-
102
- Returns
103
- -------
104
- str
105
- The moving average period (e.g., "3", "6", "30") or empty string if no period found.
106
- """
107
- periods = self._extract_moving_average_periods()
108
- return periods[0] if periods else ""
109
-
110
50
  def _extract_plot_data(self) -> Union[List[List[dict]], None]:
111
51
  """Extract data from mplfinance moving average lines."""
112
52
  data = self._extract_line_data()
@@ -151,17 +91,6 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
151
91
  line.set_gid(unique_gid)
152
92
 
153
93
  label: str = line.get_label() # type: ignore
154
-
155
- # Get the period for this specific line
156
- ma_period = getattr(line, "_maidr_ma_period", None)
157
-
158
- # Create title for this line
159
- line_title = (
160
- f"{ma_period}-Day Moving Average Line Plot"
161
- if ma_period
162
- else "Moving Average Line Plot"
163
- )
164
-
165
94
  line_data = []
166
95
 
167
96
  # Check if this line has date numbers from mplfinance
@@ -188,22 +117,12 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
188
117
  point_data = {
189
118
  MaidrKey.X: x_value,
190
119
  MaidrKey.Y: float(y),
120
+ MaidrKey.FILL: (label if not label.startswith("_child") else ""),
191
121
  }
192
122
  line_data.append(point_data)
193
123
 
194
124
  if line_data:
195
- # Create line data with title, axes, and points structure
196
- line_with_metadata = {
197
- "title": line_title,
198
- "axes": {
199
- "x": "Date",
200
- "y": f"{ma_period}-day mav price ($)"
201
- if ma_period
202
- else "Moving Average Price ($)",
203
- },
204
- "points": line_data,
205
- }
206
- all_lines_data.append(line_with_metadata)
125
+ all_lines_data.append(line_data)
207
126
 
208
127
  return all_lines_data if all_lines_data else None
209
128
 
@@ -226,38 +145,43 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
226
145
  """
227
146
  return MplfinanceDataExtractor._convert_date_num_to_string(x_value)
228
147
 
229
- def _extract_line_titles(self) -> List[str]:
148
+ def _extract_moving_average_periods(self) -> List[str]:
230
149
  """
231
- Extract titles for all moving average lines.
150
+ Extract all moving average periods from the _maidr_ma_period attributes set by the mplfinance patch.
232
151
 
233
152
  Returns
234
153
  -------
235
154
  List[str]
236
- List of titles for each line.
155
+ List of moving average periods (e.g., ["3", "6", "30"]).
237
156
  """
238
157
  all_lines = self.ax.get_lines()
239
- titles = []
240
-
158
+ periods = []
241
159
  for line in all_lines:
160
+ # Get the period that was stored by the mplfinance patch
242
161
  ma_period = getattr(line, "_maidr_ma_period", None)
243
- title = (
244
- f"{ma_period}-Day Moving Average Line Plot"
245
- if ma_period
246
- else "Moving Average Line Plot"
247
- )
248
- titles.append(title)
162
+ if ma_period is not None:
163
+ periods.append(str(ma_period))
249
164
 
250
- return titles
165
+ # Remove duplicates and sort
166
+ periods = sorted(list(set(periods)))
167
+
168
+ return periods
169
+
170
+ def _extract_moving_average_period(self) -> str:
171
+ """
172
+ Extract the moving average period from the _maidr_ma_period attribute set by the mplfinance patch.
173
+
174
+ Returns
175
+ -------
176
+ str
177
+ The moving average period (e.g., "3", "6", "30") or empty string if no period found.
178
+ """
179
+ periods = self._extract_moving_average_periods()
180
+ return periods[0] if periods else ""
251
181
 
252
182
  def render(self) -> dict:
253
183
  """Initialize the MAIDR schema dictionary with basic plot information."""
254
- # Use the first line's period for the main title
255
- ma_period = self._extract_moving_average_period()
256
- title = (
257
- f"{ma_period}-Day Moving Averages Line Plot"
258
- if ma_period
259
- else "Moving Averages Line Plot"
260
- )
184
+ title = "Moving Average Line Plot"
261
185
 
262
186
  maidr_schema = {
263
187
  MaidrKey.TYPE: self.type,
@@ -5,11 +5,10 @@ from matplotlib.axes import Axes
5
5
  from matplotlib.lines import Line2D
6
6
  from maidr.core.enum import PlotType
7
7
  from maidr.patch.common import common
8
- import numpy as np
9
8
  from maidr.core.context_manager import ContextManager
10
9
  import uuid
11
10
  from maidr.core.enum.smooth_keywords import SMOOTH_KEYWORDS
12
- from maidr.util.regression_line_utils import find_regression_line
11
+ from maidr.util.regression_line_utils import find_smooth_lines_by_label
13
12
 
14
13
 
15
14
  def regplot(wrapped, instance, args, kwargs) -> Axes:
@@ -23,27 +22,34 @@ def regplot(wrapped, instance, args, kwargs) -> Axes:
23
22
  # Prevent any MAIDR layer registration during plotting when scatter=False
24
23
  with ContextManager.set_internal_context():
25
24
  ax = wrapped(*args, **kwargs)
25
+
26
26
  axes = ax if isinstance(ax, Axes) else ax.axes if hasattr(ax, "axes") else None
27
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:
28
+ # Find and register all smooth lines
29
+ smooth_lines = find_smooth_lines_by_label(axes)
30
+ for line in smooth_lines:
31
+ # If line doesn't have a gid yet, assign one and register
32
+ if line.get_gid() is None:
38
33
  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
- )
34
+ line.set_gid(new_gid)
35
+ common(
36
+ PlotType.SMOOTH,
37
+ lambda *a, **k: ax,
38
+ instance,
39
+ args,
40
+ dict(kwargs, regression_line=line),
41
+ )
42
+ else:
43
+ # Even if it has a gid, register it as a smooth layer
44
+ # This handles the case where patched_plot already assigned a gid
45
+ common(
46
+ PlotType.SMOOTH,
47
+ lambda *a, **k: ax,
48
+ instance,
49
+ args,
50
+ dict(kwargs, regression_line=line),
51
+ )
52
+
47
53
  return ax
48
54
 
49
55
 
@@ -53,7 +59,8 @@ def patched_plot(wrapped, instance, args, kwargs):
53
59
  """
54
60
  # Call the original plot function
55
61
  lines = wrapped(*args, **kwargs)
56
- # lines can be a list of Line2D objects
62
+
63
+ # Check each line for smooth keywords and register if found
57
64
  for line in lines:
58
65
  if isinstance(line, Line2D):
59
66
  label = line.get_label() or ""
@@ -72,6 +79,7 @@ def patched_plot(wrapped, instance, args, kwargs):
72
79
  args,
73
80
  dict(kwargs, regression_line=line),
74
81
  )
82
+
75
83
  return lines
76
84
 
77
85
 
@@ -0,0 +1,69 @@
1
+ from matplotlib.lines import Line2D
2
+ from matplotlib.axes import Axes
3
+ import numpy as np
4
+ from typing import List, Union
5
+ from maidr.core.enum.smooth_keywords import SMOOTH_KEYWORDS
6
+
7
+
8
+ def find_regression_line(axes: Axes) -> Union[Line2D, None]:
9
+ """
10
+ Helper to find the regression line (Line2D) in the given axes.
11
+ """
12
+ return next(
13
+ (
14
+ artist
15
+ for artist in axes.get_children()
16
+ if isinstance(artist, Line2D)
17
+ and artist.get_label() not in (None, "", "_nolegend_")
18
+ and artist.get_xydata() is not None
19
+ and np.asarray(artist.get_xydata()).size > 0
20
+ ),
21
+ None,
22
+ )
23
+
24
+
25
+ def find_smooth_lines_by_label(axes: Axes) -> List[Line2D]:
26
+ """
27
+ Helper to find all smooth/regression lines (Line2D) in the given axes by checking their labels.
28
+
29
+ This function detects smooth lines by examining their labels for smooth-related keywords
30
+ or generic '_child' labels that are commonly created by seaborn's regplot function.
31
+
32
+ Parameters
33
+ ----------
34
+ axes : matplotlib.axes.Axes
35
+ The matplotlib axes object to search for smooth lines.
36
+
37
+ Returns
38
+ -------
39
+ List[Line2D]
40
+ List of Line2D objects that have labels matching smooth keywords or generic '_child' labels.
41
+
42
+ Examples
43
+ --------
44
+ >>> import matplotlib.pyplot as plt
45
+ >>> import seaborn as sns
46
+ >>>
47
+ >>> fig, ax = plt.subplots()
48
+ >>> # Create a regplot with smooth line
49
+ >>> sns.regplot(x=[1, 2, 3], y=[1, 2, 3], ax=ax, lowess=True)
50
+ >>>
51
+ >>> # Find smooth lines
52
+ >>> smooth_lines = find_smooth_lines_by_label(ax)
53
+ >>> print(f"Found {len(smooth_lines)} smooth lines")
54
+ """
55
+ smooth_lines = []
56
+ for line in axes.get_lines():
57
+ if isinstance(line, Line2D):
58
+ label = line.get_label() or ""
59
+ label_str = str(label)
60
+
61
+ # Check if label matches smooth keywords
62
+ if any(key in label_str.lower() for key in SMOOTH_KEYWORDS):
63
+ smooth_lines.append(line)
64
+ # Also check for seaborn regplot lines with generic labels (like '_child0', '_child1')
65
+ elif label_str.startswith("_child"):
66
+ # Lines with _child labels are likely smooth lines from seaborn regplot
67
+ smooth_lines.append(line)
68
+
69
+ return smooth_lines
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "maidr"
7
- version = "1.4.1"
7
+ version = "1.4.3"
8
8
  description = "Multimodal Access and Interactive Data Representations"
9
9
  authors = [
10
10
  "JooYoung Seo <jseo1005@illinois.edu>",
@@ -1,19 +0,0 @@
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
- )
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