From 97dc4ba0812593b3613a9a435519d43578f8978d Mon Sep 17 00:00:00 2001 From: lekss361 Date: Tue, 12 May 2026 00:54:32 +0300 Subject: [PATCH] fix(site-finder): address PR #87 auto-review minor feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (parcels.py): - Centrality factor: weight=1.0 вместо weight=center_bonus (semantic — decay для bonus не применяется, contribution = bonus IS the value). Защищает future PDF-export/UI который мог бы показать weight отдельно от contribution. - group_totals type: dict[str, dict[str, float | int]] — count это int, contribution и contribution_pct это float. Уточняет hint для future mypy. - Top-3 sort convention — добавлен inline-комментарий: positives "[:3] от descending" (most-positive first); negatives "[-3:][::-1]" (most-negative first). Оба "dominant first". Frontend (ScoreBreakdownPanel.tsx): - Stacked bar legend orphan fix: positive groups идут в legend под баром (как до того); negative groups показываются отдельной строкой ниже "Снижают балл — Шум/трамвай: −0.46". Никаких swatch'ей без bar-сегмента. Per auto-review on 1d1c169. --- backend/app/api/v1/parcels.py | 14 +++++--- .../site-finder/ScoreBreakdownPanel.tsx | 36 ++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/backend/app/api/v1/parcels.py b/backend/app/api/v1/parcels.py index d65b412c..b0dae3b2 100644 --- a/backend/app/api/v1/parcels.py +++ b/backend/app/api/v1/parcels.py @@ -612,7 +612,9 @@ def analyze_parcel( else: center_bonus = 0.0 - # X1 (#47): centrality как отдельный synthetic factor в breakdown + # X1 (#47): centrality как отдельный synthetic factor в breakdown. + # NB: для centrality decay не применяется (bonus IS the value), поэтому + # weight=1.0 семантически — "no decay multiplier"; contribution = center_bonus. if center_bonus > 0: factors_detailed.append( { @@ -621,7 +623,7 @@ def analyze_parcel( "category_ru": "Центральность", "group": "Локация", "value": round(dist_to_center_km, 2), - "weight": center_bonus, + "weight": 1.0, "contribution": round(center_bonus, 2), "verbal": ( f"Близость к центру ЕКБ ({dist_to_center_km:.1f}км) — " @@ -1004,11 +1006,15 @@ def analyze_parcel( f["contribution_pct"] = round(100.0 * abs(f["contribution"]) / abs_total, 1) factors_sorted = sorted(factors_detailed, key=lambda x: x["contribution"], reverse=True) + # Convention: оба top-list'а отсортированы "dominant first": + # positives → most-positive first (просто [:3] от descending sort) + # negatives → most-negative first ([-3:][::-1] = последние три (наиболее + # отрицательные) реверсом, чтобы first item был самым большим минусом) score_top_3_positives = [f for f in factors_sorted if f["contribution"] > 0][:3] score_top_3_negatives = [f for f in factors_sorted if f["contribution"] < 0][-3:][::-1] - # By-group totals — для stacked-bar в UI - group_totals: dict[str, dict[str, float]] = {} + # By-group totals — для stacked-bar в UI. count это int, contribution* — float. + group_totals: dict[str, dict[str, float | int]] = {} for f in factors_detailed: g = group_totals.setdefault( f["group"], {"contribution": 0.0, "count": 0, "contribution_pct": 0.0} diff --git a/frontend/src/components/site-finder/ScoreBreakdownPanel.tsx b/frontend/src/components/site-finder/ScoreBreakdownPanel.tsx index 74d382a1..5f86c52e 100644 --- a/frontend/src/components/site-finder/ScoreBreakdownPanel.tsx +++ b/frontend/src/components/site-finder/ScoreBreakdownPanel.tsx @@ -111,7 +111,8 @@ export function ScoreBreakdownPanel({ color: "#6b7280", }} > - {byGroup.map((g) => ( + {/* Positive groups — visible в баре */} + {positiveGroups.map((g) => (
))}
+ {/* Negative groups — отдельной "drag" линией под баром (legend для bar + использует только positive, чтобы не было orphan swatches без сегмента) */} + {byGroup + .filter((g) => g.contribution < 0) + .map((g) => ( +
+ + + Снижают балл — {g.group}:{" "} + + {fmtContribution(g.contribution)} + + +
+ ))} )}