188 lines
5.2 KiB
TypeScript
188 lines
5.2 KiB
TypeScript
import React, { useMemo } from "react";
|
|
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
|
|
|
|
import type { CashFlowData } from "./CashFlowChart";
|
|
import { CashFlowChart } from "./CashFlowChart";
|
|
import { LifePlanChart } from "./LifePlanChart";
|
|
import type { YearlySnapshot } from "./types";
|
|
import { UnifiedHUD } from "./UnifiedHUD";
|
|
|
|
interface DualChartViewProps {
|
|
absoluteFrame?: number;
|
|
chartStartFrame?: number;
|
|
chartEndFrame?: number;
|
|
cashflowData?: {
|
|
timeline: Array<
|
|
CashFlowData & {
|
|
totalExpense: number;
|
|
netCashFlow: number;
|
|
assetBalance: number;
|
|
}
|
|
>;
|
|
meta: {
|
|
currencyUnit: string;
|
|
};
|
|
};
|
|
}
|
|
|
|
export const DualChartView: React.FC<DualChartViewProps> = ({
|
|
absoluteFrame,
|
|
chartStartFrame,
|
|
chartEndFrame,
|
|
cashflowData = { timeline: [], meta: { currencyUnit: "万円" } },
|
|
}) => {
|
|
const frame = useCurrentFrame();
|
|
const { fps } = useVideoConfig(); // durationInFrames removed since we use chartEndFrame
|
|
|
|
// Use absolute frame if provided to maintain animation continuity across segments
|
|
const currentFrame = absoluteFrame !== undefined ? absoluteFrame : frame;
|
|
|
|
// --- Unified Animation Logic (Both charts sync) ---
|
|
// Animation should start at chartStartFrame and end at chartEndFrame
|
|
const start = chartStartFrame !== undefined ? chartStartFrame : 0;
|
|
// Fallback to absolute video end if no specific end frame provided
|
|
const end = chartEndFrame !== undefined ? chartEndFrame : 300 * fps;
|
|
|
|
const exactProgress = interpolate(
|
|
currentFrame,
|
|
[start, end],
|
|
[0, cashflowData.timeline.length - 1],
|
|
{ extrapolateRight: "clamp", extrapolateLeft: "clamp" },
|
|
);
|
|
|
|
const dataEndIndex = Math.floor(exactProgress);
|
|
const yearProgress = exactProgress - dataEndIndex;
|
|
|
|
// Current data for HUD
|
|
const currentData =
|
|
cashflowData.timeline[dataEndIndex] || cashflowData.timeline[0];
|
|
|
|
// --- Convert cashflow data to LifePlanChart format ---
|
|
// useMemo prevents re-creating this array every frame, which causes chart flickering
|
|
const lifePlanTimeline: YearlySnapshot[] = useMemo(
|
|
() =>
|
|
cashflowData.timeline.map((item) => ({
|
|
year: item.year,
|
|
ages: {
|
|
rep: item.age,
|
|
spouse: undefined, // No spouse data in cashflow
|
|
},
|
|
financials: {
|
|
income: item.income.total,
|
|
expenditure: item.totalExpense,
|
|
assetBalance: item.assetBalance,
|
|
},
|
|
events: [],
|
|
isCritical: item.assetBalance < 0,
|
|
})),
|
|
[cashflowData.timeline],
|
|
);
|
|
|
|
// Memoize meta object to prevent re-creating it every frame
|
|
const lifePlanMeta = useMemo(
|
|
() => ({
|
|
currencyUnit: cashflowData.meta.currencyUnit,
|
|
startYear: cashflowData.timeline[0].year,
|
|
}),
|
|
[cashflowData.meta.currencyUnit, cashflowData.timeline],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: "100%",
|
|
backgroundColor: "white",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: "20px",
|
|
padding: "20px",
|
|
}}
|
|
>
|
|
{/* --- Upper Chart: LifePlan (Asset Balance Trend) --- */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
backgroundColor: "white",
|
|
border: "2px solid #e2e8f0",
|
|
borderRadius: "8px",
|
|
overflow: "hidden",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: "10px 20px",
|
|
borderBottom: "2px solid #e2e8f0",
|
|
background: "#4299E1",
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
margin: 0,
|
|
fontSize: "16px",
|
|
fontWeight: "700",
|
|
color: "white",
|
|
}}
|
|
>
|
|
資産残高推移
|
|
</h3>
|
|
</div>
|
|
<div style={{ flex: 1, minHeight: 0 }}>
|
|
<LifePlanChart
|
|
key="lifeplan-chart"
|
|
timeline={lifePlanTimeline}
|
|
meta={lifePlanMeta}
|
|
hideHeader={true}
|
|
externalDataIndex={dataEndIndex}
|
|
externalFraction={yearProgress}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* --- Lower Chart: CashFlow (Income vs Expenses) --- */}
|
|
<div
|
|
style={{
|
|
flex: 1,
|
|
backgroundColor: "white",
|
|
border: "2px solid #e2e8f0",
|
|
borderRadius: "8px",
|
|
overflow: "hidden",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
padding: "10px 20px",
|
|
borderBottom: "2px solid #e2e8f0",
|
|
background: "#ED8936",
|
|
}}
|
|
>
|
|
<h3
|
|
style={{
|
|
margin: 0,
|
|
fontSize: "16px",
|
|
fontWeight: "700",
|
|
color: "white",
|
|
}}
|
|
>
|
|
収支明細
|
|
</h3>
|
|
</div>
|
|
<div style={{ flex: 1, minHeight: 0 }}>
|
|
<CashFlowChart
|
|
key="cashflow-chart"
|
|
timeline={cashflowData.timeline}
|
|
meta={cashflowData.meta}
|
|
hideHeader={true}
|
|
externalDataIndex={dataEndIndex}
|
|
externalYearProgress={yearProgress}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|