Some checks failed
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 36s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Filesystem Scan (push) Failing after 30s
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 6s
CI / E2E Tests (push) Has been skipped
Deploy / Build API Image (push) Failing after 13s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 4s
CI / AI Services (Python) — Smoke (push) Failing after 5s
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 57s
Deploy / Build Web Image (push) Failing after 6s
Deploy / Build AI Services Image (push) Failing after 6s
E2E Tests / Playwright E2E (push) Failing after 12s
Security Scanning / Trivy Scan — API Image (push) Failing after 51s
Security Scanning / Trivy Scan — Web Image (push) Failing after 35s
- price-area-chart + sparkline: replace non-existent `var(--color-signal-up)` with proper `hsl(var(--signal-up))` (and same for -down + border + muted-foreground). The previous tokens resolved to undefined, leaving the chart line + sparkline invisible against the dark background. - public/page: switch `currentPeriod()` from monthly (YYYY-MM) to quarterly (YYYY-Qn) to match the MarketIndex aggregation period — heatmap and district stats now find rows. - import-market-data: add `2026-Q2` to seeded periods so the current quarter has data on a freshly seeded dev DB. - new scripts/seed-bulk-listings-per-district.ts: top up the dev DB with 12 synthetic listings per district per 7-day window so the movers query (which requires >= 10 listings/district/window) has signal to compute against. - update price-area-chart.spec to match new color tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
AreaChart,
|
|
Area,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
} from 'recharts';
|
|
|
|
export interface PriceAreaChartPoint {
|
|
period: string;
|
|
avgPriceM2: number;
|
|
}
|
|
|
|
interface PriceAreaChartProps {
|
|
data: PriceAreaChartPoint[];
|
|
height?: number;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* 30-day price area chart using signal colors.
|
|
* Green fill when latest > first point, red otherwise.
|
|
*/
|
|
export function PriceAreaChart({ data, height = 280, className }: PriceAreaChartProps) {
|
|
const isUp =
|
|
data.length >= 2 && data[data.length - 1]!.avgPriceM2 >= data[0]!.avgPriceM2;
|
|
|
|
// CSS tokens are stored as raw HSL components (`--signal-up: 142 72% 50%`),
|
|
// so they must be wrapped in `hsl(...)`. The previous `var(--color-signal-up)`
|
|
// form referenced a non-existent variable, leaving recharts with `undefined`
|
|
// and rendering an invisible line/area.
|
|
const strokeColor = isUp ? 'hsl(var(--signal-up))' : 'hsl(var(--signal-down))';
|
|
const fillColor = strokeColor;
|
|
|
|
return (
|
|
<div className={className}>
|
|
<ResponsiveContainer width="100%" height={height}>
|
|
<AreaChart data={data} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
|
|
<defs>
|
|
<linearGradient id="priceGradient" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor={fillColor} stopOpacity={0.3} />
|
|
<stop offset="95%" stopColor={fillColor} stopOpacity={0.02} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid
|
|
strokeDasharray="3 3"
|
|
stroke="hsl(var(--border))"
|
|
strokeOpacity={0.5}
|
|
/>
|
|
<XAxis
|
|
dataKey="period"
|
|
tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 11, fill: 'hsl(var(--muted-foreground))' }}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(v: number) =>
|
|
v >= 1_000_000 ? `${(v / 1_000_000).toFixed(0)}tr` : `${Math.round(v / 1000)}k`
|
|
}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: 'hsl(var(--card))',
|
|
border: '1px solid hsl(var(--border))',
|
|
borderRadius: '0.375rem',
|
|
fontSize: '0.75rem',
|
|
fontFamily: 'var(--font-mono)',
|
|
}}
|
|
formatter={(value) => [
|
|
`${(Number(value) / 1_000_000).toFixed(2)} tr/m²`,
|
|
'Giá TB',
|
|
]}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="avgPriceM2"
|
|
stroke={strokeColor}
|
|
strokeWidth={2}
|
|
fill="url(#priceGradient)"
|
|
dot={false}
|
|
activeDot={{ r: 4, strokeWidth: 0 }}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|