| class CustomChart extends HTMLElement { |
| constructor() { |
| super(); |
| } |
|
|
| connectedCallback() { |
| this.attachShadow({ mode: 'open' }); |
| |
| const type = this.getAttribute('type') || 'line'; |
| const data = (this.getAttribute('data') || '').split(',').map(Number); |
| const labels = (this.getAttribute('labels') || '').split(','); |
| |
| const max = Math.max(...data); |
| const min = Math.min(...data); |
| const range = max - min || 1; |
| |
| const width = 600; |
| const height = 200; |
| const padding = 30; |
| |
| |
| const points = data.map((value, index) => { |
| const x = padding + (index / (data.length - 1)) * (width - 2 * padding); |
| const y = height - padding - ((value - min) / range) * (height - 2 * padding); |
| return `${x},${y}`; |
| }).join(' '); |
| |
| |
| let gridLines = ''; |
| for (let i = 0; i <= 4; i++) { |
| const y = padding + (i / 4) * (height - 2 * padding); |
| gridLines += `<line x1="${padding}" y1="${y}" x2="${width - padding}" y2="${y}" class="chart-grid"/>`; |
| } |
| |
| |
| let pointCircles = ''; |
| data.forEach((value, index) => { |
| const x = padding + (index / (data.length - 1)) * (width - 2 * padding); |
| const y = height - padding - ((value - min) / range) * (height - 2 * padding); |
| pointCircles += `<circle cx="${x}" cy="${y}" class="chart-point" data-value="${value}"/>`; |
| }); |
| |
| |
| let xLabels = ''; |
| labels.forEach((label, index) => { |
| const x = padding + (index / (labels.length - 1)) * (width - 2 * padding); |
| xLabels += `<text x="${x}" y="${height - 8}" class="chart-label" text-anchor="middle">${label}</text>`; |
| }); |
| |
| |
| let yLabels = ''; |
| for (let i = 0; i <= 4; i++) { |
| const y = padding + (i / 4) * (height - 2 * padding); |
| const value = Math.round(max - (i / 4) * range); |
| yLabels += `<text x="${padding - 8}" y="${y + 4}" class="chart-label" text-anchor="end">${value}</text>`; |
| } |
| |
| this.shadowRoot.innerHTML = ` |
| <style> |
| :host { |
| display: block; |
| width: 100%; |
| } |
| svg { |
| width: 100%; |
| height: 100%; |
| overflow: visible; |
| } |
| .chart-line { |
| fill: none; |
| stroke: #22c55e; |
| stroke-width: 2; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| filter: drop-shadow(0 0 4px rgba(34, 197, 94, 0.5)); |
| } |
| .chart-grid { |
| stroke: #1f2937; |
| stroke-width: 1; |
| stroke-dasharray: 4 4; |
| } |
| .chart-label { |
| fill: #6b7280; |
| font-size: 10px; |
| font-family: monospace; |
| } |
| .chart-point { |
| fill: #0a0a0a; |
| stroke: #22c55e; |
| stroke-width: 2; |
| r: 4; |
| transition: all 0.2s; |
| cursor: pointer; |
| } |
| .chart-point:hover { |
| r: 6; |
| fill: #22c55e; |
| } |
| .tooltip { |
| position: absolute; |
| background: #1f2937; |
| color: #fff; |
| padding: 4px 8px; |
| border-radius: 4px; |
| font-size: 12px; |
| pointer-events: none; |
| opacity: 0; |
| transition: opacity 0.2s; |
| } |
| </style> |
| <div style="height: 200px; position: relative;"> |
| <svg viewBox="0 0 ${width} ${height}"> |
| ${gridLines} |
| <polyline points="${points}" class="chart-line"/> |
| ${pointCircles} |
| ${xLabels} |
| ${yLabels} |
| </svg> |
| </div> |
| `; |
| |
| |
| const points = this.shadowRoot.querySelectorAll('.chart-point'); |
| points.forEach(point => { |
| point.addEventListener('mouseenter', (e) => { |
| const tooltip = document.createElement('div'); |
| tooltip.className = 'tooltip'; |
| tooltip.textContent = `Value: ${e.target.dataset.value}`; |
| tooltip.style.left = `${e.target.cx.baseVal.value + 10}px`; |
| tooltip.style.top = `${e.target.cy.baseVal.value - 30}px`; |
| this.shadowRoot.appendChild(tooltip); |
| setTimeout(() => tooltip.style.opacity = '1', 10); |
| }); |
| |
| point.addEventListener('mouseleave', () => { |
| const tooltip = this.shadowRoot.querySelector('.tooltip'); |
| if (tooltip) tooltip.remove(); |
| }); |
| }); |
| } |
| } |
|
|
| customElements.define('custom-chart', CustomChart); |