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",
"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",

View File

@ -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>

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 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;