gendesign/frontend/src/components/analytics/ObjectSaleChart.tsx
Light1YT e2c8c0e97b
Some checks failed
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been cancelled
fix(analytics): выровнять ObjectSaleChart серии по union месяцев всех типов (#1246)
ObjectSaleChart строил ось X из месяцев ТОЛЬКО первого типа и привязывал серии
позиционно. domrf_kn_sale_graph фетчится per-type независимо от DOM.РФ → типы
могут нести разные наборы месяцев — позиционная привязка тогда смещает бары/
линию цены и молча клипает лишние точки. Текущие prod-данные выровнены (457/457
объектов, идентичные месяцы), поэтому live-поломки нет, но код полагался на
coincidence, не invariant. Ось из union всех месяцев + маппинг значений каждого
типа по месяцу через Map (null где отсутствует), как PrinzipVelocityChart. +vitest.

Closes #1246
2026-06-13 18:20:25 +05:00

91 lines
3.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useMemo } from "react";
import { useObjectSaleGraph } from "@/lib/analytics-api";
import { ChartShell } from "./ChartShell";
export function ObjectSaleChart({ objId }: { objId: number | string }) {
const { data, isLoading } = useObjectSaleGraph(objId);
const option = useMemo(() => {
const points = data ?? [];
if (points.length === 0) {
return {
title: {
text: "Нет данных по продажам",
left: "center",
top: "middle",
textStyle: { color: "#9ca3af", fontSize: 13, fontWeight: 400 },
},
xAxis: { show: false },
yAxis: { show: false },
};
}
// Каждый тип (apartments / parking / nonliv) приходит от DOM.RF отдельным
// запросом со СВОИМ набором месяцев — они не обязаны совпадать (parking
// может стартовать позже). Строим ось X из union всех месяцев и маппим
// значения каждого типа по месяцу через Map (а не позиционно), чтобы бары
// и линия цены не «съезжали» на чужие месяцы и точки не обрезались молча.
const byType: Record<
string,
{
realised: Map<string, number>;
price: Map<string, number | null>;
}
> = {};
for (const p of points) {
const t = p.type;
if (!byType[t]) byType[t] = { realised: new Map(), price: new Map() };
const m = (p.report_month ?? "").slice(0, 7);
byType[t].realised.set(m, p.realised ?? 0);
byType[t].price.set(m, p.price_avg);
}
const months = Array.from(
new Set(points.map((p) => (p.report_month ?? "").slice(0, 7))),
).sort();
const series = Object.entries(byType).flatMap(([type, d]) => [
{
name: `${type} · реализовано`,
type: "bar",
stack: type,
data: months.map((m) => d.realised.get(m) ?? null),
itemStyle: { color: type === "apartments" ? "#1d4ed8" : "#9333ea" },
},
{
name: `${type} · цена ₽/м²`,
type: "line",
yAxisIndex: 1,
smooth: true,
symbol: "circle",
data: months.map((m) => {
const v = d.price.get(m);
return v ? Math.round(v) : null;
}),
lineStyle: { color: type === "apartments" ? "#0a7a3a" : "#c2410c" },
itemStyle: { color: type === "apartments" ? "#0a7a3a" : "#c2410c" },
connectNulls: true,
tooltip: {
valueFormatter: (v: number | null) =>
v == null ? "—" : `${v.toLocaleString("ru")} ₽/м²`,
},
},
]);
return {
tooltip: { trigger: "axis", axisPointer: { type: "cross" } },
legend: {},
grid: { left: 56, right: 64, top: 40, bottom: 36 },
xAxis: { type: "category", data: months },
yAxis: [
{ type: "value", name: "шт", position: "left" },
{ type: "value", name: "₽/м²", position: "right" },
],
series,
};
}, [data]);
return (
<ChartShell option={option} loading={isLoading} height={300} notMerge />
);
}