chore: initial clean commit

This commit is contained in:
2026-02-26 13:56:21 -05:00
commit 1755075657
53 changed files with 18068 additions and 0 deletions

533
static/js/charts.js Normal file
View File

@@ -0,0 +1,533 @@
// Chart.js Configuration and Helpers
class ChartManager {
constructor() {
this.charts = new Map();
this.defaultColors = [
'#3b82f6', // Blue
'#10b981', // Green
'#f59e0b', // Yellow
'#ef4444', // Red
'#8b5cf6', // Purple
'#ec4899', // Pink
'#06b6d4', // Cyan
'#84cc16', // Lime
'#f97316', // Orange
'#6366f1', // Indigo
];
this.init();
}
init() {
// Register Chart.js plugins if needed
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 = 'rgba(0, 0, 0, 0.1)';
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'
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
titleColor: '#1e293b',
bodyColor: '#1e293b',
borderColor: '#e2e8f0',
borderWidth: 1,
cornerRadius: 6,
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: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: '#64748b'
}
},
y: {
beginAtZero: true,
grid: {
display: true,
color: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: '#64748b',
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.1) : 'transparent',
borderWidth: 2,
pointRadius: 3,
pointHoverRadius: 6,
pointBackgroundColor: dataset.color || this.defaultColors[index % this.defaultColors.length],
pointBorderColor: '#ffffff',
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: '#ffffff',
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: '#ffffff',
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: dataset.borderColor || '#ffffff',
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: 'rgba(0, 0, 0, 0.05)'
},
ticks: {
color: '#64748b',
callback: function(value) {
return value.toLocaleString();
}
}
},
y: {
grid: {
display: false
},
ticks: {
color: '#64748b'
}
}
},
...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;
}