import { LitElement, html, css } from 'lit'; import { Chart, registerables } from 'chart.js'; Chart.register(...registerables); // 十字准星 + 拖拽选择插件 const crosshairPlugin = { id: 'crosshair', afterEvent(chart, args) { const event = args.event; if (event.type === 'mousemove') { chart.crosshair = { x: event.x, y: event.y }; } else if (event.type === 'mouseout') { chart.crosshair = null; } }, afterDraw(chart) { const ctx = chart.ctx; const topY = chart.scales.y.top; const bottomY = chart.scales.y.bottom; const leftX = chart.scales.x.left; const rightX = chart.scales.x.right; // 绘制选择区域 if (chart.dragSelect && chart.dragSelect.startX !== null) { const { startX, currentX } = chart.dragSelect; const x1 = Math.max(leftX, Math.min(startX, currentX)); const x2 = Math.min(rightX, Math.max(startX, currentX)); ctx.save(); ctx.fillStyle = 'rgba(255, 193, 7, 0.2)'; ctx.fillRect(x1, topY, x2 - x1, bottomY - topY); ctx.restore(); } // 绘制十字准星 if (!chart.crosshair) return; const { x, y } = chart.crosshair; // 检查鼠标是否在图表区域内 if (x < leftX || x > rightX || y < topY || y > bottomY) return; ctx.save(); ctx.setLineDash([4, 4]); ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; // 垂直线 ctx.beginPath(); ctx.moveTo(x, topY); ctx.lineTo(x, bottomY); ctx.stroke(); // 水平线跟随鼠标 ctx.beginPath(); ctx.moveTo(leftX, y); ctx.lineTo(rightX, y); ctx.stroke(); ctx.restore(); } }; Chart.register(crosshairPlugin); class TasksChart extends LitElement { static properties = { data: { type: Object }, timestamps: { type: Array } }; static styles = css` :host { display: block; width: 100%; height: 100%; } canvas { width: 100% !important; height: 100% !important; } `; constructor() { super(); this.data = { labels: [], datasets: {} }; this.timestamps = []; this.chart = null; this.isDragging = false; } firstUpdated() { this.initChart(); this.setupDragSelect(); } setupDragSelect() { const canvas = this.shadowRoot.querySelector('canvas'); if (!canvas) return; canvas.addEventListener('mousedown', (e) => { if (!this.chart) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const leftX = this.chart.scales.x.left; const rightX = this.chart.scales.x.right; if (x >= leftX && x <= rightX) { this.isDragging = true; this.chart.dragSelect = { startX: x, currentX: x }; this.chart.update('none'); } }); canvas.addEventListener('mousemove', (e) => { if (!this.isDragging || !this.chart) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; this.chart.dragSelect.currentX = x; this.chart.update('none'); }); canvas.addEventListener('mouseup', (e) => { if (!this.isDragging || !this.chart) return; this.isDragging = false; const { startX, currentX } = this.chart.dragSelect; const leftX = this.chart.scales.x.left; const rightX = this.chart.scales.x.right; // 计算选择的时间范围 const x1 = Math.max(leftX, Math.min(startX, currentX)); const x2 = Math.min(rightX, Math.max(startX, currentX)); // 最小选择宽度检查 if (Math.abs(x2 - x1) > 10) { const startIndex = Math.round((x1 - leftX) / (rightX - leftX) * (this.timestamps.length - 1)); const endIndex = Math.round((x2 - leftX) / (rightX - leftX) * (this.timestamps.length - 1)); if (startIndex >= 0 && endIndex < this.timestamps.length) { const startTime = this.timestamps[startIndex]; const endTime = this.timestamps[endIndex]; this.dispatchEvent(new CustomEvent('time-range-select', { detail: { start: startTime, end: endTime }, bubbles: true, composed: true })); } } this.chart.dragSelect = null; this.chart.update('none'); }); canvas.addEventListener('mouseleave', () => { if (this.isDragging && this.chart) { this.isDragging = false; this.chart.dragSelect = null; this.chart.update('none'); } }); } initChart() { const canvas = this.shadowRoot.querySelector('canvas'); if (!canvas) return; const ctx = canvas.getContext('2d'); this.chart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'Active', data: [], borderColor: '#42a5f5', backgroundColor: 'transparent', fill: false, hidden: true }, { label: 'Pending', data: [], borderColor: '#5c6bc0', backgroundColor: 'transparent', fill: false }, { label: 'Scheduled', data: [], borderColor: '#ffa726', backgroundColor: 'transparent', fill: false, hidden: true }, { label: 'Retry', data: [], borderColor: '#ef5350', backgroundColor: 'transparent', fill: false, hidden: true }, { label: 'Archived', data: [], borderColor: '#ab47bc', backgroundColor: 'transparent', fill: false, hidden: true }, { label: 'Completed', data: [], borderColor: '#66bb6a', backgroundColor: 'transparent', fill: false, hidden: true }, { label: 'Succeeded', data: [], borderColor: '#81c784', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true }, { label: 'Failed', data: [], borderColor: '#e57373', backgroundColor: 'transparent', fill: false, borderDash: [5, 5], hidden: true } ] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 0 }, clip: false, interaction: { mode: 'index', intersect: false }, hover: { mode: 'index', intersect: false }, plugins: { legend: { position: 'bottom', labels: { color: '#e0e0e0', padding: 15, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { enabled: true, backgroundColor: 'rgba(30, 30, 30, 0.9)', titleColor: '#e0e0e0', bodyColor: '#e0e0e0', borderColor: '#616161', borderWidth: 1, padding: 10, displayColors: true } }, scales: { x: { grid: { color: '#616161' }, ticks: { color: '#9e9e9e', maxTicksLimit: 10 } }, y: { grid: { color: '#616161' }, ticks: { color: '#9e9e9e' }, beginAtZero: true } }, elements: { point: { radius: 0, hoverRadius: 5, hitRadius: 10, hoverBackgroundColor: (ctx) => ctx.dataset.borderColor, hoverBorderColor: (ctx) => ctx.dataset.borderColor, hoverBorderWidth: 0 }, line: { tension: 0.3, borderWidth: 1.5, spanGaps: false } } } }); this.updateChart(); } updated(changedProperties) { if (changedProperties.has('data') && this.chart) { this.updateChart(); } } updateChart() { if (!this.chart || !this.data) return; this.chart.data.labels = this.data.labels || []; this.chart.data.datasets[0].data = this.data.datasets?.active || []; this.chart.data.datasets[1].data = this.data.datasets?.pending || []; this.chart.data.datasets[2].data = this.data.datasets?.scheduled || []; this.chart.data.datasets[3].data = this.data.datasets?.retry || []; this.chart.data.datasets[4].data = this.data.datasets?.archived || []; this.chart.data.datasets[5].data = this.data.datasets?.completed || []; this.chart.data.datasets[6].data = this.data.datasets?.succeeded || []; this.chart.data.datasets[7].data = this.data.datasets?.failed || []; this.chart.update('none'); } render() { return html``; } } customElements.define('tasks-chart', TasksChart);