文件预览

graph.py

查看 Origin Pro MCP 技能包中的文件内容。

文件内容

src/origin_pro_mcp/tools/graph.py

from pathlib import Path
import time

from ..app import mcp
from ..origin_connection import get_origin, execute_labtalk

PLOT_TYPES = {
    "scatter": 201,
    "line": 200,
    "line+symbol": 202,
    "column": 203,
    "bar": 204,
    "area": 205,
    "histogram": 206,
    "box": 207,
    "contour": 208,
    "3d_scatter": 209,
    "3d_surface": 210,
    "pie": 212,
    "bubble": 228,
}

EXPORT_FORMATS = {
    "png": "png",
    "jpg": "jpg",
    "jpeg": "jpg",
    "tif": "tif",
    "tiff": "tif",
    "bmp": "bmp",
    "emf": "emf",
    "eps": "eps",
    "pdf": "pdf",
    "svg": "svg",
}


def _quote_labtalk(value: str) -> str:
    return value.replace('"', '\\"')


def _normalize_export_path(file_path: str, format: str) -> tuple[Path, str]:
    out = Path(file_path).expanduser()
    requested_format = (format or "").strip().lower().lstrip(".")
    suffix_format = out.suffix.lower().lstrip(".")
    export_format = EXPORT_FORMATS.get(suffix_format) or EXPORT_FORMATS.get(requested_format or "png")
    if export_format is None:
        supported = ", ".join(sorted(EXPORT_FORMATS))
        raise ValueError(f"Unsupported export format '{format}'. Supported: {supported}")
    if not out.suffix or EXPORT_FORMATS.get(suffix_format) != export_format:
        out = out.with_suffix(f".{export_format}")
    return out, export_format


def _wait_for_export(file_path: Path, timeout_seconds: float = 8.0) -> bool:
    deadline = time.time() + timeout_seconds
    while time.time() < deadline:
        if file_path.exists() and file_path.stat().st_size > 0:
            return True
        time.sleep(0.25)
    return file_path.exists() and file_path.stat().st_size > 0


@mcp.tool()
def create_graph(
    graph_name: str,
    data_book: str,
    data_sheet: str,
    x_col: int,
    y_col: int,
    plot_type: str = "scatter",
    y_error_col: int = 0,
    title: str = ""
) -> str:
    """Create a graph from worksheet data.

    Args:
        graph_name: Name for the graph window
        data_book: Source workbook name
        data_sheet: Source sheet name
        x_col: X column number (1-based)
        y_col: Y column number (1-based)
        plot_type: scatter, line, line+symbol, column, bar, area, histogram,
                   box, contour, pie, bubble
        y_error_col: Optional Y error column number (1-based, 0=none)
        title: Optional graph title

    Returns:
        Created graph name
    """
    o = get_origin()
    ptype = PLOT_TYPES.get(plot_type, 202)
    name = o.CreatePage(3, graph_name, "origin")

    data_ref = f"[{data_book}]{data_sheet}!({x_col},{y_col})"
    if y_error_col > 0:
        data_ref = f"[{data_book}]{data_sheet}!({x_col},{y_col},{y_error_col})"

    execute_labtalk(f"plotxy iy:={data_ref} plot:={ptype} ogl:=[{name}]Layer1;")

    if title:
        execute_labtalk(f'label -n title -s "{title}"; title.x = 50; title.y = 95;')

    return f"Created graph: {name} ({plot_type})"

@mcp.tool()
def add_plot_to_graph(
    graph_name: str,
    data_book: str,
    data_sheet: str,
    x_col: int,
    y_col: int,
    plot_type: str = "scatter",
    y_error_col: int = 0
) -> str:
    """Add another data series to an existing graph.

    Args:
        graph_name: Existing graph name
        data_book: Source workbook name
        data_sheet: Source sheet name
        x_col: X column number (1-based)
        y_col: Y column number (1-based)
        plot_type: Plot type (scatter, line, line+symbol, etc.)
        y_error_col: Optional Y error column (1-based, 0=none)

    Returns:
        Success message
    """
    ptype = PLOT_TYPES.get(plot_type, 202)
    data_ref = f"[{data_book}]{data_sheet}!({x_col},{y_col})"
    if y_error_col > 0:
        data_ref = f"[{data_book}]{data_sheet}!({x_col},{y_col},{y_error_col})"

    execute_labtalk(f"plotxy iy:={data_ref} plot:={ptype} ogl:=[{graph_name}]Layer1;")
    return f"Added {plot_type} plot to {graph_name}"

@mcp.tool()
def set_axis_labels(
    graph_name: str,
    x_label: str = "",
    y_label: str = "",
    title: str = ""
) -> str:
    """Set axis labels and title for a graph.

    Args:
        graph_name: Graph name
        x_label: X axis label
        y_label: Y axis label
        title: Graph title

    Returns:
        Success message
    """
    execute_labtalk(f"win -a {graph_name};")
    if x_label:
        execute_labtalk(f'xb.text$ = "{x_label}";')
    if y_label:
        execute_labtalk(f'yl.text$ = "{y_label}";')
    if title:
        execute_labtalk(f'label -n title -s "{title}"; title.x = 50; title.y = 95;')
    return f"Updated labels for {graph_name}"

@mcp.tool()
def set_axis_range(
    graph_name: str,
    x_min: float = None,
    x_max: float = None,
    y_min: float = None,
    y_max: float = None
) -> str:
    """Set axis range for a graph.

    Args:
        graph_name: Graph name
        x_min: X axis minimum (None=auto)
        x_max: X axis maximum (None=auto)
        y_min: Y axis minimum (None=auto)
        y_max: Y axis maximum (None=auto)

    Returns:
        Success message
    """
    execute_labtalk(f"win -a {graph_name};")
    if x_min is not None:
        execute_labtalk(f"layer.x.from = {x_min};")
    if x_max is not None:
        execute_labtalk(f"layer.x.to = {x_max};")
    if y_min is not None:
        execute_labtalk(f"layer.y.from = {y_min};")
    if y_max is not None:
        execute_labtalk(f"layer.y.to = {y_max};")
    return f"Set axis range for {graph_name}"

@mcp.tool()
def export_graph(
    graph_name: str,
    file_path: str,
    format: str = "png",
    width: int = 600,
    height: int = 400,
    dpi: int = 300
) -> str:
    """Export a graph to an image file.

    Args:
        graph_name: Graph name to export
        file_path: Full Windows output path (e.g., C:\\Users\\fig1.png)
        format: Image format: png, jpg, tif, bmp, emf, eps, pdf, svg
        width: Reserved for API compatibility; Origin uses the active page export settings
        height: Reserved for API compatibility
        dpi: Reserved for API compatibility

    Returns:
        Path to exported file
    """
    try:
        output_path, export_format = _normalize_export_path(file_path, format)
    except ValueError as e:
        return f"Export failed: {e}"

    output_path.parent.mkdir(parents=True, exist_ok=True)
    if output_path.exists():
        output_path.unlink()

    graph = _quote_labtalk(graph_name)
    folder = _quote_labtalk(str(output_path.parent))
    filename = _quote_labtalk(output_path.stem)

    execute_labtalk(f'win -a "{graph_name}";')
    command = (
        f'expGraph type:={export_format} '
        f'path:="{folder}" filename:="{filename}" '
        f'overwrite:=replace export:=specified pages:="{graph}";'
    )

    if not execute_labtalk(command):
        return f"Export failed: Origin expGraph returned false for {graph_name}"

    if not _wait_for_export(output_path):
        return f"Export failed: no file produced at {output_path}"

    size = output_path.stat().st_size
    if size < 1000:
        return f"Export failed: output too small ({size} bytes) at {output_path}"

    return f"Exported to: {output_path} ({size} bytes)"