maidr 1.3.0__tar.gz → 1.4.1__tar.gz
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-1.3.0 → maidr-1.4.1}/PKG-INFO +1 -1
- {maidr-1.3.0 → maidr-1.4.1}/maidr/__init__.py +2 -1
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/maidr.py +74 -7
- maidr-1.4.1/maidr/core/plot/candlestick.py +147 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/maidr_plot.py +7 -2
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/maidr_plot_factory.py +12 -11
- maidr-1.4.1/maidr/core/plot/mplfinance_barplot.py +139 -0
- maidr-1.4.1/maidr/core/plot/mplfinance_lineplot.py +273 -0
- maidr-1.4.1/maidr/patch/__init__.py +15 -0
- maidr-1.4.1/maidr/patch/mplfinance.py +215 -0
- maidr-1.4.1/maidr/util/__init__.py +3 -0
- maidr-1.4.1/maidr/util/mplfinance_utils.py +409 -0
- maidr-1.4.1/maidr/util/plot_detection.py +136 -0
- {maidr-1.3.0 → maidr-1.4.1}/pyproject.toml +1 -1
- maidr-1.3.0/maidr/core/plot/candlestick.py +0 -239
- maidr-1.3.0/maidr/util/__init__.py +0 -0
- maidr-1.3.0/maidr/widget/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/LICENSE +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/README.md +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/api.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/context_manager.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/enum/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/enum/library.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/enum/maidr_key.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/enum/plot_type.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/enum/smooth_keywords.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/figure_manager.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/barplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/boxplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/grouped_barplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/heatmap.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/histogram.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/lineplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/regplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/core/plot/scatterplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/exception/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/exception/extraction_error.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/barplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/boxplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/candlestick.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/clear.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/common.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/heatmap.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/highlight.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/histogram.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/kdeplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/lineplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/regplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/patch/scatterplot.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/dedup_utils.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/environment.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/mixin/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/mixin/extractor_mixin.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/mixin/merger_mixin.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/regression_line_utils.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/util/svg_utils.py +0 -0
- {maidr-1.3.0/maidr/patch → maidr-1.4.1/maidr/widget}/__init__.py +0 -0
- {maidr-1.3.0 → maidr-1.4.1}/maidr/widget/shiny.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: maidr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: Multimodal Access and Interactive Data Representations
|
|
5
5
|
License: GPL-3.0-or-later
|
|
6
6
|
Keywords: accessibility,visualization,sonification,braille,tactile,multimodal,data representation,blind,low vision,visual impairments
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
import urllib.request
|
|
5
|
+
import urllib.error
|
|
6
|
+
import json
|
|
4
7
|
|
|
5
8
|
import io
|
|
6
9
|
import json
|
|
@@ -22,6 +25,11 @@ from maidr.core.plot import MaidrPlot
|
|
|
22
25
|
from maidr.util.environment import Environment
|
|
23
26
|
from maidr.util.dedup_utils import deduplicate_smooth_and_line
|
|
24
27
|
|
|
28
|
+
# Module-level cache for version to avoid repeated API calls
|
|
29
|
+
_MAIDR_VERSION_CACHE: str | None = None
|
|
30
|
+
_MAIDR_VERSION_CACHE_TIME: float = 0.0
|
|
31
|
+
_MAIDR_CACHE_DURATION = 3600 # Cache for 1 hour
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
class Maidr:
|
|
27
35
|
"""
|
|
@@ -34,6 +42,8 @@ class Maidr:
|
|
|
34
42
|
The matplotlib figure associated with this instance.
|
|
35
43
|
_plots : list[MaidrPlot]
|
|
36
44
|
A list of MaidrPlot objects which hold additional plot-specific configurations.
|
|
45
|
+
_cached_version : str | None
|
|
46
|
+
Cached version of maidr from npm registry to avoid repeated API calls.
|
|
37
47
|
|
|
38
48
|
Methods
|
|
39
49
|
-------
|
|
@@ -271,20 +281,77 @@ class Maidr:
|
|
|
271
281
|
"""Generate a unique identifier string using UUID4."""
|
|
272
282
|
return str(uuid.uuid4())
|
|
273
283
|
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _get_latest_maidr_version() -> str:
|
|
286
|
+
"""
|
|
287
|
+
Query the npm registry API to get the latest version of maidr with caching.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
str
|
|
292
|
+
The latest version of maidr from npm registry, or 'latest' as fallback.
|
|
293
|
+
"""
|
|
294
|
+
import time
|
|
295
|
+
|
|
296
|
+
global _MAIDR_VERSION_CACHE, _MAIDR_VERSION_CACHE_TIME
|
|
297
|
+
|
|
298
|
+
# Check if version fetching is disabled via environment variable
|
|
299
|
+
if os.getenv("MAIDR_DISABLE_VERSION_FETCH", "").lower() in ("true", "1", "yes"):
|
|
300
|
+
return "latest"
|
|
301
|
+
|
|
302
|
+
current_time = time.time()
|
|
303
|
+
|
|
304
|
+
# Check if we have a valid cached version
|
|
305
|
+
if (
|
|
306
|
+
_MAIDR_VERSION_CACHE is not None
|
|
307
|
+
and current_time - _MAIDR_VERSION_CACHE_TIME < _MAIDR_CACHE_DURATION
|
|
308
|
+
):
|
|
309
|
+
return _MAIDR_VERSION_CACHE
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Query npm registry API for maidr package
|
|
313
|
+
with urllib.request.urlopen(
|
|
314
|
+
"https://registry.npmjs.org/maidr/latest", timeout=5 # 5 second timeout
|
|
315
|
+
) as response:
|
|
316
|
+
if response.status == 200:
|
|
317
|
+
data = json.loads(response.read().decode("utf-8"))
|
|
318
|
+
version = data.get("version", "latest")
|
|
319
|
+
|
|
320
|
+
# Cache the successful result
|
|
321
|
+
_MAIDR_VERSION_CACHE = version
|
|
322
|
+
_MAIDR_VERSION_CACHE_TIME = current_time
|
|
323
|
+
|
|
324
|
+
return version
|
|
325
|
+
|
|
326
|
+
except Exception:
|
|
327
|
+
# Any error - just use latest
|
|
328
|
+
pass
|
|
329
|
+
|
|
330
|
+
# Fallback to 'latest' if API call fails
|
|
331
|
+
return "latest"
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def clear_version_cache() -> None:
|
|
335
|
+
"""Clear the cached version to force a fresh API call on next request."""
|
|
336
|
+
global _MAIDR_VERSION_CACHE, _MAIDR_VERSION_CACHE_TIME
|
|
337
|
+
_MAIDR_VERSION_CACHE = None
|
|
338
|
+
_MAIDR_VERSION_CACHE_TIME = 0.0
|
|
339
|
+
|
|
274
340
|
@staticmethod
|
|
275
341
|
def _inject_plot(plot: HTML, maidr: str, maidr_id, use_iframe: bool = True) -> Tag:
|
|
276
342
|
"""Embed the plot and associated MAIDR scripts into the HTML structure."""
|
|
277
|
-
#
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
343
|
+
# Get the latest version from npm registry
|
|
344
|
+
latest_version = Maidr._get_latest_maidr_version()
|
|
345
|
+
MAIDR_TS_CDN_URL = (
|
|
346
|
+
f"https://cdn.jsdelivr.net/npm/maidr@{latest_version}/dist/maidr.js"
|
|
347
|
+
)
|
|
281
348
|
|
|
282
349
|
script = f"""
|
|
283
|
-
if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}
|
|
350
|
+
if (!document.querySelector('script[src="{MAIDR_TS_CDN_URL}"]'))
|
|
284
351
|
{{
|
|
285
352
|
var script = document.createElement('script');
|
|
286
353
|
script.type = 'module';
|
|
287
|
-
script.src = '{MAIDR_TS_CDN_URL}
|
|
354
|
+
script.src = '{MAIDR_TS_CDN_URL}';
|
|
288
355
|
script.addEventListener('load', function() {{
|
|
289
356
|
window.main();
|
|
290
357
|
}});
|
|
@@ -299,7 +366,7 @@ class Maidr:
|
|
|
299
366
|
base_html = tags.div(
|
|
300
367
|
tags.link(
|
|
301
368
|
rel="stylesheet",
|
|
302
|
-
href="https://cdn.jsdelivr.net/npm/maidr/dist/maidr_style.css",
|
|
369
|
+
href=f"https://cdn.jsdelivr.net/npm/maidr@{latest_version}/dist/maidr_style.css",
|
|
303
370
|
),
|
|
304
371
|
tags.script(script, type="text/javascript"),
|
|
305
372
|
tags.div(plot),
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import matplotlib.dates as mdates
|
|
4
|
+
import numpy as np
|
|
5
|
+
from matplotlib.axes import Axes
|
|
6
|
+
from matplotlib.patches import Rectangle
|
|
7
|
+
|
|
8
|
+
from maidr.core.enum import PlotType
|
|
9
|
+
from maidr.core.plot import MaidrPlot
|
|
10
|
+
from maidr.core.enum.maidr_key import MaidrKey
|
|
11
|
+
from maidr.util.mplfinance_utils import MplfinanceDataExtractor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CandlestickPlot(MaidrPlot):
|
|
15
|
+
"""
|
|
16
|
+
Specialized candlestick plot class for mplfinance OHLC data.
|
|
17
|
+
|
|
18
|
+
This class handles the extraction and processing of candlestick data from mplfinance
|
|
19
|
+
plots, including proper date conversion and data validation.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, axes: list[Axes], **kwargs) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Initialize the CandlestickPlot.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
axes : list[Axes]
|
|
29
|
+
A list of Matplotlib Axes objects. Expected to contain at least
|
|
30
|
+
one Axes for OHLC data, and optionally a second for volume data.
|
|
31
|
+
**kwargs : dict
|
|
32
|
+
Additional keyword arguments.
|
|
33
|
+
"""
|
|
34
|
+
self.axes = axes
|
|
35
|
+
# Ensure there's at least one axis for the superclass init
|
|
36
|
+
if not axes:
|
|
37
|
+
raise ValueError("Axes list cannot be empty.")
|
|
38
|
+
super().__init__(axes[0], PlotType.CANDLESTICK)
|
|
39
|
+
|
|
40
|
+
# Store custom collections passed from mplfinance patch
|
|
41
|
+
self._maidr_wick_collection = kwargs.get("_maidr_wick_collection", None)
|
|
42
|
+
self._maidr_body_collection = kwargs.get("_maidr_body_collection", None)
|
|
43
|
+
self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
|
|
44
|
+
|
|
45
|
+
# Store the GID for proper selector generation
|
|
46
|
+
self._maidr_gid = None
|
|
47
|
+
if self._maidr_body_collection:
|
|
48
|
+
self._maidr_gid = self._maidr_body_collection.get_gid()
|
|
49
|
+
elif self._maidr_wick_collection:
|
|
50
|
+
self._maidr_gid = self._maidr_wick_collection.get_gid()
|
|
51
|
+
|
|
52
|
+
def _extract_plot_data(self) -> list[dict]:
|
|
53
|
+
"""Extract candlestick data from the plot."""
|
|
54
|
+
|
|
55
|
+
# Get the custom collections from kwargs
|
|
56
|
+
body_collection = self._maidr_body_collection
|
|
57
|
+
wick_collection = self._maidr_wick_collection
|
|
58
|
+
|
|
59
|
+
if body_collection and wick_collection:
|
|
60
|
+
# Store the GID from the body collection for highlighting
|
|
61
|
+
self._maidr_gid = body_collection.get_gid()
|
|
62
|
+
|
|
63
|
+
# Use the original collections for highlighting
|
|
64
|
+
self._elements = [body_collection, wick_collection]
|
|
65
|
+
|
|
66
|
+
# Use the utility class to extract data
|
|
67
|
+
return MplfinanceDataExtractor.extract_candlestick_data(
|
|
68
|
+
body_collection, wick_collection, self._maidr_date_nums
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Fallback to original detection method
|
|
72
|
+
if not self.axes:
|
|
73
|
+
return []
|
|
74
|
+
|
|
75
|
+
ax_ohlc = self.axes[0]
|
|
76
|
+
candles = []
|
|
77
|
+
|
|
78
|
+
# Look for Rectangle patches (original_flavor candlestick)
|
|
79
|
+
body_rectangles = []
|
|
80
|
+
for patch in ax_ohlc.patches:
|
|
81
|
+
if isinstance(patch, Rectangle):
|
|
82
|
+
body_rectangles.append(patch)
|
|
83
|
+
|
|
84
|
+
if body_rectangles:
|
|
85
|
+
# Set elements for highlighting
|
|
86
|
+
self._elements = body_rectangles
|
|
87
|
+
|
|
88
|
+
# Generate a GID for highlighting if none exists
|
|
89
|
+
if not self._maidr_gid:
|
|
90
|
+
import uuid
|
|
91
|
+
|
|
92
|
+
self._maidr_gid = f"maidr-{uuid.uuid4()}"
|
|
93
|
+
# Set GID on all rectangles
|
|
94
|
+
for rect in body_rectangles:
|
|
95
|
+
rect.set_gid(self._maidr_gid)
|
|
96
|
+
|
|
97
|
+
# Use the utility class to extract data
|
|
98
|
+
return MplfinanceDataExtractor.extract_rectangle_candlestick_data(
|
|
99
|
+
body_rectangles, self._maidr_date_nums
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
def _extract_axes_data(self) -> dict:
|
|
105
|
+
"""
|
|
106
|
+
Extract the plot's axes data including labels.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
dict
|
|
111
|
+
Dictionary containing x and y axis labels.
|
|
112
|
+
"""
|
|
113
|
+
x_labels = self.ax.get_xlabel()
|
|
114
|
+
if not x_labels:
|
|
115
|
+
x_labels = self.extract_shared_xlabel(self.ax)
|
|
116
|
+
if not x_labels:
|
|
117
|
+
x_labels = "X"
|
|
118
|
+
return {MaidrKey.X: x_labels, MaidrKey.Y: self.ax.get_ylabel()}
|
|
119
|
+
|
|
120
|
+
def _get_selector(self) -> str:
|
|
121
|
+
"""Return the CSS selector for highlighting candlestick elements in the SVG output."""
|
|
122
|
+
# Use the stored GID if available, otherwise fall back to generic selector
|
|
123
|
+
if self._maidr_gid:
|
|
124
|
+
# Use the full GID as the id attribute (since that's what's in the SVG)
|
|
125
|
+
selector = (
|
|
126
|
+
f"g[id='{self._maidr_gid}'] > path, g[id='{self._maidr_gid}'] > rect"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
selector = "g[maidr='true'] > path, g[maidr='true'] > rect"
|
|
130
|
+
return selector
|
|
131
|
+
|
|
132
|
+
def render(self) -> dict:
|
|
133
|
+
"""Initialize the MAIDR schema dictionary with basic plot information."""
|
|
134
|
+
title = "Candlestick Chart"
|
|
135
|
+
|
|
136
|
+
maidr_schema = {
|
|
137
|
+
MaidrKey.TYPE: self.type,
|
|
138
|
+
MaidrKey.TITLE: title,
|
|
139
|
+
MaidrKey.AXES: self._extract_axes_data(),
|
|
140
|
+
MaidrKey.DATA: self._extract_plot_data(),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Include selector only if the plot supports highlighting.
|
|
144
|
+
if self._support_highlighting:
|
|
145
|
+
maidr_schema[MaidrKey.SELECTOR] = self._get_selector()
|
|
146
|
+
|
|
147
|
+
return maidr_schema
|
|
@@ -42,8 +42,13 @@ class MaidrPlot(ABC):
|
|
|
42
42
|
self._support_highlighting = True
|
|
43
43
|
self._elements = []
|
|
44
44
|
ss = self.ax.get_subplotspec()
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
# Handle cases where subplotspec is None (dynamically created axes)
|
|
46
|
+
if ss is not None:
|
|
47
|
+
self.row_index = ss.rowspan.start
|
|
48
|
+
self.col_index = ss.colspan.start
|
|
49
|
+
else:
|
|
50
|
+
self.row_index = 0
|
|
51
|
+
self.col_index = 0
|
|
47
52
|
|
|
48
53
|
# MAIDR data
|
|
49
54
|
self.type = plot_type
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from matplotlib.axes import Axes
|
|
4
|
-
|
|
5
4
|
from maidr.core.enum import PlotType
|
|
6
5
|
from maidr.core.plot.barplot import BarPlot
|
|
7
6
|
from maidr.core.plot.boxplot import BoxPlot
|
|
@@ -13,6 +12,9 @@ from maidr.core.plot.lineplot import MultiLinePlot
|
|
|
13
12
|
from maidr.core.plot.maidr_plot import MaidrPlot
|
|
14
13
|
from maidr.core.plot.scatterplot import ScatterPlot
|
|
15
14
|
from maidr.core.plot.regplot import SmoothPlot
|
|
15
|
+
from maidr.core.plot.mplfinance_barplot import MplfinanceBarPlot
|
|
16
|
+
from maidr.core.plot.mplfinance_lineplot import MplfinanceLinePlot
|
|
17
|
+
from maidr.util.plot_detection import PlotDetectionUtils
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class MaidrPlotFactory:
|
|
@@ -38,17 +40,13 @@ class MaidrPlotFactory:
|
|
|
38
40
|
single_ax = ax
|
|
39
41
|
|
|
40
42
|
if plot_type == PlotType.CANDLESTICK:
|
|
41
|
-
|
|
42
|
-
# If ax is a list of lists, flatten it
|
|
43
|
-
if ax and isinstance(ax[0], list):
|
|
44
|
-
axes = ax[0] # Take the first inner list
|
|
45
|
-
else:
|
|
46
|
-
axes = ax # Use the list as-is
|
|
47
|
-
else:
|
|
48
|
-
axes = [ax] # Wrap single axes in list
|
|
43
|
+
axes = PlotDetectionUtils.get_candlestick_axes(ax)
|
|
49
44
|
return CandlestickPlot(axes, **kwargs)
|
|
50
45
|
elif PlotType.BAR == plot_type or PlotType.COUNT == plot_type:
|
|
51
|
-
|
|
46
|
+
if PlotDetectionUtils.is_mplfinance_bar_plot(**kwargs):
|
|
47
|
+
return MplfinanceBarPlot(single_ax, **kwargs)
|
|
48
|
+
else:
|
|
49
|
+
return BarPlot(single_ax)
|
|
52
50
|
elif PlotType.BOX == plot_type:
|
|
53
51
|
return BoxPlot(single_ax, **kwargs)
|
|
54
52
|
elif PlotType.HEAT == plot_type:
|
|
@@ -56,7 +54,10 @@ class MaidrPlotFactory:
|
|
|
56
54
|
elif PlotType.HIST == plot_type:
|
|
57
55
|
return HistPlot(single_ax)
|
|
58
56
|
elif PlotType.LINE == plot_type:
|
|
59
|
-
|
|
57
|
+
if PlotDetectionUtils.is_mplfinance_line_plot(single_ax, **kwargs):
|
|
58
|
+
return MplfinanceLinePlot(single_ax, **kwargs)
|
|
59
|
+
else:
|
|
60
|
+
return MultiLinePlot(single_ax, **kwargs)
|
|
60
61
|
elif PlotType.SCATTER == plot_type:
|
|
61
62
|
return ScatterPlot(single_ax)
|
|
62
63
|
elif PlotType.DODGED == plot_type or PlotType.STACKED == plot_type:
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from matplotlib.axes import Axes
|
|
4
|
+
from matplotlib.patches import Rectangle
|
|
5
|
+
|
|
6
|
+
from maidr.core.enum import PlotType
|
|
7
|
+
from maidr.core.plot import MaidrPlot
|
|
8
|
+
from maidr.exception import ExtractionError
|
|
9
|
+
from maidr.util.mixin import (
|
|
10
|
+
ContainerExtractorMixin,
|
|
11
|
+
DictMergerMixin,
|
|
12
|
+
LevelExtractorMixin,
|
|
13
|
+
)
|
|
14
|
+
from maidr.util.mplfinance_utils import MplfinanceDataExtractor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MplfinanceBarPlot(
|
|
18
|
+
MaidrPlot, ContainerExtractorMixin, LevelExtractorMixin, DictMergerMixin
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Specialized bar plot class for mplfinance volume bars.
|
|
22
|
+
|
|
23
|
+
This class handles the extraction and processing of volume data from mplfinance
|
|
24
|
+
plots, including proper date conversion and data validation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, ax: Axes, **kwargs) -> None:
|
|
28
|
+
super().__init__(ax, PlotType.BAR)
|
|
29
|
+
# Store custom patches passed from mplfinance patch
|
|
30
|
+
self._custom_patches = kwargs.get("_maidr_patches", None)
|
|
31
|
+
# Store date numbers for volume bars (from mplfinance)
|
|
32
|
+
self._maidr_date_nums = kwargs.get("_maidr_date_nums", None)
|
|
33
|
+
# Store custom title
|
|
34
|
+
|
|
35
|
+
def set_title(self, title: str) -> None:
|
|
36
|
+
"""Set a custom title for this volume bar plot."""
|
|
37
|
+
self._title = title
|
|
38
|
+
|
|
39
|
+
def _extract_plot_data(self) -> list:
|
|
40
|
+
"""Extract data from mplfinance volume patches."""
|
|
41
|
+
if self._custom_patches:
|
|
42
|
+
return self._extract_volume_patches_data(self._custom_patches)
|
|
43
|
+
|
|
44
|
+
# Fallback to original bar plot logic if no custom patches
|
|
45
|
+
plot = self.extract_container(
|
|
46
|
+
self.ax, ContainerExtractorMixin, include_all=True
|
|
47
|
+
)
|
|
48
|
+
data = self._extract_bar_container_data(plot)
|
|
49
|
+
levels = self.extract_level(self.ax)
|
|
50
|
+
formatted_data = []
|
|
51
|
+
combined_data = list(
|
|
52
|
+
zip(levels, data) if plot[0].orientation == "vertical" else zip(data, levels) # type: ignore
|
|
53
|
+
)
|
|
54
|
+
if combined_data: # type: ignore
|
|
55
|
+
for x, y in combined_data: # type: ignore
|
|
56
|
+
formatted_data.append({"x": x, "y": y})
|
|
57
|
+
return formatted_data
|
|
58
|
+
if len(formatted_data) == 0:
|
|
59
|
+
raise ExtractionError(self.type, plot)
|
|
60
|
+
if data is None:
|
|
61
|
+
raise ExtractionError(self.type, plot)
|
|
62
|
+
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
def _extract_volume_patches_data(self, volume_patches: list[Rectangle]) -> list:
|
|
66
|
+
"""
|
|
67
|
+
Extract data from volume Rectangle patches (used by mplfinance volume bars).
|
|
68
|
+
"""
|
|
69
|
+
if not volume_patches:
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
# Sort patches by x-coordinate to maintain order
|
|
73
|
+
sorted_patches = sorted(volume_patches, key=lambda p: p.get_x())
|
|
74
|
+
|
|
75
|
+
# Set elements for highlighting (use the patches directly)
|
|
76
|
+
self._elements = sorted_patches
|
|
77
|
+
|
|
78
|
+
# Use the utility class to extract data
|
|
79
|
+
return MplfinanceDataExtractor.extract_volume_data(
|
|
80
|
+
sorted_patches, self._maidr_date_nums
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _extract_bar_container_data(self, plot: list | None) -> list | None:
|
|
84
|
+
"""Fallback method for regular bar containers."""
|
|
85
|
+
if plot is None:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
# Since v0.13, Seaborn has transitioned from using `list[Patch]` to
|
|
89
|
+
# `list[BarContainers] for plotting bar plots.
|
|
90
|
+
# So, extract data correspondingly based on the level.
|
|
91
|
+
# Flatten all the `list[BarContainer]` to `list[Patch]`.
|
|
92
|
+
patches = [patch for container in plot for patch in container.patches]
|
|
93
|
+
level = self.extract_level(self.ax)
|
|
94
|
+
if level is None or len(level) == 0: # type: ignore
|
|
95
|
+
level = ["" for _ in range(len(patches))] # type: ignore
|
|
96
|
+
|
|
97
|
+
if len(patches) != len(level):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
self._elements.extend(patches)
|
|
101
|
+
|
|
102
|
+
return [float(patch.get_height()) for patch in patches]
|
|
103
|
+
|
|
104
|
+
def _extract_axes_data(self) -> dict:
|
|
105
|
+
"""Extract axes data with mplfinance-specific cleaning."""
|
|
106
|
+
ax_data = super()._extract_axes_data()
|
|
107
|
+
|
|
108
|
+
# For mplfinance volume plots, clean up the y-axis label
|
|
109
|
+
if self._custom_patches:
|
|
110
|
+
y_label = ax_data.get("y")
|
|
111
|
+
if y_label:
|
|
112
|
+
ax_data["y"] = MplfinanceDataExtractor.clean_axis_label(y_label)
|
|
113
|
+
|
|
114
|
+
return ax_data
|
|
115
|
+
|
|
116
|
+
def _get_selector(self) -> str:
|
|
117
|
+
"""Return the CSS selector for highlighting bar elements in the SVG output."""
|
|
118
|
+
# Use the standard working selector that gets replaced with UUID by Maidr class
|
|
119
|
+
# This works for both original bar plots and mplfinance volume bars
|
|
120
|
+
return "g[maidr='true'] > path"
|
|
121
|
+
|
|
122
|
+
def render(self) -> dict:
|
|
123
|
+
"""Initialize the MAIDR schema dictionary with basic plot information."""
|
|
124
|
+
from maidr.core.enum.maidr_key import MaidrKey
|
|
125
|
+
|
|
126
|
+
title = "Volume Bar Plot"
|
|
127
|
+
|
|
128
|
+
maidr_schema = {
|
|
129
|
+
MaidrKey.TYPE: self.type,
|
|
130
|
+
MaidrKey.TITLE: title,
|
|
131
|
+
MaidrKey.AXES: self._extract_axes_data(),
|
|
132
|
+
MaidrKey.DATA: self._extract_plot_data(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Include selector only if the plot supports highlighting.
|
|
136
|
+
if self._support_highlighting:
|
|
137
|
+
maidr_schema[MaidrKey.SELECTOR] = self._get_selector()
|
|
138
|
+
|
|
139
|
+
return maidr_schema
|