import { LitElement, html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import * as d3 from 'd3'
@customElement('kpi-radar-chart')
export class KpiRadarChart extends LitElement {
@property({ type: Array }) data: any[] = []
@property({ type: Array }) categories: string[] = []
@property({ type: String }) valueKey: string = 'value'
@property({ type: String }) currentGroup: string = ''
static styles = css`
:host {
display: block;
width: 100%;
height: 100%;
}
svg {
width: 100%;
height: 100%;
display: block;
}
`
private chartWidth = 0
private chartHeight = 0
private resizeObserver?: ResizeObserver
render() {
return html``
}
connectedCallback() {
super.connectedCallback()
this.resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const rect = entry.contentRect
this.chartWidth = rect.width
this.chartHeight = rect.height
this.requestUpdate()
}
})
this.resizeObserver.observe(this)
}
disconnectedCallback() {
this.resizeObserver?.disconnect()
super.disconnectedCallback()
}
updated() {
this.drawRadar()
}
drawRadar() {
const svg = d3.select(this.renderRoot.querySelector('#radar'))
svg.selectAll('*').remove()
const w = this.chartWidth || 300
const h = this.chartHeight || 300
const r = Math.min(w, h) / 2 - 40
const angleSlice = (2 * Math.PI) / (this.categories.length || 1)
// 데이터 변환: { org, values: [ {category, value} ... ] }
const orgData = d3
.groups(this.data, d => d.org)
.map(([org, values]) => ({
org,
values: this.categories.map((cat, i) => {
const found = values.find(v => v.category === cat)
return { category: cat, value: found ? found[this.valueKey] : 0 }
})
}))
// 스케일
const maxValue = d3.max(this.data, d => d[this.valueKey]) || 1
const radius = d3.scaleLinear().domain([0, maxValue]).range([0, r])
// SVG 기본
svg.attr('width', w).attr('height', h)
const g = svg.append('g').attr('transform', `translate(${w / 2},${h / 2})`)
// 그리드/축
for (let i = 1; i <= 5; i++) {
g.append('circle')
.attr('r', (r / 5) * i)
.attr('fill', 'none')
.attr('stroke', '#ccc')
}
this.categories.forEach((cat, i) => {
const angle = i * angleSlice - Math.PI / 2
g.append('line')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', radius(maxValue) * Math.cos(angle))
.attr('y2', radius(maxValue) * Math.sin(angle))
.attr('stroke', '#ccc')
g.append('text')
.attr('x', (radius(maxValue) + 10) * Math.cos(angle))
.attr('y', (radius(maxValue) + 10) * Math.sin(angle))
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('font-size', 12)
.text(cat)
})
// 그룹별 폴리곤
orgData.forEach(gd => {
// 마지막에 첫 점을 한 번 더 추가
const closedValues = [...gd.values, gd.values[0]]
const line = d3
.lineRadial()
.radius((d: any) => radius(d.value))
.angle((d, i) => i * angleSlice)
g.append('path')
.datum(closedValues)
.attr('d', line as any)
.attr('fill', gd.org === this.currentGroup ? 'rgba(33,150,243,0.4)' : 'rgba(200,200,200,0.2)')
.attr('stroke', gd.org === this.currentGroup ? '#2196f3' : '#aaa')
.attr('stroke-width', gd.org === this.currentGroup ? 3 : 1)
})
}
}