scitex 2.14.0__py3-none-any.whl → 2.15.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.
Files changed (264) hide show
  1. scitex/__init__.py +71 -17
  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 +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-13 (ywatanabe)"
4
3
  # File: _wrappers.py - Seaborn plot wrappers
5
4
 
@@ -113,20 +112,16 @@ class SeabornWrappersMixin:
113
112
  "sns_boxplot", data=data, x=x, y=y, track=track, id=id, **kwargs
114
113
  )
115
114
 
116
- # Post-processing: Style boxplot with black medians
115
+ # Post-processing: Style boxplot elements (0.2mm black lines)
117
116
  from scitex.plt.utils import mm_to_pt
118
117
 
119
118
  lw_pt = mm_to_pt(0.2)
120
-
121
119
  for line in self._axis_mpl.get_lines():
122
120
  line.set_linewidth(lw_pt)
123
- xdata = line.get_xdata()
124
- ydata = line.get_ydata()
125
- if len(xdata) == 2 and len(ydata) == 2:
126
- if ydata[0] == ydata[1]:
127
- x_span = abs(xdata[1] - xdata[0])
128
- if x_span < 0.4:
129
- line.set_color("black")
121
+ line.set_color("black")
122
+ for patch in self._axis_mpl.patches:
123
+ patch.set_linewidth(lw_pt)
124
+ patch.set_edgecolor("black")
130
125
 
131
126
  if strip:
132
127
  strip_kwargs = kwargs.copy()
@@ -0,0 +1,149 @@
1
+ # External Package Branding Guide
2
+
3
+ When scitex wraps external packages (like `figrecipe` for `scitex.plt` or `crossref-local` for `scitex.scholar`), those packages should support configurable branding so documentation and error messages show the scitex namespace.
4
+
5
+ ## When to Use Branding
6
+
7
+ - **Use branding**: External packages that scitex wraps (figrecipe, crossref-local, etc.)
8
+ - **Don't use branding**: Internal scitex modules (scitex.audio, scitex.stats, etc.) - just hardcode `SCITEX_*` prefix
9
+
10
+ ## Pattern Overview
11
+
12
+ The external package provides a `_branding.py` module that:
13
+ 1. Reads brand name from environment variable
14
+ 2. Derives environment variable prefix from brand name
15
+ 3. Provides helper functions for rebranding text/docstrings
16
+
17
+ ## Implementation Template
18
+
19
+ ```python
20
+ # external_package/_branding.py
21
+
22
+ import os
23
+ import re
24
+ from typing import Optional
25
+
26
+ # Environment variables for branding
27
+ # Parent package sets these before importing
28
+ BRAND_NAME = os.environ.get("{PACKAGE}_BRAND", "{package}")
29
+ BRAND_ALIAS = os.environ.get("{PACKAGE}_ALIAS", "{alias}")
30
+
31
+ # Original values for replacement
32
+ _ORIGINAL_NAME = "{package}"
33
+ _ORIGINAL_ALIAS = "{alias}"
34
+
35
+
36
+ def _brand_to_env_prefix(brand: str) -> str:
37
+ """Convert brand name to environment variable prefix.
38
+
39
+ Examples:
40
+ "figrecipe" -> "FIGRECIPE"
41
+ "scitex.plt" -> "SCITEX_PLT"
42
+ "crossref-local" -> "CROSSREF_LOCAL"
43
+ """
44
+ return brand.upper().replace(".", "_").replace("-", "_")
45
+
46
+
47
+ # Environment variable prefix based on brand
48
+ ENV_PREFIX = _brand_to_env_prefix(BRAND_NAME)
49
+
50
+
51
+ def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
52
+ """Get environment variable with brand-aware prefix.
53
+
54
+ Checks {ENV_PREFIX}_{key} first, then falls back to original prefix.
55
+ """
56
+ value = os.environ.get(f"{ENV_PREFIX}_{key}")
57
+ if value is not None:
58
+ return value
59
+
60
+ # Fall back to original prefix if different
61
+ original_prefix = _brand_to_env_prefix(_ORIGINAL_NAME)
62
+ if ENV_PREFIX != original_prefix:
63
+ value = os.environ.get(f"{original_prefix}_{key}")
64
+ if value is not None:
65
+ return value
66
+
67
+ return default
68
+
69
+
70
+ def rebrand_text(text: Optional[str]) -> Optional[str]:
71
+ """Apply branding to a text string (docstrings, error messages)."""
72
+ if text is None:
73
+ return None
74
+
75
+ if BRAND_NAME == _ORIGINAL_NAME and BRAND_ALIAS == _ORIGINAL_ALIAS:
76
+ return text
77
+
78
+ result = text
79
+
80
+ # Replace import statements
81
+ result = re.sub(
82
+ rf"import\s+{_ORIGINAL_NAME}\s+as\s+{_ORIGINAL_ALIAS}",
83
+ f"import {BRAND_NAME} as {BRAND_ALIAS}",
84
+ result,
85
+ )
86
+
87
+ # Replace "from package" statements
88
+ result = re.sub(
89
+ rf"from\s+{_ORIGINAL_NAME}(\s+import|\s*\.)",
90
+ lambda m: f"from {BRAND_NAME}{m.group(1)}",
91
+ result,
92
+ )
93
+
94
+ return result
95
+
96
+
97
+ def get_mcp_server_name() -> str:
98
+ """Get the MCP server name based on branding."""
99
+ return BRAND_NAME.replace(".", "-")
100
+ ```
101
+
102
+ ## Usage in Parent Package (scitex)
103
+
104
+ ```python
105
+ # scitex/plt/__init__.py
106
+ import os
107
+
108
+ # Set branding BEFORE importing the external package
109
+ os.environ["FIGRECIPE_BRAND"] = "scitex.plt"
110
+ os.environ["FIGRECIPE_ALIAS"] = "plt"
111
+
112
+ # Now import - docstrings will show scitex.plt instead of figrecipe
113
+ from figrecipe import *
114
+ ```
115
+
116
+ ## Port Scheme
117
+
118
+ SciTeX uses port scheme 3129X (TEX → te-ku-su → 2-9-3 in Japanese):
119
+
120
+ | Port | Service |
121
+ |-------|------------------|
122
+ | 31290 | scitex-cloud |
123
+ | 31291 | crossref-local |
124
+ | 31292 | openalex |
125
+ | 31293 | scitex-audio |
126
+
127
+ ## Environment Variable Pattern
128
+
129
+ External packages should use `{ENV_PREFIX}_{SETTING}`:
130
+
131
+ ```
132
+ SCITEX_PLT_MODE=local
133
+ CROSSREF_LOCAL_API_URL=http://localhost:8333
134
+ SCITEX_AUDIO_RELAY_URL=http://localhost:31293
135
+ ```
136
+
137
+ ## Example: crossref-local
138
+
139
+ See GitHub Issue: https://github.com/ywatanabe1989/crossref-local/issues/11
140
+
141
+ The crossref-local package should implement:
142
+ - `CROSSREF_LOCAL_BRAND` / `CROSSREF_LOCAL_ALIAS` env vars
143
+ - Dynamic `ENV_PREFIX` derived from brand name
144
+ - When used via scitex.scholar, shows `scitex.scholar` in docs
145
+
146
+ ## References
147
+
148
+ - figrecipe/_branding.py - Reference implementation
149
+ - scitex/audio/_branding.py - Simple internal module (no rebranding needed)
@@ -1,7 +1,7 @@
1
1
  <!-- ---
2
2
  !-- Timestamp: 2025-12-08 23:59:46
3
3
  !-- Author: ywatanabe
4
- !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/gallery/README.md
4
+ !-- File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/gallery/README.md
5
5
  !-- --- -->
6
6
 
7
7
  # SciTeX Plot Gallery
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/__init__.py
4
+
5
+ """
6
+ Hit map generation utilities for interactive element selection.
7
+
8
+ This package provides functions to generate hit maps for matplotlib figures,
9
+ enabling pixel-perfect element selection in web editors and interactive tools.
10
+
11
+ Supported methods:
12
+ 1. ID Colors: Single render with unique colors per element (~89ms)
13
+ 2. Export Path Data: Extract geometry for client-side hit testing (~192ms)
14
+
15
+ Reserved colors:
16
+ - Black (#000000, ID=0): Background/no element
17
+ - Dark gray (#010101, ID=65793): Non-selectable axes elements (spines, labels, ticks)
18
+ """
19
+
20
+ from ._artist_extraction import (
21
+ detect_logical_groups,
22
+ get_all_artists,
23
+ get_all_artists_with_groups,
24
+ )
25
+ from ._color_application import (
26
+ apply_hitmap_colors,
27
+ apply_id_color,
28
+ prepare_hitmap_figure,
29
+ restore_figure_props,
30
+ restore_original_colors,
31
+ )
32
+ from ._color_conversion import id_to_rgb, rgb_to_id, rgb_to_id_lookup
33
+ from ._constants import HITMAP_AXES_COLOR, HITMAP_BACKGROUND_COLOR, to_native
34
+ from ._hitmap_core import generate_hitmap_id_colors, generate_hitmap_with_bbox_tight
35
+ from ._path_extraction import extract_path_data, extract_selectable_regions
36
+ from ._query import query_hitmap_neighborhood, save_hitmap_png
37
+
38
+ # Backward compatibility aliases
39
+ _to_native = to_native
40
+ _id_to_rgb = id_to_rgb
41
+ _rgb_to_id = rgb_to_id
42
+ _rgb_to_id_lookup = rgb_to_id_lookup
43
+ _apply_id_color = apply_id_color
44
+ _prepare_hitmap_figure = prepare_hitmap_figure
45
+ _restore_figure_props = restore_figure_props
46
+
47
+ __all__ = [
48
+ # Constants
49
+ "HITMAP_BACKGROUND_COLOR",
50
+ "HITMAP_AXES_COLOR",
51
+ # Artist extraction
52
+ "get_all_artists",
53
+ "get_all_artists_with_groups",
54
+ "detect_logical_groups",
55
+ # Color conversion
56
+ "id_to_rgb",
57
+ "rgb_to_id",
58
+ "rgb_to_id_lookup",
59
+ # Core hitmap generation
60
+ "generate_hitmap_id_colors",
61
+ "generate_hitmap_with_bbox_tight",
62
+ # Path extraction
63
+ "extract_path_data",
64
+ "extract_selectable_regions",
65
+ # Query and save
66
+ "query_hitmap_neighborhood",
67
+ "save_hitmap_png",
68
+ # Color application
69
+ "apply_hitmap_colors",
70
+ "restore_original_colors",
71
+ # Backward compatibility
72
+ "_to_native",
73
+ "_id_to_rgb",
74
+ "_rgb_to_id",
75
+ "_rgb_to_id_lookup",
76
+ "_apply_id_color",
77
+ "_prepare_hitmap_figure",
78
+ "_restore_figure_props",
79
+ ]
80
+
81
+
82
+ # EOF
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_artist_extraction.py
4
+
5
+ """
6
+ Artist extraction functions for hitmap generation.
7
+
8
+ This module provides functions to extract selectable artists from matplotlib
9
+ figures and detect logical groups (histogram, bar series, etc.).
10
+ """
11
+
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ __all__ = [
15
+ "get_all_artists",
16
+ "get_all_artists_with_groups",
17
+ "detect_logical_groups",
18
+ ]
19
+
20
+
21
+ def get_all_artists(fig, include_text: bool = False) -> List[Tuple[Any, int, str]]:
22
+ """
23
+ Extract all selectable artists from a figure.
24
+
25
+ Parameters
26
+ ----------
27
+ fig : matplotlib.figure.Figure
28
+ The figure to extract artists from.
29
+ include_text : bool
30
+ Whether to include text elements.
31
+
32
+ Returns
33
+ -------
34
+ list of tuple
35
+ List of (artist, axes_index, artist_type) tuples.
36
+ """
37
+ artists = []
38
+
39
+ for ax_idx, ax in enumerate(fig.axes):
40
+ # Lines (Line2D)
41
+ for line in ax.get_lines():
42
+ label = line.get_label()
43
+ if not label.startswith("_"): # Skip internal lines
44
+ artists.append((line, ax_idx, "line"))
45
+
46
+ # Scatter plots (PathCollection)
47
+ for coll in ax.collections:
48
+ coll_type = type(coll).__name__
49
+ if "PathCollection" in coll_type:
50
+ artists.append((coll, ax_idx, "scatter"))
51
+ elif "PolyCollection" in coll_type or "FillBetween" in coll_type:
52
+ artists.append((coll, ax_idx, "fill"))
53
+ elif "QuadMesh" in coll_type:
54
+ artists.append((coll, ax_idx, "mesh"))
55
+
56
+ # Bars (Rectangle patches in containers)
57
+ for container in ax.containers:
58
+ if hasattr(container, "patches") and container.patches:
59
+ artists.append((container, ax_idx, "bar"))
60
+
61
+ # Individual patches (rectangles, circles, etc.)
62
+ for patch in ax.patches:
63
+ patch_type = type(patch).__name__
64
+ if patch_type == "Rectangle":
65
+ artists.append((patch, ax_idx, "rectangle"))
66
+ elif patch_type in ("Circle", "Ellipse"):
67
+ artists.append((patch, ax_idx, "circle"))
68
+ elif patch_type == "Polygon":
69
+ artists.append((patch, ax_idx, "polygon"))
70
+
71
+ # Images
72
+ for img in ax.images:
73
+ artists.append((img, ax_idx, "image"))
74
+
75
+ # Text (optional)
76
+ if include_text:
77
+ for text in ax.texts:
78
+ if text.get_text():
79
+ artists.append((text, ax_idx, "text"))
80
+
81
+ return artists
82
+
83
+
84
+ def detect_logical_groups(fig) -> Dict[str, Dict[str, Any]]:
85
+ """
86
+ Detect logical groups in a matplotlib figure.
87
+
88
+ Logical groups represent high-level plot elements that may consist of
89
+ multiple physical matplotlib artists. For example:
90
+ - Histogram: Many Rectangle patches grouped as one "histogram"
91
+ - Bar series: BarContainer with multiple bars
92
+ - Box plot: Box, whiskers, caps, median, fliers as one "boxplot"
93
+ - Error bars: Line + error caps as one "errorbar"
94
+
95
+ Parameters
96
+ ----------
97
+ fig : matplotlib.figure.Figure
98
+ The figure to analyze.
99
+
100
+ Returns
101
+ -------
102
+ dict
103
+ Dictionary mapping group_id to group info.
104
+ """
105
+ groups = {}
106
+ group_counter = {}
107
+
108
+ def get_group_id(group_type: str, ax_idx: int) -> str:
109
+ """Generate unique group ID."""
110
+ key = f"{group_type}_{ax_idx}"
111
+ if key not in group_counter:
112
+ group_counter[key] = 0
113
+ idx = group_counter[key]
114
+ group_counter[key] += 1
115
+ return f"{group_type}_{ax_idx}_{idx}"
116
+
117
+ for ax_idx, ax in enumerate(fig.axes):
118
+ # Detect BarContainers (covers bar charts and histograms)
119
+ bar_containers = [
120
+ c for c in ax.containers if "BarContainer" in type(c).__name__
121
+ ]
122
+ n_bar_containers = len(bar_containers)
123
+
124
+ for container in ax.containers:
125
+ container_type = type(container).__name__
126
+
127
+ if "BarContainer" in container_type:
128
+ patches = (
129
+ list(container.patches) if hasattr(container, "patches") else []
130
+ )
131
+ if not patches:
132
+ continue
133
+
134
+ # Check if bars are adjacent (histogram) or spaced (bar chart)
135
+ is_histogram = False
136
+ if len(patches) > 1:
137
+ widths = [p.get_width() for p in patches]
138
+ x_positions = [p.get_x() for p in patches]
139
+ if len(x_positions) > 1:
140
+ gaps = [
141
+ x_positions[i + 1] - (x_positions[i] + widths[i])
142
+ for i in range(len(x_positions) - 1)
143
+ ]
144
+ avg_width = sum(widths) / len(widths)
145
+ is_histogram = all(abs(g) < avg_width * 0.1 for g in gaps)
146
+
147
+ if is_histogram:
148
+ group_type = "histogram"
149
+ group_id = get_group_id(group_type, ax_idx)
150
+ label = ""
151
+ if hasattr(container, "get_label"):
152
+ label = container.get_label()
153
+ if not label or label.startswith("_"):
154
+ label = f"{group_type}_{len([g for g in groups if group_type in g])}"
155
+
156
+ groups[group_id] = {
157
+ "type": group_type,
158
+ "label": label,
159
+ "axes_index": ax_idx,
160
+ "artists": patches,
161
+ "artist_types": ["rectangle"] * len(patches),
162
+ "role": "logical",
163
+ "member_count": len(patches),
164
+ }
165
+
166
+ elif n_bar_containers > 1:
167
+ group_type = "bar_series"
168
+ group_id = get_group_id(group_type, ax_idx)
169
+ label = ""
170
+ if hasattr(container, "get_label"):
171
+ label = container.get_label()
172
+ if not label or label.startswith("_"):
173
+ label = f"{group_type}_{len([g for g in groups if group_type in g])}"
174
+
175
+ groups[group_id] = {
176
+ "type": group_type,
177
+ "label": label,
178
+ "axes_index": ax_idx,
179
+ "artists": patches,
180
+ "artist_types": ["rectangle"] * len(patches),
181
+ "role": "logical",
182
+ "member_count": len(patches),
183
+ }
184
+
185
+ elif "ErrorbarContainer" in container_type:
186
+ group_id = get_group_id("errorbar", ax_idx)
187
+ artists = []
188
+ artist_types = []
189
+
190
+ if hasattr(container, "lines"):
191
+ data_line, caplines, barlinecols = container.lines
192
+ if data_line:
193
+ artists.append(data_line)
194
+ artist_types.append("line")
195
+ artists.extend(caplines)
196
+ artist_types.extend(["line"] * len(caplines))
197
+ artists.extend(barlinecols)
198
+ artist_types.extend(["line_collection"] * len(barlinecols))
199
+
200
+ label = container.get_label() if hasattr(container, "get_label") else ""
201
+ if not label or label.startswith("_"):
202
+ label = f"errorbar_{len([g for g in groups if 'errorbar' in g])}"
203
+
204
+ groups[group_id] = {
205
+ "type": "errorbar",
206
+ "label": label,
207
+ "axes_index": ax_idx,
208
+ "artists": artists,
209
+ "artist_types": artist_types,
210
+ "role": "logical",
211
+ "member_count": len(artists),
212
+ }
213
+
214
+ # Detect pie charts (Wedge patches)
215
+ wedges = [p for p in ax.patches if type(p).__name__ == "Wedge"]
216
+ if wedges:
217
+ group_id = get_group_id("pie", ax_idx)
218
+ groups[group_id] = {
219
+ "type": "pie",
220
+ "label": "Pie Chart",
221
+ "axes_index": ax_idx,
222
+ "artists": wedges,
223
+ "artist_types": ["wedge"] * len(wedges),
224
+ "role": "logical",
225
+ "member_count": len(wedges),
226
+ }
227
+
228
+ # Detect contour sets
229
+ poly_collections = [
230
+ c
231
+ for c in ax.collections
232
+ if "PolyCollection" in type(c).__name__
233
+ and hasattr(c, "get_array")
234
+ and c.get_array() is not None
235
+ ]
236
+ if len(poly_collections) > 2:
237
+ group_id = get_group_id("contour", ax_idx)
238
+ groups[group_id] = {
239
+ "type": "contour",
240
+ "label": "Contour Plot",
241
+ "axes_index": ax_idx,
242
+ "artists": poly_collections,
243
+ "artist_types": ["poly_collection"] * len(poly_collections),
244
+ "role": "logical",
245
+ "member_count": len(poly_collections),
246
+ }
247
+
248
+ return groups
249
+
250
+
251
+ def get_all_artists_with_groups(
252
+ fig, include_text: bool = False
253
+ ) -> Tuple[List[Tuple[Any, int, str, Optional[str]]], Dict[str, Dict[str, Any]]]:
254
+ """
255
+ Extract all selectable artists from a figure with logical group information.
256
+
257
+ Parameters
258
+ ----------
259
+ fig : matplotlib.figure.Figure
260
+ The figure to extract artists from.
261
+ include_text : bool
262
+ Whether to include text elements.
263
+
264
+ Returns
265
+ -------
266
+ tuple
267
+ (artists_list, groups_dict) where:
268
+ - artists_list: List of (artist, axes_index, artist_type, group_id) tuples
269
+ - groups_dict: Dictionary of logical groups
270
+ """
271
+ groups = detect_logical_groups(fig)
272
+
273
+ artist_to_group = {}
274
+ for group_id, group_info in groups.items():
275
+ for artist in group_info["artists"]:
276
+ artist_to_group[id(artist)] = group_id
277
+
278
+ artists_with_groups = []
279
+
280
+ for ax_idx, ax in enumerate(fig.axes):
281
+ # Lines
282
+ for line in ax.get_lines():
283
+ label = line.get_label()
284
+ if not label.startswith("_"):
285
+ group_id = artist_to_group.get(id(line))
286
+ artists_with_groups.append((line, ax_idx, "line", group_id))
287
+
288
+ # Collections
289
+ for coll in ax.collections:
290
+ coll_type = type(coll).__name__
291
+ group_id = artist_to_group.get(id(coll))
292
+ if "PathCollection" in coll_type:
293
+ artists_with_groups.append((coll, ax_idx, "scatter", group_id))
294
+ elif "PolyCollection" in coll_type or "FillBetween" in coll_type:
295
+ artists_with_groups.append((coll, ax_idx, "fill", group_id))
296
+ elif "QuadMesh" in coll_type:
297
+ artists_with_groups.append((coll, ax_idx, "mesh", group_id))
298
+
299
+ # Bars
300
+ processed_patches = set()
301
+ for container in ax.containers:
302
+ if hasattr(container, "patches") and container.patches:
303
+ group_id = artist_to_group.get(id(container.patches[0]))
304
+ if group_id:
305
+ artists_with_groups.append((container, ax_idx, "bar", group_id))
306
+ for patch in container.patches:
307
+ processed_patches.add(id(patch))
308
+ else:
309
+ for patch in container.patches:
310
+ artists_with_groups.append((patch, ax_idx, "rectangle", None))
311
+ processed_patches.add(id(patch))
312
+
313
+ # Patches
314
+ for patch in ax.patches:
315
+ if id(patch) in processed_patches:
316
+ continue
317
+ patch_type = type(patch).__name__
318
+ group_id = artist_to_group.get(id(patch))
319
+ if patch_type == "Rectangle":
320
+ artists_with_groups.append((patch, ax_idx, "rectangle", group_id))
321
+ elif patch_type in ("Circle", "Ellipse"):
322
+ artists_with_groups.append((patch, ax_idx, "circle", group_id))
323
+ elif patch_type == "Polygon":
324
+ artists_with_groups.append((patch, ax_idx, "polygon", group_id))
325
+ elif patch_type == "Wedge":
326
+ artists_with_groups.append((patch, ax_idx, "wedge", group_id))
327
+
328
+ # Images
329
+ for img in ax.images:
330
+ group_id = artist_to_group.get(id(img))
331
+ artists_with_groups.append((img, ax_idx, "image", group_id))
332
+
333
+ # Text
334
+ if include_text:
335
+ for text in ax.texts:
336
+ if text.get_text():
337
+ group_id = artist_to_group.get(id(text))
338
+ artists_with_groups.append((text, ax_idx, "text", group_id))
339
+
340
+ return artists_with_groups, groups
341
+
342
+
343
+ # EOF