maidr 1.6.0__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.0"
1
+ __version__ = "1.7.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -1,11 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import uuid
4
+ from typing import Union, Dict
3
5
  from matplotlib.axes import Axes
4
6
  from matplotlib.patches import Rectangle
7
+ import numpy as np
5
8
 
6
9
  from maidr.core.enum import PlotType
7
10
  from maidr.core.plot import MaidrPlot
8
11
  from maidr.core.enum.maidr_key import MaidrKey
12
+ from maidr.exception import ExtractionError
9
13
  from maidr.util.mplfinance_utils import MplfinanceDataExtractor
10
14
 
11
15
 
@@ -40,64 +44,112 @@ class CandlestickPlot(MaidrPlot):
40
44
  self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
41
45
  self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
42
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)
43
48
 
44
- # Store the GID for proper selector generation
49
+ # Store the GID for proper selector generation (legacy/shared)
45
50
  self._maidr_gid = None
51
+ # Modern-path separate gids for body and wick
52
+ self._maidr_body_gid = None
53
+ self._maidr_wick_gid = None
46
54
  if self._maidr_body_collection:
47
55
  self._maidr_gid = self._maidr_body_collection.get_gid()
56
+ self._maidr_body_gid = self._maidr_gid
48
57
  elif self._maidr_wick_collection:
49
58
  self._maidr_gid = self._maidr_wick_collection.get_gid()
59
+ self._maidr_wick_gid = self._maidr_gid
50
60
 
51
61
  def _extract_plot_data(self) -> list[dict]:
52
- """Extract candlestick data from the plot."""
62
+ """
63
+ Extract candlestick data from the plot.
64
+
65
+ This method processes candlestick plots from both modern (mplfinance.plot) and
66
+ legacy (original_flavor) pipelines, extracting OHLC data and setting up
67
+ highlighting elements and GIDs.
68
+
69
+ Returns
70
+ -------
71
+ list[dict]
72
+ List of dictionaries containing candlestick data with keys:
73
+ - 'value': Date string
74
+ - 'open': Opening price (float)
75
+ - 'high': High price (float)
76
+ - 'low': Low price (float)
77
+ - 'close': Closing price (float)
78
+ - 'volume': Volume (float, typically 0 for candlestick-only plots)
79
+ """
53
80
 
54
81
  # Get the custom collections from kwargs
55
82
  body_collection = self._maidr_body_collection
56
83
  wick_collection = self._maidr_wick_collection
57
84
 
58
85
  if body_collection and wick_collection:
59
- # Store the GID from the body collection for highlighting
60
- self._maidr_gid = body_collection.get_gid()
86
+ # Store the GIDs from the collections (modern path)
87
+ self._maidr_body_gid = body_collection.get_gid()
88
+ self._maidr_wick_gid = wick_collection.get_gid()
89
+ # Keep legacy gid filled for backward compatibility
90
+ self._maidr_gid = self._maidr_body_gid or self._maidr_wick_gid
61
91
 
62
92
  # Use the original collections for highlighting
63
93
  self._elements = [body_collection, wick_collection]
64
94
 
65
- # Use the utility class to extract data
66
- data = MplfinanceDataExtractor.extract_candlestick_data(
67
- body_collection, wick_collection, self._maidr_date_nums, self._maidr_original_data
68
- )
69
- return data
70
-
71
- # Fallback to original detection method
72
- if not self.axes:
73
- return []
74
-
75
- ax_ohlc = self.axes[0]
76
-
77
- # Look for Rectangle patches (original_flavor candlestick)
78
- body_rectangles = []
79
- for patch in ax_ohlc.patches:
80
- if isinstance(patch, Rectangle):
81
- body_rectangles.append(patch)
82
-
83
- if body_rectangles:
84
- # Set elements for highlighting
85
- self._elements = body_rectangles
86
-
87
- # Generate a GID for highlighting if none exists
88
- if not self._maidr_gid:
89
- import uuid
90
-
91
- self._maidr_gid = f"maidr-{uuid.uuid4()}"
92
- # Set GID on all rectangles
93
- for rect in body_rectangles:
94
- rect.set_gid(self._maidr_gid)
95
-
96
- # Use the utility class to extract data
97
- data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
98
- body_rectangles, self._maidr_date_nums, self._maidr_original_data
99
- )
100
- return data
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:
141
+ continue
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
101
153
 
102
154
  return []
103
155
 
@@ -117,17 +169,57 @@ class CandlestickPlot(MaidrPlot):
117
169
  x_labels = "X"
118
170
  return {MaidrKey.X: x_labels, MaidrKey.Y: self.ax.get_ylabel()}
119
171
 
120
- def _get_selector(self) -> str:
121
- """Return the CSS selector for highlighting candlestick elements in the SVG output."""
122
- # Use the stored GID if available, otherwise fall back to generic selector
123
- if self._maidr_gid:
124
- # Use the full GID as the id attribute (since that's what's in the SVG)
125
- selector = (
126
- f"g[id='{self._maidr_gid}'] > path, g[id='{self._maidr_gid}'] > rect"
127
- )
128
- else:
129
- selector = "g[maidr='true'] > path, g[maidr='true'] > rect"
130
- return selector
172
+ def _get_selector(self) -> Union[str, Dict[str, str]]:
173
+ """Return selectors for highlighting candlestick elements.
174
+
175
+ - Modern path (collections present): return a dict with separate selectors for body, wickLow, wickHigh
176
+ - Legacy path: return a dict with body and shared wick selectors (no open/close keys)
177
+ """
178
+ # Modern path: build structured selectors using separate gids
179
+ if self._maidr_body_collection and self._maidr_wick_collection and self._maidr_body_gid and self._maidr_wick_gid:
180
+ # Determine candle count N
181
+ N = None
182
+ if self._maidr_original_data is not None:
183
+ try:
184
+ N = len(self._maidr_original_data)
185
+ except Exception:
186
+ N = None
187
+ if N is None and hasattr(self._maidr_wick_collection, "get_paths"):
188
+ try:
189
+ wick_paths = len(list(self._maidr_wick_collection.get_paths()))
190
+ if wick_paths % 2 == 0 and wick_paths > 0:
191
+ N = wick_paths // 2
192
+ except Exception:
193
+ pass
194
+ if N is None and hasattr(self._maidr_body_collection, "get_paths"):
195
+ try:
196
+ body_paths = len(list(self._maidr_body_collection.get_paths()))
197
+ if body_paths > 0:
198
+ N = body_paths
199
+ except Exception:
200
+ pass
201
+ if N is None:
202
+ raise ExtractionError(PlotType.CANDLESTICK, self._maidr_wick_collection)
203
+
204
+ selectors = {
205
+ "body": f"g[id='{self._maidr_body_gid}'] > path",
206
+ "wickLow": f"g[id='{self._maidr_wick_gid}'] > path:nth-child(-n+{N})",
207
+ "wickHigh": f"g[id='{self._maidr_wick_gid}'] > path:nth-child(n+{N + 1})",
208
+ }
209
+ return selectors
210
+
211
+ # Legacy path: build shared-id selectors; omit open/close
212
+ legacy_selectors = {}
213
+ if getattr(self, "_maidr_body_gid", None) or self._maidr_gid:
214
+ body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
215
+ legacy_selectors["body"] = f"g[id='{body_gid}'] > path"
216
+ if getattr(self, "_maidr_wick_gid", None):
217
+ legacy_selectors["wick"] = f"g[id='{self._maidr_wick_gid}'] > path"
218
+ if legacy_selectors:
219
+ return legacy_selectors
220
+
221
+ # Fallback
222
+ return "g[maidr='true'] > path, g[maidr='true'] > rect"
131
223
 
132
224
  def render(self) -> dict:
133
225
  """Initialize the MAIDR schema dictionary with basic plot information."""
@@ -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
@@ -2,6 +2,7 @@ import wrapt
2
2
  from typing import Any, Callable, Dict, Tuple
3
3
  from matplotlib.patches import Rectangle
4
4
  from mplfinance import original_flavor
5
+ import numpy as np
5
6
 
6
7
  from maidr.core.context_manager import ContextManager
7
8
  from maidr.core.enum.plot_type import PlotType
@@ -46,10 +47,35 @@ def candlestick(
46
47
  # Patch the plotting function.
47
48
  plot = wrapped(*args, **kwargs)
48
49
 
50
+ original_data = None
51
+ date_nums = None
52
+ if len(args) >= 2:
53
+ try:
54
+ quotes = args[1]
55
+ if quotes is not None:
56
+ arr = np.asarray(quotes)
57
+ if arr.ndim == 2 and arr.shape[1] >= 5 and arr.size > 0:
58
+ date_nums = arr[:, 0].tolist()
59
+ original_data = {
60
+ "Open": arr[:, 1].tolist(),
61
+ "High": arr[:, 2].tolist(),
62
+ "Low": arr[:, 3].tolist(),
63
+ "Close": arr[:, 4].tolist(),
64
+ }
65
+ except Exception:
66
+ pass
67
+
49
68
  axes = []
50
69
  for ax in plot:
51
70
  axes.append(FigureManager.get_axes(ax))
52
- FigureManager.create_maidr(axes, PlotType.CANDLESTICK)
71
+
72
+ extra_kwargs: Dict[str, Any] = {}
73
+ if original_data is not None:
74
+ extra_kwargs["_maidr_original_data"] = original_data
75
+ if date_nums is not None:
76
+ extra_kwargs["_maidr_date_nums"] = date_nums
77
+
78
+ FigureManager.create_maidr(axes, PlotType.CANDLESTICK, **extra_kwargs)
53
79
 
54
80
  return plot
55
81
 
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(
@@ -83,9 +95,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
83
95
  )
84
96
 
85
97
  if wick_collection and body_collection:
86
- gid = f"maidr-{uuid.uuid4()}"
87
- wick_collection.set_gid(gid)
88
- body_collection.set_gid(gid)
98
+ wick_gid = f"maidr-{uuid.uuid4()}"
99
+ body_gid = f"maidr-{uuid.uuid4()}"
100
+ wick_collection.set_gid(wick_gid)
101
+ body_collection.set_gid(body_gid)
89
102
 
90
103
  candlestick_kwargs = dict(
91
104
  kwargs,
@@ -93,7 +106,13 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
93
106
  _maidr_body_collection=body_collection,
94
107
  _maidr_date_nums=date_nums,
95
108
  _maidr_original_data=data,
109
+ _maidr_wick_gid=wick_gid,
110
+ _maidr_body_gid=body_gid,
96
111
  )
112
+
113
+ # Add datetime converter
114
+ if datetime_converter is not None:
115
+ candlestick_kwargs["_maidr_datetime_converter"] = datetime_converter
97
116
  common(
98
117
  PlotType.CANDLESTICK,
99
118
  lambda *a, **k: price_ax,
@@ -126,6 +145,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
126
145
  _maidr_patches=volume_patches,
127
146
  _maidr_date_nums=date_nums,
128
147
  )
148
+
149
+ # Add datetime converter
150
+ if datetime_converter is not None:
151
+ bar_kwargs["_maidr_datetime_converter"] = datetime_converter # type: ignore
152
+
129
153
  common(PlotType.BAR, lambda *a, **k: volume_ax, instance, args, bar_kwargs)
130
154
 
131
155
  # Process and register Moving Averages as LINE plots
@@ -193,6 +217,10 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
193
217
  if date_nums is not None:
194
218
  setattr(line, "_maidr_date_nums", date_nums)
195
219
 
220
+ # Store datetime converter
221
+ if datetime_converter is not None:
222
+ setattr(line, "_maidr_datetime_converter", datetime_converter)
223
+
196
224
  # Ensure GID is set for highlighting
197
225
  if line.get_gid() is None:
198
226
  gid = f"maidr-{uuid.uuid4()}"
@@ -204,6 +232,11 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
204
232
  # Register all valid lines as a single LINE plot
205
233
  if valid_lines:
206
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
+
207
240
  common(PlotType.LINE, lambda *a, **k: price_ax, instance, args, line_kwargs)
208
241
 
209
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.0
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
@@ -28,7 +28,6 @@ Requires-Dist: htmltools>=0.5
28
28
  Requires-Dist: lxml>=5.1.0
29
29
  Requires-Dist: mplfinance>=0.12.10b0
30
30
  Requires-Dist: numpy>=1.26
31
- Requires-Dist: virtualenv<21,>=20.26.6
32
31
  Requires-Dist: wrapt<2,>=1.16.0
33
32
  Provides-Extra: jupyter
34
33
  Requires-Dist: ipykernel>=6.0.0; extra == 'jupyter'
@@ -1,4 +1,4 @@
1
- maidr/__init__.py,sha256=bvyrTCLC6rRb_Iu5wCMtnDYCY0tbaZ--c21hFuNMICE,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=dk1MCU5zqGA2CFLT1_1tGrsB6kYNhsUFLPLNaEi9e04,5363
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
@@ -28,7 +28,7 @@ maidr/exception/extraction_error.py,sha256=rd37Oxa9gn2OWFWt9AOH5fv0hNd3sAWGvpDMF
28
28
  maidr/patch/__init__.py,sha256=FnkoUQJC2ODhLO37GwgRVSitBCRax42Ti0e4NIAgdO0,236
29
29
  maidr/patch/barplot.py,sha256=qdUy5Y8zkMg4dH3fFh93OYzLdar4nl1u5O2Jcw4d2zI,2433
30
30
  maidr/patch/boxplot.py,sha256=l7wDD4pDi4ZbsL5EX5XDhPRxgtSIFSrFguMOZ7IC2eg,2845
31
- maidr/patch/candlestick.py,sha256=NFkzwpxmLBpWmb5s05pjk6obNMQee-xIEZTqGkbhhqM,1776
31
+ maidr/patch/candlestick.py,sha256=R2MgPX5ih9W-RBluvF6jFNJBxETH0eMt7Tzn0Ej9LoU,2652
32
32
  maidr/patch/clear.py,sha256=2Sc4CIt5jRGkew3TxFsBZm-uowC9yDSxtraEcXZjmGw,396
33
33
  maidr/patch/common.py,sha256=RV2NayjPmcWJVhZJV7nmBCjcH7MPDhW_fIluTOPAATk,880
34
34
  maidr/patch/heatmap.py,sha256=uxLLg530Ql9KVC5rxk8vnwPHXBWWHwYgJRkyHY-tJzs,1048
@@ -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=73aYyFYnAX-AXqPid8ScfexEU2Y8fC9L_tEidzPcKZo,8008
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.0.dist-info/METADATA,sha256=e3l2lyk61dpyniYkOGEIO2Ycyt10LWgPfM-z6OR8fYU,3193
55
- maidr-1.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- maidr-1.6.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
57
- maidr-1.6.0.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