536 lines
18 KiB
JavaScript
536 lines
18 KiB
JavaScript
// Chart.js Configuration and Helpers
|
|
|
|
class ChartManager {
|
|
constructor() {
|
|
this.charts = new Map();
|
|
this.defaultColors = [
|
|
'#fe8019', // Orange
|
|
'#b8bb26', // Green
|
|
'#fabd2f', // Yellow
|
|
'#fb4934', // Red
|
|
'#83a598', // Blue
|
|
'#d3869b', // Purple
|
|
'#8ec07c', // Aqua
|
|
'#d65d0e', // Dark Orange
|
|
'#98971a', // Dark Green
|
|
'#d79921', // Dark Yellow
|
|
];
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Set Chart.js defaults for Gruvbox
|
|
Chart.defaults.color = '#bdae93'; // fg3
|
|
Chart.defaults.borderColor = '#504945'; // bg2
|
|
this.registerPlugins();
|
|
}
|
|
|
|
registerPlugins() {
|
|
// Register a plugin for tooltip background
|
|
Chart.register({
|
|
id: 'customTooltip',
|
|
beforeDraw: (chart) => {
|
|
if (chart.tooltip._active && chart.tooltip._active.length) {
|
|
const ctx = chart.ctx;
|
|
const activePoint = chart.tooltip._active[0];
|
|
const x = activePoint.element.x;
|
|
const topY = chart.scales.y.top;
|
|
const bottomY = chart.scales.y.bottom;
|
|
|
|
ctx.save();
|
|
ctx.beginPath();
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.moveTo(x, topY);
|
|
ctx.lineTo(x, bottomY);
|
|
ctx.lineWidth = 1;
|
|
ctx.strokeStyle = '#665c54'; // bg3
|
|
ctx.stroke();
|
|
ctx.restore();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
createChart(canvasId, config) {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas) {
|
|
console.warn(`Canvas element #${canvasId} not found`);
|
|
return null;
|
|
}
|
|
|
|
// Destroy existing chart if it exists
|
|
if (this.charts.has(canvasId)) {
|
|
this.charts.get(canvasId).destroy();
|
|
}
|
|
|
|
// Create new chart
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new Chart(ctx, {
|
|
...config,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
color: '#ebdbb2' // fg1
|
|
}
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
backgroundColor: '#3c3836', // bg1
|
|
titleColor: '#fbf1c7', // fg0
|
|
bodyColor: '#ebdbb2', // fg1
|
|
borderColor: '#504945', // bg2
|
|
borderWidth: 1,
|
|
cornerRadius: 4,
|
|
padding: 12,
|
|
boxPadding: 6,
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += context.parsed.y.toLocaleString();
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
intersect: false,
|
|
mode: 'nearest'
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: true,
|
|
color: '#3c3836' // bg1
|
|
},
|
|
ticks: {
|
|
color: '#a89984' // fg4
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
display: true,
|
|
color: '#3c3836' // bg1
|
|
},
|
|
ticks: {
|
|
color: '#a89984', // fg4
|
|
callback: function(value) {
|
|
return value.toLocaleString();
|
|
}
|
|
}
|
|
}
|
|
},
|
|
...config.options
|
|
}
|
|
});
|
|
|
|
// Store chart reference
|
|
this.charts.set(canvasId, chart);
|
|
|
|
return chart;
|
|
}
|
|
|
|
destroyChart(canvasId) {
|
|
if (this.charts.has(canvasId)) {
|
|
this.charts.get(canvasId).destroy();
|
|
this.charts.delete(canvasId);
|
|
}
|
|
}
|
|
|
|
destroyAllCharts() {
|
|
this.charts.forEach((chart, canvasId) => {
|
|
chart.destroy();
|
|
});
|
|
this.charts.clear();
|
|
}
|
|
|
|
// Chart templates
|
|
createLineChart(canvasId, data, options = {}) {
|
|
const config = {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.labels || [],
|
|
datasets: data.datasets.map((dataset, index) => ({
|
|
label: dataset.label,
|
|
data: dataset.data,
|
|
borderColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
|
backgroundColor: dataset.fill ? this.hexToRgba(dataset.color || this.defaultColors[index % this.defaultColors.length], 0.15) : 'transparent',
|
|
borderWidth: 2,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 6,
|
|
pointBackgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
|
pointBorderColor: '#282828', // bg0
|
|
pointBorderWidth: 2,
|
|
fill: dataset.fill || false,
|
|
tension: 0.4,
|
|
...dataset
|
|
}))
|
|
},
|
|
options: {
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += context.parsed.y.toLocaleString();
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
...options
|
|
}
|
|
};
|
|
|
|
return this.createChart(canvasId, config);
|
|
}
|
|
|
|
createBarChart(canvasId, data, options = {}) {
|
|
const config = {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.labels || [],
|
|
datasets: data.datasets.map((dataset, index) => ({
|
|
label: dataset.label,
|
|
data: dataset.data,
|
|
backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
|
borderColor: dataset.borderColor || '#ffffff',
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
borderSkipped: false,
|
|
...dataset
|
|
}))
|
|
},
|
|
options: {
|
|
plugins: {
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += context.parsed.y.toLocaleString();
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
...options
|
|
}
|
|
};
|
|
|
|
return this.createChart(canvasId, config);
|
|
}
|
|
|
|
createPieChart(canvasId, data, options = {}) {
|
|
const config = {
|
|
type: 'pie',
|
|
data: {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length),
|
|
borderColor: '#282828', // bg0
|
|
borderWidth: 2,
|
|
hoverOffset: 15
|
|
}]
|
|
},
|
|
options: {
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle'
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.raw || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return `${label}: ${value.toLocaleString()} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
...options
|
|
}
|
|
};
|
|
|
|
return this.createChart(canvasId, config);
|
|
}
|
|
|
|
createDoughnutChart(canvasId, data, options = {}) {
|
|
const config = {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: data.colors || this.defaultColors.slice(0, data.data.length),
|
|
borderColor: '#282828', // bg0
|
|
borderWidth: 2,
|
|
hoverOffset: 15
|
|
}]
|
|
},
|
|
options: {
|
|
cutout: '60%',
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle'
|
|
}
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const label = context.label || '';
|
|
const value = context.raw || 0;
|
|
const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
const percentage = Math.round((value / total) * 100);
|
|
return `${label}: ${value.toLocaleString()} (${percentage}%)`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
...options
|
|
}
|
|
};
|
|
|
|
return this.createChart(canvasId, config);
|
|
}
|
|
|
|
createHorizontalBarChart(canvasId, data, options = {}) {
|
|
const config = {
|
|
type: 'bar',
|
|
data: {
|
|
labels: data.labels || [],
|
|
datasets: data.datasets.map((dataset, index) => ({
|
|
label: dataset.label,
|
|
data: dataset.data,
|
|
backgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
|
|
borderColor: '#282828', // bg0
|
|
borderWidth: 1,
|
|
borderRadius: 4,
|
|
...dataset
|
|
}))
|
|
},
|
|
options: {
|
|
indexAxis: 'y',
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.x !== null) {
|
|
label += context.parsed.x.toLocaleString();
|
|
}
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
display: true,
|
|
color: '#3c3836' // bg1
|
|
},
|
|
ticks: {
|
|
color: '#a89984', // fg4
|
|
callback: function(value) {
|
|
return value.toLocaleString();
|
|
}
|
|
}
|
|
},
|
|
y: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
color: '#a89984' // fg4
|
|
}
|
|
}
|
|
},
|
|
...options
|
|
}
|
|
};
|
|
|
|
return this.createChart(canvasId, config);
|
|
}
|
|
|
|
// Helper methods
|
|
hexToRgba(hex, alpha = 1) {
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|
|
|
|
generateTimeLabels(hours = 24) {
|
|
const now = luxon.DateTime.now();
|
|
const labels = [];
|
|
|
|
for (let i = hours - 1; i >= 0; i--) {
|
|
const time = now.minus({ hours: i });
|
|
labels.push(time.toFormat('HH:00'));
|
|
}
|
|
|
|
return labels;
|
|
}
|
|
|
|
generateDateLabels(days = 7) {
|
|
const now = luxon.DateTime.now();
|
|
const labels = [];
|
|
|
|
for (let i = days - 1; i >= 0; i--) {
|
|
const date = now.minus({ days: i });
|
|
labels.push(date.toFormat('MMM dd'));
|
|
}
|
|
|
|
return labels;
|
|
}
|
|
|
|
// Demo data generators
|
|
generateDemoTimeSeries(hours = 24, seriesCount = 1) {
|
|
const labels = this.generateTimeLabels(hours);
|
|
const datasets = [];
|
|
|
|
for (let i = 0; i < seriesCount; i++) {
|
|
const data = [];
|
|
let value = Math.floor(Math.random() * 100) + 50;
|
|
|
|
for (let j = 0; j < hours; j++) {
|
|
// Add some randomness but keep trend
|
|
value += Math.floor(Math.random() * 20) - 10;
|
|
value = Math.max(10, value);
|
|
data.push(value);
|
|
}
|
|
|
|
datasets.push({
|
|
label: `Series ${i + 1}`,
|
|
data: data,
|
|
color: this.defaultColors[i % this.defaultColors.length]
|
|
});
|
|
}
|
|
|
|
return { labels, datasets };
|
|
}
|
|
|
|
generateDemoBarData(labels, seriesCount = 1) {
|
|
const datasets = [];
|
|
|
|
for (let i = 0; i < seriesCount; i++) {
|
|
const data = labels.map(() => Math.floor(Math.random() * 100) + 20);
|
|
|
|
datasets.push({
|
|
label: `Dataset ${i + 1}`,
|
|
data: data,
|
|
color: this.defaultColors[i % this.defaultColors.length]
|
|
});
|
|
}
|
|
|
|
return { labels, datasets };
|
|
}
|
|
|
|
generateDemoPieData(labels) {
|
|
const data = labels.map(() => Math.floor(Math.random() * 100) + 10);
|
|
const total = data.reduce((a, b) => a + b, 0);
|
|
|
|
return {
|
|
labels: labels,
|
|
data: data,
|
|
colors: labels.map((_, i) => this.defaultColors[i % this.defaultColors.length])
|
|
};
|
|
}
|
|
|
|
// Update chart data
|
|
updateChartData(canvasId, newData) {
|
|
const chart = this.charts.get(canvasId);
|
|
if (!chart) return;
|
|
|
|
chart.data.labels = newData.labels || chart.data.labels;
|
|
|
|
if (newData.datasets) {
|
|
newData.datasets.forEach((dataset, index) => {
|
|
if (chart.data.datasets[index]) {
|
|
chart.data.datasets[index].data = dataset.data;
|
|
if (dataset.label) {
|
|
chart.data.datasets[index].label = dataset.label;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
chart.update();
|
|
}
|
|
|
|
// Add data point to time series
|
|
addDataPoint(canvasId, newPoint, datasetIndex = 0) {
|
|
const chart = this.charts.get(canvasId);
|
|
if (!chart || chart.config.type !== 'line') return;
|
|
|
|
const dataset = chart.data.datasets[datasetIndex];
|
|
if (!dataset) return;
|
|
|
|
// Add new point
|
|
dataset.data.push(newPoint);
|
|
|
|
// Remove oldest point if we have too many
|
|
if (dataset.data.length > 100) {
|
|
dataset.data.shift();
|
|
chart.data.labels.shift();
|
|
} else {
|
|
// Add new label
|
|
const now = luxon.DateTime.now();
|
|
chart.data.labels.push(now.toFormat('HH:mm:ss'));
|
|
}
|
|
|
|
chart.update();
|
|
}
|
|
}
|
|
|
|
// Initialize chart manager when DOM is loaded
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
window.chartManager = new ChartManager();
|
|
});
|
|
|
|
// Export for use in other modules
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = ChartManager;
|
|
} |