feat(chart): Moon chart from D1 and transit chart for today in the natal houses (T4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2071dc7c18
commit
3f6bcb081a
@@ -37,7 +37,8 @@ export function ChartPageRoute() {
|
||||
* the natal packet that is already on the page, so they request nothing.
|
||||
*/
|
||||
function chartTypeLayer(id: ChartTypeId): ChartViewLayer | null {
|
||||
if (id === "D1" || id === "MOON" || id === "BHAVA" || id === "TRANSIT") return null;
|
||||
if (id === "D1" || id === "MOON" || id === "BHAVA") return null;
|
||||
if (id === "TRANSIT") return "transit";
|
||||
return "varga";
|
||||
}
|
||||
|
||||
@@ -86,6 +87,8 @@ export function ChartPageView({
|
||||
if (layer) onNeedLayer?.(layer);
|
||||
}
|
||||
|
||||
const vedicLayer = chartTypeLayer(vargaId);
|
||||
|
||||
const birthline = view != null && view.status === "ok"
|
||||
? `${view.profile.name} · ${view.profile.date} ${view.profile.time} · ${view.profile.placeLabel}`
|
||||
: null;
|
||||
@@ -127,8 +130,8 @@ export function ChartPageView({
|
||||
view={view}
|
||||
vargaId={vargaId}
|
||||
onSelectVarga={selectVarga}
|
||||
pending={pendingLayers?.has("varga") ?? false}
|
||||
failure={layerFailures?.get("varga")}
|
||||
pending={vedicLayer ? pendingLayers?.has(vedicLayer) ?? false : false}
|
||||
failure={vedicLayer ? layerFailures?.get(vedicLayer) : undefined}
|
||||
/>
|
||||
) : null}
|
||||
{tab === "basics" ? <ChartBasicsTab view={view} /> : null}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { VedicChartSvg } from "@/components/personal-report/vedic-chart-svg";
|
||||
import type {
|
||||
ChartViewFailureStatus,
|
||||
ChartViewAscendantRow,
|
||||
ChartViewOk,
|
||||
ChartViewPlanetRow,
|
||||
ChartViewTransitOk,
|
||||
ChartViewVarga,
|
||||
ChartViewVargaAscendantRow,
|
||||
ChartViewVargaRow,
|
||||
NorthIndianChart,
|
||||
} from "@/lib/chart-view-contract";
|
||||
import { BIRTH_TIME_SENSITIVE_VARGAS, CHART_VIEW_COPY, type ChartTypeId } from "@/lib/chart-view-labels";
|
||||
import { ChartTypePicker } from "./chart-type-picker";
|
||||
@@ -71,8 +75,11 @@ function PlanetRow({ row }: { row: ChartViewPlanetRow }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** The ascendant is a chart point: the engine returns no nakshatra for it. */
|
||||
function AscendantRow({ row }: { row: ChartViewAscendantRow }) {
|
||||
/**
|
||||
* The ascendant is a chart point: the engine returns no nakshatra for it.
|
||||
* `house` is 1 except on the Moon chart, which counts from the Moon.
|
||||
*/
|
||||
function AscendantRow({ row, house = row.house }: { row: ChartViewAscendantRow; house?: number }) {
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row">
|
||||
@@ -80,7 +87,7 @@ function AscendantRow({ row }: { row: ChartViewAscendantRow }) {
|
||||
</th>
|
||||
<td className="chart-page-planet-num">{row.signLabel} {formatDegreeMinutes(row.degreeInSign)}</td>
|
||||
<td className="chart-page-planet-mid">顺</td>
|
||||
<td className="chart-page-planet-mid">{row.house}宫</td>
|
||||
<td className="chart-page-planet-mid">{house}宫</td>
|
||||
<td>{row.nakshatra ?? "—"}</td>
|
||||
<td className="chart-page-planet-mid">
|
||||
{row.nakshatraLord ? <BodyGlyph name={row.nakshatraLord} /> : "—"}
|
||||
@@ -95,7 +102,12 @@ function AscendantRow({ row }: { row: ChartViewAscendantRow }) {
|
||||
* The natal eight-column table. Everything the chart stopped drawing lands
|
||||
* here: the chart carries symbols, this table carries the numbers behind them.
|
||||
*/
|
||||
function NatalTable({ view, caption }: { view: ChartViewOk; caption: string }) {
|
||||
function NatalTable({ planets, ascendant, ascendantHouse, caption }: {
|
||||
planets: readonly ChartViewPlanetRow[];
|
||||
ascendant: ChartViewAscendantRow | null;
|
||||
ascendantHouse?: number;
|
||||
caption: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
@@ -113,8 +125,8 @@ function NatalTable({ view, caption }: { view: ChartViewOk; caption: string }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{view.vedic.ascendant && <AscendantRow row={view.vedic.ascendant} />}
|
||||
{view.vedic.planets.map((planet) => (
|
||||
{ascendant && <AscendantRow row={ascendant} house={ascendantHouse} />}
|
||||
{planets.map((planet) => (
|
||||
<PlanetRow key={planet.name} row={planet} />
|
||||
))}
|
||||
</tbody>
|
||||
@@ -189,6 +201,122 @@ function PrecisionHint({ view, id }: { view: ChartViewOk; id: string }) {
|
||||
return <p className="chart-page-note chart-page-precision-hint">{CHART_VIEW_COPY.precisionHint}</p>;
|
||||
}
|
||||
|
||||
function TransitTable({ transit }: { transit: ChartViewTransitOk }) {
|
||||
return (
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>行运 · {transit.date} {transit.time}({transit.timezoneLabel})</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>
|
||||
{transit.rows.map((row) => {
|
||||
const key = planetGlyphKey(row.name);
|
||||
return (
|
||||
<tr key={row.name}>
|
||||
<th scope="row">
|
||||
<PlanetCell name={row.name} label={row.label} abbr={key ? PLANET_ABBR[key] : "—"} />
|
||||
</th>
|
||||
<td className="chart-page-planet-num">{row.signLabel} {formatDegreeMinutes(row.degreeInSign)}</td>
|
||||
<td className="chart-page-planet-mid">{row.retrograde ? "逆" : "顺"}</td>
|
||||
<td className="chart-page-planet-mid">{row.natalHouse}宫</td>
|
||||
<td className="chart-page-planet-mid">{row.natalPlanetHouse === null ? "—" : `${row.natalPlanetHouse}宫`}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Chart, parameter table and boundary line, then the chart type's own table. */
|
||||
function VedicLayout({ view, chart, ariaLabel, ascendantHouse, children }: {
|
||||
view: ChartViewOk;
|
||||
chart: NorthIndianChart;
|
||||
ariaLabel: string;
|
||||
ascendantHouse?: number;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="chart-page-vedic-layout">
|
||||
<div className="chart-page-vedic-stage">
|
||||
<VedicChartSvg chart={chart} ariaLabel={ariaLabel} ascendantHouse={ascendantHouse} />
|
||||
</div>
|
||||
<div className="chart-page-params">
|
||||
<ChartParamTable view={view} />
|
||||
{/* The accuracy boundary sits directly under the parameters it
|
||||
qualifies. Server copy, reproduced verbatim — it says what this
|
||||
chart can and cannot be read for. */}
|
||||
<p className="chart-page-boundary">{view.vedic.boundary}</p>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LayerWaiting({ pending, failure, copy }: { pending: boolean; failure?: ChartViewFailureStatus; copy: string }) {
|
||||
return !pending && failure
|
||||
? <p className="chart-page-note">{chartLayerFailureCopy(failure)}</p>
|
||||
: <ChartPageSkeleton copy={copy} />;
|
||||
}
|
||||
|
||||
function ChartTypeBody({ view, vargaId, pending, failure }: {
|
||||
view: ChartViewOk;
|
||||
vargaId: string;
|
||||
pending: boolean;
|
||||
failure?: ChartViewFailureStatus;
|
||||
}) {
|
||||
if (vargaId === "MOON") {
|
||||
const moon = view.vedic.moon;
|
||||
if (!moon) return <p className="chart-page-note">{CHART_VIEW_COPY.unavailable}</p>;
|
||||
return (
|
||||
<VedicLayout view={view} chart={moon.chart} ascendantHouse={moon.ascendantHouse} ariaLabel="月亮盘 北印度盘 以月亮所在星座为第 1 宫">
|
||||
<NatalTable
|
||||
planets={moon.planets}
|
||||
ascendant={view.vedic.ascendant}
|
||||
ascendantHouse={moon.ascendantHouse}
|
||||
caption={`月亮盘 · 以月亮所在星座(${moon.moonSignLabel})为第 1 宫`}
|
||||
/>
|
||||
</VedicLayout>
|
||||
);
|
||||
}
|
||||
if (vargaId === "TRANSIT") {
|
||||
const transit = view.transit;
|
||||
if (transit.status !== "ok") return <LayerWaiting pending={pending} failure={failure} copy={CHART_VIEW_COPY.waitingChart} />;
|
||||
return (
|
||||
<VedicLayout view={view} chart={transit.chart} ariaLabel={`行运 北印度盘 ${transit.date} 以本命上升为第 1 宫`}>
|
||||
<div className="chart-page-varga-table-block">
|
||||
<p className="chart-page-note chart-page-table-note">{CHART_VIEW_COPY.transitNote}</p>
|
||||
<TransitTable transit={transit} />
|
||||
</div>
|
||||
</VedicLayout>
|
||||
);
|
||||
}
|
||||
const selected = view.vedic.vargas.find((item) => item.id === vargaId);
|
||||
const natal = view.vedic.vargas.find((item) => item.id === "D1") ?? view.vedic.vargas[0];
|
||||
const varga = selected ?? (vargaId === "D1" ? natal : undefined);
|
||||
if (!varga) return <LayerWaiting pending={pending} failure={failure} copy={CHART_VIEW_COPY.waitingVarga} />;
|
||||
return (
|
||||
<VedicLayout view={view} chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`}>
|
||||
{varga.id === "D1" || !varga.rows ? (
|
||||
<NatalTable planets={view.vedic.planets} ascendant={view.vedic.ascendant} caption={`${varga.id} · ${varga.meaning}`} />
|
||||
) : (
|
||||
<div className="chart-page-varga-table-block">
|
||||
<PrecisionHint view={view} id={varga.id} />
|
||||
<VargaTable varga={varga} />
|
||||
</div>
|
||||
)}
|
||||
</VedicLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartVedicTab({
|
||||
view,
|
||||
vargaId,
|
||||
@@ -203,39 +331,10 @@ export function ChartVedicTab({
|
||||
failure?: ChartViewFailureStatus;
|
||||
onSelectVarga: (id: ChartTypeId) => void;
|
||||
}) {
|
||||
const selected = view.vedic.vargas.find((item) => item.id === vargaId);
|
||||
const natal = view.vedic.vargas.find((item) => item.id === "D1") ?? view.vedic.vargas[0];
|
||||
const varga = selected ?? (vargaId === "D1" ? natal : undefined);
|
||||
const waiting = vargaId !== "D1" && !selected;
|
||||
|
||||
return (
|
||||
<div className="chart-page-vedic">
|
||||
<ChartTypePicker current={vargaId} onSelect={onSelectVarga} />
|
||||
{waiting || !varga ? (
|
||||
!pending && failure ? <p className="chart-page-note">{chartLayerFailureCopy(failure)}</p>
|
||||
: <ChartPageSkeleton copy={CHART_VIEW_COPY.waitingVarga} />
|
||||
) : (
|
||||
<div className="chart-page-vedic-layout">
|
||||
<div className="chart-page-vedic-stage">
|
||||
<VedicChartSvg chart={varga.chart} ariaLabel={`${varga.id} 北印度盘 ${varga.meaning}`} />
|
||||
</div>
|
||||
<div className="chart-page-params">
|
||||
<ChartParamTable view={view} />
|
||||
{/* The accuracy boundary sits directly under the parameters it
|
||||
qualifies. Server copy, reproduced verbatim — it says what this
|
||||
chart can and cannot be read for. */}
|
||||
<p className="chart-page-boundary">{view.vedic.boundary}</p>
|
||||
</div>
|
||||
{varga.id === "D1" || !varga.rows ? (
|
||||
<NatalTable view={view} caption={`${varga.id} · ${varga.meaning}`} />
|
||||
) : (
|
||||
<div className="chart-page-varga-table-block">
|
||||
<PrecisionHint view={view} id={varga.id} />
|
||||
<VargaTable varga={varga} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<ChartTypeBody view={view} vargaId={vargaId} pending={pending} failure={failure} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ interface VedicChartSvgProps {
|
||||
chart: NorthIndianChartModel;
|
||||
ariaLabel: string;
|
||||
highlightHouseNumbers?: ReadonlySet<number>;
|
||||
/**
|
||||
* House that holds the natal ascendant. 1 for every chart counted from the
|
||||
* ascendant (the default); the chart page's Moon chart counts from the Moon,
|
||||
* so its lagna mark sits wherever the natal ascendant falls from there.
|
||||
*/
|
||||
ascendantHouse?: number;
|
||||
}
|
||||
|
||||
type ChartToken =
|
||||
@@ -132,9 +138,9 @@ function houseTokens(
|
||||
* is drawn from geometry rather than from data. An explicit ascendant occupant
|
||||
* (the report bundle can carry one) collapses into the same mark.
|
||||
*/
|
||||
function withAscendantMark(houseNumber: number, tokens: ChartToken[]): ChartToken[] {
|
||||
function withAscendantMark(houseNumber: number, tokens: ChartToken[], ascendantHouse = 1): ChartToken[] {
|
||||
const rest = tokens.filter((token) => token.kind !== "ascendant");
|
||||
return houseNumber === 1 ? [{ kind: "ascendant" }, ...rest] : rest;
|
||||
return houseNumber === ascendantHouse ? [{ kind: "ascendant" }, ...rest] : rest;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,7 +285,7 @@ export function VedicChartSvg(props: VedicChartSvgProps) {
|
||||
}
|
||||
|
||||
/** Shared, hook-free drawing for the reader and offline Markdown export. */
|
||||
export function VedicChartSvgDrawing({ chart: suppliedChart, ariaLabel, highlightHouseNumbers, skeleton = false, chartId }: VedicChartSvgProps & { chartId: string }) {
|
||||
export function VedicChartSvgDrawing({ chart: suppliedChart, ariaLabel, highlightHouseNumbers, skeleton = false, chartId, ascendantHouse = 1 }: VedicChartSvgProps & { chartId: string }) {
|
||||
const chart = skeleton ? EMPTY_NORTH_INDIAN_CHART : suppliedChart;
|
||||
const retrograde = retrogradeKeys(chart);
|
||||
const tokensByHouse = new Map<number, ChartToken[]>();
|
||||
@@ -291,7 +297,9 @@ export function VedicChartSvgDrawing({ chart: suppliedChart, ariaLabel, highligh
|
||||
const hasPlanets = [...tokensByHouse.values()]
|
||||
.some((tokens) => tokens.some((token) => token.kind === "planet"));
|
||||
if (hasPlanets) {
|
||||
tokensByHouse.set(1, withAscendantMark(1, tokensByHouse.get(1) ?? []));
|
||||
// An explicit ascendant occupant anywhere collapses into the one mark.
|
||||
if (ascendantHouse !== 1) tokensByHouse.set(1, withAscendantMark(1, tokensByHouse.get(1) ?? [], ascendantHouse));
|
||||
tokensByHouse.set(ascendantHouse, withAscendantMark(ascendantHouse, tokensByHouse.get(ascendantHouse) ?? [], ascendantHouse));
|
||||
}
|
||||
const viewBoxHeight = CHART_SIZE + (hasPlanets ? LEGEND_BAND_HEIGHT : 0);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user