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') } } }