- HeroSummary: все вхождения ДКП/IMV/CV обёрнуты в <abbr title="..."> с расшифровкой
- IMVBenchmark: «Avito IMV» → abbr с title
- HeroSummary/HeroTransparency: emoji ⚠️ заменены на span-точки var(--warn) + текст
- PriceHistoryChart: stroke="#2563eb"/"#ea580c" → var(--viz-1)/var(--viz-4)
- PriceRangeBar: "#bfdbfe"/"#1d4ed8" → color-mix(--viz-1)/var(--viz-1)
- SellTimeSensitivity: hardcoded bg-цвета → --success-soft/--accent-soft/--warn-soft/--danger-soft; border → var(--viz-1)
- MapCard: пины обновлены на hex-значения --viz-1/--viz-3/--viz-4 (Leaflet JS, не CSS-контекст)
- MapPicker: комментарий -- viz-1 hex (Leaflet constraint)
102 lines
3.2 KiB
TypeScript
102 lines
3.2 KiB
TypeScript
"use client";
|
||
import { useMemo } from "react";
|
||
import {
|
||
LineChart,
|
||
Line,
|
||
XAxis,
|
||
YAxis,
|
||
Tooltip,
|
||
Legend,
|
||
ResponsiveContainer,
|
||
CartesianGrid,
|
||
} from "recharts";
|
||
import type { PriceHistoryYearPoint } from "@/types/trade-in";
|
||
|
||
type Props = { points: PriceHistoryYearPoint[] };
|
||
|
||
const fmtK = (n: number) => `${Math.round(n / 1000)}k`;
|
||
|
||
interface PivotRow {
|
||
year: number;
|
||
avito_imv?: number;
|
||
yandex_valuation?: number;
|
||
n_avito?: number;
|
||
n_yandex?: number;
|
||
}
|
||
|
||
export function PriceHistoryChart({ points }: Props) {
|
||
const pivoted = useMemo<PivotRow[]>(() => {
|
||
const byYear: Record<number, PivotRow> = {};
|
||
for (const p of points) {
|
||
if (!byYear[p.year]) byYear[p.year] = { year: p.year };
|
||
if (p.source === "avito_imv") {
|
||
byYear[p.year].avito_imv = p.median_price_per_m2;
|
||
byYear[p.year].n_avito = p.n_lots;
|
||
} else if (p.source === "yandex_valuation") {
|
||
byYear[p.year].yandex_valuation = p.median_price_per_m2;
|
||
byYear[p.year].n_yandex = p.n_lots;
|
||
}
|
||
}
|
||
return Object.values(byYear).sort((a, b) => a.year - b.year);
|
||
}, [points]);
|
||
|
||
const totalLots = points.reduce((s, p) => s + p.n_lots, 0);
|
||
|
||
return (
|
||
<article className="card" style={{ marginTop: 12, padding: 16 }}>
|
||
<header style={{ marginBottom: 8 }}>
|
||
<h4 style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
|
||
История цен в этом доме
|
||
</h4>
|
||
<small style={{ color: "var(--muted, #6b7280)" }}>
|
||
Медиана ₽/м² по годам · только этот дом · Avito + Яндекс ({totalLots} лотов)
|
||
</small>
|
||
</header>
|
||
{/* #835: декоративный чарт — aria-hidden (данные в тексте/легенде карточки). */}
|
||
<div aria-hidden="true" style={{ width: "100%", height: 240 }}>
|
||
<ResponsiveContainer>
|
||
<LineChart
|
||
data={pivoted}
|
||
margin={{ top: 8, right: 16, left: 8, bottom: 8 }}
|
||
>
|
||
<CartesianGrid stroke="#eee" strokeDasharray="3 3" />
|
||
<XAxis dataKey="year" tick={{ fontSize: 11 }} />
|
||
<YAxis tickFormatter={fmtK} tick={{ fontSize: 11 }} />
|
||
<Tooltip
|
||
formatter={(value: number, name: string) =>
|
||
[
|
||
`${value.toLocaleString("ru-RU")} ₽/м²`,
|
||
name === "avito_imv"
|
||
? "Avito"
|
||
: name === "yandex_valuation"
|
||
? "Яндекс"
|
||
: name,
|
||
]
|
||
}
|
||
labelFormatter={(year) => `${year} год`}
|
||
/>
|
||
<Legend wrapperStyle={{ fontSize: 11 }} />
|
||
<Line
|
||
type="monotone"
|
||
dataKey="avito_imv"
|
||
stroke="var(--viz-1)"
|
||
strokeWidth={2}
|
||
dot={{ r: 3 }}
|
||
name="Avito"
|
||
connectNulls
|
||
/>
|
||
<Line
|
||
type="monotone"
|
||
dataKey="yandex_valuation"
|
||
stroke="var(--viz-4)"
|
||
strokeWidth={2}
|
||
dot={{ r: 3 }}
|
||
name="Яндекс"
|
||
connectNulls
|
||
/>
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</article>
|
||
);
|
||
}
|