maidr 1.8.1__py3-none-any.whl → 1.10.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.8.1"
1
+ __version__ = "1.10.0"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
maidr/api.py CHANGED
@@ -14,12 +14,12 @@ from maidr.core.figure_manager import FigureManager
14
14
  def _get_plot_or_current(plot: Any | None) -> Any:
15
15
  """
16
16
  Get the plot object or current matplotlib figure if plot is None.
17
-
17
+
18
18
  Parameters
19
19
  ----------
20
20
  plot : Any or None
21
21
  The plot object. If None, returns the current matplotlib figure.
22
-
22
+
23
23
  Returns
24
24
  -------
25
25
  Any
@@ -28,7 +28,7 @@ def _get_plot_or_current(plot: Any | None) -> Any:
28
28
  if plot is None:
29
29
  # Lazy import matplotlib.pyplot when needed
30
30
  import matplotlib.pyplot as plt
31
-
31
+
32
32
  return plt.gcf()
33
33
  return plot
34
34
 
@@ -48,7 +48,7 @@ def render(plot: Any | None = None) -> Tag:
48
48
  The rendered HTML representation of the plot.
49
49
  """
50
50
  plot = _get_plot_or_current(plot)
51
-
51
+
52
52
  ax = FigureManager.get_axes(plot)
53
53
  if isinstance(ax, list):
54
54
  for axes in ax:
@@ -82,7 +82,7 @@ def show(
82
82
  The display result.
83
83
  """
84
84
  plot = _get_plot_or_current(plot)
85
-
85
+
86
86
  ax = FigureManager.get_axes(plot)
87
87
  if isinstance(ax, list):
88
88
  for axes in ax:
@@ -95,10 +95,11 @@ def show(
95
95
 
96
96
  def save_html(
97
97
  plot: Any | None = None,
98
- *,
98
+ *,
99
99
  file: str,
100
- lib_dir: str | None = "lib",
101
- include_version: bool = True
100
+ lib_dir: str | None = "lib",
101
+ include_version: bool = True,
102
+ data_in_svg: bool = True,
102
103
  ) -> str:
103
104
  """
104
105
  Save a MAIDR plot as HTML file.
@@ -113,6 +114,8 @@ def save_html(
113
114
  Directory name for libraries.
114
115
  include_version : bool, default True
115
116
  Whether to include version information.
117
+ data_in_svg : bool, default True
118
+ Controls where the MAIDR JSON payload is placed in the HTML or SVG.
116
119
 
117
120
  Returns
118
121
  -------
@@ -120,19 +123,21 @@ def save_html(
120
123
  The path to the saved HTML file.
121
124
  """
122
125
  plot = _get_plot_or_current(plot)
123
-
126
+
124
127
  ax = FigureManager.get_axes(plot)
125
128
  htmls = []
126
129
  if isinstance(ax, list):
127
130
  for axes in ax:
128
131
  maidr = FigureManager.get_maidr(axes.get_figure())
129
- htmls.append(maidr._create_html_doc(use_iframe=False))
132
+ htmls.append(maidr._create_html_doc(use_iframe=False, data_in_svg=data_in_svg))
130
133
  return htmls[-1].save_html(
131
134
  file, libdir=lib_dir, include_version=include_version
132
135
  )
133
136
  else:
134
137
  maidr = FigureManager.get_maidr(ax.get_figure())
135
- return maidr.save_html(file, lib_dir=lib_dir, include_version=include_version)
138
+ return maidr.save_html(
139
+ file, lib_dir=lib_dir, include_version=include_version, data_in_svg=data_in_svg
140
+ )
136
141
 
137
142
 
138
143
  def stacked(plot: Axes | BarContainer) -> Maidr:
@@ -150,6 +155,6 @@ def close(plot: Any | None = None) -> None:
150
155
  The plot object to close. If None, uses the current matplotlib figure.
151
156
  """
152
157
  plot = _get_plot_or_current(plot)
153
-
158
+
154
159
  ax = FigureManager.get_axes(plot)
155
160
  FigureManager.destroy(ax.get_figure())
maidr/core/maidr.py CHANGED
@@ -72,7 +72,12 @@ class Maidr:
72
72
  return self._create_html_tag(use_iframe=True)
73
73
 
74
74
  def save_html(
75
- self, file: str, *, lib_dir: str | None = "lib", include_version: bool = True
75
+ self,
76
+ file: str,
77
+ *,
78
+ lib_dir: str | None = "lib",
79
+ include_version: bool = True,
80
+ data_in_svg: bool = True,
76
81
  ) -> str:
77
82
  """
78
83
  Save the HTML representation of the figure with MAIDR to a file.
@@ -86,9 +91,11 @@ class Maidr:
86
91
  (relative to the file's directory).
87
92
  include_version : bool, default=True
88
93
  Whether to include the version number in the dependency folder name.
94
+ data_in_svg : bool, default=True
95
+ Controls where the MAIDR JSON payload is placed in the output HTML or SVG.
89
96
  """
90
97
  html = self._create_html_doc(
91
- use_iframe=False
98
+ use_iframe=False, data_in_svg=data_in_svg
92
99
  ) # Always use direct HTML for saving
93
100
  return html.save_html(file, libdir=lib_dir, include_version=include_version)
94
101
 
@@ -180,8 +187,17 @@ class Maidr:
180
187
  else:
181
188
  webbrowser.open(f"file://{html_file_path}")
182
189
 
183
- def _create_html_tag(self, use_iframe: bool = True) -> Tag:
184
- """Create the MAIDR HTML using HTML tags."""
190
+ def _create_html_tag(self, use_iframe: bool = True, data_in_svg: bool = True) -> Tag:
191
+ """Create the MAIDR HTML using HTML tags.
192
+
193
+ Parameters
194
+ ----------
195
+ use_iframe : bool, default=True
196
+ Whether to render the plot inside an iframe (for notebooks and similar envs).
197
+ data_in_svg : bool, default=True
198
+ If True, the MAIDR JSON is embedded in the root <svg> under attribute 'maidr'.
199
+ If False, a <script>var maidr = {...}</script> tag is injected instead.
200
+ """
185
201
  tagged_elements: list[Any] = [
186
202
  element for plot in self._plots for element in plot.elements
187
203
  ]
@@ -191,16 +207,31 @@ class Maidr:
191
207
  for _ in plot.elements:
192
208
  selector_ids.append(self.selector_ids[i])
193
209
 
210
+ # Build schema once so id stays consistent across SVG and global var
211
+ schema = self._flatten_maidr()
212
+
194
213
  with HighlightContextManager.set_maidr_elements(tagged_elements, selector_ids):
195
- svg = self._get_svg()
196
- maidr = f"\nlet maidr = {json.dumps(self._flatten_maidr(), indent=2)}\n"
214
+ svg = self._get_svg(embed_data=data_in_svg, schema=schema)
215
+
216
+ # Generate external payload if data is not embedded in SVG
217
+ maidr = None
218
+ if not data_in_svg:
219
+ maidr = f"\nvar maidr = {json.dumps(schema, indent=2)}\n"
197
220
 
198
221
  # Inject plot's svg and MAIDR structure into html tag.
199
222
  return Maidr._inject_plot(svg, maidr, self.maidr_id, use_iframe)
200
223
 
201
- def _create_html_doc(self, use_iframe: bool = True) -> HTMLDocument:
202
- """Create an HTML document from Tag objects."""
203
- return HTMLDocument(self._create_html_tag(use_iframe), lang="en")
224
+ def _create_html_doc(self, use_iframe: bool = True, data_in_svg: bool = True) -> HTMLDocument:
225
+ """Create an HTML document from Tag objects.
226
+
227
+ Parameters
228
+ ----------
229
+ use_iframe : bool, default=True
230
+ Whether to render the plot inside an iframe (for notebooks and similar envs).
231
+ data_in_svg : bool, default=True
232
+ See _create_html_tag for details on payload placement strategy.
233
+ """
234
+ return HTMLDocument(self._create_html_tag(use_iframe, data_in_svg), lang="en")
204
235
 
205
236
  def _merge_plots_by_subplot_position(self) -> list[MaidrPlot]:
206
237
  """
@@ -312,10 +343,23 @@ class Maidr:
312
343
  for cell in subplot_grid[i]
313
344
  ]
314
345
 
315
- return {"id": Maidr._unique_id(), "subplots": subplot_grid}
346
+ return {
347
+ "id": Maidr._unique_id(),
348
+ "subplots": subplot_grid,
349
+ }
316
350
 
317
- def _get_svg(self) -> HTML:
318
- """Extract the chart SVG from ``matplotlib.figure.Figure``."""
351
+ def _get_svg(self, embed_data: bool = True, schema: dict | None = None) -> HTML:
352
+ """Extract the chart SVG from ``matplotlib.figure.Figure``.
353
+
354
+ Parameters
355
+ ----------
356
+ embed_data : bool, default=True
357
+ If True, embed the MAIDR JSON schema as an attribute named 'maidr' on
358
+ the root <svg> element. If False, do not embed JSON in the SVG.
359
+ schema : dict | None, default=None
360
+ If provided, this schema will be used (ensuring a consistent id across
361
+ the page). If None, a new schema will be generated.
362
+ """
319
363
  svg_buffer = io.StringIO()
320
364
  self._fig.savefig(svg_buffer, format="svg")
321
365
  str_svg = svg_buffer.getvalue()
@@ -323,12 +367,14 @@ class Maidr:
323
367
  etree.register_namespace("svg", "http://www.w3.org/2000/svg")
324
368
  tree_svg = etree.fromstring(str_svg.encode(), parser=None)
325
369
  root_svg = None
326
- # Find the `svg` tag and set unique id if not present else use it.
370
+ # Find the `svg` tag and optionally embed MAIDR data.
327
371
  for element in tree_svg.iter(tag="{http://www.w3.org/2000/svg}svg"):
328
- if "maidr-data" not in element.attrib:
329
- element.attrib["maidr-data"] = json.dumps(
330
- self._flatten_maidr(), indent=2
331
- )
372
+ current_schema = schema if schema is not None else self._flatten_maidr()
373
+ # Ensure SVG id matches schema id in both modes
374
+ if isinstance(current_schema, dict) and "id" in current_schema:
375
+ element.attrib["id"] = str(current_schema["id"]) # ensure match
376
+ if embed_data:
377
+ element.attrib["maidr"] = json.dumps(current_schema, indent=2)
332
378
  root_svg = element
333
379
  break
334
380
 
@@ -355,36 +401,41 @@ class Maidr:
355
401
  return str(uuid.uuid4())
356
402
 
357
403
  @staticmethod
358
- def _inject_plot(plot: HTML, maidr: str, maidr_id, use_iframe: bool = True) -> Tag:
404
+ def _inject_plot(plot: HTML, maidr: str | None, maidr_id, use_iframe: bool = True) -> Tag:
359
405
  """Embed the plot and associated MAIDR scripts into the HTML structure."""
360
406
  # Get the latest version from npm registry
361
407
  MAIDR_TS_CDN_URL = "https://cdn.jsdelivr.net/npm/maidr@latest/dist/maidr.js"
362
408
 
363
409
  script = f"""
364
- if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]'))
365
- {{
366
- var script = document.createElement('script');
367
- script.type = 'module';
368
- script.src = '{MAIDR_TS_CDN_URL}';
369
- script.addEventListener('load', function() {{
370
- window.main();
371
- }});
372
- document.head.appendChild(script);
373
- }} else {{
374
- document.addEventListener('DOMContentLoaded', function (e) {{
375
- window.main();
376
- }});
377
- }}
410
+ (function() {{
411
+ var existing = document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]');
412
+ if (!existing) {{
413
+ var s = document.createElement('script');
414
+ s.src = '{MAIDR_TS_CDN_URL}';
415
+ s.onload = function() {{ if (window.main) window.main(); }};
416
+ document.head.appendChild(s);
417
+ }} else {{
418
+ if (document.readyState === 'loading') {{
419
+ document.addEventListener('DOMContentLoaded', function() {{ if (window.main) window.main(); }});
420
+ }} else {{
421
+ if (window.main) window.main();
422
+ }}
423
+ }}
424
+ }})();
378
425
  """
379
426
 
380
- base_html = tags.div(
427
+ children = [
381
428
  tags.link(
382
429
  rel="stylesheet",
383
430
  href="https://cdn.jsdelivr.net/npm/maidr@latest/dist/maidr_style.css",
384
- ),
385
- tags.script(script, type="text/javascript"),
386
- tags.div(plot),
387
- )
431
+ )
432
+ ]
433
+ if maidr is not None:
434
+ children.append(tags.script(maidr, type="text/javascript"))
435
+ children.append(tags.script(script, type="text/javascript"))
436
+ children.append(tags.div(plot))
437
+
438
+ base_html = tags.div(*children)
388
439
 
389
440
  # is_quarto = os.getenv("IS_QUARTO") == "True"
390
441
 
@@ -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,23 +31,17 @@ 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:
@@ -62,105 +53,83 @@ class CandlestickPlot(MaidrPlot):
62
53
 
63
54
  def _extract_plot_data(self) -> list[dict]:
64
55
  """
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.
56
+ Extract candlestick data directly from the original DataFrame.
70
57
 
71
58
  Returns
72
59
  -------
73
60
  list[dict]
74
61
  List of dictionaries containing candlestick data with keys:
75
- - 'value': Date string
62
+ - 'value': Date string (raw from DataFrame index)
76
63
  - 'open': Opening price (float)
77
64
  - 'high': High price (float)
78
65
  - 'low': Low price (float)
79
66
  - 'close': Closing price (float)
80
- - 'volume': Volume (float, typically 0 for candlestick-only plots)
67
+ - 'volume': Volume (float)
81
68
  """
82
-
83
- # Get the custom collections from kwargs
84
69
  body_collection = self._maidr_body_collection
85
70
  wick_collection = self._maidr_wick_collection
86
71
 
87
72
  if body_collection and wick_collection:
88
- # Store the GIDs from the collections (modern path)
73
+ # Store the GIDs from the collections
89
74
  self._maidr_body_gid = body_collection.get_gid()
90
75
  self._maidr_wick_gid = wick_collection.get_gid()
91
- # Keep legacy gid filled for backward compatibility
92
76
  self._maidr_gid = self._maidr_body_gid or self._maidr_wick_gid
93
77
 
94
78
  # Use the original collections for highlighting
95
79
  self._elements = [body_collection, wick_collection]
96
80
 
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
81
+ # Extract data directly from DataFrame
82
+ if self._maidr_original_data is not None and isinstance(
83
+ self._maidr_original_data, pd.DataFrame
84
+ ):
85
+ return self._extract_from_dataframe(self._maidr_original_data)
161
86
 
162
87
  return []
163
88
 
89
+ def _extract_from_dataframe(self, df: pd.DataFrame) -> list[dict]:
90
+ """
91
+ Extract candlestick data directly from DataFrame without any formatting.
92
+
93
+ Parameters
94
+ ----------
95
+ df : pd.DataFrame
96
+ DataFrame with OHLC data and DatetimeIndex.
97
+
98
+ Returns
99
+ -------
100
+ list[dict]
101
+ List of candlestick data dictionaries with raw values.
102
+ """
103
+ candles = []
104
+
105
+ for i in range(len(df)):
106
+ try:
107
+ # Get date directly from index - raw representation
108
+ date_value = str(df.index[i])
109
+
110
+ # Get OHLC values directly from DataFrame columns
111
+ open_price = float(df.iloc[i]["Open"])
112
+ high_price = float(df.iloc[i]["High"])
113
+ low_price = float(df.iloc[i]["Low"])
114
+ close_price = float(df.iloc[i]["Close"])
115
+
116
+ # Get volume if available, otherwise 0
117
+ volume = float(df.iloc[i].get("Volume", 0.0))
118
+
119
+ candle_data = {
120
+ "value": date_value,
121
+ "open": open_price,
122
+ "high": high_price,
123
+ "low": low_price,
124
+ "close": close_price,
125
+ "volume": volume,
126
+ }
127
+ candles.append(candle_data)
128
+ except (KeyError, IndexError, ValueError, TypeError):
129
+ continue
130
+
131
+ return candles
132
+
164
133
  def _extract_axes_data(self) -> dict:
165
134
  """
166
135
  Extract the plot's axes data including labels.
@@ -6,18 +6,16 @@ from datetime import datetime
6
6
 
7
7
  class DatetimeConverter:
8
8
  """
9
- Enhanced datetime converter that automatically detects time periods
10
- and provides intelligent date/time formatting for mplfinance plots.
9
+ Datetime converter for mplfinance plots.
11
10
 
12
- This utility automatically detects the time period of financial data and formats
13
- datetime values consistently for screen reader accessibility and visual clarity.
11
+ This utility provides datetime value conversion for financial data visualization.
14
12
 
15
13
  Parameters
16
14
  ----------
17
15
  data : pd.DataFrame
18
16
  DataFrame with DatetimeIndex containing financial data.
19
17
  datetime_format : str, optional
20
- Custom datetime format string. If None, automatic format detection is used.
18
+ Custom datetime format string (currently unused, kept for compatibility).
21
19
 
22
20
  Attributes
23
21
  ----------
@@ -49,14 +47,14 @@ class DatetimeConverter:
49
47
  >>>
50
48
  >>> # Get formatted datetime
51
49
  >>> formatted = converter.get_formatted_datetime(0)
52
- >>> print(formatted) # Output: "Jan 15 2024"
50
+ >>> print(formatted) # Output: "2024-01-15 00:00:00"
53
51
  >>>
54
52
  >>> # For time-based data
55
53
  >>> hourly_dates = pd.date_range('2024-01-15 09:00:00', periods=3, freq='H')
56
54
  >>> df_hourly = pd.DataFrame({'Open': [3050, 3078, 3080]}, index=hourly_dates)
57
55
  >>> converter_hourly = create_datetime_converter(df_hourly)
58
56
  >>> formatted_hourly = converter_hourly.get_formatted_datetime(0)
59
- >>> print(formatted_hourly) # Output: "Jan 15 2024 09:00"
57
+ >>> print(formatted_hourly) # Output: "2024-01-15 09:00:00"
60
58
  """
61
59
 
62
60
  def __init__(
@@ -164,9 +162,7 @@ class DatetimeConverter:
164
162
 
165
163
  def get_formatted_datetime(self, index: int) -> Optional[str]:
166
164
  """
167
- Get formatted datetime string for given index using consistent formatting.
168
-
169
- Always includes year for screen reader accessibility.
165
+ Get datetime string for given index.
170
166
 
171
167
  Parameters
172
168
  ----------
@@ -176,13 +172,13 @@ class DatetimeConverter:
176
172
  Returns
177
173
  -------
178
174
  str or None
179
- Formatted datetime string or None if index is invalid.
175
+ Datetime string or None if index is invalid.
180
176
 
181
177
  Examples
182
178
  --------
183
179
  >>> converter = create_datetime_converter(df)
184
180
  >>> formatted = converter.get_formatted_datetime(0)
185
- >>> print(formatted) # "Jan 15 2024" for daily data
181
+ >>> print(formatted) # "2024-01-15 00:00:00"
186
182
  """
187
183
  if index not in self.date_mapping:
188
184
  return None
@@ -192,7 +188,7 @@ class DatetimeConverter:
192
188
 
193
189
  def _format_datetime_custom(self, dt: datetime) -> str:
194
190
  """
195
- Consistent datetime formatting with year always included.
191
+ Format datetime as-is using ISO format.
196
192
 
197
193
  Parameters
198
194
  ----------
@@ -202,24 +198,15 @@ class DatetimeConverter:
202
198
  Returns
203
199
  -------
204
200
  str
205
- Formatted datetime string with consistent pattern.
201
+ Formatted datetime string in ISO format.
206
202
 
207
203
  Notes
208
204
  -----
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.
205
+ Returns the datetime as a string without smart formatting.
206
+ Output format is "YYYY-MM-DD HH:MM:SS" (e.g., "2024-01-15 00:00:00").
213
207
  """
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")
208
+ # Return string representation of datetime
209
+ return str(dt)
223
210
 
224
211
  @property
225
212
  def date_nums(self) -> List[float]:
@@ -2,11 +2,10 @@
2
2
  Utility functions for handling mplfinance-specific data extraction and processing.
3
3
  """
4
4
 
5
+ import re
5
6
  import matplotlib.dates as mdates
6
- import numpy as np
7
7
  from matplotlib.patches import Rectangle
8
- from typing import List, Optional, Tuple, Any, Union
9
- import pandas as pd
8
+ from typing import List, Optional
10
9
 
11
10
 
12
11
  class MplfinanceDataExtractor:
@@ -58,229 +57,6 @@ class MplfinanceDataExtractor:
58
57
 
59
58
  return formatted_data
60
59
 
61
- @staticmethod
62
- def extract_candlestick_data(
63
- body_collection: Any,
64
- wick_collection: Any,
65
- date_nums: Optional[List[float]] = None,
66
- original_data: Optional[Union[pd.DataFrame, pd.Series, dict]] = None,
67
- ) -> List[dict]:
68
- """
69
- Extract candlestick data from mplfinance collections.
70
-
71
- Parameters
72
- ----------
73
- body_collection : Any
74
- PolyCollection containing candlestick bodies
75
- wick_collection : Any
76
- LineCollection containing candlestick wicks
77
- date_nums : Optional[List[float]], default=None
78
- List of matplotlib date numbers corresponding to the candles
79
- original_data : Optional[Union[pd.DataFrame, pd.Series, dict]], default=None
80
- Original DataFrame/Series/dict with OHLC data for accurate bull/bear classification
81
-
82
- Returns
83
- -------
84
- List[dict]
85
- List of dictionaries with OHLC data
86
- """
87
- if not body_collection or not hasattr(body_collection, "get_paths"):
88
- return []
89
-
90
- candles = []
91
- paths = body_collection.get_paths()
92
-
93
- for i, path in enumerate(paths):
94
- if len(path.vertices) >= 4:
95
- # Extract rectangle coordinates from the path
96
- vertices = path.vertices
97
- x_coords = vertices[:, 0]
98
- y_coords = vertices[:, 1]
99
-
100
- x_min, x_max = x_coords.min(), x_coords.max()
101
- y_min, y_max = y_coords.min(), y_coords.max()
102
-
103
- # Use date mapping if available
104
- if date_nums is not None and i < len(date_nums):
105
- date_num = date_nums[i]
106
- date_str = MplfinanceDataExtractor._convert_date_num_to_string(
107
- date_num
108
- )
109
- else:
110
- x_center = (x_min + x_max) / 2
111
- date_str = MplfinanceDataExtractor._convert_date_num_to_string(
112
- x_center
113
- )
114
-
115
- # Determine if this is an up or down candle using original data
116
- is_up = MplfinanceDataExtractor._determine_bull_bear_from_data(
117
- original_data, i, date_str
118
- )
119
-
120
- # Extract OHLC values
121
- (
122
- open_val,
123
- close_val,
124
- ) = MplfinanceDataExtractor._extract_ohlc_from_rectangle(
125
- y_min, y_max, is_up
126
- )
127
-
128
- # Estimate high and low (these would normally come from wick data)
129
- high_val = y_max + (y_max - y_min) * 0.1
130
- low_val = y_min - (y_max - y_min) * 0.1
131
-
132
- candle_data = {
133
- "value": date_str,
134
- "open": round(open_val, 2),
135
- "high": round(high_val, 2),
136
- "low": round(low_val, 2),
137
- "close": round(close_val, 2),
138
- "volume": 0, # Volume is handled separately
139
- }
140
-
141
- candles.append(candle_data)
142
-
143
- return candles
144
-
145
- @staticmethod
146
- def extract_rectangle_candlestick_data(
147
- body_rectangles: List[Rectangle],
148
- date_nums: Optional[List[float]] = None,
149
- original_data: Optional[Union[pd.DataFrame, pd.Series, dict]] = None,
150
- ) -> List[dict]:
151
- """
152
- Extract candlestick data from Rectangle patches (original_flavor).
153
-
154
- Parameters
155
- ----------
156
- body_rectangles : List[Rectangle]
157
- List of Rectangle patches representing candlestick bodies
158
- date_nums : Optional[List[float]], default=None
159
- List of matplotlib date numbers corresponding to the candles
160
- original_data : Optional[Union[pd.DataFrame, pd.Series, dict]], default=None
161
- Original DataFrame/Series/dict with OHLC data for accurate bull/bear classification
162
-
163
- Returns
164
- -------
165
- List[dict]
166
- List of dictionaries with OHLC data
167
- """
168
- if not body_rectangles:
169
- return []
170
-
171
- candles = []
172
-
173
- # Sort rectangles by x-coordinate
174
- body_rectangles.sort(key=lambda r: r.get_x())
175
-
176
- for i, rect in enumerate(body_rectangles):
177
- x_left = rect.get_x()
178
- width = rect.get_width()
179
- x_center_num = x_left + width / 2.0
180
-
181
- # Convert x coordinate to date
182
- if date_nums is not None and i < len(date_nums):
183
- date_str = MplfinanceDataExtractor._convert_date_num_to_string(
184
- date_nums[i]
185
- )
186
- else:
187
- date_str = MplfinanceDataExtractor._convert_date_num_to_string(
188
- x_center_num
189
- )
190
-
191
- y_bottom = rect.get_y()
192
- height = rect.get_height()
193
-
194
- # Determine if this is an up or down candle using original data
195
- is_up_candle = MplfinanceDataExtractor._determine_bull_bear_from_data(
196
- original_data, i, date_str
197
- )
198
-
199
- # Extract OHLC values from rectangle
200
- (
201
- open_price,
202
- close_price,
203
- ) = MplfinanceDataExtractor._extract_ohlc_from_rectangle(
204
- y_bottom, y_bottom + height, is_up_candle
205
- )
206
-
207
- # Estimate high and low
208
- high_price = max(open_price, close_price) + height * 0.1
209
- low_price = min(open_price, close_price) - height * 0.1
210
-
211
- # Ensure all values are valid numbers
212
- open_price = float(open_price) if not np.isnan(open_price) else 0.0
213
- high_price = float(high_price) if not np.isnan(high_price) else 0.0
214
- low_price = float(low_price) if not np.isnan(low_price) else 0.0
215
- close_price = float(close_price) if not np.isnan(close_price) else 0.0
216
-
217
- candle_data = {
218
- "value": date_str,
219
- "open": round(open_price, 2),
220
- "high": round(high_price, 2),
221
- "low": round(low_price, 2),
222
- "close": round(close_price, 2),
223
- "volume": 0,
224
- }
225
-
226
- candles.append(candle_data)
227
-
228
- return candles
229
-
230
- @staticmethod
231
- def _determine_bull_bear_from_data(
232
- original_data: Optional[Union[pd.DataFrame, pd.Series, dict]],
233
- index: int,
234
- date_str: str,
235
- ) -> bool:
236
- """
237
- Determine if a candle is bullish (up) or bearish (down) using original OHLC data.
238
-
239
- This is the most robust method as it uses the actual data rather than colors.
240
-
241
- Parameters
242
- ----------
243
- original_data : Optional[Union[pd.DataFrame, pd.Series, dict]]
244
- Original DataFrame/Series/dict with OHLC data
245
- index : int
246
- Index of the candle
247
- date_str : str
248
- Date string for the candle
249
-
250
- Returns
251
- -------
252
- bool
253
- True if bullish (close > open), False if bearish (close < open)
254
- """
255
- # Default to bullish if no data available
256
- if original_data is None:
257
- return True
258
-
259
- try:
260
- # Try to access the original data
261
- if hasattr(original_data, "iloc"):
262
- # It's a pandas DataFrame/Series
263
- if index < len(original_data):
264
- row = original_data.iloc[index]
265
- if "Close" in row and "Open" in row:
266
- is_bullish = row["Close"] > row["Open"]
267
- return is_bullish
268
-
269
- elif hasattr(original_data, "__getitem__"):
270
- # It's a dictionary or similar
271
- if "Close" in original_data and "Open" in original_data:
272
- closes = original_data["Close"]
273
- opens = original_data["Open"]
274
- if index < len(closes) and index < len(opens):
275
- is_bullish = closes[index] > opens[index]
276
- return is_bullish
277
-
278
- except (KeyError, IndexError, AttributeError):
279
- pass
280
-
281
- # Fallback to bullish if data access fails
282
- return True # Default to bullish
283
-
284
60
  @staticmethod
285
61
  def clean_axis_label(label: str) -> str:
286
62
  """
@@ -299,8 +75,6 @@ class MplfinanceDataExtractor:
299
75
  if not label or not isinstance(label, str):
300
76
  return label
301
77
 
302
- import re
303
-
304
78
  # Removes LaTeX-like scientific notation, e.g., "$10^{6}$"
305
79
  cleaned_label = re.sub(r"\s*\$.*?\$", "", label).strip()
306
80
  return cleaned_label if cleaned_label else label
@@ -318,7 +92,7 @@ class MplfinanceDataExtractor:
318
92
  Returns
319
93
  -------
320
94
  str
321
- Date string in YYYY-MM-DD format or fallback index
95
+ Date string representation (e.g., "2024-01-15 00:00:00") or fallback index
322
96
  """
323
97
  try:
324
98
  # Check if this looks like a matplotlib date number (typically > 700000)
@@ -326,14 +100,14 @@ class MplfinanceDataExtractor:
326
100
  date_dt = mdates.num2date(date_num)
327
101
  if hasattr(date_dt, "replace"):
328
102
  date_dt = date_dt.replace(tzinfo=None)
329
- return date_dt.strftime("%Y-%m-%d")
103
+ return str(date_dt)
330
104
  elif date_num > 1000:
331
105
  # Try converting as if it's a pandas timestamp
332
106
  try:
333
107
  import pandas as pd
334
108
 
335
109
  date_dt = pd.to_datetime(date_num, unit="D")
336
- return date_dt.strftime("%Y-%m-%d")
110
+ return str(date_dt)
337
111
  except (ValueError, TypeError):
338
112
  pass
339
113
  except (ValueError, TypeError, OverflowError):
@@ -341,75 +115,3 @@ class MplfinanceDataExtractor:
341
115
 
342
116
  # Fallback to index-based date string
343
117
  return f"date_{int(date_num):03d}"
344
-
345
- @staticmethod
346
- def convert_x_to_date(x_center_num: float, axes: Optional[List] = None) -> str:
347
- """
348
- Convert matplotlib x-coordinate to date string.
349
-
350
- Parameters
351
- ----------
352
- x_center_num : float
353
- X-coordinate value to convert
354
- axes : Optional[List], optional
355
- List of matplotlib axes to help with date conversion
356
-
357
- Returns
358
- -------
359
- str
360
- Date string in YYYY-MM-DD format or fallback
361
- """
362
- # First, try to get the actual dates from the axes x-axis data
363
- if axes and len(axes) > 0:
364
- ax = axes[0]
365
- try:
366
- # Get x-axis ticks which might contain the actual dates
367
- x_ticks = ax.get_xticks()
368
-
369
- # If we have x-axis ticks and they look like dates (large numbers), use them
370
- if len(x_ticks) > 0 and x_ticks[0] > 1000:
371
- # Find the closest tick to our x_center_num
372
- tick_index = int(round(x_center_num))
373
- if 0 <= tick_index < len(x_ticks):
374
- actual_date_num = x_ticks[tick_index]
375
-
376
- # Convert the actual date number
377
- if actual_date_num > 700000:
378
- date_dt = mdates.num2date(actual_date_num)
379
- if hasattr(date_dt, "replace"):
380
- date_dt = date_dt.replace(tzinfo=None)
381
- date_str = date_dt.strftime("%Y-%m-%d")
382
- return date_str
383
- except Exception:
384
- pass
385
-
386
- # Use the utility class for date conversion
387
- return MplfinanceDataExtractor._convert_date_num_to_string(x_center_num)
388
-
389
- @staticmethod
390
- def _extract_ohlc_from_rectangle(
391
- y_min: float, y_max: float, is_up: bool
392
- ) -> Tuple[float, float]:
393
- """
394
- Extract open and close values from rectangle bounds.
395
-
396
- Parameters
397
- ----------
398
- y_min : float
399
- Minimum y value of rectangle
400
- y_max : float
401
- Maximum y value of rectangle
402
- is_up : bool
403
- Whether this is an up candle
404
-
405
- Returns
406
- -------
407
- Tuple[float, float]
408
- (open_price, close_price)
409
- """
410
- if is_up:
411
- # Up candle: open at bottom, close at top
412
- return y_min, y_max
413
- else:
414
- # Down candle: open at top, close at bottom
415
- return y_max, y_min
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maidr
3
- Version: 1.8.1
3
+ Version: 1.10.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,9 +1,9 @@
1
- maidr/__init__.py,sha256=KlhM_Qllqa69TYYU98Ba_yfOHROB9TYO3rIbVitC1D8,415
2
- maidr/api.py,sha256=od539V0CKcs15FEXQo0hX_hK_A0U3noHE54obdHcTqY,4138
1
+ maidr/__init__.py,sha256=BXkAG-4A4pF_IjbBj6cOmPTmQYICXnfPniPJ-4OHnr0,416
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
5
5
  maidr/core/figure_manager.py,sha256=t-lhe4jj2gsF5-8VUBUZOPlDutKjm_AZ8xXWJU2pFRc,5555
6
- maidr/core/maidr.py,sha256=0MuyPgc1CFomA87jq70GG-MSxYaMI2Z7ct2FAOGCxQM,20067
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
9
  maidr/core/enum/maidr_key.py,sha256=ljG0omqzd8K8Yk213N7i7PXGvG-IOlnE5v7o6RoGJzc,795
@@ -12,7 +12,7 @@ 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=ofvlUwtzaaopvv6VjNDf1IZODbu1UkMHsi1zdvcG-Yo,10120
15
+ maidr/core/plot/candlestick.py,sha256=qOsYIbn2sRdGE2ES-YJ-Rwhu4NsScHs52mtKe6Bl7_A,8236
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
@@ -40,10 +40,10 @@ 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=AQ8qShbEkLVo13TUkOOmtOLnOvaI05Vh5oWhgchvXSA,14478
43
+ maidr/util/datetime_conversion.py,sha256=BF115xweGcrKyDnnjYPeScc0WgeNpCylV0Z-mYKaP4w,13769
44
44
  maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
45
45
  maidr/util/environment.py,sha256=C4VMyB16mqzrFxpJdxFdm40M0IZojxh60UX80680jgo,9403
46
- maidr/util/mplfinance_utils.py,sha256=OZe5Y7gzjdjve9DViioQvGZYTdZvz8obvN3oHElQFZw,14418
46
+ maidr/util/mplfinance_utils.py,sha256=00YEjrCUbigZZL1j9jzOTamNnwfy5ZZmXJj65AhgNbw,3662
47
47
  maidr/util/plot_detection.py,sha256=bgLHoDcHSRwOiyKzUK3EqGwdAIhF44ocHW5ox6xYGZw,3883
48
48
  maidr/util/regression_line_utils.py,sha256=yFKr-H0whT_su2YVZwNksBLp5EC5s77sr6HUFgNcsyY,2329
49
49
  maidr/util/svg_utils.py,sha256=2gyzBtNKFHs0utrw1iOlxTmznzivOWQMV2aW8zu2c8E,1442
@@ -52,7 +52,7 @@ maidr/util/mixin/extractor_mixin.py,sha256=j2Rv2vh_gqqcxLV1ka3xsPaPAfWsX94CtKIW2
52
52
  maidr/util/mixin/merger_mixin.py,sha256=V0qLw_6DUB7X6CQ3BCMpsCQX_ZuwAhoSTm_E4xAJFKM,712
53
53
  maidr/widget/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  maidr/widget/shiny.py,sha256=wrrw2KAIpE_A6CNQGBtNHauR1DjenA_n47qlFXX9_rk,745
55
- maidr-1.8.1.dist-info/METADATA,sha256=oi8YKhP-r2l0NN9AnBTngZFG11lQK1jtSnRsWj8osjc,3154
56
- maidr-1.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- maidr-1.8.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
- maidr-1.8.1.dist-info/RECORD,,
55
+ maidr-1.10.0.dist-info/METADATA,sha256=6llsnqQI7-gfp62yZJ1ucSXxf9xvrUVNtcItkoxnwnM,3155
56
+ maidr-1.10.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
57
+ maidr-1.10.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
+ maidr-1.10.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any