maidr 1.8.0__py3-none-any.whl → 1.9.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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.8.0"
1
+ __version__ = "1.9.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
maidr/api.py CHANGED
@@ -14,12 +14,12 @@ from maidr.core.figure_manager import FigureManager
14
14
  def _get_plot_or_current(plot: Any | None) -> Any:
15
15
  """
16
16
  Get the plot object or current matplotlib figure if plot is None.
17
-
17
+
18
18
  Parameters
19
19
  ----------
20
20
  plot : Any or None
21
21
  The plot object. If None, returns the current matplotlib figure.
22
-
22
+
23
23
  Returns
24
24
  -------
25
25
  Any
@@ -28,7 +28,7 @@ def _get_plot_or_current(plot: Any | None) -> Any:
28
28
  if plot is None:
29
29
  # Lazy import matplotlib.pyplot when needed
30
30
  import matplotlib.pyplot as plt
31
-
31
+
32
32
  return plt.gcf()
33
33
  return plot
34
34
 
@@ -48,7 +48,7 @@ def render(plot: Any | None = None) -> Tag:
48
48
  The rendered HTML representation of the plot.
49
49
  """
50
50
  plot = _get_plot_or_current(plot)
51
-
51
+
52
52
  ax = FigureManager.get_axes(plot)
53
53
  if isinstance(ax, list):
54
54
  for axes in ax:
@@ -82,7 +82,7 @@ def show(
82
82
  The display result.
83
83
  """
84
84
  plot = _get_plot_or_current(plot)
85
-
85
+
86
86
  ax = FigureManager.get_axes(plot)
87
87
  if isinstance(ax, list):
88
88
  for axes in ax:
@@ -95,10 +95,11 @@ def show(
95
95
 
96
96
  def save_html(
97
97
  plot: Any | None = None,
98
- *,
98
+ *,
99
99
  file: str,
100
- lib_dir: str | None = "lib",
101
- include_version: bool = True
100
+ lib_dir: str | None = "lib",
101
+ include_version: bool = True,
102
+ data_in_svg: bool = True,
102
103
  ) -> str:
103
104
  """
104
105
  Save a MAIDR plot as HTML file.
@@ -113,6 +114,8 @@ def save_html(
113
114
  Directory name for libraries.
114
115
  include_version : bool, default True
115
116
  Whether to include version information.
117
+ data_in_svg : bool, default True
118
+ Controls where the MAIDR JSON payload is placed in the HTML or SVG.
116
119
 
117
120
  Returns
118
121
  -------
@@ -120,19 +123,21 @@ def save_html(
120
123
  The path to the saved HTML file.
121
124
  """
122
125
  plot = _get_plot_or_current(plot)
123
-
126
+
124
127
  ax = FigureManager.get_axes(plot)
125
128
  htmls = []
126
129
  if isinstance(ax, list):
127
130
  for axes in ax:
128
131
  maidr = FigureManager.get_maidr(axes.get_figure())
129
- htmls.append(maidr._create_html_doc(use_iframe=False))
132
+ htmls.append(maidr._create_html_doc(use_iframe=False, data_in_svg=data_in_svg))
130
133
  return htmls[-1].save_html(
131
134
  file, libdir=lib_dir, include_version=include_version
132
135
  )
133
136
  else:
134
137
  maidr = FigureManager.get_maidr(ax.get_figure())
135
- return maidr.save_html(file, lib_dir=lib_dir, include_version=include_version)
138
+ return maidr.save_html(
139
+ file, lib_dir=lib_dir, include_version=include_version, data_in_svg=data_in_svg
140
+ )
136
141
 
137
142
 
138
143
  def stacked(plot: Axes | BarContainer) -> Maidr:
@@ -150,6 +155,6 @@ def close(plot: Any | None = None) -> None:
150
155
  The plot object to close. If None, uses the current matplotlib figure.
151
156
  """
152
157
  plot = _get_plot_or_current(plot)
153
-
158
+
154
159
  ax = FigureManager.get_axes(plot)
155
160
  FigureManager.destroy(ax.get_figure())
maidr/core/maidr.py CHANGED
@@ -72,7 +72,12 @@ class Maidr:
72
72
  return self._create_html_tag(use_iframe=True)
73
73
 
74
74
  def save_html(
75
- self, file: str, *, lib_dir: str | None = "lib", include_version: bool = True
75
+ self,
76
+ file: str,
77
+ *,
78
+ lib_dir: str | None = "lib",
79
+ include_version: bool = True,
80
+ data_in_svg: bool = True,
76
81
  ) -> str:
77
82
  """
78
83
  Save the HTML representation of the figure with MAIDR to a file.
@@ -86,9 +91,11 @@ class Maidr:
86
91
  (relative to the file's directory).
87
92
  include_version : bool, default=True
88
93
  Whether to include the version number in the dependency folder name.
94
+ data_in_svg : bool, default=True
95
+ Controls where the MAIDR JSON payload is placed in the output HTML or SVG.
89
96
  """
90
97
  html = self._create_html_doc(
91
- use_iframe=False
98
+ use_iframe=False, data_in_svg=data_in_svg
92
99
  ) # Always use direct HTML for saving
93
100
  return html.save_html(file, libdir=lib_dir, include_version=include_version)
94
101
 
@@ -180,8 +187,17 @@ class Maidr:
180
187
  else:
181
188
  webbrowser.open(f"file://{html_file_path}")
182
189
 
183
- def _create_html_tag(self, use_iframe: bool = True) -> Tag:
184
- """Create the MAIDR HTML using HTML tags."""
190
+ def _create_html_tag(self, use_iframe: bool = True, data_in_svg: bool = True) -> Tag:
191
+ """Create the MAIDR HTML using HTML tags.
192
+
193
+ Parameters
194
+ ----------
195
+ use_iframe : bool, default=True
196
+ Whether to render the plot inside an iframe (for notebooks and similar envs).
197
+ data_in_svg : bool, default=True
198
+ If True, the MAIDR JSON is embedded in the root <svg> under attribute 'maidr'.
199
+ If False, a <script>var maidr = {...}</script> tag is injected instead.
200
+ """
185
201
  tagged_elements: list[Any] = [
186
202
  element for plot in self._plots for element in plot.elements
187
203
  ]
@@ -191,16 +207,31 @@ class Maidr:
191
207
  for _ in plot.elements:
192
208
  selector_ids.append(self.selector_ids[i])
193
209
 
210
+ # Build schema once so id stays consistent across SVG and global var
211
+ schema = self._flatten_maidr()
212
+
194
213
  with HighlightContextManager.set_maidr_elements(tagged_elements, selector_ids):
195
- svg = self._get_svg()
196
- maidr = f"\nlet maidr = {json.dumps(self._flatten_maidr(), indent=2)}\n"
214
+ svg = self._get_svg(embed_data=data_in_svg, schema=schema)
215
+
216
+ # Generate external payload if data is not embedded in SVG
217
+ maidr = None
218
+ if not data_in_svg:
219
+ maidr = f"\nvar maidr = {json.dumps(schema, indent=2)}\n"
197
220
 
198
221
  # Inject plot's svg and MAIDR structure into html tag.
199
222
  return Maidr._inject_plot(svg, maidr, self.maidr_id, use_iframe)
200
223
 
201
- def _create_html_doc(self, use_iframe: bool = True) -> HTMLDocument:
202
- """Create an HTML document from Tag objects."""
203
- return HTMLDocument(self._create_html_tag(use_iframe), lang="en")
224
+ def _create_html_doc(self, use_iframe: bool = True, data_in_svg: bool = True) -> HTMLDocument:
225
+ """Create an HTML document from Tag objects.
226
+
227
+ Parameters
228
+ ----------
229
+ use_iframe : bool, default=True
230
+ Whether to render the plot inside an iframe (for notebooks and similar envs).
231
+ data_in_svg : bool, default=True
232
+ See _create_html_tag for details on payload placement strategy.
233
+ """
234
+ return HTMLDocument(self._create_html_tag(use_iframe, data_in_svg), lang="en")
204
235
 
205
236
  def _merge_plots_by_subplot_position(self) -> list[MaidrPlot]:
206
237
  """
@@ -312,10 +343,23 @@ class Maidr:
312
343
  for cell in subplot_grid[i]
313
344
  ]
314
345
 
315
- return {"id": Maidr._unique_id(), "subplots": subplot_grid}
346
+ return {
347
+ "id": Maidr._unique_id(),
348
+ "subplots": subplot_grid,
349
+ }
316
350
 
317
- def _get_svg(self) -> HTML:
318
- """Extract the chart SVG from ``matplotlib.figure.Figure``."""
351
+ def _get_svg(self, embed_data: bool = True, schema: dict | None = None) -> HTML:
352
+ """Extract the chart SVG from ``matplotlib.figure.Figure``.
353
+
354
+ Parameters
355
+ ----------
356
+ embed_data : bool, default=True
357
+ If True, embed the MAIDR JSON schema as an attribute named 'maidr' on
358
+ the root <svg> element. If False, do not embed JSON in the SVG.
359
+ schema : dict | None, default=None
360
+ If provided, this schema will be used (ensuring a consistent id across
361
+ the page). If None, a new schema will be generated.
362
+ """
319
363
  svg_buffer = io.StringIO()
320
364
  self._fig.savefig(svg_buffer, format="svg")
321
365
  str_svg = svg_buffer.getvalue()
@@ -323,12 +367,14 @@ class Maidr:
323
367
  etree.register_namespace("svg", "http://www.w3.org/2000/svg")
324
368
  tree_svg = etree.fromstring(str_svg.encode(), parser=None)
325
369
  root_svg = None
326
- # Find the `svg` tag and set unique id if not present else use it.
370
+ # Find the `svg` tag and optionally embed MAIDR data.
327
371
  for element in tree_svg.iter(tag="{http://www.w3.org/2000/svg}svg"):
328
- if "maidr-data" not in element.attrib:
329
- element.attrib["maidr-data"] = json.dumps(
330
- self._flatten_maidr(), indent=2
331
- )
372
+ current_schema = schema if schema is not None else self._flatten_maidr()
373
+ # Ensure SVG id matches schema id in both modes
374
+ if isinstance(current_schema, dict) and "id" in current_schema:
375
+ element.attrib["id"] = str(current_schema["id"]) # ensure match
376
+ if embed_data:
377
+ element.attrib["maidr"] = json.dumps(current_schema, indent=2)
332
378
  root_svg = element
333
379
  break
334
380
 
@@ -355,36 +401,41 @@ class Maidr:
355
401
  return str(uuid.uuid4())
356
402
 
357
403
  @staticmethod
358
- def _inject_plot(plot: HTML, maidr: str, maidr_id, use_iframe: bool = True) -> Tag:
404
+ def _inject_plot(plot: HTML, maidr: str | None, maidr_id, use_iframe: bool = True) -> Tag:
359
405
  """Embed the plot and associated MAIDR scripts into the HTML structure."""
360
406
  # Get the latest version from npm registry
361
407
  MAIDR_TS_CDN_URL = "https://cdn.jsdelivr.net/npm/maidr@latest/dist/maidr.js"
362
408
 
363
409
  script = f"""
364
- if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]'))
365
- {{
366
- var script = document.createElement('script');
367
- script.type = 'module';
368
- script.src = '{MAIDR_TS_CDN_URL}';
369
- script.addEventListener('load', function() {{
370
- window.main();
371
- }});
372
- document.head.appendChild(script);
373
- }} else {{
374
- document.addEventListener('DOMContentLoaded', function (e) {{
375
- window.main();
376
- }});
377
- }}
410
+ (function() {{
411
+ var existing = document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]');
412
+ if (!existing) {{
413
+ var s = document.createElement('script');
414
+ s.src = '{MAIDR_TS_CDN_URL}';
415
+ s.onload = function() {{ if (window.main) window.main(); }};
416
+ document.head.appendChild(s);
417
+ }} else {{
418
+ if (document.readyState === 'loading') {{
419
+ document.addEventListener('DOMContentLoaded', function() {{ if (window.main) window.main(); }});
420
+ }} else {{
421
+ if (window.main) window.main();
422
+ }}
423
+ }}
424
+ }})();
378
425
  """
379
426
 
380
- base_html = tags.div(
427
+ children = [
381
428
  tags.link(
382
429
  rel="stylesheet",
383
430
  href="https://cdn.jsdelivr.net/npm/maidr@latest/dist/maidr_style.css",
384
- ),
385
- tags.script(script, type="text/javascript"),
386
- tags.div(plot),
387
- )
431
+ )
432
+ ]
433
+ if maidr is not None:
434
+ children.append(tags.script(maidr, type="text/javascript"))
435
+ children.append(tags.script(script, type="text/javascript"))
436
+ children.append(tags.div(plot))
437
+
438
+ base_html = tags.div(*children)
388
439
 
389
440
  # is_quarto = os.getenv("IS_QUARTO") == "True"
390
441
 
@@ -52,18 +52,73 @@ class GroupedBarPlot(
52
52
  [patch for container in plot for patch in container.patches]
53
53
  )
54
54
 
55
- for container in plot:
55
+ # Get hue categories from legend
56
+ hue_categories = self._extract_hue_categories_from_legend()
57
+
58
+ for i, container in enumerate(plot):
56
59
  if len(x_level) != len(container.patches):
57
60
  return None
58
61
  container_data = []
62
+
63
+ # Use hue category if available, otherwise fall back to container label
64
+ fill_value = hue_categories[i] if i < len(hue_categories) else container.get_label()
65
+
59
66
  for x, y in zip(x_level, container.patches):
60
67
  container_data.append(
61
68
  {
62
69
  MaidrKey.X.value: x,
63
- MaidrKey.FILL.value: container.get_label(),
70
+ MaidrKey.FILL.value: fill_value,
64
71
  MaidrKey.Y.value: float(y.get_height()),
65
72
  }
66
73
  )
67
74
  data.append(container_data)
68
75
 
69
76
  return data
77
+
78
+ def _extract_hue_categories_from_legend(self) -> list[str]:
79
+ """
80
+ Extract hue categories from the axes legend.
81
+
82
+ This method reads the legend text elements from the axes legend,
83
+ trims whitespace from each text, and returns a list of cleaned
84
+ category names. This is used to get the actual category names
85
+ instead of using the generic container labels like '_container0', '_container1'.
86
+
87
+ Parameters
88
+ ----------
89
+ None
90
+ This method uses the instance's axes object.
91
+
92
+ Returns
93
+ -------
94
+ list[str]
95
+ List of trimmed hue category names from the legend.
96
+ Returns empty list if no legend is found or if legend has no text elements.
97
+
98
+ Examples
99
+ --------
100
+ >>> # For a seaborn barplot with hue='category' and legend showing 'Below', 'Above'
101
+ >>> plot = GroupedBarPlot(ax, PlotType.DODGED)
102
+ >>> categories = plot._extract_hue_categories_from_legend()
103
+ >>> print(categories)
104
+ ['Below', 'Above']
105
+
106
+ >>> # If no legend exists
107
+ >>> categories = plot._extract_hue_categories_from_legend()
108
+ >>> print(categories)
109
+ []
110
+ """
111
+ legend = self.ax.get_legend()
112
+ if legend is None:
113
+ return []
114
+
115
+ # Get legend text elements
116
+ legend_texts = legend.get_texts()
117
+ if not legend_texts:
118
+ return []
119
+
120
+ # Extract text content from legend elements and trim whitespace
121
+ hue_categories = [text.get_text().strip() for text in legend_texts]
122
+
123
+ # Filter out empty strings and return
124
+ return [category for category in hue_categories if category]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maidr
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Multimodal Access and Interactive Data Representations
5
5
  Project-URL: Homepage, https://xability.github.io/py-maidr
6
6
  Project-URL: Repository, https://github.com/xability/py-maidr
@@ -1,9 +1,9 @@
1
- maidr/__init__.py,sha256=WHwg8BcJQIcf4dsUFOdKS_frxDfuHewDEGnBBg9j3WE,415
2
- maidr/api.py,sha256=od539V0CKcs15FEXQo0hX_hK_A0U3noHE54obdHcTqY,4138
1
+ maidr/__init__.py,sha256=szUjuM4cnGyzipfrIxpyZV841yhHjMmIcP7yXHq7kq8,415
2
+ maidr/api.py,sha256=WK7jfQttuPeF1q45RuF_wTciYyiFY6SyjfZVSOVkiUs,4316
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/figure_manager.py,sha256=t-lhe4jj2gsF5-8VUBUZOPlDutKjm_AZ8xXWJU2pFRc,5555
6
- maidr/core/maidr.py,sha256=0MuyPgc1CFomA87jq70GG-MSxYaMI2Z7ct2FAOGCxQM,20067
6
+ maidr/core/maidr.py,sha256=Ye2C8tQPY1o8gfURiTyDPMWFoRT0toZOOCugKAz4uQU,22370
7
7
  maidr/core/enum/__init__.py,sha256=9ee78L0dlxEx4ulUGVlD-J23UcUZmrGu0rXms54up3c,93
8
8
  maidr/core/enum/library.py,sha256=e8ujT_L-McJWfoVJd1ty9K_2bwITnf1j0GPLsnAcHes,104
9
9
  maidr/core/enum/maidr_key.py,sha256=ljG0omqzd8K8Yk213N7i7PXGvG-IOlnE5v7o6RoGJzc,795
@@ -13,7 +13,7 @@ maidr/core/plot/__init__.py,sha256=xDIpRGM-4DfaSSL3nKcXrjdMecCHJ6en4K4nA_fPefQ,8
13
13
  maidr/core/plot/barplot.py,sha256=0hBgp__putezvxXc9G3qmaktmAzze3cN8pQMD9iqktE,2116
14
14
  maidr/core/plot/boxplot.py,sha256=i11GdNuz_c-hilmhydu3ah-bzyVdFoBkNvRi5lpMrrY,9946
15
15
  maidr/core/plot/candlestick.py,sha256=ofvlUwtzaaopvv6VjNDf1IZODbu1UkMHsi1zdvcG-Yo,10120
16
- maidr/core/plot/grouped_barplot.py,sha256=_zn4XMeEnSiDHtf6t4-z9ErBqg_CijhAS2CCtlHgYIQ,2077
16
+ maidr/core/plot/grouped_barplot.py,sha256=odZ52Pl22nb9cWKD3NGsZsyFDxXdBDAEcbOj66HKp9I,4063
17
17
  maidr/core/plot/heatmap.py,sha256=yMS-31tS2GW4peds9LtZesMxmmTV_YfqYO5M_t5KasQ,2521
18
18
  maidr/core/plot/histogram.py,sha256=QV5W-6ZJQQcZsrM91JJBX-ONktJzH7yg_et5_bBPfQQ,1525
19
19
  maidr/core/plot/lineplot.py,sha256=uoJpGkJB3IJSDJTwH6ECxLyXGdarsVQNULELp5NncWg,4522
@@ -52,7 +52,7 @@ maidr/util/mixin/extractor_mixin.py,sha256=j2Rv2vh_gqqcxLV1ka3xsPaPAfWsX94CtKIW2
52
52
  maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
53
53
  maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
55
- maidr-1.8.0.dist-info/METADATA,sha256=JSCtmOqsD8Mka8aDihHcqHs4wJ9Yc4eBYV3by-pQSl0,3154
56
- maidr-1.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- maidr-1.8.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
- maidr-1.8.0.dist-info/RECORD,,
55
+ maidr-1.9.0.dist-info/METADATA,sha256=EK0mJx_hRNjiVvAnGcph7sBceEbiQrgvioGFPcw8-EY,3154
56
+ maidr-1.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ maidr-1.9.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
+ maidr-1.9.0.dist-info/RECORD,,
File without changes