feat: implement dynamic color generation for charts and improve color usage in analysis and dashboard components
This commit is contained in:
parent
ec5da5c5ba
commit
502f2fdf77
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user