Visualizing data effectively can transform complex information into clear, actionable insights. Charts and graphs are indispensable tools for this purpose, offering a snapshot of trends, patterns, and outliers at a glance.
In this article, we explore ten compelling examples of charts and graphs that can elevate your data presentation. From bar charts to scatter plots, each example demonstrates unique strengths and applications.
CODE1
Here's the code:
CODETEXT1
CODE2
Here's the code:
CODETEXT2
CODE3
Here's the code:
CODETEXT3
CODE4
Here's the code:
CODETEXT4
CODE5
Here's the code:
CODETEXT5
Subframe's drag-and-drop interface and intuitive, responsive canvas make it effortless to create pixel-perfect UI every time. Loved by designers and developers alike, Subframe ensures your charts and graphs are both stunning and functional.
Start for free and elevate your data visualization today!
CODE6
Here's the code:
CODETEXT6
CODE7
Here's the code:
CODETEXT7
CODE8
Here's the code:
CODETEXT8
CODE9
Here's the code:
CODETEXT9
CODE10
Here's the code:
CODETEXT10
Ready to elevate your UI design game? With Subframe, you can create stunning, pixel-perfect UIs, including charts and graphs, in minutes. Our drag-and-drop editor ensures efficiency and precision.
Don't wait—start for free and begin designing immediately!
<html> <head> <title>Financial Dashboard</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; } :root { --primary: #1e293b; --primary-light: #334155; --secondary: #e2e8f0; --accent: #38bdf8; --accent-hover: #0ea5e9; --positive: #4ade80; --negative: #f87171; --text: #0f172a; --text-light: #64748b; --background: #f8fafc; --card: #ffffff; --border: #e2e8f0; } body { background-color: var(--background); color: var(--text); height: 700px; width: 700px; overflow-x: hidden; } .dashboard { max-width: 700px; height: 700px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 20px; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .title { font-size: 18px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; } .title-dot { height: 8px; width: 8px; border-radius: 50%; background-color: var(--accent); display: inline-block; } .live-indicator { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-light); } .pulse { height: 8px; width: 8px; border-radius: 50%; background-color: var(--positive); animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.7); } 70% { box-shadow: 0 0 0 6px rgba(74, 222, 128, 0); } 100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); } } .controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .time-periods { display: flex; gap: 10px; } .period { padding: 6px 12px; border-radius: 20px; font-size: 13px; cursor: pointer; transition: all 0.2s ease; background-color: var(--secondary); color: var(--text-light); } .period.active { background-color: var(--primary); color: white; } .period:hover:not(.active) { background-color: var(--border); } .metrics { display: flex; gap: 15px; flex-wrap: wrap; } .metric-card { flex: 1; min-width: 140px; background-color: var(--card); border-radius: 10px; padding: 15px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid var(--border); transition: transform 0.2s ease, box-shadow 0.2s ease; } .metric-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); } .metric-name { font-size: 13px; color: var(--text-light); margin-bottom: 5px; } .metric-value { font-size: 20px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 8px; } .metric-change { font-size: 12px; font-weight: 500; padding: 3px 8px; border-radius: 20px; margin-left: 5px; } .positive { color: var(--positive); background-color: rgba(74, 222, 128, 0.1); } .negative { color: var(--negative); background-color: rgba(248, 113, 113, 0.1); } .chart-container { display: flex; flex-direction: column; gap: 20px; flex: 1; } .chart-card { flex: 1; background-color: var(--card); border-radius: 10px; padding: 20px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); border: 1px solid var(--border); position: relative; overflow: hidden; display: flex; flex-direction: column; } .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .chart-title { font-size: 15px; font-weight: 600; } .chart-legend { display: flex; gap: 15px; } .legend-item { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--text-light); } .legend-color { width: 10px; height: 10px; border-radius: 2px; } .chart { flex: 1; position: relative; width: 100%; min-height: 180px; } .tooltip { position: absolute; padding: 10px; background: var(--primary); color: white; border-radius: 5px; font-size: 12px; pointer-events: none; transform: translate(-50%, -100%); transition: opacity 0.2s ease; opacity: 0; z-index: 10; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .tooltip:after { content: ""; position: absolute; left: 50%; top: 100%; transform: translateX(-50%); border: 5px solid transparent; border-top-color: var(--primary); } .tooltip-value { font-weight: 600; font-size: 14px; } .bar { position: absolute; bottom: 0; width: 12px; background-color: var(--accent); border-radius: 3px 3px 0 0; transition: height 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); cursor: pointer; } .bar::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 0; background-color: rgba(255, 255, 255, 0.2); border-radius: 3px 3px 0 0; transition: height 0.3s ease; } .bar:hover::before { height: 100%; } .line-chart { position: relative; height: 100%; width: 100%; } .axis-labels { display: flex; justify-content: space-between; margin-top: 10px; font-size: 11px; color: var(--text-light); } .axis-label { text-align: center; width: 20px; } .trend-line { position: absolute; height: 2px; background-color: var(--accent); bottom: 0; left: 0; transform-origin: left bottom; z-index: 1; transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); } .trend-point { position: absolute; width: 8px; height: 8px; background-color: var(--accent); border-radius: 50%; transform: translate(-50%, -50%); z-index: 2; cursor: pointer; } .trend-point::after { content: ''; position: absolute; width: 16px; height: 16px; background-color: rgba(56, 189, 248, 0.2); border-radius: 50%; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); transition: transform 0.2s ease; } .trend-point:hover::after { transform: translate(-50%, -50%) scale(1); } .data-table { margin-top: 10px; display: none; overflow-y: auto; max-height: 150px; } .data-table.active { display: block; animation: fadeIn 0.3s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .data-table table { width: 100%; border-collapse: collapse; font-size: 12px; } .data-table th { text-align: left; padding: 8px; border-bottom: 1px solid var(--border); color: var(--text-light); font-weight: 500; } .data-table td { padding: 8px; border-bottom: 1px solid var(--border); } .data-table tr:hover { background-color: var(--secondary); } .expand-button { background: none; border: none; color: var(--accent); font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 5px; transition: color 0.2s ease; } .expand-button:hover { color: var(--accent-hover); } .expand-icon { width: 16px; height: 16px; transition: transform 0.3s ease; } .expand-icon.rotated { transform: rotate(180deg); } /* Responsive adjustments */ @media (max-width: 600px) { .metrics { flex-direction: column; } .metric-card { width: 100%; } .time-periods { flex-wrap: wrap; } .period { font-size: 12px; padding: 5px 10px; } } </style> </head> <body> <div class="dashboard"> <div class="header"> <div class="title"> <span class="title-dot"></span> <span>Financial Performance</span> </div> <div class="live-indicator"> <span class="pulse"></span> <span>Live Data</span> </div> </div> <div class="controls"> <div class="time-periods"> <div class="period active" data-period="day">Day</div> <div class="period" data-period="week">Week</div> <div class="period" data-period="month">Month</div> <div class="period" data-period="quarter">Quarter</div> <div class="period" data-period="year">Year</div> </div> </div> <div class="metrics"> <div class="metric-card"> <div class="metric-name">Revenue</div> <div class="metric-value">$86,245 <span class="metric-change positive">+8.2%</span></div> </div> <div class="metric-card"> <div class="metric-name">Expenses</div> <div class="metric-value">$32,750 <span class="metric-change negative">+3.1%</span></div> </div> <div class="metric-card"> <div class="metric-name">Profit Margin</div> <div class="metric-value">62.0% <span class="metric-change positive">+2.8%</span></div> </div> <div class="metric-card"> <div class="metric-name">Cash Flow</div> <div class="metric-value">$53,495 <span class="metric-change positive">+11.4%</span></div> </div> </div> <div class="chart-container"> <div class="chart-card"> <div class="chart-header"> <div class="chart-title">Revenue vs Expenses</div> <div class="chart-legend"> <div class="legend-item"> <div class="legend-color" style="background-color: var(--accent);"></div> <span>Revenue</span> </div> <div class="legend-item"> <div class="legend-color" style="background-color: var(--text-light);"></div> <span>Expenses</span> </div> </div> </div> <div class="chart" id="bar-chart"> <div class="tooltip" id="bar-tooltip"></div> <!-- Bars will be added dynamically --> </div> <div class="axis-labels" id="bar-labels"> <!-- Labels will be added dynamically --> </div> <button class="expand-button" id="expand-bar-data"> <span>View detailed data</span> <svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M19 9L12 16L5 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div class="data-table" id="bar-data-table"> <table> <thead> <tr> <th>Date</th> <th>Revenue</th> <th>Expenses</th> <th>Net</th> </tr> </thead> <tbody id="bar-table-body"> <!-- Table data will be added dynamically --> </tbody> </table> </div> </div> <div class="chart-card"> <div class="chart-header"> <div class="chart-title">Cash Flow Trend</div> <div class="chart-legend"> <div class="legend-item"> <div class="legend-color" style="background-color: var(--accent);"></div> <span>Cash Flow</span> </div> </div> </div> <div class="chart" id="line-chart"> <div class="tooltip" id="line-tooltip"></div> <!-- Line chart will be added dynamically --> </div> <div class="axis-labels" id="line-labels"> <!-- Labels will be added dynamically --> </div> <button class="expand-button" id="expand-line-data"> <span>View detailed data</span> <svg class="expand-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M19 9L12 16L5 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div class="data-table" id="line-data-table"> <table> <thead> <tr> <th>Date</th> <th>Cash In</th> <th>Cash Out</th> <th>Net Flow</th> </tr> </thead> <tbody id="line-table-body"> <!-- Table data will be added dynamically --> </tbody> </table> </div> </div> </div> </div> <script> // Data generation helper function generateData(period) { const data = { barChart: [], lineChart: [] }; const now = new Date(); let dataPoints; let dateFormat; let baseRevenue, baseExpenses, baseCashIn, baseCashOut; switch(period) { case 'day': dataPoints = 24; dateFormat = (d) => `${d.getHours()}:00`; baseRevenue = 3500; baseExpenses = 1200; baseCashIn = 2800; baseCashOut = 1000; break; case 'week': dataPoints = 7; dateFormat = (d) => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][d.getDay()]; baseRevenue = 12000; baseExpenses = 4500; baseCashIn = 10000; baseCashOut = 3800; break; case 'month': dataPoints = 30; dateFormat = (d) => `${d.getDate()}`; baseRevenue = 2800; baseExpenses = 1050; baseCashIn = 2400; baseCashOut = 900; break; case 'quarter': dataPoints = 12; dateFormat = (d) => `Week ${Math.ceil(d.getDate() / 7)}`; baseRevenue = 22000; baseExpenses = 8500; baseCashIn = 18000; baseCashOut = 7000; break; case 'year': dataPoints = 12; dateFormat = (d) => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()]; baseRevenue = 82000; baseExpenses = 31000; baseCashIn = 70000; baseCashOut = 27000; break; } for (let i = 0; i < dataPoints; i++) { const date = new Date(now); if (period === 'day') { date.setHours(now.getHours() - (dataPoints - 1) + i); } else if (period === 'week') { date.setDate(now.getDate() - (dataPoints - 1) + i); } else if (period === 'month') { date.setDate(now.getDate() - (dataPoints - 1) + i); } else if (period === 'quarter') { date.setDate(now.getDate() - (dataPoints - 1) * 7 + i * 7); } else if (period === 'year') { date.setMonth(now.getMonth() - (dataPoints - 1) + i); } const volatility = 0.2; const trendFactor = 1 + (i / dataPoints) * 0.1; // Slight upward trend const revenue = Math.round(baseRevenue * trendFactor * (1 + (Math.random() * 2 - 1) * volatility)); const expenses = Math.round(baseExpenses * trendFactor * (1 + (Math.random() * 2 - 1) * volatility)); const cashIn = Math.round(baseCashIn * trendFactor * (1 + (Math.random() * 2 - 1) * volatility)); const cashOut = Math.round(baseCashOut * trendFactor * (1 + (Math.random() * 2 - 1) * volatility)); data.barChart.push({ date: date, label: dateFormat(date), revenue: revenue, expenses: expenses, net: revenue - expenses }); data.lineChart.push({ date: date, label: dateFormat(date), cashIn: cashIn, cashOut: cashOut, netFlow: cashIn - cashOut }); } return data; } // Format currency function formatCurrency(amount) { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(amount); } // Format date function formatDate(date, period) { if (period === 'day') { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } else if (period === 'week' || period === 'month' || period === 'quarter') { return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); } else { return date.toLocaleDateString([], { month: 'short', year: 'numeric' }); } } // Initialize dashboard with data let currentPeriod = 'day'; let dashboardData = generateData(currentPeriod); // Render the bar chart function renderBarChart() { const chart = document.getElementById('bar-chart'); const tooltip = document.getElementById('bar-tooltip'); const labels = document.getElementById('bar-labels'); // Clear existing chart chart.innerHTML = ''; labels.innerHTML = ''; const data = dashboardData.barChart; const maxValue = Math.max(...data.map(d => Math.max(d.revenue, d.expenses))); const barSpacing = chart.offsetWidth / data.length; const barWidth = Math.min(12, barSpacing / 3); // Add bars data.forEach((item, i) => { const revenueHeight = (item.revenue / maxValue) * chart.offsetHeight * 0.85; const expensesHeight = (item.expenses / maxValue) * chart.offsetHeight * 0.85; const revenueBar = document.createElement('div'); const expensesBar = document.createElement('div'); revenueBar.className = 'bar'; revenueBar.style.height = '0px'; // Start at 0 for animation revenueBar.style.left = `${i * barSpacing + barSpacing / 2 - barWidth}px`; revenueBar.style.backgroundColor = 'var(--accent)'; expensesBar.className = 'bar'; expensesBar.style.height = '0px'; // Start at 0 for animation expensesBar.style.left = `${i * barSpacing + barSpacing / 2 + 2}px`; expensesBar.style.backgroundColor = 'var(--text-light)'; chart.appendChild(revenueBar); chart.appendChild(expensesBar); // Tooltip event handlers revenueBar.addEventListener('mouseenter', (e) => { tooltip.innerHTML = ` <div>${item.label}</div> <div class="tooltip-value">${formatCurrency(item.revenue)}</div> <div>Revenue</div> `; tooltip.style.opacity = '1'; tooltip.style.left = `${e.target.offsetLeft + barWidth / 2}px`; tooltip.style.top = `${chart.offsetHeight - revenueHeight - 10}px`; }); revenueBar.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; }); expensesBar.addEventListener('mouseenter', (e) => { tooltip.innerHTML = ` <div>${item.label}</div> <div class="tooltip-value">${formatCurrency(item.expenses)}</div> <div>Expenses</div> `; tooltip.style.opacity = '1'; tooltip.style.left = `${e.target.offsetLeft + barWidth / 2}px`; tooltip.style.top = `${chart.offsetHeight - expensesHeight - 10}px`; }); expensesBar.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; }); // Add label const label = document.createElement('div'); label.className = 'axis-label'; label.textContent = item.label; label.style.width = `${barSpacing}px`; labels.appendChild(label); // Animate bars setTimeout(() => { revenueBar.style.height = `${revenueHeight}px`; expensesBar.style.height = `${expensesHeight}px`; }, 100 + i * 50); }); // Update data table const tableBody = document.getElementById('bar-table-body'); tableBody.innerHTML = ''; data.forEach(item => { const row = document.createElement('tr'); row.innerHTML = ` <td>${formatDate(item.date, currentPeriod)}</td> <td>${formatCurrency(item.revenue)}</td> <td>${formatCurrency(item.expenses)}</td> <td>${formatCurrency(item.net)}</td> `; tableBody.appendChild(row); }); } // Render the line chart function renderLineChart() { const chart = document.getElementById('line-chart'); const tooltip = document.getElementById('line-tooltip'); const labels = document.getElementById('line-labels'); // Clear existing chart chart.innerHTML = ''; labels.innerHTML = ''; const data = dashboardData.lineChart; const maxValue = Math.max(...data.map(d => d.netFlow)); const pointSpacing = chart.offsetWidth / (data.length - 1); // Create line chart const lineChartContainer = document.createElement('div'); lineChartContainer.className = 'line-chart'; chart.appendChild(lineChartContainer); // Add points and line segments data.forEach((item, i) => { if (i < data.length - 1) { const startY = chart.offsetHeight - (item.netFlow / maxValue) * chart.offsetHeight * 0.85; const endY = chart.offsetHeight - (data[i+1].netFlow / maxValue) * chart.offsetHeight * 0.85; const startX = i * pointSpacing; const endX = (i + 1) * pointSpacing; // Calculate line length and angle const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); const angle = Math.atan2(endY - startY, endX - startX) * 180 / Math.PI; // Create line segment const line = document.createElement('div'); line.className = 'trend-line'; line.style.width = `${length}px`; line.style.transform = `translate(${startX}px, ${startY}px) rotate(${angle}deg)`; line.style.opacity = '0'; lineChartContainer.appendChild(line); // Animate line setTimeout(() => { line.style.opacity = '1'; }, 300 + i * 50); } // Create point const point = document.createElement('div'); point.className = 'trend-point'; const yPos = chart.offsetHeight - (item.netFlow / maxValue) * chart.offsetHeight * 0.85; point.style.left = `${i * pointSpacing}px`; point.style.top = `${yPos}px`; point.style.opacity = '0'; lineChartContainer.appendChild(point); // Tooltip event handlers point.addEventListener('mouseenter', (e) => { tooltip.innerHTML = ` <div>${item.label}</div> <div class="tooltip-value">${formatCurrency(item.netFlow)}</div> <div>Cash Flow</div> `; tooltip.style.opacity = '1'; tooltip.style.left = `${e.target.offsetLeft}px`; tooltip.style.top = `${yPos - 10}px`; }); point.addEventListener('mouseleave', () => { tooltip.style.opacity = '0'; }); // Add label if (i % Math.ceil(data.length / 8) === 0 || i === data.length - 1) { const label = document.createElement('div'); label.className = 'axis-label'; label.textContent = item.label; label.style.width = `${pointSpacing}px`; labels.appendChild(label); } // Animate point setTimeout(() => { point.style.opacity = '1'; }, 500 + i * 50); }); // Update data table const tableBody = document.getElementById('line-table-body'); tableBody.innerHTML = ''; data.forEach(item => { const row = document.createElement('tr'); row.innerHTML = ` <td>${formatDate(item.date, currentPeriod)}</td> <td>${formatCurrency(item.cashIn)}</td> <td>${formatCurrency(item.cashOut)}</td> <td>${formatCurrency(item.netFlow)}</td> `; tableBody.appendChild(row); }); } // Initialize charts renderBarChart(); renderLineChart(); // Time period selector functionality document.querySelectorAll('.period').forEach(btn => { btn.addEventListener('click', () => { document.querySelector('.period.active').classList.remove('active'); btn.classList.add('active'); currentPeriod = btn.dataset.period; dashboardData = generateData(currentPeriod); renderBarChart(); renderLineChart(); }); }); // Table expand functionality document.getElementById('expand-bar-data').addEventListener('click', function() { const table = document.getElementById('bar-data-table'); const icon = this.querySelector('.expand-icon'); table.classList.toggle('active'); icon.classList.toggle('rotated'); }); document.getElementById('expand-line-data').addEventListener('click', function() { const table = document.getElementById('line-data-table'); const icon = this.querySelector('.expand-icon'); table.classList.toggle('active'); icon.classList.toggle('rotated'); }); // Simulated real-time updates function simulateRealtimeUpdates() { // Update last data point with a small random change const barData = dashboardData.barChart; const lineData = dashboardData.lineChart; const lastBarPoint = barData[barData.length - 1]; const lastLinePoint = lineData[lineData.length - 1]; // Random small variations lastBarPoint.revenue = Math.round(lastBarPoint.revenue * (1 + (Math.random() * 0.04 - 0.02))); lastBarPoint.expenses = Math.round(lastBarPoint.expenses * (1 + (Math.random() * 0.03 - 0.015))); lastBarPoint.net = lastBarPoint.revenue - lastBarPoint.expenses; lastLinePoint.cashIn = Math.round(lastLinePoint.cashIn * (1 + (Math.random() * 0.04 - 0.02))); lastLinePoint.cashOut = Math.round(lastLinePoint.cashOut * (1 + (Math.random() * 0.03 - 0.015))); lastLinePoint.netFlow = lastLinePoint.cashIn - lastLinePoint.cashOut; // Re-render charts renderBarChart(); renderLineChart(); // Schedule next update setTimeout(simulateRealtimeUpdates, 5000); } // Start simulation after initial rendering setTimeout(simulateRealtimeUpdates, 3000); </script> </body> </html>
<html> <head> <title>Health Analytics Platform</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(140deg, #f0f8ff, #e6f3f7); display: flex; flex-direction: column; height: 100vh; max-height: 700px; max-width: 700px; width: 100%; overflow: hidden; } .container { flex: 1; display: flex; flex-direction: column; padding: 1.5rem; overflow: hidden; } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; } h1 { color: #2c5282; font-size: 1.5rem; font-weight: 600; margin: 0; } .controls { display: flex; gap: 0.75rem; } .time-selector { display: flex; background: rgba(255, 255, 255, 0.8); border-radius: 20px; padding: 0.25rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .time-selector button { background: none; border: none; padding: 0.35rem 0.8rem; font-size: 0.85rem; color: #568ea6; border-radius: 15px; cursor: pointer; transition: all 0.2s ease; } .time-selector button.active { background: linear-gradient(135deg, #4299e1, #38b2ac); color: white; box-shadow: 0 2px 5px rgba(56, 178, 172, 0.3); } .dashboard { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: auto 1fr; gap: 1rem; flex: 1; overflow: hidden; } .card { background: rgba(255, 255, 255, 0.9); border-radius: 12px; padding: 1rem; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .card:hover { transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); } .summary-cards { grid-column: 1 / -1; display: flex; gap: 1rem; } .summary-card { flex: 1; padding: 1rem; background: linear-gradient(135deg, #ebf8ff, #e6fffa); border-radius: 12px; display: flex; flex-direction: column; align-items: flex-start; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); position: relative; overflow: hidden; } .summary-card::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #4299e1, #38b2ac); opacity: 0.8; } .summary-title { font-size: 0.8rem; color: #4a5568; margin-bottom: 0.5rem; } .summary-value { font-size: 1.5rem; font-weight: 600; color: #2c5282; margin-bottom: 0.2rem; } .summary-change { font-size: 0.7rem; display: flex; align-items: center; gap: 0.2rem; } .positive { color: #38a169; } .negative { color: #e53e3e; } .card-title { font-size: 0.9rem; color: #4a5568; margin-bottom: 0.8rem; display: flex; justify-content: space-between; align-items: center; } .chart-container { flex: 1; position: relative; min-height: 150px; } .chart-key { display: flex; justify-content: flex-end; gap: 1rem; font-size: 0.7rem; color: #718096; margin-top: -0.3rem; } .key-item { display: flex; align-items: center; gap: 0.3rem; } .key-color { width: 0.5rem; height: 0.5rem; border-radius: 50%; } .patient-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; color: #4a5568; margin-top: 0.5rem; overflow-y: auto; } .patient-table th { text-align: left; padding: 0.5rem; font-weight: 500; color: #2d3748; border-bottom: 1px solid #e2e8f0; } .patient-table td { padding: 0.5rem; border-bottom: 1px solid #e2e8f0; } .risk-high { color: #e53e3e; } .risk-medium { color: #dd6b20; } .risk-low { color: #38a169; } .tooltip { position: absolute; background: rgba(255, 255, 255, 0.95); border-radius: 6px; padding: 0.75rem; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); font-size: 0.8rem; color: #2d3748; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 10; max-width: 200px; } .tooltip-title { font-weight: 600; margin-bottom: 0.3rem; } .tooltip-value { display: flex; justify-content: space-between; margin-bottom: 0.2rem; } .tooltip-label { color: #718096; } .grid-lines { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 0; } .grid-line { position: absolute; left: 0; right: 0; height: 1px; background-color: rgba(226, 232, 240, 0.7); } .y-axis { position: absolute; top: 0; bottom: 0; left: 0; display: flex; flex-direction: column; justify-content: space-between; padding: 0.5rem 0; font-size: 0.7rem; color: #a0aec0; z-index: 1; } .axis-label { transform: translateY(50%); } .bar-chart { position: relative; height: 100%; display: flex; align-items: flex-end; padding-left: 1.5rem; padding-bottom: 1.5rem; z-index: 2; } .bar-group { display: flex; flex: 1; height: 100%; position: relative; align-items: flex-end; justify-content: center; gap: 2px; } .bar { width: 10px; background: linear-gradient(to top, #4299e1, #7bc6cc); border-radius: 4px 4px 0 0; transition: height 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .bar-secondary { background: linear-gradient(to top, #38b2ac, #9ae6b4); opacity: 0.8; } .x-axis { position: absolute; bottom: 0; left: 1.5rem; right: 0; display: flex; justify-content: space-between; font-size: 0.7rem; color: #a0aec0; padding-bottom: 0.2rem; } .line-chart { position: relative; height: 100%; padding-left: 1.5rem; padding-bottom: 1.5rem; z-index: 2; } .line { position: absolute; height: 3px; background: linear-gradient(90deg, transparent, #38b2ac, transparent); transform-origin: left center; z-index: 3; } .pulse-dot { position: absolute; width: 8px; height: 8px; border-radius: 50%; background-color: #4299e1; z-index: 4; box-shadow: 0 0 0 rgba(66, 153, 225, 0.4); animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(66, 153, 225, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(66, 153, 225, 0); } 100% { box-shadow: 0 0 0 0 rgba(66, 153, 225, 0); } } .donut-chart { position: relative; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; } .donut-segment { position: absolute; width: 100%; height: 100%; clip: rect(0, 150px, 150px, 75px); transform: rotate(0deg); } .donut-segment-1 { transform: rotate(0deg); } .donut-segment-2 { transform: rotate(90deg); } .donut-segment-3 { transform: rotate(180deg); } .donut-segment-4 { transform: rotate(270deg); } .donut-center { position: absolute; width: 70%; height: 70%; background: white; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05); } .donut-value { font-size: 1.5rem; font-weight: 600; color: #2c5282; } .donut-label { font-size: 0.7rem; color: #718096; } .table-container { margin-top: 0.5rem; overflow-y: auto; max-height: 150px; } /* Loading animation */ .loading { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; z-index: 100; } .loading-spinner { width: 40px; height: 40px; border: 3px solid rgba(66, 153, 225, 0.2); border-top-color: #4299e1; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Patient profile dots */ .patient-status { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 5px; } .status-online { background-color: #48bb78; box-shadow: 0 0 0 rgba(72, 187, 120, 0.4); animation: pulse 2s infinite; } .status-offline { background-color: #a0aec0; } .status-alert { background-color: #f56565; box-shadow: 0 0 0 rgba(245, 101, 101, 0.4); animation: pulse 2s infinite; } /* Responsive adjustments */ @media (max-width: 600px) { .dashboard { grid-template-columns: 1fr; } .summary-cards { flex-direction: column; gap: 0.5rem; } .container { padding: 0.75rem; } header { flex-direction: column; align-items: flex-start; gap: 0.5rem; } .time-selector button { padding: 0.25rem 0.5rem; font-size: 0.7rem; } } </style> </head> <body> <div class="container"> <header> <h1>Clinical Insights Dashboard</h1> <div class="controls"> <div class="time-selector"> <button class="time-btn" data-period="week">Week</button> <button class="time-btn active" data-period="month">Month</button> <button class="time-btn" data-period="quarter">Quarter</button> </div> </div> </header> <div class="dashboard"> <div class="summary-cards"> <div class="summary-card"> <div class="summary-title">Avg. Blood Glucose</div> <div class="summary-value">128 mg/dL</div> <div class="summary-change negative">↑ 4.2% from last period</div> </div> <div class="summary-card"> <div class="summary-title">Avg. Systolic BP</div> <div class="summary-value">124 mmHg</div> <div class="summary-change positive">↓ 2.8% from last period</div> </div> <div class="summary-card"> <div class="summary-title">Compliance Rate</div> <div class="summary-value">87%</div> <div class="summary-change positive">↑ 5.3% from last period</div> </div> </div> <div class="card"> <div class="card-title"> <span>Blood Glucose Trends</span> <div class="chart-key"> <div class="key-item"> <div class="key-color" style="background-color: #4299e1;"></div> <span>Fasting</span> </div> <div class="key-item"> <div class="key-color" style="background-color: #38b2ac;"></div> <span>Post-meal</span> </div> </div> </div> <div class="chart-container" id="glucose-chart"> <div class="grid-lines" id="glucose-grid"></div> <div class="y-axis" id="glucose-y-axis"></div> <div class="bar-chart" id="glucose-bars"></div> <div class="x-axis" id="glucose-x-axis"></div> </div> <div class="tooltip" id="glucose-tooltip"></div> </div> <div class="card"> <div class="card-title"> <span>Blood Pressure Distribution</span> </div> <div class="chart-container" id="bp-chart"> <div class="donut-chart" id="bp-donut"> <div class="donut-center"> <div class="donut-value">72%</div> <div class="donut-label">Controlled</div> </div> </div> </div> </div> <div class="card"> <div class="card-title"> <span>Medication Adherence</span> </div> <div class="chart-container" id="adherence-chart"> <div class="grid-lines" id="adherence-grid"></div> <div class="y-axis" id="adherence-y-axis"></div> <div class="line-chart" id="adherence-line"></div> <div class="x-axis" id="adherence-x-axis"></div> </div> <div class="tooltip" id="adherence-tooltip"></div> </div> <div class="card"> <div class="card-title"> <span>Patients Requiring Attention</span> </div> <div class="table-container"> <table class="patient-table"> <thead> <tr> <th>Patient</th> <th>Status</th> <th>Risk Level</th> </tr> </thead> <tbody id="patient-table-body"> <!-- Populated via JavaScript --> </tbody> </table> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Sample data for different time periods const data = { week: { glucose: { fasting: [112, 118, 107, 115, 121, 113, 110], postMeal: [145, 152, 138, 147, 158, 143, 140] }, bp: { normal: 65, elevated: 20, hypertension1: 10, hypertension2: 5 }, adherence: [82, 85, 80, 88, 82, 84, 86], days: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] }, month: { glucose: { fasting: [110, 114, 108, 116, 112, 117, 114, 110, 112, 109, 115, 119], postMeal: [142, 147, 140, 150, 145, 153, 149, 143, 146, 141, 148, 154] }, bp: { normal: 72, elevated: 15, hypertension1: 8, hypertension2: 5 }, adherence: [84, 86, 83, 89, 87, 85, 90, 88, 86, 89, 91, 87], days: ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12'] }, quarter: { glucose: { fasting: [112, 115, 110, 118, 114, 116], postMeal: [145, 150, 143, 155, 147, 152] }, bp: { normal: 68, elevated: 18, hypertension1: 9, hypertension2: 5 }, adherence: [83, 85, 87, 86, 88, 90], days: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] } }; // Patients data const patients = [ { id: 1, name: "Sarah Johnson", status: "alert", risk: "high", lastReading: "Glucose: 210 mg/dL", lastVisit: "3 days ago" }, { id: 2, name: "Michael Chen", status: "online", risk: "medium", lastReading: "BP: 145/95 mmHg", lastVisit: "1 day ago" }, { id: 3, name: "Robert Garcia", status: "offline", risk: "low", lastReading: "Glucose: 118 mg/dL", lastVisit: "5 days ago" }, { id: 4, name: "Emily Wilson", status: "alert", risk: "high", lastReading: "BP: 160/100 mmHg", lastVisit: "2 days ago" }, { id: 5, name: "David Patel", status: "online", risk: "medium", lastReading: "Glucose: 165 mg/dL", lastVisit: "Today" } ]; let currentPeriod = 'month'; let tooltipTimeout; // Initialize charts initializeGlucoseChart(currentPeriod); initializeBPDonut(currentPeriod); initializeAdherenceChart(currentPeriod); populatePatientTable(); // Set up time period selectors document.querySelectorAll('.time-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentPeriod = this.dataset.period; // Add loading animation addLoading(); // Update charts with animation delay setTimeout(() => { initializeGlucoseChart(currentPeriod); initializeBPDonut(currentPeriod); initializeAdherenceChart(currentPeriod); removeLoading(); }, 600); }); }); function addLoading() { const loadingEls = document.querySelectorAll('.chart-container'); loadingEls.forEach(el => { const loading = document.createElement('div'); loading.className = 'loading'; loading.innerHTML = '<div class="loading-spinner"></div>'; el.appendChild(loading); }); } function removeLoading() { const loadingEls = document.querySelectorAll('.loading'); loadingEls.forEach(el => { el.remove(); }); } function initializeGlucoseChart(period) { const chartData = data[period].glucose; const days = data[period].days; const container = document.getElementById('glucose-chart'); const barChart = document.getElementById('glucose-bars'); const gridLines = document.getElementById('glucose-grid'); const yAxis = document.getElementById('glucose-y-axis'); const xAxis = document.getElementById('glucose-x-axis'); const tooltip = document.getElementById('glucose-tooltip'); // Clear previous content barChart.innerHTML = ''; gridLines.innerHTML = ''; yAxis.innerHTML = ''; xAxis.innerHTML = ''; // Find max value for scaling const allValues = [...chartData.fasting, ...chartData.postMeal]; const maxValue = Math.max(...allValues) * 1.1; const minValue = Math.min(...allValues) * 0.9; const range = maxValue - minValue; // Create grid lines and y-axis labels const gridCount = 5; for (let i = 0; i <= gridCount; i++) { const position = 100 - (i * (100 / gridCount)); const value = Math.round(minValue + (i * (range / gridCount))); const gridLine = document.createElement('div'); gridLine.className = 'grid-line'; gridLine.style.top = `${position}%`; gridLines.appendChild(gridLine); const label = document.createElement('div'); label.className = 'axis-label'; label.textContent = value; label.style.bottom = `${position}%`; yAxis.appendChild(label); } // Create x-axis labels days.forEach((day, i) => { const label = document.createElement('div'); label.textContent = day; xAxis.appendChild(label); }); // Create bars with animation chartData.fasting.forEach((value, i) => { const barGroup = document.createElement('div'); barGroup.className = 'bar-group'; const bar1 = document.createElement('div'); bar1.className = 'bar'; const height1 = ((value - minValue) / range) * 100; bar1.style.height = '0'; const bar2 = document.createElement('div'); bar2.className = 'bar bar-secondary'; const postMealValue = chartData.postMeal[i]; const height2 = ((postMealValue - minValue) / range) * 100; bar2.style.height = '0'; barGroup.appendChild(bar1); barGroup.appendChild(bar2); barChart.appendChild(barGroup); // Add hover effect for tooltip barGroup.addEventListener('mouseenter', function(e) { tooltip.innerHTML = ` <div class="tooltip-title">${days[i]}</div> <div class="tooltip-value"> <span class="tooltip-label">Fasting:</span> <span>${value} mg/dL</span> </div> <div class="tooltip-value"> <span class="tooltip-label">Post-meal:</span> <span>${postMealValue} mg/dL</span> </div> `; // Position tooltip const rect = barGroup.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); tooltip.style.left = `${rect.left - containerRect.left + rect.width/2 - 100/2}px`; tooltip.style.top = `${rect.top - containerRect.top - 70}px`; tooltip.style.opacity = '1'; }); barGroup.addEventListener('mouseleave', function() { tooltip.style.opacity = '0'; }); // Animate bars after a short delay setTimeout(() => { bar1.style.height = `${height1}%`; bar2.style.height = `${height2}%`; }, 100 + i * 50); }); } function initializeBPDonut(period) { const chartData = data[period].bp; const donutChart = document.getElementById('bp-donut'); const centerValue = document.querySelector('.donut-value'); // Update center value centerValue.textContent = `${chartData.normal}%`; // Clear previous segments donutChart.querySelectorAll('.donut-segment').forEach(el => el.remove()); // Calculate segment angles const total = chartData.normal + chartData.elevated + chartData.hypertension1 + chartData.hypertension2; const normalAngle = (chartData.normal / total) * 360; const elevatedAngle = (chartData.elevated / total) * 360; const hyper1Angle = (chartData.hypertension1 / total) * 360; const hyper2Angle = (chartData.hypertension2 / total) * 360; // Create segments createDonutSegment(donutChart, 0, normalAngle, '#48bb78', 'Normal', chartData.normal); createDonutSegment(donutChart, normalAngle, normalAngle + elevatedAngle, '#ecc94b', 'Elevated', chartData.elevated); createDonutSegment(donutChart, normalAngle + elevatedAngle, normalAngle + elevatedAngle + hyper1Angle, '#ed8936', 'Stage 1 HTN', chartData.hypertension1); createDonutSegment(donutChart, normalAngle + elevatedAngle + hyper1Angle, 360, '#e53e3e', 'Stage 2 HTN', chartData.hypertension2); } function createDonutSegment(container, startAngle, endAngle, color, label, value) { const segment = document.createElement('div'); segment.className = 'donut-segment'; segment.style.transform = `rotate(${startAngle}deg)`; const fill = document.createElement('div'); fill.style.transform = `rotate(${endAngle - startAngle}deg)`; fill.style.width = '100%'; fill.style.height = '100%'; fill.style.borderRadius = '50%'; fill.style.clip = 'rect(0, 75px, 150px, 0)'; fill.style.background = color; fill.style.opacity = '0'; fill.style.transition = 'opacity 0.5s ease-in-out'; segment.appendChild(fill); container.appendChild(segment); // Tooltip behavior segment.addEventListener('mouseenter', function() { const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.innerHTML = ` <div class="tooltip-title">${label}</div> <div class="tooltip-value"> <span class="tooltip-label">Patients:</span> <span>${value}%</span> </div> `; // Position tooltip const rect = container.getBoundingClientRect(); tooltip.style.left = `${rect.width/2 - 50}px`; tooltip.style.top = `${rect.height/2 - 60}px`; tooltip.style.opacity = '1'; container.appendChild(tooltip); // Highlight segment fill.style.opacity = '1'; tooltipTimeout = setTimeout(() => { tooltip.style.opacity = '0'; setTimeout(() => { tooltip.remove(); }, 300); }, 3000); }); segment.addEventListener('mouseleave', function() { clearTimeout(tooltipTimeout); const tooltip = container.querySelector('.tooltip'); if (tooltip) { tooltip.style.opacity = '0'; setTimeout(() => { tooltip.remove(); }, 300); } fill.style.opacity = '0.8'; }); // Animate in setTimeout(() => { fill.style.opacity = '0.8'; }, 200); } function initializeAdherenceChart(period) { const chartData = data[period].adherence; const days = data[period].days; const container = document.getElementById('adherence-chart'); const lineChart = document.getElementById('adherence-line'); const gridLines = document.getElementById('adherence-grid'); const yAxis = document.getElementById('adherence-y-axis'); const xAxis = document.getElementById('adherence-x-axis'); const tooltip = document.getElementById('adherence-tooltip'); // Clear previous content lineChart.innerHTML = ''; gridLines.innerHTML = ''; yAxis.innerHTML = ''; xAxis.innerHTML = ''; // Find max and min values for scaling const maxValue = Math.max(...chartData) * 1.05; const minValue = Math.min(...chartData) * 0.95; const range = maxValue - minValue; // Create grid lines and y-axis labels const gridCount = 5; for (let i = 0; i <= gridCount; i++) { const position = 100 - (i * (100 / gridCount)); const value = Math.round(minValue + (i * (range / gridCount))); const gridLine = document.createElement('div'); gridLine.className = 'grid-line'; gridLine.style.top = `${position}%`; gridLines.appendChild(gridLine); const label = document.createElement('div'); label.className = 'axis-label'; label.textContent = `${value}%`; label.style.bottom = `${position}%`; yAxis.appendChild(label); } // Create x-axis labels days.forEach((day, i) => { const label = document.createElement('div'); label.textContent = day; xAxis.appendChild(label); }); // Prepare points for the line const points = chartData.map((value, i) => { const x = (i / (chartData.length - 1)) * 100; const y = 100 - ((value - minValue) / range) * 100; return { x, y, value }; }); // Add lines between points with animation for (let i = 0; i < points.length - 1; i++) { const start = points[i]; const end = points[i + 1]; // Calculate line length and angle const deltaX = end.x - start.x; const deltaY = end.y - start.y; const length = Math.sqrt(deltaX * deltaX + deltaY * deltaY); const angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI); // Create line element const line = document.createElement('div'); line.className = 'line'; line.style.top = `${start.y}%`; line.style.left = `${start.x}%`; line.style.width = `0`; line.style.transform = `rotate(${angle}deg)`; lineChart.appendChild(line); // Animate line drawing setTimeout(() => { line.style.width = `${length}%`; }, 300 + i * 100); } // Add points with tooltip behavior points.forEach((point, i) => { const dot = document.createElement('div'); dot.className = 'pulse-dot'; dot.style.top = `${point.y}%`; dot.style.left = `${point.x}%`; dot.style.transform = 'translate(-50%, -50%)'; dot.style.opacity = '0'; lineChart.appendChild(dot); // Animate dot appearance setTimeout(() => { dot.style.opacity = '1'; }, 800 + i * 100); // Add hover effect for tooltip dot.addEventListener('mouseenter', function() { tooltip.innerHTML = ` <div class="tooltip-title">${days[i]}</div> <div class="tooltip-value"> <span class="tooltip-label">Adherence:</span> <span>${point.value}%</span> </div> `; // Position tooltip const rect = dot.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); tooltip.style.left = `${rect.left - containerRect.left}px`; tooltip.style.top = `${rect.top - containerRect.top - 60}px`; tooltip.style.opacity = '1'; }); dot.addEventListener('mouseleave', function() { tooltip.style.opacity = '0'; }); }); } function populatePatientTable() { const tableBody = document.getElementById('patient-table-body'); tableBody.innerHTML = ''; patients.forEach(patient => { const row = document.createElement('tr'); const nameCell = document.createElement('td'); const statusDot = document.createElement('span'); statusDot.className = `patient-status status-${patient.status}`; nameCell.appendChild(statusDot); nameCell.appendChild(document.createTextNode(patient.name
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Social Media Analytics Dashboard</title> <style> :root { --primary: #5E60CE; --secondary: #6930C3; --accent1: #64DFDF; --accent2: #FB5607; --accent3: #FF006E; --accent4: #FFBE0B; --light: #F8F9FA; --dark: #212529; --gray: #6C757D; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--light); color: var(--dark); display: flex; flex-direction: column; width: 100%; height: 100%; max-width: 700px; max-height: 700px; overflow: hidden; } .dashboard { display: grid; grid-template-rows: auto 1fr auto; height: 100vh; max-height: 700px; padding: 15px; gap: 15px; } header { display: flex; justify-content: space-between; align-items: center; } h1 { font-size: 1.6rem; font-weight: 700; color: var(--secondary); margin: 0; } .time-filter { display: flex; gap: 8px; } .time-btn { background: none; border: 1px solid var(--gray); border-radius: 20px; padding: 5px 12px; font-size: 0.8rem; color: var(--gray); cursor: pointer; transition: all 0.2s ease; } .time-btn.active { background-color: var(--primary); color: white; border-color: var(--primary); } .time-btn:hover:not(.active) { background-color: var(--light); border-color: var(--primary); color: var(--primary); } main { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; gap: 15px; overflow: hidden; } .card { background-color: white; border-radius: 10px; padding: 15px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; position: relative; overflow: hidden; } .card-title { font-size: 0.9rem; font-weight: 600; color: var(--gray); display: flex; justify-content: space-between; margin-bottom: 10px; } .card-help { width: 18px; height: 18px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.7rem; background-color: var(--gray); color: white; cursor: pointer; position: relative; } .tooltip { position: absolute; top: 25px; right: 0; width: 180px; padding: 8px; background-color: var(--dark); color: white; border-radius: 5px; font-size: 0.75rem; z-index: 10; opacity: 0; visibility: hidden; transition: opacity 0.3s, visibility 0.3s; } .card-help:hover .tooltip { opacity: 1; visibility: visible; } .chart-container { flex: 1; position: relative; display: flex; align-items: center; justify-content: center; } .legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; } .legend-item { display: flex; align-items: center; gap: 5px; font-size: 0.7rem; cursor: pointer; transition: opacity 0.3s; } .legend-color { width: 10px; height: 10px; border-radius: 2px; } .summary { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; } footer { display: flex; justify-content: space-between; align-items: center; padding-top: 5px; } .insight-card { background: linear-gradient(135deg, var(--accent1), var(--primary)); color: white; border-radius: 8px; padding: 10px 15px; font-size: 0.85rem; } .insight-title { font-weight: 600; margin-bottom: 3px; display: flex; justify-content: space-between; } .insight-title i { font-size: 0.9rem; } .metric-card { background-color: white; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 5px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); } .metric-title { font-size: 0.7rem; color: var(--gray); } .metric-value { font-size: 1.1rem; font-weight: 700; } .metric-change { font-size: 0.7rem; display: flex; align-items: center; gap: 3px; } .metric-change.positive { color: #28a745; } .metric-change.negative { color: #dc3545; } .pie-segment { transition: transform 0.3s; cursor: pointer; } .pie-segment:hover { transform: translateX(10px); } .bar { transition: height 1s cubic-bezier(0.2, 0.8, 0.2, 1); } .line { stroke-dasharray: 1000; stroke-dashoffset: 1000; animation: dash 3s forwards; } @keyframes dash { to { stroke-dashoffset: 0; } } .donut-segment { transition: all 0.3s; } .donut-segment:hover { opacity: 0.8; transform: scale(1.05); transform-origin: center; } .legend-item.dimmed { opacity: 0.5; } .pulse { animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .data-point { transition: all 0.3s; cursor: pointer; } .data-point:hover { r: 6; } .data-tooltip { position: absolute; background-color: var(--dark); color: white; padding: 5px 10px; border-radius: 4px; font-size: 0.75rem; pointer-events: none; opacity: 0; transition: opacity 0.3s; z-index: 10; } .progress-ring { transform: rotate(-90deg); transform-origin: center; } .progress-ring-circle-bg { fill: none; stroke: #e9ecef; stroke-width: 12; } .progress-ring-circle { fill: none; stroke-width: 12; stroke-linecap: round; transition: stroke-dashoffset 1s ease; } .key-stat { text-align: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .key-stat .value { font-size: 1.8rem; font-weight: 700; color: var(--secondary); } .key-stat .label { font-size: 0.7rem; color: var(--gray); } .bar-chart-container { width: 100%; height: 100%; display: flex; flex-direction: column; } .bar-chart { flex: 1; display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-bottom: 20px; } .bar-wrapper { flex: 1; height: 100%; display: flex; flex-direction: column; align-items: center; position: relative; } .bar { width: 100%; background-color: var(--primary); border-radius: 3px 3px 0 0; position: relative; transition: height 0.5s ease; } .bar-label { font-size: 0.6rem; margin-top: 5px; color: var(--gray); } .line-chart-container { position: relative; width: 100%; height: 100%; } .axis-line { stroke: #e9ecef; stroke-width: 1; } .grid-line { stroke: #f8f9fa; stroke-width: 1; } .comparison-toggle { display: flex; gap: 5px; justify-content: center; } .toggle-btn { background: none; border: none; font-size: 0.7rem; color: var(--gray); cursor: pointer; padding: 2px 8px; border-radius: 4px; transition: all 0.3s; } .toggle-btn.active { background-color: var(--primary); color: white; } .toggle-btn:hover:not(.active) { color: var(--primary); } .line-chart { margin-top: 5px; } .notification-badge { position: absolute; top: -5px; right: -5px; background-color: var(--accent3); color: white; width: 16px; height: 16px; border-radius: 50%; font-size: 0.6rem; display: flex; align-items: center; justify-content: center; animation: pulse 2s infinite; } @media (max-width: 600px) { .dashboard { padding: 10px; gap: 10px; } main { grid-template-columns: 1fr; } h1 { font-size: 1.3rem; } .summary { grid-template-columns: 1fr 1fr; } } </style> </head> <body> <div class="dashboard"> <header> <h1>Social Media Pulse</h1> <div class="time-filter"> <button class="time-btn">Day</button> <button class="time-btn active">Week</button> <button class="time-btn">Month</button> </div> </header> <main> <div class="card"> <div class="card-title"> <span>Engagement Distribution</span> <div class="card-help">? <div class="tooltip">Shows distribution of engagement types across platforms</div> </div> </div> <div class="chart-container" id="donut-container"> <svg width="100%" height="100%" viewBox="0 0 200 200" id="donut-chart"> <!-- Donut chart will be drawn here --> </svg> </div> <div class="legend" id="donut-legend"> <!-- Legend will be populated by JavaScript --> </div> </div> <div class="card"> <div class="card-title"> <span>Audience Growth</span> <div class="card-help">? <div class="tooltip">Tracks follower/subscriber count changes over time</div> </div> </div> <div class="comparison-toggle"> <button class="toggle-btn active" data-platform="all">All Platforms</button> <button class="toggle-btn" data-platform="instagram">Instagram</button> <button class="toggle-btn" data-platform="twitter">Twitter</button> </div> <div class="chart-container" id="line-container"> <div class="line-chart"> <svg width="100%" height="100%" viewBox="0 0 300 150" id="line-chart"> <!-- Line chart will be drawn here --> </svg> </div> <div class="data-tooltip" id="lineTooltip"></div> </div> </div> <div class="card"> <div class="card-title"> <span>Daily Post Performance</span> <div class="card-help">? <div class="tooltip">Shows engagement metrics for each day's posts</div> </div> </div> <div class="bar-chart-container" id="bar-container"> <div class="bar-chart" id="bar-chart"> <!-- Bars will be added here by JavaScript --> </div> </div> </div> <div class="card"> <div class="card-title"> <span>Audience Sentiment</span> <div class="card-help">? <div class="tooltip">Measures positive vs negative sentiment in comments and mentions</div> </div> </div> <div class="chart-container"> <svg width="100%" height="160" viewBox="0 0 200 200" id="sentiment-gauge"> <circle class="progress-ring-circle-bg" cx="100" cy="100" r="85"></circle> <circle class="progress-ring-circle" cx="100" cy="100" r="85" stroke-dasharray="533.8" stroke-dashoffset="0"></circle> </svg> <div class="key-stat"> <div class="value">72%</div> <div class="label">Positive</div> </div> </div> </div> </main> <footer> <div class="summary"> <div class="metric-card"> <div class="metric-title">Total Reach</div> <div class="metric-value">287.5K</div> <div class="metric-change positive">+12.3% <span>↑</span></div> </div> <div class="metric-card"> <div class="metric-title">Avg. Engagement</div> <div class="metric-value">4.8%</div> <div class="metric-change positive">+0.6% <span>↑</span></div> </div> <div class="metric-card" style="position: relative;"> <div class="notification-badge">1</div> <div class="metric-title">Click Rate</div> <div class="metric-value">2.1%</div> <div class="metric-change negative">-0.3% <span>↓</span></div> </div> </div> <div class="insight-card pulse"> <div class="insight-title"> <span>Key Insight</span> <i>✨</i> </div> <div>Video content is generating 3.2x more engagement than images this week.</div> </div> </footer> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Time filter interaction const timeButtons = document.querySelectorAll('.time-btn'); timeButtons.forEach(btn => { btn.addEventListener('click', function() { timeButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); // We would refresh data based on the time range here updateAllCharts(); }); }); // Toggle buttons for line chart const toggleButtons = document.querySelectorAll('.toggle-btn'); toggleButtons.forEach(btn => { btn.addEventListener('click', function() { toggleButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); updateLineChart(this.dataset.platform); }); }); // Donut Chart const donutData = [ { label: "Likes", value: 45, color: "#5E60CE" }, { label: "Comments", value: 25, color: "#6930C3" }, { label: "Shares", value: 20, color: "#64DFDF" }, { label: "Saves", value: 10, color: "#FFBE0B" } ]; function createDonutChart() { const svg = document.getElementById('donut-chart'); const width = 200; const height = 200; const radius = Math.min(width, height) / 2; const donutWidth = 35; let total = donutData.reduce((sum, item) => sum + item.value, 0); let startAngle = 0; donutData.forEach((item, index) => { const angle = (item.value / total) * 360; const endAngle = startAngle + angle; // Convert angles to radians for calculations const startRad = (startAngle - 90) * Math.PI / 180; const endRad = (endAngle - 90) * Math.PI / 180; // Calculate path const x1 = radius + radius * Math.cos(startRad); const y1 = radius + radius * Math.sin(startRad); const x2 = radius + radius * Math.cos(endRad); const y2 = radius + radius * Math.sin(endRad); // Determine if the arc should take the long way around (large-arc-flag) const largeArcFlag = angle > 180 ? 1 : 0; // Create path for outer arc const outerPath = `M ${radius},${radius} L ${x1},${y1} A ${radius},${radius} 0 ${largeArcFlag},1 ${x2},${y2} Z`; // Inner radius const innerRadius = radius - donutWidth; // Calculate inner arc points const x3 = radius + innerRadius * Math.cos(startRad); const y3 = radius + innerRadius * Math.sin(startRad); const x4 = radius + innerRadius * Math.cos(endRad); const y4 = radius + innerRadius * Math.sin(endRad); // Create path for donut segment const donutPath = ` M ${x1},${y1} A ${radius},${radius} 0 ${largeArcFlag},1 ${x2},${y2} L ${x4},${y4} A ${innerRadius},${innerRadius} 0 ${largeArcFlag},0 ${x3},${y3} Z `; // Create path element const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", donutPath); path.setAttribute("fill", item.color); path.setAttribute("class", "donut-segment"); path.setAttribute("data-name", item.label); path.setAttribute("data-value", item.value); // Add to SVG svg.appendChild(path); // Update start angle for next segment startAngle = endAngle; }); // Create legend const legend = document.getElementById('donut-legend'); donutData.forEach(item => { const legendItem = document.createElement('div'); legendItem.className = 'legend-item'; legendItem.setAttribute('data-name', item.label); const colorBox = document.createElement('div'); colorBox.className = 'legend-color'; colorBox.style.backgroundColor = item.color; const labelText = document.createElement('span'); labelText.textContent = `${item.label} (${item.value}%)`; legendItem.appendChild(colorBox); legendItem.appendChild(labelText); legend.appendChild(legendItem); // Add click event to highlight/dim segments legendItem.addEventListener('click', function() { const name = this.getAttribute('data-name'); const segments = document.querySelectorAll('.donut-segment'); const legendItems = document.querySelectorAll('.legend-item'); if (this.classList.contains('dimmed')) { // Restore all segments.forEach(seg => seg.style.opacity = 1); legendItems.forEach(item => item.classList.remove('dimmed')); } else { // Dim all except selected segments.forEach(seg => { if (seg.getAttribute('data-name') === name) { seg.style.opacity = 1; } else { seg.style.opacity = 0.3; } }); legendItems.forEach(item => { if (item.getAttribute('data-name') !== name) { item.classList.add('dimmed'); } }); } }); }); } // Line Chart const lineData = { all: [ { day: 'Mon', value: 12500 }, { day: 'Tue', value: 13200 }, { day: 'Wed', value: 14800 }, { day: 'Thu', value: 14200 }, { day: 'Fri', value: 15400 }, { day: 'Sat', value: 17200 }, { day: 'Sun', value: 18500 } ], instagram: [ { day: 'Mon', value: 7500 }, { day: 'Tue', value: 7800 }, { day: 'Wed', value: 8200 }, { day: 'Thu', value: 8500 }, { day: 'Fri', value: 9100 }, { day: 'Sat', value: 10500 }, { day: 'Sun', value: 11200 } ], twitter: [ { day: 'Mon', value: 5000 }, { day: 'Tue', value: 5400 }, { day: 'Wed', value: 6600 }, { day: 'Thu', value: 5700 }, { day: 'Fri', value: 6300 }, { day: 'Sat', value: 6700 }, { day: 'Sun', value: 7300 } ] }; function updateLineChart(platform = 'all') { const svg = document.getElementById('line-chart'); svg.innerHTML = ''; // Clear previous chart const data = lineData[platform]; const width = 300; const height = 150; const padding = { top: 10, right: 10, bottom: 30, left: 40 }; // Find max value for scaling const maxValue = Math.max(...data.map(d => d.value)); // Create scales const xScale = (width - padding.left - padding.right) / (data.length - 1); const yScale = (height - padding.top - padding.bottom) / maxValue; // Draw grid lines for (let i = 0; i <= 4; i++) { const y = height - padding.bottom - (i * (height - padding.top - padding.bottom) / 4); const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); line.setAttribute("x1", padding.left); line.setAttribute("y1", y); line.setAttribute("x2", width - padding.right); line.setAttribute("y2", y); line.setAttribute("class", "grid-line"); svg.appendChild(line); // Add y-axis labels const label = document.createElementNS("http://www.w3.org/2000/svg", "text"); label.setAttribute("x", padding.left - 5); label.setAttribute("y", y + 4); label.setAttribute("text-anchor", "end"); label.setAttribute("font-size", "8"); label.setAttribute("fill", "#6C757D"); const value = Math.round(i * maxValue / 4 / 1000) + "K"; label.textContent = value; svg.appendChild(label); } // Draw x-axis const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line"); xAxis.setAttribute("x1", padding.left); xAxis.setAttribute("y1", height - padding.bottom); xAxis.setAttribute("x2", width - padding.right); xAxis.setAttribute("y2", height - padding.bottom); xAxis.setAttribute("class", "axis-line"); svg.appendChild(xAxis); // Add x-axis labels data.forEach((item, i) => { const x = padding.left + i * xScale; const label = document.createElementNS("http://www.w3.org/2000/svg", "text"); label.setAttribute("x", x); label.setAttribute("y", height - padding.bottom + 15); label.setAttribute("text-anchor", "middle"); label.setAttribute("font-size", "8"); label.setAttribute("fill", "#6C757D"); label.textContent = item.day; svg.appendChild(label); }); // Create line let pathD = ""; data.forEach((item, i) => { const x = padding.left + i * xScale; const y = height - padding.bottom - (item.value * yScale); if (i === 0) { pathD += `M ${x},${y}`; } else { pathD += ` L ${x},${y}`; } }); const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); path.setAttribute("d", pathD); path.setAttribute("fill", "none"); path.setAttribute("stroke", "#6930C3"); path.setAttribute("stroke-width", "2"); path.setAttribute("class", "line"); svg.appendChild(path); // Add data points data.forEach((item, i) => { const x = padding.left + i * xScale; const y = height - padding.bottom - (item.value * yScale); const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); circle.setAttribute("cx", x); circle.setAttribute("cy", y); circle.setAttribute("r", "4"); circle.setAttribute("fill", "#5E60CE"); circle.setAttribute("class", "data-point"); circle.setAttribute("data-value", item.value.toLocaleString()); circle.setAttribute("data-day", item.day); svg.appendChild(circle); // Add tooltip event circle.addEventListener('mouseover', function(e) { const tooltip = document.getElementById('lineTooltip'); tooltip.textContent = `${this.getAttribute('data-day')}: ${this.getAttribute('data-value')}`; tooltip.style.opacity = 1; tooltip.style.left = `${e.pageX - 50}px`; tooltip.style.top = `${e.pageY - 35}px`; }); circle.addEventListener('mouseout', function() { document.getElementById('lineTooltip').style.opacity = 0; }); }); // Add area fill under the line let areaD = pathD + ` L ${padding.left + (data.length - 1) * xScale},${height - padding.bottom} L ${padding.left},${height - padding.bottom} Z`; const area = document.createElementNS("http://www.w3.org/2000/svg", "path"); area.setAttribute("d", areaD); area.setAttribute("fill", "url(#gradient)"); area.setAttribute("opacity", "0.3"); // Define gradient const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); const gradient = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient"); gradient.setAttribute("id", "gradient"); gradient.setAttribute("x1", "0%"); gradient.setAttribute("y1", "0%"); gradient.setAttribute("x2", "0%"); gradient.setAttribute("y2", "100%"); const stop1 = document.createElementNS("http://www.w3.org/2000/svg", "stop"); stop1.setAttribute("offset", "0%"); stop1.setAttribute("stop-color", "#6930C3"); gradient.appendChild(stop1); const stop2 = document.createElementNS("http://www.w3.org/2000/svg", "stop"); stop2.setAttribute("offset", "100%"); stop2.setAttribute("stop-color", "#6930C3"); stop2.setAttribute("stop-opacity", "0.1"); gradient.appendChild(stop2); defs.appendChild(gradient); svg.appendChild(defs); svg.insertBefore(area, path); } // Bar Chart const barData = [ { day: 'Mon', value: 156 }, { day: 'Tue', value: 237 }, { day: 'Wed', value: 312 }, { day: 'Thu', value: 198 }, { day: 'Fri', value: 265 }, { day: 'Sat', value: 427 }, { day: 'Sun', value: 389 } ]; function createBarChart() { const container = document.getElementById('bar-chart'); const maxValue = Math.max(...barData.map(d => d.value)); barData.forEach(item => { const percentHeight = (item.value / maxValue) * 100; const barWrapper = document.createElement('div'); barWrapper.className = 'bar-wrapper'; const bar = document.createElement('div'); bar.className = 'bar'; bar.style.height = '0%'; // Start at 0 for animation // Vary the color based on value const hue = 240 + (120 * (item.value / maxValue)); bar.style.backgroundColor = `hsl(${hue}, 70%, 60%)`; const label = document.createElement('div'); label.className = 'bar-label'; label.textContent = item.day; bar.setAttribute('data-value', item.value); bar.setAttribute('data-percent', percentHeight.toFixed(1) + '%'); barWrapper.appendChild(bar); barWrapper.appendChild(label); container.appendChild(barWrapper); // Add hover effect barWrapper.addEventListener('mouseenter', function() { const value = this.querySelector('.bar').getAttribute('data-value'); const tooltip = document.createElement('div'); tooltip.className = 'data-tooltip'; tooltip.style.opacity = 1; tooltip.textContent = value + ' engagements'; tooltip.style.position = 'absolute'; tooltip.style.bottom = 'calc(100% + 5px)'; tooltip.style.left = '50%'; tooltip.style.transform = 'translateX(-50%)'; this.appendChild(tooltip); }); barWrapper.addEventListener('mouseleave', function() { const tooltip = this.querySelector('.data-tooltip'); if (tooltip) this.removeChild(tooltip); }); // Trigger animation after a delay setTimeout(() => { bar.style.height = percentHeight + '%'; }, 300); }); } // Sentiment Gauge function createSentimentGauge() { const sentimentValue = 72; // Percentage of positive sentiment const circle = document.querySelector('.progress-ring-circle'); const circumference = circle.getTotalLength(); // Adjust strokeDashoffset based on percentage const offset = circumference - (sentimentValue / 100) * circumference; // Set color based on sentiment value let color; if (sentimentValue >= 70) { color = '#28a745'; // Green for very positive } else if (sentimentValue >= 50) { color = '#FFBE0B'; // Yellow for neutral/positive } else if (sentimentValue >= 30) { color = '#fd7e14'; // Orange for somewhat negative } else { color = '#dc3545'; // Red for very negative } circle.style.stroke = color; circle.style.strokeDasharray = `${circumference} ${circumference}`; circle.style.strokeDashoffset = circumference; // Start at zero filled // Animate the fill setTimeout(() => { circle.style.strokeDashoffset = offset; }, 300); } // Initialize all charts function updateAllCharts() { createDonutChart(); updateLineChart('all'); createBarChart(); createSentimentGauge(); // Add animated interaction to insight card const insightCard = document.querySelector('.insight-card'); insightCard.addEventListener('click', function() { this.classList.toggle('pulse'); }); } updateAllCharts(); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #0f1520; color: #e1ecff; width: 100%; height: 100vh; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-start; align-items: center; } .container { width: 100%; max-width: 700px; height: 700px; padding: 20px; display: flex; flex-direction: column; position: relative; } header { margin-bottom: 20px; position: relative; z-index: 10; } h1 { font-size: 1.8rem; color: #00f0ff; margin-bottom: 5px; font-weight: 500; text-transform: uppercase; letter-spacing: 1px; position: relative; display: inline-block; } h1::after { content: ''; position: absolute; bottom: -5px; left: 0; width: 100%; height: 2px; background: linear-gradient(90deg, #00f0ff, transparent); } p.subtitle { font-size: 0.9rem; color: #7a8baa; margin-bottom: 15px; max-width: 600px; } .dashboard { flex: 1; display: grid; grid-template-rows: 1fr 1fr; gap: 20px; position: relative; z-index: 5; } .chart-container { background-color: rgba(16, 26, 46, 0.7); border-radius: 10px; padding: 15px; border: 1px solid rgba(0, 240, 255, 0.2); position: relative; box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); overflow: hidden; } .chart-container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.5), transparent); } .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .chart-title { font-size: 1rem; color: #ffffff; display: flex; align-items: center; gap: 8px; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; background-color: #0f0; position: relative; display: inline-block; margin-right: 5px; } .status-indicator::after { content: ''; position: absolute; top: -2px; left: -2px; right: -2px; bottom: -2px; border-radius: 50%; background-color: rgba(0, 255, 0, 0.3); animation: pulse 2s infinite; } @keyframes pulse { 0% { transform: scale(1); opacity: 0.7; } 70% { transform: scale(1.5); opacity: 0; } 100% { transform: scale(1); opacity: 0; } } .controls { display: flex; gap: 10px; } .control-btn { background: rgba(0, 240, 255, 0.1); border: 1px solid rgba(0, 240, 255, 0.3); color: #00f0ff; border-radius: 4px; padding: 4px 8px; font-size: 0.7rem; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; gap: 5px; } .control-btn:hover { background: rgba(0, 240, 255, 0.2); } .control-btn.active { background: rgba(0, 240, 255, 0.3); } canvas { width: 100%; height: calc(100% - 30px); } .grid-lines { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } .grid-line { position: absolute; background-color: rgba(121, 140, 181, 0.1); } .vertical { width: 1px; height: 100%; } .horizontal { height: 1px; width: 100%; } .data-point { position: absolute; width: 6px; height: 6px; border-radius: 50%; transform: translate(-50%, -50%); transition: all 0.3s ease; } .data-point:hover { transform: translate(-50%, -50%) scale(1.5); z-index: 5; } .tooltip { position: absolute; background-color: rgba(16, 26, 46, 0.95); border: 1px solid rgba(0, 240, 255, 0.5); padding: 8px 12px; border-radius: 4px; font-size: 0.8rem; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 100; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); max-width: 180px; } .tooltip::after { content: ''; position: absolute; top: 100%; left: 50%; margin-left: -5px; border-width: 5px; border-style: solid; border-color: rgba(0, 240, 255, 0.5) transparent transparent transparent; } .tooltip-title { color: #00f0ff; font-weight: bold; margin-bottom: 3px; font-size: 0.75rem; } .tooltip-data { display: flex; flex-direction: column; gap: 3px; } .tooltip-row { display: flex; justify-content: space-between; gap: 10px; } .tooltip-label { color: #7a8baa; } .tooltip-value { color: #ffffff; font-weight: 500; } .time-controls { display: flex; justify-content: space-between; margin-top: 15px; gap: 10px; } .time-btn { flex: 1; background: rgba(16, 26, 46, 0.7); border: 1px solid rgba(0, 240, 255, 0.2); color: #7a8baa; border-radius: 4px; padding: 8px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s ease; text-align: center; } .time-btn.active { background: rgba(0, 240, 255, 0.15); color: #00f0ff; border-color: rgba(0, 240, 255, 0.5); } .time-btn:hover:not(.active) { background: rgba(16, 26, 46, 0.9); color: #a7badb; } .bg-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(rgba(0, 240, 255, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 240, 255, 0.03) 1px, transparent 1px); background-size: 20px 20px; pointer-events: none; z-index: 1; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .chart-container { animation: fadeIn 0.6s ease-out forwards; } .chart-container:nth-child(2) { animation-delay: 0.2s; } .glowing-corner { position: absolute; width: 5px; height: 5px; background: #00f0ff; border-radius: 50%; filter: blur(3px); } .top-left { top: 0; left: 0; } .top-right { top: 0; right: 0; } .bottom-left { bottom: 0; left: 0; } .bottom-right { bottom: 0; right: 0; } .loading-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(15, 21, 32, 0.8); display: flex; justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(5px); transition: opacity 0.5s, visibility 0.5s; } .loader { width: 50px; height: 50px; border: 3px solid rgba(0, 240, 255, 0.3); border-radius: 50%; border-top-color: #00f0ff; animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .hidden { opacity: 0; visibility: hidden; } @media (max-width: 500px) { h1 { font-size: 1.4rem; } p.subtitle { font-size: 0.8rem; } .chart-title { font-size: 0.9rem; } .control-btn { padding: 3px 6px; font-size: 0.65rem; } .time-btn { font-size: 0.7rem; padding: 6px 4px; } } .data-stream { position: absolute; height: 2px; background: linear-gradient(90deg, transparent, #00f0ff); width: 50px; opacity: 0; } @keyframes streamIn { 0% { opacity: 0; width: 0; } 50% { opacity: 1; width: 30px; } 100% { opacity: 0; width: 0; } } </style> </head> <body> <div class="container"> <div class="bg-grid"></div> <header> <h1>Quantum Edge IoT Monitor</h1> <p class="subtitle">Real-time performance metrics from distributed edge devices. Visualizing temperature patterns and CPU utilization across network.</p> </header> <div class="dashboard"> <div class="chart-container" id="line-chart-container"> <div class="chart-header"> <div class="chart-title"> <span class="status-indicator"></span> Temperature Sensor Array (°C) </div> <div class="controls"> <button class="control-btn" id="zoom-in-line"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M15 3H9V9H3V15H9V21H15V15H21V9H15V3Z" stroke="#00f0ff" stroke-width="2"/> </svg> Zoom </button> <button class="control-btn" id="reset-line"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#00f0ff" stroke-width="2"/> <path d="M12 8V16M8 12H16" stroke="#00f0ff" stroke-width="2"/> </svg> Reset </button> </div> </div> <canvas id="line-chart"></canvas> <div class="glowing-corner top-left"></div> <div class="glowing-corner top-right"></div> <div class="glowing-corner bottom-left"></div> <div class="glowing-corner bottom-right"></div> </div> <div class="chart-container" id="scatter-chart-container"> <div class="chart-header"> <div class="chart-title"> <span class="status-indicator"></span> CPU Utilization vs. Network Latency </div> <div class="controls"> <button class="control-btn" id="zoom-in-scatter"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M15 3H9V9H3V15H9V21H15V15H21V9H15V3Z" stroke="#00f0ff" stroke-width="2"/> </svg> Zoom </button> <button class="control-btn" id="reset-scatter"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" stroke="#00f0ff" stroke-width="2"/> <path d="M12 8V16M8 12H16" stroke="#00f0ff" stroke-width="2"/> </svg> Reset </button> </div> </div> <canvas id="scatter-chart"></canvas> <div class="glowing-corner top-left"></div> <div class="glowing-corner top-right"></div> <div class="glowing-corner bottom-left"></div> <div class="glowing-corner bottom-right"></div> </div> </div> <div class="time-controls"> <button class="time-btn active" data-time="1h">1H</button> <button class="time-btn" data-time="6h">6H</button> <button class="time-btn" data-time="24h">24H</button> <button class="time-btn" data-time="7d">7D</button> <button class="time-btn" data-time="realtime">REAL-TIME</button> </div> <div id="tooltip" class="tooltip"></div> <div class="loading-overlay" id="loading-overlay"> <div class="loader"></div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { // Simulate loading setTimeout(() => { document.getElementById('loading-overlay').classList.add('hidden'); }, 1500); // Line Chart const lineCtx = document.getElementById('line-chart').getContext('2d'); // Generate data for the line chart - simulating temperature data from multiple sensors function generateTemperatureData() { const data = { labels: [], datasets: [ { label: 'Sensor A', data: [], borderColor: '#00f0ff', backgroundColor: 'rgba(0, 240, 255, 0.1)', borderWidth: 2, pointBackgroundColor: '#00f0ff', pointBorderColor: '#0f1520', pointRadius: 3, pointHoverRadius: 5, tension: 0.4, fill: true }, { label: 'Sensor B', data: [], borderColor: '#ff00c8', backgroundColor: 'rgba(255, 0, 200, 0.1)', borderWidth: 2, pointBackgroundColor: '#ff00c8', pointBorderColor: '#0f1520', pointRadius: 3, pointHoverRadius: 5, tension: 0.4, fill: true }, { label: 'Sensor C', data: [], borderColor: '#7fff00', backgroundColor: 'rgba(127, 255, 0, 0.1)', borderWidth: 2, pointBackgroundColor: '#7fff00', pointBorderColor: '#0f1520', pointRadius: 3, pointHoverRadius: 5, tension: 0.4, fill: true } ] }; // Generate timestamps for the past 24 hours const now = new Date(); for (let i = 24; i >= 0; i--) { const time = new Date(now.getTime() - i * 3600000); // Every hour const formattedTime = time.getHours().toString().padStart(2, '0') + ':00'; data.labels.push(formattedTime); // Generate temperature data with some realistic patterns const baseTemp = 23 + Math.sin(i/4) * 3; // Base temperature with sine wave pattern data.datasets[0].data.push((baseTemp + Math.random() * 2).toFixed(1)); data.datasets[1].data.push((baseTemp - 1 + Math.random() * 2.5).toFixed(1)); data.datasets[2].data.push((baseTemp + 2 + Math.random() * 1.8).toFixed(1)); } return data; } const temperatureData = generateTemperatureData(); // Create the line chart const lineChart = new Chart(lineCtx, { type: 'line', data: temperatureData, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1000, easing: 'easeOutQuart' }, scales: { x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#7a8baa', font: { size: 10 }, maxRotation: 0 } }, y: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#7a8baa', font: { size: 10 } }, title: { display: true, text: 'Temperature (°C)', color: '#7a8baa', font: { size: 12 } }, suggestedMin: 18, suggestedMax: 30 } }, plugins: { legend: { display: true, position: 'top', labels: { color: '#7a8baa', boxWidth: 12, font: { size: 10 } } }, tooltip: { enabled: true, backgroundColor: 'rgba(16, 26, 46, 0.9)', titleColor: '#00f0ff', bodyColor: '#ffffff', borderColor: 'rgba(0, 240, 255, 0.5)', borderWidth: 1, cornerRadius: 4, displayColors: true, boxWidth: 8, boxHeight: 8, usePointStyle: true, padding: 10 } }, interaction: { mode: 'index', intersect: false }, elements: { point: { hitRadius: 8 } } } }); // Scatter Chart const scatterCtx = document.getElementById('scatter-chart').getContext('2d'); // Generate data for the scatter chart - CPU utilization vs. Network Latency function generateScatterData() { const data = { datasets: [ { label: 'Edge Devices', data: [], backgroundColor: function(context) { const value = context.raw.y; if (value < 50) return 'rgba(0, 240, 255, 0.7)'; if (value < 75) return 'rgba(255, 255, 0, 0.7)'; return 'rgba(255, 0, 0, 0.7)'; }, borderColor: '#0f1520', borderWidth: 1, pointRadius: function(context) { const value = context.raw.z; return 5 + value/10; }, pointHoverRadius: function(context) { const value = context.raw.z; return 8 + value/10; } } ] }; // Generate random data points for 25 devices for (let i = 0; i < 25; i++) { const deviceGroup = i % 5; // Group devices into 5 types // CPU usage (x-axis) let cpuUsage; if (deviceGroup === 0) { cpuUsage = 20 + Math.random() * 15; // Low CPU usage group } else if (deviceGroup === 1) { cpuUsage = 35 + Math.random() * 20; // Medium-low CPU usage } else if (deviceGroup === 2) { cpuUsage = 50 + Math.random() * 15; // Medium CPU usage } else if (deviceGroup === 3) { cpuUsage = 65 + Math.random() * 15; // Medium-high CPU usage } else { cpuUsage = 80 + Math.random() * 15; // High CPU usage } // Network latency (y-axis) // More CPU usage often correlates with higher latency, but with variability const baseLatency = 20 + (cpuUsage / 20); const latency = baseLatency + Math.random() * 20 - 10; // Device memory usage (bubble size) const memoryUsage = 10 + Math.random() * 20; // Add data point with x, y, and z (size) values data.datasets[0].data.push({ x: cpuUsage.toFixed(1), y: latency.toFixed(1), z: memoryUsage.toFixed(1), deviceId: `IOT-${1000 + i}`, status: latency > 70 ? 'Critical' : latency > 50 ? 'Warning' : 'Normal', uptime: Math.floor(24 + Math.random() * 100).toString() + 'h' }); } return data; } const scatterData = generateScatterData(); // Create the scatter chart const scatterChart = new Chart(scatterCtx, { type: 'bubble', data: scatterData, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1000, easing: 'easeOutQuart' }, scales: { x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#7a8baa', font: { size: 10 } }, title: { display: true, text: 'CPU Utilization (%)', color: '#7a8baa', font: { size: 12 } }, min: 0, max: 100 }, y: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: '#7a8baa', font: { size: 10 } }, title: { display: true, text: 'Network Latency (ms)', color: '#7a8baa', font: { size: 12 } }, min: 0, max: 100 } }, plugins: { legend: { display: false }, tooltip: { enabled: true, backgroundColor: 'rgba(16, 26, 46, 0.9)', titleColor: '#00f0ff', bodyColor: '#ffffff', borderColor: 'rgba(0, 240, 255, 0.5)', borderWidth: 1, cornerRadius: 4, displayColors: false, padding: 10, callbacks: { title: function(items) { return items[0].raw.deviceId; }, label: function(item) { return [ `CPU: ${item.raw.x}%`, `Latency: ${item.raw.y}ms`, `Memory: ${item.raw.z}%`, `Status: ${item.raw.status}`, `Uptime: ${item.raw.uptime}` ]; } } } }, interaction: { mode: 'point', intersect: true } } }); // Zoom functionality for line chart document.getElementById('zoom-in-line').addEventListener('click', function() { if (this.classList.contains('active')) { // Reset zoom lineChart.options.scales.y.min = undefined; lineChart.options.scales.y.max = undefined; lineChart.update(); this.classList.remove('active'); } else { // Zoom into middle range const midPoint = (lineChart.scales.y.max + lineChart.scales.y.min) / 2; const range = lineChart.scales.y.max - lineChart.scales.y.min; lineChart.options.scales.y.min = midPoint - range / 4; lineChart.options.scales.y.max = midPoint + range / 4; lineChart.update(); this.classList.add('active'); } }); document.getElementById('reset-line').addEventListener('click', function() { lineChart.options.scales.y.min = undefined; lineChart.options.scales.y.max = undefined; lineChart.update(); document.getElementById('zoom-in-line').classList.remove('active'); }); // Zoom functionality for scatter chart document.getElementById('zoom-in-scatter').addEventListener('click', function() { if (this.classList.contains('active')) { // Reset zoom scatterChart.options.scales.x.min = 0; scatterChart.options.scales.x.max = 100; scatterChart.options.scales.y.min = 0; scatterChart.options.scales.y.max = 100; scatterChart.update(); this.classList.remove('active'); } else { // Zoom into cluster scatterChart.options.scales.x.min = 30; scatterChart.options.scales.x.max = 70; scatterChart.options.scales.y.min = 20; scatterChart.options.scales.y.max = 60; scatterChart.update(); this.classList.add('active'); } }); document.getElementById('reset-scatter').addEventListener('click', function() { scatterChart.options.scales.x.min = 0; scatterChart.options.scales.x.max = 100; scatterChart.options.scales.y.min = 0; scatterChart.options.scales.y.max = 100; scatterChart.update(); document.getElementById('zoom-in-scatter').classList.remove('active'); }); // Time period buttons const timeButtons = document.querySelectorAll('.time-btn'); timeButtons.forEach(btn => { btn.addEventListener('click', function() { // Remove active class from all buttons timeButtons.forEach(b => b.classList.remove('active')); // Add active class to clicked button this.classList.add('active'); const timeRange = this.dataset.time; // Update charts based on time range updateChartsForTimeRange(timeRange); // If real-time mode is selected, start the simulation if (timeRange === 'realtime') { startRealTimeSimulation(); } else { stopRealTimeSimulation(); } }); }); let realTimeInterval; function startRealTimeSimulation() { // Clear any existing interval if (realTimeInterval) { clearInterval(realTimeInterval); } // Start real-time updates every 2 seconds realTimeInterval = setInterval(() => { // Add new data point to the line chart const now = new Date(); const formattedTime = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0'); // Add new data to line chart if (lineChart.data.labels.length > 20) { lineChart.data.labels.shift(); lineChart.data.datasets.forEach(dataset => { dataset.data.shift(); }); } lineChart.data.labels.push(formattedTime); // Generate new temperature readings const baseTemp = 23 + Math.sin(now.getMinutes()/10) * 3; lineChart.data.datasets[0].data.push((baseTemp + Math.random() * 2).toFixed(1)); lineChart.data.datasets[1].data.push((baseTemp - 1 + Math.random() * 2.5).toFixed(1)); lineChart.data.datasets[2].data.push((baseTemp + 2 + Math.random() * 1.8).toFixed(1)); // Update the line chart lineChart.update(); // Create a data stream animation effect createDataStreamEffect(); // Update 2-3 random data points in scatter chart const numPoints = scatterChart.data.datasets[0].data.length; const pointsToUpdate = Math.floor(Math.random() * 3) + 2; // 2-4 points for (let i = 0; i < pointsToUpdate; i++) { const pointIndex = Math.floor(Math.random() * numPoints); const point = scatterChart.data.datasets[0].data[pointIndex]; // Slightly modify the point values const newCpu = Math.max(0, Math.min(100, parseFloat(point.x) + (Math.random() * 10 - 5))); const newLatency = Math.max(0, Math.min(100, parseFloat(point.y) + (Math.random() * 8 - 4))); const newMemory = Math.max(0, Math.min(30, parseFloat(point.z) + (Math.random() * 4 - 2))); // Update the point scatterChart.data.datasets[0].data[pointIndex] = { x: newCpu.toFixed(1), y: newLatency.toFixed(1), z: newMemory.toFixed(1), deviceId: point.deviceId, status: newLatency > 70 ? 'Critical' : newLatency > 50 ? 'Warning' : 'Normal', uptime: point.uptime }; } // Update the scatter chart scatterChart.update(); }, 2000); } function stopRealTimeSimulation() { if (realTimeInterval) { clearInterval(realTimeInterval); realTimeInterval = null; } } function updateChartsForTimeRange(timeRange) { // This would typically fetch different data ranges from a server // For this demo, we'll simulate different data patterns // Update line chart based on time range const labels = []; const newData1 = []; const newData2 = []; const newData3 = []; const now = new Date(); let pointCount; let timeFormat; let timeStep; switch(timeRange) { case '1h': pointCount = 12; timeFormat = (date) => `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; timeStep = 5 * 60 * 1000; // 5 minutes break; case '6h': pointCount = 24; timeFormat = (date) => `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; timeStep = 15 * 60 * 1000; // 15 minutes break; case '24h': pointCount = 24; timeFormat = (date) => `${date.getHours().toString().padStart(2, '0')}:00`; timeStep = 60 * 60 * 1000; // 1 hour break; case '7d': pointCount = 14; timeFormat = (date) => `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1).toString().padStart(2, '0')}`; timeStep = 12 * 60 * 60 * 1000; // 12 hours break; default: return; // For real-time, we don't update immediately } for (let i = pointCount - 1; i >= 0; i--) { const time = new Date(now.getTime() - i * timeStep); labels.
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>EarthPulse: Environmental Data Visualization</title> <style> :root { --earth-brown: #8B4513; --moss-green: #6B8E23; --sand: #F4A460; --forest-green: #228B22; --ocean-blue: #1E90FF; --clay: #B87333; --leaf: #32CD32; --stone: #708090; --soil: #5E2605; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background: linear-gradient(to bottom, #f8f8f8, #e8e8e0); width: 100%; height: 100vh; overflow: hidden; display: flex; justify-content: center; align-items: center; } .container { width: 700px; height: 700px; background-color: #f9f7f4; border-radius: 15px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); padding: 20px; display: flex; flex-direction: column; position: relative; overflow: hidden; } .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } h1 { font-size: 24px; color: var(--earth-brown); font-weight: 500; } .controls { display: flex; gap: 15px; } .time-selector { display: flex; background-color: #ece5dd; border-radius: 20px; overflow: hidden; } .time-btn { padding: 8px 15px; background: none; border: none; cursor: pointer; font-size: 14px; color: var(--stone); transition: all 0.3s ease; } .time-btn.active { background-color: var(--moss-green); color: white; } .metrics-selector { position: relative; z-index: 10; } .metric-dropdown { appearance: none; background-color: #ece5dd; border: none; border-radius: 20px; padding: 8px 30px 8px 15px; font-size: 14px; color: var(--earth-brown); cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%238B4513' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 10px center; } .visualization { flex-grow: 1; position: relative; padding-top: 10px; } .graph-container { height: 400px; position: relative; overflow: hidden; } .axis { position: absolute; background-color: rgba(139, 69, 19, 0.2); } .x-axis { bottom: 0; left: 40px; right: 20px; height: 1px; } .y-axis { top: 0; bottom: 30px; left: 40px; width: 1px; } .x-labels, .y-labels { position: absolute; display: flex; justify-content: space-between; color: var(--stone); font-size: 12px; } .x-labels { bottom: -25px; left: 40px; right: 20px; } .y-labels { flex-direction: column-reverse; justify-content: space-between; top: 0; bottom: 30px; left: 0; width: 35px; text-align: right; } .grid-lines { position: absolute; top: 0; left: 40px; right: 20px; bottom: 30px; z-index: 0; } .grid-line { position: absolute; left: 0; right: 0; height: 1px; background-color: rgba(112, 128, 144, 0.1); } .data-line { fill: none; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; } .data-area { opacity: 0.2; } .temp-line { stroke: var(--clay); } .temp-area { fill: var(--clay); } .co2-line { stroke: var(--forest-green); } .co2-area { fill: var(--forest-green); } .sea-line { stroke: var(--ocean-blue); } .sea-area { fill: var(--ocean-blue); } .data-point { r: 5; transition: r 0.3s ease; cursor: pointer; } .data-point:hover { r: 8; } .tooltip { position: absolute; padding: 10px 15px; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); pointer-events: none; opacity: 0; transition: opacity 0.3s ease; z-index: 100; font-size: 13px; color: var(--soil); max-width: 200px; } .legend { display: flex; justify-content: center; gap: 20px; margin-top: 30px; } .legend-item { display: flex; align-items: center; gap: 5px; font-size: 13px; color: var(--stone); } .legend-color { width: 15px; height: 15px; border-radius: 50%; } .temp-color { background-color: var(--clay); } .co2-color { background-color: var(--forest-green); } .sea-color { background-color: var(--ocean-blue); } .info-panel { margin-top: 20px; padding: 15px; background-color: rgba(244, 164, 96, 0.1); border-radius: 10px; border-left: 4px solid var(--sand); } .info-title { font-size: 16px; color: var(--earth-brown); margin-bottom: 8px; } .info-text { font-size: 14px; line-height: 1.5; color: var(--stone); } .highlight { position: relative; display: inline-block; color: var(--earth-brown); font-weight: 500; } .highlight::after { content: ''; position: absolute; bottom: 0; left: 0; width: 100%; height: 5px; background-color: rgba(139, 69, 19, 0.2); border-radius: 2px; z-index: -1; } .leaf-decoration { position: absolute; width: 30px; height: 30px; opacity: 0.1; pointer-events: none; } @media (max-width: 700px) { .container { width: 100%; height: 100%; border-radius: 0; padding: 15px; } h1 { font-size: 20px; } .header { flex-direction: column; align-items: flex-start; gap: 15px; } .graph-container { height: 300px; } .legend { flex-wrap: wrap; } } .organic-shape { position: absolute; border-radius: 50%; filter: blur(40px); opacity: 0.1; z-index: -1; } .shape-1 { width: 300px; height: 300px; background-color: var(--moss-green); top: -100px; right: -100px; } .shape-2 { width: 200px; height: 200px; background-color: var(--clay); bottom: -50px; left: -50px; } .metric-toggle-container { display: flex; gap: 10px; margin-top: 15px; } .metric-toggle { display: flex; align-items: center; gap: 5px; cursor: pointer; } .toggle-checkbox { appearance: none; width: 18px; height: 18px; border: 2px solid var(--stone); border-radius: 4px; position: relative; cursor: pointer; } .toggle-checkbox:checked { background-color: var(--moss-green); border-color: var(--moss-green); } .toggle-checkbox:checked::after { content: '✓'; position: absolute; color: white; font-size: 12px; top: 0px; left: 3px; } .toggle-label { font-size: 14px; color: var(--stone); } </style> </head> <body> <div class="container"> <div class="organic-shape shape-1"></div> <div class="organic-shape shape-2"></div> <div class="header"> <h1>EarthPulse: Climate Metrics Visualization</h1> <div class="controls"> <div class="time-selector"> <button class="time-btn" data-time="5">5Y</button> <button class="time-btn" data-time="10">10Y</button> <button class="time-btn active" data-time="20">20Y</button> </div> <div class="metrics-selector"> <select class="metric-dropdown" id="region-select"> <option value="global">Global Average</option> <option value="north-america">North America</option> <option value="europe">Europe</option> <option value="asia">Asia</option> <option value="oceania">Oceania</option> </select> </div> </div> </div> <div class="metric-toggle-container"> <div class="metric-toggle"> <input type="checkbox" class="toggle-checkbox" id="temp-toggle" checked> <label for="temp-toggle" class="toggle-label">Temperature</label> </div> <div class="metric-toggle"> <input type="checkbox" class="toggle-checkbox" id="co2-toggle" checked> <label for="co2-toggle" class="toggle-label">CO₂ Levels</label> </div> <div class="metric-toggle"> <input type="checkbox" class="toggle-checkbox" id="sea-toggle" checked> <label for="sea-toggle" class="toggle-label">Sea Level Rise</label> </div> </div> <div class="visualization"> <div class="graph-container"> <div class="axis x-axis"></div> <div class="axis y-axis"></div> <div class="grid-lines" id="grid-lines"></div> <div class="x-labels" id="x-labels"></div> <div class="y-labels" id="y-labels"></div> <svg id="data-viz" width="100%" height="100%" viewBox="0 0 640 400" preserveAspectRatio="none"> <!-- Data will be inserted here via JS --> </svg> <div class="tooltip" id="tooltip"></div> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color temp-color"></div> <span>Temperature (°C)</span> </div> <div class="legend-item"> <div class="legend-color co2-color"></div> <span>CO₂ (ppm)</span> </div> <div class="legend-item"> <div class="legend-color sea-color"></div> <span>Sea Level (mm)</span> </div> </div> </div> <div class="info-panel"> <h3 class="info-title">Climate Impact Insights</h3> <p class="info-text">The data reveals a <span class="highlight">0.32°C temperature increase</span> per decade, with accelerating sea level rise reaching <span class="highlight">4.5mm annually</span>. These changes correlate with ecosystem shifts observed in 78% of monitored habitats worldwide.</p> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Generate sample data const years = 20; const currentYear = new Date().getFullYear(); // Temperature data (increasing trend with seasonal variations) const tempData = Array.from({length: years}, (_, i) => { const baseTemp = 14.5 + (i * 0.032); const seasonalVariation = Math.sin(i * Math.PI / 2) * 0.2; const randomVariation = (Math.random() - 0.5) * 0.1; return { year: currentYear - years + i + 1, value: baseTemp + seasonalVariation + randomVariation, unit: '°C' }; }); // CO2 data (steadily increasing) const co2Data = Array.from({length: years}, (_, i) => { const baseCO2 = 380 + (i * 2.1); const randomVariation = (Math.random() - 0.5) * 0.8; return { year: currentYear - years + i + 1, value: baseCO2 + randomVariation, unit: 'ppm' }; }); // Sea level data (accelerating increase) const seaData = Array.from({length: years}, (_, i) => { const baseSeaLevel = 20 + (i * 3.5) + (i * i * 0.08); const randomVariation = (Math.random() - 0.5) * 1.2; return { year: currentYear - years + i + 1, value: baseSeaLevel + randomVariation, unit: 'mm' }; }); // Current visible data sets let datasets = { temp: { data: tempData, visible: true, color: '#B87333', min: Math.min(...tempData.map(d => d.value)) - 0.2, max: Math.max(...tempData.map(d => d.value)) + 0.2, name: 'Temperature', className: 'temp' }, co2: { data: co2Data, visible: true, color: '#228B22', min: Math.min(...co2Data.map(d => d.value)) - 5, max: Math.max(...co2Data.map(d => d.value)) + 5, name: 'CO₂ Levels', className: 'co2' }, sea: { data: seaData, visible: true, color: '#1E90FF', min: Math.min(...seaData.map(d => d.value)) - 2, max: Math.max(...seaData.map(d => d.value)) + 2, name: 'Sea Level Rise', className: 'sea' } }; // Setup the visualization const svg = document.getElementById('data-viz'); const tooltip = document.getElementById('tooltip'); const graphContainer = document.querySelector('.graph-container'); const gridLines = document.getElementById('grid-lines'); const xLabels = document.getElementById('x-labels'); const yLabels = document.getElementById('y-labels'); // Set up time buttons const timeButtons = document.querySelectorAll('.time-btn'); timeButtons.forEach(btn => { btn.addEventListener('click', function() { timeButtons.forEach(b => b.classList.remove('active')); this.classList.add('active'); const timeRange = parseInt(this.dataset.time); updateTimeRange(timeRange); }); }); // Set up metric toggles document.getElementById('temp-toggle').addEventListener('change', function() { datasets.temp.visible = this.checked; updateVisualization(); }); document.getElementById('co2-toggle').addEventListener('change', function() { datasets.co2.visible = this.checked; updateVisualization(); }); document.getElementById('sea-toggle').addEventListener('change', function() { datasets.sea.visible = this.checked; updateVisualization(); }); // Update time range function function updateTimeRange(years) { const endYear = currentYear; const startYear = endYear - years; // Filter data to show only the selected range Object.keys(datasets).forEach(key => { const fullData = key === 'temp' ? tempData : key === 'co2' ? co2Data : seaData; datasets[key].data = fullData.filter(d => d.year > startYear); // Update min and max for the visible range datasets[key].min = Math.min(...datasets[key].data.map(d => d.value)) - (key === 'temp' ? 0.2 : key === 'co2' ? 5 : 2); datasets[key].max = Math.max(...datasets[key].data.map(d => d.value)) + (key === 'temp' ? 0.2 : key === 'co2' ? 5 : 2); }); updateVisualization(); } // Create visualization elements function updateVisualization() { // Clear previous elements svg.innerHTML = ''; gridLines.innerHTML = ''; xLabels.innerHTML = ''; yLabels.innerHTML = ''; // Calculate visible datasets const visibleDatasets = Object.values(datasets).filter(ds => ds.visible); if (visibleDatasets.length === 0) return; // Set up scales const width = svg.viewBox.baseVal.width; const height = svg.viewBox.baseVal.height; const padding = { top: 10, right: 20, bottom: 30, left: 40 }; const innerWidth = width - padding.left - padding.right; const innerHeight = height - padding.top - padding.bottom; // Draw grid lines (5 horizontal lines) const gridLineCount = 5; for (let i = 0; i <= gridLineCount; i++) { const y = padding.top + (innerHeight * (1 - i / gridLineCount)); const gridLine = document.createElement('div'); gridLine.className = 'grid-line'; gridLine.style.top = `${y}px`; gridLines.appendChild(gridLine); // Add y-axis labels const label = document.createElement('div'); label.textContent = Math.round(i / gridLineCount * 100) + '%'; yLabels.appendChild(label); } // Add x-axis labels (years) const yearStep = visibleDatasets[0].data.length <= 5 ? 1 : Math.ceil(visibleDatasets[0].data.length / 5); const visibleYears = visibleDatasets[0].data.map(d => d.year); for (let i = 0; i < visibleYears.length; i += yearStep) { if (i === 0 || i === visibleYears.length - 1 || i % yearStep === 0) { const label = document.createElement('div'); label.textContent = visibleYears[i]; xLabels.appendChild(label); } } // Draw data for each visible dataset visibleDatasets.forEach(dataset => { const { data, color, min, max, className } = dataset; // Create path elements const linePath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); linePath.classList.add('data-line', `${className}-line`); const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path'); areaPath.classList.add('data-area', `${className}-area`); // Calculate points const points = data.map((d, i) => { const x = padding.left + (i / (data.length - 1)) * innerWidth; const y = padding.top + (1 - (d.value - min) / (max - min)) * innerHeight; return { x, y, data: d }; }); // Generate line path let lineDef = `M ${points[0].x},${points[0].y}`; for (let i = 1; i < points.length; i++) { // Use cubic bezier curves for smoother lines const prevPoint = points[i-1]; const currPoint = points[i]; const cpX1 = prevPoint.x + (currPoint.x - prevPoint.x) / 3; const cpX2 = prevPoint.x + 2 * (currPoint.x - prevPoint.x) / 3; lineDef += ` C ${cpX1},${prevPoint.y} ${cpX2},${currPoint.y} ${currPoint.x},${currPoint.y}`; } linePath.setAttribute('d', lineDef); // Generate area path (extends line to bottom) let areaDef = lineDef; areaDef += ` L ${points[points.length-1].x},${padding.top + innerHeight}`; areaDef += ` L ${points[0].x},${padding.top + innerHeight} Z`; areaPath.setAttribute('d', areaDef); // Add paths to SVG svg.appendChild(areaPath); svg.appendChild(linePath); // Add data points with interactive tooltip points.forEach(point => { const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.classList.add('data-point'); circle.setAttribute('cx', point.x); circle.setAttribute('cy', point.y); circle.setAttribute('fill', color); // Add interactivity circle.addEventListener('mouseover', () => { circle.style.filter = 'drop-shadow(0 0 3px rgba(0,0,0,0.3))'; // Show tooltip tooltip.style.opacity = '1'; tooltip.style.left = `${point.x + 10}px`; tooltip.style.top = `${point.y - 40}px`; tooltip.innerHTML = ` <strong>${dataset.name} (${point.data.year})</strong><br> ${point.data.value.toFixed(2)}${point.data.unit} `; }); circle.addEventListener('mouseout', () => { circle.style.filter = 'none'; tooltip.style.opacity = '0'; }); svg.appendChild(circle); }); }); // Add organic leaf decorations addLeafDecorations(); } function addLeafDecorations() { // Remove existing decorations document.querySelectorAll('.leaf-decoration').forEach(leaf => leaf.remove()); // Add new random leaf decorations const leafCount = 5; const container = document.querySelector('.container'); for (let i = 0; i < leafCount; i++) { const leaf = document.createElement('div'); leaf.className = 'leaf-decoration'; leaf.style.top = `${Math.random() * 100}%`; leaf.style.left = `${Math.random() * 100}%`; leaf.style.transform = `rotate(${Math.random() * 360}deg) scale(${0.8 + Math.random() * 0.7})`; leaf.style.backgroundImage = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23228B22' d='M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z'/%3E%3C/svg%3E\")"; container.appendChild(leaf); } } // Initialize visualization updateVisualization(); // Add window resize listener for responsiveness window.addEventListener('resize', updateVisualization); // Animate the appearance of the app setTimeout(() => { document.querySelectorAll('.data-line, .data-area').forEach(el => { el.style.transition = 'opacity 1s ease-in-out, transform 1s ease-out'; el.style.opacity = '0'; el.style.transform = 'translateY(20px)'; setTimeout(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }, 100); }); }, 300); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Supply Chain Visualization</title> <style> :root { --primary-color: #2c3e50; --secondary-color: #34495e; --accent-color: #3498db; --highlight-color: #f39c12; --light-gray: #ecf0f1; --medium-gray: #bdc3c7; --dark-gray: #7f8c8d; --white: #ffffff; --black: #121212; --success: #27ae60; --warning: #e67e22; --danger: #e74c3c; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--light-gray); color: var(--primary-color); overflow: hidden; width: 100%; height: 100vh; } .container { width: 100%; height: 100%; max-width: 700px; max-height: 700px; padding: 15px; margin: 0 auto; display: flex; flex-direction: column; } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .title-container h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 5px; } .title-container p { font-size: 0.85rem; color: var(--dark-gray); } .filters { display: flex; gap: 10px; } .filter-btn { background-color: var(--white); border: 1px solid var(--medium-gray); border-radius: 4px; padding: 5px 10px; font-size: 0.8rem; cursor: pointer; transition: all 0.2s ease; } .filter-btn:hover { border-color: var(--accent-color); } .filter-btn.active { background-color: var(--accent-color); color: var(--white); border-color: var(--accent-color); } .tabs { display: flex; margin-bottom: 15px; border-bottom: 1px solid var(--medium-gray); } .tab { padding: 8px 15px; cursor: pointer; position: relative; font-weight: 500; color: var(--dark-gray); transition: color 0.3s ease; } .tab.active { color: var(--accent-color); } .tab.active::after { content: ''; position: absolute; bottom: -1px; left: 0; width: 100%; height: 2px; background-color: var(--accent-color); } .tab-content { display: none; flex: 1; position: relative; } .tab-content.active { display: block; } .network-chart { width: 100%; height: calc(100% - 50px); background-color: var(--white); border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); overflow: hidden; position: relative; } .bar-chart { width: 100%; height: calc(100% - 50px); background-color: var(--white); border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); padding: 20px; position: relative; } .bar-container { display: flex; flex-direction: column; height: 100%; justify-content: flex-end; gap: 15px; } .bar-group { display: flex; align-items: flex-end; height: 20px; width: 100%; position: relative; } .bar-label { width: 100px; text-align: right; padding-right: 10px; font-size: 0.8rem; color: var(--dark-gray); } .bar-wrapper { flex: 1; height: 100%; display: flex; align-items: center; } .bar { height: 15px; background-color: var(--accent-color); border-radius: 3px; transition: width 0.8s cubic-bezier(0.19, 1, 0.22, 1); cursor: pointer; position: relative; } .bar:hover { opacity: 0.9; } .bar-value { position: absolute; right: -35px; font-size: 0.75rem; color: var(--primary-color); } .bar[data-status="ontime"] { background-color: var(--success); } .bar[data-status="delayed"] { background-color: var(--warning); } .bar[data-status="critical"] { background-color: var(--danger); } .x-axis { display: flex; justify-content: space-between; margin-top: 10px; padding-left: 100px; font-size: 0.7rem; color: var(--dark-gray); } .tooltip { position: absolute; background-color: var(--secondary-color); color: var(--white); padding: 8px 12px; border-radius: 4px; font-size: 0.8rem; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); pointer-events: none; opacity: 0; transition: opacity 0.2s ease; z-index: 100; max-width: 200px; } .tooltip-title { font-weight: 600; margin-bottom: 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.2); padding-bottom: 4px; } .tooltip-content { font-size: 0.75rem; } .legend { display: flex; justify-content: center; gap: 15px; margin-top: 10px; } .legend-item { display: flex; align-items: center; font-size: 0.75rem; color: var(--dark-gray); } .legend-color { width: 12px; height: 12px; border-radius: 3px; margin-right: 5px; } .legend-color.ontime { background-color: var(--success); } .legend-color.delayed { background-color: var(--warning); } .legend-color.critical { background-color: var(--danger); } .kpi-tiles { display: flex; gap: 10px; margin-bottom: 15px; } .kpi-tile { flex: 1; background-color: var(--white); border-radius: 6px; padding: 10px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); transition: transform 0.2s ease; } .kpi-tile:hover { transform: translateY(-2px); } .kpi-title { font-size: 0.7rem; color: var(--dark-gray); margin-bottom: 5px; } .kpi-value { font-size: 1.2rem; font-weight: 600; } .kpi-change { font-size: 0.7rem; display: flex; align-items: center; } .kpi-change.positive { color: var(--success); } .kpi-change.negative { color: var(--danger); } svg { width: 100%; height: 100%; } .node { cursor: pointer; transition: all 0.3s ease; } .node:hover { filter: brightness(1.2); } .node.selected { stroke: var(--highlight-color); stroke-width: 2px; } .node-label { font-size: 10px; pointer-events: none; text-anchor: middle; dy: 0.35em; } .link { stroke-opacity: 0.6; stroke-width: 1.5; } .link.highlighted { stroke-opacity: 0.9; stroke-width: 2.5; } .mini-details { position: absolute; bottom: 15px; right: 15px; background-color: rgba(255, 255, 255, 0.95); border-radius: 5px; padding: 10px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); width: 200px; opacity: 0; transform: translateY(10px); transition: all 0.3s ease; z-index: 10; } .mini-details.visible { opacity: 1; transform: translateY(0); } .mini-title { font-weight: 600; font-size: 0.85rem; margin-bottom: 5px; color: var(--primary-color); } .mini-stats { font-size: 0.75rem; color: var(--dark-gray); } .mini-status { margin-top: 5px; padding: 3px 6px; display: inline-block; border-radius: 3px; font-size: 0.7rem; font-weight: 500; } .mini-status.ontime { background-color: rgba(39, 174, 96, 0.2); color: var(--success); } .mini-status.delayed { background-color: rgba(243, 156, 18, 0.2); color: var(--warning); } .mini-status.critical { background-color: rgba(231, 76, 60, 0.2); color: var(--danger); } .loader { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255, 255, 255, 0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; } .spinner { width: 40px; height: 40px; border: 3px solid rgba(52, 152, 219, 0.2); border-radius: 50%; border-top-color: var(--accent-color); animation: spin 1s ease-in-out infinite; } @keyframes spin { to { transform: rotate(360deg); } } .time-marker { position: absolute; top: 15px; right: 15px; background-color: var(--white); border-radius: 4px; padding: 5px 10px; font-size: 0.7rem; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); color: var(--dark-gray); z-index: 5; } .toggle-view { position: absolute; top: 15px; left: 15px; z-index: 5; background-color: var(--white); border: 1px solid var(--medium-gray); border-radius: 4px; padding: 5px 10px; font-size: 0.7rem; cursor: pointer; transition: all 0.2s ease; } .toggle-view:hover { background-color: var(--light-gray); } .zoom-controls { position: absolute; right: 15px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 5px; z-index: 5; } .zoom-btn { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background-color: var(--white); border: 1px solid var(--medium-gray); border-radius: 4px; font-size: 14px; cursor: pointer; transition: all 0.2s ease; } .zoom-btn:hover { background-color: var(--light-gray); } @media (max-width: 600px) { .kpi-tiles { flex-wrap: wrap; } .kpi-tile { flex: 1 0 calc(50% - 5px); } .title-container h1 { font-size: 1.2rem; } .bar-label { width: 70px; font-size: 0.7rem; } .x-axis { padding-left: 70px; } .legend { flex-wrap: wrap; } } </style> </head> <body> <div class="container"> <header> <div class="title-container"> <h1>Global Supply Chain Dashboard</h1> <p>Interactive visualization of logistical flows and performance metrics</p> </div> <div class="filters"> <button class="filter-btn active">All Regions</button> <button class="filter-btn">APAC</button> <button class="filter-btn">EMEA</button> <button class="filter-btn">Americas</button> </div> </header> <div class="kpi-tiles"> <div class="kpi-tile"> <div class="kpi-title">On-Time Delivery</div> <div class="kpi-value">87.3%</div> <div class="kpi-change positive">+2.1% ↑</div> </div> <div class="kpi-tile"> <div class="kpi-title">Avg Lead Time</div> <div class="kpi-value">4.2 days</div> <div class="kpi-change positive">-0.5d ↑</div> </div> <div class="kpi-tile"> <div class="kpi-title">Inventory Turnover</div> <div class="kpi-value">12.8x</div> <div class="kpi-change negative">-0.4x ↓</div> </div> <div class="kpi-tile"> <div class="kpi-title">Active Suppliers</div> <div class="kpi-value">346</div> <div class="kpi-change positive">+12 ↑</div> </div> </div> <div class="tabs"> <div class="tab active" data-tab="network">Network View</div> <div class="tab" data-tab="distribution">Distribution Centers</div> <div class="tab" data-tab="suppliers">Supplier Performance</div> </div> <div class="tab-content active" data-tab="network"> <div class="network-chart"> <div class="time-marker">Data as of Aug 12, 2023</div> <button class="toggle-view">Toggle View</button> <div class="zoom-controls"> <button class="zoom-btn" id="zoom-in">+</button> <button class="zoom-btn" id="zoom-out">-</button> <button class="zoom-btn" id="zoom-reset">⟲</button> </div> <svg id="network-svg"></svg> <div class="mini-details"> <div class="mini-title">Shanghai Distribution Hub</div> <div class="mini-stats"> <div>Throughput: 4,320 units/day</div> <div>Connections: 8 suppliers, 12 retailers</div> <div>Utilization: 87%</div> </div> <div class="mini-status delayed">Minor Delays</div> </div> <div class="loader"> <div class="spinner"></div> </div> </div> </div> <div class="tab-content" data-tab="distribution"> <div class="bar-chart"> <div class="bar-container" id="distribution-bars"> <!-- Will be filled by JS --> </div> <div class="x-axis"> <div>0</div> <div>20%</div> <div>40%</div> <div>60%</div> <div>80%</div> <div>100%</div> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color ontime"></div> <span>On Time</span> </div> <div class="legend-item"> <div class="legend-color delayed"></div> <span>Delayed</span> </div> <div class="legend-item"> <div class="legend-color critical"></div> <span>Critical</span> </div> </div> </div> </div> <div class="tab-content" data-tab="suppliers"> <div class="bar-chart"> <div class="bar-container" id="supplier-bars"> <!-- Will be filled by JS --> </div> <div class="x-axis"> <div>0</div> <div>20%</div> <div>40%</div> <div>60%</div> <div>80%</div> <div>100%</div> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color ontime"></div> <span>Good</span> </div> <div class="legend-item"> <div class="legend-color delayed"></div> <span>Warning</span> </div> <div class="legend-item"> <div class="legend-color critical"></div> <span>At Risk</span> </div> </div> </div> </div> <div class="tooltip"> <div class="tooltip-title">Distribution Center</div> <div class="tooltip-content"> Capacity: 5,000 units<br> Status: Operating at 78% capacity<br> Risk Level: Low </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Tabs functionality const tabs = document.querySelectorAll('.tab'); const tabContents = document.querySelectorAll('.tab-content'); tabs.forEach(tab => { tab.addEventListener('click', function() { const targetTab = this.getAttribute('data-tab'); tabs.forEach(t => t.classList.remove('active')); tabContents.forEach(content => content.classList.remove('active')); this.classList.add('active'); document.querySelector(`.tab-content[data-tab="${targetTab}"]`).classList.add('active'); }); }); // Filter buttons const filterBtns = document.querySelectorAll('.filter-btn'); filterBtns.forEach(btn => { btn.addEventListener('click', function() { filterBtns.forEach(b => b.classList.remove('active')); this.classList.add('active'); updateCharts(this.textContent); }); }); // Tooltip functionality const tooltip = document.querySelector('.tooltip'); function showTooltip(event, title, content) { tooltip.querySelector('.tooltip-title').textContent = title; tooltip.querySelector('.tooltip-content').innerHTML = content; tooltip.style.left = `${event.pageX + 10}px`; tooltip.style.top = `${event.pageY + 10}px`; tooltip.style.opacity = 1; // Make sure tooltip stays in viewport const tooltipRect = tooltip.getBoundingClientRect(); const container = document.querySelector('.container'); const containerRect = container.getBoundingClientRect(); if (tooltipRect.right > containerRect.right) { tooltip.style.left = `${event.pageX - tooltipRect.width - 10}px`; } if (tooltipRect.bottom > containerRect.bottom) { tooltip.style.top = `${event.pageY - tooltipRect.height - 10}px`; } } function hideTooltip() { tooltip.style.opacity = 0; } // Distribution Center data const distributionData = [ { name: "New York DC", value: 92, status: "ontime", details: { throughput: "3,850 units/day", utilization: "92%", backlog: "None" } }, { name: "Shanghai Hub", value: 78, status: "delayed", details: { throughput: "4,320 units/day", utilization: "87%", backlog: "Minor (2 days)" } }, { name: "Rotterdam Port", value: 85, status: "ontime", details: { throughput: "5,100 units/day", utilization: "85%", backlog: "None" } }, { name: "Mumbai Center", value: 62, status: "delayed", details: { throughput: "2,740 units/day", utilization: "62%", backlog: "Significant (5 days)" } }, { name: "São Paulo DC", value: 45, status: "critical", details: { throughput: "1,890 units/day", utilization: "45%", backlog: "Critical (10+ days)" } }, { name: "Sydney Hub", value: 88, status: "ontime", details: { throughput: "2,530 units/day", utilization: "88%", backlog: "None" } }, { name: "Singapore Port", value: 81, status: "ontime", details: { throughput: "3,900 units/day", utilization: "81%", backlog: "Minor (1 day)" } } ]; // Supplier data const supplierData = [ { name: "Tech Components Inc.", value: 93, status: "ontime", details: { leadTime: "2.1 days", defectRate: "0.3%", onTime: "97%" } }, { name: "Global Materials Ltd.", value: 68, status: "delayed", details: { leadTime: "4.7 days", defectRate: "2.1%", onTime: "82%" } }, { name: "Eastern Electronics", value: 89, status: "ontime", details: { leadTime: "3.2 days", defectRate: "0.8%", onTime: "94%" } }, { name: "Pacific Plastics", value: 72, status: "delayed", details: { leadTime: "5.5 days", defectRate: "1.7%", onTime: "79%" } }, { name: "American Metals Corp", value: 35, status: "critical", details: { leadTime: "8.3 days", defectRate: "4.2%", onTime: "65%" } }, { name: "European Fabrics", value: 95, status: "ontime", details: { leadTime: "2.8 days", defectRate: "0.5%", onTime: "98%" } }, { name: "Nordic Chemicals", value: 51, status: "critical", details: { leadTime: "7.1 days", defectRate: "3.6%", onTime: "72%" } } ]; // Create bar charts function createBarChart(containerId, data) { const container = document.getElementById(containerId); container.innerHTML = ''; data.forEach(item => { const barGroup = document.createElement('div'); barGroup.className = 'bar-group'; const label = document.createElement('div'); label.className = 'bar-label'; label.textContent = item.name; const barWrapper = document.createElement('div'); barWrapper.className = 'bar-wrapper'; const bar = document.createElement('div'); bar.className = 'bar'; bar.style.width = `${item.value}%`; bar.setAttribute('data-status', item.status); const value = document.createElement('div'); value.className = 'bar-value'; value.textContent = `${item.value}%`; bar.addEventListener('mousemove', (e) => { let tooltipTitle = item.name; let tooltipContent = ''; for (const [key, val] of Object.entries(item.details)) { tooltipContent += `${key.charAt(0).toUpperCase() + key.slice(1)}: ${val}<br>`; } showTooltip(e, tooltipTitle, tooltipContent); }); bar.addEventListener('mouseleave', hideTooltip); barWrapper.appendChild(bar); bar.appendChild(value); barGroup.appendChild(label); barGroup.appendChild(barWrapper); container.appendChild(barGroup); }); } // Create networks chart function createNetworkChart() { // Using D3.js would be ideal here, but we'll simulate with a simple SVG const svg = document.getElementById('network-svg'); // Sample network data with nodes and links const nodesData = [ { id: 'dc1', name: 'New York DC', x: 150, y: 150, r: 25, type: 'dc', status: 'ontime' }, { id: 'dc2', name: 'Shanghai Hub', x: 350, y: 200, r: 25, type: 'dc', status: 'delayed' }, { id: 'dc3', name: 'Rotterdam Port', x: 250, y: 350, r: 25, type: 'dc', status: 'ontime' }, { id: 's1', name: 'Tech Components Inc.', x: 100, y: 250, r: 15, type: 'supplier', status: 'ontime' }, { id: 's2', name: 'Global Materials Ltd.', x: 450, y: 150, r: 15, type: 'supplier', status: 'delayed' }, { id: 's3', name: 'Eastern Electronics', x: 450, y: 300, r: 15, type: 'supplier', status: 'ontime' }, { id: 'r1', name: 'Chicago Retail', x: 200, y: 50, r: 12, type: 'retail', status: 'ontime' }, { id: 'r2', name: 'Los Angeles Stores', x: 50, y: 350, r: 12, type: 'retail', status: 'critical' }, { id: 'r3', name: 'Beijing Markets', x: 300, y: 450, r: 12, type: 'retail', status: 'delayed' } ]; const linksData = [ { source: 's1', target: 'dc1', volume: 250 }, { source: 's2', target: 'dc2', volume: 180 }, { source: 's3', target: 'dc2', volume: 120 }, { source: 's3', target: 'dc3', volume: 90 }, { source: 'dc1', target: 'r1', volume: 150 }, { source: 'dc1', target: 'r2', volume: 100 }, { source: 'dc2', target: 'r3', volume: 220 }, { source: 'dc3', target: 'r2', volume: 80 }, { source: 'dc3', target: 'r3', volume: 110 } ]; // Clear SVG svg.innerHTML = ''; // Create links first (so they appear behind nodes) linksData.forEach(link => { const source = nodesData.find(n => n.id === link.source); const target = nodesData.find(n => n.id === link.target); const lineWidth = Math.max(1, Math.min(5, link.volume / 50)); const svgLink = document.createElementNS('http://www.w3.org/2000/svg', 'line'); svgLink.setAttribute('x1', source.x); svgLink.setAttribute('y1', source.y); svgLink.setAttribute('x2', target.x); svgLink.setAttribute('y2', target.y); svgLink.setAttribute('stroke', '#7f8c8d'); svgLink.setAttribute('stroke-width', lineWidth); svgLink.setAttribute('class', 'link'); svgLink.setAttribute('data-source', source.id); svgLink.setAttribute('data-target', target.id); svg.appendChild(svgLink); }); // Create nodes nodesData.forEach(node => { const svgNode = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); svgNode.setAttribute('cx', node.x); svgNode.setAttribute('cy', node.y); svgNode.setAttribute('r', node.r); svgNode.setAttribute('class', 'node'); svgNode.setAttribute('data-id', node.id); // Set colors based on node types and status if (node.type === 'dc') { svgNode.setAttribute('fill', '#3498db'); } else if (node.type === 'supplier') { svgNode.setAttribute('fill', '#9b59b6'); } else { svgNode.setAttribute('fill', '#1abc9c'); } // Add status indicator if (node.status === 'delayed') { svgNode.setAttribute('stroke', '#f39c12'); svgNode.setAttribute('stroke-width', '2'); } else if (node.status === 'critical') { svgNode.setAttribute('stroke', '#e74c3c'); svgNode.setAttribute('stroke-width', '3'); } // Add event listeners svgNode.addEventListener('mousemove', (e) => { let tooltipTitle = node.name; let tooltipContent = `Type: ${node.type.charAt(0).toUpperCase() + node.type.slice(1)}<br>`; if (node.type === 'dc') { tooltipContent += 'Capacity: 5,000 units<br>'; tooltipContent += `Status: ${node.status === 'ontime' ? 'Normal' : node.status === 'delayed' ? 'Delayed' : 'Critical'}<br>`; tooltipContent += `Connected suppliers: ${linksData.filter(l => l.target === node.id).length}<br>`; tooltipContent += `Serves retailers: ${linksData.filter(l => l.source === node.id).length}`; } else if (node.type === 'supplier') { tooltipContent += 'Lead Time: 3.2 days<br>'; tooltipContent += 'Quality Rating: 92%<br>'; tooltipContent += 'Current Order Volume: 240 units'; } else { tooltipContent += 'Demand: 180 units/day<br>'; tooltipContent += 'Stock Level: 87%<br>'; tooltipContent += 'Forecast Accuracy: 92%'; } showTooltip(e, tooltipTitle, tooltipContent); // Highlight connected links document.querySelectorAll('.link').forEach(link => { if (link.getAttribute('data-source') === node.id || link.getAttribute('data-target') === node.id) { link.classList.add('highlighted'); } }); }); svgNode.addEventListener('mouseleave', () => { hideTooltip(); document.querySelectorAll('.link').forEach(link => { link.classList.remove('highlighted'); }); }); svgNode.addEventListener('click', () => { // Show mini details panel const miniDetails = document.querySelector('.mini-details'); const miniTitle = miniDetails.querySelector('.mini-title'); const miniStats = miniDetails.querySelector('.mini-stats'); const miniStatus = miniDetails.querySelector('.mini-status'); miniTitle.textContent = node.name; if (node.type === 'dc') { miniStats.innerHTML = ` <div>Throughput: ${Math.floor(Math.random() * 3000 + 2000)} units/day</div> <div>Connections: ${linksData.filter(l => l.target === node.id).length} suppliers, ${linksData.filter(l => l.source === node.id).length} retailers</div> <div>Utilization: ${Math.floor(Math.random() * 30 + 70)}%</div> `; miniStatus.className = `mini-status ${node.status}`; miniStatus.textContent = node.status === 'ontime' ? 'Normal Operations' : node.status === 'delayed' ? 'Minor Delays' : 'Critical Issues'; } else if (node.type === 'supplier') { miniStats.innerHTML = ` <div>Lead Time: ${(Math.random() * 4 + 1).toFixed(1)} days</div> <div>Reliability: ${Math.floor(Math.random() * 20 + 80)}%</div> <div>Current Orders: ${Math.floor(Math.random() * 200 + 100)} units</div> `; miniStatus.className = `mini-status ${node.status}`; miniStatus.textContent = node.status === 'ontime' ? 'Good Standing' : node.status === 'delayed' ? 'Performance Warning' : 'At Risk'; } else { miniStats.innerHTML = ` <div>Daily Demand: ${Math.floor(Math.random() * 200 + 100)} units</div> <div>Stock Level: ${Math.floor(Math.random() * 20 + 80)}%</div> <div>Forecast Accuracy: ${Math.floor(Math.random() * 10 + 85)}%</div> `; miniStatus.className = `mini-status ${node.status}`; miniStatus.textContent = node.status === 'ontime' ? 'Fully Stocked' : node.status === 'delayed' ? 'Low Stock Warning' : 'Stockout Risk'; } miniDetails.classList.add('visible'); // Select the node document.querySelectorAll('.node').forEach(n => n.classList.remove('selected')); svgNode.classList.add('selected'); });
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Sports Performance Metrics</title> <style> @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@500;700&display=swap'); :root { --primary: #ff1e50; --secondary: #3c00ff; --accent: #00f0b5; --dark: #060624; --light: #f2f2fd; --grid: rgba(255, 255, 255, 0.05); } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Rajdhani', sans-serif; background-color: var(--dark); color: var(--light); position: relative; width: 100%; height: 100%; overflow: hidden; } .container { width: 100%; height: 700px; padding: 20px; overflow-y: auto; position: relative; scroll-behavior: smooth; background: linear-gradient(135deg, rgba(16, 16, 40, 0.9), rgba(6, 6, 36, 0.95)); } .container::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0 / 20px 20px, linear-gradient(0deg, var(--grid) 1px, transparent 1px) 0 0 / 20px 20px; pointer-events: none; z-index: -1; } .header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 15px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); margin-bottom: 20px; } .title { font-family: 'Orbitron', sans-serif; font-weight: 900; font-size: 24px; text-transform: uppercase; background: linear-gradient(90deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: 0 0 15px rgba(255, 30, 80, 0.5); } .subtitle { font-size: 14px; color: var(--accent); margin-top: 5px; font-weight: 500; } .time-filter { display: flex; gap: 8px; } .time-filter button { font-family: 'Rajdhani', sans-serif; background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); color: var(--light); padding: 4px 12px; border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s ease; } .time-filter button:hover, .time-filter button.active { background: var(--secondary); border-color: var(--secondary); box-shadow: 0 0 15px rgba(60, 0, 255, 0.5); } .tabs { display: flex; gap: 5px; margin-bottom: 20px; } .tab { font-family: 'Orbitron', sans-serif; padding: 8px 15px; background: rgba(255, 255, 255, 0.05); border: none; color: var(--light); cursor: pointer; font-size: 14px; border-radius: 5px 5px 0 0; transition: all 0.3s ease; } .tab:hover, .tab.active { background: var(--primary); color: white; box-shadow: 0 0 15px rgba(255, 30, 80, 0.5); } .dashboard { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; } @media (max-width: 700px) { .dashboard { grid-template-columns: 1fr; } } .card { background: rgba(16, 16, 40, 0.8); border-radius: 10px; padding: 15px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); border: 1px solid rgba(255, 255, 255, 0.05); position: relative; overflow: hidden; height: 100%; display: flex; flex-direction: column; } .card::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 3px; background: linear-gradient(90deg, var(--primary), var(--secondary)); } .card-title { font-family: 'Orbitron', sans-serif; font-size: 16px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; } .card-title span { color: var(--accent); font-size: 14px; } .chart-container { position: relative; height: 280px; margin-top: auto; } .radar-container { position: relative; height: 280px; } .stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 15px; } .stat-card { background: rgba(255, 255, 255, 0.03); border-radius: 8px; padding: 10px; text-align: center; position: relative; overflow: hidden; transition: all 0.3s ease; } .stat-card:hover { background: rgba(255, 255, 255, 0.08); transform: translateY(-2px); } .stat-name { font-size: 14px; color: rgba(255, 255, 255, 0.7); } .stat-value { font-size: 24px; font-weight: 700; margin: 5px 0; } .stat-trend { font-size: 13px; color: var(--accent); display: flex; align-items: center; justify-content: center; gap: 3px; } .stat-trend.up { color: var(--accent); } .stat-trend.down { color: var(--primary); } .player-selector { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .player { padding: 8px 12px; background: rgba(255, 255, 255, 0.03); border-radius: 20px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; border: 1px solid transparent; } .player:hover, .player.active { background: rgba(255, 30, 80, 0.2); border-color: var(--primary); } .player.active { background: var(--primary); color: white; } .callout { position: absolute; background: var(--secondary); color: white; padding: 5px 10px; border-radius: 5px; font-size: 12px; z-index: 10; pointer-events: none; opacity: 0; transform: translateY(10px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; } .callout.show { opacity: 1; transform: translateY(0); } .callout::after { content: ''; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); border-width: 5px 5px 0; border-style: solid; border-color: var(--secondary) transparent transparent; } .highlight-point { position: absolute; width: 12px; height: 12px; background: var(--accent); border-radius: 50%; z-index: 5; cursor: pointer; box-shadow: 0 0 0 3px rgba(0, 240, 181, 0.3); animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 240, 181, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(0, 240, 181, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 240, 181, 0); } } .tooltip { position: absolute; background: rgba(6, 6, 36, 0.9); border: 1px solid var(--accent); color: white; padding: 10px; border-radius: 5px; font-size: 14px; z-index: 100; pointer-events: none; opacity: 0; transition: all 0.2s ease; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); max-width: 200px; } .tooltip-title { font-weight: 700; margin-bottom: 5px; color: var(--accent); } .key-metrics { display: flex; gap: 15px; flex-wrap: wrap; margin-top: 15px; } .key-metric { display: flex; flex-direction: column; align-items: center; } .metric-value { font-size: 20px; font-weight: 700; } .metric-label { font-size: 12px; color: rgba(255, 255, 255, 0.7); } /* Custom scrollbar */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); } ::-webkit-scrollbar-thumb { background: var(--secondary); border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: var(--primary); } /* Retro glitch effect for highlights */ .glitch-highlight { position: relative; color: var(--accent); font-weight: bold; } .glitch-highlight::before, .glitch-highlight::after { content: attr(data-text); position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0.8; } .glitch-highlight::before { color: var(--primary); z-index: -1; animation: glitch-anim-1 0.4s infinite; } .glitch-highlight::after { color: var(--secondary); z-index: -2; animation: glitch-anim-2 0.3s infinite; } @keyframes glitch-anim-1 { 0%, 100% { transform: translate(0); } 20% { transform: translate(-2px, 2px); } 40% { transform: translate(-2px, -2px); } 60% { transform: translate(2px, 2px); } } @keyframes glitch-anim-2 { 0%, 100% { transform: translate(0); } 20% { transform: translate(2px, -2px); } 40% { transform: translate(2px, 2px); } 60% { transform: translate(-2px, -2px); } } </style> </head> <body> <div class="container"> <div class="header"> <div> <h1 class="title">ATHLETE METRICS NEXUS</h1> <p class="subtitle">NBA PLAYOFFS 2023 - PERFORMANCE ANALYTICS</p> </div> <div class="time-filter"> <button class="active">Game</button> <button>Week</button> <button>Season</button> </div> </div> <div class="tabs"> <button class="tab active">Team Overview</button> <button class="tab">Player Analysis</button> <button class="tab">Game Breakdowns</button> </div> <div class="player-selector"> <div class="player active">J. Tatum</div> <div class="player">J. Brown</div> <div class="player">K. Durant</div> <div class="player">N. Jokić</div> <div class="player">L. Dončić</div> </div> <div class="dashboard"> <div class="card"> <div class="card-title"> Performance Trend <span>Last 10 Games</span> </div> <div class="chart-container"> <canvas id="lineChart"></canvas> <div id="callout1" class="callout">Career high 51 PTS vs PHI</div> <div id="callout2" class="callout">Triple-double 30/10/11</div> <div class="highlight-point" style="left: 70%; top: 35%;" data-callout="callout1"></div> <div class="highlight-point" style="left: 85%; top: 25%;" data-callout="callout2"></div> </div> <div class="key-metrics"> <div class="key-metric"> <div class="metric-value">27.6</div> <div class="metric-label">PPG</div> </div> <div class="key-metric"> <div class="metric-value">8.2</div> <div class="metric-label">RPG</div> </div> <div class="key-metric"> <div class="metric-value">4.8</div> <div class="metric-label">APG</div> </div> <div class="key-metric"> <div class="metric-value">37.2%</div> <div class="metric-label">3PT%</div> </div> </div> </div> <div class="card"> <div class="card-title"> Skill Radar <span>vs. League Avg</span> </div> <div class="radar-container"> <canvas id="radarChart"></canvas> </div> <div class="stats-grid"> <div class="stat-card"> <div class="stat-name">True Shooting %</div> <div class="stat-value">58.7%</div> <div class="stat-trend up">↑ 3.2%</div> </div> <div class="stat-card"> <div class="stat-name">Usage Rate</div> <div class="stat-value">32.5%</div> <div class="stat-trend up">↑ 1.8%</div> </div> </div> </div> </div> <div id="tooltip" class="tooltip"> <div class="tooltip-title">Game vs Brooklyn Nets</div> <div>30 PTS, 8 REB, 4 AST</div> <div>FG: 11-19 (57.8%)</div> <div>3PT: 5-9 (55.6%)</div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { // Line Chart const lineCtx = document.getElementById('lineChart').getContext('2d'); const gradientFill = lineCtx.createLinearGradient(0, 0, 0, 400); gradientFill.addColorStop(0, 'rgba(255, 30, 80, 0.5)'); gradientFill.addColorStop(1, 'rgba(255, 30, 80, 0)'); const lineData = { labels: ['G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8', 'G9', 'G10'], datasets: [{ label: 'Points', data: [24, 28, 19, 32, 25, 30, 51, 22, 30, 38], borderColor: '#ff1e50', backgroundColor: gradientFill, tension: 0.4, borderWidth: 3, fill: true, pointBackgroundColor: '#ff1e50', pointBorderColor: '#fff', pointBorderWidth: 2, pointRadius: 5, pointHoverRadius: 8 }] }; const lineChart = new Chart(lineCtx, { type: 'line', data: lineData, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.7)' } }, x: { grid: { color: 'rgba(255, 255, 255, 0.05)' }, ticks: { color: 'rgba(255, 255, 255, 0.7)' } } }, plugins: { legend: { display: false }, tooltip: { enabled: false, external: function(context) { let tooltip = document.getElementById('tooltip'); if (context.tooltip.opacity === 0) { tooltip.style.opacity = 0; return; } const position = context.chart.canvas.getBoundingClientRect(); tooltip.style.opacity = 1; tooltip.style.left = position.left + context.tooltip.caretX + 'px'; tooltip.style.top = position.top + context.tooltip.caretY + 'px'; // Customize tooltip content based on game number const gameIndex = context.tooltip.dataPoints[0].dataIndex; const games = [ { opponent: "vs Miami Heat", stats: "24 PTS, 6 REB, 3 AST", fg: "9-20 (45.0%)", pt3: "2-7 (28.6%)" }, { opponent: "vs Philadelphia 76ers", stats: "28 PTS, 10 REB, 5 AST", fg: "10-18 (55.6%)", pt3: "4-9 (44.4%)" }, { opponent: "vs Milwaukee Bucks", stats: "19 PTS, 5 REB, 3 AST", fg: "7-22 (31.8%)", pt3: "1-8 (12.5%)" }, { opponent: "vs Chicago Bulls", stats: "32 PTS, 8 REB, 7 AST", fg: "12-21 (57.1%)", pt3: "5-10 (50.0%)" }, { opponent: "vs Cleveland Cavaliers", stats: "25 PTS, 9 REB, 4 AST", fg: "9-19 (47.4%)", pt3: "3-8 (37.5%)" }, { opponent: "vs Toronto Raptors", stats: "30 PTS, 7 REB, 5 AST", fg: "11-20 (55.0%)", pt3: "4-9 (44.4%)" }, { opponent: "vs Philadelphia 76ers", stats: "51 PTS, 13 REB, 5 AST", fg: "18-28 (64.3%)", pt3: "9-15 (60.0%)" }, { opponent: "vs Atlanta Hawks", stats: "22 PTS, 7 REB, 9 AST", fg: "8-19 (42.1%)", pt3: "2-8 (25.0%)" }, { opponent: "vs Brooklyn Nets", stats: "30 PTS, 8 REB, 4 AST", fg: "11-19 (57.8%)", pt3: "5-9 (55.6%)" }, { opponent: "vs New York Knicks", stats: "38 PTS, 10 REB, 11 AST", fg: "14-22 (63.6%)", pt3: "6-11 (54.5%)" } ]; const game = games[gameIndex]; tooltip.innerHTML = ` <div class="tooltip-title">Game ${gameIndex+1} ${game.opponent}</div> <div>${game.stats}</div> <div>FG: ${game.fg}</div> <div>3PT: ${game.pt3}</div> `; } } }, hover: { mode: 'index', intersect: false }, animation: { duration: 1000, easing: 'easeOutQuart' } } }); // Radar Chart const radarCtx = document.getElementById('radarChart').getContext('2d'); const radarData = { labels: ['Scoring', 'Playmaking', 'Defense', 'Rebounding', 'Efficiency', 'Clutch'], datasets: [{ label: 'Player', data: [85, 75, 70, 68, 78, 90], borderColor: '#ff1e50', backgroundColor: 'rgba(255, 30, 80, 0.2)', borderWidth: 2, pointBackgroundColor: '#ff1e50', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: '#ff1e50', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6 }, { label: 'League Avg', data: [70, 68, 68, 65, 72, 75], borderColor: '#3c00ff', backgroundColor: 'rgba(60, 0, 255, 0.2)', borderWidth: 2, pointBackgroundColor: '#3c00ff', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: '#3c00ff', pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6 }] }; const radarChart = new Chart(radarCtx, { type: 'radar', data: radarData, options: { responsive: true, maintainAspectRatio: false, scales: { r: { angleLines: { color: 'rgba(255, 255, 255, 0.1)' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, pointLabels: { color: 'rgba(255, 255, 255, 0.7)' }, ticks: { backdropColor: 'transparent', color: 'rgba(255, 255, 255, 0.5)' } } }, plugins: { legend: { position: 'bottom', labels: { color: 'rgba(255, 255, 255, 0.7)', boxWidth: 12, font: { family: 'Rajdhani' } } } }, animation: { duration: 1500, easing: 'easeOutQuart' } } }); // Highlight callouts animation const highlightPoints = document.querySelectorAll('.highlight-point'); highlightPoints.forEach(point => { point.addEventListener('mouseenter', function() { const calloutId = this.getAttribute('data-callout'); const callout = document.getElementById(calloutId); // Position the callout const pointRect = this.getBoundingClientRect(); const containerRect = document.querySelector('.chart-container').getBoundingClientRect(); callout.style.left = (this.offsetLeft - callout.offsetWidth/2 + 6) + 'px'; callout.style.top = (this.offsetTop - callout.offsetHeight - 10) + 'px'; callout.classList.add('show'); }); point.addEventListener('mouseleave', function() { const calloutId = this.getAttribute('data-callout'); document.getElementById(calloutId).classList.remove('show'); }); }); // Tab switching const tabs = document.querySelectorAll('.tab'); tabs.forEach(tab => { tab.addEventListener('click', function() { tabs.forEach(t => t.classList.remove('active')); this.classList.add('active'); }); }); // Time filter switching const timeFilters = document.querySelectorAll('.time-filter button'); timeFilters.forEach(button => { button.addEventListener('click', function() { timeFilters.forEach(b => b.classList.remove('active')); this.classList.add('active'); }); }); // Player selector const players = document.querySelectorAll('.player'); players.forEach(player => { player.addEventListener('click', function() { players.forEach(p => p.classList.remove('active')); this.classList.add('active'); // Simulate data change for different players const playerName = this.textContent; // Update line chart with "new" data const newLineData = generateRandomData(10, 15, 45); lineChart.data.datasets[0].data = newLineData; lineChart.update(); // Update radar chart with "new" data const newRadarData = generateRandomData(6, 60, 95); radarChart.data.datasets[0].data = newRadarData; radarChart.update(); // Update stats document.querySelector('.key-metrics .metric-value:nth-child(1)').textContent = (Math.random() * 15 + 20).toFixed(1); document.querySelector('.key-metrics .metric-value:nth-child(3)').textContent = (Math.random() * 5 + 5).toFixed(1); document.querySelector('.key-metrics .metric-value:nth-child(5)').textContent = (Math.random() * 5 + 3).toFixed(1); document.querySelector('.key-metrics .metric-value:nth-child(7)').textContent = (Math.random() * 10 + 30).toFixed(1) + '%'; // Reposition callouts for new data const callout1 = document.getElementById('callout1'); const callout2 = document.getElementById('callout2'); const point1 = document.querySelector('.highlight-point:nth-child(3)'); const point2 = document.querySelector('.highlight-point:nth-child(4)'); point1.style.left = (Math.random() * 50 + 20) + '%'; point1.style.top = (Math.random() * 50 + 20) + '%'; point2.style.left = (Math.random() * 30 + 50) + '%'; point2.style.top = (Math.random() * 50 + 20) + '%'; }); }); // Helper function for random data generation function generateRandomData(count, min, max) { return Array.from({length: count}, () => Math.floor(Math.random() * (max - min + 1)) + min); } }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> :root { --primary-green: #2ecc71; --dark-green: #27ae60; --light-green: #a8e6cf; --background: #f9fafb; --text-dark: #2c3e50; --text-light: #7f8c8d; --shadow: 0 4px 12px rgba(0, 0, 0, 0.08); --card-shadow: 0 8px 24px rgba(0, 0, 0, 0.06); --transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; } body { background-color: var(--background); color: var(--text-dark); max-width: 700px; height: 700px; overflow-x: hidden; padding: 24px; } .dashboard { display: grid; grid-template-rows: auto 1fr auto; height: 100%; gap: 20px; } .header { display: flex; justify-content: space-between; align-items: center; } .title { font-size: 1.5rem; font-weight: 600; color: var(--text-dark); } .subtitle { font-size: 0.95rem; color: var(--text-light); margin-top: 4px; max-width: 480px; } .time-selectors { display: flex; gap: 8px; margin-top: 16px; } .time-btn { padding: 8px 16px; background-color: white; border: 1px solid #e0e0e0; border-radius: 20px; font-size: 0.85rem; cursor: pointer; transition: var(--transition); } .time-btn:hover { background-color: #f5f5f5; } .time-btn.active { background-color: var(--primary-green); color: white; border-color: var(--primary-green); } .chart-container { background-color: white; border-radius: 16px; box-shadow: var(--card-shadow); padding: 24px; position: relative; min-height: 300px; transition: var(--transition); } .chart-container:hover { box-shadow: 0 12px 32px rgba(0, 0, 0, 0.09); transform: translateY(-2px); } #energy-chart { width: 100%; height: 100%; } .chart-tooltip { position: absolute; background-color: rgba(255, 255, 255, 0.95); box-shadow: var(--shadow); padding: 12px; border-radius: 8px; font-size: 0.85rem; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 10; max-width: 180px; } .tooltip-label { font-weight: 600; color: var(--text-dark); margin-bottom: 4px; } .tooltip-value { color: var(--primary-green); font-weight: 700; font-size: 1.25rem; margin-bottom: 4px; } .tooltip-date { color: var(--text-light); font-size: 0.8rem; } .key-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-top: 20px; } .metric-card { background-color: white; border-radius: 12px; padding: 16px; box-shadow: var(--card-shadow); transition: var(--transition); cursor: pointer; position: relative; overflow: hidden; } .metric-card:hover { transform: translateY(-3px); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.1); } .metric-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; background-color: var(--primary-green); opacity: 0.7; } .metric-title { font-size: 0.85rem; color: var(--text-light); margin-bottom: 8px; } .metric-value { font-size: 1.5rem; font-weight: 600; color: var(--text-dark); margin-bottom: 4px; } .metric-trend { font-size: 0.75rem; display: flex; align-items: center; gap: 4px; } .trend-up { color: #e74c3c; } .trend-down { color: var(--primary-green); } .legend { display: flex; gap: 16px; margin-top: 12px; justify-content: center; flex-wrap: wrap; } .legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; color: var(--text-light); } .legend-color { width: 12px; height: 12px; border-radius: 3px; } .insights { background-color: rgba(46, 204, 113, 0.08); border-radius: 12px; padding: 12px 16px; margin-top: 12px; border-left: 3px solid var(--primary-green); } .insights-title { font-size: 0.9rem; font-weight: 600; margin-bottom: 4px; color: var(--dark-green); } .insights-text { font-size: 0.85rem; color: var(--text-dark); line-height: 1.4; } .insights-icon { margin-right: 6px; color: var(--primary-green); } .eco-tip { background-color: #f0f9f4; border-radius: 8px; padding: 12px; font-size: 0.85rem; color: var(--text-dark); margin-top: 16px; position: relative; padding-left: 36px; } .eco-tip::before { content: '💡'; position: absolute; left: 12px; top: 12px; font-size: 1rem; } .sparkline { height: 30px; margin-top: 8px; } @media (max-width: 600px) { body { padding: 16px; } .header { flex-direction: column; align-items: flex-start; } .key-metrics { grid-template-columns: 1fr 1fr; } .time-selectors { margin-top: 12px; flex-wrap: wrap; } } .chart-help { position: absolute; top: 20px; right: 20px; font-size: 0.85rem; color: var(--text-light); opacity: 0; transition: opacity 0.3s; pointer-events: none; } .chart-loading { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.8); display: flex; justify-content: center; align-items: center; border-radius: 16px; z-index: 5; opacity: 0; transition: opacity 0.3s; pointer-events: none; } .loader { width: 40px; height: 40px; border: 3px solid #f3f3f3; border-top: 3px solid var(--primary-green); border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .pulse-animation { animation: pulse 2s infinite; } @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(46, 204, 113, 0); } 100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); } } </style> <title>Energy Usage Analytics</title> </head> <body> <div class="dashboard"> <div class="header"> <div> <h1 class="title">Energy Consumption Dashboard</h1> <p class="subtitle">Monitor your building's energy usage patterns and discover optimization opportunities to reduce carbon footprint.</p> <div class="time-selectors"> <button class="time-btn" data-period="day">Day</button> <button class="time-btn active" data-period="week">Week</button> <button class="time-btn" data-period="month">Month</button> <button class="time-btn" data-period="year">Year</button> </div> </div> </div> <div class="chart-container"> <canvas id="energy-chart"></canvas> <div class="chart-tooltip"> <div class="tooltip-label">Energy Usage</div> <div class="tooltip-value">0 kWh</div> <div class="tooltip-date">Date</div> </div> <div class="chart-help">Hover over data points for details</div> <div class="chart-loading"> <div class="loader"></div> </div> </div> <div> <div class="key-metrics"> <div class="metric-card"> <div class="metric-title">Daily Average</div> <div class="metric-value">37.2 kWh</div> <div class="metric-trend trend-down"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M7 10L12 15L17 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> </svg> 8.3% vs last week </div> <canvas class="sparkline" id="sparkline1"></canvas> </div> <div class="metric-card"> <div class="metric-title">Peak Time</div> <div class="metric-value">2-4 PM</div> <div class="metric-trend"> Office cooling hours </div> <canvas class="sparkline" id="sparkline2"></canvas> </div> <div class="metric-card pulse-animation"> <div class="metric-title">Potential Savings</div> <div class="metric-value">12.6%</div> <div class="metric-trend"> Through optimization </div> <canvas class="sparkline" id="sparkline3"></canvas> </div> </div> <div class="legend"> <div class="legend-item"> <div class="legend-color" style="background-color: rgba(46, 204, 113, 0.7)"></div> <span>Actual Usage</span> </div> <div class="legend-item"> <div class="legend-color" style="background-color: rgba(46, 204, 113, 0.2)"></div> <span>Baseline</span> </div> <div class="legend-item"> <div class="legend-color" style="background-color: rgba(231, 76, 60, 0.5)"></div> <span>Peak Hours</span> </div> </div> <div class="insights"> <h3 class="insights-title"> <svg class="insights-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="currentColor" stroke-width="2"/> <path d="M12 8V12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <circle cx="12" cy="16" r="1" fill="currentColor"/> </svg> Smart Insights </h3> <p class="insights-text"> Your HVAC system shows 23% higher consumption on Tuesdays and Wednesdays. Adjusting temperature settings by 2°C during 2-4 PM could save approximately 315 kWh per month. </p> </div> <div class="eco-tip"> Consider scheduling automated system shutdowns for unused zones during weekends to reduce phantom energy usage. </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> document.addEventListener('DOMContentLoaded', function() { // Chart configuration const ctx = document.getElementById('energy-chart').getContext('2d'); // Data setup for different time periods const chartData = { day: { labels: ['12 AM', '2 AM', '4 AM', '6 AM', '8 AM', '10 AM', '12 PM', '2 PM', '4 PM', '6 PM', '8 PM', '10 PM'], datasets: [ { label: 'Energy Usage (kWh)', data: [12, 8, 7, 9, 15, 22, 30, 42, 38, 32, 25, 18], borderColor: 'rgba(46, 204, 113, 0.7)', backgroundColor: 'rgba(46, 204, 113, 0.1)', fill: true, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.7)', pointBorderColor: '#fff', pointHoverRadius: 6, pointHoverBackgroundColor: 'rgba(46, 204, 113, 1)', pointHoverBorderColor: 'white', pointHoverBorderWidth: 2 }, { label: 'Baseline (kWh)', data: [11, 10, 9, 11, 14, 20, 26, 32, 30, 28, 23, 16], borderColor: 'rgba(46, 204, 113, 0.2)', backgroundColor: 'rgba(46, 204, 113, 0.05)', borderDash: [5, 5], fill: false, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.2)', pointBorderColor: '#fff', pointRadius: 0, pointHoverRadius: 3, } ] }, week: { labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], datasets: [ { label: 'Energy Usage (kWh)', data: [285, 378, 352, 310, 275, 180, 142], borderColor: 'rgba(46, 204, 113, 0.7)', backgroundColor: 'rgba(46, 204, 113, 0.1)', fill: true, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.7)', pointBorderColor: '#fff', pointHoverRadius: 6, pointHoverBackgroundColor: 'rgba(46, 204, 113, 1)', pointHoverBorderColor: 'white', pointHoverBorderWidth: 2 }, { label: 'Baseline (kWh)', data: [280, 320, 310, 295, 270, 190, 150], borderColor: 'rgba(46, 204, 113, 0.2)', backgroundColor: 'rgba(46, 204, 113, 0.05)', borderDash: [5, 5], fill: false, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.2)', pointBorderColor: '#fff', pointRadius: 0, pointHoverRadius: 3, }, { label: 'Peak Hours', data: [60, 92, 85, 70, 55, 0, 0], borderColor: 'rgba(231, 76, 60, 0.5)', backgroundColor: 'rgba(231, 76, 60, 0.1)', borderDash: [3, 3], fill: true, tension: 0.4, pointRadius: 0, pointHoverRadius: 0, } ] }, month: { labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'], datasets: [ { label: 'Energy Usage (kWh)', data: [1850, 1780, 1920, 1670], borderColor: 'rgba(46, 204, 113, 0.7)', backgroundColor: 'rgba(46, 204, 113, 0.1)', fill: true, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.7)', pointBorderColor: '#fff', pointHoverRadius: 6, pointHoverBackgroundColor: 'rgba(46, 204, 113, 1)', pointHoverBorderColor: 'white', pointHoverBorderWidth: 2 }, { label: 'Baseline (kWh)', data: [1800, 1850, 1850, 1830], borderColor: 'rgba(46, 204, 113, 0.2)', backgroundColor: 'rgba(46, 204, 113, 0.05)', borderDash: [5, 5], fill: false, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.2)', pointBorderColor: '#fff', pointRadius: 0, pointHoverRadius: 3, } ] }, year: { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], datasets: [ { label: 'Energy Usage (kWh)', data: [7200, 6850, 6300, 5500, 4800, 5200, 6100, 6350, 5800, 6200, 6800, 7300], borderColor: 'rgba(46, 204, 113, 0.7)', backgroundColor: 'rgba(46, 204, 113, 0.1)', fill: true, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.7)', pointBorderColor: '#fff', pointHoverRadius: 6, pointHoverBackgroundColor: 'rgba(46, 204, 113, 1)', pointHoverBorderColor: 'white', pointHoverBorderWidth: 2 }, { label: 'Baseline (kWh)', data: [7500, 7300, 6800, 6000, 5200, 5000, 5800, 6000, 5600, 6000, 6500, 7100], borderColor: 'rgba(46, 204, 113, 0.2)', backgroundColor: 'rgba(46, 204, 113, 0.05)', borderDash: [5, 5], fill: false, tension: 0.4, pointBackgroundColor: 'rgba(46, 204, 113, 0.2)', pointBorderColor: '#fff', pointRadius: 0, pointHoverRadius: 3, } ] } }; // Create chart let energyChart = new Chart(ctx, { type: 'line', data: chartData.week, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 1500, easing: 'easeOutQuart' }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0, 0, 0, 0.05)' }, ticks: { callback: function(value) { return value + ' kWh'; } } }, x: { grid: { display: false } } }, plugins: { legend: { display: false }, tooltip: { enabled: false, external: externalTooltipHandler } }, interaction: { mode: 'index', intersect: false, }, onHover: (event, elements) => { const chartHelp = document.querySelector('.chart-help'); if (elements && elements.length) { chartHelp.style.opacity = 0; } else { chartHelp.style.opacity = 0.7; } } } }); // Custom tooltip handler function externalTooltipHandler(context) { const {chart, tooltip} = context; const tooltipEl = document.querySelector('.chart-tooltip'); // Hide if no tooltip if (tooltip.opacity === 0) { tooltipEl.style.opacity = 0; return; } // Set content if (tooltip.body) { const titleLines = tooltip.title || []; const bodyLines = tooltip.body.map(b => b.lines); const dataIndex = tooltip.dataPoints[0].dataIndex; const datasetIndex = tooltip.dataPoints[0].datasetIndex; if (datasetIndex === 0) { // Only show for main dataset const value = chart.data.datasets[datasetIndex].data[dataIndex]; document.querySelector('.tooltip-label').textContent = 'Energy Usage'; document.querySelector('.tooltip-value').textContent = `${value} kWh`; document.querySelector('.tooltip-date').textContent = titleLines[0] || ''; // Position tooltip const position = chart.canvas.getBoundingClientRect(); tooltipEl.style.opacity = 1; tooltipEl.style.left = position.left + window.pageXOffset + tooltip.caretX + 'px'; tooltipEl.style.top = position.top + window.pageYOffset + tooltip.caretY - 80 + 'px'; } else { tooltipEl.style.opacity = 0; } } } // Time period selector functionality const timeButtons = document.querySelectorAll('.time-btn'); timeButtons.forEach(button => { button.addEventListener('click', function() { // Update button states timeButtons.forEach(btn => btn.classList.remove('active')); this.classList.add('active'); // Show loading const loadingEl = document.querySelector('.chart-loading'); loadingEl.style.opacity = 1; // Get selected time period const period = this.getAttribute('data-period'); // Update chart with slight delay to show loading animation setTimeout(() => { // Update chart data energyChart.data = chartData[period]; energyChart.update(); // Hide loading loadingEl.style.opacity = 0; }, 800); }); }); // Initialize sparklines function createSparkline(elementId, data, color) { const ctx = document.getElementById(elementId).getContext('2d'); return new Chart(ctx, { type: 'line', data: { labels: Array(data.length).fill(''), datasets: [{ data: data, borderColor: color, backgroundColor: 'rgba(0,0,0,0)', borderWidth: 2, pointRadius: 0, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { display: false } }, animation: { duration: 1000 }, elements: { line: { tension: 0.4 } } } }); } // Initialize sparklines with sample data createSparkline('sparkline1', [35, 38, 42, 40, 37, 36, 33], 'rgba(46, 204, 113, 0.7)'); createSparkline('sparkline2', [20, 32, 45, 30, 25, 18, 12], 'rgba(231, 76, 60, 0.5)'); createSparkline('sparkline3', [8, 10, 9, 12, 14, 12, 13], 'rgba(46, 204, 113, 0.7)'); // Show help text on chart load document.querySelector('.chart-help').style.opacity = 0.7; setTimeout(() => { document.querySelector('.chart-help').style.opacity = 0; }, 3000); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ProjectSphere - Project Management Tool</title> <style> :root { --primary: #3a506b; --secondary: #5bc0be; --accent: #0b132b; --light: #f7f9fb; --success: #6fffe9; --warning: #ffb703; --danger: #e76f51; --neutral: #ddd; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: var(--light); color: var(--accent); display: flex; flex-direction: column; height: 100vh; overflow: hidden; padding: 20px; } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .logo { font-weight: 700; font-size: 1.5rem; color: var(--primary); display: flex; align-items: center; } .logo-icon { width: 24px; height: 24px; background: linear-gradient(135deg, var(--secondary), var(--success)); border-radius: 4px; margin-right: 8px; } .tab-container { display: flex; gap: 5px; background-color: white; padding: 5px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .tab { padding: 8px 16px; cursor: pointer; border-radius: 6px; transition: all 0.3s ease; } .tab.active { background-color: var(--primary); color: white; } .controls { display: flex; gap: 10px; } button { background: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 5px; transition: all 0.2s ease; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); } button:hover { background-color: var(--primary); color: white; } button.primary { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; } .container { flex: 1; display: flex; gap: 20px; overflow: hidden; } .sidebar { width: 220px; background-color: white; border-radius: 10px; padding: 15px; box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05); overflow-y: auto; } .sidebar h3 { margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px; color: var(--primary); } .project-list { list-style: none; } .project-item { padding: 10px; margin-bottom: 8px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; } .project-item:hover { background-color: rgba(91, 192, 190, 0.1); } .project-item.active { background-color: var(--primary); color: white; } .project-item-status { width: 8px; height: 8px; border-radius: 50%; } .status-on-track { background-color: var(--success); } .status-at-risk { background-color: var(--warning); } .status-delayed { background-color: var(--danger); } .main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .project-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .project-title { font-size: 1.5rem; font-weight: 600; } .project-stats { display: flex; gap: 15px; margin-bottom: 20px; } .stat-card { background: white; border-radius: 8px; padding: 15px; flex: 1; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.05); position: relative; overflow: hidden; } .stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 5px; height: 100%; background: linear-gradient(to bottom, var(--secondary), var(--success)); } .stat-title { font-size: 0.8rem; color: #666; margin-bottom: 5px; } .stat-value { font-size: 1.5rem; font-weight: 600; } .chart-container { flex: 1; background: white; border-radius: 10px; padding: 20px; box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05); overflow-y: auto; position: relative; } .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .chart-title { font-size: 1.1rem; font-weight: 600; } .gantt-chart { position: relative; overflow-x: auto; } .timeline { display: flex; border-bottom: 1px solid #eee; padding-bottom: 10px; margin-bottom: 15px; } .timeline-cell { flex: 1; min-width: 60px; text-align: center; font-size: 0.8rem; color: #666; } .gantt-row { display: flex; align-items: center; margin-bottom: 15px; position: relative; } .task-label { width: 150px; padding-right: 15px; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .task-owner { font-size: 0.75rem; color: #666; } .gantt-bars { flex: 1; position: relative; height: 40px; display: flex; } .gantt-bar-container { flex: 1; min-width: 60px; position: relative; } .gantt-bar { position: absolute; height: 16px; top: 8px; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; z-index: 2; } .gantt-bar:hover { transform: scaleY(1.2); } .gantt-bar::before, .gantt-bar::after { content: ''; position: absolute; width: 10px; height: 16px; background: inherit; top: 0; cursor: ew-resize; opacity: 0; transition: opacity 0.2s ease; } .gantt-bar::before { left: 0; border-top-left-radius: 4px; border-bottom-left-radius: 4px; } .gantt-bar::after { right: 0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .gantt-bar:hover::before, .gantt-bar:hover::after { opacity: 1; } .milestone { position: absolute; width: 12px; height: 12px; transform: rotate(45deg); background: var(--warning); z-index: 3; border: 2px solid white; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); top: 10px; } .gantt-bar-tooltip { position: absolute; background: var(--accent); color: white; padding: 8px 12px; border-radius: 4px; font-size: 0.8rem; top: -45px; left: 50%; transform: translateX(-50%); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.2s ease; z-index: 10; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2); } .gantt-bar-tooltip::after { content: ''; position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid var(--accent); } .gantt-bar:hover .gantt-bar-tooltip { opacity: 1; } .progress-track { position: absolute; height: 5px; background-color: rgba(255, 255, 255, 0.3); bottom: 3px; left: 4px; right: 4px; border-radius: 2px; } .progress-bar { height: 100%; background-color: rgba(255, 255, 255, 0.9); border-radius: 2px; transition: width 0.3s ease; } .category-design { background: linear-gradient(135deg, #3a506b, #1c2541); } .category-development { background: linear-gradient(135deg, #5bc0be, #3a506b); } .category-marketing { background: linear-gradient(135deg, #6fffe9, #5bc0be); } .category-testing { background: linear-gradient(135deg, #ffb703, #fb8500); } .today-marker { position: absolute; top: 0; bottom: 0; width: 2px; background-color: var(--danger); z-index: 1; } .today-marker::before { content: 'Today'; position: absolute; top: -20px; left: 50%; transform: translateX(-50%); background: var(--danger); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; white-space: nowrap; } .zoom-controls { display: flex; gap: 5px; } .btn-zoom { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 4px; background-color: white; border: 1px solid #eee; cursor: pointer; transition: all 0.2s ease; } .btn-zoom:hover { background-color: var(--primary); color: white; } .dependency-line { position: absolute; stroke: #aaa; stroke-width: 1.5; stroke-dasharray: 4; z-index: 1; } @media (max-width: 700px) { .container { flex-direction: column; } .sidebar { width: 100%; max-height: 180px; } .project-stats { flex-wrap: wrap; } .stat-card { min-width: 45%; } .task-label { width: 100px; } } /* Animations */ @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .pulse { animation: pulse 2s infinite; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .fade-in { animation: fadeIn 0.5s ease forwards; } /* Custom scrollbar */ ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; } ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: var(--primary); } </style> </head> <body> <header> <div class="logo"> <div class="logo-icon"></div> ProjectSphere </div> <div class="tab-container"> <div class="tab active">Gantt</div> <div class="tab">Kanban</div> <div class="tab">Timeline</div> <div class="tab">Calendar</div> </div> <div class="controls"> <button id="shareButton"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg> Share </button> <button class="primary" id="addTaskButton"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Add Task </button> </div> </header> <div class="container"> <div class="sidebar"> <h3>Projects</h3> <ul class="project-list"> <li class="project-item active"> <span>Website Redesign</span> <span class="project-item-status status-on-track"></span> </li> <li class="project-item"> <span>Mobile App Development</span> <span class="project-item-status status-at-risk"></span> </li> <li class="project-item"> <span>Q4 Marketing Campaign</span> <span class="project-item-status status-delayed"></span> </li> <li class="project-item"> <span>API Integration</span> <span class="project-item-status status-on-track"></span> </li> <li class="project-item"> <span>Product Launch</span> <span class="project-item-status status-at-risk"></span> </li> </ul> </div> <div class="main-content"> <div class="project-header"> <h2 class="project-title">Website Redesign</h2> <div class="zoom-controls"> <div class="btn-zoom" id="zoomOut">−</div> <div class="btn-zoom" id="zoomIn">+</div> </div> </div> <div class="project-stats"> <div class="stat-card"> <div class="stat-title">Progress</div> <div class="stat-value">68%</div> </div> <div class="stat-card"> <div class="stat-title">Days Left</div> <div class="stat-value">14</div> </div> <div class="stat-card"> <div class="stat-title">Tasks</div> <div class="stat-value">24/35</div> </div> <div class="stat-card"> <div class="stat-title">Team Members</div> <div class="stat-value">8</div> </div> </div> <div class="chart-container"> <div class="chart-header"> <h3 class="chart-title">Project Timeline</h3> </div> <div class="gantt-chart" id="ganttChart"> <div class="timeline" id="timeline"> <!-- Timeline populated by JS --> </div> <div id="ganttContent"> <!-- Gantt content populated by JS --> </div> </div> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // Project data const projectData = { startDate: new Date(2023, 10, 1), // Nov 1, 2023 endDate: new Date(2023, 11, 15), // Dec 15, 2023 tasks: [ { id: 1, name: "Research & Analysis", owner: "Sarah Chen", startDate: new Date(2023, 10, 1), endDate: new Date(2023, 10, 10), progress: 100, category: "design", dependencies: [] }, { id: 2, name: "Wireframing", owner: "Michael Lee", startDate: new Date(2023, 10, 8), endDate: new Date(2023, 10, 18), progress: 100, category: "design", dependencies: [1] }, { id: 3, name: "UI Design", owner: "Alex Wong", startDate: new Date(2023, 10, 15), endDate: new Date(2023, 10, 30), progress: 85, category: "design", dependencies: [2] }, { id: 4, name: "Frontend Development", owner: "Jordan Taylor", startDate: new Date(2023, 10, 25), endDate: new Date(2023, 11, 10), progress: 60, category: "development", dependencies: [3] }, { id: 5, name: "Backend Integration", owner: "Riley Johnson", startDate: new Date(2023, 11, 1), endDate: new Date(2023, 11, 15), progress: 30, category: "development", dependencies: [4] }, { id: 6, name: "QA Testing", owner: "Jamie Garcia", startDate: new Date(2023, 11, 5), endDate: new Date(2023, 11, 15), progress: 10, category: "testing", dependencies: [4] }, { id: 7, name: "SEO Optimization", owner: "Casey Smith", startDate: new Date(2023, 11, 10), endDate: new Date(2023, 11, 15), progress: 0, category: "marketing", dependencies: [] } ], milestones: [ { id: 1, name: "Design Approval", date: new Date(2023, 10, 30), relatedTaskId: 3 }, { id: 2, name: "Beta Launch", date: new Date(2023, 11, 10), relatedTaskId: 4 } ] }; // Current zoom level let zoomLevel = 1; const minZoom = 0.5; const maxZoom = 2; // Generate timeline function generateTimeline() { const timeline = document.getElementById('timeline'); timeline.innerHTML = ''; // Calculate number of days const days = Math.ceil((projectData.endDate - projectData.startDate) / (1000 * 60 * 60 * 24)); const daysToShow = Math.ceil(days / zoomLevel); // Generate timeline cells for (let i = 0; i < daysToShow; i++) { const date = new Date(projectData.startDate); date.setDate(date.getDate() + i); const cell = document.createElement('div'); cell.className = 'timeline-cell'; // Format date for display const day = date.getDate(); const month = date.toLocaleString('default', { month: 'short' }); if (day === 1 || i === 0) { cell.textContent = `${day} ${month}`; } else { cell.textContent = day; } timeline.appendChild(cell); } } // Generate Gantt chart function generateGanttChart() { const ganttContent = document.getElementById('ganttContent'); ganttContent.innerHTML = ''; // Calculate total days in project const totalDays = Math.ceil((projectData.endDate - projectData.startDate) / (1000 * 60 * 60 * 24)); const daysToShow = Math.ceil(totalDays / zoomLevel); // Today marker const today = new Date(); if (today >= projectData.startDate && today <= projectData.endDate) { const daysSinceStart = Math.ceil((today - projectData.startDate) / (1000 * 60 * 60 * 24)); const position = (daysSinceStart / daysToShow) * 100; const todayMarker = document.createElement('div'); todayMarker.className = 'today-marker'; todayMarker.style.left = `${position}%`; ganttContent.appendChild(todayMarker); } // Generate task rows projectData.tasks.forEach(task => { const row = document.createElement('div'); row.className = 'gantt-row'; // Task label const label = document.createElement('div'); label.className = 'task-label'; label.innerHTML = `${task.name}<div class="task-owner">${task.owner}</div>`; row.appendChild(label); // Gantt bars container const barsContainer = document.createElement('div'); barsContainer.className = 'gantt-bars'; // Calculate position const startDayOffset = Math.ceil((task.startDate - projectData.startDate) / (1000 * 60 * 60 * 24)); const taskDuration = Math.ceil((task.endDate - task.startDate) / (1000 * 60 * 60 * 24)) + 1; const startPosition = (startDayOffset / daysToShow) * 100; const width = (taskDuration / daysToShow) * 100; // Create the bar const bar = document.createElement('div'); bar.className = `gantt-bar category-${task.category}`; bar.style.left = `${startPosition}%`; bar.style.width = `${width}%`; bar.setAttribute('data-task-id', task.id); // Create tooltip const tooltip = document.createElement('div'); tooltip.className = 'gantt-bar-tooltip'; tooltip.innerHTML = ` <strong>${task.name}</strong><br> ${task.startDate.toLocaleDateString()} - ${task.endDate.toLocaleDateString()}<br> Progress: ${task.progress}% `; bar.appendChild(tooltip); // Create progress indicator const progressTrack = document.createElement('div'); progressTrack.className = 'progress-track'; const progressBar = document.createElement('div'); progressBar.className = 'progress-bar'; progressBar.style.width = `${task.progress}%`; progressTrack.appendChild(progressBar); bar.appendChild(progressTrack); barsContainer.appendChild(bar); row.appendChild(barsContainer); ganttContent.appendChild(row); }); // Add milestones projectData.milestones.forEach(milestone => { const dayOffset = Math.ceil((milestone.date - projectData.startDate) / (1000 * 60 * 60 * 24)); const position = (dayOffset / daysToShow) * 100; const milestoneElement = document.createElement('div'); milestoneElement.className = 'milestone pulse'; milestoneElement.style.left = `${position}%`; milestoneElement.setAttribute('title', milestone.name); // Find the related task row const relatedTask = projectData.tasks.find(t => t.id === milestone.relatedTaskId); if (relatedTask) { const taskRows = ganttContent.querySelectorAll('.gantt-row'); const index = projectData.tasks.indexOf(relatedTask); if (taskRows[index]) { const barsContainer = taskRows[index].querySelector('.gantt-bars'); barsContainer.appendChild(milestoneElement); } } }); // Draw dependency lines projectData.tasks.forEach(task => { if (task.dependencies.length > 0) { task.dependencies.forEach(depId => { const dependencyTask = projectData.tasks.find(t => t.id === depId); if (dependencyTask) { drawDependencyLine(dependencyTask, task); } }); } }); } function drawDependencyLine(fromTask, toTask) { const fromIndex = projectData.tasks.indexOf(fromTask); const toIndex = projectData.tasks.indexOf(toTask); const fromTaskEl = document.querySelector(`.gantt-bar[data-task-id="${fromTask.id}"]`); const toTaskEl = document.querySelector(`.gantt-bar[data-task-id="${toTask.id}"]`); if (!fromTaskEl || !toTaskEl) return; const ganttContent = document.getElementById('ganttContent'); const fromRect = fromTaskEl.getBoundingClientRect(); const toRect = toTaskEl.getBoundingClientRect(); const containerRect = ganttContent.getBoundingClientRect(); // Calculate points relative to the container const x1 = fromRect.right - containerRect.left; const y1 = fromRect.top - containerRect.top + fromRect.height / 2; const x2 = toRect.left - containerRect.left; const y2 = toRect.top - containerRect.top + toRect.height / 2; // Create SVG line const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("class", "dependency-line"); svg.style.position = "absolute"; svg.style.top = "0"; svg.style.left = "0"; svg.style.width = "100%"; svg.style.height = "100%"; svg.style.pointerEvents = "none"; const path = document.createElementNS(svgNS, "path"); // Create a curved path const midX = x1 + (x2 - x1) / 2; const pathD = `M ${x1},${y1} C ${midX},${y1} ${midX},${y2} ${x2},${y2}`; path.setAttribute("d", pathD); path.setAttribute("fill", "none"); path.setAttribute("stroke", "#aaa"); path.setAttribute("stroke-width", "1.5"); path.setAttribute("stroke-dasharray", "4"); svg.appendChild(path); ganttContent.appendChild(svg); } // Initialize chart function initChart() { generateTimeline(); generateGanttChart(); } // Zoom controls document.getElementById('zoomIn').addEventListener('click', function() { if (zoomLevel < maxZoom) { zoomLevel += 0.25; initChart(); } }); document.getElementById('zoomOut').addEventListener('click', function() { if (zoomLevel > minZoom) { zoomLevel -= 0.25; initChart(); } }); // Make gantt bars resizable document.addEventListener('mousedown', function(e) { if (e.target.classList.contains('gantt-bar')) { const bar = e.target; const barRect = bar.getBoundingClientRect(); const barLeftEdge = barRect.left; const barRightEdge = barRect.right; // Check if we're near the left or right edge if (Math.abs(e.clientX - barLeftEdge) < 10) { // Resize from left resizeBar(bar, 'left', e); } else if (Math.abs(e.clientX - barRightEdge) < 10) { // Resize from right resizeBar(bar, 'right', e); } else { // Move the entire bar moveBar(bar, e); } } }); function resizeBar(bar, direction, startEvent) { const initialBarRect = bar.getBoundingClientRect(); const initialMouseX = startEvent.clientX; const initialLeft = parseFloat(bar.style.left); const initialWidth = parseFloat(bar.style.width); const container = bar.closest('.gantt-bars'); const containerWidth = container.offsetWidth; function onMouseMove(e) { e.preventDefault(); const deltaX = e.clientX - initialMouseX; const deltaPercent = (deltaX / containerWidth) * 100; if (direction === 'left') { const newLeft = Math.max(0, initialLeft + deltaPercent); const newWidth = Math.max(5, initialWidth - (newLeft - initialLeft)); bar.style.left = `${newLeft}%`; bar.style.width = `${newWidth}%`; } else { const newWidth = Math.max(5, initialWidth + deltaPercent); bar.style.width = `${newWidth}%`; } } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); startEvent.preventDefault(); } function moveBar(bar, startEvent) { const initialMouseX = startEvent.clientX; const initialLeft = parseFloat(bar.style.left); const container = bar.closest('.gantt-bars'); const containerWidth = container.offsetWidth; const barWidth = parseFloat(bar.style.width); function onMouseMove(e) { e.preventDefault(); const deltaX = e.clientX - initialMouseX; const deltaPercent = (deltaX / containerWidth) * 100; const newLeft = Math.max(0, Math.min(100 - barWidth, initialLeft + deltaPercent)); bar.style.left = `${newLeft}%`; } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); startEvent.preventDefault(); } // Add task button document.getElementById('addTaskButton').addEventListener('click', function() { // In a real app, this would open a modal or form // For this demo, we'll just add a placeholder task const newTask = { id: projectData.tasks.length + 1, name: "New Task", owner: "You", startDate: new Date(2023, 11, 1), endDate: new Date(2023, 11, 8), progress: 0, category: "development", dependencies: [] }; projectData.tasks.push(newTask); initChart(); // Animate the new task setTimeout(() => { const newTaskEl = document.querySelector(`.gantt-bar[data-task-id="${newTask.id}"]`); if (newTaskEl) { newTaskEl.classList.add('fade-in'); } }, 10); }); // Share button document.getElementById('shareButton').addEventListener('click', function() { this.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> Shared! `; setTimeout(() => { this.innerHTML = ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg> Share `; }, 2000); }); // Tab switching document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', function() { document.querySelector('.tab.active').classList.remove('active'); this.classList.add('active'); }); }); // Project switching document.querySelectorAll('.project-item').forEach(project => { project.addEventListener('click', function() { document.querySelector('.project-item.active').classList.remove('active'); this.classList.add('active'); }); }); // Initialize the chart initChart(); // Handle window resize window.addEventListener('resize', initChart); }); </script> </body> </html>
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Real Estate Market Trends</title> <script src="https://d3js.org/d3.v7.min.js"></script> <style> @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', sans-serif; transition: all 0.3s ease; } :root { --bg-color: #f8f9fa; --text-primary: #333; --text-secondary: #6c757d; --accent: #7c4dff; --accent-light: #e0d3ff; --primary: #37474f; --secondary: #78909c; --border: #ced4da; --luxe-gold: #d4af37; } body { display: flex; flex-direction: column; height: 700px; width: 700px; padding: 20px; background-color: var(--bg-color); color: var(--text-primary); overflow: hidden; } header { margin-bottom: 15px; } h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 5px; color: var(--primary); letter-spacing: -0.5px; } h2 { font-size: 1.1rem; font-weight: 500; color: var(--secondary); margin-bottom: 15px; letter-spacing: -0.3px; } p { font-size: 0.9rem; line-height: 1.5; color: var(--text-secondary); margin-bottom: 10px; } .dashboard { display: flex; flex-direction: column; gap: 20px; flex: 1; overflow: hidden; } .chart-container { position: relative; flex: 1; background: white; border-radius: 10px; padding: 20px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); overflow: hidden; } .chart-title { font-size: 1rem; font-weight: 600; margin-bottom: 5px; color: var(--primary); } .chart-description { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 15px; } .charts-row { display: flex; gap: 20px; flex: 1; min-height: 250px; } .axis-title { font-size: 0.75rem; fill: var(--text-secondary); font-weight: 500; } .axis line, .axis path { stroke: var(--border); } .axis text { fill: var(--text-secondary); font-size: 0.7rem; } .tooltip { position: absolute; padding: 10px; background: rgba(255, 255, 255, 0.95); border-radius: 5px; pointer-events: none; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); font-size: 0.85rem; transition: opacity 0.2s; z-index: 10; max-width: 200px; border-left: 3px solid var(--luxe-gold); } .tooltip h4 { font-weight: 600; margin-bottom: 5px; font-size: 0.9rem; color: var(--primary); } .tooltip p { margin: 2px 0; font-size: 0.8rem; } .tooltip-value { font-weight: 500; color: var(--primary); } .legend { display: flex; gap: 15px; font-size: 0.8rem; margin-top: 5px; flex-wrap: wrap; } .legend-item { display: flex; align-items: center; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: background-color 0.2s; } .legend-item:hover { background-color: #f0f0f0; } .legend-color { width: 12px; height: 12px; border-radius: 2px; margin-right: 5px; } .legend-text { color: var(--text-secondary); } .time-selector { display: flex; gap: 10px; margin-top: 10px; } .time-option { padding: 5px 10px; font-size: 0.8rem; border-radius: 5px; cursor: pointer; background: #eee; color: var(--text-secondary); font-weight: 500; } .time-option.active { background: var(--accent-light); color: var(--accent); } .controls { display: flex; justify-content: space-between; align-items: center; } .scatter-dot { cursor: pointer; transition: r 0.3s, opacity 0.3s, stroke-width 0.3s; } .scatter-dot:hover { r: 7; stroke-width: 2; } .scatter-marker { fill: white; stroke: var(--accent); stroke-width: 1.5; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .grid line { stroke: #e0e0e0; stroke-opacity: 0.7; shape-rendering: crispEdges; } .area-path { transition: opacity 0.5s; } .area-path:hover { opacity: 0.8; } /* Responsive styles */ @media (max-width: 700px) { body { padding: 15px; } h1 { font-size: 1.5rem; } .charts-row { flex-direction: column; gap: 15px; } .chart-container { padding: 15px; } } </style> </head> <body> <header> <h1>Real Estate Insights Dashboard</h1> <p>Analyzing 2022-2023 property trends across metropolitan areas, highlighting market dynamics and investment opportunities.</p> </header> <div class="dashboard"> <div class="chart-container"> <div class="chart-title">Property Price vs. Size Correlation</div> <div class="chart-description">Examine how property size influences pricing across different neighborhoods, with marker size indicating property age.</div> <div class="controls"> <div class="legend" id="scatter-legend"></div> </div> <div id="scatter-chart"></div> </div> <div class="charts-row"> <div class="chart-container"> <div class="chart-title">Price Trends by Property Type</div> <div class="chart-description">Track price movements across different property categories over time.</div> <div class="controls"> <div class="legend" id="area-legend"></div> <div class="time-selector"> <div class="time-option active" data-period="1m">1M</div> <div class="time-option" data-period="6m">6M</div> <div class="time-option" data-period="1y">1Y</div> </div> </div> <div id="area-chart"></div> </div> </div> </div> <script> // Generate realistic property data function generatePropertyData() { const neighborhoods = ["Downtown", "Riverside", "Highland Park", "Oakwood", "Westside"]; const propertyTypes = ["Condo", "Single Family", "Townhouse", "Luxury", "Multi-Unit"]; const properties = []; // Generate scatter plot data for (let i = 0; i < 100; i++) { const neighborhood = neighborhoods[Math.floor(Math.random() * neighborhoods.length)]; const multiplier = { "Downtown": 1.6, "Riverside": 1.2, "Highland Park": 1.4, "Oakwood": 0.9, "Westside": 1.3 }[neighborhood]; const sqft = 800 + Math.random() * 3000; const price = (300000 + Math.random() * 900000) * multiplier; const age = 1 + Math.floor(Math.random() * 40); properties.push({ id: i, neighborhood, sqft, price, age, bedrooms: Math.floor(sqft / 600) + 1, bathrooms: Math.floor(sqft / 750) + 1 }); } // Generate time series data for area chart const timeSeriesData = []; const now = new Date(); for (let i = 365; i >= 0; i--) { const date = new Date(now); date.setDate(date.getDate() - i); const dataPoint = { date }; propertyTypes.forEach(type => { // Create a unique but consistent pattern for each property type const basePrice = { "Condo": 350000, "Single Family": 550000, "Townhouse": 450000, "Luxury": 850000, "Multi-Unit": 650000 }[type]; // Add seasonal variations and general trend const seasonal = Math.sin((date.getMonth() + 1) / 12 * Math.PI * 2) * 0.05; const trend = 0.1 * (1 - i / 365); // 10% growth over the year const noise = (Math.random() - 0.5) * 0.03; dataPoint[type] = basePrice * (1 + seasonal + trend + noise); }); timeSeriesData.push(dataPoint); } return { properties, timeSeriesData }; } // Main visualization code document.addEventListener('DOMContentLoaded', function() { const data = generatePropertyData(); // Create tooltip const tooltip = d3.select("body") .append("div") .attr("class", "tooltip") .style("opacity", 0); // Initialize scatter plot initScatterPlot(data.properties, tooltip); // Initialize area chart initAreaChart(data.timeSeriesData, tooltip); // Handle time period selection document.querySelectorAll('.time-option').forEach(option => { option.addEventListener('click', function() { document.querySelectorAll('.time-option').forEach(o => o.classList.remove('active')); this.classList.add('active'); updateAreaChart(this.dataset.period); }); }); }); function initScatterPlot(data, tooltip) { // SVG dimensions and margins const margin = {top: 20, right: 30, bottom: 50, left: 60}; const container = document.getElementById('scatter-chart'); const width = container.clientWidth - margin.left - margin.right; const height = 250 - margin.top - margin.bottom; // Create SVG const svg = d3.select("#scatter-chart") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // Set up scales const x = d3.scaleLinear() .domain([0, d3.max(data, d => d.sqft) * 1.05]) .range([0, width]); const y = d3.scaleLinear() .domain([0, d3.max(data, d => d.price) * 1.05]) .range([height, 0]); const size = d3.scaleLinear() .domain([d3.min(data, d => d.age), d3.max(data, d => d.age)]) .range([3, 10]); // Color scale for neighborhoods const color = d3.scaleOrdinal() .domain(["Downtown", "Riverside", "Highland Park", "Oakwood", "Westside"]) .range(["#7c4dff", "#4dabff", "#ff4d4d", "#5ad45a", "#ffab4d"]); // Add grid lines svg.append("g") .attr("class", "grid") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x) .tickSize(-height) .tickFormat("")) .selectAll("line") .style("stroke-dasharray", "3,3"); svg.append("g") .attr("class", "grid") .call(d3.axisLeft(y) .tickSize(-width) .tickFormat("")) .selectAll("line") .style("stroke-dasharray", "3,3"); // Add axes svg.append("g") .attr("class", "axis") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x) .tickFormat(d => `${d.toLocaleString()} sqft`)) .selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("transform", "rotate(-35)"); svg.append("g") .attr("class", "axis") .call(d3.axisLeft(y) .tickFormat(d => `$${(d/1000).toFixed(0)}k`)); // X-axis label svg.append("text") .attr("class", "axis-title") .attr("text-anchor", "middle") .attr("x", width / 2) .attr("y", height + margin.bottom - 5) .text("Property Size (Square Feet)"); // Y-axis label svg.append("text") .attr("class", "axis-title") .attr("text-anchor", "middle") .attr("transform", "rotate(-90)") .attr("y", -margin.left + 15) .attr("x", -height / 2) .text("Price (USD)"); // Add dots const dots = svg.selectAll(".scatter-dot") .data(data) .enter() .append("circle") .attr("class", "scatter-dot") .attr("cx", d => x(d.sqft)) .attr("cy", d => y(d.price)) .attr("r", d => size(d.age)) .style("fill", d => color(d.neighborhood)) .style("opacity", 0.7) .style("stroke", "white") .style("stroke-width", 1); // Mouse interaction dots.on("mouseover", function(event, d) { // Highlight the dot d3.select(this) .transition() .duration(200) .attr("r", size(d.age) + 2) .style("opacity", 1) .style("stroke-width", 2); // Show tooltip tooltip.transition() .duration(200) .style("opacity", 1); tooltip.html(` <h4>${d.neighborhood} Property</h4> <p>Price: <span class="tooltip-value">$${d.price.toLocaleString()}</span></p> <p>Size: <span class="tooltip-value">${d.sqft.toFixed(0)} sqft</span></p> <p>Age: <span class="tooltip-value">${d.age} years</span></p> <p>Bedrooms: <span class="tooltip-value">${d.bedrooms}</span></p> <p>Bathrooms: <span class="tooltip-value">${d.bathrooms}</span></p> `) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function(event, d) { d3.select(this) .transition() .duration(200) .attr("r", size(d.age)) .style("opacity", 0.7) .style("stroke-width", 1); tooltip.transition() .duration(500) .style("opacity", 0); }); // Add marker for highlighting selected dots svg.append("circle") .attr("class", "scatter-marker") .attr("r", 12) .attr("cx", 0) .attr("cy", 0); // Create legend const neighborhoods = Array.from(new Set(data.map(d => d.neighborhood))); const legend = d3.select("#scatter-legend") .selectAll(".legend-item") .data(neighborhoods) .enter() .append("div") .attr("class", "legend-item") .style("opacity", 1) .on("click", function(event, neighborhood) { // Check if any neighborhood is already filtered const currentOpacity = d3.select(this).style("opacity"); let isFiltered = false; d3.selectAll(".legend-item").each(function() { if (d3.select(this).style("opacity") != 1) isFiltered = true; }); // If clicking the only highlighted item, reset all if (currentOpacity == 1 && isFiltered) { d3.selectAll(".legend-item").style("opacity", 1); dots.transition().duration(300).style("opacity", 0.7); } else { // Otherwise toggle the clicked neighborhood const newOpacity = currentOpacity == 1 ? 0.5 : 1; d3.select(this).style("opacity", newOpacity); // Update all dots based on legend state const activeNeighborhoods = []; d3.selectAll(".legend-item").each(function(d) { if (d3.select(this).style("opacity") == 1) { activeNeighborhoods.push(d); } }); dots.transition().duration(300) .style("opacity", d => activeNeighborhoods.includes(d.neighborhood) ? 0.7 : 0.1); } }); legend.append("div") .attr("class", "legend-color") .style("background", d => color(d)); legend.append("div") .attr("class", "legend-text") .text(d => d); // Make chart responsive function resizeScatter() { const newWidth = container.clientWidth - margin.left - margin.right; // Update SVG size d3.select("#scatter-chart svg") .attr("width", newWidth + margin.left + margin.right); // Update scales x.range([0, newWidth]); // Update x-axis svg.select(".axis:nth-child(3)") .call(d3.axisBottom(x).tickFormat(d => `${d.toLocaleString()} sqft`)) .selectAll("text") .style("text-anchor", "end") .attr("dx", "-.8em") .attr("dy", ".15em") .attr("transform", "rotate(-35)"); // Update grid svg.select(".grid:nth-child(1)") .call(d3.axisBottom(x).tickSize(-height).tickFormat("")); svg.select(".grid:nth-child(2)") .call(d3.axisLeft(y).tickSize(-newWidth).tickFormat("")); // Update x-axis label svg.select(".axis-title:nth-child(4)") .attr("x", newWidth / 2); // Update dots dots.attr("cx", d => x(d.sqft)); } window.addEventListener("resize", resizeScatter); } function initAreaChart(data, tooltip) { // SVG dimensions and margins const margin = {top: 20, right: 30, bottom: 50, left: 60}; const container = document.getElementById('area-chart'); const width = container.clientWidth - margin.left - margin.right; const height = 250 - margin.top - margin.bottom; // Create SVG const svg = d3.select("#area-chart") .append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); // Set up scales const x = d3.scaleTime() .domain(d3.extent(data, d => d.date)) .range([0, width]); const y = d3.scaleLinear() .domain([0, d3.max(data, d => { const values = Object.entries(d).filter(([key]) => key !== 'date').map(([_, value]) => value); return d3.max(values) * 1.1; })]) .range([height, 0]); // Property types to display const propertyTypes = ["Condo", "Single Family", "Townhouse", "Luxury", "Multi-Unit"]; // Color scale const color = d3.scaleOrdinal() .domain(propertyTypes) .range(["#4dabff", "#ff4d4d", "#5ad45a", "#7c4dff", "#ffab4d"]); // Create area generator const area = d3.area() .x(d => x(d.date)) .y0(height) .y1(d => y(d.value)) .curve(d3.curveCatmullRom.alpha(0.5)); // Create line generator for area borders const line = d3.line() .x(d => x(d.date)) .y(d => y(d.value)) .curve(d3.curveCatmullRom.alpha(0.5)); // Add grid lines svg.append("g") .attr("class", "grid") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x) .ticks(5) .tickSize(-height) .tickFormat("")) .selectAll("line") .style("stroke-dasharray", "3,3"); svg.append("g") .attr("class", "grid") .call(d3.axisLeft(y) .tickSize(-width) .tickFormat("")) .selectAll("line") .style("stroke-dasharray", "3,3"); // Add X axis svg.append("g") .attr("class", "axis") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(x) .ticks(5) .tickFormat(d3.timeFormat("%b %Y"))); // Add Y axis svg.append("g") .attr("class", "axis") .call(d3.axisLeft(y) .tickFormat(d => `$${(d/1000).toFixed(0)}k`)); // X-axis label svg.append("text") .attr("class", "axis-title") .attr("text-anchor", "middle") .attr("x", width / 2) .attr("y", height + margin.bottom - 5) .text("Date"); // Y-axis label svg.append("text") .attr("class", "axis-title") .attr("text-anchor", "middle") .attr("transform", "rotate(-90)") .attr("y", -margin.left + 15) .attr("x", -height / 2) .text("Average Price (USD)"); // Prepare and add areas for each property type propertyTypes.forEach(type => { // Extract data for this property type const typeData = data.map(d => ({ date: d.date, value: d[type] })); // Add area svg.append("path") .datum(typeData) .attr("class", "area-path") .attr("fill", color(type)) .attr("fill-opacity", 0.5) .attr("d", area) .on("mouseover", function(event) { // Highlight this area d3.select(this) .transition() .duration(200) .attr("fill-opacity", 0.8); // Get data at mouse position const mouseX = d3.pointer(event)[0]; const xDate = x.invert(mouseX); // Find closest data point const bisect = d3.bisector(d => d.date).left; const index = bisect(typeData, xDate); const dataPoint = typeData[index] || typeData[typeData.length - 1]; // Show tooltip tooltip.transition() .duration(200) .style("opacity", 1); const date = dataPoint.date.toLocaleDateString('en-US', { year: 'numeric', month: 'short' }); tooltip.html(` <h4>${type} Properties</h4> <p>Date: <span class="tooltip-value">${date}</span></p> <p>Avg. Price: <span class="tooltip-value">$${dataPoint.value.toLocaleString(undefined, {maximumFractionDigits: 0})}</span></p> `) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 28) + "px"); }) .on("mouseout", function() { d3.select(this) .transition() .duration(200) .attr("fill-opacity", 0.5); tooltip.transition() .duration(500) .style("opacity", 0); }); // Add border line svg.append("path") .datum(typeData) .attr("class", "line") .attr("fill", "none") .attr("stroke", color(type)) .attr("stroke-width", 1.5) .attr("d", line); }); // Create legend const legend = d3.select("#area-legend") .selectAll(".legend-item") .data(propertyTypes) .enter() .append("div") .attr("class", "legend-item") .style("opacity", 1) .on("click", function(event, type) { // Check if any type is already filtered const currentOpacity = d3.select(this).style("opacity"); let isFiltered = false; d3.selectAll("#area-legend .legend-item").each(function() { if (d3.select(this).style("opacity") != 1) isFiltered = true; }); // If clicking the only highlighted item, reset all if (currentOpacity == 1 && isFiltered) { d3.selectAll("#area-legend .legend-item").style("opacity", 1); svg.selectAll(".area-path").transition().duration(300).style("opacity", 1); svg.selectAll(".line").transition().duration(300).style("opacity", 1); } else { // Otherwise toggle the clicked type const newOpacity = currentOpacity == 1 ? 0.5 : 1; d3.select(this).style("opacity", newOpacity); // Update all areas based on legend state const activeTypes = []; d3.selectAll("#area-legend .legend-item").each(function(d, i) { if (d3.select(this).style("opacity") == 1) { activeTypes.push(d); } }); svg.selectAll(".area-path") .transition().duration(300) .style("opacity", (d, i) => activeTypes.includes(propertyTypes[i]) ? 1 : 0.1); svg.selectAll(".line") .transition().duration(300) .style("opacity", (d, i) => activeTypes.includes(propertyTypes[i]) ? 1 : 0.1); } }); legend.append("div") .attr("class", "legend-color") .style("background", d => color(d)); legend.append("div") .attr("class", "legend-text") .text(d => d); // Make chart responsive function resizeArea() { const newWidth = container.clientWidth - margin.left - margin.right; // Update SVG size d3.select("#area-chart svg") .attr("width", newWidth + margin.left + margin.right); // Update scales x.range([0, newWidth]); // Update x-axis svg.select(".axis:nth-child(3)") .call(d3.axisBottom(x).ticks(5).tickFormat(d3.timeFormat("%b %Y"))); // Update grid svg.select(".grid:nth-child(1)") .call(d3.axisBottom(x).ticks(5).tickSize(-height).tickFormat("")); svg.select(".grid:nth-child(2)") .call(d3.axisLeft(y).tickSize(-newWidth).tickFormat("")); // Update x-axis label svg.select(".axis-title:nth-child(4)") .attr("x", newWidth / 2); // Update area paths propertyTypes.forEach((type, i) => { const typeData = data.map(d => ({ date: d.date, value: d[type] })); svg.selectAll(".area-path") .data([0, 1, 2, 3, 4]) .attr("d", (d, i) => area(typeData)); svg.selectAll(".line") .data([0, 1, 2, 3, 4]) .attr("d", (d, i) => line(typeData)); }); } window.addEventListener("resize", resizeArea); // Store reference for updating time periods window.areaChartData = { data, svg, x, y, area, line, propertyTypes }; } function updateAreaChart(period) { const { data, svg, x, y, area, line, propertyTypes } = window.areaChartData; // Filter data based on selected time period let filteredData; const now = new Date(); switch(period) { case "1m": const oneMonthAgo = new Date(now); oneMonthAgo.setMonth(now.getMonth() - 1); filteredData = data.filter(d => d.date >= oneMonthAgo); break; case "6m": const sixMonthsAgo = new Date(now); sixMonthsAgo.setMonth(now.getMonth() - 6); filteredData = data.filter(d => d.date >= sixMonthsAgo); break; case "1y": default: filteredData = data; break; } // Update x scale domain x.domain(d3.extent(filteredData, d => d.date)); // Update y scale domain y.domain([0, d3.max(filteredData, d => { const values = Object.entries(d).filter(([key]) => key !== 'date').map(([_, value]) => value); return d3.max(values) * 1.1; })]); // Update axes with animation svg.select(".axis:nth-child(3)") .transition() .duration(1000) .call(d3.axisBottom(x).ticks(5).tickFormat(d3.timeFormat("%b %Y"))); svg.select(".axis:nth-child(4)") .transition() .duration(1000) .call(d3.axisLeft(y).tickFormat(d => `$${(d/1000).toFixed(0)}k`)); // Update grid lines svg.select(".grid:nth-child(1)") .transition() .duration(1000) .call(d3.axisBottom(x).ticks(5).tickSize(-y.range()[0]).tickFormat("")); svg.select(".grid:nth-child(2)") .transition() .duration(1000) .call(d3.axisLeft(y).tickSize(-x.range()[1]).tickFormat("")); // Update area and line paths with animation propertyTypes.forEach((type, i) => { const typeData = filteredData.map(d => ({ date: d.date, value: d[type] })); svg.selectAll(".area-path") .filter((d, j) => j === i) .transition() .duration(1000) .attr("d", area(typeData)); svg.selectAll(".line") .filter((d, j) => j === i) .transition() .duration(1000) .attr("d", line(typeData)); }); } </script> </body> </html>