Init repo
This commit is contained in:
533
static/js/charts.js
Normal file
533
static/js/charts.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user