scitex 2.14.0__py3-none-any.whl → 2.15.2__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.
- scitex/__init__.py +71 -17
- scitex/_env_loader.py +156 -0
- scitex/_mcp_resources/__init__.py +37 -0
- scitex/_mcp_resources/_cheatsheet.py +135 -0
- scitex/_mcp_resources/_figrecipe.py +138 -0
- scitex/_mcp_resources/_formats.py +102 -0
- scitex/_mcp_resources/_modules.py +337 -0
- scitex/_mcp_resources/_session.py +149 -0
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/audio.py +66 -0
- scitex/_mcp_tools/diagram.py +11 -95
- scitex/_mcp_tools/introspect.py +210 -0
- scitex/_mcp_tools/plt.py +260 -305
- scitex/_mcp_tools/scholar.py +74 -0
- scitex/_mcp_tools/social.py +244 -0
- scitex/_mcp_tools/template.py +24 -0
- scitex/_mcp_tools/writer.py +21 -204
- scitex/ai/_gen_ai/_PARAMS.py +10 -7
- scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
- scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
- scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
- scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
- scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
- scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
- scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
- scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
- scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
- scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
- scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
- scitex/audio/README.md +40 -36
- scitex/audio/__init__.py +129 -61
- scitex/audio/_branding.py +185 -0
- scitex/audio/_mcp/__init__.py +32 -0
- scitex/audio/_mcp/handlers.py +59 -6
- scitex/audio/_mcp/speak_handlers.py +238 -0
- scitex/audio/_relay.py +225 -0
- scitex/audio/_tts.py +18 -10
- scitex/audio/engines/base.py +17 -10
- scitex/audio/engines/elevenlabs_engine.py +7 -2
- scitex/audio/mcp_server.py +228 -75
- scitex/canvas/README.md +1 -1
- scitex/canvas/editor/_dearpygui/__init__.py +25 -0
- scitex/canvas/editor/_dearpygui/_editor.py +147 -0
- scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
- scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
- scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
- scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
- scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
- scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
- scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
- scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
- scitex/canvas/editor/_dearpygui/_selection.py +295 -0
- scitex/canvas/editor/_dearpygui/_state.py +93 -0
- scitex/canvas/editor/_dearpygui/_utils.py +61 -0
- scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
- scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
- scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
- scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
- scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
- scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
- scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
- scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
- scitex/canvas/editor/flask_editor/_core.py +25 -1684
- scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
- scitex/cli/__init__.py +38 -43
- scitex/cli/audio.py +76 -27
- scitex/cli/capture.py +13 -20
- scitex/cli/introspect.py +481 -0
- scitex/cli/main.py +200 -109
- scitex/cli/mcp.py +60 -34
- scitex/cli/plt.py +357 -0
- scitex/cli/repro.py +15 -8
- scitex/cli/resource.py +15 -8
- scitex/cli/scholar/__init__.py +23 -8
- scitex/cli/scholar/_crossref_scitex.py +296 -0
- scitex/cli/scholar/_fetch.py +25 -3
- scitex/cli/social.py +314 -0
- scitex/cli/stats.py +15 -8
- scitex/cli/template.py +129 -12
- scitex/cli/tex.py +15 -8
- scitex/cli/writer.py +132 -8
- scitex/cloud/__init__.py +41 -2
- scitex/config/README.md +1 -1
- scitex/config/__init__.py +16 -2
- scitex/config/_env_registry.py +256 -0
- scitex/context/__init__.py +22 -0
- scitex/dev/__init__.py +20 -1
- scitex/diagram/__init__.py +42 -19
- scitex/diagram/mcp_server.py +13 -125
- scitex/gen/__init__.py +50 -14
- scitex/gen/_list_packages.py +4 -4
- scitex/introspect/__init__.py +82 -0
- scitex/introspect/_call_graph.py +303 -0
- scitex/introspect/_class_hierarchy.py +163 -0
- scitex/introspect/_core.py +41 -0
- scitex/introspect/_docstring.py +131 -0
- scitex/introspect/_examples.py +113 -0
- scitex/introspect/_imports.py +271 -0
- scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
- scitex/introspect/_mcp/__init__.py +41 -0
- scitex/introspect/_mcp/handlers.py +233 -0
- scitex/introspect/_members.py +155 -0
- scitex/introspect/_resolve.py +89 -0
- scitex/introspect/_signature.py +131 -0
- scitex/introspect/_source.py +80 -0
- scitex/introspect/_type_hints.py +172 -0
- scitex/io/_save.py +1 -2
- scitex/io/bundle/README.md +1 -1
- scitex/logging/_formatters.py +19 -9
- scitex/mcp_server.py +98 -5
- scitex/os/__init__.py +4 -0
- scitex/{gen → os}/_check_host.py +4 -5
- scitex/plt/__init__.py +245 -550
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
- scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/plt/gallery/README.md +1 -1
- scitex/plt/utils/_hitmap/__init__.py +82 -0
- scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
- scitex/plt/utils/_hitmap/_color_application.py +346 -0
- scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
- scitex/plt/utils/_hitmap/_constants.py +40 -0
- scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
- scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
- scitex/plt/utils/_hitmap/_query.py +113 -0
- scitex/plt/utils/_hitmap.py +46 -1616
- scitex/plt/utils/_metadata/__init__.py +80 -0
- scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
- scitex/plt/utils/_metadata/_artists/_base.py +195 -0
- scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
- scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
- scitex/plt/utils/_metadata/_artists/_images.py +80 -0
- scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
- scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
- scitex/plt/utils/_metadata/_artists/_text.py +106 -0
- scitex/plt/utils/_metadata/_csv.py +416 -0
- scitex/plt/utils/_metadata/_detect.py +225 -0
- scitex/plt/utils/_metadata/_legend.py +127 -0
- scitex/plt/utils/_metadata/_rounding.py +117 -0
- scitex/plt/utils/_metadata/_verification.py +202 -0
- scitex/schema/README.md +1 -1
- scitex/scholar/__init__.py +8 -0
- scitex/scholar/_mcp/crossref_handlers.py +265 -0
- scitex/scholar/core/Scholar.py +63 -1700
- scitex/scholar/core/_mixins/__init__.py +36 -0
- scitex/scholar/core/_mixins/_enrichers.py +270 -0
- scitex/scholar/core/_mixins/_library_handlers.py +100 -0
- scitex/scholar/core/_mixins/_loaders.py +103 -0
- scitex/scholar/core/_mixins/_pdf_download.py +375 -0
- scitex/scholar/core/_mixins/_pipeline.py +312 -0
- scitex/scholar/core/_mixins/_project_handlers.py +125 -0
- scitex/scholar/core/_mixins/_savers.py +69 -0
- scitex/scholar/core/_mixins/_search.py +103 -0
- scitex/scholar/core/_mixins/_services.py +88 -0
- scitex/scholar/core/_mixins/_url_finding.py +105 -0
- scitex/scholar/crossref_scitex.py +367 -0
- scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/scholar/examples/00_run_all.sh +120 -0
- scitex/scholar/jobs/_executors.py +27 -3
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
- scitex/scholar/pdf_download/_cli.py +154 -0
- scitex/scholar/pdf_download/strategies/__init__.py +11 -8
- scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
- scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
- scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
- scitex/scholar/pipelines/_single_steps.py +71 -36
- scitex/scholar/storage/_LibraryManager.py +97 -1695
- scitex/scholar/storage/_mixins/__init__.py +30 -0
- scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
- scitex/scholar/storage/_mixins/_library_operations.py +218 -0
- scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
- scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
- scitex/scholar/storage/_mixins/_resolution.py +376 -0
- scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
- scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
- scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
- scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
- scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
- scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
- scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
- scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
- scitex/security/README.md +3 -3
- scitex/session/README.md +1 -1
- scitex/session/__init__.py +26 -7
- scitex/session/_decorator.py +1 -1
- scitex/sh/README.md +1 -1
- scitex/sh/__init__.py +7 -4
- scitex/social/__init__.py +155 -0
- scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
- scitex/stats/_mcp/_handlers/__init__.py +31 -0
- scitex/stats/_mcp/_handlers/_corrections.py +113 -0
- scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
- scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
- scitex/stats/_mcp/_handlers/_format.py +94 -0
- scitex/stats/_mcp/_handlers/_normality.py +110 -0
- scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
- scitex/stats/_mcp/_handlers/_power.py +247 -0
- scitex/stats/_mcp/_handlers/_recommend.py +102 -0
- scitex/stats/_mcp/_handlers/_run_test.py +279 -0
- scitex/stats/_mcp/_handlers/_stars.py +48 -0
- scitex/stats/_mcp/handlers.py +19 -1171
- scitex/stats/auto/_stat_style.py +175 -0
- scitex/stats/auto/_style_definitions.py +411 -0
- scitex/stats/auto/_styles.py +22 -620
- scitex/stats/descriptive/__init__.py +11 -8
- scitex/stats/descriptive/_ci.py +39 -0
- scitex/stats/power/_power.py +15 -4
- scitex/str/__init__.py +2 -1
- scitex/str/_title_case.py +63 -0
- scitex/template/README.md +1 -1
- scitex/template/__init__.py +25 -10
- scitex/template/_code_templates.py +147 -0
- scitex/template/_mcp/handlers.py +81 -0
- scitex/template/_mcp/tool_schemas.py +55 -0
- scitex/template/_templates/__init__.py +51 -0
- scitex/template/_templates/audio.py +233 -0
- scitex/template/_templates/canvas.py +312 -0
- scitex/template/_templates/capture.py +268 -0
- scitex/template/_templates/config.py +43 -0
- scitex/template/_templates/diagram.py +294 -0
- scitex/template/_templates/io.py +107 -0
- scitex/template/_templates/module.py +53 -0
- scitex/template/_templates/plt.py +202 -0
- scitex/template/_templates/scholar.py +267 -0
- scitex/template/_templates/session.py +130 -0
- scitex/template/_templates/session_minimal.py +43 -0
- scitex/template/_templates/session_plot.py +67 -0
- scitex/template/_templates/session_stats.py +77 -0
- scitex/template/_templates/stats.py +323 -0
- scitex/template/_templates/writer.py +296 -0
- scitex/template/clone_writer_directory.py +5 -5
- scitex/ui/_backends/_email.py +10 -2
- scitex/ui/_backends/_webhook.py +5 -1
- scitex/web/_search_pubmed.py +10 -6
- scitex/writer/README.md +1 -1
- scitex/writer/_mcp/handlers.py +11 -744
- scitex/writer/_mcp/tool_schemas.py +5 -335
- scitex-2.15.2.dist-info/METADATA +648 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
- scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
- scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
- scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
- scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
- scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
- scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
- scitex/diagram/_compile.py +0 -312
- scitex/diagram/_diagram.py +0 -355
- scitex/diagram/_mcp/__init__.py +0 -4
- scitex/diagram/_mcp/handlers.py +0 -400
- scitex/diagram/_mcp/tool_schemas.py +0 -157
- scitex/diagram/_presets.py +0 -173
- scitex/diagram/_schema.py +0 -182
- scitex/diagram/_split.py +0 -278
- scitex/gen/_ci.py +0 -12
- scitex/gen/_title_case.py +0 -89
- scitex/plt/_mcp/__init__.py +0 -4
- scitex/plt/_mcp/_handlers_annotation.py +0 -102
- scitex/plt/_mcp/_handlers_figure.py +0 -195
- scitex/plt/_mcp/_handlers_plot.py +0 -252
- scitex/plt/_mcp/_handlers_style.py +0 -219
- scitex/plt/_mcp/handlers.py +0 -74
- scitex/plt/_mcp/tool_schemas.py +0 -497
- scitex/plt/mcp_server.py +0 -231
- scitex/scholar/data/.gitkeep +0 -0
- scitex/scholar/data/README.md +0 -44
- scitex/scholar/data/bib_files/bibliography.bib +0 -1952
- scitex/scholar/data/bib_files/neurovista.bib +0 -277
- scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
- scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
- scitex/scholar/data/bib_files/openaccess.bib +0 -89
- scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
- scitex/scholar/data/bib_files/pac.bib +0 -698
- scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
- scitex/scholar/data/bib_files/pac_processed.bib +0 -0
- scitex/scholar/data/bib_files/pac_titles.txt +0 -75
- scitex/scholar/data/bib_files/paywalled.bib +0 -98
- scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
- scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
- scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
- scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
- scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
- scitex/scholar/data/bib_files/test_seizure.bib +0 -46
- scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
- scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
- scitex/scholar/data/impact_factor.db +0 -0
- scitex/scholar/examples/SUGGESTIONS.md +0 -865
- scitex/scholar/examples/dev.py +0 -38
- scitex-2.14.0.dist-info/METADATA +0 -1238
- /scitex/{gen → context}/_detect_environment.py +0 -0
- /scitex/{gen → context}/_get_notebook_path.py +0 -0
- /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
- {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
scitex/diagram/_compile.py
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: 2025-12-15
|
|
4
|
-
# Author: ywatanabe / Claude
|
|
5
|
-
# File: scitex/diagram/_compile.py
|
|
6
|
-
|
|
7
|
-
"""
|
|
8
|
-
Compilers from DiagramSpec to backend formats (Mermaid, Graphviz).
|
|
9
|
-
|
|
10
|
-
The compiler applies paper constraints to generate backend-specific
|
|
11
|
-
layout directives. This is where domain knowledge about "good paper figures"
|
|
12
|
-
gets encoded.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
import json
|
|
16
|
-
from typing import Optional
|
|
17
|
-
from scitex.diagram._schema import (
|
|
18
|
-
DiagramSpec, DiagramType, ColumnLayout, SpacingLevel, PaperMode
|
|
19
|
-
)
|
|
20
|
-
from scitex.diagram._presets import get_preset, DiagramPreset
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def compile_to_mermaid(
|
|
24
|
-
spec: DiagramSpec,
|
|
25
|
-
preset: Optional[DiagramPreset] = None
|
|
26
|
-
) -> str:
|
|
27
|
-
"""
|
|
28
|
-
Compile DiagramSpec to Mermaid format with paper-optimized settings.
|
|
29
|
-
|
|
30
|
-
Parameters
|
|
31
|
-
----------
|
|
32
|
-
spec : DiagramSpec
|
|
33
|
-
The semantic diagram specification.
|
|
34
|
-
preset : DiagramPreset, optional
|
|
35
|
-
Override preset (default: inferred from spec.type).
|
|
36
|
-
|
|
37
|
-
Returns
|
|
38
|
-
-------
|
|
39
|
-
str
|
|
40
|
-
Mermaid diagram source code.
|
|
41
|
-
"""
|
|
42
|
-
if preset is None:
|
|
43
|
-
preset = get_preset(spec.type.value)
|
|
44
|
-
|
|
45
|
-
lines = []
|
|
46
|
-
|
|
47
|
-
# Theme initialization
|
|
48
|
-
theme_vars = {**preset.mermaid_theme, **spec.theme}
|
|
49
|
-
theme_json = json.dumps({"theme": "base", "themeVariables": theme_vars})
|
|
50
|
-
lines.append(f"%%{{init: {theme_json}}}%%")
|
|
51
|
-
|
|
52
|
-
# Determine direction based on paper constraints
|
|
53
|
-
direction = preset.mermaid_direction
|
|
54
|
-
if spec.paper.reading_direction == "top_to_bottom":
|
|
55
|
-
direction = "TB"
|
|
56
|
-
elif spec.paper.column == ColumnLayout.DOUBLE:
|
|
57
|
-
# Double column prefers vertical to save horizontal space
|
|
58
|
-
direction = "TB"
|
|
59
|
-
|
|
60
|
-
lines.append(f"graph {direction}")
|
|
61
|
-
|
|
62
|
-
# Build node ID to spec mapping
|
|
63
|
-
node_map = {n.id: n for n in spec.nodes}
|
|
64
|
-
|
|
65
|
-
# Generate subgraphs for groups
|
|
66
|
-
indent = " "
|
|
67
|
-
for group_name, group_nodes in spec.layout.groups.items():
|
|
68
|
-
lines.append(f'{indent}subgraph {_sanitize_id(group_name)}["{group_name}"]')
|
|
69
|
-
for node_id in group_nodes:
|
|
70
|
-
if node_id in node_map:
|
|
71
|
-
node = node_map[node_id]
|
|
72
|
-
lines.append(f"{indent}{indent}{_mermaid_node(node, preset)}")
|
|
73
|
-
lines.append(f"{indent}end")
|
|
74
|
-
|
|
75
|
-
# Generate standalone nodes (not in any group)
|
|
76
|
-
grouped_nodes = set()
|
|
77
|
-
for group_nodes in spec.layout.groups.values():
|
|
78
|
-
grouped_nodes.update(group_nodes)
|
|
79
|
-
|
|
80
|
-
for node in spec.nodes:
|
|
81
|
-
if node.id not in grouped_nodes:
|
|
82
|
-
lines.append(f"{indent}{_mermaid_node(node, preset)}")
|
|
83
|
-
|
|
84
|
-
# Generate edges
|
|
85
|
-
for edge in spec.edges:
|
|
86
|
-
edge_str = _mermaid_edge(edge)
|
|
87
|
-
lines.append(f"{indent}{edge_str}")
|
|
88
|
-
|
|
89
|
-
# Generate styles for emphasized nodes
|
|
90
|
-
for node in spec.nodes:
|
|
91
|
-
if node.emphasis != "normal" or node.id in spec.paper.emphasize:
|
|
92
|
-
emphasis = "primary" if node.id in spec.paper.emphasize else node.emphasis
|
|
93
|
-
style = preset.emphasis_styles.get(emphasis, {})
|
|
94
|
-
if style:
|
|
95
|
-
style_parts = [f"{k}:{v}" for k, v in style.items()]
|
|
96
|
-
lines.append(f"{indent}style {_sanitize_id(node.id)} {','.join(style_parts)}")
|
|
97
|
-
|
|
98
|
-
return "\n".join(lines)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def compile_to_graphviz(
|
|
102
|
-
spec: DiagramSpec,
|
|
103
|
-
preset: Optional[DiagramPreset] = None
|
|
104
|
-
) -> str:
|
|
105
|
-
"""
|
|
106
|
-
Compile DiagramSpec to Graphviz DOT format.
|
|
107
|
-
|
|
108
|
-
Parameters
|
|
109
|
-
----------
|
|
110
|
-
spec : DiagramSpec
|
|
111
|
-
The semantic diagram specification.
|
|
112
|
-
preset : DiagramPreset, optional
|
|
113
|
-
Override preset.
|
|
114
|
-
|
|
115
|
-
Returns
|
|
116
|
-
-------
|
|
117
|
-
str
|
|
118
|
-
Graphviz DOT source code.
|
|
119
|
-
"""
|
|
120
|
-
if preset is None:
|
|
121
|
-
preset = get_preset(spec.type.value)
|
|
122
|
-
|
|
123
|
-
is_publication = spec.paper.mode == PaperMode.PUBLICATION
|
|
124
|
-
lines = []
|
|
125
|
-
|
|
126
|
-
# Determine direction
|
|
127
|
-
rankdir = preset.graphviz_rankdir
|
|
128
|
-
if spec.paper.reading_direction == "top_to_bottom":
|
|
129
|
-
rankdir = "TB"
|
|
130
|
-
elif spec.paper.column == ColumnLayout.DOUBLE:
|
|
131
|
-
rankdir = "TB"
|
|
132
|
-
|
|
133
|
-
# Get spacing - publication mode uses tight spacing
|
|
134
|
-
if is_publication:
|
|
135
|
-
spacing = preset.spacing_map.get("tight", {})
|
|
136
|
-
else:
|
|
137
|
-
spacing = preset.spacing_map.get(spec.layout.layer_gap.value, {})
|
|
138
|
-
ranksep = spacing.get("ranksep", preset.graphviz_ranksep)
|
|
139
|
-
nodesep = spacing.get("nodesep", preset.graphviz_nodesep)
|
|
140
|
-
|
|
141
|
-
lines.append("digraph G {")
|
|
142
|
-
lines.append(f" rankdir={rankdir};")
|
|
143
|
-
lines.append(f" ranksep={ranksep};")
|
|
144
|
-
lines.append(f" nodesep={nodesep};")
|
|
145
|
-
lines.append(" splines=ortho;") # Orthogonal edges for cleaner look
|
|
146
|
-
lines.append(' node [fontname="Helvetica", fontsize=10];')
|
|
147
|
-
lines.append(' edge [fontname="Helvetica", fontsize=9];')
|
|
148
|
-
lines.append("")
|
|
149
|
-
|
|
150
|
-
# Node map
|
|
151
|
-
node_map = {n.id: n for n in spec.nodes}
|
|
152
|
-
|
|
153
|
-
# Build return edges set for publication mode
|
|
154
|
-
return_edge_set = set()
|
|
155
|
-
for e in spec.paper.return_edges:
|
|
156
|
-
if len(e) >= 2:
|
|
157
|
-
return_edge_set.add((e[0], e[1]))
|
|
158
|
-
|
|
159
|
-
# Generate subgraphs (without clusters for tighter layout in publication)
|
|
160
|
-
if is_publication and spec.layout.layers:
|
|
161
|
-
# In publication mode with layers, skip clusters - use rank=same instead
|
|
162
|
-
for node in spec.nodes:
|
|
163
|
-
lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
|
|
164
|
-
else:
|
|
165
|
-
# Draft mode: use clusters for visual grouping
|
|
166
|
-
cluster_idx = 0
|
|
167
|
-
for group_name, group_nodes in spec.layout.groups.items():
|
|
168
|
-
lines.append(f' subgraph cluster_{cluster_idx} {{')
|
|
169
|
-
lines.append(f' label="{group_name}";')
|
|
170
|
-
for node_id in group_nodes:
|
|
171
|
-
if node_id in node_map:
|
|
172
|
-
node = node_map[node_id]
|
|
173
|
-
lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
|
|
174
|
-
lines.append(" }")
|
|
175
|
-
cluster_idx += 1
|
|
176
|
-
|
|
177
|
-
# Standalone nodes
|
|
178
|
-
grouped_nodes = set()
|
|
179
|
-
for group_nodes in spec.layout.groups.values():
|
|
180
|
-
grouped_nodes.update(group_nodes)
|
|
181
|
-
|
|
182
|
-
for node in spec.nodes:
|
|
183
|
-
if node.id not in grouped_nodes:
|
|
184
|
-
lines.append(f" {_graphviz_node(node, preset, spec.paper.emphasize)}")
|
|
185
|
-
|
|
186
|
-
lines.append("")
|
|
187
|
-
|
|
188
|
-
# Rank constraints from layers (CRITICAL for minimizing whitespace)
|
|
189
|
-
for layer in spec.layout.layers:
|
|
190
|
-
if layer:
|
|
191
|
-
node_ids = "; ".join(_sanitize_id(n) for n in layer)
|
|
192
|
-
lines.append(f" {{ rank=same; {node_ids}; }}")
|
|
193
|
-
|
|
194
|
-
lines.append("")
|
|
195
|
-
|
|
196
|
-
# Edges - handle return edges in publication mode
|
|
197
|
-
for edge in spec.edges:
|
|
198
|
-
edge_key = (edge.source, edge.target)
|
|
199
|
-
if is_publication and edge_key in return_edge_set:
|
|
200
|
-
# Make return edges invisible in publication mode
|
|
201
|
-
lines.append(f" {_graphviz_edge_with_style(edge, invisible=True)}")
|
|
202
|
-
else:
|
|
203
|
-
lines.append(f" {_graphviz_edge(edge)}")
|
|
204
|
-
|
|
205
|
-
lines.append("}")
|
|
206
|
-
|
|
207
|
-
return "\n".join(lines)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _sanitize_id(s: str) -> str:
|
|
211
|
-
"""Make string safe for use as node ID."""
|
|
212
|
-
import re
|
|
213
|
-
# Remove or replace problematic characters for Mermaid/Graphviz
|
|
214
|
-
s = re.sub(r'[^\w]', '_', s) # Replace non-word chars with _
|
|
215
|
-
s = re.sub(r'_+', '_', s) # Collapse multiple underscores
|
|
216
|
-
s = s.strip('_') # Remove leading/trailing underscores
|
|
217
|
-
return s or "node"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _mermaid_node(node, preset: DiagramPreset) -> str:
|
|
221
|
-
"""Generate Mermaid node definition."""
|
|
222
|
-
shape_template = preset.mermaid_shapes.get(node.shape, '["__LABEL__"]')
|
|
223
|
-
shape_str = shape_template.replace("__LABEL__", node.label)
|
|
224
|
-
return f"{_sanitize_id(node.id)}{shape_str}"
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def _mermaid_edge(edge) -> str:
|
|
228
|
-
"""Generate Mermaid edge definition."""
|
|
229
|
-
arrow = "-->" if edge.arrow == "normal" else "---"
|
|
230
|
-
if edge.style == "dashed":
|
|
231
|
-
arrow = "-.->" if edge.arrow == "normal" else "-.-"
|
|
232
|
-
elif edge.style == "dotted":
|
|
233
|
-
arrow = "..>" if edge.arrow == "normal" else "..."
|
|
234
|
-
|
|
235
|
-
src = _sanitize_id(edge.source)
|
|
236
|
-
tgt = _sanitize_id(edge.target)
|
|
237
|
-
|
|
238
|
-
if edge.label:
|
|
239
|
-
return f'{src} {arrow}|"{edge.label}"| {tgt}'
|
|
240
|
-
return f"{src} {arrow} {tgt}"
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def _graphviz_node(node, preset: DiagramPreset, emphasize: list) -> str:
|
|
244
|
-
"""Generate Graphviz node definition."""
|
|
245
|
-
shape = preset.graphviz_shapes.get(node.shape, "box")
|
|
246
|
-
|
|
247
|
-
# Get emphasis style
|
|
248
|
-
emphasis_key = "primary" if node.id in emphasize else node.emphasis
|
|
249
|
-
style = preset.emphasis_styles.get(emphasis_key, {})
|
|
250
|
-
|
|
251
|
-
attrs = [f'label="{node.label}"', f'shape={shape}']
|
|
252
|
-
|
|
253
|
-
# Collect style values (filled, rounded, etc.) - combine with comma
|
|
254
|
-
styles = []
|
|
255
|
-
if style.get("fill"):
|
|
256
|
-
attrs.append(f'fillcolor="{style["fill"]}"')
|
|
257
|
-
styles.append("filled")
|
|
258
|
-
if style.get("stroke"):
|
|
259
|
-
attrs.append(f'color="{style["stroke"]}"')
|
|
260
|
-
if node.shape == "rounded":
|
|
261
|
-
styles.append("rounded")
|
|
262
|
-
|
|
263
|
-
# Output style once with comma-separated values
|
|
264
|
-
if styles:
|
|
265
|
-
attrs.append(f'style="{",".join(styles)}"')
|
|
266
|
-
|
|
267
|
-
return f'{_sanitize_id(node.id)} [{", ".join(attrs)}];'
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
def _graphviz_edge(edge) -> str:
|
|
271
|
-
"""Generate Graphviz edge definition."""
|
|
272
|
-
src = _sanitize_id(edge.source)
|
|
273
|
-
tgt = _sanitize_id(edge.target)
|
|
274
|
-
|
|
275
|
-
attrs = []
|
|
276
|
-
if edge.label:
|
|
277
|
-
attrs.append(f'label="{edge.label}"')
|
|
278
|
-
if edge.style == "dashed":
|
|
279
|
-
attrs.append("style=dashed")
|
|
280
|
-
elif edge.style == "dotted":
|
|
281
|
-
attrs.append("style=dotted")
|
|
282
|
-
if edge.arrow == "none":
|
|
283
|
-
attrs.append("arrowhead=none")
|
|
284
|
-
|
|
285
|
-
if attrs:
|
|
286
|
-
return f'{src} -> {tgt} [{", ".join(attrs)}];'
|
|
287
|
-
return f"{src} -> {tgt};"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _graphviz_edge_with_style(edge, invisible: bool = False) -> str:
|
|
291
|
-
"""Generate Graphviz edge with optional invisible style."""
|
|
292
|
-
src = _sanitize_id(edge.source)
|
|
293
|
-
tgt = _sanitize_id(edge.target)
|
|
294
|
-
|
|
295
|
-
attrs = []
|
|
296
|
-
if invisible:
|
|
297
|
-
attrs.append("style=invis")
|
|
298
|
-
# Invisible edges still constrain layout
|
|
299
|
-
attrs.append("constraint=true")
|
|
300
|
-
else:
|
|
301
|
-
if edge.label:
|
|
302
|
-
attrs.append(f'label="{edge.label}"')
|
|
303
|
-
if edge.style == "dashed":
|
|
304
|
-
attrs.append("style=dashed")
|
|
305
|
-
elif edge.style == "dotted":
|
|
306
|
-
attrs.append("style=dotted")
|
|
307
|
-
if edge.arrow == "none":
|
|
308
|
-
attrs.append("arrowhead=none")
|
|
309
|
-
|
|
310
|
-
if attrs:
|
|
311
|
-
return f'{src} -> {tgt} [{", ".join(attrs)}];'
|
|
312
|
-
return f"{src} -> {tgt};"
|
scitex/diagram/_diagram.py
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# Timestamp: 2025-12-15
|
|
4
|
-
# Author: ywatanabe / Claude
|
|
5
|
-
# File: scitex/diagram/_diagram.py
|
|
6
|
-
|
|
7
|
-
"""
|
|
8
|
-
Main Diagram class - the user-facing API.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Optional, Union, List
|
|
13
|
-
import yaml
|
|
14
|
-
import re
|
|
15
|
-
|
|
16
|
-
from scitex.diagram._schema import DiagramSpec, DiagramType, NodeSpec, EdgeSpec, PaperMode
|
|
17
|
-
from scitex.diagram._compile import compile_to_mermaid, compile_to_graphviz
|
|
18
|
-
from scitex.diagram._presets import get_preset
|
|
19
|
-
from scitex.diagram._split import split_diagram, SplitConfig, SplitStrategy, SplitResult
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class Diagram:
|
|
23
|
-
"""
|
|
24
|
-
Paper-optimized diagram with semantic specification.
|
|
25
|
-
|
|
26
|
-
This class provides the main interface for creating diagrams
|
|
27
|
-
that compile to Mermaid or Graphviz with paper-appropriate
|
|
28
|
-
layout constraints.
|
|
29
|
-
|
|
30
|
-
Examples
|
|
31
|
-
--------
|
|
32
|
-
>>> # From YAML spec
|
|
33
|
-
>>> d = Diagram.from_yaml("workflow.diagram.yaml")
|
|
34
|
-
>>> d.to_mermaid("workflow.mmd")
|
|
35
|
-
|
|
36
|
-
>>> # From existing Mermaid (parse and enhance)
|
|
37
|
-
>>> d = Diagram.from_mermaid("existing.mmd", diagram_type="workflow")
|
|
38
|
-
>>> d.spec.paper.column = "double"
|
|
39
|
-
>>> d.to_mermaid("enhanced.mmd")
|
|
40
|
-
|
|
41
|
-
>>> # Programmatic creation
|
|
42
|
-
>>> d = Diagram(type="pipeline")
|
|
43
|
-
>>> d.add_node("input", "Raw Data")
|
|
44
|
-
>>> d.add_node("process", "Transform", emphasis="primary")
|
|
45
|
-
>>> d.add_node("output", "Results")
|
|
46
|
-
>>> d.add_edge("input", "process")
|
|
47
|
-
>>> d.add_edge("process", "output")
|
|
48
|
-
>>> print(d.to_mermaid())
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
def __init__(
|
|
52
|
-
self,
|
|
53
|
-
type: str = "workflow",
|
|
54
|
-
title: str = "",
|
|
55
|
-
column: str = "single",
|
|
56
|
-
):
|
|
57
|
-
"""
|
|
58
|
-
Initialize a new diagram.
|
|
59
|
-
|
|
60
|
-
Parameters
|
|
61
|
-
----------
|
|
62
|
-
type : str
|
|
63
|
-
Diagram type: workflow, decision, pipeline, hierarchy.
|
|
64
|
-
title : str
|
|
65
|
-
Diagram title.
|
|
66
|
-
column : str
|
|
67
|
-
Paper column: single or double.
|
|
68
|
-
"""
|
|
69
|
-
self.spec = DiagramSpec(
|
|
70
|
-
type=DiagramType(type),
|
|
71
|
-
title=title,
|
|
72
|
-
)
|
|
73
|
-
self.spec.paper.column = column
|
|
74
|
-
|
|
75
|
-
@classmethod
|
|
76
|
-
def from_yaml(cls, path: Union[str, Path]) -> "Diagram":
|
|
77
|
-
"""
|
|
78
|
-
Load diagram from YAML specification file.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
path : str or Path
|
|
83
|
-
Path to YAML file.
|
|
84
|
-
|
|
85
|
-
Returns
|
|
86
|
-
-------
|
|
87
|
-
Diagram
|
|
88
|
-
Loaded diagram.
|
|
89
|
-
"""
|
|
90
|
-
path = Path(path)
|
|
91
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
92
|
-
data = yaml.safe_load(f)
|
|
93
|
-
|
|
94
|
-
diagram = cls.__new__(cls)
|
|
95
|
-
diagram.spec = DiagramSpec.from_dict(data)
|
|
96
|
-
return diagram
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def from_mermaid(
|
|
100
|
-
cls,
|
|
101
|
-
path: Union[str, Path],
|
|
102
|
-
diagram_type: str = "workflow",
|
|
103
|
-
) -> "Diagram":
|
|
104
|
-
"""
|
|
105
|
-
Parse existing Mermaid file and create enhanced Diagram.
|
|
106
|
-
|
|
107
|
-
This allows upgrading existing Mermaid files with SciTeX
|
|
108
|
-
paper constraints while preserving the original structure.
|
|
109
|
-
|
|
110
|
-
Parameters
|
|
111
|
-
----------
|
|
112
|
-
path : str or Path
|
|
113
|
-
Path to .mmd file.
|
|
114
|
-
diagram_type : str
|
|
115
|
-
Inferred diagram type.
|
|
116
|
-
|
|
117
|
-
Returns
|
|
118
|
-
-------
|
|
119
|
-
Diagram
|
|
120
|
-
Parsed diagram (can be enhanced and re-exported).
|
|
121
|
-
"""
|
|
122
|
-
path = Path(path)
|
|
123
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
124
|
-
content = f.read()
|
|
125
|
-
|
|
126
|
-
diagram = cls(type=diagram_type)
|
|
127
|
-
diagram._parse_mermaid(content)
|
|
128
|
-
return diagram
|
|
129
|
-
|
|
130
|
-
def _parse_mermaid(self, content: str):
|
|
131
|
-
"""Parse Mermaid content to extract structure."""
|
|
132
|
-
# Extract nodes
|
|
133
|
-
# Pattern: ID["Label"] or ID("Label") etc.
|
|
134
|
-
node_pattern = r'(\w+)\s*[\[\(\{<][\"\']?([^"\'\]\)\}>]+)[\"\']?[\]\)\}>]'
|
|
135
|
-
|
|
136
|
-
for match in re.finditer(node_pattern, content):
|
|
137
|
-
node_id = match.group(1)
|
|
138
|
-
label = match.group(2).strip()
|
|
139
|
-
# Skip if it looks like a subgraph name
|
|
140
|
-
if node_id.lower() not in ["subgraph", "end", "style", "graph", "direction"]:
|
|
141
|
-
# Check for duplicates
|
|
142
|
-
existing = [n for n in self.spec.nodes if n.id == node_id]
|
|
143
|
-
if not existing:
|
|
144
|
-
self.spec.nodes.append(NodeSpec(id=node_id, label=label))
|
|
145
|
-
|
|
146
|
-
# Extract edges
|
|
147
|
-
# Pattern: A --> B or A -->|label| B
|
|
148
|
-
edge_pattern = r'(\w+)\s*(-->|-.->|-----|---)\s*(?:\|["\']?([^|"\']+)["\']?\|)?\s*(\w+)'
|
|
149
|
-
|
|
150
|
-
for match in re.finditer(edge_pattern, content):
|
|
151
|
-
source = match.group(1)
|
|
152
|
-
arrow = match.group(2)
|
|
153
|
-
label = match.group(3)
|
|
154
|
-
target = match.group(4)
|
|
155
|
-
|
|
156
|
-
style = "solid"
|
|
157
|
-
if "-.->" in arrow or "-.." in arrow:
|
|
158
|
-
style = "dashed"
|
|
159
|
-
|
|
160
|
-
self.spec.edges.append(EdgeSpec(
|
|
161
|
-
source=source,
|
|
162
|
-
target=target,
|
|
163
|
-
label=label.strip() if label else None,
|
|
164
|
-
style=style,
|
|
165
|
-
))
|
|
166
|
-
|
|
167
|
-
# Extract subgraphs as groups
|
|
168
|
-
subgraph_pattern = r'subgraph\s+(\w+)\s*\[?"?([^"\]]*)"?\]?'
|
|
169
|
-
for match in re.finditer(subgraph_pattern, content):
|
|
170
|
-
group_id = match.group(1)
|
|
171
|
-
group_name = match.group(2) or group_id
|
|
172
|
-
self.spec.layout.groups[group_name] = []
|
|
173
|
-
|
|
174
|
-
def add_node(
|
|
175
|
-
self,
|
|
176
|
-
id: str,
|
|
177
|
-
label: str,
|
|
178
|
-
shape: str = "box",
|
|
179
|
-
emphasis: str = "normal",
|
|
180
|
-
):
|
|
181
|
-
"""Add a node to the diagram."""
|
|
182
|
-
self.spec.nodes.append(NodeSpec(
|
|
183
|
-
id=id,
|
|
184
|
-
label=label,
|
|
185
|
-
shape=shape,
|
|
186
|
-
emphasis=emphasis,
|
|
187
|
-
))
|
|
188
|
-
|
|
189
|
-
def add_edge(
|
|
190
|
-
self,
|
|
191
|
-
source: str,
|
|
192
|
-
target: str,
|
|
193
|
-
label: Optional[str] = None,
|
|
194
|
-
style: str = "solid",
|
|
195
|
-
):
|
|
196
|
-
"""Add an edge between nodes."""
|
|
197
|
-
self.spec.edges.append(EdgeSpec(
|
|
198
|
-
source=source,
|
|
199
|
-
target=target,
|
|
200
|
-
label=label,
|
|
201
|
-
style=style,
|
|
202
|
-
))
|
|
203
|
-
|
|
204
|
-
def set_group(self, group_name: str, node_ids: list):
|
|
205
|
-
"""Define a group of nodes (rendered as subgraph)."""
|
|
206
|
-
self.spec.layout.groups[group_name] = node_ids
|
|
207
|
-
|
|
208
|
-
def emphasize(self, *node_ids: str):
|
|
209
|
-
"""Mark nodes as emphasized (primary styling)."""
|
|
210
|
-
self.spec.paper.emphasize.extend(node_ids)
|
|
211
|
-
|
|
212
|
-
def to_mermaid(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
213
|
-
"""
|
|
214
|
-
Compile to Mermaid format.
|
|
215
|
-
|
|
216
|
-
Parameters
|
|
217
|
-
----------
|
|
218
|
-
path : str or Path, optional
|
|
219
|
-
If provided, write to file.
|
|
220
|
-
|
|
221
|
-
Returns
|
|
222
|
-
-------
|
|
223
|
-
str
|
|
224
|
-
Mermaid source code.
|
|
225
|
-
"""
|
|
226
|
-
result = compile_to_mermaid(self.spec)
|
|
227
|
-
|
|
228
|
-
if path:
|
|
229
|
-
path = Path(path)
|
|
230
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
231
|
-
f.write(result)
|
|
232
|
-
|
|
233
|
-
return result
|
|
234
|
-
|
|
235
|
-
def to_graphviz(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
236
|
-
"""
|
|
237
|
-
Compile to Graphviz DOT format.
|
|
238
|
-
|
|
239
|
-
Parameters
|
|
240
|
-
----------
|
|
241
|
-
path : str or Path, optional
|
|
242
|
-
If provided, write to file.
|
|
243
|
-
|
|
244
|
-
Returns
|
|
245
|
-
-------
|
|
246
|
-
str
|
|
247
|
-
Graphviz DOT source code.
|
|
248
|
-
"""
|
|
249
|
-
result = compile_to_graphviz(self.spec)
|
|
250
|
-
|
|
251
|
-
if path:
|
|
252
|
-
path = Path(path)
|
|
253
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
254
|
-
f.write(result)
|
|
255
|
-
|
|
256
|
-
return result
|
|
257
|
-
|
|
258
|
-
def to_yaml(self, path: Optional[Union[str, Path]] = None) -> str:
|
|
259
|
-
"""
|
|
260
|
-
Export specification as YAML.
|
|
261
|
-
|
|
262
|
-
Parameters
|
|
263
|
-
----------
|
|
264
|
-
path : str or Path, optional
|
|
265
|
-
If provided, write to file.
|
|
266
|
-
|
|
267
|
-
Returns
|
|
268
|
-
-------
|
|
269
|
-
str
|
|
270
|
-
YAML specification.
|
|
271
|
-
"""
|
|
272
|
-
data = {
|
|
273
|
-
"type": self.spec.type.value,
|
|
274
|
-
"title": self.spec.title,
|
|
275
|
-
"paper": {
|
|
276
|
-
"column": self.spec.paper.column.value if hasattr(self.spec.paper.column, 'value') else self.spec.paper.column,
|
|
277
|
-
"max_width_mm": self.spec.paper.max_width_mm,
|
|
278
|
-
"reading_direction": self.spec.paper.reading_direction,
|
|
279
|
-
"mode": self.spec.paper.mode.value if hasattr(self.spec.paper.mode, 'value') else self.spec.paper.mode,
|
|
280
|
-
"emphasize": self.spec.paper.emphasize,
|
|
281
|
-
},
|
|
282
|
-
"layout": {
|
|
283
|
-
"groups": self.spec.layout.groups,
|
|
284
|
-
"layers": self.spec.layout.layers,
|
|
285
|
-
"layer_gap": self.spec.layout.layer_gap.value,
|
|
286
|
-
"node_gap": self.spec.layout.node_gap.value,
|
|
287
|
-
},
|
|
288
|
-
"nodes": [
|
|
289
|
-
{"id": n.id, "label": n.label, "shape": n.shape, "emphasis": n.emphasis}
|
|
290
|
-
for n in self.spec.nodes
|
|
291
|
-
],
|
|
292
|
-
"edges": [
|
|
293
|
-
{"from": e.source, "to": e.target, "label": e.label, "style": e.style}
|
|
294
|
-
for e in self.spec.edges
|
|
295
|
-
],
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
result = yaml.dump(data, default_flow_style=False, allow_unicode=True)
|
|
299
|
-
|
|
300
|
-
if path:
|
|
301
|
-
path = Path(path)
|
|
302
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
303
|
-
f.write(result)
|
|
304
|
-
|
|
305
|
-
return result
|
|
306
|
-
|
|
307
|
-
def split(
|
|
308
|
-
self,
|
|
309
|
-
max_nodes: int = 12,
|
|
310
|
-
strategy: str = "by_groups",
|
|
311
|
-
keep_hubs: bool = True,
|
|
312
|
-
) -> List["Diagram"]:
|
|
313
|
-
"""
|
|
314
|
-
Split diagram into multiple figures if too large.
|
|
315
|
-
|
|
316
|
-
Parameters
|
|
317
|
-
----------
|
|
318
|
-
max_nodes : int
|
|
319
|
-
Maximum nodes per figure before splitting.
|
|
320
|
-
strategy : str
|
|
321
|
-
Split strategy: "by_groups" or "by_articulation".
|
|
322
|
-
keep_hubs : bool
|
|
323
|
-
Show hub nodes as ghosts in both parts.
|
|
324
|
-
|
|
325
|
-
Returns
|
|
326
|
-
-------
|
|
327
|
-
List[Diagram]
|
|
328
|
-
List of split diagrams (or single diagram if no split needed).
|
|
329
|
-
|
|
330
|
-
Examples
|
|
331
|
-
--------
|
|
332
|
-
>>> d = Diagram.from_yaml("large_workflow.yaml")
|
|
333
|
-
>>> parts = d.split(max_nodes=8)
|
|
334
|
-
>>> for i, part in enumerate(parts):
|
|
335
|
-
... part.to_mermaid(f"workflow_part_{i+1}.mmd")
|
|
336
|
-
"""
|
|
337
|
-
config = SplitConfig(
|
|
338
|
-
enabled=True,
|
|
339
|
-
max_nodes=max_nodes,
|
|
340
|
-
strategy=SplitStrategy(strategy),
|
|
341
|
-
keep_hubs=keep_hubs,
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
result = split_diagram(self.spec, config)
|
|
345
|
-
|
|
346
|
-
# Wrap each spec in a Diagram object
|
|
347
|
-
diagrams = []
|
|
348
|
-
for fig_spec, label in zip(result.figures, result.labels):
|
|
349
|
-
d = Diagram.__new__(Diagram)
|
|
350
|
-
d.spec = fig_spec
|
|
351
|
-
if label:
|
|
352
|
-
d.spec.title = f"{self.spec.title} ({label})" if self.spec.title else f"Part {label}"
|
|
353
|
-
diagrams.append(d)
|
|
354
|
-
|
|
355
|
-
return diagrams
|
scitex/diagram/_mcp/__init__.py
DELETED