scitex 2.14.0__py3-none-any.whl → 2.15.1__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.
Files changed (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
scitex/plt/__init__.py CHANGED
@@ -1,46 +1,200 @@
1
1
  #!/usr/bin/env python3
2
- # Timestamp: "2025-12-02 12:30:00 (ywatanabe)"
2
+ # Timestamp: "2026-01-19 (ywatanabe)"
3
3
  # File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/__init__.py
4
4
  # ----------------------------------------
5
- import os
6
-
7
- __FILE__ = __file__
8
- __DIR__ = os.path.dirname(__FILE__)
9
- # ----------------------------------------
10
5
  """
11
- SciTeX plt module - Publication-quality plotting.
6
+ SciTeX plt module - Publication-quality plotting via figrecipe.
12
7
 
8
+ This module provides a thin wrapper around figrecipe with scitex branding.
13
9
  Simply importing this module automatically configures matplotlib with
14
- SciTeX publication defaults from SCITEX_STYLE.yaml:
10
+ SciTeX publication defaults.
11
+
12
+ Usage
13
+ -----
14
+ >>> import scitex.plt as plt
15
+ >>> fig, ax = plt.subplots()
16
+ >>> ax.plot([1, 2, 3], [1, 4, 9])
17
+ >>> plt.save(fig, "figure.png")
18
+
19
+ Style Management
20
+ ----------------
21
+ >>> plt.load_style("SCITEX") # Load publication style
22
+ >>> plt.STYLE # Access current style configuration
23
+ >>> plt.list_presets() # Show available presets
24
+
25
+ The module delegates to figrecipe for:
26
+ - Recording and reproducing figures
27
+ - Style management (mm-based layouts)
28
+ - Figure composition
29
+ - Graph visualization
30
+
31
+ SciTeX-specific features (kept locally):
32
+ - AxisWrapper/FigWrapper compatibility
33
+ - Color palettes (scitex.plt.color)
34
+ - Gallery utilities (scitex.plt.gallery)
35
+ """
15
36
 
16
- import scitex.plt as splt
17
- fig, ax = splt.subplots()
18
- ax.plot([1, 2, 3], [1, 4, 9])
19
- fig.savefig("figure.png")
37
+ import os
20
38
 
21
- No need to call scitex.session() or configure_mpl() - it's automatic.
39
+ # ============================================================================
40
+ # Set branding environment variables BEFORE importing figrecipe
41
+ # This enables automatic docstring replacement: figrecipe -> scitex.plt, fr -> plt
42
+ # ============================================================================
43
+ os.environ.setdefault("FIGRECIPE_BRAND", "scitex.plt")
44
+ os.environ.setdefault("FIGRECIPE_ALIAS", "plt")
22
45
 
23
- Style values can be customized by:
24
- 1. Editing SCITEX_STYLE.yaml
25
- 2. Setting environment variables (SCITEX_PLT_FONTS_AXIS_LABEL_PT=8)
26
- 3. Passing parameters directly to subplots()
27
- """
46
+ # ============================================================================
47
+ # Now import figrecipe (branding will be applied)
48
+ # ============================================================================
49
+ try:
50
+ import figrecipe as _fr
51
+
52
+ _FIGRECIPE_AVAILABLE = True
53
+ except ImportError:
54
+ _FIGRECIPE_AVAILABLE = False
55
+ _fr = None
28
56
 
57
+ # Standard library and matplotlib imports
29
58
  import matplotlib as mpl
30
59
  import matplotlib.font_manager as fm
31
- import matplotlib.pyplot as plt
60
+ import matplotlib.pyplot as _plt
32
61
 
33
62
  from scitex import logging as _logging
34
63
 
35
64
  _logger = _logging.getLogger(__name__)
36
65
 
37
- # =============================================================================
38
- # Auto-configure matplotlib with SciTeX style on import
39
- # =============================================================================
66
+ __FILE__ = __file__
67
+ __DIR__ = os.path.dirname(__FILE__)
68
+
69
+ # ============================================================================
70
+ # Re-export figrecipe public API with scitex branding
71
+ # ============================================================================
72
+ if _FIGRECIPE_AVAILABLE:
73
+ # Core functions
74
+ from figrecipe import (
75
+ STYLE,
76
+ align_panels,
77
+ apply_style,
78
+ compose,
79
+ crop,
80
+ distribute_panels,
81
+ edit,
82
+ enable_svg,
83
+ extract_data,
84
+ get_graph_preset,
85
+ info,
86
+ list_graph_presets,
87
+ list_presets,
88
+ load_style,
89
+ register_graph_preset,
90
+ reproduce,
91
+ save,
92
+ smart_align,
93
+ sns,
94
+ subplots,
95
+ unload_style,
96
+ validate,
97
+ )
98
+ from figrecipe import (
99
+ __version__ as _figrecipe_version,
100
+ )
101
+
102
+ # Also export load as alias for reproduce
103
+ load = reproduce
104
+ else:
105
+ # Provide stub versions when figrecipe is not available
106
+ _figrecipe_version = "0.0.0"
107
+
108
+ def _not_available(*args, **kwargs):
109
+ raise ImportError(
110
+ "figrecipe is required for this feature. Install with: pip install figrecipe"
111
+ )
112
+
113
+ STYLE = None
114
+ load_style = _not_available
115
+ unload_style = _not_available
116
+ list_presets = _not_available
117
+ apply_style = _not_available
118
+ subplots = _not_available
119
+ save = _not_available
120
+ reproduce = _not_available
121
+ load = _not_available
122
+ crop = _not_available
123
+ validate = _not_available
124
+ extract_data = _not_available
125
+ info = _not_available
126
+ edit = _not_available
127
+ compose = _not_available
128
+ align_panels = _not_available
129
+ distribute_panels = _not_available
130
+ smart_align = _not_available
131
+ sns = None
132
+ enable_svg = _not_available
133
+ get_graph_preset = _not_available
134
+ list_graph_presets = _not_available
135
+ register_graph_preset = _not_available
136
+
137
+ # ============================================================================
138
+ # Local scitex submodules (kept for compatibility)
139
+ # ============================================================================
140
+ try:
141
+ from ._tpl import termplot
142
+ except ImportError:
143
+ termplot = None
144
+
145
+ # Backward compatibility: expose styles submodule (deprecated, use figrecipe)
146
+ from . import ax, color, gallery, styles, utils
147
+
148
+ # Import draw_graph from figrecipe integration (handles AxisWrapper)
149
+ from ._figrecipe_integration import draw_graph
150
+ from .styles import presets
151
+
152
+ # ============================================================================
153
+ # Auto-configure matplotlib with SciTeX defaults on import
154
+ # ============================================================================
155
+
156
+
157
+ def _register_arial_fonts():
158
+ """Register Arial fonts if available."""
159
+ try:
160
+ fm.findfont("Arial", fallback_to_default=False)
161
+ return True
162
+ except Exception:
163
+ # Search for Arial font files and register them
164
+ arial_paths = [
165
+ f
166
+ for f in fm.findSystemFonts()
167
+ if os.path.basename(f).lower().startswith("arial")
168
+ ]
169
+
170
+ if arial_paths:
171
+ for path in arial_paths:
172
+ try:
173
+ fm.fontManager.addfont(path)
174
+ except Exception:
175
+ pass
176
+
177
+ # Verify Arial is now available
178
+ try:
179
+ fm.findfont("Arial", fallback_to_default=False)
180
+ return True
181
+ except Exception:
182
+ pass
183
+ return False
40
184
 
41
185
 
42
186
  def _auto_configure_mpl():
43
187
  """Apply SciTeX style configuration automatically on import."""
188
+ # Try to use figrecipe's style system first
189
+ if _FIGRECIPE_AVAILABLE:
190
+ try:
191
+ # Load SCITEX style preset from figrecipe
192
+ load_style("SCITEX")
193
+ return
194
+ except Exception:
195
+ pass
196
+
197
+ # Fallback: use local style loader
44
198
  from .styles import resolve_style_value
45
199
 
46
200
  # mm to pt conversion factor
@@ -111,32 +265,8 @@ def _auto_configure_mpl():
111
265
  mpl.rcParams.update(mpl_config)
112
266
 
113
267
 
114
- # Register Arial fonts eagerly (before style configuration)
115
- _arial_enabled = False
116
- try:
117
- fm.findfont("Arial", fallback_to_default=False)
118
- _arial_enabled = True
119
- except Exception:
120
- # Search for Arial font files and register them
121
- arial_paths = [
122
- f
123
- for f in fm.findSystemFonts()
124
- if os.path.basename(f).lower().startswith("arial")
125
- ]
126
-
127
- if arial_paths:
128
- for path in arial_paths:
129
- try:
130
- fm.fontManager.addfont(path)
131
- except Exception:
132
- pass
133
-
134
- # Verify Arial is now available
135
- try:
136
- fm.findfont("Arial", fallback_to_default=False)
137
- _arial_enabled = True
138
- except Exception:
139
- pass
268
+ # Register Arial fonts eagerly
269
+ _arial_enabled = _register_arial_fonts()
140
270
 
141
271
  # Configure font family
142
272
  if _arial_enabled:
@@ -165,458 +295,35 @@ _auto_configure_mpl()
165
295
 
166
296
  # Set up color cycle from scitex colors
167
297
  try:
168
- from . import color as _color_module
169
-
170
298
  _rgba_norm_cycle = {
171
- k: tuple(_color_module.update_alpha(v, 1.0))
172
- for k, v in _color_module.PARAMS.get("RGBA_NORM_FOR_CYCLE", {}).items()
299
+ k: tuple(color.update_alpha(v, 1.0))
300
+ for k, v in color.PARAMS.get("RGBA_NORM_FOR_CYCLE", {}).items()
173
301
  }
174
302
  if _rgba_norm_cycle:
175
- mpl.rcParams["axes.prop_cycle"] = plt.cycler(
303
+ mpl.rcParams["axes.prop_cycle"] = _plt.cycler(
176
304
  color=list(_rgba_norm_cycle.values())
177
305
  )
178
306
  except Exception:
179
307
  pass # Use matplotlib default colors if color module fails
180
308
 
181
- try:
182
- from ._tpl import termplot
183
- except ImportError:
184
- termplot = None
185
- from . import ax, color, gallery, styles, utils
186
-
187
- # Figrecipe integration (graph visualization, editor)
188
- from ._figrecipe_integration import draw_graph, edit
189
- from .styles import presets
190
-
191
- # Lazy import for subplots to avoid circular dependencies
192
- # Note: Use names that don't conflict with submodule names like _subplots
193
- _subplots_func_cached = None
194
- _figure_func_cached = None
195
- _crop_func_cached = None
196
-
197
309
 
198
- def subplots(*args, **kwargs):
199
- """Lazy-loaded subplots function."""
200
- global _subplots_func_cached
201
- if _subplots_func_cached is None:
202
- from ._subplots._SubplotsWrapper import subplots as _subplots_impl
203
-
204
- _subplots_func_cached = _subplots_impl
205
- return _subplots_func_cached(*args, **kwargs)
310
+ # ============================================================================
311
+ # SciTeX-specific wrapper functions (for AxisWrapper/FigWrapper compatibility)
312
+ # ============================================================================
206
313
 
207
314
 
208
315
  def figure(*args, **kwargs):
209
- """Lazy-loaded figure function that returns a FigWrapper."""
210
- global _figure_func_cached
211
- if _figure_func_cached is None:
212
- import matplotlib.pyplot as plt
213
-
214
- from ._subplots._FigWrapper import FigWrapper
215
-
216
- def _figure_impl(*args, **kwargs):
217
- fig_mpl = plt.figure(*args, **kwargs)
218
- return FigWrapper(fig_mpl)
219
-
220
- _figure_func_cached = _figure_impl
221
- return _figure_func_cached(*args, **kwargs)
222
-
223
-
224
- def crop(input_path, output_path=None, margin=12, overwrite=False, verbose=False):
225
- """
226
- Auto-crop a figure to its content area.
227
-
228
- This function automatically detects the content area of a saved figure
229
- and crops it, removing excess whitespace. Designed for publication figures
230
- created with large margins.
231
-
232
- Parameters
233
- ----------
234
- input_path : str
235
- Path to the input image
236
- output_path : str, optional
237
- Path to save cropped image. If None and overwrite=True, overwrites input.
238
- If None and overwrite=False, adds '_cropped' suffix.
239
- margin : int, optional
240
- Margin in pixels around content (default: 12, ~1mm at 300 DPI)
241
- overwrite : bool, optional
242
- Overwrite input file (default: False)
243
- verbose : bool, optional
244
- Print detailed information (default: False)
245
-
246
- Returns
247
- -------
248
- str
249
- Path to the saved cropped image
250
-
251
- Examples
252
- --------
253
- >>> fig, ax = stx.plt.subplots(**stx.plt.presets.SCITEX_STYLE)
254
- >>> ax.plot([1, 2, 3], [1, 2, 3])
255
- >>> stx.io.save(fig, "figure.png")
256
- >>> stx.plt.crop("figure.png", "figure_cropped.png") # 1mm margin
257
- """
258
- global _crop_func_cached
259
- if _crop_func_cached is None:
260
- from .utils._crop import crop as _crop_impl
261
-
262
- _crop_func_cached = _crop_impl
263
- return _crop_func_cached(input_path, output_path, margin, overwrite, verbose)
316
+ """Create a figure that returns a FigWrapper.
264
317
 
318
+ This is the scitex-specific figure function that creates FigWrapper
319
+ objects for compatibility with scitex.plt.ax utilities.
265
320
 
266
- def load(path, apply_manual=True):
321
+ For figrecipe-style recording figures, use subplots() instead.
267
322
  """
268
- Load a figure from saved JSON + CSV files.
323
+ from ._subplots._FigWrapper import FigWrapper
269
324
 
270
- Parameters
271
- ----------
272
- path : str or Path
273
- Path to JSON file, PNG file, or CSV file.
274
- Will auto-detect sibling files in same directory or organized subdirectories.
275
- apply_manual : bool, optional
276
- If True, apply .manual.json overrides if exists (default: True)
277
-
278
- Returns
279
- -------
280
- tuple
281
- (fig, axes) where fig is FigWrapper and axes is AxisWrapper or array
282
-
283
- Raises
284
- ------
285
- FileNotFoundError
286
- If required JSON file is not found
287
- ValueError
288
- If manual.json hash doesn't match (stale manual edits)
289
-
290
- Examples
291
- --------
292
- >>> # Load from JSON (sibling pattern)
293
- >>> fig, axes = stx.plt.load("output/figure.json")
294
-
295
- >>> # Load from PNG (finds sibling JSON + CSV)
296
- >>> fig, axes = stx.plt.load("output/figure.png")
297
-
298
- >>> # Load from organized directory pattern
299
- >>> fig, axes = stx.plt.load("output/json/figure.json")
300
-
301
- >>> # Skip manual overrides
302
- >>> fig, axes = stx.plt.load("figure.json", apply_manual=False)
303
-
304
- Notes
305
- -----
306
- Supports two directory patterns:
307
-
308
- Pattern 1 (flat/sibling):
309
- output/figure.png
310
- output/figure.json
311
- output/figure.csv
312
-
313
- Pattern 2 (organized):
314
- output/png/figure.png
315
- output/json/figure.json
316
- output/csv/figure.csv
317
-
318
- Manual overrides (.manual.json) are applied if:
319
- - apply_manual=True
320
- - figure.manual.json exists alongside figure.json
321
- - Hash validation passes (warns if stale)
322
- """
323
- import hashlib
324
- from pathlib import Path
325
-
326
- import scitex as stx
327
-
328
- path = Path(path)
329
-
330
- # Resolve JSON path from any input (png, csv, or json)
331
- json_path, csv_path = _resolve_figure_paths(path)
332
-
333
- if not json_path.exists():
334
- raise FileNotFoundError(f"JSON file not found: {json_path}")
335
-
336
- # Load JSON metadata
337
- metadata = stx.io.load(json_path)
338
-
339
- # Load CSV data if exists
340
- csv_data = None
341
- if csv_path and csv_path.exists():
342
- csv_data = stx.io.load(csv_path)
343
-
344
- # Check for manual overrides
345
- manual_path = json_path.with_suffix(".manual.json")
346
- manual_overrides = None
347
- if apply_manual and manual_path.exists():
348
- manual_data = stx.io.load(manual_path)
349
-
350
- # Validate hash
351
- if "base_hash" in manual_data:
352
- current_hash = _compute_file_hash(json_path)
353
- if manual_data["base_hash"] != current_hash:
354
- _logger.warning(
355
- f"Manual overrides may be stale: base data changed since manual edits.\n"
356
- f" Expected hash: {manual_data['base_hash'][:16]}...\n"
357
- f" Current hash: {current_hash[:16]}...\n"
358
- f" Review: {manual_path}"
359
- )
360
- manual_overrides = manual_data.get("overrides", {})
361
-
362
- # Reconstruct figure
363
- fig, axes = _reconstruct_figure(metadata, csv_data, manual_overrides)
364
-
365
- return fig, axes
366
-
367
-
368
- def _resolve_figure_paths(path):
369
- """
370
- Resolve JSON and CSV paths from any input file path.
371
-
372
- Supports both flat (sibling) and organized (subdirectory) patterns.
373
- """
374
- from pathlib import Path
375
-
376
- path = Path(path)
377
- stem = path.stem
378
- suffix = path.suffix.lower()
379
- parent = path.parent
380
-
381
- # Determine base name (remove .manual if present)
382
- if stem.endswith(".manual"):
383
- stem = stem[:-7]
384
-
385
- json_path = None
386
- csv_path = None
387
-
388
- if suffix == ".json":
389
- json_path = path
390
- # Try sibling CSV first
391
- csv_sibling = parent / f"{stem}.csv"
392
- if csv_sibling.exists():
393
- csv_path = csv_sibling
394
- # Try organized pattern (../csv/)
395
- elif parent.name == "json":
396
- csv_organized = parent.parent / "csv" / f"{stem}.csv"
397
- if csv_organized.exists():
398
- csv_path = csv_organized
399
-
400
- elif suffix in (".png", ".jpg", ".jpeg", ".pdf", ".svg"):
401
- # Look for sibling JSON
402
- json_sibling = parent / f"{stem}.json"
403
- csv_sibling = parent / f"{stem}.csv"
404
-
405
- if json_sibling.exists():
406
- json_path = json_sibling
407
- if csv_sibling.exists():
408
- csv_path = csv_sibling
409
- # Try organized pattern (parent has png/, look for json/)
410
- elif parent.name in ("png", "jpg", "jpeg", "pdf", "svg"):
411
- json_organized = parent.parent / "json" / f"{stem}.json"
412
- csv_organized = parent.parent / "csv" / f"{stem}.csv"
413
- if json_organized.exists():
414
- json_path = json_organized
415
- if csv_organized.exists():
416
- csv_path = csv_organized
417
-
418
- elif suffix == ".csv":
419
- csv_path = path
420
- # Try sibling JSON
421
- json_sibling = parent / f"{stem}.json"
422
- if json_sibling.exists():
423
- json_path = json_sibling
424
- # Try organized pattern (../json/)
425
- elif parent.name == "csv":
426
- json_organized = parent.parent / "json" / f"{stem}.json"
427
- if json_organized.exists():
428
- json_path = json_organized
429
-
430
- # Fallback: assume it's the JSON path
431
- if json_path is None:
432
- json_path = path if suffix == ".json" else path.with_suffix(".json")
433
-
434
- return json_path, csv_path
435
-
436
-
437
- def _compute_file_hash(path):
438
- """Compute SHA256 hash of a file."""
439
- import hashlib
440
- from pathlib import Path
441
-
442
- path = Path(path)
443
- sha256 = hashlib.sha256()
444
- with open(path, "rb") as f:
445
- for chunk in iter(lambda: f.read(8192), b""):
446
- sha256.update(chunk)
447
- return f"sha256:{sha256.hexdigest()}"
448
-
449
-
450
- def _reconstruct_figure(metadata, csv_data, manual_overrides=None):
451
- """
452
- Reconstruct figure from metadata and CSV data.
453
-
454
- Parameters
455
- ----------
456
- metadata : dict
457
- JSON metadata from stx.io.save()
458
- csv_data : DataFrame or None
459
- CSV data with plot values
460
- manual_overrides : dict or None
461
- Manual style/annotation overrides
462
-
463
- Returns
464
- -------
465
- tuple
466
- (fig, axes)
467
- """
468
- import numpy as np
469
-
470
- # Extract dimensions from metadata
471
- dims = metadata.get("dimensions", {})
472
- fig_size_mm = dims.get("figure_size_mm", [80, 68])
473
- dpi = dims.get("dpi", 300)
474
-
475
- # Get style from metadata
476
- scitex_meta = metadata.get("scitex", {})
477
- style_mm = scitex_meta.get("style_mm", {})
478
-
479
- # Create figure with same dimensions
480
- fig, axes = subplots(
481
- axes_width_mm=style_mm.get(
482
- "axes_width_mm", dims.get("axes_size_mm", [40, 28])[0]
483
- ),
484
- axes_height_mm=style_mm.get(
485
- "axes_height_mm", dims.get("axes_size_mm", [40, 28])[1]
486
- ),
487
- dpi=dpi,
488
- )
489
-
490
- # Handle single vs multiple axes
491
- ax = axes if not hasattr(axes, "flat") else list(axes.flat)[0]
492
-
493
- # Set axis labels from metadata
494
- axes_meta = metadata.get("axes", {})
495
- x_meta = axes_meta.get("x", {})
496
- y_meta = axes_meta.get("y", {})
497
-
498
- xlabel = x_meta.get("label", "")
499
- ylabel = y_meta.get("label", "")
500
- x_unit = x_meta.get("unit", "")
501
- y_unit = y_meta.get("unit", "")
502
-
503
- if xlabel:
504
- full_xlabel = f"{xlabel} [{x_unit}]" if x_unit else xlabel
505
- ax.set_xlabel(full_xlabel)
506
- if ylabel:
507
- full_ylabel = f"{ylabel} [{y_unit}]" if y_unit else ylabel
508
- ax.set_ylabel(full_ylabel)
509
-
510
- # Reconstruct plots from CSV data
511
- if csv_data is not None and not csv_data.empty:
512
- _reconstruct_plots_from_csv(ax, csv_data, metadata)
513
-
514
- # Apply manual overrides
515
- if manual_overrides:
516
- _apply_manual_overrides(fig, axes, manual_overrides)
517
-
518
- return fig, axes
519
-
520
-
521
- def _reconstruct_plots_from_csv(ax, csv_data, metadata):
522
- """
523
- Reconstruct plot elements from CSV data.
524
-
525
- CSV columns follow pattern: ax_00_<type>_<name>
526
- """
527
- import numpy as np
528
- import pandas as pd
529
-
530
- # Group columns by plot type
531
- plot_type = metadata.get("plot_type", metadata.get("method", "line"))
532
-
533
- # Parse column names to find plot data
534
- columns = csv_data.columns.tolist()
535
-
536
- # Find x/y data columns
537
- x_cols = [c for c in columns if "_x" in c.lower() and "text" not in c.lower()]
538
- y_cols = [c for c in columns if "_y" in c.lower() and "text" not in c.lower()]
539
-
540
- if plot_type == "line" or plot_type == "plot":
541
- # For line plots, look for paired x/y or just y with index
542
- if x_cols and y_cols:
543
- for x_col, y_col in zip(x_cols, y_cols):
544
- x = csv_data[x_col].dropna().values
545
- y = csv_data[y_col].dropna().values
546
- if len(x) > 0 and len(y) > 0:
547
- ax.plot(x[: len(y)], y[: len(x)])
548
- elif y_cols:
549
- for y_col in y_cols:
550
- y = csv_data[y_col].dropna().values
551
- if len(y) > 0:
552
- ax.plot(y)
553
-
554
- elif plot_type == "scatter":
555
- if x_cols and y_cols:
556
- x = csv_data[x_cols[0]].dropna().values
557
- y = csv_data[y_cols[0]].dropna().values
558
- ax.scatter(x[: min(len(x), len(y))], y[: min(len(x), len(y))])
559
-
560
- # Add text annotations
561
- text_cols = [c for c in columns if "text" in c.lower() and "content" in c.lower()]
562
- for text_col in text_cols:
563
- # Find corresponding x, y columns
564
- prefix = text_col.rsplit("_content", 1)[0]
565
- x_col = f"{prefix}_x"
566
- y_col = f"{prefix}_y"
567
-
568
- if x_col in columns and y_col in columns:
569
- x_vals = csv_data[x_col].dropna()
570
- y_vals = csv_data[y_col].dropna()
571
- text_vals = csv_data[text_col].dropna()
572
-
573
- for i in range(min(len(x_vals), len(y_vals), len(text_vals))):
574
- ax.text(
575
- x_vals.iloc[i],
576
- y_vals.iloc[i],
577
- str(text_vals.iloc[i]),
578
- transform=ax.transAxes,
579
- fontsize=6,
580
- verticalalignment="top",
581
- horizontalalignment="right",
582
- )
583
-
584
-
585
- def _apply_manual_overrides(fig, axes, overrides):
586
- """
587
- Apply manual style/annotation overrides to figure.
588
-
589
- Parameters
590
- ----------
591
- fig : FigWrapper
592
- Figure to modify
593
- axes : AxisWrapper or array
594
- Axes to modify
595
- overrides : dict
596
- Override specifications like {"axes[0].style.linewidth": 0.5}
597
- """
598
- # Simple override application - can be extended
599
- for key, value in overrides.items():
600
- # Parse key like "axes[0].title" or "style.linewidth"
601
- parts = key.split(".")
602
-
603
- if parts[0].startswith("axes["):
604
- # Extract axis index
605
- import re
606
-
607
- match = re.match(r"axes\[(\d+)\]", parts[0])
608
- if match:
609
- idx = int(match.group(1))
610
- ax = axes if not hasattr(axes, "flat") else list(axes.flat)[idx]
611
-
612
- if len(parts) > 1:
613
- attr = parts[1]
614
- if attr == "title":
615
- ax.set_title(value)
616
- elif attr == "xlabel":
617
- ax.set_xlabel(value)
618
- elif attr == "ylabel":
619
- ax.set_ylabel(value)
325
+ fig_mpl = _plt.figure(*args, **kwargs)
326
+ return FigWrapper(fig_mpl)
620
327
 
621
328
 
622
329
  def tight_layout(**kwargs):
@@ -627,9 +334,6 @@ def tight_layout(**kwargs):
627
334
  1. UserWarning: "The figure layout has changed to tight" - informational only
628
335
  2. RuntimeError: Colorbar layout incompatibility - occurs when colorbars exist with old engine
629
336
 
630
- When a colorbar layout error occurs, the function silently continues as the layout
631
- is still functional even if the engine cannot be changed.
632
-
633
337
  Parameters
634
338
  ----------
635
339
  **kwargs
@@ -637,18 +341,14 @@ def tight_layout(**kwargs):
637
341
  """
638
342
  import warnings
639
343
 
640
- import matplotlib.pyplot as plt
641
-
642
344
  with warnings.catch_warnings():
643
345
  warnings.filterwarnings(
644
346
  "ignore", message="The figure layout has changed to tight"
645
347
  )
646
348
  try:
647
- plt.tight_layout(**kwargs)
349
+ _plt.tight_layout(**kwargs)
648
350
  except RuntimeError as e:
649
351
  # Silently handle colorbar layout engine incompatibility
650
- # This occurs when colorbars were created before tight_layout is called
651
- # The layout is still usable, so we can safely ignore this error
652
352
  if "Colorbar layout" not in str(e):
653
353
  raise
654
354
 
@@ -664,12 +364,10 @@ def colorbar(mappable=None, cax=None, ax=None, **kwargs):
664
364
  ----------
665
365
  mappable : ScalarMappable, optional
666
366
  The image, contour set, etc. to which the colorbar applies.
667
- If None, uses the current image.
668
367
  cax : Axes, optional
669
368
  Axes into which the colorbar will be drawn.
670
369
  ax : Axes or AxisWrapper or list thereof, optional
671
370
  Parent axes from which space for the colorbar will be stolen.
672
- If None, uses current axes.
673
371
  **kwargs
674
372
  Additional keyword arguments passed to matplotlib.pyplot.colorbar()
675
373
 
@@ -678,8 +376,6 @@ def colorbar(mappable=None, cax=None, ax=None, **kwargs):
678
376
  Colorbar
679
377
  The created colorbar object
680
378
  """
681
- import matplotlib.pyplot as plt
682
-
683
379
  # Unwrap ax if it's a SciTeX AxisWrapper
684
380
  if ax is not None:
685
381
  if hasattr(ax, "__iter__") and not isinstance(ax, str):
@@ -694,7 +390,7 @@ def colorbar(mappable=None, cax=None, ax=None, **kwargs):
694
390
  cax = cax._axis_mpl if hasattr(cax, "_axis_mpl") else cax
695
391
 
696
392
  # Call matplotlib's colorbar with unwrapped axes
697
- return plt.colorbar(mappable=mappable, cax=cax, ax=ax, **kwargs)
393
+ return _plt.colorbar(mappable=mappable, cax=cax, ax=ax, **kwargs)
698
394
 
699
395
 
700
396
  def close(fig=None):
@@ -712,54 +408,72 @@ def close(fig=None):
712
408
  - Figure or FigWrapper: close the specified figure
713
409
  - int: close figure with that number
714
410
  - str: close figure with that label, or 'all' to close all figures
715
-
716
- Examples
717
- --------
718
- >>> import scitex.plt as splt
719
- >>> fig, ax = splt.subplots()
720
- >>> ax.plot([1, 2, 3])
721
- >>> splt.close(fig) # Works with FigWrapper
722
-
723
- >>> splt.close('all') # Close all figures
724
- >>> splt.close() # Close current figure
725
-
726
- See Also
727
- --------
728
- matplotlib.pyplot.close : Standard matplotlib close function
729
411
  """
730
- import matplotlib.pyplot as plt
731
-
732
412
  if fig is None:
733
- # Close current figure
734
- plt.close()
413
+ _plt.close()
735
414
  elif isinstance(fig, (int, str)):
736
- # Close by figure number or label (including 'all')
737
- plt.close(fig)
415
+ _plt.close(fig)
738
416
  elif hasattr(fig, "_fig_mpl"):
739
417
  # FigWrapper object - unwrap and close
740
- plt.close(fig._fig_mpl)
418
+ _plt.close(fig._fig_mpl)
741
419
  elif hasattr(fig, "figure"):
742
420
  # Alternative attribute name (backward compatibility)
743
- plt.close(fig.figure)
421
+ _plt.close(fig.figure)
422
+ elif hasattr(fig, "fig"):
423
+ # figrecipe RecordingFigure - unwrap and close
424
+ _plt.close(fig.fig)
744
425
  else:
745
426
  # Assume it's a matplotlib Figure
746
- plt.close(fig)
427
+ _plt.close(fig)
747
428
 
748
429
 
430
+ # ============================================================================
431
+ # Public API
432
+ # ============================================================================
433
+
749
434
  __all__ = [
750
- "close",
751
- "color",
752
- "colorbar",
753
- "draw_graph",
435
+ # Figrecipe core (re-exported with branding)
436
+ "subplots",
437
+ "save",
438
+ "reproduce",
439
+ "load", # Alias for reproduce
440
+ "crop",
441
+ "validate",
442
+ "extract_data",
443
+ "info",
754
444
  "edit",
445
+ # Style management
446
+ "STYLE",
447
+ "load_style",
448
+ "unload_style",
449
+ "list_presets",
450
+ "apply_style",
451
+ # Composition
452
+ "compose",
453
+ "align_panels",
454
+ "distribute_panels",
455
+ "smart_align",
456
+ # Graph visualization
457
+ "draw_graph",
458
+ "get_graph_preset",
459
+ "list_graph_presets",
460
+ "register_graph_preset",
461
+ # Extensions
462
+ "sns",
463
+ "enable_svg",
464
+ # SciTeX-specific wrappers
755
465
  "figure",
466
+ "colorbar",
467
+ "close",
468
+ "tight_layout",
469
+ # Local submodules
470
+ "ax",
471
+ "color",
756
472
  "gallery",
757
- "load",
473
+ "utils",
474
+ "styles",
758
475
  "presets",
759
- "subplots",
760
476
  "termplot",
761
- "tight_layout",
762
- "utils",
763
477
  ]
764
478
 
765
479
 
@@ -768,35 +482,19 @@ def __getattr__(name):
768
482
  Fallback to matplotlib.pyplot for any missing attributes.
769
483
  This makes scitex.plt a complete drop-in replacement for matplotlib.pyplot.
770
484
  """
771
- try:
772
- import matplotlib.pyplot as plt
773
-
774
- if hasattr(plt, name):
775
- return getattr(plt, name)
776
- else:
777
- raise AttributeError(f"module 'scitex.plt' has no attribute '{name}'")
778
- except ImportError:
779
- raise AttributeError(
780
- f"module 'scitex.plt' has no attribute '{name}' (matplotlib not available)"
781
- )
485
+ if hasattr(_plt, name):
486
+ return getattr(_plt, name)
487
+ raise AttributeError(f"module 'scitex.plt' has no attribute '{name}'")
782
488
 
783
489
 
784
490
  def __dir__():
785
491
  """
786
492
  Provide comprehensive directory listing including matplotlib.pyplot functions.
787
493
  """
788
- # Get local attributes
789
- local_attrs = __all__.copy()
790
-
494
+ local_attrs = list(__all__)
791
495
  # Add matplotlib.pyplot attributes
792
- try:
793
- import matplotlib.pyplot as plt
794
-
795
- mpl_attrs = [attr for attr in dir(plt) if not attr.startswith("_")]
796
- local_attrs.extend(mpl_attrs)
797
- except ImportError:
798
- pass
799
-
496
+ mpl_attrs = [attr for attr in dir(_plt) if not attr.startswith("_")]
497
+ local_attrs.extend(mpl_attrs)
800
498
  return sorted(set(local_attrs))
801
499
 
802
500