|
|
|
|
|
import { Chart, registerables } from 'chart.js';
|
|
|
|
|
|
Chart.register(...registerables);
|
|
|
|
|
|
|
|
|
export const COLORS = {
|
|
|
revenue: {
|
|
|
primary: 'rgba(52, 211, 153, 0.8)',
|
|
|
secondary: 'rgba(52, 211, 153, 0.2)',
|
|
|
gradient: ['rgba(52, 211, 153, 0.8)', 'rgba(52, 211, 153, 0.2)']
|
|
|
},
|
|
|
profit: {
|
|
|
primary: 'rgba(168, 85, 247, 0.8)',
|
|
|
secondary: 'rgba(168, 85, 247, 0.2)',
|
|
|
gradient: ['rgba(168, 85, 247, 0.8)', 'rgba(168, 85, 247, 0.2)']
|
|
|
},
|
|
|
orders: {
|
|
|
primary: 'rgba(59, 130, 246, 0.8)',
|
|
|
secondary: 'rgba(59, 130, 246, 0.2)',
|
|
|
gradient: ['rgba(59, 130, 246, 0.8)', 'rgba(59, 130, 246, 0.2)']
|
|
|
},
|
|
|
customers: {
|
|
|
primary: 'rgba(251, 146, 60, 0.8)',
|
|
|
secondary: 'rgba(251, 146, 60, 0.2)',
|
|
|
gradient: ['rgba(251, 146, 60, 0.8)', 'rgba(251, 146, 60, 0.2)']
|
|
|
},
|
|
|
text: {
|
|
|
primary: 'rgba(255, 255, 255, 0.9)',
|
|
|
secondary: 'rgba(255, 255, 255, 0.6)',
|
|
|
muted: 'rgba(255, 255, 255, 0.4)'
|
|
|
},
|
|
|
grid: 'rgba(255, 255, 255, 0.1)',
|
|
|
border: 'rgba(255, 255, 255, 0.2)'
|
|
|
};
|
|
|
|
|
|
|
|
|
export const baseChartConfig = {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
interaction: {
|
|
|
intersect: false,
|
|
|
mode: 'index'
|
|
|
},
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
display: true,
|
|
|
position: 'top',
|
|
|
labels: {
|
|
|
color: COLORS.text.primary,
|
|
|
font: {
|
|
|
size: 12,
|
|
|
weight: '500'
|
|
|
},
|
|
|
padding: 20,
|
|
|
usePointStyle: true,
|
|
|
pointStyle: 'circle'
|
|
|
}
|
|
|
},
|
|
|
tooltip: {
|
|
|
enabled: true,
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
|
backdropFilter: 'blur(10px)',
|
|
|
titleColor: COLORS.text.primary,
|
|
|
bodyColor: COLORS.text.secondary,
|
|
|
borderColor: COLORS.border,
|
|
|
borderWidth: 1,
|
|
|
cornerRadius: 12,
|
|
|
padding: 12,
|
|
|
displayColors: true,
|
|
|
callbacks: {
|
|
|
title: function(context) {
|
|
|
return context[0].label;
|
|
|
},
|
|
|
label: function(context) {
|
|
|
const label = context.dataset.label || '';
|
|
|
const value = context.parsed.y;
|
|
|
|
|
|
if (label.toLowerCase().includes('revenue') || label.toLowerCase().includes('profit')) {
|
|
|
return `${label}: ฿${value.toLocaleString()}`;
|
|
|
} else if (label.toLowerCase().includes('percentage') || label.toLowerCase().includes('margin')) {
|
|
|
return `${label}: ${value.toFixed(2)}%`;
|
|
|
}
|
|
|
return `${label}: ${value.toLocaleString()}`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
grid: {
|
|
|
color: COLORS.grid,
|
|
|
borderColor: COLORS.border,
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: COLORS.text.secondary,
|
|
|
font: {
|
|
|
size: 11
|
|
|
},
|
|
|
maxTicksLimit: 12
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
grid: {
|
|
|
color: COLORS.grid,
|
|
|
borderColor: COLORS.border,
|
|
|
drawBorder: false
|
|
|
},
|
|
|
ticks: {
|
|
|
color: COLORS.text.secondary,
|
|
|
font: {
|
|
|
size: 11
|
|
|
},
|
|
|
callback: function(value) {
|
|
|
if (this.chart.config.type === 'line' && this.chart.data.datasets[0].label?.toLowerCase().includes('revenue')) {
|
|
|
return '฿' + value.toLocaleString();
|
|
|
}
|
|
|
return value.toLocaleString();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
elements: {
|
|
|
point: {
|
|
|
radius: 4,
|
|
|
hoverRadius: 6,
|
|
|
borderWidth: 2,
|
|
|
hoverBorderWidth: 3
|
|
|
},
|
|
|
line: {
|
|
|
borderWidth: 3,
|
|
|
tension: 0.4
|
|
|
},
|
|
|
bar: {
|
|
|
borderRadius: 6,
|
|
|
borderSkipped: false
|
|
|
}
|
|
|
},
|
|
|
animation: {
|
|
|
duration: 1000,
|
|
|
easing: 'easeInOutQuart'
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
export function createGradient(ctx, colorArray, direction = 'vertical') {
|
|
|
const gradient = direction === 'vertical'
|
|
|
? ctx.createLinearGradient(0, 0, 0, ctx.canvas.height)
|
|
|
: ctx.createLinearGradient(0, 0, ctx.canvas.width, 0);
|
|
|
|
|
|
gradient.addColorStop(0, colorArray[0]);
|
|
|
gradient.addColorStop(1, colorArray[1]);
|
|
|
|
|
|
return gradient;
|
|
|
}
|
|
|
|
|
|
|
|
|
export function createLineChartConfig(labels, datasets, options = {}) {
|
|
|
return {
|
|
|
type: 'line',
|
|
|
data: {
|
|
|
labels,
|
|
|
datasets: datasets.map(dataset => ({
|
|
|
...dataset,
|
|
|
fill: true,
|
|
|
backgroundColor: function(context) {
|
|
|
const chart = context.chart;
|
|
|
const {ctx} = chart;
|
|
|
return createGradient(ctx, dataset.gradientColors || COLORS.revenue.gradient);
|
|
|
},
|
|
|
borderColor: dataset.borderColor || COLORS.revenue.primary,
|
|
|
pointBackgroundColor: dataset.borderColor || COLORS.revenue.primary,
|
|
|
pointBorderColor: '#ffffff',
|
|
|
pointHoverBackgroundColor: '#ffffff',
|
|
|
pointHoverBorderColor: dataset.borderColor || COLORS.revenue.primary
|
|
|
}))
|
|
|
},
|
|
|
options: {
|
|
|
...baseChartConfig,
|
|
|
...options
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
export function createBarChartConfig(labels, datasets, options = {}) {
|
|
|
return {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels,
|
|
|
datasets: datasets.map(dataset => ({
|
|
|
...dataset,
|
|
|
backgroundColor: function(context) {
|
|
|
const chart = context.chart;
|
|
|
const {ctx} = chart;
|
|
|
return createGradient(ctx, dataset.gradientColors || COLORS.orders.gradient);
|
|
|
},
|
|
|
borderColor: dataset.borderColor || COLORS.orders.primary,
|
|
|
borderWidth: 1
|
|
|
}))
|
|
|
},
|
|
|
options: {
|
|
|
...baseChartConfig,
|
|
|
...options
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
export function createDoughnutChartConfig(labels, data, options = {}) {
|
|
|
const backgroundColors = [
|
|
|
COLORS.revenue.primary,
|
|
|
COLORS.profit.primary,
|
|
|
COLORS.orders.primary,
|
|
|
COLORS.customers.primary,
|
|
|
'rgba(236, 72, 153, 0.8)',
|
|
|
'rgba(34, 197, 94, 0.8)',
|
|
|
'rgba(239, 68, 68, 0.8)',
|
|
|
'rgba(245, 158, 11, 0.8)',
|
|
|
'rgba(139, 92, 246, 0.8)',
|
|
|
'rgba(6, 182, 212, 0.8)'
|
|
|
];
|
|
|
|
|
|
return {
|
|
|
type: 'doughnut',
|
|
|
data: {
|
|
|
labels,
|
|
|
datasets: [{
|
|
|
data,
|
|
|
backgroundColor: backgroundColors.slice(0, data.length),
|
|
|
borderColor: backgroundColors.slice(0, data.length).map(color => color.replace('0.8', '1')),
|
|
|
borderWidth: 2,
|
|
|
hoverBorderWidth: 3
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
...baseChartConfig,
|
|
|
cutout: '60%',
|
|
|
plugins: {
|
|
|
...baseChartConfig.plugins,
|
|
|
legend: {
|
|
|
...baseChartConfig.plugins.legend,
|
|
|
position: 'right'
|
|
|
}
|
|
|
},
|
|
|
...options
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
export function createMixedChartConfig(labels, datasets, options = {}) {
|
|
|
return {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels,
|
|
|
datasets: datasets.map(dataset => ({
|
|
|
...dataset,
|
|
|
backgroundColor: dataset.type === 'line'
|
|
|
? 'transparent'
|
|
|
: function(context) {
|
|
|
const chart = context.chart;
|
|
|
const {ctx} = chart;
|
|
|
return createGradient(ctx, dataset.gradientColors || COLORS.customers.gradient);
|
|
|
},
|
|
|
borderColor: dataset.borderColor || COLORS.customers.primary,
|
|
|
borderWidth: dataset.type === 'line' ? 3 : 1,
|
|
|
yAxisID: dataset.yAxisID || 'y'
|
|
|
}))
|
|
|
},
|
|
|
options: {
|
|
|
...baseChartConfig,
|
|
|
scales: {
|
|
|
...baseChartConfig.scales,
|
|
|
y1: {
|
|
|
type: 'linear',
|
|
|
display: true,
|
|
|
position: 'right',
|
|
|
grid: {
|
|
|
drawOnChartArea: false,
|
|
|
color: COLORS.grid
|
|
|
},
|
|
|
ticks: {
|
|
|
color: COLORS.text.secondary,
|
|
|
font: {
|
|
|
size: 11
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
...options
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
export const chartAnimations = {
|
|
|
fadeIn: {
|
|
|
duration: 800,
|
|
|
easing: 'easeOutQuart'
|
|
|
},
|
|
|
slideUp: {
|
|
|
duration: 1000,
|
|
|
easing: 'easeOutBack'
|
|
|
},
|
|
|
bounce: {
|
|
|
duration: 1200,
|
|
|
easing: 'easeOutBounce'
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
window.COLORS = COLORS;
|
|
|
window.createLineChartConfig = createLineChartConfig;
|
|
|
window.createDoughnutChartConfig = createDoughnutChartConfig;
|
|
|
window.createMixedChartConfig = createMixedChartConfig;
|
|
|
window.chartAnimations = chartAnimations; |