Files
Jyotisha/jyotish-app/chart-renderer.js
T
732642856 2457e23271 feat: Jyotish Web App v4.3 完整印度占星前端应用
技术栈: Vite + 原生HTML/JS/CSS + swisseph-wasm
功能模块:
- 排盘引擎: 行星/宫位/Dasha/Yoga/相位/燃烧检测/130+城市
- 高级计算: Karaka/Arudha/16分盘/Ashtakavarga/Shadbala/Panchanga/Chara Dasha
- 73个Yoga识别 (Pancha Mahapurusha + 扩展A/B)
- 生时校正v2.0: K.N.Rao方法论, 多维评分
- Transit v2.0: 日期选择/全行星/AV评分/Sade Sati
- 深度分析7子模块 + 扩展分析5子模块
- 导出v2.0: JSON 100%覆盖Python 13模块 + SVG/PNG
- i18n中英双语 + 点读机95词条 + AI聊天面板
- 认证+订阅 (JWT/Apple Sign-In/IAP)
- 浅色极简主题 (fatetell.com风格)
2026-04-30 03:51:30 +08:00

290 lines
12 KiB
JavaScript

/**
* 星盘渲染 — 南印度 + 北印度风格 v3.0
* 浅色极简主题,支持南/北印度风格切换
*/
import { SIGNS, SIGNS_CN, SIGNS_EN_SHORT, PLANET_SYMBOLS, PLANET_CN } from './jyotish-engine.js';
import { signName, getLang } from './i18n.js';
// 南印度星盘宫位映射(星座固定位置)
const SOUTH_INDIAN_LAYOUT = [
[0,0],[0,1],[0,2],[0,3], // Pisces, Aries, Taurus, Gemini
[1,3],[2,3],[3,3], // Cancer, Leo, Virgo
[3,2],[3,1],[3,0], // Libra, Scorpio, Sagittarius
[2,0],[1,0], // Capricorn, Aquarius
];
// 北印度星盘宫位布局(固定上升在顶部菱形)
// 12宫按固定位置排列,上升始终在顶部菱形格
const NORTH_INDIAN_HOUSES = [
// [row, col] — 从12宫开始,顺时针: 12,1,2,3,4,5,6,7,8,9,10,11
// 顶行: 12, 1, 2
// 右侧下行: 3, 4
// 底行: 11, 10, 9, 8(中心偏), 7
// 左侧上行: 6, 5
[0,0],[0,1],[0,2], // H12, H1, H2
[1,2],[2,3], // H3, H4
[3,3],[3,2],[3,1],[3,0], // H5, H6, H7, H8
[2,0],[1,0], // H9, H10
[1,1], // H11 (center-ish)
];
// 浅色主题色值
const THEME = {
bg: '#ffffff',
bgAsc: 'rgba(109,40,217,0.04)',
border: '#c9c8c8',
borderAsc: 'rgba(109,40,217,0.35)',
signText: '#6b6b6b',
signTextAsc: '#6d28d9',
houseNum: '#8c8c8c',
planetDefault: '#2e2e2e',
planetExalted: '#16a34a',
planetDebilitated: '#dc2626',
planetOwn: '#d97706',
ascLabel: '#6d28d9',
combust: '#dc2626',
};
/**
* 渲染南印度风格星盘
*/
export function renderSouthIndianChart(container, chartData, options = {}) {
const { planets, ascendant } = chartData;
const ascIdx = SIGNS.indexOf(ascendant.sign);
const size = options.size || 420;
const title = options.title || '';
// 按宫位分组行星
const housePlanets = {};
for (const [pname, pinfo] of Object.entries(planets)) {
if (pinfo.error) continue;
const h = pinfo.house;
if (!housePlanets[h]) housePlanets[h] = [];
housePlanets[h].push({ name: pname, ...pinfo });
}
const cellSize = size / 4;
let svg = '';
if (title) {
svg += `<div style="text-align:center;font-size:12px;color:#6b6b6b;margin-bottom:6px;">${title}</div>`;
}
svg += `<svg viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;">`;
// 背景
svg += `<rect width="${size}" height="${size}" fill="${THEME.bg}" rx="10"/>`;
// 绘制12个宫格
for (let signIdx = 0; signIdx < 12; signIdx++) {
const [row, col] = SOUTH_INDIAN_LAYOUT[signIdx];
const x = col * cellSize;
const y = row * cellSize;
const signName = SIGNS[signIdx];
const isAscSign = signIdx === ascIdx;
// 宫格背景
const bgColor = isAscSign ? THEME.bgAsc : THEME.bg;
svg += `<rect x="${x+1}" y="${y+1}" width="${cellSize-2}" height="${cellSize-2}" fill="${bgColor}" rx="4"/>`;
// 宫格边框
const borderColor = isAscSign ? THEME.borderAsc : THEME.border;
svg += `<rect x="${x+1}" y="${y+1}" width="${cellSize-2}" height="${cellSize-2}" fill="none" stroke="${borderColor}" stroke-width="1" rx="4"/>`;
// 星座名(小字,左上角)
const signColor = isAscSign ? THEME.signTextAsc : THEME.signText;
svg += `<text x="${x+8}" y="${y+14}" font-size="9" fill="${signColor}" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-weight="${isAscSign?'600':'400'}">${SIGNS_EN_SHORT[signName]}</text>`;
// 宫位编号
const houseNum = ((signIdx - ascIdx + 12) % 12) + 1;
svg += `<text x="${x+cellSize-10}" y="${y+14}" font-size="8" fill="${THEME.houseNum}" text-anchor="end" font-family="sans-serif">${houseNum}</text>`;
// 该宫的行星
const pInHouse = housePlanets[houseNum] || [];
let py = y + 28;
for (const p of pInHouse) {
const symbol = PLANET_SYMBOLS[p.name] || '';
let pColor = THEME.planetDefault;
if (p.status === '入旺') pColor = THEME.planetExalted;
else if (p.status === '落陷') pColor = THEME.planetDebilitated;
else if (p.status === '入庙') pColor = THEME.planetOwn;
const retroMark = p.retrograde ? ' ℞' : '';
const combustMark = p.combust ? ' C' : '';
const degStr = (p.degree_in_sign != null) ? p.degree_in_sign.toFixed(1) + '°' : '';
svg += `<text x="${x+8}" y="${py}" font-size="12" fill="${pColor}" font-family="sans-serif">${symbol}${degStr}${retroMark}${combustMark}</text>`;
py += 16;
}
// 上升标记
if (isAscSign) {
svg += `<text x="${x+cellSize/2}" y="${y+cellSize-8}" font-size="10" fill="${THEME.ascLabel}" text-anchor="middle" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-weight="600">ASC</text>`;
}
}
// ===== 中心区域 — 核心摘要 =====
const cx = size / 2, cy0 = cellSize, cw = cellSize * 2;
const summary = options.summary || null;
// 背景框
svg += `<rect x="${cy0+3}" y="${cy0+3}" width="${cw-6}" height="${cw-6}" fill="rgba(109,40,217,0.015)" rx="6"/>`;
svg += `<rect x="${cy0+3}" y="${cy0+3}" width="${cw-6}" height="${cw-6}" fill="none" stroke="rgba(109,40,217,0.08)" stroke-width="0.5" rx="6"/>`;
// Lagna 上升星座 + 度数
const ascSignSymbol = {Aries:'♈',Taurus:'♉',Gemini:'♊',Cancer:'♋',Leo:'♌',Virgo:'♍',Libra:'♎',Scorpio:'♏',Sagittarius:'♐',Capricorn:'♑',Aquarius:'♒',Pisces:'♓'}[ascendant.sign] || '';
svg += `<text x="${cx}" y="${cy0+20}" text-anchor="middle" font-size="12" fill="${THEME.ascLabel}" font-weight="700" font-family="-apple-system,BlinkMacSystemFont,sans-serif">${ascSignSymbol} ${signName(ascendant.sign)} ${(ascendant.degree_in_sign != null ? ascendant.degree_in_sign : ascendant.degree % 30).toFixed(2)}°</text>`;
// 月亮星宿
if (summary?.moonNakshatra) {
svg += `<text x="${cx}" y="${cy0+34}" text-anchor="middle" font-size="9" fill="${THEME.signText}" font-family="sans-serif">☽ ${summary.moonNakshatra}</text>`;
}
// 分隔线
svg += `<line x1="${cx-40}" y1="${cy0+42}" x2="${cx+40}" y2="${cy0+42}" stroke="rgba(109,40,217,0.12)" stroke-width="0.5"/>`;
// 当前 Dasha
if (summary?.dasha) {
const dColor = THEME.planetDefault;
svg += `<text x="${cx}" y="${cy0+56}" text-anchor="middle" font-size="10" fill="${dColor}" font-weight="600" font-family="sans-serif">Dasha</text>`;
svg += `<text x="${cx}" y="${cy0+70}" text-anchor="middle" font-size="10" fill="${THEME.ascLabel}" font-weight="600" font-family="sans-serif">${summary.dasha.maha}/${summary.dasha.antar}</text>`;
svg += `<text x="${cx}" y="${cy0+82}" text-anchor="middle" font-size="8" fill="${THEME.houseNum}" font-family="sans-serif">${summary.dasha.period}</text>`;
}
// AK / DK
if (summary?.karaka) {
const parts = [];
if (summary.karaka.AK) parts.push(`AK:${summary.karaka.AK}`);
if (summary.karaka.DK) parts.push(`DK:${summary.karaka.DK}`);
if (parts.length) {
svg += `<text x="${cx}" y="${cy0+96}" text-anchor="middle" font-size="9" fill="${THEME.planetDefault}" font-family="sans-serif">${parts.join(' · ')}</text>`;
}
}
// 关键 Yoga(最多3个)
if (summary?.topYogas && summary.topYogas.length > 0) {
let ty = cy0 + 112;
for (const y of summary.topYogas.slice(0, 3)) {
const yColor = y.negative ? '#dc2626' : '#16a34a';
svg += `<text x="${cx}" y="${ty}" text-anchor="middle" font-size="8" fill="${yColor}" font-family="sans-serif">◆ ${y.name_cn}</text>`;
ty += 12;
}
}
svg += '</svg>';
container.innerHTML = svg;
}
/**
* 渲染北印度风格星盘(Diamond 菱形布局)
* 北印盘固定12宫位置,上升始终在顶部中心格
*/
export function renderNorthIndianChart(container, chartData, options = {}) {
const { planets, ascendant } = chartData;
const ascIdx = SIGNS.indexOf(ascendant.sign);
const size = options.size || 420;
const title = options.title || '';
// 按宫位分组行星
const housePlanets = {};
for (const [pname, pinfo] of Object.entries(planets)) {
if (pinfo.error) continue;
const h = pinfo.house;
if (!housePlanets[h]) housePlanets[h] = [];
housePlanets[h].push({ name: pname, ...pinfo });
}
// 北印盘布局: 4x4 网格,外圈12格 = 12宫
// 位置定义 (row, col) — 按宫位编号 1-12
const houseCells = {
1: [0,1], 2: [0,2], // 顶行右半
3: [1,3], 4: [2,3], // 右列下半
5: [3,2], 6: [3,1], // 底行左半
7: [2,0], 8: [1,0], // 左列上半
9: [1,1], 10: [1,2], // 内层上
11: [2,2], 12: [2,1], // 内层下
};
const cellSize = size / 4;
let svg = '';
if (title) {
svg += `<div style="text-align:center;font-size:12px;color:#6b6b6b;margin-bottom:6px;">${title}</div>`;
}
svg += `<svg viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;">`;
svg += `<rect width="${size}" height="${size}" fill="${THEME.bg}" rx="10"/>`;
// 绘制菱形外框线
const cx = size / 2, cy = size / 2, r = size * 0.47;
svg += `<polygon points="${cx},${cy-r} ${cx+r},${cy} ${cx},${cy+r} ${cx-r},${cy}" fill="none" stroke="${THEME.border}" stroke-width="1.5"/>`;
// 内层菱形
const ri = r * 0.5;
svg += `<polygon points="${cx},${cy-ri} ${cx+ri},${cy} ${cx},${cy+ri} ${cx-ri},${cy}" fill="none" stroke="${THEME.border}" stroke-width="1"/>`;
// 对角线
svg += `<line x1="${cx}" y1="${cy-r}" x2="${cx}" y2="${cy-ri}" stroke="${THEME.border}" stroke-width="0.5"/>`;
svg += `<line x1="${cx}" y1="${cy+ri}" x2="${cx}" y2="${cy+r}" stroke="${THEME.border}" stroke-width="0.5"/>`;
svg += `<line x1="${cx-r}" y1="${cy}" x2="${cx-ri}" y2="${cy}" stroke="${THEME.border}" stroke-width="0.5"/>`;
svg += `<line x1="${cx+ri}" y1="${cy}" x2="${cx+r}" y2="${cy}" stroke="${THEME.border}" stroke-width="0.5"/>`;
// 各宫位
for (let house = 1; house <= 12; house++) {
const [row, col] = houseCells[house];
const x = col * cellSize;
const y = row * cellSize;
const isAsc = house === 1;
// 计算该宫的星座
const signIdx = (ascIdx + house - 1) % 12;
const signName = SIGNS[signIdx];
// 背景高亮上升宫
if (isAsc) {
svg += `<rect x="${x+2}" y="${y+2}" width="${cellSize-4}" height="${cellSize-4}" fill="${THEME.bgAsc}" rx="3" opacity="0.5"/>`;
}
// 星座名
const signColor = isAsc ? THEME.signTextAsc : THEME.signText;
svg += `<text x="${x+6}" y="${y+13}" font-size="8" fill="${signColor}" font-family="-apple-system,BlinkMacSystemFont,sans-serif" font-weight="${isAsc?'600':'400'}">${SIGNS_EN_SHORT[signName]}</text>`;
// 宫位编号
svg += `<text x="${x+cellSize-6}" y="${y+13}" font-size="7" fill="${THEME.houseNum}" text-anchor="end" font-family="sans-serif">H${house}</text>`;
// 行星
const pInHouse = housePlanets[house] || [];
let py = y + 26;
for (const p of pInHouse) {
const symbol = PLANET_SYMBOLS[p.name] || '';
let pColor = THEME.planetDefault;
if (p.status === '入旺') pColor = THEME.planetExalted;
else if (p.status === '落陷') pColor = THEME.planetDebilitated;
else if (p.status === '入庙') pColor = THEME.planetOwn;
const retroMark = p.retrograde ? ' ℞' : '';
const degStr = (p.degree_in_sign != null) ? p.degree_in_sign.toFixed(1) + '°' : '';
svg += `<text x="${x+6}" y="${py}" font-size="11" fill="${pColor}" font-family="sans-serif">${symbol}${degStr}${retroMark}</text>`;
py += 14;
}
// 上升标记
if (isAsc) {
svg += `<text x="${x+cellSize/2}" y="${y+cellSize-6}" font-size="9" fill="${THEME.ascLabel}" text-anchor="middle" font-weight="600" font-family="sans-serif">ASC</text>`;
}
}
svg += '</svg>';
container.innerHTML = svg;
}
// 全局默认渲染函数(可切换风格)
let _chartStyle = 'south';
export function setChartStyle(style) { _chartStyle = style; }
export function getChartStyle() { return _chartStyle; }
export function renderChart(container, chartData, options = {}) {
if (_chartStyle === 'north') {
renderNorthIndianChart(container, chartData, options);
} else {
renderSouthIndianChart(container, chartData, options);
}
}