3 pages (market, PRINZIP drilldown, developers leaderboard) on top of existing v_developer_full_metrics + domrf_realization views. ECharts on the frontend, FastAPI router /api/v1/analytics on the backend.
70 lines
2 KiB
TypeScript
70 lines
2 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
|
|
import { useMarketPulse } from "@/lib/analytics-api";
|
|
|
|
import { ChartShell } from "./ChartShell";
|
|
|
|
export function MarketPulseChart() {
|
|
const { data, isLoading } = useMarketPulse();
|
|
|
|
const option = useMemo(() => {
|
|
const points = data ?? [];
|
|
const dates = points.map((p) => p.snapshot_date.slice(0, 7));
|
|
return {
|
|
tooltip: { trigger: "axis", axisPointer: { type: "cross" } },
|
|
legend: { data: ["Объём, тыс м²", "sold %", "Цена, ₽/м²"] },
|
|
grid: { left: 56, right: 64, top: 40, bottom: 36 },
|
|
xAxis: { type: "category", data: dates },
|
|
yAxis: [
|
|
{
|
|
type: "value",
|
|
name: "тыс м²",
|
|
position: "left",
|
|
axisLabel: { color: "#5b6066" },
|
|
},
|
|
{
|
|
type: "value",
|
|
name: "% / ₽",
|
|
position: "right",
|
|
axisLabel: { color: "#5b6066" },
|
|
},
|
|
],
|
|
series: [
|
|
{
|
|
name: "Объём, тыс м²",
|
|
type: "bar",
|
|
yAxisIndex: 0,
|
|
data: points.map((p) => p.total_square_th_sqm),
|
|
itemStyle: { color: "rgba(33, 99, 232, 0.55)" },
|
|
},
|
|
{
|
|
name: "sold %",
|
|
type: "line",
|
|
yAxisIndex: 1,
|
|
smooth: true,
|
|
symbol: "circle",
|
|
data: points.map((p) => p.sold_perc),
|
|
lineStyle: { color: "#0a7a3a", width: 2 },
|
|
itemStyle: { color: "#0a7a3a" },
|
|
},
|
|
{
|
|
name: "Цена, ₽/м²",
|
|
type: "line",
|
|
yAxisIndex: 1,
|
|
smooth: true,
|
|
symbol: "circle",
|
|
data: points.map((p) =>
|
|
p.price_avg ? Math.round(p.price_avg / 1000) : null,
|
|
),
|
|
lineStyle: { color: "#c2410c", width: 2 },
|
|
itemStyle: { color: "#c2410c" },
|
|
tooltip: { valueFormatter: (v: number) => `${v} тыс ₽/м²` },
|
|
},
|
|
],
|
|
};
|
|
}, [data]);
|
|
|
|
return <ChartShell option={option} loading={isLoading} height={360} />;
|
|
}
|