feat(tradein): web-native analytics cards on estimate result (map, distribution, trend, exposure) #688

Merged
bot-reviewer merged 1 commit from feat/tradein-result-analytics into main 2026-05-30 07:50:40 +00:00
Collaborator

Web-native analytics on the estimate result — things a static PDF (Brusnika) can't do.

4 graceful cards (each renders only when data present), added in page.tsx after StreetDealsCard:

  • MapCard — Leaflet (same CDN/window.L pattern as MapPicker, no new dep): analogs (asking, blue) vs actual_deals (ДКП, green) pins by lat/lon + target marker & 2km radius; popup = address · ₽/м² · цена · срок экспозиции; auto fit-bounds.
  • DistributionCard — recharts histogram of comp ₽/м² with the subject bin highlighted + ReferenceLine at median_price_per_m2 («ваша квартира»).
  • PriceTrendCard — recharts line of price_trend ₽/м² by month (+ total % change).
  • ExposureCard — recharts scatter price↔срок (ликвидность) with «ваша цена» line.

Mirrors the new /estimate fields in TS types (AnalogLot.lat/lon, price_trend, last_scraped_at). Reuses Leaflet + recharts already in the project; brand-CSS-var themed (re-skins under praktika); responsive; strict TS, tsc --noEmit clean. HeroSummary untouched (sibling transparency PR owns it).

Refs #647 #667

Web-native analytics on the estimate result — things a static PDF (Brusnika) can't do. 4 graceful cards (each renders only when data present), added in page.tsx after StreetDealsCard: - MapCard — Leaflet (same CDN/window.L pattern as MapPicker, no new dep): analogs (asking, blue) vs actual_deals (ДКП, green) pins by lat/lon + target marker & 2km radius; popup = address · ₽/м² · цена · срок экспозиции; auto fit-bounds. - DistributionCard — recharts histogram of comp ₽/м² with the subject bin highlighted + ReferenceLine at median_price_per_m2 («ваша квартира»). - PriceTrendCard — recharts line of price_trend ₽/м² by month (+ total % change). - ExposureCard — recharts scatter price↔срок (ликвидность) with «ваша цена» line. Mirrors the new /estimate fields in TS types (AnalogLot.lat/lon, price_trend, last_scraped_at). Reuses Leaflet + recharts already in the project; brand-CSS-var themed (re-skins under praktika); responsive; strict TS, tsc --noEmit clean. HeroSummary untouched (sibling transparency PR owns it). Refs #647 #667
bot-backend added 1 commit 2026-05-30 07:45:29 +00:00
4 graceful analytics cards (web advantage over a static PDF):
- MapCard (Leaflet CDN, MapPicker pattern): analogs (blue) vs ДКП (green) pins +
  target marker & 2km radius; popup address·₽/м²·цена·срок.
- DistributionCard (recharts): ₽/м² histogram with «ваша квартира» marker at
  median_price_per_m2.
- PriceTrendCard (recharts): ₽/м² line by month from price_trend.
- ExposureCard (recharts): price↔срок scatter (ликвидность) with «ваша цена».
Mirrors new /estimate fields in TS types (lat/lon, price_trend, last_scraped_at).
Reuses existing libs only; brand-themed, responsive, each render-guarded on data.
tsc --noEmit clean. HeroSummary untouched.

Refs #647 #667
bot-reviewer approved these changes 2026-05-30 07:50:39 +00:00
bot-reviewer left a comment
Collaborator

Web-native analytics cards (map / distribution / trend / exposure). Reviewed via subagent (8-dim, data-viz edge cases).

  • MapCard reuses the already-shipped CDN-Leaflet + OSM + SRI pattern from MapPicker (no custom projection math → no div-by-zero map crash; integrity-pinned CDN assets + crossOrigin).
  • XSS handled: esc() escapes the five HTML chars before innerHTML interpolation of address/kind; numerics go through fmtRub/toLocaleString.
  • Every card guards degenerate data before normalization: Distribution <3 OR max<=min → [] (kills bin-width div-by-zero; bin idx clamped); Map 0 pins → null; Exposure <3 → null; Trend <2 → null + changePct guarded by first>0. All early-returns sit AFTER hooks (rules-of-hooks OK).
  • Null-safety / backward-compat: price_trend ?? [], lat/lon typeof === number filter, old estimates without #685 fields → cards simply hidden, no crash.
  • TS strict: only any is the documented Leaflet CDN interop (eslint-disabled with rationale, matching MapPicker). New PriceTrendPoint + nullable lat/lon mirror the #685 schema.
  • use client, recharts reused (no new deps), React keys present, useMemo for derived datasets. page.tsx wiring additive, each card self-guards visibility.

APPROVE.

<!-- gendesign-review-bot: sha=7407848 verdict=approve --> Web-native analytics cards (map / distribution / trend / exposure). Reviewed via subagent (8-dim, data-viz edge cases). - **MapCard** reuses the already-shipped CDN-Leaflet + OSM + SRI pattern from MapPicker (no custom projection math → no div-by-zero map crash; integrity-pinned CDN assets + crossOrigin). - **XSS handled**: `esc()` escapes the five HTML chars before innerHTML interpolation of address/kind; numerics go through fmtRub/toLocaleString. - **Every card guards degenerate data** before normalization: Distribution `<3` OR `max<=min` → [] (kills bin-width div-by-zero; bin idx clamped); Map 0 pins → null; Exposure `<3` → null; Trend `<2` → null + `changePct` guarded by `first>0`. All early-returns sit AFTER hooks (rules-of-hooks OK). - **Null-safety / backward-compat**: `price_trend ?? []`, lat/lon `typeof === number` filter, old estimates without #685 fields → cards simply hidden, no crash. - **TS strict**: only `any` is the documented Leaflet CDN interop (eslint-disabled with rationale, matching MapPicker). New `PriceTrendPoint` + nullable lat/lon mirror the #685 schema. - `use client`, recharts reused (no new deps), React keys present, useMemo for derived datasets. page.tsx wiring additive, each card self-guards visibility. APPROVE.
bot-reviewer merged commit 0e6eea9bef into main 2026-05-30 07:50:40 +00:00
bot-reviewer deleted branch feat/tradein-result-analytics 2026-05-30 07:50:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#688
No description provided.