maidr 1.9.0__py3-none-any.whl → 1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
maidr/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "1.9.0"
1
+ __version__ = "1.11.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -11,6 +11,7 @@ class MaidrKey(str, Enum):
11
11
  # Plot data keys.
12
12
  AXES = "axes"
13
13
  DATA = "data"
14
+ FORMAT = "format"
14
15
  POINTS = "points"
15
16
  LEVEL = "level"
16
17
  X = "x"
@@ -1,24 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
- import uuid
4
3
  from typing import Union, Dict
5
4
  from matplotlib.axes import Axes
6
- from matplotlib.patches import Rectangle
7
- import numpy as np
5
+ import pandas as pd
8
6
 
9
7
  from maidr.core.enum import PlotType
10
8
  from maidr.core.plot import MaidrPlot
11
9
  from maidr.core.enum.maidr_key import MaidrKey
12
10
  from maidr.exception import ExtractionError
13
- from maidr.util.mplfinance_utils import MplfinanceDataExtractor
14
11
 
15
12
 
16
13
  class CandlestickPlot(MaidrPlot):
17
14
  """
18
15
  Specialized candlestick plot class for mplfinance OHLC data.
19
16
 
20
- This class handles the extraction and processing of candlestick data from mplfinance
21
- plots, including proper date conversion and data validation.
17
+ This class extracts candlestick data directly from the original DataFrame
18
+ without any formatting or transformation.
22
19
  """
23
20
 
24
21
  def __init__(self, axes: list[Axes], **kwargs) -> None:
@@ -34,133 +31,106 @@ class CandlestickPlot(MaidrPlot):
34
31
  Additional keyword arguments.
35
32
  """
36
33
  self.axes = axes
37
- # Ensure there's at least one axis for the superclass init
38
34
  if not axes:
39
35
  raise ValueError("Axes list cannot be empty.")
40
36
  super().__init__(axes[0], PlotType.CANDLESTICK)
41
37
 
42
- # Store custom collections passed from mplfinance patch
38
+ # Store collections passed from mplfinance patch
43
39
  self._maidr_wick_collection = kwargs.get("_maidr_wick_collection", None)
44
40
  self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
45
- self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
46
- self._maidr_original_data = kwargs.get(
47
- "_maidr_original_data", None
48
- ) # Store original data
49
- self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
41
+ self._maidr_original_data = kwargs.get("_maidr_original_data", None)
50
42
 
51
- # Store the GID for proper selector generation (legacy/shared)
43
+ # Store the GID for selector generation
52
44
  self._maidr_gid = None
53
- # Modern-path separate gids for body and wick
54
45
  self._maidr_body_gid = None
55
46
  self._maidr_wick_gid = None
56
47
  if self._maidr_body_collection:
57
48
  self._maidr_gid = self._maidr_body_collection.get_gid()
58
49
  self._maidr_body_gid = self._maidr_gid
59
- elif self._maidr_wick_collection:
60
- self._maidr_gid = self._maidr_wick_collection.get_gid()
61
- self._maidr_wick_gid = self._maidr_gid
50
+ if self._maidr_wick_collection:
51
+ self._maidr_wick_gid = self._maidr_wick_collection.get_gid()
52
+ if not self._maidr_gid:
53
+ self._maidr_gid = self._maidr_wick_gid
62
54
 
63
55
  def _extract_plot_data(self) -> list[dict]:
64
56
  """
65
- Extract candlestick data from the plot.
66
-
67
- This method processes candlestick plots from both modern (mplfinance.plot) and
68
- legacy (original_flavor) pipelines, extracting OHLC data and setting up
69
- highlighting elements and GIDs.
57
+ Extract candlestick data directly from the original DataFrame.
70
58
 
71
59
  Returns
72
60
  -------
73
61
  list[dict]
74
62
  List of dictionaries containing candlestick data with keys:
75
- - 'value': Date string
63
+ - 'value': Date string (raw from DataFrame index)
76
64
  - 'open': Opening price (float)
77
65
  - 'high': High price (float)
78
66
  - 'low': Low price (float)
79
67
  - 'close': Closing price (float)
80
- - 'volume': Volume (float, typically 0 for candlestick-only plots)
68
+ - 'volume': Volume (float)
81
69
  """
82
-
83
- # Get the custom collections from kwargs
84
70
  body_collection = self._maidr_body_collection
85
71
  wick_collection = self._maidr_wick_collection
86
72
 
87
73
  if body_collection and wick_collection:
88
- # Store the GIDs from the collections (modern path)
74
+ # Store the GIDs from the collections
89
75
  self._maidr_body_gid = body_collection.get_gid()
90
76
  self._maidr_wick_gid = wick_collection.get_gid()
91
- # Keep legacy gid filled for backward compatibility
92
77
  self._maidr_gid = self._maidr_body_gid or self._maidr_wick_gid
93
78
 
94
79
  # Use the original collections for highlighting
95
80
  self._elements = [body_collection, wick_collection]
96
81
 
97
- # Use datetime converter for enhanced data extraction
98
- if self._maidr_datetime_converter is not None:
99
- data = self._maidr_datetime_converter.extract_candlestick_data(
100
- self.axes[0], wick_collection, body_collection
101
- )
102
- return data
103
-
104
- # Fallback to original detection method
105
- if not self.axes:
106
- return []
107
-
108
- ax_ohlc = self.axes[0]
109
-
110
- # Look for Rectangle patches (original_flavor candlestick)
111
- body_rectangles = []
112
- for patch in ax_ohlc.patches:
113
- if isinstance(patch, Rectangle):
114
- body_rectangles.append(patch)
115
-
116
- if body_rectangles:
117
- # Set elements for highlighting
118
- self._elements = body_rectangles
119
-
120
- # Generate a GID for highlighting if none exists
121
- if not self._maidr_gid:
122
- self._maidr_gid = f"maidr-{uuid.uuid4()}"
123
- # Set GID on all rectangles
124
- for rect in body_rectangles:
125
- rect.set_gid(self._maidr_gid)
126
- # Keep a dedicated body gid for legacy dict selectors
127
- self._maidr_body_gid = (
128
- getattr(self, "_maidr_body_gid", None) or self._maidr_gid
129
- )
130
-
131
- # Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
132
- wick_lines = []
133
- for line in ax_ohlc.get_lines():
134
- try:
135
- xydata = line.get_xydata()
136
- if xydata is None:
137
- continue
138
- xy_arr = np.asarray(xydata)
139
- if (
140
- xy_arr.ndim == 2
141
- and xy_arr.shape[0] == 2
142
- and xy_arr.shape[1] >= 2
143
- ):
144
- x0 = float(xy_arr[0, 0])
145
- x1 = float(xy_arr[1, 0])
146
- if abs(x0 - x1) < 1e-10:
147
- wick_lines.append(line)
148
- except Exception:
149
- continue
150
- if wick_lines:
151
- if not getattr(self, "_maidr_wick_gid", None):
152
- self._maidr_wick_gid = f"maidr-{uuid.uuid4()}"
153
- for line in wick_lines:
154
- line.set_gid(self._maidr_wick_gid)
155
-
156
- # Use the utility class to extract data
157
- data = MplfinanceDataExtractor.extract_rectangle_candlestick_data(
158
- body_rectangles, self._maidr_date_nums, self._maidr_original_data
159
- )
160
- return data
82
+ # Extract data directly from DataFrame
83
+ if self._maidr_original_data is not None and isinstance(
84
+ self._maidr_original_data, pd.DataFrame
85
+ ):
86
+ return self._extract_from_dataframe(self._maidr_original_data)
161
87
 
162
88
  return []
163
89
 
90
+ def _extract_from_dataframe(self, df: pd.DataFrame) -> list[dict]:
91
+ """
92
+ Extract candlestick data directly from DataFrame without any formatting.
93
+
94
+ Parameters
95
+ ----------
96
+ df : pd.DataFrame
97
+ DataFrame with OHLC data and DatetimeIndex.
98
+
99
+ Returns
100
+ -------
101
+ list[dict]
102
+ List of candlestick data dictionaries with raw values.
103
+ """
104
+ candles = []
105
+
106
+ for i in range(len(df)):
107
+ try:
108
+ # Get date directly from index - raw representation
109
+ date_value = str(df.index[i])
110
+
111
+ # Get OHLC values directly from DataFrame columns
112
+ open_price = float(df.iloc[i]["Open"])
113
+ high_price = float(df.iloc[i]["High"])
114
+ low_price = float(df.iloc[i]["Low"])
115
+ close_price = float(df.iloc[i]["Close"])
116
+
117
+ # Get volume if available, otherwise 0
118
+ volume = float(df.iloc[i].get("Volume", 0.0))
119
+
120
+ candle_data = {
121
+ "value": date_value,
122
+ "open": open_price,
123
+ "high": high_price,
124
+ "low": low_price,
125
+ "close": close_price,
126
+ "volume": volume,
127
+ }
128
+ candles.append(candle_data)
129
+ except (KeyError, IndexError, ValueError, TypeError):
130
+ continue
131
+
132
+ return candles
133
+
164
134
  def _extract_axes_data(self) -> dict:
165
135
  """
166
136
  Extract the plot's axes data including labels.
@@ -178,11 +148,7 @@ class CandlestickPlot(MaidrPlot):
178
148
  return {MaidrKey.X: x_labels, MaidrKey.Y: self.ax.get_ylabel()}
179
149
 
180
150
  def _get_selector(self) -> Union[str, Dict[str, str]]:
181
- """Return selectors for highlighting candlestick elements.
182
-
183
- - Modern path (collections present): return a dict with separate selectors for body, wickLow, wickHigh
184
- - Legacy path: return a dict with body and shared wick selectors (no open/close keys)
185
- """
151
+ """Return selectors for highlighting candlestick elements."""
186
152
  # Modern path: build structured selectors using separate gids
187
153
  if (
188
154
  self._maidr_body_collection
@@ -221,12 +187,12 @@ class CandlestickPlot(MaidrPlot):
221
187
  }
222
188
  return selectors
223
189
 
224
- # Legacy path: build shared-id selectors; omit open/close
190
+ # Legacy path
225
191
  legacy_selectors = {}
226
- if getattr(self, "_maidr_body_gid", None) or self._maidr_gid:
227
- body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
192
+ if self._maidr_body_gid or self._maidr_gid:
193
+ body_gid = self._maidr_body_gid or self._maidr_gid
228
194
  legacy_selectors["body"] = f"g[id='{body_gid}'] > path"
229
- if getattr(self, "_maidr_wick_gid", None):
195
+ if self._maidr_wick_gid:
230
196
  legacy_selectors["wick"] = f"g[id='{self._maidr_wick_gid}'] > path"
231
197
  if legacy_selectors:
232
198
  return legacy_selectors
@@ -238,9 +204,12 @@ class CandlestickPlot(MaidrPlot):
238
204
  """Initialize the MAIDR schema dictionary with basic plot information."""
239
205
  base_schema = super().render()
240
206
  base_schema[MaidrKey.TITLE] = "Candlestick Chart"
241
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
207
+ # Update axes labels while preserving format from parent
208
+ axes_data = self._extract_axes_data()
209
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
210
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
211
+ base_schema[MaidrKey.AXES] = axes_data
242
212
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
243
- # Include selector only if the plot supports highlighting.
244
213
  if self._support_highlighting:
245
214
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
246
215
  return base_schema
@@ -5,12 +5,13 @@ from abc import ABC, abstractmethod
5
5
  from matplotlib.axes import Axes
6
6
 
7
7
  from maidr.core.enum import MaidrKey, PlotType
8
+ from maidr.util.mixin import FormatExtractorMixin
8
9
 
9
10
  # uuid is used to generate unique identifiers for each plot layer in the MAIDR schema.
10
11
  import uuid
11
12
 
12
13
 
13
- class MaidrPlot(ABC):
14
+ class MaidrPlot(ABC, FormatExtractorMixin):
14
15
  """
15
16
  Abstract base class for plots managed by the MAIDR system.
16
17
 
@@ -61,13 +62,21 @@ class MaidrPlot(ABC):
61
62
  """
62
63
  Generate the MAIDR schema for this plot layer, including a unique id for layer identification.
63
64
  """
65
+ # Extract axes data first
66
+ axes_data = self._extract_axes_data()
67
+
68
+ # Extract and include format configuration inside axes if available.
69
+ format_config = self.extract_format(self.ax)
70
+ if format_config:
71
+ axes_data[MaidrKey.FORMAT] = format_config
72
+
64
73
  # Generate a unique UUID for this layer to ensure each plot layer can be distinctly identified
65
74
  # in the MAIDR frontend. This supports robust layer switching.
66
75
  maidr_schema = {
67
76
  MaidrKey.ID: str(uuid.uuid4()),
68
77
  MaidrKey.TYPE: self.type,
69
78
  MaidrKey.TITLE: self.ax.get_title(),
70
- MaidrKey.AXES: self._extract_axes_data(),
79
+ MaidrKey.AXES: axes_data,
71
80
  MaidrKey.DATA: self._extract_plot_data(),
72
81
  }
73
82
 
@@ -142,7 +142,11 @@ class MplfinanceBarPlot(
142
142
  def render(self) -> dict:
143
143
  base_schema = super().render()
144
144
  base_schema[MaidrKey.TITLE] = "Volume Bar Plot"
145
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
145
+ # Update axes labels while preserving format from parent
146
+ axes_data = self._extract_axes_data()
147
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
148
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
149
+ base_schema[MaidrKey.AXES] = axes_data
146
150
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
147
151
  if self._support_highlighting:
148
152
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
@@ -192,7 +192,11 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
192
192
  def render(self) -> dict:
193
193
  base_schema = super().render()
194
194
  base_schema[MaidrKey.TITLE] = "Moving Average Line Plot"
195
- base_schema[MaidrKey.AXES] = self._extract_axes_data()
195
+ # Update axes labels while preserving format from parent
196
+ axes_data = self._extract_axes_data()
197
+ if MaidrKey.AXES in base_schema and MaidrKey.FORMAT in base_schema[MaidrKey.AXES]:
198
+ axes_data[MaidrKey.FORMAT] = base_schema[MaidrKey.AXES][MaidrKey.FORMAT]
199
+ base_schema[MaidrKey.AXES] = axes_data
196
200
  base_schema[MaidrKey.DATA] = self._extract_plot_data()
197
201
  if self._support_highlighting:
198
202
  base_schema[MaidrKey.SELECTOR] = self._get_selector()
@@ -49,14 +49,14 @@ class DatetimeConverter:
49
49
  >>>
50
50
  >>> # Get formatted datetime
51
51
  >>> formatted = converter.get_formatted_datetime(0)
52
- >>> print(formatted) # Output: "Jan 15 2024"
52
+ >>> print(formatted) # Output: "2024-01-15 00:00:00"
53
53
  >>>
54
54
  >>> # For time-based data
55
55
  >>> hourly_dates = pd.date_range('2024-01-15 09:00:00', periods=3, freq='H')
56
56
  >>> df_hourly = pd.DataFrame({'Open': [3050, 3078, 3080]}, index=hourly_dates)
57
57
  >>> converter_hourly = create_datetime_converter(df_hourly)
58
58
  >>> formatted_hourly = converter_hourly.get_formatted_datetime(0)
59
- >>> print(formatted_hourly) # Output: "Jan 15 2024 09:00"
59
+ >>> print(formatted_hourly) # Output: "2024-01-15 09:00:00"
60
60
  """
61
61
 
62
62
  def __init__(
@@ -182,7 +182,7 @@ class DatetimeConverter:
182
182
  --------
183
183
  >>> converter = create_datetime_converter(df)
184
184
  >>> formatted = converter.get_formatted_datetime(0)
185
- >>> print(formatted) # "Jan 15 2024" for daily data
185
+ >>> print(formatted) # "2024-01-15 00:00:00" (plain datetime string)
186
186
  """
187
187
  if index not in self.date_mapping:
188
188
  return None
@@ -192,7 +192,7 @@ class DatetimeConverter:
192
192
 
193
193
  def _format_datetime_custom(self, dt: datetime) -> str:
194
194
  """
195
- Consistent datetime formatting with year always included.
195
+ Return plain datetime string representation.
196
196
 
197
197
  Parameters
198
198
  ----------
@@ -202,24 +202,14 @@ class DatetimeConverter:
202
202
  Returns
203
203
  -------
204
204
  str
205
- Formatted datetime string with consistent pattern.
205
+ Plain string representation of datetime (ISO format).
206
206
 
207
207
  Notes
208
208
  -----
209
- Formatting rules:
210
- - Daily data: "Jan 15 2024"
211
- - Time-based data: "Jan 15 2024 09:00" or "Jan 15 2024 09:00:30"
212
- - Seconds are only shown when they are non-zero for cleaner display.
209
+ Returns the raw string representation of the datetime object,
210
+ allowing the frontend to handle formatting as needed.
213
211
  """
214
- if self.time_period in ["minute", "intraday", "hour"]:
215
- # Time-based data: include time with optional seconds
216
- if dt.second == 0:
217
- return dt.strftime("%b %d %Y %H:%M")
218
- else:
219
- return dt.strftime("%b %d %Y %H:%M:%S")
220
- else:
221
- # Daily/weekly/monthly data: just date
222
- return dt.strftime("%b %d %Y")
212
+ return str(dt)
223
213
 
224
214
  @property
225
215
  def date_nums(self) -> List[float]: