jupytermind 0.3.0

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 (193) hide show
  1. package/.github/skills/ai-chemistry-scientist/SKILL.md +97 -0
  2. package/.github/skills/ai-chemistry-scientist/manifest.json +156 -0
  3. package/.github/skills/ai-data-scientist/SKILL.md +330 -0
  4. package/.github/skills/ai-genomics-scientist/SKILL.md +98 -0
  5. package/.github/skills/ai-genomics-scientist/manifest.json +93 -0
  6. package/.github/skills/ai-materials-scientist/SKILL.md +51 -0
  7. package/.github/skills/ai-materials-scientist/manifest.json +58 -0
  8. package/.github/skills/ai-scientist/SKILL.md +69 -0
  9. package/.github/skills/ai-scientist/manifest.json +61 -0
  10. package/.github/skills/ai-structural-biology-scientist/SKILL.md +67 -0
  11. package/.github/skills/ai-structural-biology-scientist/manifest.json +72 -0
  12. package/.github/skills/japanese-prose/NOTICE.md +17 -0
  13. package/.github/skills/japanese-prose/SKILL.md +111 -0
  14. package/.github/skills/japanese-prose/references/review-workflow.md +50 -0
  15. package/.github/skills/japanese-prose/references/scoring.md +24 -0
  16. package/.github/skills/japanese-prose/references/writing-guidelines.md +60 -0
  17. package/.github/skills/japanese-prose/scripts/core.py +192 -0
  18. package/.github/skills/japanese-prose/scripts/fixtures/natural.md +5 -0
  19. package/.github/skills/japanese-prose/scripts/fixtures/unnatural.md +5 -0
  20. package/.github/skills/japanese-prose/scripts/lint.py +378 -0
  21. package/.github/skills/japanese-prose/scripts/outline.py +68 -0
  22. package/.github/skills/japanese-prose/scripts/terms.py +112 -0
  23. package/.github/skills/japanese-prose/scripts/test_engine.py +117 -0
  24. package/.github/skills/presentation-planner/SKILL.md +257 -0
  25. package/.github/skills/presentation-planner/assets/design-templates/data-report.yaml +97 -0
  26. package/.github/skills/presentation-planner/assets/design-templates/executive-proposal.yaml +92 -0
  27. package/.github/skills/presentation-planner/assets/design-templates/technical-briefing.yaml +96 -0
  28. package/.github/skills/presentation-planner/assets/scenario-templates/data-report.md +47 -0
  29. package/.github/skills/presentation-planner/assets/scenario-templates/executive-decision.md +43 -0
  30. package/.github/skills/presentation-planner/assets/scenario-templates/technical-briefing.md +45 -0
  31. package/.github/skills/presentation-planner/references/customizing-design-templates.md +160 -0
  32. package/.github/skills/presentation-planner/references/design-spec-schema.md +72 -0
  33. package/.github/skills/presentation-planner/references/handoff-contract.md +49 -0
  34. package/.github/skills/presentation-planner/references/responsibility-boundary.md +32 -0
  35. package/.github/skills/presentation-planner/references/scenario-templates.md +55 -0
  36. package/.github/skills/tech-writer/SKILL.md +434 -0
  37. package/.github/skills/tech-writer/assets/templates/blueprint.md +187 -0
  38. package/.github/skills/tech-writer/assets/templates/design-doc.md +29 -0
  39. package/.github/skills/tech-writer/assets/templates/migration-plan.md +173 -0
  40. package/.github/skills/tech-writer/assets/templates/operations-runbook.md +202 -0
  41. package/.github/skills/tech-writer/assets/templates/pr-description.md +23 -0
  42. package/.github/skills/tech-writer/assets/templates/qiita.md +44 -0
  43. package/.github/skills/tech-writer/assets/templates/readme.md +38 -0
  44. package/.github/skills/tech-writer/assets/templates/requirements-definition.md +170 -0
  45. package/.github/skills/tech-writer/assets/templates/rfi.md +113 -0
  46. package/.github/skills/tech-writer/assets/templates/rfp.md +180 -0
  47. package/.github/skills/tech-writer/assets/templates/security-design.md +167 -0
  48. package/.github/skills/tech-writer/assets/templates/system-design.md +220 -0
  49. package/.github/skills/tech-writer/assets/templates/technical-proposal.md +112 -0
  50. package/.github/skills/tech-writer/assets/templates/test-plan.md +153 -0
  51. package/.github/skills/tech-writer/assets/templates/user-manual.md +22 -0
  52. package/.github/skills/tech-writer/assets/templates/white-paper.md +192 -0
  53. package/.github/skills/tech-writer/references/doctypes/api-docs.md +33 -0
  54. package/.github/skills/tech-writer/references/doctypes/blueprint.md +81 -0
  55. package/.github/skills/tech-writer/references/doctypes/code-comments.md +39 -0
  56. package/.github/skills/tech-writer/references/doctypes/design-doc.md +42 -0
  57. package/.github/skills/tech-writer/references/doctypes/migration-plan.md +63 -0
  58. package/.github/skills/tech-writer/references/doctypes/operations-runbook.md +63 -0
  59. package/.github/skills/tech-writer/references/doctypes/pr-commit.md +82 -0
  60. package/.github/skills/tech-writer/references/doctypes/qiita.md +75 -0
  61. package/.github/skills/tech-writer/references/doctypes/readme.md +43 -0
  62. package/.github/skills/tech-writer/references/doctypes/release-notes.md +30 -0
  63. package/.github/skills/tech-writer/references/doctypes/requirements-definition.md +61 -0
  64. package/.github/skills/tech-writer/references/doctypes/rfi.md +43 -0
  65. package/.github/skills/tech-writer/references/doctypes/rfp.md +46 -0
  66. package/.github/skills/tech-writer/references/doctypes/security-design.md +71 -0
  67. package/.github/skills/tech-writer/references/doctypes/system-design.md +74 -0
  68. package/.github/skills/tech-writer/references/doctypes/technical-proposal.md +49 -0
  69. package/.github/skills/tech-writer/references/doctypes/test-plan.md +67 -0
  70. package/.github/skills/tech-writer/references/doctypes/user-manual.md +58 -0
  71. package/.github/skills/tech-writer/references/doctypes/white-paper.md +84 -0
  72. package/.github/skills/tech-writer/references/doctypes/zenn.md +66 -0
  73. package/.github/skills/tech-writer/references/japanese-prose-optimization.md +110 -0
  74. package/.github/skills/tech-writer/references/style-constitution.md +104 -0
  75. package/.github/skills/tech-writer/scripts/lint.py +412 -0
  76. package/LICENSE +21 -0
  77. package/README.md +92 -0
  78. package/bin/ai-data-scientist.js +123 -0
  79. package/package.json +41 -0
  80. package/pyproject.toml +45 -0
  81. package/src/ai_chemistry_scientist/__init__.py +0 -0
  82. package/src/ai_chemistry_scientist/admet_prediction.py +71 -0
  83. package/src/ai_chemistry_scientist/bioactivity_classification.py +73 -0
  84. package/src/ai_chemistry_scientist/data/sample_molecules.csv +21 -0
  85. package/src/ai_chemistry_scientist/dispatch.py +369 -0
  86. package/src/ai_chemistry_scientist/docking_score.py +97 -0
  87. package/src/ai_chemistry_scientist/drug_likeness_rules.py +84 -0
  88. package/src/ai_chemistry_scientist/evidence.py +41 -0
  89. package/src/ai_chemistry_scientist/molecular_descriptors.py +97 -0
  90. package/src/ai_chemistry_scientist/molecular_formula_mass.py +40 -0
  91. package/src/ai_chemistry_scientist/molecular_similarity.py +78 -0
  92. package/src/ai_chemistry_scientist/qsar_modeling.py +105 -0
  93. package/src/ai_chemistry_scientist/salt_standardization.py +81 -0
  94. package/src/ai_chemistry_scientist/structural_alerts.py +76 -0
  95. package/src/ai_chemistry_scientist/structure_format_conversion.py +84 -0
  96. package/src/ai_chemistry_scientist/validation.py +70 -0
  97. package/src/ai_data_scientist/__init__.py +0 -0
  98. package/src/ai_data_scientist/analysis_assumptions.py +121 -0
  99. package/src/ai_data_scientist/anomaly_detection.py +39 -0
  100. package/src/ai_data_scientist/automl.py +109 -0
  101. package/src/ai_data_scientist/cleaning.py +56 -0
  102. package/src/ai_data_scientist/cli.py +90 -0
  103. package/src/ai_data_scientist/clustering.py +54 -0
  104. package/src/ai_data_scientist/dashboard.py +33 -0
  105. package/src/ai_data_scientist/data_definition.py +100 -0
  106. package/src/ai_data_scientist/data_quality.py +164 -0
  107. package/src/ai_data_scientist/dataset_validation.py +135 -0
  108. package/src/ai_data_scientist/dependency_pins.py +60 -0
  109. package/src/ai_data_scientist/eda.py +82 -0
  110. package/src/ai_data_scientist/experiment_evaluation.py +635 -0
  111. package/src/ai_data_scientist/explainability.py +340 -0
  112. package/src/ai_data_scientist/feature_engineering.py +163 -0
  113. package/src/ai_data_scientist/gate_config.py +32 -0
  114. package/src/ai_data_scientist/ingestion.py +127 -0
  115. package/src/ai_data_scientist/insight_engine.py +180 -0
  116. package/src/ai_data_scientist/japanese_nlp.py +43 -0
  117. package/src/ai_data_scientist/jupyter_launcher.py +137 -0
  118. package/src/ai_data_scientist/jupyter_mcp_client.py +94 -0
  119. package/src/ai_data_scientist/language_router.py +28 -0
  120. package/src/ai_data_scientist/lifecycle.py +221 -0
  121. package/src/ai_data_scientist/mcp_gateway.py +113 -0
  122. package/src/ai_data_scientist/mcp_runtime.py +194 -0
  123. package/src/ai_data_scientist/mcp_transport.py +53 -0
  124. package/src/ai_data_scientist/ml_modeling.py +451 -0
  125. package/src/ai_data_scientist/model_tuning.py +104 -0
  126. package/src/ai_data_scientist/notebook_audit.py +574 -0
  127. package/src/ai_data_scientist/project_manager.py +243 -0
  128. package/src/ai_data_scientist/report_export.py +73 -0
  129. package/src/ai_data_scientist/sensitivity.py +445 -0
  130. package/src/ai_data_scientist/signal_analysis.py +201 -0
  131. package/src/ai_data_scientist/skill_packaging.py +40 -0
  132. package/src/ai_data_scientist/stats_analysis.py +88 -0
  133. package/src/ai_data_scientist/text_nlp.py +44 -0
  134. package/src/ai_data_scientist/timeseries.py +68 -0
  135. package/src/ai_data_scientist/visualization.py +708 -0
  136. package/src/ai_genomics_scientist/__init__.py +1 -0
  137. package/src/ai_genomics_scientist/differential_expression.py +147 -0
  138. package/src/ai_genomics_scientist/dispatch.py +267 -0
  139. package/src/ai_genomics_scientist/evidence.py +45 -0
  140. package/src/ai_genomics_scientist/gene_set_enrichment.py +76 -0
  141. package/src/ai_genomics_scientist/sequence_alignment.py +97 -0
  142. package/src/ai_genomics_scientist/sequence_features.py +111 -0
  143. package/src/ai_genomics_scientist/splice_site_scoring.py +66 -0
  144. package/src/ai_genomics_scientist/validation.py +83 -0
  145. package/src/ai_genomics_scientist/variant_effect.py +147 -0
  146. package/src/ai_genomics_scientist/variant_pathogenicity.py +125 -0
  147. package/src/ai_materials_scientist/__init__.py +0 -0
  148. package/src/ai_materials_scientist/calphad.py +117 -0
  149. package/src/ai_materials_scientist/classical_monte_carlo.py +165 -0
  150. package/src/ai_materials_scientist/crystal_plasticity.py +184 -0
  151. package/src/ai_materials_scientist/dispatch.py +100 -0
  152. package/src/ai_materials_scientist/evidence.py +84 -0
  153. package/src/ai_materials_scientist/fem.py +279 -0
  154. package/src/ai_materials_scientist/kinetic_monte_carlo.py +145 -0
  155. package/src/ai_materials_scientist/molecular_dynamics.py +240 -0
  156. package/src/ai_materials_scientist/phase_field.py +167 -0
  157. package/src/ai_materials_scientist/validation.py +70 -0
  158. package/src/ai_scientist/__init__.py +1 -0
  159. package/src/ai_scientist/completion_gate.py +15 -0
  160. package/src/ai_scientist/data_analysis.py +46 -0
  161. package/src/ai_scientist/evidence_registry.py +99 -0
  162. package/src/ai_scientist/experimental_design.py +20 -0
  163. package/src/ai_scientist/language.py +14 -0
  164. package/src/ai_scientist/latex_renderer.py +41 -0
  165. package/src/ai_scientist/literature_review.py +37 -0
  166. package/src/ai_scientist/manifest.py +87 -0
  167. package/src/ai_scientist/manuscript.py +94 -0
  168. package/src/ai_scientist/mcp_config.py +76 -0
  169. package/src/ai_scientist/mcp_external.py +42 -0
  170. package/src/ai_scientist/mcp_failures.py +23 -0
  171. package/src/ai_scientist/mcp_gateway.py +38 -0
  172. package/src/ai_scientist/mcp_managed.py +180 -0
  173. package/src/ai_scientist/npm_packaging.py +49 -0
  174. package/src/ai_scientist/orchestrator.py +133 -0
  175. package/src/ai_scientist/peer_review.py +60 -0
  176. package/src/ai_scientist/phase_gate.py +74 -0
  177. package/src/ai_scientist/phase_state.py +230 -0
  178. package/src/ai_scientist/presentation.py +56 -0
  179. package/src/ai_scientist/project_config.py +31 -0
  180. package/src/ai_scientist/project_handle.py +74 -0
  181. package/src/ai_scientist/reproducibility.py +20 -0
  182. package/src/ai_scientist/research_planning.py +20 -0
  183. package/src/ai_scientist/skill_invocation.py +21 -0
  184. package/src/ai_scientist/tdd_gate.py +99 -0
  185. package/src/ai_structural_biology_scientist/__init__.py +0 -0
  186. package/src/ai_structural_biology_scientist/contact_map.py +87 -0
  187. package/src/ai_structural_biology_scientist/dispatch.py +269 -0
  188. package/src/ai_structural_biology_scientist/evidence.py +43 -0
  189. package/src/ai_structural_biology_scientist/hydrophobicity.py +101 -0
  190. package/src/ai_structural_biology_scientist/protein_docking_score.py +104 -0
  191. package/src/ai_structural_biology_scientist/secondary_structure.py +95 -0
  192. package/src/ai_structural_biology_scientist/structural_similarity.py +74 -0
  193. package/src/ai_structural_biology_scientist/validation.py +100 -0
@@ -0,0 +1,708 @@
1
+ """Visualization generation.
2
+
3
+ Implements DES-AIDS-009 (REQ-AIDS-007): renders a requested chart to a PNG
4
+ image and packages it as an nbformat-compatible output MIME bundle.
5
+ CHANGE-012's DES-AIDS-073 through DES-AIDS-076 (CODE-AIDS-111 through
6
+ CODE-AIDS-118) add box/barh/heatmap kinds, hue-grouped series with legend
7
+ titles, symmetric/asymmetric error bars, and chart_metadata_from_figure.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import io
14
+ import re
15
+ import secrets
16
+ import warnings
17
+ from dataclasses import dataclass, field
18
+ from typing import Self
19
+
20
+ import matplotlib
21
+
22
+ matplotlib.use("Agg")
23
+ import matplotlib.pyplot as plt
24
+ import nbformat
25
+ import pandas as pd
26
+
27
+ from ai_data_scientist.project_manager import ProjectHandle, enqueue_write, next_execution_count
28
+
29
+ _SUPPORTED_KINDS = ("scatter", "line", "bar", "barh", "box", "hist", "heatmap")
30
+ _MISSING_GLYPH_RE = re.compile(r"Glyph (\d+) .* missing from (?:current )?font")
31
+
32
+ # @id CODE-AIDS-054
33
+ # @implements REQ-AIDS-046
34
+ # @design DES-AIDS-034
35
+ _japanese_font_applied = False
36
+ _JAPANESE_FONT_FAMILY = "IPAexGothic"
37
+
38
+
39
+ def _contains_non_ascii(text: str | None) -> bool:
40
+ """True if ``text`` contains a character matplotlib's default font
41
+ cannot render legibly (anything outside the printable ASCII range)."""
42
+ return bool(text) and any(ord(ch) > 127 for ch in text)
43
+
44
+
45
+ def _ensure_japanese_font() -> None:
46
+ """Register (once) and reassert (every call) the bundled Japanese font.
47
+
48
+ Importing ``japanize_matplotlib`` registers its bundled IPAexGothic
49
+ TrueType font with matplotlib's font manager — an expensive, idempotent
50
+ side effect gated by ``_japanese_font_applied`` so it runs at most once
51
+ per process. Setting ``font.family`` to the registered font name is
52
+ cheap and, unlike the import, is *not* gated: it is reasserted on every
53
+ call that needs it (GitHub #32). Without this, a caller resetting
54
+ matplotlib's global ``rcParams`` between calls (e.g. ``plt.rcdefaults()``)
55
+ would silently revert ``font.family`` to its default, and the previous
56
+ once-per-process guard would then skip reapplying it, causing Japanese
57
+ text to render as "tofu" boxes on later calls even though the font was
58
+ already registered.
59
+ """
60
+ global _japanese_font_applied
61
+ if not _japanese_font_applied:
62
+ with warnings.catch_warnings():
63
+ # japanize_matplotlib relies on distutils.version.LooseVersion,
64
+ # which emits a harmless DeprecationWarning under modern setuptools.
65
+ warnings.simplefilter("ignore", DeprecationWarning)
66
+ import japanize_matplotlib # noqa: F401 - import side effect registers the font
67
+ _japanese_font_applied = True
68
+ matplotlib.rcParams["font.family"] = _JAPANESE_FONT_FAMILY
69
+
70
+
71
+ # @id CODE-AIDS-084
72
+ # @implements REQ-AIDS-064
73
+ # @design DES-AIDS-052
74
+ _JAPANESE_CHAR_RE = re.compile(
75
+ "["
76
+ "\u3040-\u309f" # Hiragana
77
+ "\u30a0-\u30ff" # Katakana
78
+ "\u31f0-\u31ff" # Katakana Phonetic Extensions
79
+ "\uff65-\uff9f" # Halfwidth Katakana
80
+ "\u3000-\u303f" # CJK Symbols and Punctuation
81
+ "\u4e00-\u9fff" # CJK Unified Ideographs
82
+ "\u3400-\u4dbf" # CJK Unified Ideographs Extension A
83
+ "\uf900-\ufaff" # CJK Compatibility Ideographs
84
+ "]"
85
+ )
86
+
87
+
88
+ def _contains_japanese(text) -> bool:
89
+ """True if ``str(text)`` contains a Japanese-derived character.
90
+
91
+ Narrower than ``_contains_non_ascii``: only matches Hiragana, Katakana
92
+ (including phonetic extensions and halfwidth forms), CJK symbols and
93
+ punctuation, and CJK (Unified/Extension-A/Compatibility) ideographs, per
94
+ REQ-AIDS-064's scoping to Japanese-derived plotted data/tick/legend text.
95
+ Non-string, non-stringifiable values are treated as not containing
96
+ Japanese text.
97
+ """
98
+ if text is None:
99
+ return False
100
+ try:
101
+ value = str(text)
102
+ except Exception: # noqa: BLE001 - arbitrary user objects may raise from __str__
103
+ return False
104
+ return bool(_JAPANESE_CHAR_RE.search(value))
105
+
106
+
107
+ def _plotted_data_contains_japanese(
108
+ df: pd.DataFrame,
109
+ kind: str,
110
+ x: str | None,
111
+ y: str | None,
112
+ hue: str | None = None,
113
+ legend_title: str | None = None,
114
+ ) -> bool:
115
+ """True if any plotted value/index/legend-source/auto-axis-label text
116
+ for this ``render_chart`` call contains Japanese characters (DES-AIDS-052).
117
+ """
118
+ candidates: list = [legend_title]
119
+
120
+ def _add_column(column_name: str | None) -> None:
121
+ if column_name is None:
122
+ return
123
+ candidates.append(column_name)
124
+ if column_name in df.columns:
125
+ candidates.extend(df[column_name])
126
+
127
+ _add_column(hue)
128
+
129
+ if kind == "hist" and x is not None:
130
+ _add_column(x)
131
+ candidates.append(df.index.name)
132
+ candidates.extend(df.index)
133
+ elif kind == "heatmap":
134
+ if x is not None and y is not None:
135
+ _add_column(x)
136
+ _add_column(y)
137
+ else:
138
+ candidates.extend(df.columns)
139
+ candidates.append(df.index.name)
140
+ candidates.extend(df.index)
141
+ elif kind == "box":
142
+ _add_column(x)
143
+ _add_column(y)
144
+ if x is None and y is None:
145
+ candidates.extend(df.columns)
146
+ else:
147
+ if x is not None:
148
+ _add_column(x)
149
+ else:
150
+ candidates.append(df.index.name)
151
+ candidates.extend(df.index)
152
+
153
+ if y is not None:
154
+ _add_column(y)
155
+ else:
156
+ implicit_columns = [column for column in df.columns if column not in {x, hue}]
157
+ for column in implicit_columns:
158
+ _add_column(column)
159
+
160
+ return any(_contains_japanese(candidate) for candidate in candidates)
161
+
162
+
163
+ # @id CODE-AIDS-085
164
+ # @implements REQ-AIDS-060
165
+ # @design DES-AIDS-048
166
+ @dataclass(frozen=True)
167
+ class ChartMetadata:
168
+ """Chart authoring metadata captured at render time (REQ-AIDS-060)."""
169
+
170
+ title: str | None
171
+ xlabel: str | None
172
+ ylabel: str | None
173
+ legend: bool
174
+ legend_title: str | None = None
175
+ missing_glyphs: tuple[str, ...] = field(default_factory=tuple)
176
+
177
+
178
+ class RenderedChart(bytes):
179
+ """PNG bytes returned by ``render_chart``, carrying ``chart_metadata``.
180
+
181
+ A real ``bytes`` subclass so every existing caller that treats the
182
+ return value via ordinary ``bytes`` operations (``base64.b64encode``,
183
+ equality, slicing, hashing) continues to work unmodified (REQ-AIDS-060).
184
+ """
185
+
186
+ chart_metadata: ChartMetadata
187
+
188
+ def __new__(cls, data: bytes, chart_metadata: ChartMetadata) -> Self:
189
+ instance = super().__new__(cls, data)
190
+ instance.chart_metadata = chart_metadata
191
+ return instance
192
+
193
+ @property
194
+ def title(self) -> str | None:
195
+ return self.chart_metadata.title
196
+
197
+ @property
198
+ def xlabel(self) -> str | None:
199
+ return self.chart_metadata.xlabel
200
+
201
+ @property
202
+ def ylabel(self) -> str | None:
203
+ return self.chart_metadata.ylabel
204
+
205
+ @property
206
+ def legend(self) -> bool:
207
+ return self.chart_metadata.legend
208
+
209
+ @property
210
+ def legend_title(self) -> str | None:
211
+ return self.chart_metadata.legend_title
212
+
213
+ @property
214
+ def missing_glyphs(self) -> tuple[str, ...]:
215
+ return self.chart_metadata.missing_glyphs
216
+
217
+
218
+ # @id CODE-AIDS-111
219
+ # @implements REQ-AIDS-088
220
+ # @design DES-AIDS-076
221
+ def chart_metadata_from_figure(
222
+ fig, *, missing_glyphs: tuple[str, ...] | list[str] = ()
223
+ ) -> ChartMetadata:
224
+ """Build ``ChartMetadata`` from an existing matplotlib ``Figure``."""
225
+ if not fig.axes:
226
+ raise ValueError("Figure has no axes to inspect")
227
+ ax = fig.axes[0]
228
+ legend = ax.get_legend()
229
+ legend_title = None
230
+ if legend is not None:
231
+ legend_title = legend.get_title().get_text() or None
232
+ return ChartMetadata(
233
+ title=ax.get_title() or None,
234
+ xlabel=ax.get_xlabel() or None,
235
+ ylabel=ax.get_ylabel() or None,
236
+ legend=legend is not None,
237
+ legend_title=legend_title,
238
+ missing_glyphs=tuple(str(codepoint) for codepoint in missing_glyphs),
239
+ )
240
+
241
+
242
+ def _apply_legend_title(ax, hue: str | None, legend_title: str | None) -> None:
243
+ legend = ax.get_legend()
244
+ if legend is None:
245
+ return
246
+ if legend_title is not None:
247
+ legend.set_title(legend_title)
248
+ elif hue is not None:
249
+ legend.set_title(hue)
250
+
251
+
252
+ def _require_columns(df: pd.DataFrame, *columns: str | None) -> None:
253
+ for column in columns:
254
+ if column is not None and column not in df.columns:
255
+ raise ValueError(f"Unknown column: {column!r}")
256
+
257
+
258
+ # @id CODE-AIDS-112
259
+ # @implements REQ-AIDS-087
260
+ # @design DES-AIDS-075
261
+ def _resolve_error_values(
262
+ df: pd.DataFrame, spec: str | tuple[str, str] | list[str] | None
263
+ ) -> list[float] | list[list[float]] | None:
264
+ if spec is None:
265
+ return None
266
+ if isinstance(spec, str):
267
+ _require_columns(df, spec)
268
+ return df[spec].tolist()
269
+ if (
270
+ isinstance(spec, (tuple, list))
271
+ and len(spec) == 2
272
+ and all(isinstance(column, str) for column in spec)
273
+ ):
274
+ lower, upper = spec
275
+ _require_columns(df, lower, upper)
276
+ return [df[lower].tolist(), df[upper].tolist()]
277
+ raise ValueError("Error ranges must be a column name or a two-column (lower, upper) pair")
278
+
279
+
280
+ def _group_label(value) -> str:
281
+ return "NaN" if pd.isna(value) else str(value)
282
+
283
+
284
+ # @id CODE-AIDS-116
285
+ # @implements REQ-AIDS-086
286
+ # @design DES-AIDS-074
287
+ def _normalize_hue_for_pivot(series: pd.Series) -> tuple[pd.Series, str]:
288
+ sentinel = f"__missing_hue__{secrets.token_hex(8)}"
289
+ existing_values = {str(value) for value in series.dropna().tolist()}
290
+ while sentinel in existing_values:
291
+ sentinel = f"__missing_hue__{secrets.token_hex(8)}"
292
+ normalized = series.astype("object").where(~series.isna(), sentinel)
293
+ return normalized, sentinel
294
+
295
+
296
+ def _pivot_grouped_values(df: pd.DataFrame, *, x: str, hue: str, value: str) -> pd.DataFrame:
297
+ normalized_hue, sentinel = _normalize_hue_for_pivot(df[hue])
298
+ order = list(dict.fromkeys(normalized_hue.tolist()))
299
+ pivot_source = pd.DataFrame({x: df[x], "__hue__": normalized_hue, value: df[value]})
300
+ duplicates = pivot_source.duplicated(subset=[x, "__hue__"], keep=False)
301
+ if duplicates.any():
302
+ raise ValueError(
303
+ f"Grouped {value!r} data must be unique per ({x!r}, {hue!r}) pair; "
304
+ "pre-aggregate duplicate rows before plotting."
305
+ )
306
+ pivot = pivot_source.pivot(index=x, columns="__hue__", values=value)
307
+ pivot = pivot.reindex(columns=order)
308
+ pivot = pivot.rename(columns={sentinel: "NaN"})
309
+ return pivot
310
+
311
+
312
+ # @id CODE-AIDS-117
313
+ # @implements REQ-AIDS-087
314
+ # @design DES-AIDS-075
315
+ def _pivot_error_values(
316
+ df: pd.DataFrame,
317
+ *,
318
+ x: str,
319
+ hue: str,
320
+ spec: str | tuple[str, str] | list[str] | None,
321
+ ):
322
+ if spec is None:
323
+ return None
324
+ if isinstance(spec, str):
325
+ return _pivot_grouped_values(df, x=x, hue=hue, value=spec)
326
+ if isinstance(spec, (tuple, list)) and len(spec) == 2:
327
+ lower, upper = spec
328
+ return [
329
+ _pivot_grouped_values(df, x=x, hue=hue, value=lower),
330
+ _pivot_grouped_values(df, x=x, hue=hue, value=upper),
331
+ ]
332
+ raise ValueError("Error ranges must be a column name or a two-column (lower, upper) pair")
333
+
334
+
335
+ # @id CODE-AIDS-113
336
+ # @implements REQ-AIDS-086
337
+ # @design DES-AIDS-074
338
+ def _plot_with_hue(
339
+ df: pd.DataFrame,
340
+ *,
341
+ kind: str,
342
+ x: str | None,
343
+ y: str | None,
344
+ hue: str,
345
+ ax,
346
+ xerr: list[float] | list[list[float]] | None,
347
+ yerr: list[float] | list[list[float]] | None,
348
+ ) -> None:
349
+ _require_columns(df, hue)
350
+ if kind == "scatter":
351
+ _require_columns(df, x, y)
352
+ for label, group in df.groupby(hue, sort=False, dropna=False):
353
+ ax.errorbar(
354
+ group[x],
355
+ group[y],
356
+ xerr=_resolve_error_values(group, xerr),
357
+ yerr=_resolve_error_values(group, yerr),
358
+ fmt="o",
359
+ linestyle="none",
360
+ label=_group_label(label),
361
+ )
362
+ ax.legend()
363
+ elif kind == "line":
364
+ _require_columns(df, y)
365
+ if y is None:
366
+ raise ValueError("Line charts with hue require a y column")
367
+ for label, group in df.groupby(hue, sort=False, dropna=False):
368
+ x_values = group[x] if x is not None else group.index
369
+ ax.errorbar(
370
+ x_values,
371
+ group[y],
372
+ xerr=_resolve_error_values(group, xerr),
373
+ yerr=_resolve_error_values(group, yerr),
374
+ fmt="-",
375
+ label=_group_label(label),
376
+ )
377
+ ax.legend()
378
+ if x is not None:
379
+ ax.set_xlabel(x)
380
+ if y is not None:
381
+ ax.set_ylabel(y)
382
+ elif kind == "hist":
383
+ _require_columns(df, x)
384
+ for label, group in df.groupby(hue, sort=False, dropna=False):
385
+ ax.hist(group[x].dropna(), label=_group_label(label), alpha=0.7)
386
+ ax.legend()
387
+ elif kind in {"bar", "barh"}:
388
+ _require_columns(df, x, y)
389
+ pivot = _pivot_grouped_values(df, x=x, hue=hue, value=y)
390
+ kwargs = {}
391
+ if xerr is not None:
392
+ kwargs["xerr"] = _pivot_error_values(df, x=x, hue=hue, spec=xerr)
393
+ if yerr is not None:
394
+ kwargs["yerr"] = _pivot_error_values(df, x=x, hue=hue, spec=yerr)
395
+ pivot.plot(kind=kind, ax=ax, **kwargs)
396
+ else:
397
+ raise ValueError(f"hue is not supported for chart kind: {kind!r}")
398
+
399
+
400
+ # @id CODE-AIDS-114
401
+ # @implements REQ-AIDS-085
402
+ # @design DES-AIDS-073
403
+ def _plot_box_chart(df: pd.DataFrame, *, x: str | None, y: str | None, ax) -> None:
404
+ if y is None:
405
+ numeric = df.select_dtypes(include="number")
406
+ if numeric.empty:
407
+ raise ValueError("Box charts require at least one numeric column")
408
+ values = [numeric[column].dropna().tolist() for column in numeric.columns]
409
+ labels = [str(column) for column in numeric.columns]
410
+ elif x is None:
411
+ _require_columns(df, y)
412
+ values = [df[y].dropna().tolist()]
413
+ labels = [str(y)]
414
+ ax.set_ylabel(y)
415
+ else:
416
+ _require_columns(df, x, y)
417
+ values = []
418
+ labels = []
419
+ for label, group in df.groupby(x, sort=False, dropna=False):
420
+ points = group[y].dropna().tolist()
421
+ if not points:
422
+ continue
423
+ values.append(points)
424
+ labels.append(_group_label(label))
425
+ if not values:
426
+ raise ValueError("Box charts require at least one non-empty group")
427
+ ax.set_xlabel(x)
428
+ ax.set_ylabel(y)
429
+
430
+ ax.boxplot(values)
431
+ ax.set_xticks(range(1, len(labels) + 1))
432
+ ax.set_xticklabels(labels)
433
+
434
+
435
+ # @id CODE-AIDS-115
436
+ # @implements REQ-AIDS-085
437
+ # @design DES-AIDS-073
438
+ def _plot_heatmap(df: pd.DataFrame, *, x: str | None, y: str | None, ax) -> None:
439
+ if x is not None and y is not None:
440
+ _require_columns(df, x, y)
441
+ matrix = df[[x, y]].select_dtypes(include="number").corr()
442
+ else:
443
+ numeric = df.select_dtypes(include="number")
444
+ if (
445
+ not numeric.empty
446
+ and numeric.shape[0] == numeric.shape[1]
447
+ and list(map(str, numeric.index)) == list(map(str, numeric.columns))
448
+ ):
449
+ matrix = numeric
450
+ else:
451
+ matrix = numeric.corr()
452
+ if matrix.empty:
453
+ raise ValueError("Heatmap charts require numeric data")
454
+ image = ax.imshow(matrix.to_numpy(), aspect="auto")
455
+ ax.set_xticks(range(len(matrix.columns)))
456
+ ax.set_xticklabels([str(column) for column in matrix.columns])
457
+ ax.set_yticks(range(len(matrix.index)))
458
+ ax.set_yticklabels([str(index) for index in matrix.index])
459
+ fig = ax.figure
460
+ fig.colorbar(image, ax=ax)
461
+
462
+
463
+ def _plot_with_existing_paths(
464
+ df: pd.DataFrame,
465
+ *,
466
+ kind: str,
467
+ x: str | None,
468
+ y: str | None,
469
+ ax,
470
+ xerr: list[float] | list[list[float]] | None,
471
+ yerr: list[float] | list[list[float]] | None,
472
+ ) -> None:
473
+ if kind == "hist":
474
+ if xerr is not None or yerr is not None:
475
+ raise ValueError("Histogram charts do not support error ranges")
476
+ _require_columns(df, x)
477
+ df[x].plot(kind="hist", ax=ax)
478
+ return
479
+
480
+ if kind == "scatter":
481
+ if xerr is None and yerr is None:
482
+ df.plot(kind="scatter", x=x, y=y, ax=ax)
483
+ return
484
+ _require_columns(df, x, y)
485
+ ax.errorbar(df[x], df[y], xerr=xerr, yerr=yerr, fmt="o", linestyle="none")
486
+ ax.set_xlabel(x)
487
+ ax.set_ylabel(y)
488
+ return
489
+
490
+ if kind == "line":
491
+ if xerr is None and yerr is None:
492
+ df.plot(kind="line", x=x, y=y, ax=ax)
493
+ return
494
+ if y is None:
495
+ raise ValueError("Line charts require y when error ranges are requested")
496
+ x_values = df[x] if x is not None else df.index
497
+ ax.errorbar(x_values, df[y], xerr=xerr, yerr=yerr, fmt="-")
498
+ if x is not None:
499
+ ax.set_xlabel(x)
500
+ ax.set_ylabel(y)
501
+ return
502
+
503
+ kwargs = {}
504
+ if xerr is not None:
505
+ kwargs["xerr"] = xerr
506
+ if yerr is not None:
507
+ kwargs["yerr"] = yerr
508
+ df.plot(kind=kind, x=x, y=y, ax=ax, **kwargs)
509
+
510
+
511
+ # @id CODE-AIDS-007
512
+ # @implements REQ-AIDS-007 REQ-AIDS-058 REQ-AIDS-060 REQ-AIDS-064
513
+ # @design DES-AIDS-009 DES-AIDS-048 DES-AIDS-052
514
+ # @id CODE-AIDS-118
515
+ # @implements REQ-AIDS-085 REQ-AIDS-086 REQ-AIDS-087
516
+ # @design DES-AIDS-073 DES-AIDS-074 DES-AIDS-075
517
+ # CHANGE-004: returns RenderedChart (chart metadata) and widens the
518
+ # bundled-Japanese-font trigger; see the render_chart docstring for detail.
519
+ def render_chart(
520
+ df: pd.DataFrame,
521
+ kind: str = "scatter",
522
+ x: str | None = None,
523
+ y: str | None = None,
524
+ title: str | None = None,
525
+ xlabel: str | None = None,
526
+ ylabel: str | None = None,
527
+ hue: str | None = None,
528
+ legend_title: str | None = None,
529
+ xerr: str | tuple[str, str] | list[str] | None = None,
530
+ yerr: str | tuple[str, str] | list[str] | None = None,
531
+ ) -> RenderedChart:
532
+ """Render ``df`` as ``kind`` chart and return a ``RenderedChart``.
533
+
534
+ ``title``/``xlabel``/``ylabel`` containing non-ASCII characters (e.g.
535
+ Japanese), or plotted data/tick labels/legend/pandas-auto-generated axis
536
+ labels containing Japanese characters, are rendered using a bundled
537
+ Japanese-capable font (REQ-AIDS-046, REQ-AIDS-064) so they display as
538
+ legible glyphs instead of matplotlib default placeholder boxes,
539
+ regardless of fonts installed on the host. The returned ``RenderedChart``
540
+ is a ``bytes`` subclass carrying the actually-rendered title, axis
541
+ labels, legend presence, and any missing-glyph warnings (REQ-AIDS-060),
542
+ so every existing caller using it as plain PNG bytes continues to work
543
+ unmodified.
544
+ """
545
+ if kind not in _SUPPORTED_KINDS:
546
+ raise ValueError(f"Unsupported chart kind: {kind!r}")
547
+
548
+ if any(_contains_non_ascii(text) for text in (title, xlabel, ylabel)) or (
549
+ _plotted_data_contains_japanese(df, kind, x, y, hue=hue, legend_title=legend_title)
550
+ ):
551
+ _ensure_japanese_font()
552
+
553
+ fig, ax = plt.subplots()
554
+ try:
555
+ with warnings.catch_warnings(record=True) as captured:
556
+ warnings.simplefilter("always")
557
+ resolved_xerr = _resolve_error_values(df, xerr)
558
+ resolved_yerr = _resolve_error_values(df, yerr)
559
+ if kind == "box":
560
+ _plot_box_chart(df, x=x, y=y, ax=ax)
561
+ elif kind == "heatmap":
562
+ _plot_heatmap(df, x=x, y=y, ax=ax)
563
+ elif hue is not None:
564
+ _plot_with_hue(
565
+ df,
566
+ kind=kind,
567
+ x=x,
568
+ y=y,
569
+ hue=hue,
570
+ ax=ax,
571
+ xerr=xerr,
572
+ yerr=yerr,
573
+ )
574
+ else:
575
+ _plot_with_existing_paths(
576
+ df,
577
+ kind=kind,
578
+ x=x,
579
+ y=y,
580
+ ax=ax,
581
+ xerr=resolved_xerr,
582
+ yerr=resolved_yerr,
583
+ )
584
+ _apply_legend_title(ax, hue=hue, legend_title=legend_title)
585
+ if title is not None:
586
+ ax.set_title(title)
587
+ if xlabel is not None:
588
+ ax.set_xlabel(xlabel)
589
+ if ylabel is not None:
590
+ ax.set_ylabel(ylabel)
591
+ # GitHub #31: reposition/pad title, axis labels, and tick labels
592
+ # to fit within the saved canvas instead of being clipped.
593
+ # Applied on every call (not gated by a label-length heuristic)
594
+ # so behavior is deterministic. Runs after text is set
595
+ # (tight_layout measures already-rendered text metrics) and
596
+ # after font configuration above.
597
+ try:
598
+ fig.tight_layout()
599
+ except Exception: # noqa: BLE001 - backend/projection-specific matplotlib failure
600
+ # Some axes projections (e.g. 3D) raise from tight_layout();
601
+ # constrained_layout is a safe fallback that still
602
+ # repositions text to avoid clipping.
603
+ fig.set_layout_engine("constrained")
604
+ buffer = io.BytesIO()
605
+ fig.savefig(buffer, format="png", bbox_inches="tight")
606
+
607
+ missing_glyphs: list[str] = []
608
+ for captured_warning in captured:
609
+ match = _MISSING_GLYPH_RE.search(str(captured_warning.message))
610
+ if match:
611
+ codepoint = match.group(1)
612
+ if codepoint not in missing_glyphs:
613
+ missing_glyphs.append(codepoint)
614
+ else:
615
+ warnings.warn_explicit(
616
+ captured_warning.message,
617
+ captured_warning.category,
618
+ captured_warning.filename,
619
+ captured_warning.lineno,
620
+ )
621
+
622
+ metadata = chart_metadata_from_figure(fig, missing_glyphs=tuple(missing_glyphs))
623
+ return RenderedChart(buffer.getvalue(), chart_metadata=metadata)
624
+ finally:
625
+ plt.close(fig)
626
+
627
+
628
+ # @id CODE-AIDS-091
629
+ # @implements REQ-AIDS-071
630
+ # @design DES-AIDS-059
631
+ def _chart_metadata_dict(chart_metadata: ChartMetadata) -> dict:
632
+ """Build the JSON-serializable metadata dict shared by
633
+ ``build_image_output`` (output-level) and ``record_chart`` (cell-level)
634
+ for a rendered chart's authoring metadata (DES-AIDS-059)."""
635
+ return {
636
+ "title": chart_metadata.title,
637
+ "xlabel": chart_metadata.xlabel,
638
+ "ylabel": chart_metadata.ylabel,
639
+ "legend": chart_metadata.legend,
640
+ "legend_title": chart_metadata.legend_title,
641
+ "missing_glyphs": list(chart_metadata.missing_glyphs),
642
+ }
643
+
644
+
645
+ # @id CODE-AIDS-034
646
+ # @implements REQ-AIDS-007 REQ-AIDS-071
647
+ # @design DES-AIDS-009 DES-AIDS-059
648
+ def build_image_output(png_bytes: bytes) -> nbformat.NotebookNode:
649
+ """Wrap PNG bytes as an nbformat execute_result output with image/png data.
650
+
651
+ When ``png_bytes`` is a ``RenderedChart`` (the result of
652
+ ``render_chart``), its ``chart_metadata`` is additionally persisted into
653
+ the output's own ``metadata["chart"]`` mapping (REQ-AIDS-071), so a
654
+ single image output is independently auditable even outside
655
+ ``record_chart``'s cell-level metadata. Plain ``bytes`` leave
656
+ ``metadata`` empty, preserving prior behavior.
657
+ """
658
+ encoded = base64.b64encode(png_bytes).decode("ascii")
659
+ metadata = {}
660
+ if isinstance(png_bytes, RenderedChart):
661
+ metadata["chart"] = _chart_metadata_dict(png_bytes.chart_metadata)
662
+ return nbformat.v4.new_output(
663
+ "execute_result",
664
+ data={"image/png": encoded, "text/plain": "<matplotlib chart>"},
665
+ metadata=metadata,
666
+ )
667
+
668
+
669
+ # @id CODE-AIDS-035
670
+ # @implements REQ-AIDS-007 REQ-AIDS-040 REQ-AIDS-061
671
+ # @design DES-AIDS-009 DES-AIDS-028 DES-AIDS-049
672
+ def record_chart(handle: ProjectHandle, code: str, png_bytes: bytes) -> int:
673
+ """Persist a rendered chart as an executed code cell in the project notebook.
674
+
675
+ Mirrors mcp_gateway.run_and_record's ergonomics for chart/image outputs so
676
+ callers don't need to hand-roll enqueue_write boilerplate. Returns the
677
+ stamped execution_count of the new cell.
678
+
679
+ ``code`` is never executed against the live Jupyter kernel (REQ-AIDS-040):
680
+ rendering happens locally via ``render_chart`` and ``code`` is stored only
681
+ as a human-readable record of how the chart was produced. Callers must
682
+ ensure ``code`` references only variables already established by a prior
683
+ ``mcp_gateway.run_and_record`` call, so the notebook stays consistent if a
684
+ human re-runs it top-to-bottom against the live kernel later.
685
+
686
+ When ``png_bytes`` is a ``RenderedChart`` (the result of ``render_chart``),
687
+ its ``chart_metadata`` is persisted into the new cell's
688
+ ``metadata["chart"]`` mapping so ``audit_notebook(..., visual_audit=True)``
689
+ can audit it without re-rendering (REQ-AIDS-061). Plain ``bytes`` (e.g.
690
+ read from a file, not produced by ``render_chart``) leave
691
+ ``metadata["chart"]`` unset, preserving the existing "unaudited" finding.
692
+ """
693
+ stamped_count: dict[str, int] = {}
694
+
695
+ def add_cell(nb):
696
+ execution_count = next_execution_count(nb)
697
+ stamped_count["value"] = execution_count
698
+ cell = nbformat.v4.new_code_cell(code)
699
+ cell["execution_count"] = execution_count
700
+ output = build_image_output(png_bytes)
701
+ output["execution_count"] = execution_count
702
+ cell["outputs"] = [output]
703
+ if isinstance(png_bytes, RenderedChart):
704
+ cell["metadata"]["chart"] = _chart_metadata_dict(png_bytes.chart_metadata)
705
+ nb.cells.append(cell)
706
+
707
+ enqueue_write(handle, add_cell)
708
+ return stamped_count["value"]
@@ -0,0 +1 @@
1
+ """AI Genomics Scientist skill package."""