import { LitElement, html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import * as d3 from 'd3'
@customElement('kpi-trend-chart')
export class KpiTrendChart extends LitElement {
@property({ type: Array }) data: { date: string; value: number; color?: string }[] = []
@property({ type: String }) valueKey: string = 'value'
@property({ type: String }) dateKey: string = 'date'
@property({ type: Number }) width: number = 400
@property({ type: Number }) height: number = 200
@property({ type: String }) lineColor: string = '#2196f3'
@property({ type: Number }) strokeWidth: number = 2
@property({ type: Boolean }) showPoints: boolean = true
@property({ type: Number }) pointRadius: number = 4
private chartWidth = 0
private chartHeight = 0
private resizeObserver?: ResizeObserver
static styles = css`
:host {
display: block;
width: 100%;
height: 100%;
}
.chart-container {
width: 100%;
height: 100%;
}
.trend-line {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.data-point {
fill: white;
stroke-width: 2;
}
.axis line,
.axis path {
stroke: #ddd;
}
.axis text {
font-size: 10px;
fill: #666;
}
`
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.drawTrendChart()
}
drawTrendChart() {
if (!this.data || this.data.length === 0) return
const svg = d3.select(this.renderRoot.querySelector('#trend-chart'))
svg.selectAll('*').remove()
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
const width = this.chartWidth || this.width
const height = this.chartHeight || this.height
const chartWidth = width - margin.left - margin.right
const chartHeight = height - margin.top - margin.bottom
// 데이터 파싱
const parsedData = this.data.map(d => ({
date: new Date(d[this.dateKey]),
value: +d[this.valueKey],
color: d.color || this.lineColor
}))
// 스케일 설정
const xScale = d3
.scaleTime()
.domain(d3.extent(parsedData, d => d.date) as [Date, Date])
.range([0, chartWidth])
const yScale = d3
.scaleLinear()
.domain([0, d3.max(parsedData, d => d.value) || 100])
.range([chartHeight, 0])
// SVG 설정
svg.attr('width', width).attr('height', height)
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
// 축 생성
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat('%m/%d')).ticks(5)
const yAxis = d3.axisLeft(yScale).ticks(5)
g.append('g').attr('class', 'axis').attr('transform', `translate(0,${chartHeight})`).call(xAxis)
g.append('g').attr('class', 'axis').call(yAxis)
// 라인 생성기
const line = d3
.line<{ date: Date; value: number; color: string }>()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX)
// 트렌드 라인 그리기
g.append('path')
.datum(parsedData)
.attr('class', 'trend-line')
.attr('d', line as any)
.attr('stroke', this.lineColor)
.attr('stroke-width', this.strokeWidth)
// 데이터 포인트 그리기
if (this.showPoints) {
g.selectAll('.data-point')
.data(parsedData)
.enter()
.append('circle')
.attr('class', 'data-point')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.value))
.attr('r', this.pointRadius)
.attr('stroke', d => d.color)
.attr('fill', 'white')
}
}
}