Add recharts: BarChart + PieChart, switch by chart_type

This commit is contained in:
Bhanu Prakash Sai Potteri 2026-05-23 11:19:24 +05:30
parent dde6fdda9d
commit 8a32282c09
4 changed files with 79 additions and 24 deletions

View File

@ -14,7 +14,8 @@
"lucide-react": "^0.441.0", "lucide-react": "^0.441.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^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": { "devDependencies": {
"@types/react": "^18.3.5", "@types/react": "^18.3.5",

View File

@ -1,3 +1,5 @@
import { BarChart as RBarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Cell } from "recharts";
interface Row { interface Row {
dimension: unknown; dimension: unknown;
value: unknown; value: unknown;
@ -9,9 +11,12 @@ interface Props {
loading?: boolean; loading?: boolean;
} }
const RED_PALETTE = ["#C8102E", "#E84258", "#F26471", "#F69199", "#FABFBF"];
export default function BarChart({ title, rows, loading }: Props) { export default function BarChart({ title, rows, loading }: Props) {
const data = (rows ?? []).map(r => ({ label: String(r.dimension ?? "—"), value: Number(r.value ?? 0) })); const data = (rows ?? [])
const max = Math.max(1, ...data.map(d => d.value)); .map(r => ({ name: String(r.dimension ?? "—"), value: Number(r.value ?? 0) }))
.filter(d => d.name && d.name !== "—" && d.value > 0);
return ( 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"> <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 ? ( ) : data.length === 0 ? (
<div className="text-[12px] text-stone-400 dark:text-zinc-500 py-4 text-center">No data</div> <div className="text-[12px] text-stone-400 dark:text-zinc-500 py-4 text-center">No data</div>
) : ( ) : (
<div className="space-y-2"> <div style={{ width: "100%", height: 220 }}>
{data.map((d, i) => { <ResponsiveContainer>
const pct = (d.value / max) * 100; <RBarChart data={data} margin={{ top: 5, right: 10, left: -10, bottom: 5 }}>
return ( <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" />
<div key={i} className="grid grid-cols-[140px_1fr_48px] items-center gap-2 text-[12px]"> <XAxis dataKey="name" tick={{ fontSize: 11, fill: "#78716c" }} interval={0} />
<span className="truncate text-stone-600 dark:text-zinc-400" title={d.label}>{d.label}</span> <YAxis tick={{ fontSize: 11, fill: "#78716c" }} allowDecimals={false} />
<div className="h-3 bg-stone-100 dark:bg-zinc-800 rounded-full overflow-hidden"> <Tooltip cursor={{ fill: "#fef2f2" }} contentStyle={{ fontSize: 12, borderRadius: 8 }} />
<div <Bar dataKey="value" radius={[6, 6, 0, 0]}>
className="h-full rounded-full transition-all" {data.map((_, i) => <Cell key={i} fill={RED_PALETTE[i % RED_PALETTE.length]} />)}
style={{ width: `${pct}%`, background: "linear-gradient(90deg, #C8102E, #E84258)" }} </Bar>
/> </RBarChart>
</div> </ResponsiveContainer>
<span className="text-right font-semibold tabular-nums text-stone-700 dark:text-zinc-200">
{d.value.toLocaleString("en-IN")}
</span>
</div>
);
})}
</div> </div>
)} )}
</div> </div>

View 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>
);
}

View File

@ -8,6 +8,7 @@ import { getRVScreen, getRecordView, type RecordViewField, type SearchQuery, typ
import FormModal from "./FormModal"; import FormModal from "./FormModal";
import Tile from "./Tile"; import Tile from "./Tile";
import BarChart from "./BarChart"; import BarChart from "./BarChart";
import PieChart from "./PieChart";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Action column (config-driven from rv-screen layout) // Action column (config-driven from rv-screen layout)
@ -147,7 +148,7 @@ interface Props {
type AnalyticsSpec = type AnalyticsSpec =
| { kind: "tile"; uid: string; title?: string } | { 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 // Walk rv-screen layout to find rv_tile / rv_chart elements (the platform
// places them as sibling blocks above the rv_table). // places them as sibling blocks above the rv_table).
@ -157,7 +158,7 @@ function extractAnalyticsElements(layout: any): AnalyticsSpec[] {
for (const n of nodes ?? []) { for (const n of nodes ?? []) {
const cfg = n?.config; 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_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); 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} />; 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); 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> </div>
) : null; ) : null;