diff --git a/scripts/chart_renderer.py b/scripts/chart_renderer.py new file mode 100644 index 00000000..6bec524b --- /dev/null +++ b/scripts/chart_renderer.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +南印度占星盘图可视化渲染器 v1.0 + +生成南印度风格 (4×4方格) 的可视化星盘SVG。 +支持:D1(本命)/D9(Navamsa)/D10(Dasamsa)等分盘 +""" + +from typing import Dict, List, Optional + +SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', + 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'] + +# 南印度盘星座固定布局(从左上角顺时针读数) +# 星座位置固定不变,行星散布其中 +SOUTH_INDIAN_GRID = [ + # Row 0: Pisces, Aries, Taurus, Gemini + [11, 0, 1, 2], + # Row 1: Aquarius, (CENTER), (CENTER), Cancer + [10, -1, -1, 3], + # Row 2: Capricorn, (CENTER), (CENTER), Leo + [9, -1, -1, 4], + # Row 3: Sagittarius, Scorpio, Libra, Virgo + [8, 7, 6, 5], +] + +PLANET_SYMBOLS = { + 'Sun': '☉', 'Moon': '☽', 'Mars': '♂', 'Mercury': '☿', + 'Jupiter': '♃', 'Venus': '♀', 'Saturn': '♄', + 'Rahu': '☊', 'Ketu': '☋', 'Asc': 'Asc', +} + +PLANET_COLORS = { + 'Sun': '#E65100', 'Moon': '#1565C0', 'Mars': '#C62828', + 'Mercury': '#2E7D32', 'Jupiter': '#F9A825', 'Venus': '#6A1B9A', + 'Saturn': '#37474F', 'Rahu': '#00695C', 'Ketu': '#BF360C', +} + +SIGN_NAMES_CN = { + 'Aries': '白羊', 'Taurus': '金牛', 'Gemini': '双子', 'Cancer': '巨蟹', + 'Leo': '狮子', 'Virgo': '处女', 'Libra': '天秤', 'Scorpio': '天蝎', + 'Sagittarius': '射手', 'Capricorn': '摩羯', 'Aquarius': '水瓶', 'Pisces': '双鱼', +} + +SIGN_ABBREV = { + 'Aries': 'Ar', 'Taurus': 'Ta', 'Gemini': 'Ge', 'Cancer': 'Cn', + 'Leo': 'Le', 'Virgo': 'Vi', 'Libra': 'Li', 'Scorpio': 'Sc', + 'Sagittarius': 'Sg', 'Capricorn': 'Cp', 'Aquarius': 'Aq', 'Pisces': 'Pi', +} + +CELL_SIZE = 80 +PADDING = 10 +GRID_OFFSET_X = 50 +GRID_OFFSET_Y = 50 +SVG_WIDTH = GRID_OFFSET_X * 2 + CELL_SIZE * 4 +SVG_HEIGHT = GRID_OFFSET_Y * 2 + CELL_SIZE * 4 + 60 + + +def render_south_indian_chart(planets: Dict, asc_sign: str, title: str = "D1 — Rashi Chart") -> str: + """ + 生成南印度风格星盘SVG字符串。 + + Args: + planets: {planet: sign_name} 或 {planet: {'sign': name, 'degree': float}} + asc_sign: 上升星座名称 + title: 图表标题 + + Returns: + 完整SVG字符串 + """ + lines = [] + lines.append(f'') + + # 背景 + lines.append(f'') + lines.append(f'{title}') + + # 绘制网格 + for row in range(4): + for col in range(4): + sign_idx = SOUTH_INDIAN_GRID[row][col] + if sign_idx < 0: + continue + + x = GRID_OFFSET_X + col * CELL_SIZE + y = GRID_OFFSET_Y + row * CELL_SIZE + sign_name = SIGNS[sign_idx] + abbrev = SIGN_ABBREV[sign_name] + + # 单元格背景(上升星座高亮) + is_asc = sign_name == asc_sign + bg = '#fff3e0' if is_asc else '#fff' + lines.append(f'') + + # 星座缩写(左上角) + lines.append(f'{abbrev}') + + # 上升标记 + if is_asc: + lines.append(f'') + + # 行星 + planet_y = y + 30 + for pname in ['Sun','Moon','Mars','Mercury','Jupiter','Venus','Saturn','Rahu','Ketu']: + pdata = planets.get(pname) + if not pdata: + continue + p_sign = pdata if isinstance(pdata, str) else pdata.get('sign', '') + p_deg = "" if isinstance(pdata, str) else f" {pdata.get('degree',0)%30:.0f}°" + if p_sign == sign_name: + symbol = PLANET_SYMBOLS.get(pname, pname[:2]) + color = PLANET_COLORS.get(pname, '#333') + lines.append(f'{symbol}{p_deg}') + planet_y += 16 + + # 中心区域(传统上写星盘信息) + cx = GRID_OFFSET_X + 1.5 * CELL_SIZE + cy = GRID_OFFSET_Y + 1.5 * CELL_SIZE + lines.append(f'Rasi Chart') + lines.append(f'(D1)') + + lines.append('') + return '\n'.join(lines) + + +def render_html_report(birth_data: Dict, chart_data: Dict, analysis: Dict, + title: str = "印度占星分析报告") -> str: + """ + 生成完整的HTML分析报告。 + + Args: + birth_data: {'name': str, 'date': str, 'time': str, 'place': str} + chart_data: 星盘数据(用于生成图表) + analysis: 分析结果 {'career': {...}, 'relationship': {...}, 'remedies': {...}, ...} + title: 报告标题 + + Returns: + 完整HTML字符串 + """ + html = f''' + + + + +{title} +
+ +
+

🔮 {title}

+
印度占星 (Jyotish) · 基于BPHS标准 · Lahiri岁差
+
+ +
+

📋 出生信息

+
+
姓名:{birth_data.get('name', '—')}
+
日期:{birth_data.get('date', '—')}
+
时间:{birth_data.get('time', '—')}
+
地点:{birth_data.get('place', '—')}
+
+
+''' + + # 星盘图 + if chart_data: + svg_chart = render_south_indian_chart( + chart_data.get('planets', {}), + chart_data.get('asc_sign', 'Aries'), + "D1 — Rashi Chart (本命盘)" + ) + html += f'

🌟 本命盘

{svg_chart}
' + + # 事业分析 + if analysis.get('career'): + c = analysis['career'] + html += '

💼 事业分析

' + html += f'

评估:{c.get("assessment", "")}

' + if c.get('fields'): + html += '

推荐领域:
' + for f in c['fields'][:5]: + perc = int(f.get('score', 0) / max(1, max(x.get('score', 0) for x in c['fields'] or [{'score': 1}])) * 100) + color = '#1a237e' if perc > 70 else '#283593' if perc > 40 else '#5c6bc0' + html += f'{f["field"]} ' + html += '

' + html += '
' + + # 感情分析 + if analysis.get('relationship'): + r = analysis['relationship'] + html += '

💕 感情分析

' + html += f'

评估:{r.get("assessment", "")}

' + if r.get('partnership_style'): + for s in r['partnership_style'][:1]: + html += f'

Venus:{s.get("expression", "")}

' + html += '
' + + # 补救建议 + if analysis.get('remedies'): + rem = analysis['remedies'] + html += '

🪷 补救建议

' + html += f'

{rem.get("summary", "")}

' + recs = rem.get('recommendations', {}) + if recs.get('gems'): + html += '

宝石建议:
' + for g in recs['gems'][:3]: + html += f'{g["planet"]}: {g["gem"]} ' + html += '

' + if recs.get('mantras'): + html += '

咒语建议:
' + for m in recs['mantras'][:3]: + html += f'{m["mantra"]} × {m["repetitions"]} ' + html += '

' + html += '
' + + html += '' + html += '
' + return html