v6.5.0: 可视化报告 — 南印度盘图渲染 + HTML报告生成

## 用户体验升级
- chart_renderer.py: 南印度风格(4×4方格)SVG星盘渲染
- chart_renderer.py: 完整HTML分析报告(含星盘图+事业+感情+补救)
- 支持: D1/D9等分盘、行星符号、星座缩写、上升标记
- 移动端响应式设计
This commit is contained in:
732642856
2026-06-11 19:53:57 +08:00
parent ba0a619f26
commit 76332f9ca9
+238
View File
@@ -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'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {SVG_WIDTH} {SVG_HEIGHT}" style="font-family:Arial,sans-serif">')
# 背景
lines.append(f'<rect width="{SVG_WIDTH}" height="{SVG_HEIGHT}" fill="#fafafa" rx="8"/>')
lines.append(f'<text x="{SVG_WIDTH/2}" y="25" text-anchor="middle" font-size="16" font-weight="bold" fill="#333">{title}</text>')
# 绘制网格
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'<rect x="{x}" y="{y}" width="{CELL_SIZE}" height="{CELL_SIZE}" fill="{bg}" stroke="#ccc" stroke-width="1" rx="2"/>')
# 星座缩写(左上角)
lines.append(f'<text x="{x+5}" y="{y+14}" font-size="11" fill="#999">{abbrev}</text>')
# 上升标记
if is_asc:
lines.append(f'<text x="{x+CELL_SIZE-5}" y="{y+14}" font-size="10" fill="#e65100" text-anchor="end">▲</text>')
# 行星
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'<text x="{x+CELL_SIZE/2}" y="{planet_y}" text-anchor="middle" font-size="13" fill="{color}">{symbol}{p_deg}</text>')
planet_y += 16
# 中心区域(传统上写星盘信息)
cx = GRID_OFFSET_X + 1.5 * CELL_SIZE
cy = GRID_OFFSET_Y + 1.5 * CELL_SIZE
lines.append(f'<text x="{cx}" y="{cy-5}" text-anchor="middle" font-size="10" fill="#999">Rasi Chart</text>')
lines.append(f'<text x="{cx}" y="{cy+12}" text-anchor="middle" font-size="10" fill="#999">(D1)</text>')
lines.append('</svg>')
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'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family: 'Segoe UI', system-ui, sans-serif; background: #f5f5f5; color: #333; line-height:1.6; }}
.container {{ max-width: 900px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #1a237e, #283593); color: white; padding: 30px; border-radius: 12px; margin-bottom: 20px; text-align: center; }}
.header h1 {{ font-size: 24px; margin-bottom: 10px; }}
.header .subtitle {{ font-size: 14px; opacity: 0.8; }}
.card {{ background: white; border-radius: 10px; padding: 20px; margin-bottom: 15px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }}
.card h2 {{ font-size: 18px; color: #1a237e; margin-bottom: 12px; border-bottom: 2px solid #e8eaf6; padding-bottom: 8px; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; }}
.tag {{ display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; margin: 2px; }}
.tag-yes {{ background: #e8f5e9; color: #2e7d32; }}
.tag-no {{ background: #ffebee; color: #c62828; }}
.tag-warn {{ background: #fff3e0; color: #e65100; }}
.strength-bar {{ height: 8px; border-radius: 4px; background: #e0e0e0; margin: 5px 0; overflow: hidden; }}
.strength-fill {{ height: 100%; border-radius: 4px; }}
.footer {{ text-align: center; color: #999; font-size: 12px; margin-top: 30px; padding: 20px; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 8px 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; }}
th {{ background: #f5f5f5; color: #555; font-weight: 600; }}
</style></head><body><div class="container">
<div class="header">
<h1>🔮 {title}</h1>
<div class="subtitle">印度占星 (Jyotish) · 基于BPHS标准 · Lahiri岁差</div>
</div>
<div class="card">
<h2>📋 出生信息</h2>
<div class="grid">
<div><strong>姓名:</strong>{birth_data.get('name', '')}</div>
<div><strong>日期:</strong>{birth_data.get('date', '')}</div>
<div><strong>时间:</strong>{birth_data.get('time', '')}</div>
<div><strong>地点:</strong>{birth_data.get('place', '')}</div>
</div>
</div>
'''
# 星盘图
if chart_data:
svg_chart = render_south_indian_chart(
chart_data.get('planets', {}),
chart_data.get('asc_sign', 'Aries'),
"D1 — Rashi Chart (本命盘)"
)
html += f'<div class="card"><h2>🌟 本命盘</h2><div style="text-align:center">{svg_chart}</div></div>'
# 事业分析
if analysis.get('career'):
c = analysis['career']
html += '<div class="card"><h2>💼 事业分析</h2>'
html += f'<p><strong>评估:</strong>{c.get("assessment", "")}</p>'
if c.get('fields'):
html += '<p><strong>推荐领域:</strong><br/>'
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'<span style="display:inline-block;width:{max(10,perc)}%;background:{color};color:white;padding:2px 8px;border-radius:4px;margin:2px;font-size:12px">{f["field"]}</span> '
html += '</p>'
html += '</div>'
# 感情分析
if analysis.get('relationship'):
r = analysis['relationship']
html += '<div class="card"><h2>💕 感情分析</h2>'
html += f'<p><strong>评估:</strong>{r.get("assessment", "")}</p>'
if r.get('partnership_style'):
for s in r['partnership_style'][:1]:
html += f'<p><strong>Venus</strong>{s.get("expression", "")}</p>'
html += '</div>'
# 补救建议
if analysis.get('remedies'):
rem = analysis['remedies']
html += '<div class="card"><h2>🪷 补救建议</h2>'
html += f'<p>{rem.get("summary", "")}</p>'
recs = rem.get('recommendations', {})
if recs.get('gems'):
html += '<p><strong>宝石建议:</strong><br/>'
for g in recs['gems'][:3]:
html += f'<span class="tag tag-yes">{g["planet"]}: {g["gem"]}</span> '
html += '</p>'
if recs.get('mantras'):
html += '<p><strong>咒语建议:</strong><br/>'
for m in recs['mantras'][:3]:
html += f'<span class="tag tag-warn">{m["mantra"]} × {m["repetitions"]}</span> '
html += '</p>'
html += '</div>'
html += '<div class="footer">Generated by yinduzhanxing v6.4.0 · MIT License · 仅供学习参考</div>'
html += '</div></body></html>'
return html