feat(chart): add a read-only natal chart page
Open /chart from the sidebar without billing or a model. Five tabs; Western and Qizheng stay unavailable until those endpoints land. Ephemeris nav points at /ephemeris (404 until that page ships).
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
|
||||
import { Menu } from "@base-ui/react/menu";
|
||||
import {
|
||||
CalendarDays,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
FileText,
|
||||
LogOut,
|
||||
Orbit,
|
||||
Settings,
|
||||
WalletCards,
|
||||
MessageSquareText,
|
||||
@@ -16,6 +18,7 @@ import {
|
||||
UserRound,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { Ref } from "react";
|
||||
import {
|
||||
@@ -110,6 +113,8 @@ export function AppSidebar({
|
||||
onOpenLogout,
|
||||
}: AppSidebarProps) {
|
||||
const { isMobile, setOpen, setOpenMobile, state, viewport } = useSidebar();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const firstSessionRef = useRef<HTMLButtonElement>(null);
|
||||
const historyHeadingRef = useRef<HTMLElement>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
@@ -149,6 +154,16 @@ export function AppSidebar({
|
||||
if (isMobile) setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handleOpenChart() {
|
||||
router.push("/chart");
|
||||
if (isMobile) setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handleOpenEphemeris() {
|
||||
router.push("/ephemeris");
|
||||
if (isMobile) setOpenMobile(false);
|
||||
}
|
||||
|
||||
function handleExpandHistory() {
|
||||
setOpen(true);
|
||||
window.requestAnimationFrame(() => {
|
||||
@@ -202,6 +217,30 @@ export function AppSidebar({
|
||||
{showExpandedContent ? <span>{creatingSession ? "正在创建" : "新建对话"}</span> : null}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="report-nav-button"
|
||||
type="button"
|
||||
tooltip="星盘"
|
||||
isActive={pathname === "/chart"}
|
||||
onClick={handleOpenChart}
|
||||
>
|
||||
<Orbit size={18} strokeWidth={1.75} aria-hidden="true" />
|
||||
{showExpandedContent ? <span>星盘</span> : null}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="report-nav-button"
|
||||
type="button"
|
||||
tooltip="星历"
|
||||
isActive={pathname === "/ephemeris" || pathname.startsWith("/ephemeris/")}
|
||||
onClick={handleOpenEphemeris}
|
||||
>
|
||||
<CalendarDays size={18} strokeWidth={1.75} aria-hidden="true" />
|
||||
{showExpandedContent ? <span>星历</span> : null}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
className="report-nav-button"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { formatDegree } from "./chart-format";
|
||||
|
||||
export function ChartBasicsTab({ view }: { view: ChartViewOk }) {
|
||||
return (
|
||||
<div className="chart-page-basics">
|
||||
<p className="chart-page-boundary">{view.vedic.symbolBoundary}</p>
|
||||
<div className="chart-page-basics-grid">
|
||||
{view.vedic.planets.map((planet) => (
|
||||
<article key={planet.name} className="chart-page-planet-card">
|
||||
<h3>{planet.label}</h3>
|
||||
<p>本命 {planet.signLabel} {formatDegree(planet.degreeInSign)} · 第{planet.house}宫</p>
|
||||
<p>{planet.nakshatra} 第{planet.pada}足 · 星宿主 {planet.nakshatraLord}</p>
|
||||
<p>D9 {planet.d9SignLabel ?? "未返回"}</p>
|
||||
{planet.symbol ? <p className="chart-page-symbol">{planet.symbol}</p> : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
|
||||
function Track({
|
||||
title,
|
||||
method,
|
||||
currentLabel,
|
||||
antardashaNote,
|
||||
periods,
|
||||
}: ChartViewOk["dasha"]["vimshottari"]) {
|
||||
return (
|
||||
<section className="chart-page-dasha-track">
|
||||
<header>
|
||||
<h3>{title}</h3>
|
||||
<p>{method}</p>
|
||||
<p>当前 {currentLabel}</p>
|
||||
</header>
|
||||
<ol className="chart-page-dasha-list">
|
||||
{periods.map((period) => (
|
||||
<li
|
||||
key={`${period.lord}-${period.start}`}
|
||||
className={period.current ? "chart-page-dasha-period is-current" : "chart-page-dasha-period"}
|
||||
>
|
||||
<p>
|
||||
<strong>{period.label}</strong>
|
||||
<span>{period.start} – {period.end}</span>
|
||||
</p>
|
||||
{period.antardashas.length > 0 ? (
|
||||
<ul>
|
||||
{period.antardashas.map((antar) => (
|
||||
<li key={`${antar.lord}-${antar.start}`} className={antar.current ? "is-current" : undefined}>
|
||||
{antar.label} · {antar.start} – {antar.end}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{antardashaNote ? <p className="chart-page-note">{antardashaNote}</p> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartDashaTab({ view }: { view: ChartViewOk }) {
|
||||
return (
|
||||
<div className="chart-page-dasha">
|
||||
<div className="chart-page-dasha-columns">
|
||||
<Track {...view.dasha.vimshottari} />
|
||||
<Track {...view.dasha.chara} />
|
||||
</div>
|
||||
<p className="chart-page-boundary">{view.dasha.dualTrackNote}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export function formatCoord(latitude: number, longitude: number): string {
|
||||
const ns = latitude >= 0 ? "N" : "S";
|
||||
const ew = longitude >= 0 ? "E" : "W";
|
||||
return `${Math.abs(latitude).toFixed(2)}°${ns} ${Math.abs(longitude).toFixed(2)}°${ew}`;
|
||||
}
|
||||
|
||||
export function formatDegree(degree: number): string {
|
||||
return `${Math.min(29.9, Math.max(0, degree)).toFixed(1)}°`;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
CHART_VIEW_TABS,
|
||||
type ChartViewResponse,
|
||||
type ChartViewTabId,
|
||||
} from "@/lib/chart-view-contract";
|
||||
import { ChartBasicsTab } from "./chart-basics-tab";
|
||||
import { ChartDashaTab } from "./chart-dasha-tab";
|
||||
import { ChartQizhengTab } from "./chart-qizheng-tab";
|
||||
import { ChartVedicTab } from "./chart-vedic-tab";
|
||||
import { ChartWesternTab } from "./chart-western-tab";
|
||||
|
||||
export function ChartPageView({
|
||||
view,
|
||||
initialTab = "vedic",
|
||||
}: {
|
||||
view: ChartViewResponse;
|
||||
initialTab?: ChartViewTabId;
|
||||
}) {
|
||||
const [tab, setTab] = useState<ChartViewTabId>(initialTab);
|
||||
const [vargaId, setVargaId] = useState(
|
||||
view.status === "ok" ? view.vedic.vargas[0]?.id ?? "D1" : "D1",
|
||||
);
|
||||
|
||||
return (
|
||||
<main className="chart-page-shell">
|
||||
<header className="chart-page-topbar">
|
||||
<Link href="/" className="chart-page-back">
|
||||
<ArrowLeft aria-hidden="true" />
|
||||
返回对话
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
{view.status !== "ok" ? (
|
||||
<section className="chart-page-hero">
|
||||
<p className="chart-page-eyebrow">直接计算 · 打开即有 · 不消耗点数</p>
|
||||
<h1>星盘</h1>
|
||||
<p>{view.message}</p>
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<section className="chart-page-hero">
|
||||
<p className="chart-page-eyebrow">直接计算 · 打开即有 · 不消耗点数</p>
|
||||
<h1>星盘</h1>
|
||||
<p className="chart-page-birthline">
|
||||
{view.profile.name} · {view.profile.date} {view.profile.time} · {view.profile.placeLabel}
|
||||
</p>
|
||||
</section>
|
||||
<div className="chart-page-tabs" role="tablist" aria-label="星盘体系">
|
||||
{CHART_VIEW_TABS.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === item.id}
|
||||
className={tab === item.id ? "chart-page-tab is-current" : "chart-page-tab"}
|
||||
onClick={() => setTab(item.id)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<section className="chart-page-panel" role="tabpanel">
|
||||
{tab === "vedic" ? (
|
||||
<ChartVedicTab
|
||||
view={view}
|
||||
varga={view.vedic.vargas.find((item) => item.id === vargaId) ?? view.vedic.vargas[0]!}
|
||||
onSelectVarga={setVargaId}
|
||||
/>
|
||||
) : null}
|
||||
{tab === "basics" ? <ChartBasicsTab view={view} /> : null}
|
||||
{tab === "dasha" ? <ChartDashaTab view={view} /> : null}
|
||||
{tab === "western" ? <ChartWesternTab view={view} /> : null}
|
||||
{tab === "qizheng" ? <ChartQizhengTab view={view} /> : null}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { formatCoord } from "./chart-format";
|
||||
|
||||
export function ChartParamLines({
|
||||
view,
|
||||
compact,
|
||||
}: {
|
||||
view: ChartViewOk;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const { profile } = view;
|
||||
const place = `${profile.placeLabel} · ${formatCoord(profile.latitude, profile.longitude)}`;
|
||||
if (compact) {
|
||||
return (
|
||||
<>
|
||||
<p>岁差 {profile.ayanamsaDisplay}</p>
|
||||
<p>{profile.nodeModeLabel}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<p>{profile.time} · {profile.birthTimeStatusLabel}</p>
|
||||
<p>{place}</p>
|
||||
<p>岁差 {profile.ayanamsaDisplay}</p>
|
||||
<p>{profile.nodeModeLabel}</p>
|
||||
<p>升 {profile.ascendantSignLabel} · 月宿 {profile.moonNakshatra} 第{profile.moonPada}足</p>
|
||||
<p>{profile.currentDashaLabel} · {profile.engineName}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { COORDINATE_BOUNDARY, QIZHENG_RULES } from "@/lib/chart-view-labels";
|
||||
import { QizhengPalaceGrid } from "./qizheng-palace-grid";
|
||||
|
||||
function Rules() {
|
||||
return (
|
||||
<ul className="chart-page-qizheng-rules">
|
||||
{QIZHENG_RULES.map((rule) => <li key={rule}>{rule}</li>)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartQizhengTab({ view }: { view: ChartViewOk }) {
|
||||
const qizheng = view.qizheng;
|
||||
if (qizheng.status === "unavailable") {
|
||||
return (
|
||||
<div className="chart-page-unavailable">
|
||||
<p className="chart-page-boundary">{COORDINATE_BOUNDARY.qizheng}</p>
|
||||
<p>{qizheng.copy}</p>
|
||||
<Rules />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="chart-page-qizheng">
|
||||
<p className="chart-page-boundary">{qizheng.boundary}</p>
|
||||
<p className="chart-page-note">计都派别:{qizheng.ketuMode} · 坐标系 {qizheng.coordinateSystem}</p>
|
||||
<Rules />
|
||||
<QizhengPalaceGrid qizheng={qizheng} />
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>十一曜宿度</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">曜</th>
|
||||
<th scope="col">宿</th>
|
||||
<th scope="col">宿度</th>
|
||||
<th scope="col">宫</th>
|
||||
<th scope="col">庙旺</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{qizheng.bodies.map((body) => (
|
||||
<tr key={body.name}>
|
||||
<th scope="row">{body.name}</th>
|
||||
<td>{body.mansion}</td>
|
||||
<td>{body.mansionDegree.toFixed(2)}</td>
|
||||
<td>{body.palace}</td>
|
||||
<td>{body.dignityClosed ? body.dignity : `${body.dignity}(未闭合)`}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
|
||||
import type { ChartViewOk, ChartViewVarga } from "@/lib/chart-view-contract";
|
||||
import { formatDegree } from "./chart-format";
|
||||
import { ChartParamLines } from "./chart-param-lines";
|
||||
|
||||
export function ChartVedicTab({
|
||||
view,
|
||||
varga,
|
||||
onSelectVarga,
|
||||
}: {
|
||||
view: ChartViewOk;
|
||||
varga: ChartViewVarga;
|
||||
onSelectVarga: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="chart-page-vedic">
|
||||
<div className="chart-page-varga-chips" role="tablist" aria-label="分盘">
|
||||
{view.vedic.vargas.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={item.id === varga.id}
|
||||
className={item.id === varga.id ? "chart-page-chip is-current" : "chart-page-chip"}
|
||||
onClick={() => onSelectVarga(item.id)}
|
||||
>
|
||||
{item.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="chart-page-vedic-layout">
|
||||
<div className="chart-page-vedic-stage">
|
||||
<VedicChartSvg chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`} />
|
||||
<div className="chart-page-center-card">
|
||||
<div className="chart-page-center-compact">
|
||||
<ChartParamLines view={view} compact />
|
||||
</div>
|
||||
<div className="chart-page-center-extra">
|
||||
<ChartParamLines view={view} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chart-page-params-below">
|
||||
<ChartParamLines view={view} />
|
||||
<p className="chart-page-boundary">{view.vedic.boundary}</p>
|
||||
</div>
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>{varga.id} · {varga.meaning}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">星体</th>
|
||||
<th scope="col">星座与度数</th>
|
||||
<th scope="col">宫位</th>
|
||||
<th scope="col">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{view.vedic.planets.map((planet) => (
|
||||
<tr key={planet.name}>
|
||||
<th scope="row">{planet.label}</th>
|
||||
<td>{planet.signLabel} {formatDegree(planet.degreeInSign)}</td>
|
||||
<td>{planet.house}</td>
|
||||
<td>{planet.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<p className="chart-page-boundary chart-page-boundary-desktop">{view.vedic.boundary}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { ChartViewOk } from "@/lib/chart-view-contract";
|
||||
import { COORDINATE_BOUNDARY } from "@/lib/chart-view-labels";
|
||||
import { WesternWheelSvg } from "./western-wheel-svg";
|
||||
|
||||
export function ChartWesternTab({ view }: { view: ChartViewOk }) {
|
||||
const western = view.western;
|
||||
if (western.status === "unavailable") {
|
||||
return (
|
||||
<div className="chart-page-unavailable">
|
||||
<p className="chart-page-boundary">{COORDINATE_BOUNDARY.western}</p>
|
||||
<p>{western.copy}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="chart-page-western">
|
||||
<div className="chart-page-western-layout">
|
||||
<WesternWheelSvg western={western} />
|
||||
<div className="chart-page-western-side">
|
||||
<p className="chart-page-boundary">{western.boundary}</p>
|
||||
<p className="chart-page-note">{western.ayanamsaGapNote}</p>
|
||||
<section>
|
||||
<h3>元素 / 模式</h3>
|
||||
<p>{western.elements.map((item) => `${item.label} ${item.count}`).join(" · ")}</p>
|
||||
<p>{western.modes.map((item) => `${item.label} ${item.count}`).join(" · ")}</p>
|
||||
</section>
|
||||
<section>
|
||||
<h3>主要相位</h3>
|
||||
<table className="chart-page-planet-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">星体</th>
|
||||
<th scope="col">相位</th>
|
||||
<th scope="col">容许度</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{western.aspects.slice(0, 12).map((aspect) => (
|
||||
<tr key={`${aspect.left}-${aspect.right}-${aspect.aspect}`}>
|
||||
<td>{aspect.left} · {aspect.right}</td>
|
||||
<td>{aspect.aspectLabel}</td>
|
||||
<td>{aspect.orb.toFixed(1)}°</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<p>宫制 {western.houseSystem === "P" ? "Placidus" : western.houseSystem}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ChartViewQizhengOk } from "@/lib/chart-view-contract";
|
||||
import { QIZHENG_BRANCH_GRID } from "@/lib/chart-view-labels";
|
||||
|
||||
export function QizhengPalaceGrid({ qizheng }: { qizheng: ChartViewQizhengOk }) {
|
||||
const byBranch = new Map(qizheng.palaces.map((palace) => [palace.branch, palace]));
|
||||
return (
|
||||
<div className="chart-page-qizheng-grid" role="img" aria-label="七政四余地支十二宫">
|
||||
{QIZHENG_BRANCH_GRID.flatMap((row, rowIndex) => row.map((branch, columnIndex) => {
|
||||
if (!branch) {
|
||||
return <div key={`empty-${rowIndex}-${columnIndex}`} className="chart-page-qizheng-hole" />;
|
||||
}
|
||||
const palace = byBranch.get(branch);
|
||||
return (
|
||||
<article key={branch} className="chart-page-qizheng-cell">
|
||||
<strong>{branch}</strong>
|
||||
<span>{palace?.lifePalace ?? "—"}</span>
|
||||
<small>{palace?.mansions.length ? palace.mansions.join(" ") : "所辖宿未返回"}</small>
|
||||
<p>{palace?.occupants.length ? palace.occupants.join(" ") : "空"}</p>
|
||||
</article>
|
||||
);
|
||||
}))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { ChartViewWesternOk } from "@/lib/chart-view-contract";
|
||||
import { SIGN_ORDER, signZh } from "@/lib/chart-view-labels";
|
||||
|
||||
const SIZE = 400;
|
||||
const CX = SIZE / 2;
|
||||
const CY = SIZE / 2;
|
||||
const OUTER = 188;
|
||||
const ZODIAC = 168;
|
||||
const HOUSE = 128;
|
||||
const INNER = 78;
|
||||
|
||||
function polar(radius: number, deg: number): { x: number; y: number } {
|
||||
const rad = (deg * Math.PI) / 180;
|
||||
return { x: CX + radius * Math.cos(rad), y: CY - radius * Math.sin(rad) };
|
||||
}
|
||||
|
||||
function screenAngle(longitude: number, ascendant: number): number {
|
||||
return 180 - (longitude - ascendant);
|
||||
}
|
||||
|
||||
export function WesternWheelSvg({ western }: { western: ChartViewWesternOk }) {
|
||||
const asc = western.ascendantLongitude;
|
||||
const signWedges = SIGN_ORDER.map((sign, index) => {
|
||||
const mid = screenAngle(index * 30 + 15, asc);
|
||||
return { sign, label: polar((ZODIAC + OUTER) / 2, mid) };
|
||||
});
|
||||
return (
|
||||
<svg
|
||||
viewBox={`0 0 ${SIZE} ${SIZE}`}
|
||||
width="100%"
|
||||
height="auto"
|
||||
role="img"
|
||||
aria-label="回归黄道本命盘,上升在左,宫位按 Placidus"
|
||||
className="chart-page-western-svg"
|
||||
>
|
||||
<circle cx={CX} cy={CY} r={OUTER} className="chart-page-western-ring" />
|
||||
<circle cx={CX} cy={CY} r={ZODIAC} className="chart-page-western-ring" />
|
||||
<circle cx={CX} cy={CY} r={HOUSE} className="chart-page-western-ring" />
|
||||
<circle cx={CX} cy={CY} r={INNER} className="chart-page-western-core" />
|
||||
{signWedges.map((wedge) => {
|
||||
const start = polar(ZODIAC, screenAngle(SIGN_ORDER.indexOf(wedge.sign) * 30, asc));
|
||||
const outer = polar(OUTER, screenAngle(SIGN_ORDER.indexOf(wedge.sign) * 30, asc));
|
||||
return (
|
||||
<g key={wedge.sign}>
|
||||
<line x1={start.x} y1={start.y} x2={outer.x} y2={outer.y} className="chart-page-western-spoke" />
|
||||
<text x={wedge.label.x} y={wedge.label.y} textAnchor="middle" dominantBaseline="middle">
|
||||
{signZh(wedge.sign)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{western.houses.map((house) => {
|
||||
const angle = screenAngle(house.longitude, asc);
|
||||
const inner = polar(INNER, angle);
|
||||
const outer = polar(ZODIAC, angle);
|
||||
const label = polar((INNER + HOUSE) / 2, angle);
|
||||
return (
|
||||
<g key={house.house}>
|
||||
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} className="chart-page-western-house" />
|
||||
<text x={label.x} y={label.y} textAnchor="middle" dominantBaseline="middle" className="chart-page-western-house-label">
|
||||
{house.house}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
{western.planets.map((planet) => {
|
||||
const point = polar((HOUSE + INNER) / 2 + 10, screenAngle(planet.longitude, asc));
|
||||
return (
|
||||
<text key={planet.id} x={point.x} y={point.y} textAnchor="middle" dominantBaseline="middle">
|
||||
{planet.label}
|
||||
</text>
|
||||
);
|
||||
})}
|
||||
<text x={polar(OUTER + 4, 180).x - 8} y={CY} textAnchor="end" dominantBaseline="middle" className="chart-page-western-asc">
|
||||
升
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user