maidr 1.7.1__py3-none-any.whl → 1.7.3__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.7.1"
1
+ __version__ = "1.7.3"
2
2
 
3
3
  from .api import close, render, save_html, show, stacked
4
4
  from .core import Maidr
@@ -15,20 +15,24 @@ from maidr.core.plot import MaidrPlotFactory
15
15
 
16
16
  class FigureManager:
17
17
  """
18
- Manages creation and retrieval of Maidr instances associated with matplotlib figures.
18
+ Manages creation and retrieval of Maidr instances associated with figures.
19
19
 
20
- This class provides methods to manage Maidr objects which facilitate the organization and
21
- manipulation of plots within matplotlib figures.
20
+ This class provides methods to manage Maidr objects which facilitate the
21
+ organization and manipulation of plots within matplotlib figures.
22
22
 
23
23
  Attributes
24
24
  ----------
25
25
  figs : dict
26
- A dictionary that maps matplotlib Figure objects to their corresponding Maidr instances.
26
+ A dictionary that maps matplotlib Figure objects to their corresponding
27
+ Maidr instances.
28
+ PLOT_TYPE_PRIORITY : dict
29
+ Defines the priority order for plot types. Higher numbers take precedence.
27
30
 
28
31
  Methods
29
32
  -------
30
33
  create_maidr(ax, plot_type, **kwargs)
31
- Creates a Maidr instance for the given Axes and plot type, and adds a plot to it.
34
+ Creates a Maidr instance for the given Axes and plot type, and adds a
35
+ plot to it.
32
36
  _get_maidr(fig)
33
37
  Retrieves or creates a Maidr instance associated with the given Figure.
34
38
  get_axes(artist)
@@ -37,6 +41,21 @@ class FigureManager:
37
41
 
38
42
  figs = {}
39
43
 
44
+ # Define plot type priority order (higher numbers take precedence)
45
+ PLOT_TYPE_PRIORITY = {
46
+ PlotType.BAR: 1,
47
+ PlotType.STACKED: 2,
48
+ PlotType.DODGED: 2, # DODGED and STACKED have same priority
49
+ PlotType.LINE: 1,
50
+ PlotType.SCATTER: 1,
51
+ PlotType.HIST: 1,
52
+ PlotType.BOX: 1,
53
+ PlotType.HEAT: 1,
54
+ PlotType.COUNT: 1,
55
+ PlotType.SMOOTH: 1,
56
+ PlotType.CANDLESTICK: 1,
57
+ }
58
+
40
59
  _instance = None
41
60
  _lock = threading.Lock()
42
61
 
@@ -44,14 +63,15 @@ class FigureManager:
44
63
  if not cls._instance:
45
64
  with cls._lock:
46
65
  if not cls._instance:
47
- cls._instance = super(FigureManager, cls).__new__()
66
+ cls._instance = super(FigureManager, cls).__new__(cls)
48
67
  return cls._instance
49
68
 
50
69
  @classmethod
51
70
  def create_maidr(
52
71
  cls, axes: Axes | list[Axes], plot_type: PlotType, **kwargs
53
72
  ) -> Maidr:
54
- """Create a Maidr instance for the given Axes and plot type, and adds a plot to it."""
73
+ """Create a Maidr instance for the given Axes and plot type, and
74
+ adds a plot to it."""
55
75
  if axes is None:
56
76
  raise ValueError("No plot found.")
57
77
  if plot_type is None:
@@ -72,9 +92,34 @@ class FigureManager:
72
92
 
73
93
  @classmethod
74
94
  def _get_maidr(cls, fig: Figure, plot_type: PlotType) -> Maidr:
75
- """Retrieve or create a Maidr instance for the given Figure."""
95
+ """
96
+ Retrieve or create a Maidr instance for the given Figure.
97
+
98
+ If a Maidr instance already exists for the figure, update its plot type
99
+ if the new plot type has higher priority (DODGED/STACKED > BAR).
100
+
101
+ Parameters
102
+ ----------
103
+ fig : Figure
104
+ The matplotlib figure to get or create a Maidr instance for.
105
+ plot_type : PlotType
106
+ The plot type to set or update for the Maidr instance.
107
+
108
+ Returns
109
+ -------
110
+ Maidr
111
+ The Maidr instance associated with the figure.
112
+ """
76
113
  if fig not in cls.figs.keys():
77
114
  cls.figs[fig] = Maidr(fig, plot_type)
115
+ else:
116
+ # Update plot type if the new type has higher priority
117
+ maidr = cls.figs[fig]
118
+ current_priority = cls.PLOT_TYPE_PRIORITY.get(maidr.plot_type, 0)
119
+ new_priority = cls.PLOT_TYPE_PRIORITY.get(plot_type, 0)
120
+
121
+ if new_priority > current_priority:
122
+ maidr.plot_type = plot_type
78
123
  return cls.figs[fig]
79
124
 
80
125
  @classmethod
maidr/core/maidr.py CHANGED
@@ -10,6 +10,7 @@ import webbrowser
10
10
  import subprocess
11
11
  from pathlib import Path
12
12
  from typing import Any, Literal, cast
13
+ from collections import defaultdict
13
14
 
14
15
  import matplotlib.pyplot as plt
15
16
  from htmltools import HTML, HTMLDocument, Tag, tags
@@ -159,10 +160,7 @@ class Maidr:
159
160
  if explorer_path:
160
161
  try:
161
162
  result = subprocess.run(
162
- [explorer_path, url],
163
- capture_output=True,
164
- text=True,
165
- timeout=10
163
+ [explorer_path, url], capture_output=True, text=True, timeout=10
166
164
  )
167
165
 
168
166
  if result.returncode == 0:
@@ -204,10 +202,49 @@ class Maidr:
204
202
  """Create an HTML document from Tag objects."""
205
203
  return HTMLDocument(self._create_html_tag(use_iframe), lang="en")
206
204
 
205
+ def _merge_plots_by_subplot_position(self) -> list[MaidrPlot]:
206
+ """
207
+ Merge plots by their subplot position, keeping only the first plot per position.
208
+
209
+ For DODGED and STACKED plot types, multiple plots on the same subplot
210
+ should be merged into a single plot since GroupedBarPlot extracts all
211
+ containers from the axes itself.
212
+
213
+ Returns
214
+ -------
215
+ list[MaidrPlot]
216
+ List of plots with one plot per unique subplot position.
217
+
218
+ Examples
219
+ --------
220
+ If we have plots at positions [(0,0), (0,0), (0,1), (1,0)],
221
+ this will return plots at positions [(0,0), (0,1), (1,0)].
222
+ """
223
+ # Group plots by their subplot position (row, col) using defaultdict
224
+ subplot_groups: dict[tuple[int, int], list[MaidrPlot]] = defaultdict(list)
225
+
226
+ for plot in self._plots:
227
+ # Get subplot position, defaulting to (0, 0) if not set
228
+ position = (getattr(plot, "row_index", 0), getattr(plot, "col_index", 0))
229
+ subplot_groups[position].append(plot)
230
+
231
+ # Keep only the first plot for each subplot position
232
+ # The GroupedBarPlot will extract all containers from that axes
233
+ merged_plots: list[MaidrPlot] = []
234
+ for position_plots in subplot_groups.values():
235
+ merged_plots.append(
236
+ position_plots[0]
237
+ ) # Each list is guaranteed to have at least one plot
238
+
239
+ return merged_plots
240
+
207
241
  def _flatten_maidr(self) -> dict | list[dict]:
208
242
  """Return a single plot schema or a list of schemas from the Maidr instance."""
243
+ # Handle DODGED/STACKED plots: only keep one plot per subplot position
244
+ # because GroupedBarPlot extracts all containers from the axes itself
209
245
  if self.plot_type in (PlotType.DODGED, PlotType.STACKED):
210
- self._plots = [self._plots[0]]
246
+ self._plots = self._merge_plots_by_subplot_position()
247
+
211
248
  # Deduplicate: if any SMOOTH plots exist, remove LINE plots
212
249
  self._plots = deduplicate_smooth_and_line(self._plots)
213
250
 
@@ -354,7 +391,11 @@ class Maidr:
354
391
  # Render the plot inside an iframe if in a Jupyter notebook, Google Colab
355
392
  # or VSCode notebook. No need for iframe if this is a Quarto document.
356
393
  # For TypeScript we will use iframe by default for now
357
- if use_iframe and (Environment.is_flask() or Environment.is_notebook() or Environment.is_shiny()):
394
+ if use_iframe and (
395
+ Environment.is_flask()
396
+ or Environment.is_notebook()
397
+ or Environment.is_shiny()
398
+ ):
358
399
  unique_id = "iframe_" + Maidr._unique_id()
359
400
 
360
401
  def generate_iframe_script(unique_id: str) -> str:
@@ -366,16 +407,29 @@ class Maidr:
366
407
  iframe.contentWindow.document
367
408
  ) {{
368
409
  let iframeDocument = iframe.contentWindow.document;
369
- let brailleContainer =
370
- iframeDocument.getElementById('braille-input');
410
+ // Detect braille textarea by dynamic id prefix
411
+ let brailleContainer = iframeDocument.querySelector('[id^="maidr-braille-textarea"]');
412
+ // Detect review input container by class name
413
+ let reviewInputContainer = iframeDocument.querySelector('.maidr-review-input');
371
414
  iframe.style.height = 'auto';
372
415
  let height = iframeDocument.body.scrollHeight;
373
- if (brailleContainer &&
374
- brailleContainer === iframeDocument.activeElement
375
- ) {{
416
+ // Consider braille active if it or any descendant has focus
417
+ let isBrailleActive = brailleContainer && (
418
+ brailleContainer === iframeDocument.activeElement ||
419
+ (typeof brailleContainer.contains === 'function' && brailleContainer.contains(iframeDocument.activeElement))
420
+ );
421
+ // Consider review input active if it or any descendant has focus
422
+ let isReviewInputActive = reviewInputContainer && (
423
+ reviewInputContainer === iframeDocument.activeElement ||
424
+ (typeof reviewInputContainer.contains === 'function' && reviewInputContainer.contains(iframeDocument.activeElement))
425
+ );
426
+ // (logs removed)
427
+ if (isBrailleActive) {{
376
428
  height += 100;
377
- }}else{{
378
- height += 50
429
+ }} else if (isReviewInputActive) {{
430
+ height += 50;
431
+ }} else {{
432
+ height += 50;
379
433
  }}
380
434
  iframe.style.height = (height) + 'px';
381
435
  iframe.style.width = iframeDocument.body.scrollWidth + 'px';
@@ -387,12 +441,32 @@ class Maidr:
387
441
  resizeIframe();
388
442
  iframe.contentWindow.addEventListener('resize', resizeIframe);
389
443
  }};
390
- iframe.contentWindow.document.addEventListener('focusin', () => {{
391
- resizeIframe();
392
- }});
393
- iframe.contentWindow.document.addEventListener('focusout', () => {{
394
- resizeIframe();
395
- }});
444
+ // Delegate focus events for braille textarea (by id prefix)
445
+ iframe.contentWindow.document.addEventListener('focusin', (e) => {{
446
+ try {{
447
+ const t = e && e.target ? e.target : null;
448
+ if (t && typeof t.id === 'string' && t.id.startsWith('maidr-braille-textarea')) resizeIframe();
449
+ }} catch (_) {{ resizeIframe(); }}
450
+ }}, true);
451
+ iframe.contentWindow.document.addEventListener('focusout', (e) => {{
452
+ try {{
453
+ const t = e && e.target ? e.target : null;
454
+ if (t && typeof t.id === 'string' && t.id.startsWith('maidr-braille-textarea')) resizeIframe();
455
+ }} catch (_) {{ resizeIframe(); }}
456
+ }}, true);
457
+ // Delegate focus events for review input container (by class name)
458
+ iframe.contentWindow.document.addEventListener('focusin', (e) => {{
459
+ try {{
460
+ const t = e && e.target ? e.target : null;
461
+ if (t && t.classList && t.classList.contains('maidr-review-input')) resizeIframe();
462
+ }} catch (_) {{ resizeIframe(); }}
463
+ }}, true);
464
+ iframe.contentWindow.document.addEventListener('focusout', (e) => {{
465
+ try {{
466
+ const t = e && e.target ? e.target : null;
467
+ if (t && t.classList && t.classList.contains('maidr-review-input')) resizeIframe();
468
+ }} catch (_) {{ resizeIframe(); }}
469
+ }}, true);
396
470
  """
397
471
  return resizing_script
398
472
 
@@ -43,7 +43,9 @@ class CandlestickPlot(MaidrPlot):
43
43
  self._maidr_wick_collection = kwargs.get("_maidr_wick_collection", None)
44
44
  self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
45
45
  self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
46
- self._maidr_original_data = kwargs.get("_maidr_original_data", None) # Store original data
46
+ self._maidr_original_data = kwargs.get(
47
+ "_maidr_original_data", None
48
+ ) # Store original data
47
49
  self._maidr_datetime_converter = kwargs.get("_maidr_datetime_converter", None)
48
50
 
49
51
  # Store the GID for proper selector generation (legacy/shared)
@@ -122,7 +124,9 @@ class CandlestickPlot(MaidrPlot):
122
124
  for rect in body_rectangles:
123
125
  rect.set_gid(self._maidr_gid)
124
126
  # Keep a dedicated body gid for legacy dict selectors
125
- self._maidr_body_gid = getattr(self, "_maidr_body_gid", None) or self._maidr_gid
127
+ self._maidr_body_gid = (
128
+ getattr(self, "_maidr_body_gid", None) or self._maidr_gid
129
+ )
126
130
 
127
131
  # Assign a shared gid to wick Line2D (vertical 2-point lines) on the same axis
128
132
  wick_lines = []
@@ -132,7 +136,11 @@ class CandlestickPlot(MaidrPlot):
132
136
  if xydata is None:
133
137
  continue
134
138
  xy_arr = np.asarray(xydata)
135
- if xy_arr.ndim == 2 and xy_arr.shape[0] == 2 and xy_arr.shape[1] >= 2:
139
+ if (
140
+ xy_arr.ndim == 2
141
+ and xy_arr.shape[0] == 2
142
+ and xy_arr.shape[1] >= 2
143
+ ):
136
144
  x0 = float(xy_arr[0, 0])
137
145
  x1 = float(xy_arr[1, 0])
138
146
  if abs(x0 - x1) < 1e-10:
@@ -176,7 +184,12 @@ class CandlestickPlot(MaidrPlot):
176
184
  - Legacy path: return a dict with body and shared wick selectors (no open/close keys)
177
185
  """
178
186
  # 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:
187
+ if (
188
+ self._maidr_body_collection
189
+ and self._maidr_wick_collection
190
+ and self._maidr_body_gid
191
+ and self._maidr_wick_gid
192
+ ):
180
193
  # Determine candle count N
181
194
  N = None
182
195
  if self._maidr_original_data is not None:
@@ -115,7 +115,9 @@ class MultiLinePlot(MaidrPlot, LineExtractorMixin):
115
115
  line_type = label
116
116
 
117
117
  # Use the new method to extract data with categorical labels
118
- line_coords = LineExtractorMixin.extract_line_data_with_categorical_labels(self.ax, line)
118
+ line_coords = LineExtractorMixin.extract_line_data_with_categorical_labels(
119
+ self.ax, line
120
+ )
119
121
  if line_coords is None:
120
122
  continue
121
123
 
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
5
5
  from matplotlib.axes import Axes
6
6
 
7
7
  from maidr.core.enum import MaidrKey, PlotType
8
+
8
9
  # uuid is used to generate unique identifiers for each plot layer in the MAIDR schema.
9
10
  import uuid
10
11
 
@@ -105,7 +105,10 @@ class MplfinanceLinePlot(MaidrPlot, LineExtractorMixin):
105
105
  continue
106
106
 
107
107
  # Use datetime converter for enhanced data extraction
108
- datetime_converter = getattr(line, "_maidr_datetime_converter", None) or self._maidr_datetime_converter
108
+ datetime_converter = (
109
+ getattr(line, "_maidr_datetime_converter", None)
110
+ or self._maidr_datetime_converter
111
+ )
109
112
  if datetime_converter is not None:
110
113
  # Convert x-coordinate (matplotlib index) to formatted datetime
111
114
  x_value = datetime_converter.get_formatted_datetime(int(round(x)))
maidr/patch/barplot.py CHANGED
@@ -18,9 +18,10 @@ def bar(
18
18
 
19
19
  This function patches the bar plotting functions to identify whether the
20
20
  plot should be rendered as a normal, stacked, or dodged bar plot.
21
- It uses the 'bottom' keyword to identify stacked bar plots. If 'bottom'
22
- is not provided and the x positions (first positional argument) are numeric,
23
- then a dodged plot is assumed.
21
+ It uses the 'bottom' keyword to identify stacked bar plots. For dodged plots,
22
+ it first checks for seaborn-specific indicators (hue parameter with dodge=True),
23
+ then uses robust detection logic that considers both width and context
24
+ to avoid misclassifying simple bar plots with narrow widths as dodged plots.
24
25
 
25
26
  Parameters
26
27
  ----------
@@ -33,6 +34,7 @@ def bar(
33
34
  For a dodged plot, the first argument (x positions) should be numeric.
34
35
  kwargs : dict
35
36
  Keyword arguments passed to the original function.
37
+ For seaborn plots, may contain 'hue' and 'dodge' parameters.
36
38
 
37
39
  Returns
38
40
  -------
@@ -41,33 +43,172 @@ def bar(
41
43
 
42
44
  Examples
43
45
  --------
44
- >>> # For a dodged (grouped) bar plot, pass numeric x positions:
46
+ >>> # For a seaborn dodged (grouped) bar plot:
47
+ >>> sns.barplot(data=df, x='category', y='value', hue='group', dodge=True)
48
+
49
+ >>> # For a manual dodged (grouped) bar plot, pass numeric x positions:
45
50
  >>> x_positions = np.arange(3)
46
51
  >>> ax.bar(x_positions, heights, width, label='Group') # Dodged bar plot.
47
52
  """
48
53
  plot_type = PlotType.BAR
54
+
55
+ # Check for stacked plots first (explicit bottom parameter)
49
56
  if "bottom" in kwargs:
50
57
  bottom = kwargs.get("bottom")
51
58
  if bottom is not None:
52
59
  plot_type = PlotType.STACKED
53
60
  else:
54
- if len(args) >= 3:
55
- real_width = args[2]
61
+ # Check for seaborn-specific dodged plot indicators first
62
+ # This handles seaborn.barplot with hue and dodge=True
63
+ if "hue" in kwargs and kwargs.get("dodge"):
64
+ plot_type = PlotType.DODGED
56
65
  else:
57
- real_width = kwargs.get("width", 0.8)
66
+ # Extract width and align parameters
67
+ if len(args) >= 3:
68
+ real_width = args[2]
69
+ else:
70
+ real_width = kwargs.get("width", 0.8)
58
71
 
59
- align = kwargs.get("align", "center")
72
+ align = kwargs.get("align", "center")
60
73
 
61
- if (isinstance(real_width, (int, float)) and float(real_width) < 0.8) or (
62
- align == "edge"
63
- ):
64
- plot_type = PlotType.DODGED
65
- if "dodge" in kwargs:
66
- plot_type = PlotType.DODGED
74
+ # More robust dodged plot detection: consider multiple factors
75
+ # Only classify as DODGED if there are strong indicators of grouping
76
+ should_be_dodged = _should_classify_as_dodged(
77
+ instance, real_width, align, args, kwargs
78
+ )
79
+
80
+ if should_be_dodged:
81
+ plot_type = PlotType.DODGED
67
82
 
68
83
  return common(plot_type, wrapped, instance, args, kwargs)
69
84
 
70
85
 
86
+ def _should_classify_as_dodged(
87
+ ax: Any, width: Any, align: str, args: Tuple[Any, ...], kwargs: Dict[str, Any]
88
+ ) -> bool:
89
+ """
90
+ Determine if a bar plot should be classified as dodged based on context.
91
+
92
+ This function uses more sophisticated logic than just checking width < 0.8,
93
+ as simple bar plots with narrow widths should not be considered dodged.
94
+
95
+ Parameters
96
+ ----------
97
+ ax : Any
98
+ The axes instance where the plot is being created.
99
+ width : Any
100
+ The width parameter for the bar plot.
101
+ align : str
102
+ The alignment parameter for the bar plot.
103
+ args : tuple
104
+ Positional arguments passed to the bar function.
105
+ kwargs : dict
106
+ Keyword arguments passed to the bar function.
107
+
108
+ Returns
109
+ -------
110
+ bool
111
+ True if the plot should be classified as DODGED, False otherwise.
112
+
113
+ Examples
114
+ --------
115
+ >>> # These should be DODGED:
116
+ >>> ax.bar([0.1, 1.1, 2.1], [1, 2, 3], width=0.4, label='Group A')
117
+ >>> ax.bar([0.4, 1.4, 2.4], [4, 5, 6], width=0.4, label='Group B')
118
+
119
+ >>> # These should remain BAR:
120
+ >>> ax.bar(['A', 'B', 'C'], [1, 2, 3], width=0.6) # Simple categorical bar plot
121
+ """
122
+ # If align is 'edge', it's likely a dodged plot
123
+ if align == "edge":
124
+ return True
125
+
126
+ # If width is specified and very narrow (< 0.5), more likely to be dodged
127
+ # But only if there are other indicators
128
+ if isinstance(width, (int, float)) and float(width) < 0.5:
129
+ # Check if x positions suggest grouping (numeric positions with fractional parts)
130
+ if len(args) > 0:
131
+ x_positions = args[0]
132
+ if _has_numeric_grouping_pattern(x_positions):
133
+ return True
134
+
135
+ # Check if there are already multiple bar containers on the axes
136
+ # This suggests that this might be part of a grouped bar plot
137
+ if hasattr(ax, "containers") and len(ax.containers) > 0:
138
+ # If there are existing containers, this might be adding to a group
139
+ if isinstance(width, (int, float)) and float(width) < 0.8:
140
+ return True
141
+
142
+ # Check for explicit grouping indicators in kwargs
143
+ if "label" in kwargs and isinstance(width, (int, float)) and float(width) < 0.8:
144
+ # If there's a label and narrow width, it might be part of a group
145
+ # But we need to be conservative here to avoid false positives
146
+ if _has_numeric_grouping_pattern(args[0] if len(args) > 0 else None):
147
+ return True
148
+
149
+ # Default to False - prefer BAR over DODGED for ambiguous cases
150
+ return False
151
+
152
+
153
+ def _has_numeric_grouping_pattern(x_positions: Any) -> bool:
154
+ """
155
+ Check if x positions suggest a grouping pattern typical of dodged plots.
156
+
157
+ Parameters
158
+ ----------
159
+ x_positions : Any
160
+ The x positions for the bar plot.
161
+
162
+ Returns
163
+ -------
164
+ bool
165
+ True if the positions suggest grouping, False otherwise.
166
+
167
+ Examples
168
+ --------
169
+ >>> _has_numeric_grouping_pattern([0.1, 1.1, 2.1]) # True - fractional offsets
170
+ >>> _has_numeric_grouping_pattern(['A', 'B', 'C']) # False - categorical
171
+ >>> _has_numeric_grouping_pattern([0, 1, 2]) # False - simple numeric
172
+ """
173
+ try:
174
+ # Convert to list if possible (duck typing)
175
+ try:
176
+ positions = list(x_positions)
177
+ except TypeError:
178
+ return False
179
+
180
+ # If all positions are strings, it's categorical (not dodged)
181
+ if all(isinstance(pos, str) for pos in positions):
182
+ return False
183
+
184
+ # If positions are numeric, check for fractional offsets
185
+ # that suggest manual positioning for grouping
186
+ numeric_positions = []
187
+ for pos in positions:
188
+ try:
189
+ numeric_positions.append(float(pos))
190
+ except (ValueError, TypeError):
191
+ return False
192
+
193
+ if len(numeric_positions) < 2:
194
+ return False
195
+
196
+ # Check if positions have fractional parts that suggest manual offset
197
+ # for grouping (e.g., [0.1, 1.1, 2.1] or [0.8, 1.8, 2.8])
198
+ fractional_parts = [pos % 1 for pos in numeric_positions]
199
+
200
+ # If all have the same non-zero fractional part, it suggests grouping
201
+ if all(abs(frac - fractional_parts[0]) < 0.01 for frac in fractional_parts):
202
+ if fractional_parts[0] > 0.01: # Non-zero fractional part
203
+ return True
204
+
205
+ return False
206
+
207
+ except Exception:
208
+ # If anything goes wrong in analysis, default to False
209
+ return False
210
+
211
+
71
212
  # Patch matplotlib functions.
72
213
  wrapt.wrap_function_wrapper(Axes, "bar", bar)
73
214
  wrapt.wrap_function_wrapper(Axes, "barh", bar)
maidr/patch/mplfinance.py CHANGED
@@ -72,6 +72,7 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
72
72
  # fallback: use index if it's a DatetimeIndex
73
73
  try:
74
74
  import matplotlib.dates as mdates
75
+
75
76
  date_nums = [mdates.date2num(d) for d in data.index]
76
77
  except Exception:
77
78
  pass
@@ -82,7 +83,7 @@ def mplfinance_plot_patch(wrapped, instance, args, kwargs):
82
83
  datetime_converter = create_datetime_converter(data)
83
84
 
84
85
  # Use enhanced converter's date_nums for mplfinance compatibility
85
- if date_nums is None and hasattr(datetime_converter, 'date_nums'):
86
+ if date_nums is None and hasattr(datetime_converter, "date_nums"):
86
87
  date_nums = datetime_converter.date_nums
87
88
 
88
89
  # Process and register the Candlestick plot
@@ -3,6 +3,7 @@ import numpy as np
3
3
  from typing import Optional, Dict, Any, List, Tuple
4
4
  from datetime import datetime
5
5
 
6
+
6
7
  class DatetimeConverter:
7
8
  """
8
9
  Enhanced datetime converter that automatically detects time periods
@@ -58,7 +59,9 @@ class DatetimeConverter:
58
59
  >>> print(formatted_hourly) # Output: "Jan 15 2024 09:00"
59
60
  """
60
61
 
61
- def __init__(self, data: pd.DataFrame, datetime_format: Optional[str] = None) -> None:
62
+ def __init__(
63
+ self, data: pd.DataFrame, datetime_format: Optional[str] = None
64
+ ) -> None:
62
65
  """
63
66
  Initialize the DatetimeConverter.
64
67
 
@@ -120,7 +123,7 @@ class DatetimeConverter:
120
123
  # Calculate average time difference between consecutive data points
121
124
  time_diffs = []
122
125
  for i in range(1, len(self.data)):
123
- diff = self.data.index[i] - self.data.index[i-1]
126
+ diff = self.data.index[i] - self.data.index[i - 1]
124
127
  time_diffs.append(diff.total_seconds())
125
128
 
126
129
  avg_diff_seconds = np.mean(time_diffs)
@@ -155,7 +158,7 @@ class DatetimeConverter:
155
158
  "day": "Daily data",
156
159
  "week": "Weekly data",
157
160
  "month": "Monthly data",
158
- "unknown": "Unknown time period"
161
+ "unknown": "Unknown time period",
159
162
  }
160
163
  return period_descriptions.get(self.time_period, "Unknown time period")
161
164
 
@@ -236,6 +239,7 @@ class DatetimeConverter:
236
239
  """
237
240
  try:
238
241
  import matplotlib.dates as mdates
242
+
239
243
  date_nums = []
240
244
  for d in self.data.index:
241
245
  try:
@@ -247,7 +251,9 @@ class DatetimeConverter:
247
251
  except Exception:
248
252
  return []
249
253
 
250
- def extract_candlestick_data(self, ax, wick_collection=None, body_collection=None) -> List[Dict[str, Any]]:
254
+ def extract_candlestick_data(
255
+ self, ax, wick_collection=None, body_collection=None
256
+ ) -> List[Dict[str, Any]]:
251
257
  """
252
258
  Extract candlestick data with proper datetime formatting using original DataFrame.
253
259
 
@@ -273,33 +279,40 @@ class DatetimeConverter:
273
279
  datetime values using the enhanced datetime conversion logic.
274
280
  """
275
281
  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'):
282
+ if (
283
+ not hasattr(self.data, "Open")
284
+ or not hasattr(self.data, "High")
285
+ or not hasattr(self.data, "Low")
286
+ or not hasattr(self.data, "Close")
287
+ ):
277
288
  return candles
278
289
 
279
290
  for i in range(len(self.data)):
280
291
  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)
292
+ open_price = self.data.iloc[i]["Open"]
293
+ high_price = self.data.iloc[i]["High"]
294
+ low_price = self.data.iloc[i]["Low"]
295
+ close_price = self.data.iloc[i]["Close"]
296
+ volume = self.data.iloc[i].get("Volume", 0.0)
286
297
 
287
298
  formatted_datetime = self.get_formatted_datetime(i)
288
299
 
289
300
  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)
301
+ "value": formatted_datetime or f"datetime_{i:03d}",
302
+ "open": float(open_price),
303
+ "high": float(high_price),
304
+ "low": float(low_price),
305
+ "close": float(close_price),
306
+ "volume": float(volume),
296
307
  }
297
308
  candles.append(candle_data)
298
309
  except (KeyError, IndexError, ValueError):
299
310
  continue
300
311
  return candles
301
312
 
302
- def extract_moving_average_data(self, ax, line_index: int = 0) -> List[Tuple[str, float]]:
313
+ def extract_moving_average_data(
314
+ self, ax, line_index: int = 0
315
+ ) -> List[Tuple[str, float]]:
303
316
  """
304
317
  Extract moving average data with proper datetime formatting and NaN filtering.
305
318
 
@@ -364,10 +377,10 @@ class DatetimeConverter:
364
377
  datetime values using the enhanced datetime conversion logic.
365
378
  """
366
379
  volume_data = []
367
- if hasattr(self.data, 'Volume'):
380
+ if hasattr(self.data, "Volume"):
368
381
  for i in range(len(self.data)):
369
382
  try:
370
- volume = self.data.iloc[i]['Volume']
383
+ volume = self.data.iloc[i]["Volume"]
371
384
  if pd.isna(volume) or volume <= 0:
372
385
  continue
373
386
  formatted_datetime = self.get_formatted_datetime(i)
@@ -377,7 +390,9 @@ class DatetimeConverter:
377
390
  return volume_data
378
391
 
379
392
 
380
- def create_datetime_converter(data: pd.DataFrame, datetime_format: Optional[str] = None) -> DatetimeConverter:
393
+ def create_datetime_converter(
394
+ data: pd.DataFrame, datetime_format: Optional[str] = None
395
+ ) -> DatetimeConverter:
381
396
  """
382
397
  Factory function to create a DatetimeConverter instance.
383
398
 
maidr/util/environment.py CHANGED
@@ -101,6 +101,7 @@ class Environment:
101
101
  return False
102
102
  except ImportError:
103
103
  return False
104
+
104
105
  @staticmethod
105
106
  def is_wsl() -> bool:
106
107
  """
@@ -122,9 +123,9 @@ class Environment:
122
123
  False # When not in WSL
123
124
  """
124
125
  try:
125
- with open('/proc/version', 'r') as f:
126
+ with open("/proc/version", "r") as f:
126
127
  version_info = f.read().lower()
127
- if 'microsoft' in version_info or 'wsl' in version_info:
128
+ if "microsoft" in version_info or "wsl" in version_info:
128
129
  return True
129
130
  except FileNotFoundError:
130
131
  pass
@@ -151,7 +152,7 @@ class Environment:
151
152
  >>> Environment.get_wsl_distro_name()
152
153
  '' # When not in WSL or WSL_DISTRO_NAME not set
153
154
  """
154
- return os.environ.get('WSL_DISTRO_NAME', '')
155
+ return os.environ.get("WSL_DISTRO_NAME", "")
155
156
 
156
157
  @staticmethod
157
158
  def find_explorer_path() -> Union[str, None]:
@@ -175,10 +176,7 @@ class Environment:
175
176
  # Check if explorer.exe is in PATH
176
177
  try:
177
178
  result = subprocess.run(
178
- ['which', 'explorer.exe'],
179
- capture_output=True,
180
- text=True,
181
- timeout=5
179
+ ["which", "explorer.exe"], capture_output=True, text=True, timeout=5
182
180
  )
183
181
  if result.returncode == 0:
184
182
  return result.stdout.strip()
@@ -187,8 +185,6 @@ class Environment:
187
185
 
188
186
  return None
189
187
 
190
-
191
-
192
188
  @staticmethod
193
189
  def get_renderer() -> str:
194
190
  """Return renderer which can be ipython or browser."""
@@ -114,7 +114,9 @@ class LineExtractorMixin:
114
114
  return ax.get_lines()
115
115
 
116
116
  @staticmethod
117
- def extract_line_data_with_categorical_labels(ax: Axes, line: Line2D) -> Optional[List[Tuple[Union[str, float], float]]]:
117
+ def extract_line_data_with_categorical_labels(
118
+ ax: Axes, line: Line2D
119
+ ) -> Optional[List[Tuple[Union[str, float], float]]]:
118
120
  """
119
121
  Extract line data with proper handling of categorical x-axis labels.
120
122
 
@@ -231,7 +231,7 @@ class MplfinanceDataExtractor:
231
231
  def _determine_bull_bear_from_data(
232
232
  original_data: Optional[Union[pd.DataFrame, pd.Series, dict]],
233
233
  index: int,
234
- date_str: str
234
+ date_str: str,
235
235
  ) -> bool:
236
236
  """
237
237
  Determine if a candle is bullish (up) or bearish (down) using original OHLC data.
@@ -258,19 +258,19 @@ class MplfinanceDataExtractor:
258
258
 
259
259
  try:
260
260
  # Try to access the original data
261
- if hasattr(original_data, 'iloc'):
261
+ if hasattr(original_data, "iloc"):
262
262
  # It's a pandas DataFrame/Series
263
263
  if index < len(original_data):
264
264
  row = original_data.iloc[index]
265
- if 'Close' in row and 'Open' in row:
266
- is_bullish = row['Close'] > row['Open']
265
+ if "Close" in row and "Open" in row:
266
+ is_bullish = row["Close"] > row["Open"]
267
267
  return is_bullish
268
268
 
269
- elif hasattr(original_data, '__getitem__'):
269
+ elif hasattr(original_data, "__getitem__"):
270
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']
271
+ if "Close" in original_data and "Open" in original_data:
272
+ closes = original_data["Close"]
273
+ opens = original_data["Open"]
274
274
  if index < len(closes) and index < len(opens):
275
275
  is_bullish = closes[index] > opens[index]
276
276
  return is_bullish
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maidr
3
- Version: 1.7.1
3
+ Version: 1.7.3
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=5pE1kfXuUom-1WcK9Z4Dl8PFegDww1BeKyscUSWJ4Rg,415
1
+ maidr/__init__.py,sha256=yWh6GLg3euB1LRZdmADmvGibtFcvZtr48m0w2uFEykU,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
5
- maidr/core/figure_manager.py,sha256=jXs-Prkeru1Pahj21hjh8BAwXM9ZFUZ3GFfKUfIRX_M,4117
6
- maidr/core/maidr.py,sha256=1f3H6E2gXTWTdO_vNmiGmPq1dDn-Q96wjirkosxIgPg,15802
5
+ maidr/core/figure_manager.py,sha256=t-lhe4jj2gsF5-8VUBUZOPlDutKjm_AZ8xXWJU2pFRc,5555
6
+ maidr/core/maidr.py,sha256=0MuyPgc1CFomA87jq70GG-MSxYaMI2Z7ct2FAOGCxQM,20067
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,21 +12,21 @@ 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=9R1ddSnUkGEE9WJgUPeuZhRtxXNBnOM9rbJdfJcY8bo,9886
15
+ maidr/core/plot/candlestick.py,sha256=ofvlUwtzaaopvv6VjNDf1IZODbu1UkMHsi1zdvcG-Yo,10120
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
- maidr/core/plot/lineplot.py,sha256=VbMjG-D5X-oeehduAtbqsyZ-hbcFc4NQYBwpCzgk4lo,4492
20
- maidr/core/plot/maidr_plot.py,sha256=9T0boWaonM99jggEed97-rCy_cufRMWXrXo-CbmPudE,4230
19
+ maidr/core/plot/lineplot.py,sha256=uoJpGkJB3IJSDJTwH6ECxLyXGdarsVQNULELp5NncWg,4522
20
+ maidr/core/plot/maidr_plot.py,sha256=heotWue1IzMOXnpoHVCqKSW88Sfep1IuuP4MBarpSek,4231
21
21
  maidr/core/plot/maidr_plot_factory.py,sha256=NW2iFScswgXbAC9rAOo4iMkAFsjY43DAvFioGr0yzx4,2732
22
22
  maidr/core/plot/mplfinance_barplot.py,sha256=zhTp2i6BH0xn7vQvGTotKgu2HbzlKT4p6zA5CVUUHHc,5673
23
- maidr/core/plot/mplfinance_lineplot.py,sha256=oRXFAa13wsz5i9tDN267ZSqd1-gGpOEzntmFTAz8o1w,7269
23
+ maidr/core/plot/mplfinance_lineplot.py,sha256=pIbsusnQg1_GrstVVHfMw-t9yipWJowa9ZvGsiVV6l8,7329
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
27
27
  maidr/exception/extraction_error.py,sha256=rd37Oxa9gn2OWFWt9AOH5fv0hNd3sAWGvpDMFBuJY2I,607
28
28
  maidr/patch/__init__.py,sha256=FnkoUQJC2ODhLO37GwgRVSitBCRax42Ti0e4NIAgdO0,236
29
- maidr/patch/barplot.py,sha256=qdUy5Y8zkMg4dH3fFh93OYzLdar4nl1u5O2Jcw4d2zI,2433
29
+ maidr/patch/barplot.py,sha256=QncV4Wv0B5bfY3OekA_ga8tgRLNmRDJf89mnx7xeQzs,7762
30
30
  maidr/patch/boxplot.py,sha256=l7wDD4pDi4ZbsL5EX5XDhPRxgtSIFSrFguMOZ7IC2eg,2845
31
31
  maidr/patch/candlestick.py,sha256=R2MgPX5ih9W-RBluvF6jFNJBxETH0eMt7Tzn0Ej9LoU,2652
32
32
  maidr/patch/clear.py,sha256=2Sc4CIt5jRGkew3TxFsBZm-uowC9yDSxtraEcXZjmGw,396
@@ -36,23 +36,23 @@ 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=L_C_EsIghUVVdtOxooa1a9Lxe-UcUpKuYV2Tw_bMDBw,9490
39
+ maidr/patch/mplfinance.py,sha256=ySD32onanoMgdQkV6XlSAbVd_BQuLWuEQtpkYSEDSzA,9491
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
+ maidr/util/datetime_conversion.py,sha256=AQ8qShbEkLVo13TUkOOmtOLnOvaI05Vh5oWhgchvXSA,14478
44
44
  maidr/util/dedup_utils.py,sha256=RpgPL5p-3oULUHaTCZJaQKhPHfyPkvBLHMt8lAGpJ5A,438
45
- maidr/util/environment.py,sha256=SSfxzjmY2ZVA98B-IVql8n9AX3d6h2f7_R4t3_53dYI,9452
46
- maidr/util/mplfinance_utils.py,sha256=aWNRkNS2IAF4YYEF9w7CcmcKWMGg3KkYJAv0xL8KcyY,14417
45
+ maidr/util/environment.py,sha256=C4VMyB16mqzrFxpJdxFdm40M0IZojxh60UX80680jgo,9403
46
+ maidr/util/mplfinance_utils.py,sha256=OZe5Y7gzjdjve9DViioQvGZYTdZvz8obvN3oHElQFZw,14418
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
50
50
  maidr/util/mixin/__init__.py,sha256=aGJZNhtWh77yIVPc7ipIZm1OajigjMtCWYKPuDWTC-c,217
51
- maidr/util/mixin/extractor_mixin.py,sha256=l1DQTqZiVeZ18qXrz-rKCh8ETFofucBfwjIjO4S85HU,6923
51
+ maidr/util/mixin/extractor_mixin.py,sha256=j2Rv2vh_gqqcxLV1ka3xsPaPAfWsX94CtKIW2FgPLnI,6937
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.7.1.dist-info/METADATA,sha256=WjKqqdKOwH8ZkI00md0BINyrpZv_hpD3WL5ZL1_iXkM,3154
56
- maidr-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- maidr-1.7.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
- maidr-1.7.1.dist-info/RECORD,,
55
+ maidr-1.7.3.dist-info/METADATA,sha256=uOTyppfpjH4RVQSO0x9S4owUWLM9X2Ta9-sCjr3xQD0,3154
56
+ maidr-1.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
+ maidr-1.7.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
58
+ maidr-1.7.3.dist-info/RECORD,,
File without changes