Add recharts: BarChart + PieChart, switch by chart_type
This commit is contained in:
parent
dde6fdda9d
commit
8a32282c09
@ -14,7 +14,8 @@
|
||||
"lucide-react": "^0.441.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0"
|
||||
"react-router-dom": "^6.26.0",
|
||||
"recharts": "^2.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.5",
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { BarChart as RBarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Cell } from "recharts";
|
||||
|
||||
interface Row {
|
||||
dimension: unknown;
|
||||
value: unknown;
|
||||
@ -9,9 +11,12 @@ interface Props {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const RED_PALETTE = ["#C8102E", "#E84258", "#F26471", "#F69199", "#FABFBF"];
|
||||
|
||||
export default function BarChart({ title, rows, loading }: Props) {
|
||||
const data = (rows ?? []).map(r => ({ label: String(r.dimension ?? "—"), value: Number(r.value ?? 0) }));
|
||||
const max = Math.max(1, ...data.map(d => d.value));
|
||||
const data = (rows ?? [])
|
||||
.map(r => ({ name: String(r.dimension ?? "—"), value: Number(r.value ?? 0) }))
|
||||
.filter(d => d.name && d.name !== "—" && d.value > 0);
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[280px] bg-white dark:bg-zinc-900 rounded-2xl border border-stone-200/80 dark:border-zinc-700/60 px-5 py-4 shadow-sm">
|
||||
@ -27,24 +32,18 @@ export default function BarChart({ title, rows, loading }: Props) {
|
||||
) : data.length === 0 ? (
|
||||
<div className="text-[12px] text-stone-400 dark:text-zinc-500 py-4 text-center">No data</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{data.map((d, i) => {
|
||||
const pct = (d.value / max) * 100;
|
||||
return (
|
||||
<div key={i} className="grid grid-cols-[140px_1fr_48px] items-center gap-2 text-[12px]">
|
||||
<span className="truncate text-stone-600 dark:text-zinc-400" title={d.label}>{d.label}</span>
|
||||
<div className="h-3 bg-stone-100 dark:bg-zinc-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{ width: `${pct}%`, background: "linear-gradient(90deg, #C8102E, #E84258)" }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-right font-semibold tabular-nums text-stone-700 dark:text-zinc-200">
|
||||
{d.value.toLocaleString("en-IN")}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={{ width: "100%", height: 220 }}>
|
||||
<ResponsiveContainer>
|
||||
<RBarChart data={data} margin={{ top: 5, right: 10, left: -10, bottom: 5 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
|
||||
<XAxis dataKey="name" tick={{ fontSize: 11, fill: "#78716c" }} interval={0} />
|
||||
<YAxis tick={{ fontSize: 11, fill: "#78716c" }} allowDecimals={false} />
|
||||
<Tooltip cursor={{ fill: "#fef2f2" }} contentStyle={{ fontSize: 12, borderRadius: 8 }} />
|
||||
<Bar dataKey="value" radius={[6, 6, 0, 0]}>
|
||||
{data.map((_, i) => <Cell key={i} fill={RED_PALETTE[i % RED_PALETTE.length]} />)}
|
||||
</Bar>
|
||||
</RBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
53
src/components/PieChart.tsx
Normal file
53
src/components/PieChart.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { PieChart as RPieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from "recharts";
|
||||
|
||||
interface Row {
|
||||
dimension: unknown;
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
rows: Row[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const RED_PALETTE = ["#C8102E", "#E84258", "#F26471", "#F69199", "#FABFBF", "#FFD7D7"];
|
||||
|
||||
export default function PieChart({ title, rows, loading }: Props) {
|
||||
const data = (rows ?? [])
|
||||
.map(r => ({ name: String(r.dimension ?? "—"), value: Number(r.value ?? 0) }))
|
||||
.filter(d => d.name && d.name !== "—" && d.value > 0);
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-[280px] bg-white dark:bg-zinc-900 rounded-2xl border border-stone-200/80 dark:border-zinc-700/60 px-5 py-4 shadow-sm">
|
||||
{title && (
|
||||
<div className="text-[11px] font-semibold uppercase tracking-widest text-stone-400 dark:text-zinc-500 mb-3">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className="h-[220px] bg-stone-100 dark:bg-zinc-800 rounded-full animate-pulse" />
|
||||
) : data.length === 0 ? (
|
||||
<div className="text-[12px] text-stone-400 dark:text-zinc-500 py-4 text-center">No data</div>
|
||||
) : (
|
||||
<div style={{ width: "100%", height: 220 }}>
|
||||
<ResponsiveContainer>
|
||||
<RPieChart>
|
||||
<Pie data={data} dataKey="value" nameKey="name" innerRadius={45} outerRadius={75} paddingAngle={2}>
|
||||
{data.map((_, i) => <Cell key={i} fill={RED_PALETTE[i % RED_PALETTE.length]} />)}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={{ fontSize: 12, borderRadius: 8 }} />
|
||||
<Legend
|
||||
verticalAlign="middle"
|
||||
align="right"
|
||||
layout="vertical"
|
||||
iconType="circle"
|
||||
wrapperStyle={{ fontSize: 11 }}
|
||||
/>
|
||||
</RPieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, typ
|
||||
import FormModal from "./FormModal";
|
||||
import Tile from "./Tile";
|
||||
import BarChart from "./BarChart";
|
||||
import PieChart from "./PieChart";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action column (config-driven from rv-screen layout)
|
||||
@ -147,7 +148,7 @@ interface Props {
|
||||
|
||||
type AnalyticsSpec =
|
||||
| { kind: "tile"; uid: string; title?: string }
|
||||
| { kind: "chart"; uid: string; title?: string };
|
||||
| { kind: "chart"; uid: string; title?: string; chartType?: string };
|
||||
|
||||
// Walk rv-screen layout to find rv_tile / rv_chart elements (the platform
|
||||
// places them as sibling blocks above the rv_table).
|
||||
@ -157,7 +158,7 @@ function extractAnalyticsElements(layout: any): AnalyticsSpec[] {
|
||||
for (const n of nodes ?? []) {
|
||||
const cfg = n?.config;
|
||||
if (cfg?.type === "rv_tile" && cfg.tile_uid) out.push({ kind: "tile", uid: cfg.tile_uid, title: cfg.title });
|
||||
if (cfg?.type === "rv_chart" && cfg.chart_uid) out.push({ kind: "chart", uid: cfg.chart_uid, title: cfg.title });
|
||||
if (cfg?.type === "rv_chart" && cfg.chart_uid) out.push({ kind: "chart", uid: cfg.chart_uid, title: cfg.title, chartType: cfg.chart_type });
|
||||
if (Array.isArray(n?.children)) walk(n.children);
|
||||
}
|
||||
};
|
||||
@ -275,7 +276,8 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
|
||||
return <Tile key={i} title={a.title} value={tv?.value as number | undefined} loading={loading || dataLoading} />;
|
||||
}
|
||||
const cd = chartData.find(c => c.chart_uid === a.uid);
|
||||
return <BarChart key={i} title={a.title} rows={cd?.rows ?? []} loading={loading || dataLoading} />;
|
||||
const Chart = a.chartType === "pie" ? PieChart : BarChart;
|
||||
return <Chart key={i} title={a.title} rows={cd?.rows ?? []} loading={loading || dataLoading} />;
|
||||
})}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user