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

View File

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

View File

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