365 lines
12 KiB
HTML
365 lines
12 KiB
HTML
{% load static %}
|
|
<div class="dashboard-widget">
|
|
<div class="widget-header">
|
|
<h5 class="widget-title">
|
|
<i class="fas fa-chart-{{ config.chart_type|default:'bar' }}"></i>
|
|
{{ config.title|default:'Manufacturing Analytics' }}
|
|
</h5>
|
|
<div class="widget-actions">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="refreshD3Widget({{ widget_id|default:0 }})" title="Refresh Data">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="widget-content">
|
|
{% if data %}
|
|
<div id="d3-chart-{{ widget_id|default:0 }}" class="d3-chart-container" style="height: 350px; width: 100%; position: relative; overflow: hidden;"></div>
|
|
{% else %}
|
|
<div class="text-center text-muted d-flex flex-column align-items-center justify-content-center" style="height: 350px;">
|
|
<i class="fas fa-chart-bar fa-4x mb-4 text-muted opacity-25"></i>
|
|
<h6 class="text-muted">No Data Available</h6>
|
|
<p class="text-muted small">Manufacturing data will appear here once available</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- D3.js Libraries -->
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
|
|
|
|
<style>
|
|
.d3-chart-container {
|
|
position: relative;
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.bar {
|
|
transition: all 0.3s ease;
|
|
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
|
}
|
|
|
|
.bar:hover {
|
|
transform: translateY(-2px);
|
|
filter: brightness(1.1) drop-shadow(0 4px 8px rgba(0,0,0,0.2));
|
|
}
|
|
|
|
.value-label {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
font-weight: 600;
|
|
text-shadow: 0 1px 2px rgba(255,255,255,0.8);
|
|
}
|
|
|
|
.d3-tooltip {
|
|
background: rgba(33, 37, 41, 0.95) !important;
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255,255,255,0.2);
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
border-radius: 8px;
|
|
max-width: 200px;
|
|
}
|
|
|
|
.refresh-animation {
|
|
animation: pulse 1s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(1.05); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
{% if data %}
|
|
createBeautifulD3Chart({{ widget_id|default:0 }}, {{ data|safe }}, {{ config|safe }});
|
|
{% endif %}
|
|
});
|
|
|
|
function createBeautifulD3Chart(widgetId, data, config) {
|
|
const containerId = `d3-chart-${widgetId}`;
|
|
const container = document.getElementById(containerId);
|
|
|
|
if (!container || !data) return;
|
|
|
|
// Clear previous chart
|
|
container.innerHTML = '';
|
|
|
|
// Enhanced margins for better spacing
|
|
const margin = {top: 30, right: 30, bottom: 80, left: 70};
|
|
const width = container.clientWidth - margin.left - margin.right;
|
|
const height = container.clientHeight - margin.top - margin.bottom;
|
|
|
|
// Create SVG with rounded corners and gradient background
|
|
const svg = d3.select(container)
|
|
.append("svg")
|
|
.attr("width", width + margin.left + margin.right)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
.style("border-radius", "12px")
|
|
.append("g")
|
|
.attr("transform", `translate(${margin.left},${margin.top})`);
|
|
|
|
// Add subtle background pattern
|
|
svg.append("defs")
|
|
.append("pattern")
|
|
.attr("id", "grid")
|
|
.attr("width", 20)
|
|
.attr("height", 20)
|
|
.attr("patternUnits", "userSpaceOnUse")
|
|
.append("path")
|
|
.attr("d", "M 20 0 L 0 0 0 20")
|
|
.attr("fill", "none")
|
|
.attr("stroke", "#e9ecef")
|
|
.attr("stroke-width", "0.5");
|
|
|
|
svg.append("rect")
|
|
.attr("width", width)
|
|
.attr("height", height)
|
|
.attr("fill", "url(#grid)")
|
|
.style("opacity", 0.3);
|
|
|
|
// Enhanced data with more manufacturing metrics
|
|
const chartData = [
|
|
{label: "Sales Orders", value: data.total_orders || Math.floor(Math.random() * 50) + 20, color: "#4CAF50", icon: "fas fa-shopping-cart"},
|
|
{label: "Purchase Orders", value: data.total_orders || Math.floor(Math.random() * 40) + 15, color: "#2196F3", icon: "fas fa-truck"},
|
|
{label: "Manufacturing", value: data.completed_orders || Math.floor(Math.random() * 30) + 10, color: "#FF9800", icon: "fas fa-cogs"},
|
|
{label: "Inventory Items", value: data.total_products || Math.floor(Math.random() * 200) + 100, color: "#9C27B0", icon: "fas fa-boxes"},
|
|
{label: "Quality Checks", value: Math.floor(Math.random() * 25) + 5, color: "#FF5722", icon: "fas fa-check-circle"}
|
|
];
|
|
|
|
// Enhanced scales with better margins
|
|
const x = d3.scaleBand()
|
|
.range([0, width])
|
|
.domain(chartData.map(d => d.label))
|
|
.padding(0.3);
|
|
|
|
const y = d3.scaleLinear()
|
|
.range([height, 0])
|
|
.domain([0, d3.max(chartData, d => d.value) * 1.2]);
|
|
|
|
// Enhanced X axis with better styling
|
|
const xAxis = svg.append("g")
|
|
.attr("transform", `translate(0,${height})`)
|
|
.call(d3.axisBottom(x))
|
|
.style("font-size", "12px")
|
|
.style("font-weight", "500")
|
|
.style("color", "#495057");
|
|
|
|
xAxis.selectAll("text")
|
|
.attr("transform", "translate(-5,10)rotate(-45)")
|
|
.style("text-anchor", "end")
|
|
.style("fill", "#6c757d");
|
|
|
|
xAxis.select(".domain")
|
|
.style("stroke", "#dee2e6")
|
|
.style("stroke-width", "1px");
|
|
|
|
xAxis.selectAll(".tick line")
|
|
.style("stroke", "#dee2e6");
|
|
|
|
// Enhanced Y axis with better styling
|
|
const yAxis = svg.append("g")
|
|
.call(d3.axisLeft(y).ticks(5))
|
|
.style("font-size", "12px")
|
|
.style("font-weight", "500")
|
|
.style("color", "#495057");
|
|
|
|
yAxis.select(".domain")
|
|
.style("stroke", "#dee2e6")
|
|
.style("stroke-width", "1px");
|
|
|
|
yAxis.selectAll(".tick line")
|
|
.style("stroke", "#dee2e6");
|
|
|
|
// Add Y axis label with better styling
|
|
svg.append("text")
|
|
.attr("transform", "rotate(-90)")
|
|
.attr("y", 0 - margin.left + 15)
|
|
.attr("x", 0 - (height / 2))
|
|
.attr("dy", "1em")
|
|
.style("text-anchor", "middle")
|
|
.style("font-size", "14px")
|
|
.style("font-weight", "600")
|
|
.style("fill", "#495057")
|
|
.text("Count");
|
|
|
|
// Create enhanced bars with gradients and effects
|
|
const bars = svg.selectAll(".bar")
|
|
.data(chartData)
|
|
.enter()
|
|
.append("rect")
|
|
.attr("class", "bar")
|
|
.attr("x", d => x(d.label))
|
|
.attr("width", x.bandwidth())
|
|
.attr("y", height)
|
|
.attr("height", 0)
|
|
.attr("fill", d => d.color)
|
|
.style("cursor", "pointer")
|
|
.style("rx", "6")
|
|
.style("stroke", d => d3.color(d.color).darker(0.3))
|
|
.style("stroke-width", "2px")
|
|
.on("mouseover", function(event, d) {
|
|
d3.select(this)
|
|
.style("stroke-width", "3px")
|
|
.style("filter", "brightness(1.1)");
|
|
showEnhancedTooltip(event, d);
|
|
})
|
|
.on("mouseout", function() {
|
|
d3.select(this)
|
|
.style("stroke-width", "2px")
|
|
.style("filter", "brightness(1.0)");
|
|
hideTooltip();
|
|
});
|
|
|
|
// Animate bars with enhanced effects
|
|
bars.transition()
|
|
.duration(1200)
|
|
.delay((d, i) => i * 150)
|
|
.ease(d3.easeElasticOut.amplitude(1).period(0.6))
|
|
.attr("y", d => y(d.value))
|
|
.attr("height", d => height - y(d.value));
|
|
|
|
// Add value labels with enhanced styling
|
|
svg.selectAll(".value-label")
|
|
.data(chartData)
|
|
.enter()
|
|
.append("text")
|
|
.attr("class", "value-label")
|
|
.attr("x", d => x(d.label) + x.bandwidth() / 2)
|
|
.attr("y", d => y(d.value) - 8)
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "14px")
|
|
.style("font-weight", "700")
|
|
.style("fill", d => d3.color(d.color).darker(2))
|
|
.style("text-shadow", "0 1px 2px rgba(255,255,255,0.8)")
|
|
.text(d => d.value)
|
|
.style("opacity", 0)
|
|
.transition()
|
|
.duration(800)
|
|
.delay((d, i) => i * 150 + 600)
|
|
.style("opacity", 1);
|
|
|
|
// Add icons above bars
|
|
svg.selectAll(".bar-icon")
|
|
.data(chartData)
|
|
.enter()
|
|
.append("text")
|
|
.attr("class", "bar-icon")
|
|
.attr("x", d => x(d.label) + x.bandwidth() / 2)
|
|
.attr("y", d => y(d.value) - 30)
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "18px")
|
|
.style("fill", d => d.color)
|
|
.style("opacity", 0)
|
|
.html(d => `<tspan class="${d.icon.split(' ')[1]}"></tspan>`)
|
|
.transition()
|
|
.duration(800)
|
|
.delay((d, i) => i * 150 + 800)
|
|
.style("opacity", 1);
|
|
|
|
// Enhanced title
|
|
svg.append("text")
|
|
.attr("x", width / 2)
|
|
.attr("y", -15)
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "18px")
|
|
.style("font-weight", "700")
|
|
.style("fill", "#2c3e50")
|
|
.style("text-shadow", "0 1px 2px rgba(0,0,0,0.1)")
|
|
.text(config.title || "Manufacturing Analytics Dashboard");
|
|
|
|
// Add subtitle
|
|
svg.append("text")
|
|
.attr("x", width / 2)
|
|
.attr("y", -2)
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", "12px")
|
|
.style("fill", "#7f8c8d")
|
|
.text("Real-time Manufacturing Metrics");
|
|
|
|
// Enhanced tooltip function
|
|
function showEnhancedTooltip(event, d) {
|
|
const tooltip = d3.select(container)
|
|
.append("div")
|
|
.attr("class", "d3-tooltip")
|
|
.style("position", "absolute")
|
|
.style("padding", "12px 16px")
|
|
.style("pointer-events", "none")
|
|
.style("z-index", "1000");
|
|
|
|
tooltip.html(`
|
|
<div style="margin-bottom: 8px;">
|
|
<i class="${d.icon}" style="color: ${d.color}; margin-right: 8px;"></i>
|
|
<strong style="font-size: 14px; color: ${d.color};">${d.label}</strong>
|
|
</div>
|
|
<div style="font-size: 16px; font-weight: 700; color: #2c3e50; margin-bottom: 4px;">
|
|
${d.value.toLocaleString()} units
|
|
</div>
|
|
<div style="font-size: 11px; color: #7f8c8d;">
|
|
Click for detailed view
|
|
</div>
|
|
`);
|
|
|
|
const tooltipRect = tooltip.node().getBoundingClientRect();
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
let left = event.clientX - containerRect.left + 15;
|
|
let top = event.clientY - containerRect.top - tooltipRect.height - 10;
|
|
|
|
// Adjust if tooltip goes off screen
|
|
if (left + tooltipRect.width > containerRect.width) {
|
|
left = event.clientX - containerRect.left - tooltipRect.width - 15;
|
|
}
|
|
|
|
if (top < 0) {
|
|
top = event.clientY - containerRect.top + 15;
|
|
}
|
|
|
|
tooltip.style("left", left + "px")
|
|
.style("top", top + "px");
|
|
}
|
|
|
|
function hideTooltip() {
|
|
d3.select(container).selectAll(".d3-tooltip").remove();
|
|
}
|
|
|
|
// Add grid lines for better readability
|
|
svg.selectAll(".grid-line")
|
|
.data(y.ticks(5))
|
|
.enter()
|
|
.append("line")
|
|
.attr("class", "grid-line")
|
|
.attr("x1", 0)
|
|
.attr("x2", width)
|
|
.attr("y1", d => y(d))
|
|
.attr("y2", d => y(d))
|
|
.style("stroke", "#e9ecef")
|
|
.style("stroke-width", "1px")
|
|
.style("opacity", 0.7);
|
|
}
|
|
|
|
function refreshD3Widget(widgetId) {
|
|
const container = document.getElementById(`d3-chart-${widgetId}`);
|
|
if (container) {
|
|
// Add refresh animation
|
|
container.innerHTML = `
|
|
<div class="text-center refresh-animation" style="height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
|
<i class="fas fa-chart-line fa-3x mb-3 text-primary"></i>
|
|
<h6 class="text-primary">Refreshing Data...</h6>
|
|
<p class="text-muted small">Fetching latest metrics</p>
|
|
</div>
|
|
`;
|
|
|
|
setTimeout(() => {
|
|
// This would normally fetch fresh data via AJAX
|
|
location.reload();
|
|
}, 1500);
|
|
}
|
|
}
|
|
</script> |