AI-video-lifeplan-demo/src/DualChartView.tsx

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>
);
};