Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
464
resources/js/modules/canvas-animations/DataVisualization.js
Normal file
464
resources/js/modules/canvas-animations/DataVisualization.js
Normal file
@@ -0,0 +1,464 @@
|
||||
// modules/canvas-animations/DataVisualization.js
|
||||
import { CanvasManager } from './CanvasManager.js';
|
||||
import { Logger } from '../../core/logger.js';
|
||||
|
||||
/**
|
||||
* Data Visualization Canvas Components - Animated charts and graphs
|
||||
*/
|
||||
export const DataVisualization = {
|
||||
|
||||
/**
|
||||
* Initialize data visualization canvas
|
||||
*/
|
||||
init(canvas, config) {
|
||||
const manager = new CanvasManager(canvas);
|
||||
const vizType = config.type || 'bar';
|
||||
|
||||
// Get data from config or data attributes
|
||||
const data = this.parseData(canvas, config);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
Logger.warn('[DataVisualization] No data provided for canvas');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (vizType) {
|
||||
case 'bar':
|
||||
this.createBarChart(manager, data, config);
|
||||
break;
|
||||
case 'line':
|
||||
this.createLineChart(manager, data, config);
|
||||
break;
|
||||
case 'pie':
|
||||
this.createPieChart(manager, data, config);
|
||||
break;
|
||||
case 'progress':
|
||||
this.createProgressChart(manager, data, config);
|
||||
break;
|
||||
default:
|
||||
this.createBarChart(manager, data, config);
|
||||
}
|
||||
|
||||
Logger.info('[DataVisualization] Initialized', vizType, 'chart with', data.length, 'data points');
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse data from canvas element or config
|
||||
*/
|
||||
parseData(canvas, config) {
|
||||
let data = config.data;
|
||||
|
||||
// Try to get data from data-canvas-data attribute
|
||||
if (!data && canvas.dataset.canvasData) {
|
||||
try {
|
||||
data = JSON.parse(canvas.dataset.canvasData);
|
||||
} catch (e) {
|
||||
Logger.warn('[DataVisualization] Failed to parse canvas data', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get data from script tag
|
||||
if (!data) {
|
||||
const scriptTag = canvas.nextElementSibling;
|
||||
if (scriptTag && scriptTag.tagName === 'SCRIPT' && scriptTag.type === 'application/json') {
|
||||
try {
|
||||
data = JSON.parse(scriptTag.textContent);
|
||||
} catch (e) {
|
||||
Logger.warn('[DataVisualization] Failed to parse script data', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure data is always an array
|
||||
if (!data) {
|
||||
Logger.warn('[DataVisualization] No data found');
|
||||
return [];
|
||||
}
|
||||
|
||||
// If data is not an array, wrap it or convert it
|
||||
if (!Array.isArray(data)) {
|
||||
// If it's a single number (for progress charts)
|
||||
if (typeof data === 'number') {
|
||||
data = [{ value: data, label: 'Progress' }];
|
||||
}
|
||||
// If it's an object with values, convert to array
|
||||
else if (typeof data === 'object') {
|
||||
data = Object.entries(data).map(([key, value]) => ({
|
||||
label: key,
|
||||
value: typeof value === 'object' ? value.value || value : value
|
||||
}));
|
||||
}
|
||||
// Fallback: empty array
|
||||
else {
|
||||
Logger.warn('[DataVisualization] Data format not recognized:', data);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated bar chart
|
||||
*/
|
||||
createBarChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const padding = config.padding || 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
const barWidth = chartWidth / data.length * 0.8;
|
||||
const barSpacing = chartWidth / data.length * 0.2;
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
// Easing function (easeOutCubic)
|
||||
const progress = 1 - Math.pow(1 - animationProgress, 3);
|
||||
|
||||
manager.clear();
|
||||
this.renderBarChart(manager, data, config, progress, {
|
||||
padding,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
barWidth,
|
||||
barSpacing,
|
||||
maxValue
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render bar chart frame
|
||||
*/
|
||||
renderBarChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { padding, chartHeight, barWidth, barSpacing, maxValue } = layout;
|
||||
|
||||
// Draw bars
|
||||
data.forEach((item, index) => {
|
||||
const x = padding + index * (barWidth + barSpacing);
|
||||
const barHeight = (item.value / maxValue) * chartHeight * progress;
|
||||
const y = padding + chartHeight - barHeight;
|
||||
|
||||
// Bar
|
||||
ctx.fillStyle = item.color || config.barColor || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.fillRect(x, y, barWidth, barHeight);
|
||||
|
||||
// Label
|
||||
if (config.showLabels !== false) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(
|
||||
item.label,
|
||||
x + barWidth / 2,
|
||||
padding + chartHeight + 20
|
||||
);
|
||||
|
||||
// Value
|
||||
if (config.showValues !== false) {
|
||||
ctx.fillText(
|
||||
Math.round(item.value * progress),
|
||||
x + barWidth / 2,
|
||||
y - 5
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Draw axes if enabled
|
||||
if (config.showAxes !== false) {
|
||||
ctx.strokeStyle = config.axisColor || '#ccc';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Y-axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding, padding);
|
||||
ctx.lineTo(padding, padding + chartHeight);
|
||||
ctx.stroke();
|
||||
|
||||
// X-axis
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padding, padding + chartHeight);
|
||||
ctx.lineTo(padding + layout.chartWidth, padding + chartHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated line chart
|
||||
*/
|
||||
createLineChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const padding = config.padding || 40;
|
||||
const chartWidth = width - padding * 2;
|
||||
const chartHeight = height - padding * 2;
|
||||
|
||||
const maxValue = Math.max(...data.map(d => d.value));
|
||||
const minValue = Math.min(...data.map(d => d.value));
|
||||
const valueRange = maxValue - minValue;
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderLineChart(manager, data, config, animationProgress, {
|
||||
padding,
|
||||
chartWidth,
|
||||
chartHeight,
|
||||
maxValue,
|
||||
minValue,
|
||||
valueRange
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render line chart frame
|
||||
*/
|
||||
renderLineChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { padding, chartWidth, chartHeight, minValue, valueRange } = layout;
|
||||
|
||||
const pointsToShow = Math.floor(data.length * progress);
|
||||
|
||||
ctx.strokeStyle = config.lineColor || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineWidth = config.lineWidth || 3;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
|
||||
// Draw line
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= pointsToShow && i < data.length; i++) {
|
||||
const x = padding + (i / (data.length - 1)) * chartWidth;
|
||||
const y = padding + chartHeight - ((data[i].value - minValue) / valueRange) * chartHeight;
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
// Draw points
|
||||
if (config.showPoints !== false) {
|
||||
ctx.fillStyle = config.pointColor || config.lineColor || 'rgba(100, 150, 255, 0.8)';
|
||||
|
||||
for (let i = 0; i <= pointsToShow && i < data.length; i++) {
|
||||
const x = padding + (i / (data.length - 1)) * chartWidth;
|
||||
const y = padding + chartHeight - ((data[i].value - minValue) / valueRange) * chartHeight;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, config.pointSize || 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Labels
|
||||
if (config.showLabels !== false && data[i].label) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(data[i].label, x, padding + chartHeight + 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create animated pie chart
|
||||
*/
|
||||
createPieChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) / 2 * 0.8;
|
||||
|
||||
const total = data.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 2000;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderPieChart(manager, data, config, animationProgress, {
|
||||
centerX,
|
||||
centerY,
|
||||
radius,
|
||||
total
|
||||
});
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render pie chart frame
|
||||
*/
|
||||
renderPieChart(manager, data, config, progress, layout) {
|
||||
const { ctx } = manager;
|
||||
const { centerX, centerY, radius, total } = layout;
|
||||
|
||||
let currentAngle = -Math.PI / 2; // Start at top
|
||||
const maxAngle = currentAngle + (Math.PI * 2 * progress);
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
if (currentAngle >= maxAngle) break;
|
||||
|
||||
const item = data[index];
|
||||
const sliceAngle = (item.value / total) * Math.PI * 2;
|
||||
const endAngle = Math.min(currentAngle + sliceAngle, maxAngle);
|
||||
|
||||
if (endAngle > currentAngle) {
|
||||
// Draw slice
|
||||
ctx.fillStyle = item.color || this.getDefaultColor(index);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(centerX, centerY);
|
||||
ctx.arc(centerX, centerY, radius, currentAngle, endAngle);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Draw labels if enabled
|
||||
if (config.showLabels !== false && endAngle === currentAngle + sliceAngle) {
|
||||
const labelAngle = currentAngle + sliceAngle / 2;
|
||||
const labelX = centerX + Math.cos(labelAngle) * radius * 0.7;
|
||||
const labelY = centerY + Math.sin(labelAngle) * radius * 0.7;
|
||||
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(item.label, labelX, labelY);
|
||||
}
|
||||
}
|
||||
|
||||
currentAngle += sliceAngle;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create progress visualization
|
||||
*/
|
||||
createProgressChart(manager, data, config) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
let animationProgress = 0;
|
||||
const animationDuration = config.animationDuration || 1500;
|
||||
let startTime = null;
|
||||
|
||||
const animate = (timestamp) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const elapsed = timestamp - startTime;
|
||||
|
||||
animationProgress = Math.min(elapsed / animationDuration, 1);
|
||||
|
||||
manager.clear();
|
||||
this.renderProgressChart(manager, data, config, animationProgress);
|
||||
|
||||
if (animationProgress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render progress chart
|
||||
*/
|
||||
renderProgressChart(manager, data, config, progress) {
|
||||
const { ctx } = manager;
|
||||
const { width, height } = manager.getSize();
|
||||
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = Math.min(width, height) / 2 * 0.8;
|
||||
const lineWidth = config.lineWidth || 20;
|
||||
|
||||
// Get progress value from data array
|
||||
const value = data[0]?.value || 0;
|
||||
const maxValue = config.maxValue || 100;
|
||||
const progressValue = (value / maxValue) * progress;
|
||||
|
||||
// Background circle
|
||||
ctx.strokeStyle = config.backgroundColor || 'rgba(200, 200, 200, 0.3)';
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Progress arc
|
||||
ctx.strokeStyle = config.color || 'rgba(100, 150, 255, 0.8)';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + (progressValue / maxValue) * Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Center text
|
||||
if (config.showText !== false) {
|
||||
ctx.fillStyle = config.textColor || '#333';
|
||||
ctx.font = config.font || 'bold 24px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(Math.round(progressValue) + '%', centerX, centerY);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get default color for data point
|
||||
*/
|
||||
getDefaultColor(index) {
|
||||
const colors = [
|
||||
'rgba(100, 150, 255, 0.8)',
|
||||
'rgba(255, 100, 150, 0.8)',
|
||||
'rgba(150, 255, 100, 0.8)',
|
||||
'rgba(255, 200, 100, 0.8)',
|
||||
'rgba(200, 100, 255, 0.8)',
|
||||
'rgba(100, 255, 200, 0.8)'
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user