Spaces:
Running
Running
| class ChartViz extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.attachShadow({ mode: 'open' }); | |
| } | |
| connectedCallback() { | |
| this.render(); | |
| this.initChart(); | |
| } | |
| render() { | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| </style> | |
| <canvas id="chartCanvas"></canvas> | |
| `; | |
| } | |
| initChart() { | |
| const canvas = this.shadowRoot.getElementById('chartCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const type = this.getAttribute('type') || 'line'; | |
| let width, height; | |
| const resize = () => { | |
| width = canvas.width = canvas.offsetWidth; | |
| height = canvas.height = canvas.offsetHeight; | |
| }; | |
| new ResizeObserver(resize).observe(this); | |
| // Data generation | |
| const generateData = (points) => { | |
| return Array.from({ length: points }, (_, i) => ({ | |
| x: i, | |
| y: Math.random() * 0.5 + 0.25 + Math.sin(i * 0.1) * 0.15 | |
| })); | |
| }; | |
| let data = generateData(type === 'training' ? 100 : 24); | |
| let offset = 0; | |
| const drawLineChart = () => { | |
| ctx.clearRect(0, 0, width, height); | |
| // Grid | |
| ctx.strokeStyle = '#1e293b'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i < 5; i++) { | |
| const y = (height / 4) * i; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, y); | |
| ctx.lineTo(width, y); | |
| ctx.stroke(); | |
| } | |
| // Line | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#10b981'; | |
| ctx.lineWidth = 2; | |
| data.forEach((point, i) => { | |
| const x = (i / (data.length - 1)) * width; | |
| const y = height - (point.y * height * 0.8 + height * 0.1); | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.stroke(); | |
| // Gradient fill | |
| const gradient = ctx.createLinearGradient(0, 0, 0, height); | |
| gradient.addColorStop(0, 'rgba(16, 185, 129, 0.3)'); | |
| gradient.addColorStop(1, 'rgba(16, 185, 129, 0)'); | |
| ctx.lineTo(width, height); | |
| ctx.lineTo(0, height); | |
| ctx.fillStyle = gradient; | |
| ctx.fill(); | |
| // Points | |
| data.forEach((point, i) => { | |
| if (i % 4 === 0) { | |
| const x = (i / (data.length - 1)) * width; | |
| const y = height - (point.y * height * 0.8 + height * 0.1); | |
| ctx.beginPath(); | |
| ctx.arc(x, y, 4, 0, Math.PI * 2); | |
| ctx.fillStyle = '#10b981'; | |
| ctx.fill(); | |
| } | |
| }); | |
| }; | |
| const drawBarChart = () => { | |
| ctx.clearRect(0, 0, width, height); | |
| const barWidth = width / data.length - 4; | |
| data.forEach((point, i) => { | |
| const x = i * (width / data.length) + 2; | |
| const barHeight = point.y * height * 0.8; | |
| const y = height - barHeight; | |
| // Color based on value | |
| const hue = 120 + (1 - point.y) * 60; // Green to yellow | |
| ctx.fillStyle = `hsl(${hue}, 70%, 50%)`; | |
| // Rounded top bars | |
| ctx.beginPath(); | |
| ctx.roundRect(x, y, barWidth, barHeight, [4, 4, 0, 0]); | |
| ctx.fill(); | |
| }); | |
| }; | |
| const drawTrainingChart = () => { | |
| ctx.clearRect(0, 0, width, height); | |
| // Loss line (decreasing) | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#f97316'; | |
| ctx.lineWidth = 2; | |
| data.forEach((point, i) => { | |
| const x = (i / (data.length - 1)) * width; | |
| const normalizedLoss = 1 - (i / data.length) * 0.8 + Math.random() * 0.05; | |
| const y = height - (normalizedLoss * height * 0.7 + height * 0.15); | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.stroke(); | |
| // Accuracy line (increasing) | |
| ctx.beginPath(); | |
| ctx.strokeStyle = '#3b82f6'; | |
| ctx.lineWidth = 2; | |
| data.forEach((point, i) => { | |
| const x = (i / (data.length - 1)) * width; | |
| const normalizedAcc = 0.5 + (i / data.length) * 0.48 + Math.random() * 0.02; | |
| const y = height - (normalizedAcc * height * 0.7 + height * 0.15); | |
| if (i === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| }); | |
| ctx.stroke(); | |
| // Legend | |
| ctx.fillStyle = '#f97316'; | |
| ctx.fillRect(10, 10, 12, 12); | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.font = '11px sans-serif'; | |
| ctx.fillText('Loss', 28, 20); | |
| ctx.fillStyle = '#3b82f6'; | |
| ctx.fillRect(80, 10, 12, 12); | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.fillText('Accuracy', 98, 20); | |
| }; | |
| const animate = () => { | |
| if (type === 'training') { | |
| // Update data occasionally | |
| offset++; | |
| if (offset % 60 === 0) { | |
| data = data.map((point, i) => ({ | |
| ...point, | |
| y: Math.random() * 0.5 + 0.25 + Math.sin(i * 0.1 + offset * 0.01) * 0.15 | |
| })); | |
| } | |
| } else if (type === 'line') { | |
| // Shift data for real-time effect | |
| if (offset % 10 === 0) { | |
| data.shift(); | |
| data.push({ | |
| x: data.length, | |
| y: Math.random() * 0.5 + 0.25 + Math.sin(offset * 0.1) * 0.15 | |
| }); | |
| } | |
| offset++; | |
| } | |
| if (type === 'line' || type === 'training') { | |
| type === 'training' ? drawTrainingChart() : drawLineChart(); | |
| } else { | |
| drawBarChart(); | |
| } | |
| requestAnimationFrame(animate); | |
| }; | |
| resize(); | |
| animate(); | |
| } | |
| } | |
| customElements.define('chart-viz', ChartViz); |