maidr 1.10.0__py3-none-any.whl → 1.11.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.10.0"
1
+ __version__ = "1.11.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -11,6 +11,7 @@ class MaidrKey(str, Enum):
11
11
  # Plot data keys.
12
12
  AXES = "axes"
13
13
  DATA = "data"
14
+ FORMAT = "format"
14
15
  POINTS = "points"
15
16
  LEVEL = "level"
16
17
  X = "x"
@@ -47,9 +47,10 @@ class CandlestickPlot(MaidrPlot):
47
47
  if self._maidr_body_collection:
48
48
  self._maidr_gid = self._maidr_body_collection.get_gid()
49
49
  self._maidr_body_gid = self._maidr_gid
50
- elif self._maidr_wick_collection:
51
- self._maidr_gid = self._maidr_wick_collection.get_gid()
52
- self._maidr_wick_gid = self._maidr_gid
50
+ if self._maidr_wick_collection:
51
+ self._maidr_wick_gid = self._maidr_wick_collection.get_gid()
52
+ if not self._maidr_gid:
53
+ self._maidr_gid = self._maidr_wick_gid
53
54
 
54
55
  def _extract_plot_data(self) -> list[dict]:
55
56
  """
@@ -147,11 +148,7 @@ class CandlestickPlot(MaidrPlot):
147
148
  return {MaidrKey.X: x_labels, MaidrKey.Y: self.ax.get_ylabel()}
148
149
 
149
150
  def _get_selector(self) -> Union[str, Dict[str, str]]:
150
- """Return selectors for highlighting candlestick elements.
151
-
152
- - Modern path (collections present): return a dict with separate selectors for body, wickLow, wickHigh
153
- - Legacy path: return a dict with body and shared wick selectors (no open/close keys)
154
- """
151
+ """Return selectors for highlighting candlestick elements."""
155
152
  # Modern path: build structured selectors using separate gids
156
153
  if (
157
154
  self._maidr_body_collection
@@ -190,12 +187,12 @@ class CandlestickPlot(MaidrPlot):
190
187
  }
191
188
  return selectors
192
189
 
193
- # Legacy path: build shared-id selectors; omit open/close
190
+ # Legacy path
194
191
  legacy_selectors = {}
195
- if getattr(self, "_maidr_body_gid", None) or self._maidr_gid:
196
- body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
192
+ if self._maidr_body_gid or self._maidr_gid:
193
+ body_gid = self._maidr_body_gid or self._maidr_gid
197
194
  legacy_selectors["body"] = f"g[id='{body_gid}'] > path"
198
- if getattr(self, "_maidr_wick_gid", None):
195
+ if self._maidr_wick_gid:
199
196
  legacy_selectors["wick"] = f"g[id='{self._maidr_wick_gid}'] > path"
200
197
  if legacy_selectors:
201
198
  return legacy_selectors
@@ -207,9 +204,12 @@ class CandlestickPlot(MaidrPlot):
207
204
  """Initialize the MAIDR schema dictionary with basic plot information."""
208
205
  base_schema = super().render()
209
206
  base_schema[MaidrKey.TITLE] = "Candlestick Chart"
210
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
207
+ # Update axes labels while preserving format from parent
208
+ axes_data = self._extract_axes_data()
209
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
210
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
211
+ base_schema[MaidrKey.AXES] = axes_data
211
212
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
212
- # Include selector only if the plot supports highlighting.
213
213
  if self._support_highlighting:
214
214
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
215
215
  return base_schema
@@ -5,12 +5,13 @@ from abc import ABC, abstractmethod
5
5
  from matplotlib.axes import Axes
6
6
 
7
7
  from maidr.core.enum import MaidrKey, PlotType
8
+ from maidr.util.mixin import FormatExtractorMixin
8
9
 
9
10
  # uuid is used to generate unique identifiers for each plot layer in the MAIDR schema.
10
11
  import uuid
11
12
 
12
13
 
13
- class MaidrPlot(ABC):
14
+ class MaidrPlot(ABC, FormatExtractorMixin):
14
15
  """
15
16
  Abstract base class for plots managed by the MAIDR system.
16
17
 
@@ -61,13 +62,21 @@ class MaidrPlot(ABC):
61
62
  """
62
63
  Generate the MAIDR schema for this plot layer, including a unique id for layer identification.
63
64
  """
65
+ # Extract axes data first
66
+ axes_data = self._extract_axes_data()
67
+
68
+ # Extract and include format configuration inside axes if available.
69
+ format_config = self.extract_format(self.ax)
70
+ if format_config:
71
+ axes_data[MaidrKey.FORMAT] = format_config
72
+
64
73
  # Generate a unique UUID for this layer to ensure each plot layer can be distinctly identified
65
74
  # in the MAIDR frontend. This supports robust layer switching.
66
75
  maidr_schema = {
67
76
  MaidrKey.ID: str(uuid.uuid4()),
68
77
  MaidrKey.TYPE: self.type,
69
78
  MaidrKey.TITLE: self.ax.get_title(),
70
- MaidrKey.AXES: self._extract_axes_data(),
79
+ MaidrKey.AXES: axes_data,
71
80
  MaidrKey.DATA: self._extract_plot_data(),
72
81
  }
73
82
 
@@ -142,7 +142,11 @@ class MplfinanceBarPlot(
142
142
  def render(self) -> dict:
143
143
  base_schema = super().render()
144
144
  base_schema[MaidrKey.TITLE] = "Volume Bar Plot"
145
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
145
+ # Update axes labels while preserving format from parent
146
+ axes_data = self._extract_axes_data()
147
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
148
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
149
+ base_schema[MaidrKey.AXES] = axes_data
146
150
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
147
151
  if self._support_highlighting:
148
152
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
@@ -192,7 +192,11 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
192
192
  def render(self) -> dict:
193
193
  base_schema = super().render()
194
194
  base_schema[MaidrKey.TITLE] = "Moving Average Line Plot"
195
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
195
+ # Update axes labels while preserving format from parent
196
+ axes_data = self._extract_axes_data()
197
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
198
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
199
+ base_schema[MaidrKey.AXES] = axes_data
196
200
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
197
201
  if self._support_highlighting:
198
202
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
@@ -6,16 +6,18 @@ from datetime import datetime
6
6
 
7
7
  class DatetimeConverter:
8
8
  """
9
- Datetime converter for mplfinance plots.
9
+ Enhanced datetime converter that automatically detects time periods
10
+ and provides intelligent date/time formatting for mplfinance plots.
10
11
 
11
- This utility provides datetime value conversion for financial data visualization.
12
+ This utility automatically detects the time period of financial data and formats
13
+ datetime values consistently for screen reader accessibility and visual clarity.
12
14
 
13
15
  Parameters
14
16
  ----------
15
17
  data : pd.DataFrame
16
18
  DataFrame with DatetimeIndex containing financial data.
17
19
  datetime_format : str, optional
18
- Custom datetime format string (currently unused, kept for compatibility).
20
+ Custom datetime format string. If None, automatic format detection is used.
19
21
 
20
22
  Attributes
21
23
  ----------
@@ -162,7 +164,9 @@ class DatetimeConverter:
162
164
 
163
165
  def get_formatted_datetime(self, index: int) -> Optional[str]:
164
166
  """
165
- Get datetime string for given index.
167
+ Get formatted datetime string for given index using consistent formatting.
168
+
169
+ Always includes year for screen reader accessibility.
166
170
 
167
171
  Parameters
168
172
  ----------
@@ -172,13 +176,13 @@ class DatetimeConverter:
172
176
  Returns
173
177
  -------
174
178
  str or None
175
- Datetime string or None if index is invalid.
179
+ Formatted datetime string or None if index is invalid.
176
180
 
177
181
  Examples
178
182
  --------
179
183
  >>> converter = create_datetime_converter(df)
180
184
  >>> formatted = converter.get_formatted_datetime(0)
181
- >>> print(formatted) # "2024-01-15 00:00:00"
185
+ >>> print(formatted) # "2024-01-15 00:00:00" (plain datetime string)
182
186
  """
183
187
  if index not in self.date_mapping:
184
188
  return None
@@ -188,7 +192,7 @@ class DatetimeConverter:
188
192
 
189
193
  def _format_datetime_custom(self, dt: datetime) -> str:
190
194
  """
191
- Format datetime as-is using ISO format.
195
+ Return plain datetime string representation.
192
196
 
193
197
  Parameters
194
198
  ----------
@@ -198,14 +202,13 @@ class DatetimeConverter:
198
202
  Returns
199
203
  -------
200
204
  str
201
- Formatted datetime string in ISO format.
205
+ Plain string representation of datetime (ISO format).
202
206
 
203
207
  Notes
204
208
  -----
205
- Returns the datetime as a string without smart formatting.
206
- Output format is "YYYY-MM-DD HH:MM:SS" (e.g., "2024-01-15 00:00:00").
209
+ Returns the raw string representation of the datetime object,
210
+ allowing the frontend to handle formatting as needed.
207
211
  """
208
- # Return string representation of datetime
209
212
  return str(dt)
210
213
 
211
214
  @property
@@ -0,0 +1,679 @@
1
+ """
2
+ Format configuration utilities for extracting and representing axis formatting.
3
+
4
+ This module provides classes and utilities for detecting matplotlib axis formatters
5
+ and converting them to MAIDR-compatible format configurations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from typing import Any, Dict, Optional
14
+
15
+ from matplotlib.axes import Axes
16
+ from matplotlib.dates import DateFormatter
17
+ from matplotlib.ticker import (
18
+ Formatter,
19
+ FuncFormatter,
20
+ PercentFormatter,
21
+ ScalarFormatter,
22
+ StrMethodFormatter,
23
+ FormatStrFormatter,
24
+ )
25
+
26
+
27
+ class FormatType(str, Enum):
28
+ """
29
+ Enumeration of supported format types for MAIDR.
30
+
31
+ These types correspond to the formatting options supported by
32
+ the MAIDR JavaScript library's FormatterService.
33
+ """
34
+
35
+ CURRENCY = "currency"
36
+ PERCENT = "percent"
37
+ DATE = "date"
38
+ NUMBER = "number"
39
+ SCIENTIFIC = "scientific"
40
+ FIXED = "fixed"
41
+
42
+
43
+ @dataclass
44
+ class FormatConfig:
45
+ """
46
+ Configuration for axis value formatting.
47
+
48
+ This class represents a format configuration that can be serialized
49
+ to the MAIDR schema format. It supports both type-based formatting
50
+ and custom JavaScript function bodies.
51
+
52
+ Parameters
53
+ ----------
54
+ type : FormatType, optional
55
+ The type of formatting to apply.
56
+ function : str, optional
57
+ JavaScript function body for custom formatting.
58
+ This is evaluated by MAIDR JS using: new Function('value', functionBody)
59
+ Example: "return parseFloat(value).toFixed(2)"
60
+ decimals : int, optional
61
+ Number of decimal places to display.
62
+ currency : str, optional
63
+ Currency code (e.g., "USD", "EUR") for currency formatting.
64
+ locale : str, optional
65
+ BCP 47 locale string (e.g., "en-US") for locale-specific formatting.
66
+ dateFormat : str, optional
67
+ Date format string (e.g., "%b %d" for "Jan 15") for date formatting.
68
+
69
+ Examples
70
+ --------
71
+ >>> config = FormatConfig(type=FormatType.CURRENCY, decimals=2, currency="USD")
72
+ >>> config.to_dict()
73
+ {'type': 'currency', 'decimals': 2, 'currency': 'USD'}
74
+
75
+ >>> config = FormatConfig(function="return '$' + parseFloat(value).toFixed(2)")
76
+ >>> config.to_dict()
77
+ {'function': "return '$' + parseFloat(value).toFixed(2)"}
78
+ """
79
+
80
+ type: Optional[FormatType] = None
81
+ function: Optional[str] = None
82
+ decimals: Optional[int] = None
83
+ currency: Optional[str] = None
84
+ locale: Optional[str] = None
85
+ dateFormat: Optional[str] = None
86
+
87
+ def to_dict(self) -> Dict[str, Any]:
88
+ """
89
+ Convert the format configuration to a dictionary.
90
+
91
+ Returns
92
+ -------
93
+ Dict[str, Any]
94
+ Dictionary representation suitable for MAIDR schema.
95
+ Only includes non-None values. If function is provided,
96
+ it takes precedence over type-based formatting.
97
+ """
98
+ result: Dict[str, Any] = {}
99
+
100
+ # Function takes precedence - if provided, use it directly
101
+ if self.function is not None:
102
+ result["function"] = self.function
103
+ return result
104
+
105
+ # Otherwise use type-based formatting
106
+ if self.type is not None:
107
+ result["type"] = self.type.value
108
+
109
+ if self.decimals is not None:
110
+ result["decimals"] = self.decimals
111
+ if self.currency is not None:
112
+ result["currency"] = self.currency
113
+ if self.locale is not None:
114
+ result["locale"] = self.locale
115
+ if self.dateFormat is not None:
116
+ result["dateFormat"] = self.dateFormat
117
+
118
+ return result
119
+
120
+
121
+ class JSBodyConverter:
122
+ """
123
+ Converter for generating JavaScript function bodies from matplotlib formatters.
124
+
125
+ These function bodies are evaluated by MAIDR JS using:
126
+ new Function('value', functionBody)
127
+ """
128
+
129
+ # Mapping of Python strftime codes to JavaScript date formatting
130
+ # Common patterns with optimized JS bodies
131
+ STRFTIME_PATTERNS: Dict[str, str] = {
132
+ "%b %d": (
133
+ "var m=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];"
134
+ "var d=new Date(value);return m[d.getMonth()]+' '+d.getDate()"
135
+ ),
136
+ "%b %d, %Y": (
137
+ "var m=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];"
138
+ "var d=new Date(value);return m[d.getMonth()]+' '+d.getDate()+', '+d.getFullYear()"
139
+ ),
140
+ "%Y-%m-%d": (
141
+ "var d=new Date(value);"
142
+ "return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'"
143
+ "+String(d.getDate()).padStart(2,'0')"
144
+ ),
145
+ "%m/%d/%Y": (
146
+ "var d=new Date(value);"
147
+ "return String(d.getMonth()+1).padStart(2,'0')+'/'"
148
+ "+String(d.getDate()).padStart(2,'0')+'/'+d.getFullYear()"
149
+ ),
150
+ "%d/%m/%Y": (
151
+ "var d=new Date(value);"
152
+ "return String(d.getDate()).padStart(2,'0')+'/'"
153
+ "+String(d.getMonth()+1).padStart(2,'0')+'/'+d.getFullYear()"
154
+ ),
155
+ "%Y": "return new Date(value).getFullYear().toString()",
156
+ "%B %Y": (
157
+ "var m=['January','February','March','April','May','June',"
158
+ "'July','August','September','October','November','December'];"
159
+ "var d=new Date(value);return m[d.getMonth()]+' '+d.getFullYear()"
160
+ ),
161
+ "%H:%M": (
162
+ "var d=new Date(value);"
163
+ "return String(d.getHours()).padStart(2,'0')+':'"
164
+ "+String(d.getMinutes()).padStart(2,'0')"
165
+ ),
166
+ "%H:%M:%S": (
167
+ "var d=new Date(value);"
168
+ "return String(d.getHours()).padStart(2,'0')+':'"
169
+ "+String(d.getMinutes()).padStart(2,'0')+':'"
170
+ "+String(d.getSeconds()).padStart(2,'0')"
171
+ ),
172
+ "%I:%M %p": (
173
+ "var d=new Date(value);"
174
+ "var h=d.getHours();var ampm=h>=12?'PM':'AM';h=h%12;h=h?h:12;"
175
+ "return String(h).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+' '+ampm"
176
+ ),
177
+ }
178
+
179
+ @staticmethod
180
+ def date_format_to_js(fmt: str) -> str:
181
+ """
182
+ Convert Python strftime format to JavaScript function body.
183
+
184
+ Parameters
185
+ ----------
186
+ fmt : str
187
+ Python strftime format string (e.g., '%b %d', '%Y-%m-%d')
188
+
189
+ Returns
190
+ -------
191
+ str
192
+ JavaScript function body that formats dates similarly.
193
+ """
194
+ if fmt in JSBodyConverter.STRFTIME_PATTERNS:
195
+ return JSBodyConverter.STRFTIME_PATTERNS[fmt]
196
+
197
+ # Fallback: use toLocaleString for unrecognized formats
198
+ return "return new Date(value).toLocaleDateString()"
199
+
200
+ @staticmethod
201
+ def currency_format_to_js(symbol: str, decimals: int = 2) -> str:
202
+ """
203
+ Convert currency format to JavaScript function body.
204
+
205
+ Parameters
206
+ ----------
207
+ symbol : str
208
+ Currency symbol ($, €, £, ¥)
209
+ decimals : int
210
+ Number of decimal places
211
+
212
+ Returns
213
+ -------
214
+ str
215
+ JavaScript function body for currency formatting.
216
+ """
217
+ # Map symbols to locales
218
+ locale_map = {
219
+ "$": "en-US",
220
+ "€": "de-DE",
221
+ "£": "en-GB",
222
+ "¥": "ja-JP",
223
+ }
224
+ locale = locale_map.get(symbol, "en-US")
225
+
226
+ # Yen typically has no decimals
227
+ if symbol == "¥":
228
+ decimals = 0
229
+
230
+ return (
231
+ f"return '{symbol}'+parseFloat(value).toLocaleString('{locale}',"
232
+ f"{{minimumFractionDigits:{decimals},maximumFractionDigits:{decimals}}})"
233
+ )
234
+
235
+ @staticmethod
236
+ def number_format_to_js(decimals: Optional[int] = None) -> str:
237
+ """
238
+ Convert number format (with thousands separator) to JavaScript function body.
239
+
240
+ Parameters
241
+ ----------
242
+ decimals : int, optional
243
+ Number of decimal places
244
+
245
+ Returns
246
+ -------
247
+ str
248
+ JavaScript function body for number formatting.
249
+ """
250
+ if decimals is not None:
251
+ return (
252
+ f"return parseFloat(value).toLocaleString('en-US',"
253
+ f"{{minimumFractionDigits:{decimals},maximumFractionDigits:{decimals}}})"
254
+ )
255
+ return "return parseFloat(value).toLocaleString('en-US')"
256
+
257
+ @staticmethod
258
+ def fixed_format_to_js(decimals: int) -> str:
259
+ """
260
+ Convert fixed decimal format to JavaScript function body.
261
+
262
+ Parameters
263
+ ----------
264
+ decimals : int
265
+ Number of decimal places
266
+
267
+ Returns
268
+ -------
269
+ str
270
+ JavaScript function body for fixed decimal formatting.
271
+ """
272
+ return f"return parseFloat(value).toFixed({decimals})"
273
+
274
+ @staticmethod
275
+ def percent_format_to_js(decimals: int = 1, multiply: bool = True) -> str:
276
+ """
277
+ Convert percent format to JavaScript function body.
278
+
279
+ Parameters
280
+ ----------
281
+ decimals : int
282
+ Number of decimal places
283
+ multiply : bool
284
+ Whether to multiply by 100 (for values stored as decimals)
285
+
286
+ Returns
287
+ -------
288
+ str
289
+ JavaScript function body for percent formatting.
290
+ """
291
+ if multiply:
292
+ return f"return (parseFloat(value)*100).toFixed({decimals})+'%'"
293
+ return f"return parseFloat(value).toFixed({decimals})+'%'"
294
+
295
+ @staticmethod
296
+ def scientific_format_to_js(decimals: int = 2) -> str:
297
+ """
298
+ Convert scientific notation format to JavaScript function body.
299
+
300
+ Parameters
301
+ ----------
302
+ decimals : int
303
+ Number of decimal places in mantissa
304
+
305
+ Returns
306
+ -------
307
+ str
308
+ JavaScript function body for scientific notation formatting.
309
+ """
310
+ return f"return parseFloat(value).toExponential({decimals})"
311
+
312
+ @staticmethod
313
+ def default_format_to_js() -> str:
314
+ """
315
+ Return default JavaScript function body for unsupported formatters.
316
+
317
+ Returns
318
+ -------
319
+ str
320
+ JavaScript function body that returns value as string.
321
+ """
322
+ return "return String(value)"
323
+
324
+
325
+ class FormatConfigBuilder:
326
+ """
327
+ Builder for extracting format configurations from matplotlib formatters.
328
+
329
+ This class provides static methods to detect and parse matplotlib axis
330
+ formatters and convert them to FormatConfig objects with JavaScript
331
+ function bodies for MAIDR JS evaluation.
332
+
333
+ Examples
334
+ --------
335
+ >>> from matplotlib import pyplot as plt
336
+ >>> fig, ax = plt.subplots()
337
+ >>> ax.yaxis.set_major_formatter('${x:,.2f}')
338
+ >>> config = FormatConfigBuilder.from_formatter(ax.yaxis.get_major_formatter())
339
+ >>> config.function
340
+ "return '$'+parseFloat(value).toLocaleString('en-US',{minimumFractionDigits:2,maximumFractionDigits:2})"
341
+ """
342
+
343
+ # Currency symbol patterns for detection
344
+ CURRENCY_PATTERNS = {
345
+ "$": "USD",
346
+ "USD": "USD",
347
+ "€": "EUR",
348
+ "EUR": "EUR",
349
+ "£": "GBP",
350
+ "GBP": "GBP",
351
+ "¥": "JPY",
352
+ "JPY": "JPY",
353
+ }
354
+
355
+ @staticmethod
356
+ def from_formatter(formatter: Optional[Formatter]) -> Optional[FormatConfig]:
357
+ """
358
+ Create a FormatConfig from a matplotlib Formatter.
359
+
360
+ Parameters
361
+ ----------
362
+ formatter : Formatter or None
363
+ A matplotlib ticker Formatter object, or None.
364
+
365
+ Returns
366
+ -------
367
+ FormatConfig or None
368
+ The detected format configuration, or None if the formatter
369
+ type could not be determined.
370
+ """
371
+ if formatter is None:
372
+ return None
373
+
374
+ # Check for DateFormatter (for date/time axes)
375
+ if isinstance(formatter, DateFormatter):
376
+ return FormatConfigBuilder._parse_date_formatter(formatter)
377
+
378
+ # Check for PercentFormatter
379
+ if isinstance(formatter, PercentFormatter):
380
+ return FormatConfigBuilder._parse_percent_formatter(formatter)
381
+
382
+ # Check for StrMethodFormatter (most common for custom formats)
383
+ if isinstance(formatter, StrMethodFormatter):
384
+ return FormatConfigBuilder._parse_str_method_formatter(formatter)
385
+
386
+ # Check for FormatStrFormatter (old-style % formatting)
387
+ if isinstance(formatter, FormatStrFormatter):
388
+ return FormatConfigBuilder._parse_format_str_formatter(formatter)
389
+
390
+ # Check for ScalarFormatter with scientific notation
391
+ if isinstance(formatter, ScalarFormatter):
392
+ return FormatConfigBuilder._parse_scalar_formatter(formatter)
393
+
394
+ # Check for FuncFormatter (custom function)
395
+ if isinstance(formatter, FuncFormatter):
396
+ return FormatConfigBuilder._parse_func_formatter(formatter)
397
+
398
+ return None
399
+
400
+ @staticmethod
401
+ def _parse_date_formatter(formatter: DateFormatter) -> FormatConfig:
402
+ """Parse a DateFormatter to FormatConfig with JS function body."""
403
+ date_format = None
404
+ if hasattr(formatter, "fmt") and formatter.fmt is not None:
405
+ date_format = formatter.fmt
406
+
407
+ # Generate JS function body for date formatting
408
+ js_body = JSBodyConverter.date_format_to_js(date_format) if date_format else None
409
+
410
+ if js_body:
411
+ return FormatConfig(function=js_body)
412
+
413
+ # Fallback to type-based config
414
+ return FormatConfig(type=FormatType.DATE, dateFormat=date_format)
415
+
416
+ @staticmethod
417
+ def _parse_percent_formatter(formatter: PercentFormatter) -> FormatConfig:
418
+ """Parse a PercentFormatter to FormatConfig using type-based preset."""
419
+ decimals = None
420
+ if hasattr(formatter, "decimals") and formatter.decimals is not None:
421
+ decimals = int(formatter.decimals)
422
+
423
+ # Use type-based preset for percent
424
+ return FormatConfig(type=FormatType.PERCENT, decimals=decimals)
425
+
426
+ @staticmethod
427
+ def _parse_str_method_formatter(
428
+ formatter: StrMethodFormatter,
429
+ ) -> Optional[FormatConfig]:
430
+ """Parse a StrMethodFormatter to FormatConfig using hybrid approach."""
431
+ fmt = getattr(formatter, "fmt", None)
432
+ if fmt is None:
433
+ return None
434
+
435
+ return FormatConfigBuilder._parse_format_string_hybrid(fmt)
436
+
437
+ @staticmethod
438
+ def _parse_format_str_formatter(
439
+ formatter: FormatStrFormatter,
440
+ ) -> Optional[FormatConfig]:
441
+ """Parse a FormatStrFormatter (old-style %) to FormatConfig using type-based presets."""
442
+ fmt = getattr(formatter, "fmt", None)
443
+ if fmt is None:
444
+ return None
445
+
446
+ # Convert old-style format to type-based config
447
+ # e.g., "%.2f" -> fixed with 2 decimals
448
+ match = re.search(r"%\.?(\d*)([efg])", fmt, re.IGNORECASE)
449
+ if match:
450
+ decimals_str = match.group(1)
451
+ format_char = match.group(2).lower()
452
+
453
+ decimals = int(decimals_str) if decimals_str else None
454
+
455
+ if format_char == "e":
456
+ return FormatConfig(type=FormatType.SCIENTIFIC, decimals=decimals)
457
+ elif format_char in ("f", "g"):
458
+ return FormatConfig(type=FormatType.FIXED, decimals=decimals)
459
+
460
+ return None
461
+
462
+ @staticmethod
463
+ def _parse_scalar_formatter(formatter: ScalarFormatter) -> Optional[FormatConfig]:
464
+ """Parse a ScalarFormatter to FormatConfig using type-based preset.
465
+
466
+ ScalarFormatter is the default matplotlib formatter and often has _scientific=True
467
+ due to auto-detection. We only return a FormatConfig if useMathText is explicitly
468
+ enabled, as this indicates the user wants formatted scientific notation display.
469
+ """
470
+ # useMathText is only True when explicitly set by user via set_useMathText(True)
471
+ # This provides nice-looking scientific notation like 10^6 instead of 1e6
472
+ use_math_text = getattr(formatter, "_useMathText", False)
473
+ if use_math_text is True:
474
+ return FormatConfig(type=FormatType.SCIENTIFIC)
475
+
476
+ # Default ScalarFormatter - no explicit format configured by user
477
+ # We ignore _scientific since matplotlib auto-sets it based on data magnitude
478
+ return None
479
+
480
+ @staticmethod
481
+ def _parse_func_formatter(formatter: FuncFormatter) -> Optional[FormatConfig]:
482
+ """
483
+ Attempt to parse a FuncFormatter by examining the function.
484
+
485
+ This is a best-effort approach since FuncFormatter can contain
486
+ arbitrary functions. For unsupported functions, returns a default
487
+ formatter that shows the value as-is.
488
+ """
489
+ func = getattr(formatter, "func", None)
490
+ if func is None:
491
+ return None
492
+
493
+ # Try to get function source or docstring for hints
494
+ func_name = getattr(func, "__name__", "")
495
+
496
+ # Common naming conventions - generate JS function bodies
497
+ if "percent" in func_name.lower():
498
+ js_body = JSBodyConverter.percent_format_to_js(1, multiply=True)
499
+ return FormatConfig(function=js_body)
500
+ elif "currency" in func_name.lower() or "dollar" in func_name.lower():
501
+ js_body = JSBodyConverter.currency_format_to_js("$", 2)
502
+ return FormatConfig(function=js_body)
503
+ elif "date" in func_name.lower() or "time" in func_name.lower():
504
+ js_body = "return new Date(value).toLocaleDateString()"
505
+ return FormatConfig(function=js_body)
506
+
507
+ # For unknown FuncFormatters, return default (value as-is)
508
+ # This ensures the value is still displayed
509
+ return FormatConfig(function=JSBodyConverter.default_format_to_js())
510
+
511
+ @staticmethod
512
+ def _parse_format_string_hybrid(fmt: str) -> Optional[FormatConfig]:
513
+ """
514
+ Parse a format string using hybrid approach: type-based presets for simple
515
+ formats, JS functions for complex formats.
516
+
517
+ Parameters
518
+ ----------
519
+ fmt : str
520
+ A Python format string (e.g., "${x:,.2f}", "{x:.1%}").
521
+
522
+ Returns
523
+ -------
524
+ FormatConfig or None
525
+ The detected format configuration using appropriate approach.
526
+ """
527
+ if not fmt:
528
+ return None
529
+
530
+ decimals = FormatConfigBuilder._extract_decimals(fmt)
531
+
532
+ # Detect currency by symbol prefix - use type-based preset
533
+ for symbol, currency_code in FormatConfigBuilder.CURRENCY_PATTERNS.items():
534
+ if symbol in fmt:
535
+ return FormatConfig(
536
+ type=FormatType.CURRENCY, decimals=decimals, currency=currency_code
537
+ )
538
+
539
+ # Detect percent format (ends with %) like {x:.1%} - use type-based preset
540
+ if "%" in fmt and "{" in fmt:
541
+ if re.search(r"\{[^}]*%\}", fmt):
542
+ return FormatConfig(type=FormatType.PERCENT, decimals=decimals)
543
+
544
+ # Detect scientific notation like {x:.2e} - use type-based preset
545
+ if re.search(r"\{[^}]*[eE]\}", fmt):
546
+ return FormatConfig(type=FormatType.SCIENTIFIC, decimals=decimals)
547
+
548
+ # Detect number format with comma separators like {x:,.2f} or {x:,}
549
+ # - use type-based preset
550
+ if re.search(r"\{[^}]*,", fmt):
551
+ return FormatConfig(type=FormatType.NUMBER, decimals=decimals)
552
+
553
+ # Detect fixed-point format (no comma separator) like {x:.2f}
554
+ # - use type-based preset
555
+ match = re.search(r"\{[^}]*\.(\d+)f\}", fmt)
556
+ if match:
557
+ decimals = int(match.group(1))
558
+ return FormatConfig(type=FormatType.FIXED, decimals=decimals)
559
+
560
+ # No recognized format - return None (don't add format config)
561
+ return None
562
+
563
+ @staticmethod
564
+ def _parse_format_string(fmt: str) -> Optional[FormatConfig]:
565
+ """
566
+ Parse a format string to detect the format type and options.
567
+ (Legacy method - kept for backwards compatibility)
568
+
569
+ Parameters
570
+ ----------
571
+ fmt : str
572
+ A Python format string (e.g., "${x:,.2f}", "{x:.1%}").
573
+
574
+ Returns
575
+ -------
576
+ FormatConfig or None
577
+ The detected format configuration.
578
+ """
579
+ if not fmt:
580
+ return None
581
+
582
+ # Detect currency by symbol prefix
583
+ for symbol, currency_code in FormatConfigBuilder.CURRENCY_PATTERNS.items():
584
+ if symbol in fmt:
585
+ decimals = FormatConfigBuilder._extract_decimals(fmt)
586
+ return FormatConfig(
587
+ type=FormatType.CURRENCY, decimals=decimals, currency=currency_code
588
+ )
589
+
590
+ # Detect percent format (ends with %)
591
+ if "%" in fmt and "{" in fmt:
592
+ # Check if it's a percent format like {x:.1%}
593
+ if re.search(r"\{[^}]*%\}", fmt):
594
+ decimals = FormatConfigBuilder._extract_decimals(fmt)
595
+ return FormatConfig(type=FormatType.PERCENT, decimals=decimals)
596
+
597
+ # Detect scientific notation
598
+ if re.search(r"\{[^}]*[eE]\}", fmt):
599
+ decimals = FormatConfigBuilder._extract_decimals(fmt)
600
+ return FormatConfig(type=FormatType.SCIENTIFIC, decimals=decimals)
601
+
602
+ # Detect number format with comma separators (must check before fixed-point)
603
+ # This matches formats like {x:,.2f} or {x:,}
604
+ if re.search(r"\{[^}]*,", fmt):
605
+ decimals = FormatConfigBuilder._extract_decimals(fmt)
606
+ return FormatConfig(type=FormatType.NUMBER, decimals=decimals)
607
+
608
+ # Detect fixed-point format (no comma separator)
609
+ match = re.search(r"\{[^}]*\.(\d+)f\}", fmt)
610
+ if match:
611
+ decimals = int(match.group(1))
612
+ return FormatConfig(type=FormatType.FIXED, decimals=decimals)
613
+
614
+ return None
615
+
616
+ @staticmethod
617
+ def _extract_decimals(fmt: str) -> Optional[int]:
618
+ """
619
+ Extract the number of decimal places from a format string.
620
+
621
+ Parameters
622
+ ----------
623
+ fmt : str
624
+ A Python format string.
625
+
626
+ Returns
627
+ -------
628
+ int or None
629
+ The number of decimal places, or None if not specified.
630
+ """
631
+ # Match patterns like .2f, .1%, .3e
632
+ match = re.search(r"\.(\d+)[fFeE%]", fmt)
633
+ if match:
634
+ return int(match.group(1))
635
+ return None
636
+
637
+
638
+ def extract_axis_format(ax: Optional[Axes]) -> Dict[str, Dict[str, Any]]:
639
+ """
640
+ Extract format configurations from both axes of a plot.
641
+
642
+ Parameters
643
+ ----------
644
+ ax : Axes or None
645
+ The matplotlib Axes object to extract formats from, or None.
646
+
647
+ Returns
648
+ -------
649
+ Dict[str, Dict[str, Any]]
650
+ Dictionary with 'x' and/or 'y' keys containing format configurations.
651
+ Only includes axes where a format could be detected.
652
+
653
+ Examples
654
+ --------
655
+ >>> from matplotlib import pyplot as plt
656
+ >>> fig, ax = plt.subplots()
657
+ >>> ax.yaxis.set_major_formatter('${x:,.2f}')
658
+ >>> formats = extract_axis_format(ax)
659
+ >>> formats
660
+ {'y': {'type': 'currency', 'decimals': 2, 'currency': 'USD'}}
661
+ """
662
+ if ax is None:
663
+ return {}
664
+
665
+ result: Dict[str, Dict[str, Any]] = {}
666
+
667
+ # Extract X-axis format
668
+ x_formatter = ax.xaxis.get_major_formatter()
669
+ x_config = FormatConfigBuilder.from_formatter(x_formatter)
670
+ if x_config is not None:
671
+ result["x"] = x_config.to_dict()
672
+
673
+ # Extract Y-axis format
674
+ y_formatter = ax.yaxis.get_major_formatter()
675
+ y_config = FormatConfigBuilder.from_formatter(y_formatter)
676
+ if y_config is not None:
677
+ result["y"] = y_config.to_dict()
678
+
679
+ return result
@@ -5,4 +5,15 @@ from .extractor_mixin import (
5
5
  LineExtractorMixin,
6
6
  ScalarMappableExtractorMixin,
7
7
  )
8
+ from .format_extractor_mixin import FormatExtractorMixin
8
9
  from .merger_mixin import DictMergerMixin
10
+
11
+ __all__ = [
12
+ "CollectionExtractorMixin",
13
+ "ContainerExtractorMixin",
14
+ "LevelExtractorMixin",
15
+ "LineExtractorMixin",
16
+ "ScalarMappableExtractorMixin",
17
+ "FormatExtractorMixin",
18
+ "DictMergerMixin",
19
+ ]
@@ -0,0 +1,83 @@
1
+ """
2
+ Mixin for extracting format configuration from matplotlib axes.
3
+
4
+ This module provides a mixin class that can be used by plot classes
5
+ to extract axis formatting information for the MAIDR schema.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+ from matplotlib.axes import Axes
13
+
14
+ from maidr.util.format_config import extract_axis_format
15
+
16
+
17
+ class FormatExtractorMixin:
18
+ """
19
+ Mixin class for extracting format configuration from axes.
20
+
21
+ This mixin provides methods to detect and extract formatting
22
+ configurations from matplotlib axis formatters, which can then
23
+ be included in the MAIDR schema.
24
+
25
+ Examples
26
+ --------
27
+ >>> class MyPlot(MaidrPlot, FormatExtractorMixin):
28
+ ... def render(self):
29
+ ... schema = super().render()
30
+ ... format_config = self.extract_format(self.ax)
31
+ ... if format_config:
32
+ ... schema["format"] = format_config
33
+ ... return schema
34
+ """
35
+
36
+ @staticmethod
37
+ def extract_format(ax: Axes) -> Optional[Dict[str, Dict[str, Any]]]:
38
+ """
39
+ Extract format configuration from an axes object.
40
+
41
+ This method detects matplotlib formatters applied to the x and y axes
42
+ and converts them to MAIDR-compatible format configurations.
43
+
44
+ Parameters
45
+ ----------
46
+ ax : Axes
47
+ The matplotlib Axes object to extract formats from.
48
+
49
+ Returns
50
+ -------
51
+ Dict[str, Dict[str, Any]] or None
52
+ Dictionary with 'x' and/or 'y' keys containing format configurations,
53
+ or None if no formats could be detected.
54
+
55
+ Notes
56
+ -----
57
+ Supported formatter types:
58
+ - StrMethodFormatter: Detected by parsing format string
59
+ - PercentFormatter: Detected as percent type
60
+ - ScalarFormatter: Detected for scientific notation
61
+ - FormatStrFormatter: Detected by parsing old-style format string
62
+
63
+ Format detection examples:
64
+ - "${x:,.2f}" -> {"type": "currency", "currency": "USD", "decimals": 2}
65
+ - "{x:.1%}" -> {"type": "percent", "decimals": 1}
66
+ - "{x:.2e}" -> {"type": "scientific", "decimals": 2}
67
+
68
+ Examples
69
+ --------
70
+ >>> from matplotlib import pyplot as plt
71
+ >>> fig, ax = plt.subplots()
72
+ >>> ax.yaxis.set_major_formatter('${x:,.2f}')
73
+ >>> format_config = FormatExtractorMixin.extract_format(ax)
74
+ >>> format_config
75
+ {'y': {'type': 'currency', 'decimals': 2, 'currency': 'USD'}}
76
+ """
77
+ if ax is None:
78
+ return None
79
+
80
+ format_config = extract_axis_format(ax)
81
+
82
+ # Return None if no formats were detected
83
+ return format_config if format_config else None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maidr
3
- Version: 1.10.0
3
+ Version: 1.11.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,4 +1,4 @@
1
- maidr/__init__.py,sha256=BXkAG-4A4pF_IjbBj6cOmPTmQYICXnfPniPJ-4OHnr0,416
1
+ maidr/__init__.py,sha256=F_rKqEP7cG3G5_iKehrmY967FA1fiwdVXlibD7MotpM,416
2
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
@@ -6,21 +6,21 @@ maidr/core/figure_manager.py,sha256=t-lhe4jj2gsF5-8VUBUZOPlDutKjm_AZ8xXWJU2pFRc,
6
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
- maidr/core/enum/maidr_key.py,sha256=ljG0omqzd8K8Yk213N7i7PXGvG-IOlnE5v7o6RoGJzc,795
9
+ maidr/core/enum/maidr_key.py,sha256=qO8dsRuMqiGmBHygSLRcu-wvfhch_3nY0PUvFBZkLH4,817
10
10
  maidr/core/enum/plot_type.py,sha256=7Orx3b_0NdpI_PtVJfLyJPh4qBqYMTsYBBr8VwOtiAM,347
11
11
  maidr/core/enum/smooth_keywords.py,sha256=z2kVZZ-mETWWh5reWu_hj9WkJD6WFj7_2-6s1e4C1JE,236
12
12
  maidr/core/plot/__init__.py,sha256=xDIpRGM-4DfaSSL3nKcXrjdMecCHJ6en4K4nA_fPefQ,83
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
- maidr/core/plot/candlestick.py,sha256=qOsYIbn2sRdGE2ES-YJ-Rwhu4NsScHs52mtKe6Bl7_A,8236
15
+ maidr/core/plot/candlestick.py,sha256=r_x3KVLNn6wFX7wYsRUFEp_rNTEjhbAkLk55kX2tIso,8170
16
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
20
- maidr/core/plot/maidr_plot.py,sha256=heotWue1IzMOXnpoHVCqKSW88Sfep1IuuP4MBarpSek,4231
20
+ maidr/core/plot/maidr_plot.py,sha256=_Jt0ILyUaMP68kX35Fgd16FcsizUAy52Y54YfVm2bEs,4580
21
21
  maidr/core/plot/maidr_plot_factory.py,sha256=NW2iFScswgXbAC9rAOo4iMkAFsjY43DAvFioGr0yzx4,2732
22
- maidr/core/plot/mplfinance_barplot.py,sha256=zhTp2i6BH0xn7vQvGTotKgu2HbzlKT4p6zA5CVUUHHc,5673
23
- maidr/core/plot/mplfinance_lineplot.py,sha256=pIbsusnQg1_GrstVVHfMw-t9yipWJowa9ZvGsiVV6l8,7329
22
+ maidr/core/plot/mplfinance_barplot.py,sha256=WSr5OPK05SEDK1VCjMeCb2vNWJgMcz2c8TTbTZqow7U,5944
23
+ maidr/core/plot/mplfinance_lineplot.py,sha256=bE6NHuUYzym_yG7WvjMTIDKOiiqSe67KvCtYHjLhPJ8,7600
24
24
  maidr/core/plot/regplot.py,sha256=b7u6bGTz1IxKahplNUrfwIr_OGSwMJ2BuLgFAVjL0s0,2744
25
25
  maidr/core/plot/scatterplot.py,sha256=o0i0uS-wXK9ZrENxneoHbh3-u-2goRONp19Yu9QLsaY,1257
26
26
  maidr/exception/__init__.py,sha256=PzaXoYBhyZxMDcJkuxJugDx7jZeseI0El6LpxIwXyG4,46
@@ -40,19 +40,21 @@ maidr/patch/mplfinance.py,sha256=ySD32onanoMgdQkV6XlSAbVd_BQuLWuEQtpkYSEDSzA,949
40
40
  maidr/patch/regplot.py,sha256=k86ekd0E4XJ_L1u85zObuDnxuXlM83z7tKtyXRTj2rI,3240
41
41
  maidr/patch/scatterplot.py,sha256=kln6zZwjVsdQzICalo-RnBOJrx1BnIB2xYUwItHvSNY,525
42
42
  maidr/util/__init__.py,sha256=eRJZfRpDX-n7UoV3JXw_9Lbfu_qNl_D0W1UTvLL-Iv4,81
43
- maidr/util/datetime_conversion.py,sha256=BF115xweGcrKyDnnjYPeScc0WgeNpCylV0Z-mYKaP4w,13769
43
+ maidr/util/datetime_conversion.py,sha256=7L8gYw3Fb7oL1swiMRpXgFO06U473vbnJ81Bexnyb2o,14043
44
44
  maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
45
45
  maidr/util/environment.py,sha256=C4VMyB16mqzrFxpJdxFdm40M0IZojxh60UX80680jgo,9403
46
+ maidr/util/format_config.py,sha256=mU4BEZJ05FFYTk8mbDQrjADLn486Blb7_eKZQZORvYA,23438
46
47
  maidr/util/mplfinance_utils.py,sha256=00YEjrCUbigZZL1j9jzOTamNnwfy5ZZmXJj65AhgNbw,3662
47
48
  maidr/util/plot_detection.py,sha256=bgLHoDcHSRwOiyKzUK3EqGwdAIhF44ocHW5ox6xYGZw,3883
48
49
  maidr/util/regression_line_utils.py,sha256=yFKr-H0whT_su2YVZwNksBLp5EC5s77sr6HUFgNcsyY,2329
49
50
  maidr/util/svg_utils.py,sha256=2gyzBtNKFHs0utrw1iOlxTmznzivOWQMV2aW8zu2c8E,1442
50
- maidr/util/mixin/__init__.py,sha256=aGJZNhtWh77yIVPc7ipIZm1OajigjMtCWYKPuDWTC-c,217
51
+ maidr/util/mixin/__init__.py,sha256=f_70pOT9tAUm8KXjoU2TJcTNXCTlDBsfPsDuxV101j4,492
51
52
  maidr/util/mixin/extractor_mixin.py,sha256=j2Rv2vh_gqqcxLV1ka3xsPaPAfWsX94CtKIW2FgPLnI,6937
53
+ maidr/util/mixin/format_extractor_mixin.py,sha256=jBmZ8JflDOC3Co-tKC2yka0viZawyI-zKNZijQPU8_Y,2726
52
54
  maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
53
55
  maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
56
  maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
55
- maidr-1.10.0.dist-info/METADATA,sha256=6llsnqQI7-gfp62yZJ1ucSXxf9xvrUVNtcItkoxnwnM,3155
56
- maidr-1.10.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
57
- maidr-1.10.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
- maidr-1.10.0.dist-info/RECORD,,
57
+ maidr-1.11.0.dist-info/METADATA,sha256=kriH4WLxxakBNSpLxHNk_nvPdqMXbw4JuK8aejvP9-4,3155
58
+ maidr-1.11.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
59
+ maidr-1.11.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
60
+ maidr-1.11.0.dist-info/RECORD,,
File without changes