maidr 1.6.1__py3-none-any.whl → 1.7.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.6.1"
1
+ __version__ = "1.7.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -44,6 +44,7 @@ class CandlestickPlot(MaidrPlot):
44
44
  self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
45
45
  self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
46
46
  self._maidr_original_data = kwargs.get("_maidr_original_data", None) # Store original data
47
+ self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
47
48
 
48
49
  # Store the GID for proper selector generation (legacy/shared)
49
50
  self._maidr_gid = None
@@ -91,63 +92,64 @@ class CandlestickPlot(MaidrPlot):
91
92
  # Use the original collections for highlighting
92
93
  self._elements = [body_collection, wick_collection]
93
94
 
94
- # Use the utility class to extract data
95
- data = MplfinanceDataExtractor.extract_candlestick_data(
96
- body_collection, wick_collection, self._maidr_date_nums, self._maidr_original_data
97
- )
98
- return data
99
-
100
- # Fallback to original detection method
101
- if not self.axes:
102
- return []
103
-
104
- ax_ohlc = self.axes[0]
105
-
106
- # Look for Rectangle patches (original_flavor candlestick)
107
- body_rectangles = []
108
- for patch in ax_ohlc.patches:
109
- if isinstance(patch, Rectangle):
110
- body_rectangles.append(patch)
111
-
112
- if body_rectangles:
113
- # Set elements for highlighting
114
- self._elements = body_rectangles
115
-
116
- # Generate a GID for highlighting if none exists
117
- if not self._maidr_gid:
118
- self._maidr_gid = f"maidr-{uuid.uuid4()}"
119
- # Set GID on all rectangles
120
- for rect in body_rectangles:
121
- rect.set_gid(self._maidr_gid)
122
- # Keep a dedicated body gid for legacy dict selectors
123
- self._maidr_body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
124
-
125
- # Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
126
- wick_lines = []
127
- for line in ax_ohlc.get_lines():
128
- try:
129
- xydata = line.get_xydata()
130
- if xydata is None:
95
+ # Use datetime converter for enhanced data extraction
96
+ if self._maidr_datetime_converter is not None:
97
+ data = self._maidr_datetime_converter.extract_candlestick_data(
98
+ self.axes[0], wick_collection, body_collection
99
+ )
100
+ return data
101
+
102
+ # Fallback to original detection method
103
+ if not self.axes:
104
+ return []
105
+
106
+ ax_ohlc = self.axes[0]
107
+
108
+ # Look for Rectangle patches (original_flavor candlestick)
109
+ body_rectangles = []
110
+ for patch in ax_ohlc.patches:
111
+ if isinstance(patch, Rectangle):
112
+ body_rectangles.append(patch)
113
+
114
+ if body_rectangles:
115
+ # Set elements for highlighting
116
+ self._elements = body_rectangles
117
+
118
+ # Generate a GID for highlighting if none exists
119
+ if not self._maidr_gid:
120
+ self._maidr_gid = f"maidr-{uuid.uuid4()}"
121
+ # Set GID on all rectangles
122
+ for rect in body_rectangles:
123
+ rect.set_gid(self._maidr_gid)
124
+ # Keep a dedicated body gid for legacy dict selectors
125
+ self._maidr_body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
126
+
127
+ # Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
128
+ wick_lines = []
129
+ for line in ax_ohlc.get_lines():
130
+ try:
131
+ xydata = line.get_xydata()
132
+ if xydata is None:
133
+ continue
134
+ xy_arr = np.asarray(xydata)
135
+ if xy_arr.ndim == 2 and xy_arr.shape[0] == 2 and xy_arr.shape[1] >= 2:
136
+ x0 = float(xy_arr[0, 0])
137
+ x1 = float(xy_arr[1, 0])
138
+ if abs(x0 - x1) < 1e-10:
139
+ wick_lines.append(line)
140
+ except Exception:
131
141
  continue
132
- xy_arr = np.asarray(xydata)
133
- if xy_arr.ndim == 2 and xy_arr.shape[0] == 2 and xy_arr.shape[1] >= 2:
134
- x0 = float(xy_arr[0, 0])
135
- x1 = float(xy_arr[1, 0])
136
- if abs(x0 - x1) < 1e-10:
137
- wick_lines.append(line)
138
- except Exception:
139
- continue
140
- if wick_lines:
141
- if not getattr(self, "_maidr_wick_gid", None):
142
- self._maidr_wick_gid = f"maidr-{uuid.uuid4()}"
143
- for line in wick_lines:
144
- line.set_gid(self._maidr_wick_gid)
145
-
146
- # Use the utility class to extract data
147
- data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
148
- body_rectangles, self._maidr_date_nums, self._maidr_original_data
149
- )
150
- return data
142
+ if wick_lines:
143
+ if not getattr(self, "_maidr_wick_gid", None):
144
+ self._maidr_wick_gid = f"maidr-{uuid.uuid4()}"
145
+ for line in wick_lines:
146
+ line.set_gid(self._maidr_wick_gid)
147
+
148
+ # Use the utility class to extract data
149
+ data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
150
+ body_rectangles, self._maidr_date_nums, self._maidr_original_data
151
+ )
152
+ return data
151
153
 
152
154
  return []
153
155
 
@@ -31,6 +31,10 @@ class MplfinanceBarPlot(
31
31
  self._custom_patches = kwargs.get("_maidr_patches", None)
32
32
  # Store date numbers for volume bars (from mplfinance)
33
33
  self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
34
+
35
+ # Store datetime converter if available
36
+ self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
37
+
34
38
  # Store custom title
35
39
 
36
40
  def set_title(self, title: str) -> None:
@@ -48,6 +52,13 @@ class MplfinanceBarPlot(
48
52
  )
49
53
  data = self._extract_bar_container_data(plot)
50
54
  levels = self.extract_level(self.ax)
55
+
56
+ # Ensure we have valid data and levels before processing
57
+ if data is None or levels is None:
58
+ if data is None:
59
+ raise ExtractionError(self.type, plot)
60
+ return []
61
+
51
62
  formatted_data = []
52
63
  combined_data = list(
53
64
  zip(levels, data)
@@ -78,7 +89,13 @@ class MplfinanceBarPlot(
78
89
  # Set elements for highlighting (use the patches directly)
79
90
  self._elements = sorted_patches
80
91
 
81
- # Use the utility class to extract data
92
+ # Use datetime converter for enhanced data extraction
93
+ if self._maidr_datetime_converter is not None:
94
+ data = self._maidr_datetime_converter.extract_volume_data(self.ax)
95
+ if data: # Only use if successful
96
+ return [{"x": item[0], "y": item[1]} for item in data]
97
+
98
+ # Fallback to existing logic
82
99
  return MplfinanceDataExtractor.extract_volume_data(
83
100
  sorted_patches, self._maidr_date_nums
84
101
  )
@@ -23,6 +23,9 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
23
23
  def __init__(self, ax: Axes, **kwargs):
24
24
  super().__init__(ax, PlotType.LINE)
25
25
 
26
+ # Store datetime converter if available
27
+ self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
28
+
26
29
  def _get_selector(self) -> Union[str, List[str]]:
27
30
  """Return selectors for all lines that have data."""
28
31
  all_lines = self.ax.get_lines()
@@ -101,10 +104,15 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
101
104
  if np.isnan(x) or np.isnan(y) or np.isinf(x) or np.isinf(y):
102
105
  continue
103
106
 
104
- # Handle x-value conversion - could be string (date) or numeric
105
- if isinstance(x, str):
106
- x_value = x # Keep string as-is (for dates)
107
+ # Use datetime converter for enhanced data extraction
108
+ datetime_converter = getattr(line, "_maidr_datetime_converter", None) or self._maidr_datetime_converter
109
+ if datetime_converter is not None:
110
+ # Convert x-coordinate (matplotlib index) to formatted datetime
111
+ x_value = datetime_converter.get_formatted_datetime(int(round(x)))
112
+ if x_value is None:
113
+ x_value = float(x) # Fallback to numeric
107
114
  else:
115
+ # Fallback to existing logic
108
116
  # Check if we have date numbers from mplfinance
109
117
  if date_nums is not None and i < len(date_nums):
110
118
  # Use the date number to convert to date string
maidr/patch/mplfinance.py CHANGED
@@ -8,6 +8,7 @@ from matplotlib.lines import Line2D
8
8
  from maidr.core.enum import PlotType
9
9
  from maidr.patch.common import common
10
10
  from maidr.core.context_manager import ContextManager
11
+ from maidr.util.datetime_conversion import create_datetime_converter
11
12
 
12
13
 
13
14
  def mplfinance_plot_patch(wrapped, instance, args, kwargs):
@@ -53,26 +54,37 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
53
54
  elif volume_ax is None and "volume" in ax.get_ylabel().lower():
54
55
  volume_ax = ax
55
56
 
56
- # Try to extract date numbers from the data
57
+ # Try to extract date numbers from the data (existing logic preserved)
57
58
  date_nums = None
58
59
  data = None
60
+ datetime_converter = None
61
+
59
62
  if len(args) > 0:
60
63
  data = args[0]
61
64
  elif "data" in kwargs:
62
65
  data = kwargs["data"]
63
66
 
64
67
  if data is not None:
68
+ # Existing date_nums logic (preserved)
65
69
  if hasattr(data, "Date_num"):
66
70
  date_nums = list(data["Date_num"])
67
71
  elif hasattr(data, "index"):
68
72
  # fallback: use index if it's a DatetimeIndex
69
73
  try:
70
74
  import matplotlib.dates as mdates
71
-
72
75
  date_nums = [mdates.date2num(d) for d in data.index]
73
76
  except Exception:
74
77
  pass
75
78
 
79
+ # Create datetime converter for DatetimeIndex data
80
+ if hasattr(data, "index") and hasattr(data.index, "dtype"):
81
+ if "datetime" in str(data.index.dtype).lower():
82
+ datetime_converter = create_datetime_converter(data)
83
+
84
+ # Use enhanced converter's date_nums for mplfinance compatibility
85
+ if date_nums is None and hasattr(datetime_converter, 'date_nums'):
86
+ date_nums = datetime_converter.date_nums
87
+
76
88
  # Process and register the Candlestick plot
77
89
  if price_ax:
78
90
  wick_collection = next(
@@ -97,6 +109,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
97
109
  _maidr_wick_gid=wick_gid,
98
110
  _maidr_body_gid=body_gid,
99
111
  )
112
+
113
+ # Add datetime converter
114
+ if datetime_converter is not None:
115
+ candlestick_kwargs["_maidr_datetime_converter"] = datetime_converter
100
116
  common(
101
117
  PlotType.CANDLESTICK,
102
118
  lambda *a, **k: price_ax,
@@ -129,6 +145,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
129
145
  _maidr_patches=volume_patches,
130
146
  _maidr_date_nums=date_nums,
131
147
  )
148
+
149
+ # Add datetime converter
150
+ if datetime_converter is not None:
151
+ bar_kwargs["_maidr_datetime_converter"] = datetime_converter # type: ignore
152
+
132
153
  common(PlotType.BAR, lambda *a, **k: volume_ax, instance, args, bar_kwargs)
133
154
 
134
155
  # Process and register Moving Averages as LINE plots
@@ -196,6 +217,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
196
217
  if date_nums is not None:
197
218
  setattr(line, "_maidr_date_nums", date_nums)
198
219
 
220
+ # Store datetime converter
221
+ if datetime_converter is not None:
222
+ setattr(line, "_maidr_datetime_converter", datetime_converter)
223
+
199
224
  # Ensure GID is set for highlighting
200
225
  if line.get_gid() is None:
201
226
  gid = f"maidr-{uuid.uuid4()}"
@@ -207,6 +232,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
207
232
  # Register all valid lines as a single LINE plot
208
233
  if valid_lines:
209
234
  line_kwargs = dict(kwargs)
235
+
236
+ # Add datetime converter
237
+ if datetime_converter is not None:
238
+ line_kwargs["_maidr_datetime_converter"] = datetime_converter
239
+
210
240
  common(PlotType.LINE, lambda *a, **k: price_ax, instance, args, line_kwargs)
211
241
 
212
242
  if not original_returnfig:
@@ -0,0 +1,406 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from typing import Optional, Dict, Any, List, Tuple
4
+ from datetime import datetime
5
+
6
+ class DatetimeConverter:
7
+ """
8
+ Enhanced datetime converter that automatically detects time periods
9
+ and provides intelligent date/time formatting for mplfinance plots.
10
+
11
+ This utility automatically detects the time period of financial data and formats
12
+ datetime values consistently for screen reader accessibility and visual clarity.
13
+
14
+ Parameters
15
+ ----------
16
+ data : pd.DataFrame
17
+ DataFrame with DatetimeIndex containing financial data.
18
+ datetime_format : str, optional
19
+ Custom datetime format string. If None, automatic format detection is used.
20
+
21
+ Attributes
22
+ ----------
23
+ data : pd.DataFrame
24
+ The input DataFrame with DatetimeIndex.
25
+ datetime_format : str or None
26
+ Custom datetime format string if provided.
27
+ date_mapping : Dict[int, datetime]
28
+ Mapping from integer index to datetime objects.
29
+ time_period : str
30
+ Detected time period ('minute', 'intraday', 'hour', 'day', 'week', 'month').
31
+
32
+ Raises
33
+ ------
34
+ ValueError
35
+ If the input data does not have a DatetimeIndex.
36
+
37
+ Examples
38
+ --------
39
+ >>> import pandas as pd
40
+ >>> from maidr.util.datetime_conversion import create_datetime_converter
41
+ >>>
42
+ >>> # Create sample data with DatetimeIndex
43
+ >>> dates = pd.date_range('2024-01-15', periods=5, freq='D')
44
+ >>> df = pd.DataFrame({'Open': [3050, 3078, 3080, 3075, 3087]}, index=dates)
45
+ >>>
46
+ >>> # Create converter
47
+ >>> converter = create_datetime_converter(df)
48
+ >>>
49
+ >>> # Get formatted datetime
50
+ >>> formatted = converter.get_formatted_datetime(0)
51
+ >>> print(formatted) # Output: "Jan 15 2024"
52
+ >>>
53
+ >>> # For time-based data
54
+ >>> hourly_dates = pd.date_range('2024-01-15 09:00:00', periods=3, freq='H')
55
+ >>> df_hourly = pd.DataFrame({'Open': [3050, 3078, 3080]}, index=hourly_dates)
56
+ >>> converter_hourly = create_datetime_converter(df_hourly)
57
+ >>> formatted_hourly = converter_hourly.get_formatted_datetime(0)
58
+ >>> print(formatted_hourly) # Output: "Jan 15 2024 09:00"
59
+ """
60
+
61
+ def __init__(self, data: pd.DataFrame, datetime_format: Optional[str] = None) -> None:
62
+ """
63
+ Initialize the DatetimeConverter.
64
+
65
+ Parameters
66
+ ----------
67
+ data : pd.DataFrame
68
+ DataFrame with DatetimeIndex containing financial data.
69
+ datetime_format : str, optional
70
+ Custom datetime format string. If None, automatic format detection is used.
71
+
72
+ Raises
73
+ ------
74
+ ValueError
75
+ If the input data does not have a DatetimeIndex.
76
+
77
+ Notes
78
+ -----
79
+ The converter automatically detects the time period of the data based on
80
+ average time differences between consecutive data points.
81
+ """
82
+ self.data = data
83
+ self.datetime_format = datetime_format
84
+
85
+ if not isinstance(data.index, pd.DatetimeIndex):
86
+ raise ValueError("Data must have a DatetimeIndex")
87
+
88
+ self.date_mapping = self._create_date_mapping()
89
+ self.time_period = self._detect_time_period()
90
+
91
+ def _create_date_mapping(self) -> Dict[int, datetime]:
92
+ """
93
+ Create mapping from integer index to datetime objects.
94
+
95
+ Returns
96
+ -------
97
+ Dict[int, datetime]
98
+ Dictionary mapping integer indices to corresponding datetime objects
99
+ from the DataFrame index.
100
+ """
101
+ return {i: date for i, date in enumerate(self.data.index)}
102
+
103
+ def _detect_time_period(self) -> str:
104
+ """
105
+ Detect the time period of the data based on average time differences.
106
+
107
+ Returns
108
+ -------
109
+ str
110
+ Detected time period: 'minute', 'intraday', 'hour', 'day', 'week', 'month', or 'unknown'.
111
+
112
+ Notes
113
+ -----
114
+ Time period detection is based on average time differences between consecutive
115
+ data points in the DatetimeIndex.
116
+ """
117
+ if len(self.data) < 2:
118
+ return "unknown"
119
+
120
+ # Calculate average time difference between consecutive data points
121
+ time_diffs = []
122
+ for i in range(1, len(self.data)):
123
+ diff = self.data.index[i] - self.data.index[i-1]
124
+ time_diffs.append(diff.total_seconds())
125
+
126
+ avg_diff_seconds = np.mean(time_diffs)
127
+
128
+ # Determine time period based on average difference
129
+ if avg_diff_seconds < 60: # Less than 1 minute
130
+ return "minute"
131
+ elif avg_diff_seconds < 3600: # Less than 1 hour
132
+ return "intraday"
133
+ elif avg_diff_seconds < 86400: # Less than 1 day
134
+ return "hour"
135
+ elif avg_diff_seconds < 604800: # Less than 1 week
136
+ return "day"
137
+ elif avg_diff_seconds < 2592000: # Less than 1 month
138
+ return "week"
139
+ else:
140
+ return "month"
141
+
142
+ def get_time_period_description(self) -> str:
143
+ """
144
+ Get human-readable description of detected time period.
145
+
146
+ Returns
147
+ -------
148
+ str
149
+ Human-readable description of the detected time period.
150
+ """
151
+ period_descriptions = {
152
+ "minute": "Sub-minute data",
153
+ "intraday": "Intraday (minute-level) data",
154
+ "hour": "Hourly data",
155
+ "day": "Daily data",
156
+ "week": "Weekly data",
157
+ "month": "Monthly data",
158
+ "unknown": "Unknown time period"
159
+ }
160
+ return period_descriptions.get(self.time_period, "Unknown time period")
161
+
162
+ def get_formatted_datetime(self, index: int) -> Optional[str]:
163
+ """
164
+ Get formatted datetime string for given index using consistent formatting.
165
+
166
+ Always includes year for screen reader accessibility.
167
+
168
+ Parameters
169
+ ----------
170
+ index : int
171
+ Integer index into the DataFrame.
172
+
173
+ Returns
174
+ -------
175
+ str or None
176
+ Formatted datetime string or None if index is invalid.
177
+
178
+ Examples
179
+ --------
180
+ >>> converter = create_datetime_converter(df)
181
+ >>> formatted = converter.get_formatted_datetime(0)
182
+ >>> print(formatted) # "Jan 15 2024" for daily data
183
+ """
184
+ if index not in self.date_mapping:
185
+ return None
186
+
187
+ dt = self.date_mapping[index]
188
+ return self._format_datetime_custom(dt)
189
+
190
+ def _format_datetime_custom(self, dt: datetime) -> str:
191
+ """
192
+ Consistent datetime formatting with year always included.
193
+
194
+ Parameters
195
+ ----------
196
+ dt : datetime
197
+ Datetime object to format.
198
+
199
+ Returns
200
+ -------
201
+ str
202
+ Formatted datetime string with consistent pattern.
203
+
204
+ Notes
205
+ -----
206
+ Formatting rules:
207
+ - Daily data: "Jan 15 2024"
208
+ - Time-based data: "Jan 15 2024 09:00" or "Jan 15 2024 09:00:30"
209
+ - Seconds are only shown when they are non-zero for cleaner display.
210
+ """
211
+ if self.time_period in ["minute", "intraday", "hour"]:
212
+ # Time-based data: include time with optional seconds
213
+ if dt.second == 0:
214
+ return dt.strftime("%b %d %Y %H:%M")
215
+ else:
216
+ return dt.strftime("%b %d %Y %H:%M:%S")
217
+ else:
218
+ # Daily/weekly/monthly data: just date
219
+ return dt.strftime("%b %d %Y")
220
+
221
+ @property
222
+ def date_nums(self) -> List[float]:
223
+ """
224
+ Convert DatetimeIndex to matplotlib date numbers for backward compatibility.
225
+
226
+ Returns
227
+ -------
228
+ List[float]
229
+ List of matplotlib date numbers converted from the DatetimeIndex.
230
+ Empty list if conversion fails.
231
+
232
+ Notes
233
+ -----
234
+ This property provides backward compatibility with existing matplotlib
235
+ plotting code that expects date numbers instead of datetime objects.
236
+ """
237
+ try:
238
+ import matplotlib.dates as mdates
239
+ date_nums = []
240
+ for d in self.data.index:
241
+ try:
242
+ date_num = mdates.date2num(d)
243
+ date_nums.append(float(date_num))
244
+ except (ValueError, TypeError):
245
+ continue
246
+ return date_nums
247
+ except Exception:
248
+ return []
249
+
250
+ def extract_candlestick_data(self, ax, wick_collection=None, body_collection=None) -> List[Dict[str, Any]]:
251
+ """
252
+ Extract candlestick data with proper datetime formatting using original DataFrame.
253
+
254
+ Parameters
255
+ ----------
256
+ ax : matplotlib.axes.Axes
257
+ Matplotlib axes object (not used in current implementation).
258
+ wick_collection : matplotlib.collections.LineCollection, optional
259
+ Collection containing wick lines for candlestick plots.
260
+ body_collection : matplotlib.collections.PolyCollection, optional
261
+ Collection containing body rectangles for candlestick plots.
262
+
263
+ Returns
264
+ -------
265
+ List[Dict[str, Any]]
266
+ List of dictionaries containing candlestick data with keys:
267
+ 'value', 'open', 'high', 'low', 'close', 'volume'.
268
+ Each 'value' contains formatted datetime string.
269
+
270
+ Notes
271
+ -----
272
+ This method extracts OHLC data from the original DataFrame and formats
273
+ datetime values using the enhanced datetime conversion logic.
274
+ """
275
+ candles = []
276
+ if not hasattr(self.data, 'Open') or not hasattr(self.data, 'High') or not hasattr(self.data, 'Low') or not hasattr(self.data, 'Close'):
277
+ return candles
278
+
279
+ for i in range(len(self.data)):
280
+ try:
281
+ open_price = self.data.iloc[i]['Open']
282
+ high_price = self.data.iloc[i]['High']
283
+ low_price = self.data.iloc[i]['Low']
284
+ close_price = self.data.iloc[i]['Close']
285
+ volume = self.data.iloc[i].get('Volume', 0.0)
286
+
287
+ formatted_datetime = self.get_formatted_datetime(i)
288
+
289
+ candle_data = {
290
+ 'value': formatted_datetime or f"datetime_{i:03d}",
291
+ 'open': float(open_price),
292
+ 'high': float(high_price),
293
+ 'low': float(low_price),
294
+ 'close': float(close_price),
295
+ 'volume': float(volume)
296
+ }
297
+ candles.append(candle_data)
298
+ except (KeyError, IndexError, ValueError):
299
+ continue
300
+ return candles
301
+
302
+ def extract_moving_average_data(self, ax, line_index: int = 0) -> List[Tuple[str, float]]:
303
+ """
304
+ Extract moving average data with proper datetime formatting and NaN filtering.
305
+
306
+ Parameters
307
+ ----------
308
+ ax : matplotlib.axes.Axes
309
+ Matplotlib axes object containing the moving average lines.
310
+ line_index : int, default=0
311
+ Index of the line to extract data from.
312
+
313
+ Returns
314
+ -------
315
+ List[Tuple[str, float]]
316
+ List of tuples containing (formatted_datetime, y_value) pairs.
317
+ NaN and infinite values are filtered out.
318
+
319
+ Notes
320
+ -----
321
+ This method filters out invalid data points and formats datetime values
322
+ using the enhanced datetime conversion logic.
323
+ """
324
+ ma_data = []
325
+ lines = ax.get_lines() if ax else []
326
+ if line_index >= len(lines):
327
+ return ma_data
328
+ line = lines[line_index]
329
+ xydata = line.get_xydata()
330
+ if xydata is None or len(xydata) == 0:
331
+ return ma_data
332
+
333
+ for i, (x, y) in enumerate(xydata):
334
+ if np.isnan(y) or np.isinf(y):
335
+ continue
336
+ try:
337
+ df_index = int(round(x))
338
+ if 0 <= df_index < len(self.data):
339
+ formatted_datetime = self.get_formatted_datetime(df_index)
340
+ if formatted_datetime:
341
+ ma_data.append((formatted_datetime, float(y)))
342
+ except (ValueError, TypeError):
343
+ continue
344
+ return ma_data
345
+
346
+ def extract_volume_data(self, ax) -> List[Tuple[str, float]]:
347
+ """
348
+ Extract volume data with proper datetime formatting using original DataFrame.
349
+
350
+ Parameters
351
+ ----------
352
+ ax : matplotlib.axes.Axes
353
+ Matplotlib axes object (not used in current implementation).
354
+
355
+ Returns
356
+ -------
357
+ List[Tuple[str, float]]
358
+ List of tuples containing (formatted_datetime, volume) pairs.
359
+ Zero and NaN volume values are filtered out.
360
+
361
+ Notes
362
+ -----
363
+ This method extracts volume data from the original DataFrame and formats
364
+ datetime values using the enhanced datetime conversion logic.
365
+ """
366
+ volume_data = []
367
+ if hasattr(self.data, 'Volume'):
368
+ for i in range(len(self.data)):
369
+ try:
370
+ volume = self.data.iloc[i]['Volume']
371
+ if pd.isna(volume) or volume <= 0:
372
+ continue
373
+ formatted_datetime = self.get_formatted_datetime(i)
374
+ volume_data.append((formatted_datetime, float(volume)))
375
+ except (KeyError, IndexError, ValueError):
376
+ continue
377
+ return volume_data
378
+
379
+
380
+ def create_datetime_converter(data: pd.DataFrame, datetime_format: Optional[str] = None) -> DatetimeConverter:
381
+ """
382
+ Factory function to create a DatetimeConverter instance.
383
+
384
+ Parameters
385
+ ----------
386
+ data : pd.DataFrame
387
+ DataFrame with DatetimeIndex containing financial data.
388
+ datetime_format : str, optional
389
+ Custom datetime format string. If None, automatic format detection is used.
390
+
391
+ Returns
392
+ -------
393
+ DatetimeConverter
394
+ Configured DatetimeConverter instance for the given data.
395
+
396
+ Examples
397
+ --------
398
+ >>> import pandas as pd
399
+ >>> from maidr.util.datetime_conversion import create_datetime_converter
400
+ >>>
401
+ >>> dates = pd.date_range('2024-01-15', periods=5, freq='D')
402
+ >>> df = pd.DataFrame({'Open': [3050, 3078, 3080, 3075, 3087]}, index=dates)
403
+ >>> converter = create_datetime_converter(df)
404
+ >>> print(type(converter)) # <class 'maidr.util.datetime_conversion.DatetimeConverter'>
405
+ """
406
+ return DatetimeConverter(data, datetime_format)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maidr
3
- Version: 1.6.1
3
+ Version: 1.7.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=n8luuuD48JSxB6D5jXxE2QsGGoeQI-3S9lM4qhb4RPI,415
1
+ maidr/__init__.py,sha256=N6tFj8F07eJxaRvcZZNW0wNWTR2I2JcTgM9PTSKWGtM,415
2
2
  maidr/api.py,sha256=gRNLXqUWpFGdD-I7Nu6J0_LeEni9KRAr0TBHwHaDAsc,1928
3
3
  maidr/core/__init__.py,sha256=WgxLpSEYMc4k3OyEOf1shOxfEq0ASzppEIZYmE91ThQ,25
4
4
  maidr/core/context_manager.py,sha256=6cT7ZGOApSpC-SLD2XZWWU_H08i-nfv-JUlzXOtvWYw,3374
@@ -12,15 +12,15 @@ maidr/core/enum/smooth_keywords.py,sha256=z2kVZZ-mETWWh5reWu_hj9WkJD6WFj7_2-6s1e
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=pDuO7ljGiwpqLUWpMgPYARTZGfLHdEoPVbAoNkv3lV0,9559
15
+ maidr/core/plot/candlestick.py,sha256=9R1ddSnUkGEE9WJgUPeuZhRtxXNBnOM9rbJdfJcY8bo,9886
16
16
  maidr/core/plot/grouped_barplot.py,sha256=_zn4XMeEnSiDHtf6t4-z9ErBqg_CijhAS2CCtlHgYIQ,2077
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=48aEkx8rjZ9lYEZSHWCJcSzdVblKwcdJZDZK0vnxmPo,4291
20
20
  maidr/core/plot/maidr_plot.py,sha256=9T0boWaonM99jggEed97-rCy_cufRMWXrXo-CbmPudE,4230
21
21
  maidr/core/plot/maidr_plot_factory.py,sha256=NW2iFScswgXbAC9rAOo4iMkAFsjY43DAvFioGr0yzx4,2732
22
- maidr/core/plot/mplfinance_barplot.py,sha256=8H9fKxkLIRiTDMoSjtf87hjFPuP1WCo_mdsyEWBsLjQ,5016
23
- maidr/core/plot/mplfinance_lineplot.py,sha256=3UP701S8Rz8vjuC5WIEVHnsZMJCo3AoSUrbQG3CBqkM,6750
22
+ maidr/core/plot/mplfinance_barplot.py,sha256=zhTp2i6BH0xn7vQvGTotKgu2HbzlKT4p6zA5CVUUHHc,5673
23
+ maidr/core/plot/mplfinance_lineplot.py,sha256=oRXFAa13wsz5i9tDN267ZSqd1-gGpOEzntmFTAz8o1w,7269
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
@@ -36,10 +36,11 @@ maidr/patch/highlight.py,sha256=I1dGFHJAnVd0AHVnMJzk_TE8BC8Uv-I6fTzSrJLU5QM,1155
36
36
  maidr/patch/histogram.py,sha256=k3N0RUf1SQ2402pwbaY5QyS98KnLWvr9glCHQw9NTko,2378
37
37
  maidr/patch/kdeplot.py,sha256=qv-OKzuop2aTrkZgUe2OnLxvV-KMyeXt1Td0_uZeHzE,2338
38
38
  maidr/patch/lineplot.py,sha256=kvTAiOddy1g5GMP456Awk21NUEZfn-vWaYXy5GTVtuA,1841
39
- maidr/patch/mplfinance.py,sha256=FJYM5xRDMpTwaKZUzT6IjpgsR1UiVnvATZsDTyDxqmY,8154
39
+ maidr/patch/mplfinance.py,sha256=L_C_EsIghUVVdtOxooa1a9Lxe-UcUpKuYV2Tw_bMDBw,9490
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=fl2MnSCmz8IqYIHsiQPWKkNz3lDrFyWoStkQPIDReCA,14364
43
44
  maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
44
45
  maidr/util/environment.py,sha256=SSfxzjmY2ZVA98B-IVql8n9AX3d6h2f7_R4t3_53dYI,9452
45
46
  maidr/util/mplfinance_utils.py,sha256=aWNRkNS2IAF4YYEF9w7CcmcKWMGg3KkYJAv0xL8KcyY,14417
@@ -51,7 +52,7 @@ maidr/util/mixin/extractor_mixin.py,sha256=oHtwpmS5kARvaLrSO3DKTPQxyFUw9nOcKN7rz
51
52
  maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
52
53
  maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
54
  maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
54
- maidr-1.6.1.dist-info/METADATA,sha256=WygnnC777b2MAHatLuMFLRjtoRZIW9gc5Ft1RKHE06I,3154
55
- maidr-1.6.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- maidr-1.6.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
57
- maidr-1.6.1.dist-info/RECORD,,
55
+ maidr-1.7.0.dist-info/METADATA,sha256=rjXKEyignRmgCOB6C6E_FhvqEkPvjGHIMuxFkZAHpos,3154
56
+ maidr-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ maidr-1.7.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
+ maidr-1.7.0.dist-info/RECORD,,
File without changes