feat: implement dynamic color generation for charts and improve color usage in analysis and dashboard components

This commit is contained in:
Yoga Pangestu 2026-08-14 04:00:22 +07:00
parent ec5da5c5ba
commit 502f2fdf77
4 changed files with 84 additions and 101 deletions

View File

@ -85,12 +85,12 @@ :root {
--border: oklch(0.922 0 0); --border: oklch(0.922 0 0);
--input: oklch(0.922 0 0); --input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0); --ring: oklch(0.708 0 0);
--chart-1: oklch(0.879 0.169 91.605); --chart-1: oklch(0.65 0.15 250);
--chart-2: oklch(0.769 0.188 70.08); --chart-2: oklch(0.72 0.17 145);
--chart-3: oklch(0.666 0.179 58.318); --chart-3: oklch(0.75 0.18 70);
--chart-4: oklch(0.555 0.163 48.998); --chart-4: oklch(0.60 0.18 300);
--chart-5: oklch(0.473 0.137 46.201); --chart-5: oklch(0.65 0.20 25);
--chart-6: oklch(0.696 0.17 162.48); --chart-6: oklch(0.75 0.12 195);
--radius: 0.625rem; --radius: 0.625rem;
--sidebar: oklch(0.985 0 0); --sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0); --sidebar-foreground: oklch(0.145 0 0);
@ -122,12 +122,12 @@ .dark {
--border: oklch(1 0 0 / 10%); --border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%); --input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0); --ring: oklch(0.556 0 0);
--chart-1: oklch(0.879 0.169 91.605); --chart-1: oklch(0.70 0.15 250);
--chart-2: oklch(0.769 0.188 70.08); --chart-2: oklch(0.77 0.17 145);
--chart-3: oklch(0.666 0.179 58.318); --chart-3: oklch(0.80 0.18 70);
--chart-4: oklch(0.555 0.163 48.998); --chart-4: oklch(0.65 0.18 300);
--chart-5: oklch(0.473 0.137 46.201); --chart-5: oklch(0.70 0.20 25);
--chart-6: oklch(0.696 0.17 162.48); --chart-6: oklch(0.80 0.12 195);
--sidebar: oklch(0.205 0 0); --sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0); --sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.769 0.188 70.08); --sidebar-primary: oklch(0.769 0.188 70.08);

View File

@ -18,3 +18,14 @@ export function formatCurrency(amount: number): string {
minimumFractionDigits: 0, minimumFractionDigits: 0,
}).format(amount); }).format(amount);
} }
export function generateRandomColors(count: number): string[] {
const colors: string[] = [];
for (let i = 0; i < count; i++) {
const hue = Math.floor(Math.random() * 360);
const saturation = 65 + Math.floor(Math.random() * 20);
const lightness = 45 + Math.floor(Math.random() * 20);
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
}
return colors;
}

View File

@ -12,6 +12,7 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { useCan } from '@/hooks/use-can'; import { useCan } from '@/hooks/use-can';
import { generateRandomColors } from '@/lib/utils';
import { formatRupiah } from '@/lib/rupiah'; import { formatRupiah } from '@/lib/rupiah';
import { Head, router } from '@inertiajs/react'; import { Head, router } from '@inertiajs/react';
import { import {
@ -198,85 +199,59 @@ type AnalysisProps = {
}>; }>;
}; };
const revenueChartConfig = { const revenueChartConfig = (() => {
total: { const colors = generateRandomColors(6);
label: 'Total', return {
color: 'var(--chart-1)', total: { label: 'Total', color: colors[0] },
}, gross: { label: 'Keuntungan Kotor', color: colors[1] },
gross: { net: { label: 'Keuntungan Bersih', color: colors[2] },
label: 'Keuntungan Kotor', deduction: { label: 'Potongan Nego', color: colors[3] },
color: 'var(--chart-2)', discount: { label: 'Diskon', color: colors[4] },
}, cogs: { label: 'HPP', color: colors[5] },
net: { } satisfies ChartConfig;
label: 'Keuntungan Bersih', })();
color: 'var(--chart-6)',
},
deduction: {
label: 'Potongan Nego',
color: 'var(--chart-3)',
},
discount: {
label: 'Diskon',
color: 'var(--chart-4)',
},
cogs: {
label: 'HPP',
color: 'var(--chart-5)',
},
} satisfies ChartConfig;
const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const; const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const;
const expenseChartConfig = { const expenseChartConfig = (() => {
total: { const colors = generateRandomColors(4);
label: 'Total', return {
color: 'var(--chart-1)', total: { label: 'Total', color: colors[0] },
}, purchase: { label: 'Belanja', color: colors[1] },
purchase: { expense: { label: 'Pengeluaran Toko', color: colors[2] },
label: 'Belanja', advance: { label: 'Kasbon', color: colors[3] },
color: 'var(--chart-2)', } satisfies ChartConfig;
}, })();
expense: {
label: 'Pengeluaran Toko',
color: 'var(--chart-3)',
},
advance: {
label: 'Kasbon',
color: 'var(--chart-4)',
},
} satisfies ChartConfig;
const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const; const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const;
const revenueTrendChartConfig = { const revenueTrendChartConfig = (() => {
qty: { const colors = generateRandomColors(1);
label: 'Qty', return {
color: 'var(--chart-1)', qty: { label: 'Qty', color: colors[0] },
}, } satisfies ChartConfig;
} satisfies ChartConfig; })();
const revenueTrendKeys = ['qty'] as const; const revenueTrendKeys = ['qty'] as const;
const CHANNEL_COLORS: Record<string, string> = { const CHANNEL_COLORS: Record<string, string> = (() => {
store: '#22c55e', const colors = generateRandomColors(3);
shopee: '#ee4d2d', return {
tiktok: '#000000', store: colors[0],
}; shopee: colors[1],
tiktok: colors[2],
};
})();
const PAYMENT_COLORS: Record<string, string> = { const PAYMENT_COLORS: Record<string, string> = (() => {
cash: '#22c55e', const colors = generateRandomColors(4);
transfer: '#60a5fa', return {
qris: '#a855f7', cash: colors[0],
marketplace: '#f97316', transfer: colors[1],
}; qris: colors[2],
marketplace: colors[3],
const PIE_COLORS = [ };
'var(--chart-1)', })();
'var(--chart-2)',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
];
type PieChartItem = { type PieChartItem = {
name: string; name: string;
@ -293,23 +268,25 @@ type DashboardPieChartProps = {
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
const hasData = data.length > 0 && data.some((d) => d.count > 0); const hasData = data.length > 0 && data.some((d) => d.count > 0);
const pieColors = useMemo(() => generateRandomColors(5), []);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
const config: ChartConfig = {}; const config: ChartConfig = {};
data.forEach((item, index) => { data.forEach((item, index) => {
config[item.name] = { config[item.name] = {
label: item.name, label: item.name,
color: PIE_COLORS[index % PIE_COLORS.length], color: pieColors[index % pieColors.length],
}; };
}); });
return config; return config;
}, [data]); }, [data, pieColors]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
return data.map((item) => ({ return data.map((item) => ({
...item, ...item,
fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length], fill: pieColors[data.indexOf(item) % pieColors.length],
})); }));
}, [data]); }, [data, pieColors]);
return ( return (
<Card> <Card>
@ -1042,7 +1019,7 @@ export default function Analysis({
</CardHeader> </CardHeader>
<CardContent className="px-2 sm:p-6"> <CardContent className="px-2 sm:p-6">
{topSuppliers.length > 0 ? ( {topSuppliers.length > 0 ? (
<ChartContainer config={{ amount: { label: 'Total Pembelian', color: 'var(--chart-1)' } }} className="aspect-auto h-[250px] w-full"> <ChartContainer config={{ amount: { label: 'Total Pembelian', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
<BarChart data={topSuppliers.map((s) => ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}> <BarChart data={topSuppliers.map((s) => ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} /> <XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
@ -1084,7 +1061,7 @@ export default function Analysis({
</CardHeader> </CardHeader>
<CardContent className="px-2 sm:p-6"> <CardContent className="px-2 sm:p-6">
{topProducts.length > 0 ? ( {topProducts.length > 0 ? (
<ChartContainer config={{ qty: { label: 'Jumlah Terjual', color: 'var(--chart-3)' } }} className="aspect-auto h-[250px] w-full"> <ChartContainer config={{ qty: { label: 'Jumlah Terjual', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
<BarChart data={topProducts.map((p) => ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}> <BarChart data={topProducts.map((p) => ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} /> <XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
@ -1126,7 +1103,7 @@ export default function Analysis({
</CardHeader> </CardHeader>
<CardContent className="px-2 sm:p-6"> <CardContent className="px-2 sm:p-6">
{topCustomers.length > 0 ? ( {topCustomers.length > 0 ? (
<ChartContainer config={{ amount: { label: 'Total Pesanan', color: 'var(--chart-2)' } }} className="aspect-auto h-[250px] w-full"> <ChartContainer config={{ amount: { label: 'Total Pesanan', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
<BarChart data={topCustomers.map((c) => ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}> <BarChart data={topCustomers.map((c) => ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} /> <XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
@ -1180,7 +1157,7 @@ export default function Analysis({
</CardHeader> </CardHeader>
<CardContent className="px-2 sm:p-6"> <CardContent className="px-2 sm:p-6">
{busyHours.length > 0 ? ( {busyHours.length > 0 ? (
<ChartContainer config={{ orders: { label: 'Pesanan', color: 'var(--chart-1)' } }} className="aspect-auto h-[250px] w-full"> <ChartContainer config={{ orders: { label: 'Pesanan', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
<BarChart data={busyHours} margin={{ left: 12, right: 12 }}> <BarChart data={busyHours} margin={{ left: 12, right: 12 }}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
<XAxis dataKey="hour" tickLine={false} axisLine={false} tickMargin={8} /> <XAxis dataKey="hour" tickLine={false} axisLine={false} tickMargin={8} />

View File

@ -8,6 +8,7 @@ import {
DialogContent, DialogContent,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { useCan } from '@/hooks/use-can'; import { useCan } from '@/hooks/use-can';
import { generateRandomColors } from '@/lib/utils';
import { formatRupiah } from '@/lib/rupiah'; import { formatRupiah } from '@/lib/rupiah';
import { store, update } from '@/routes/admin/hr/attendances'; import { store, update } from '@/routes/admin/hr/attendances';
import { Head, router, usePage } from '@inertiajs/react'; import { Head, router, usePage } from '@inertiajs/react';
@ -467,34 +468,28 @@ type DashboardPieChartProps = {
nameKey: string; nameKey: string;
}; };
const PIE_COLORS = [
'var(--chart-1)',
'var(--chart-2)',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
];
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
const hasData = data.length > 0 && data.some((d) => d.count > 0); const hasData = data.length > 0 && data.some((d) => d.count > 0);
const pieColors = useMemo(() => generateRandomColors(5), []);
const chartConfig = useMemo(() => { const chartConfig = useMemo(() => {
const config: ChartConfig = {}; const config: ChartConfig = {};
data.forEach((item, index) => { data.forEach((item, index) => {
config[item.name] = { config[item.name] = {
label: item.name, label: item.name,
color: PIE_COLORS[index % PIE_COLORS.length], color: pieColors[index % pieColors.length],
}; };
}); });
return config; return config;
}, [data]); }, [data, pieColors]);
const chartData = useMemo(() => { const chartData = useMemo(() => {
return data.map((item) => ({ return data.map((item) => ({
...item, ...item,
fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length], fill: pieColors[data.indexOf(item) % pieColors.length],
})); }));
}, [data]); }, [data, pieColors]);
return ( return (
<Card> <Card>