""" Chart rendering tools for astro-mcp. Render visual chart wheels (SVG/PNG/JPG) from calculated chart data. Combines calculation + rendering in one step. """ from __future__ import annotations from typing import Any from .server import mcp from .chart_renderer import ( render_natal_wheel, render_transit_wheel, render_synastry_wheel, ) from .chart_tools import ( calculate_natal_chart, calculate_transit_chart, calculate_synastry_chart, calculate_composite_chart, calculate_davison_chart, ) from .by_id_tools import ( calculate_natal_chart_by_id, calculate_transit_chart_by_id, calculate_synastry_chart_by_id, calculate_composite_chart_by_id, calculate_davison_chart_by_id, ) # ═══════════════════════════════════════════════════════════════════════ # CHART RENDERING TOOLS # ═══════════════════════════════════════════════════════════════════════ # These tools render visual chart wheels from birth data. # They combine calculation + rendering in one step. # For data-only output, use the calculate_* tools instead. # ═══════════════════════════════════════════════════════════════════════ # ── Shared render options (used by all render_* tools) ──────────────── _RENDER_STYLE_HELP = ( "Chart visual style: 'modern' (clean, minimal), 'traditional' " "(ornate, classical), or 'minimal' (bare bones)." ) _RENDER_COLOR_HELP = ( "Color mode: 'color' (full color with element-themed zodiac ring), " "'bw' (black/white, aspect lines distinguished by style), or " "'dark' (dark background for web display)." ) _RENDER_SIZE_HELP = "SVG width/height in pixels (default: 600)." _RENDER_TABLE_HELP = "Include an aspect table below the wheel." _RENDER_PLANETS_HELP = "Include a planet data table below the wheel." _RENDER_HOUSES_HELP = "Include a house cusp table below the wheel." _RENDER_TITLE_HELP = "Custom chart title. Auto-generated if not provided." # ── render_natal_chart ──────────────────────────────────────────────── @mcp.tool() async def render_natal_chart( # ── Birth data (same as calculate_natal_chart) ────────────────── birth_datetime: str, latitude: float, longitude: float, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, top_n_aspects: int | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, subtitle: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a natal chart wheel. Calculates planetary positions and renders a visual zodiac wheel with planets, houses, and aspect lines. Output as SVG or raster image (PNG/JPG). BIRTH DATA (required): birth_datetime: ISO 8601 datetime with timezone (e.g. "1990-05-15T10:30:00+01:00"). latitude: Birth latitude in decimal degrees (-90 to 90). longitude: Birth longitude in decimal degrees (-180 to 180). BIRTH DATA (optional): elevation: Birth elevation in meters (default: 0). house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides, e.g. {"conjunction": 10}. top_n_aspects: Limit aspects to the N tightest by orb. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait layout with tables under the wheel), or "right" (landscape layout with tables to the right). include_planets: Include a planet data table (requires table_position != "none"). include_houses: Include a house cusp table (requires table_position != "none"). title: Custom chart title. Auto-generated if not provided. subtitle: Custom subtitle. Auto-generated from birth data if not provided. format: Output format — "svg" (default), "png", or "jpg". Returns: Dict with "content", "format", "content_type", "width", "height", and "included" (list of what's in the chart). """ from .chart_renderer import render_natal_wheel chart_data = await calculate_natal_chart( birth_datetime=birth_datetime, latitude=latitude, longitude=longitude, elevation=elevation, house_system=house_system, orb_limits=orb_limits, top_n_aspects=top_n_aspects, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, subtitle=subtitle, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── render_natal_chart_by_id ────────────────────────────────────────── @mcp.tool() async def render_natal_chart_by_id( # ── Person lookup (same as calculate_natal_chart_by_id) ───────── person_id: str, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, top_n_aspects: int | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a natal chart wheel for a person from the database. Looks up birth data by person_id or nickname, calculates the chart, and renders it as a wheel. Output as SVG or raster image (PNG/JPG). PERSON LOOKUP (required): person_id: ID or nickname of a person in the persons database. PERSON LOOKUP (optional): house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. top_n_aspects: Limit aspects to the N tightest by orb. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait), or "right" (landscape). include_planets: {_RENDER_PLANETS_HELP} include_houses: {_RENDER_HOUSES_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg" (SVG string), "format", "width", "height", "included". """ from .chart_renderer import render_natal_wheel chart_data = await calculate_natal_chart_by_id( person_id=person_id, house_system=house_system, orb_limits=orb_limits, top_n_aspects=top_n_aspects, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── render_transit_chart ────────────────────────────────────────────── @mcp.tool() async def render_transit_chart( # ── Birth data + transit date (same as calculate_transit_chart) ─ birth_datetime: str, transit_datetime: str, latitude: float, longitude: float, transit_latitude: float | None = None, transit_longitude: float | None = None, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a transit chart as a bi-wheel (natal inner, transit outer). Calculates natal and transiting planet positions, then renders a bi-wheel showing natal planets inside and transiting planets outside, with transit-to-natal aspect lines. Output as SVG or raster image (PNG/JPG). BIRTH DATA (required): birth_datetime: ISO 8601 birth datetime with timezone. transit_datetime: ISO 8601 transit datetime (the "now" or future date). latitude: Birth latitude in decimal degrees. longitude: Birth longitude in decimal degrees. TRANSIT LOCATION (optional): transit_latitude: Location latitude for transit calculation. Defaults to birth latitude. transit_longitude: Location longitude for transit calculation. Defaults to birth longitude. CHART OPTIONS: elevation: Birth elevation in meters (default: 0). house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} title: {_RENDER_TITLE_HELP} format: Output format — "svg" (default), "png", or "jpg". Returns: Dict with "content", "format", "content_type", "width", "height", "included". """ from .chart_renderer import render_transit_wheel chart_data = await calculate_transit_chart( birth_datetime=birth_datetime, transit_datetime=transit_datetime, latitude=latitude, longitude=longitude, transit_latitude=transit_latitude, transit_longitude=transit_longitude, elevation=elevation, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_transit_wheel( chart_data, style=style, color_mode=color_mode, size=size, title=title, format=format, ) result["included"] = ["wheel"] return result # ── render_transit_chart_by_id ──────────────────────────────────────── @mcp.tool() async def render_transit_chart_by_id( # ── Person lookup + transit date ──────────────────────────────── person_id: str, transit_datetime: str, transit_latitude: float | None = None, transit_longitude: float | None = None, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a transit bi-wheel for a person from the database. Looks up birth data by person_id, calculates transits for the given date, and renders a bi-wheel chart. Output as SVG or raster image (PNG/JPG). PERSON LOOKUP (required): person_id: ID or nickname of a person in the persons database. transit_datetime: ISO 8601 transit datetime. TRANSIT LOCATION (optional): transit_latitude: Location latitude. Defaults to birth latitude. transit_longitude: Location longitude. Defaults to birth longitude. CHART OPTIONS: house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_transit_wheel chart_data = await calculate_transit_chart_by_id( person_id=person_id, transit_datetime=transit_datetime, transit_latitude=transit_latitude, transit_longitude=transit_longitude, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_transit_wheel( chart_data, style=style, color_mode=color_mode, size=size, title=title, format=format, ) result["included"] = ["wheel"] return result # ── render_synastry_chart ───────────────────────────────────────────── @mcp.tool() async def render_synastry_chart( # ── Two people's birth data (same as calculate_synastry_chart) ── person1_datetime: str, person1_latitude: float, person1_longitude: float, person2_datetime: str, person2_latitude: float, person2_longitude: float, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, top_n_aspects: int | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 800, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a synastry (relationship) chart with two side-by-side wheels. Calculates both natal charts and renders them side by side with interaspect lines between the two charts. Output as SVG or raster image (PNG/JPG). PERSON 1 (required): person1_datetime: ISO 8601 birth datetime with timezone. person1_latitude: Birth latitude in decimal degrees. person1_longitude: Birth longitude in decimal degrees. PERSON 2 (required): person2_datetime: ISO 8601 birth datetime with timezone. person2_latitude: Birth latitude in decimal degrees. person2_longitude: Birth longitude in decimal degrees. CHART OPTIONS: elevation: Birth elevation in meters (default: 0). house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. top_n_aspects: Limit interaspects to the N tightest by orb. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_synastry_wheel chart_data = await calculate_synastry_chart( person1_datetime=person1_datetime, person1_latitude=person1_latitude, person1_longitude=person1_longitude, person2_datetime=person2_datetime, person2_latitude=person2_latitude, person2_longitude=person2_longitude, elevation=elevation, house_system=house_system, orb_limits=orb_limits, top_n_aspects=top_n_aspects, ) if "error" in chart_data: return chart_data result = render_synastry_wheel( chart_data, style=style, color_mode=color_mode, size=size, title=title, format=format, ) result["included"] = ["wheel"] return result # ── render_synastry_chart_by_id ─────────────────────────────────────── @mcp.tool() async def render_synastry_chart_by_id( # ── Two person IDs ────────────────────────────────────────────── person1_id: str, person2_id: str, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, top_n_aspects: int | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 800, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a synastry chart for two people from the database. Looks up both persons by ID or nickname, calculates their synastry, and renders side-by-side natal wheels with interaspect lines. Output as SVG or raster image (PNG/JPG). PERSON LOOKUP (required): person1_id: ID or nickname of person 1 in the persons database. person2_id: ID or nickname of person 2 in the persons database. CHART OPTIONS: house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. top_n_aspects: Limit interaspects to the N tightest by orb. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_synastry_wheel chart_data = await calculate_synastry_chart_by_id( person1_id=person1_id, person2_id=person2_id, house_system=house_system, orb_limits=orb_limits, top_n_aspects=top_n_aspects, ) if "error" in chart_data: return chart_data result = render_synastry_wheel( chart_data, style=style, color_mode=color_mode, size=size, title=title, format=format, ) result["included"] = ["wheel"] return result # ── render_composite_chart ──────────────────────────────────────────── @mcp.tool() async def render_composite_chart( # ── Two people's birth data (same as calculate_composite_chart) ─ person1_datetime: str, person1_latitude: float, person1_longitude: float, person2_datetime: str, person2_latitude: float, person2_longitude: float, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a composite chart (midpoint method) as a single wheel. Calculates the composite chart from two people's birth data and renders it as a standard natal-style wheel representing the relationship. Output as SVG or raster image (PNG/JPG). PERSON 1 (required): person1_datetime: ISO 8601 birth datetime with timezone. person1_latitude: Birth latitude in decimal degrees. person1_longitude: Birth longitude in decimal degrees. PERSON 2 (required): person2_datetime: ISO 8601 birth datetime with timezone. person2_latitude: Birth latitude in decimal degrees. person2_longitude: Birth longitude in decimal degrees. CHART OPTIONS: elevation: Birth elevation in meters (default: 0). house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait), or "right" (landscape). include_planets: {_RENDER_PLANETS_HELP} include_houses: {_RENDER_HOUSES_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_natal_wheel chart_data = await calculate_composite_chart( person1_datetime=person1_datetime, person1_latitude=person1_latitude, person1_longitude=person1_longitude, person2_datetime=person2_datetime, person2_latitude=person2_latitude, person2_longitude=person2_longitude, elevation=elevation, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── render_composite_chart_by_id ────────────────────────────────────── @mcp.tool() async def render_composite_chart_by_id( # ── Two person IDs ────────────────────────────────────────────── person1_id: str, person2_id: str, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a composite chart for two people from the database. Looks up both persons by ID, calculates the composite chart, and renders it as a single natal-style wheel. Output as SVG or raster image (PNG/JPG). PERSON LOOKUP (required): person1_id: ID or nickname of person 1 in the persons database. person2_id: ID or nickname of person 2 in the persons database. CHART OPTIONS: house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait), or "right" (landscape). include_planets: {_RENDER_PLANETS_HELP} include_houses: {_RENDER_HOUSES_HELP} title: {_RENDER_TITLE_HELP} format: Output format — "svg" (default), "png", or "jpg". Returns: Dict with "content", "format", "content_type", "width", "height", "included". """ from .chart_renderer import render_natal_wheel chart_data = await calculate_composite_chart_by_id( person1_id=person1_id, person2_id=person2_id, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── render_davison_chart ────────────────────────────────────────────── @mcp.tool() async def render_davison_chart( # ── Two people's birth data (same as calculate_davison_chart) ─── person1_datetime: str, person1_latitude: float, person1_longitude: float, person2_datetime: str, person2_latitude: float, person2_longitude: float, elevation: float = 0.0, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a Davison chart (midpoint in time and space) as a single wheel. Calculates the Davison chart from two people's birth data and renders it as a standard natal-style wheel. Output as SVG or raster image (PNG/JPG). PERSON 1 (required): person1_datetime: ISO 8601 birth datetime with timezone. person1_latitude: Birth latitude in decimal degrees. person1_longitude: Birth longitude in decimal degrees. PERSON 2 (required): person2_datetime: ISO 8601 birth datetime with timezone. person2_latitude: Birth latitude in decimal degrees. person2_longitude: Birth longitude in decimal degrees. CHART OPTIONS: elevation: Birth elevation in meters (default: 0). house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait), or "right" (landscape). include_planets: {_RENDER_PLANETS_HELP} include_houses: {_RENDER_HOUSES_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_natal_wheel chart_data = await calculate_davison_chart( person1_datetime=person1_datetime, person1_latitude=person1_latitude, person1_longitude=person1_longitude, person2_datetime=person2_datetime, person2_latitude=person2_latitude, person2_longitude=person2_longitude, elevation=elevation, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── render_davison_chart_by_id ──────────────────────────────────────── @mcp.tool() async def render_davison_chart_by_id( # ── Two person IDs ────────────────────────────────────────────── person1_id: str, person2_id: str, house_system: str = "placidus", orb_limits: dict[str, float] | None = None, # ── Rendering options ─────────────────────────────────────────── style: str = "modern", color_mode: str = "color", size: int = 600, table_position: str = "none", include_planets: bool = False, include_houses: bool = False, title: str | None = None, format: str = "svg", ) -> dict[str, Any]: """Render a Davison chart for two people from the database. Looks up both persons by ID, calculates the Davison chart, and renders it as a single natal-style wheel. Output as SVG or raster image. PERSON LOOKUP (required): person1_id: ID or nickname of person 1 in the persons database. person2_id: ID or nickname of person 2 in the persons database. CHART OPTIONS: house_system: "placidus" (default), "equal", or "whole_sign". orb_limits: Per-aspect orb overrides. RENDERING OPTIONS: style: {_RENDER_STYLE_HELP} color_mode: {_RENDER_COLOR_HELP} size: {_RENDER_SIZE_HELP} table_position: "none" (wheel only), "below" (portrait), or "right" (landscape). include_planets: {_RENDER_PLANETS_HELP} include_houses: {_RENDER_HOUSES_HELP} title: {_RENDER_TITLE_HELP} Returns: Dict with "svg", "format", "width", "height", "included". """ from .chart_renderer import render_natal_wheel chart_data = await calculate_davison_chart_by_id( person1_id=person1_id, person2_id=person2_id, house_system=house_system, orb_limits=orb_limits, ) if "error" in chart_data: return chart_data result = render_natal_wheel( chart_data, style=style, color_mode=color_mode, size=size, table_position=table_position, include_planets=include_planets, include_houses=include_houses, title=title, format=format, ) result["included"] = _included_list(table_position, include_planets, include_houses) return result # ── Helper ──────────────────────────────────────────────────────────── def _included_list(table_position: str, planets: bool, houses: bool) -> list[str]: result = ["wheel"] if table_position in ("below", "right"): if planets: result.append("planet_table") if houses: result.append("house_table") return result