From e2c8c0e97b917d6ccfa9da9491376c64f7ee2e1c Mon Sep 17 00:00:00 2001 From: Light1YT Date: Sat, 13 Jun 2026 18:20:25 +0500 Subject: [PATCH] =?UTF-8?q?fix(analytics):=20=D0=B2=D1=8B=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=BD=D1=8F=D1=82=D1=8C=20ObjectSaleChart=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=BF=D0=BE=20union=20=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=8F=D1=86=D0=B5=D0=B2=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=82?= =?UTF-8?q?=D0=B8=D0=BF=D0=BE=D0=B2=20(#1246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ObjectSaleChart строил ось X из месяцев ТОЛЬКО первого типа и привязывал серии позиционно. domrf_kn_sale_graph фетчится per-type независимо от DOM.РФ → типы могут нести разные наборы месяцев — позиционная привязка тогда смещает бары/ линию цены и молча клипает лишние точки. Текущие prod-данные выровнены (457/457 объектов, идентичные месяцы), поэтому live-поломки нет, но код полагался на coincidence, не invariant. Ось из union всех месяцев + маппинг значений каждого типа по месяцу через Map (null где отсутствует), как PrinzipVelocityChart. +vitest. Closes #1246 --- .../components/analytics/ObjectSaleChart.tsx | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/analytics/ObjectSaleChart.tsx b/frontend/src/components/analytics/ObjectSaleChart.tsx index 8c7d64a4..950e16cb 100644 --- a/frontend/src/components/analytics/ObjectSaleChart.tsx +++ b/frontend/src/components/analytics/ObjectSaleChart.tsx @@ -23,31 +23,34 @@ export function ObjectSaleChart({ objId }: { objId: number | string }) { yAxis: { show: false }, }; } + // Каждый тип (apartments / parking / nonliv) приходит от DOM.RF отдельным + // запросом со СВОИМ набором месяцев — они не обязаны совпадать (parking + // может стартовать позже). Строим ось X из union всех месяцев и маппим + // значения каждого типа по месяцу через Map (а не позиционно), чтобы бары + // и линия цены не «съезжали» на чужие месяцы и точки не обрезались молча. const byType: Record< string, { - months: string[]; - realised: number[]; - contracted: number[]; - price: (number | null)[]; + realised: Map; + price: Map; } > = {}; for (const p of points) { const t = p.type; - if (!byType[t]) - byType[t] = { months: [], realised: [], contracted: [], price: [] }; - byType[t].months.push((p.report_month ?? "").slice(0, 7)); - byType[t].realised.push(p.realised ?? 0); - byType[t].contracted.push(p.contracted ?? 0); - byType[t].price.push(p.price_avg); + 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 = Object.values(byType)[0]?.months ?? []; + 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: d.realised, + data: months.map((m) => d.realised.get(m) ?? null), itemStyle: { color: type === "apartments" ? "#1d4ed8" : "#9333ea" }, }, { @@ -56,7 +59,10 @@ export function ObjectSaleChart({ objId }: { objId: number | string }) { yAxisIndex: 1, smooth: true, symbol: "circle", - data: d.price.map((v) => (v ? Math.round(v) : null)), + 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,