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 +1 -1
- maidr/core/enum/maidr_key.py +1 -0
- maidr/core/plot/candlestick.py +14 -14
- maidr/core/plot/maidr_plot.py +11 -2
- maidr/core/plot/mplfinance_barplot.py +5 -1
- maidr/core/plot/mplfinance_lineplot.py +5 -1
- maidr/util/datetime_conversion.py +14 -11
- maidr/util/format_config.py +679 -0
- maidr/util/mixin/__init__.py +11 -0
- maidr/util/mixin/format_extractor_mixin.py +83 -0
- {maidr-1.10.0.dist-info → maidr-1.11.0.dist-info}/METADATA +1 -1
- {maidr-1.10.0.dist-info → maidr-1.11.0.dist-info}/RECORD +14 -12
- {maidr-1.10.0.dist-info → maidr-1.11.0.dist-info}/WHEEL +0 -0
- {maidr-1.10.0.dist-info → maidr-1.11.0.dist-info}/licenses/LICENSE +0 -0
maidr/__init__.py
CHANGED
maidr/core/enum/maidr_key.py
CHANGED
maidr/core/plot/candlestick.py
CHANGED
|
@@ -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
|
-
|
|
51
|
-
self.
|
|
52
|
-
|
|
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
|
|
190
|
+
# Legacy path
|
|
194
191
|
legacy_selectors = {}
|
|
195
|
-
if
|
|
196
|
-
body_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
|
|
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
|
-
|
|
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
|
maidr/core/plot/maidr_plot.py
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
Enhanced datetime converter that automatically detects time periods
|
|
10
|
+
and provides intelligent date/time formatting for mplfinance plots.
|
|
10
11
|
|
|
11
|
-
This utility
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
Plain string representation of datetime (ISO format).
|
|
202
206
|
|
|
203
207
|
Notes
|
|
204
208
|
-----
|
|
205
|
-
Returns the
|
|
206
|
-
|
|
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
|
maidr/util/mixin/__init__.py
CHANGED
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
maidr/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
23
|
-
maidr/core/plot/mplfinance_lineplot.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
56
|
-
maidr-1.
|
|
57
|
-
maidr-1.
|
|
58
|
-
maidr-1.
|
|
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
|
|
File without changes
|