@things-factory/kpi 9.0.30 → 9.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/charts/kpi-mini-trend-chart.ts +125 -0
- package/client/charts/kpi-trend-chart.ts +163 -0
- package/client/google-map/common-google-map.ts +45 -7
- package/client/google-map/google-map-loader.ts +1 -1
- package/client/pages/kpi-dashboard/components/kpi-chart-toggle.ts +1 -2
- package/client/pages/kpi-dashboard/components/kpi-left-panel.ts +399 -0
- package/client/pages/kpi-dashboard/components/kpi-map-panel.ts +110 -30
- package/client/pages/kpi-dashboard/components/kpi-region-popup.ts +355 -0
- package/client/pages/kpi-dashboard/kpi-dashboard-map.ts +46 -589
- package/dist-client/charts/kpi-mini-trend-chart.d.ts +14 -0
- package/dist-client/charts/kpi-mini-trend-chart.js +148 -0
- package/dist-client/charts/kpi-mini-trend-chart.js.map +1 -0
- package/dist-client/charts/kpi-trend-chart.d.ts +25 -0
- package/dist-client/charts/kpi-trend-chart.js +186 -0
- package/dist-client/charts/kpi-trend-chart.js.map +1 -0
- package/dist-client/google-map/common-google-map.js +40 -7
- package/dist-client/google-map/common-google-map.js.map +1 -1
- package/dist-client/google-map/google-map-loader.js +1 -1
- package/dist-client/google-map/google-map-loader.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js +1 -2
- package/dist-client/pages/kpi-dashboard/components/kpi-chart-toggle.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.d.ts +22 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js +404 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-left-panel.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.d.ts +6 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js +100 -25
- package/dist-client/pages/kpi-dashboard/components/kpi-map-panel.js.map +1 -1
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.d.ts +23 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js +368 -0
- package/dist-client/pages/kpi-dashboard/components/kpi-region-popup.js.map +1 -0
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.d.ts +6 -15
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js +43 -585
- package/dist-client/pages/kpi-dashboard/kpi-dashboard-map.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/client/google-map/script-loader.ts +0 -173
- package/dist-client/google-map/script-loader.d.ts +0 -3
- package/dist-client/google-map/script-loader.js +0 -144
- package/dist-client/google-map/script-loader.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
|
|
5
|
+
@customElement('kpi-mini-trend-chart')
|
|
6
|
+
export class KpiMiniTrendChart extends LitElement {
|
|
7
|
+
@property({ type: Array }) data: number[] = []
|
|
8
|
+
@property({ type: Number }) width: number = 60
|
|
9
|
+
@property({ type: Number }) height: number = 30
|
|
10
|
+
@property({ type: String }) lineColor: string = '#2196f3'
|
|
11
|
+
@property({ type: Number }) strokeWidth: number = 1.5
|
|
12
|
+
@property({ type: Boolean }) showPoints: boolean = true
|
|
13
|
+
@property({ type: Number }) pointRadius: number = 1.5
|
|
14
|
+
|
|
15
|
+
static styles = css`
|
|
16
|
+
:host {
|
|
17
|
+
display: block;
|
|
18
|
+
width: 100%;
|
|
19
|
+
height: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.mini-chart {
|
|
23
|
+
width: 100%;
|
|
24
|
+
height: 100%;
|
|
25
|
+
background: #f8f9fa;
|
|
26
|
+
border-radius: 4px;
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.trend-line {
|
|
33
|
+
fill: none;
|
|
34
|
+
stroke-linecap: round;
|
|
35
|
+
stroke-linejoin: round;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.data-point {
|
|
39
|
+
fill: white;
|
|
40
|
+
stroke-width: 1;
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
return html`
|
|
46
|
+
<div class="mini-chart">
|
|
47
|
+
<svg
|
|
48
|
+
id="mini-trend"
|
|
49
|
+
width=${this.width}
|
|
50
|
+
height=${this.height}
|
|
51
|
+
viewBox="0 0 ${this.width} ${this.height}"
|
|
52
|
+
preserveAspectRatio="xMidYMid meet"
|
|
53
|
+
></svg>
|
|
54
|
+
</div>
|
|
55
|
+
`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
updated() {
|
|
59
|
+
this.drawMiniTrend()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
drawMiniTrend() {
|
|
63
|
+
if (!this.data || this.data.length === 0) return
|
|
64
|
+
|
|
65
|
+
const svg = d3.select(this.renderRoot.querySelector('#mini-trend'))
|
|
66
|
+
svg.selectAll('*').remove()
|
|
67
|
+
|
|
68
|
+
const margin = { top: 2, right: 2, bottom: 2, left: 2 }
|
|
69
|
+
const width = this.width - margin.left - margin.right
|
|
70
|
+
const height = this.height - margin.top - margin.bottom
|
|
71
|
+
|
|
72
|
+
// 스케일 설정
|
|
73
|
+
const xScale = d3
|
|
74
|
+
.scaleLinear()
|
|
75
|
+
.domain([0, this.data.length - 1])
|
|
76
|
+
.range([0, width])
|
|
77
|
+
|
|
78
|
+
const yScale = d3
|
|
79
|
+
.scaleLinear()
|
|
80
|
+
.domain([0, d3.max(this.data) || 100])
|
|
81
|
+
.range([height, 0])
|
|
82
|
+
|
|
83
|
+
// SVG 설정
|
|
84
|
+
svg.attr('width', this.width).attr('height', this.height)
|
|
85
|
+
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
|
|
86
|
+
|
|
87
|
+
// 라인 생성기
|
|
88
|
+
const line = d3
|
|
89
|
+
.line<number>()
|
|
90
|
+
.x((d, i) => xScale(i))
|
|
91
|
+
.y(d => yScale(d))
|
|
92
|
+
.curve(d3.curveMonotoneX)
|
|
93
|
+
|
|
94
|
+
// 트렌드 라인 그리기
|
|
95
|
+
g.append('path')
|
|
96
|
+
.datum(this.data)
|
|
97
|
+
.attr('class', 'trend-line')
|
|
98
|
+
.attr('d', line as any)
|
|
99
|
+
.attr('stroke', this.lineColor)
|
|
100
|
+
.attr('stroke-width', this.strokeWidth)
|
|
101
|
+
|
|
102
|
+
// 데이터 포인트 그리기 (첫 번째와 마지막 포인트만)
|
|
103
|
+
if (this.showPoints && this.data.length > 0) {
|
|
104
|
+
// 첫 번째 포인트
|
|
105
|
+
g.append('circle')
|
|
106
|
+
.attr('class', 'data-point')
|
|
107
|
+
.attr('cx', xScale(0))
|
|
108
|
+
.attr('cy', yScale(this.data[0]))
|
|
109
|
+
.attr('r', this.pointRadius)
|
|
110
|
+
.attr('stroke', '#4caf50')
|
|
111
|
+
.attr('fill', 'white')
|
|
112
|
+
|
|
113
|
+
// 마지막 포인트
|
|
114
|
+
if (this.data.length > 1) {
|
|
115
|
+
g.append('circle')
|
|
116
|
+
.attr('class', 'data-point')
|
|
117
|
+
.attr('cx', xScale(this.data.length - 1))
|
|
118
|
+
.attr('cy', yScale(this.data[this.data.length - 1]))
|
|
119
|
+
.attr('r', this.pointRadius)
|
|
120
|
+
.attr('stroke', '#2196f3')
|
|
121
|
+
.attr('fill', 'white')
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
|
|
5
|
+
@customElement('kpi-trend-chart')
|
|
6
|
+
export class KpiTrendChart extends LitElement {
|
|
7
|
+
@property({ type: Array }) data: { date: string; value: number; color?: string }[] = []
|
|
8
|
+
@property({ type: String }) valueKey: string = 'value'
|
|
9
|
+
@property({ type: String }) dateKey: string = 'date'
|
|
10
|
+
@property({ type: Number }) width: number = 400
|
|
11
|
+
@property({ type: Number }) height: number = 200
|
|
12
|
+
@property({ type: String }) lineColor: string = '#2196f3'
|
|
13
|
+
@property({ type: Number }) strokeWidth: number = 2
|
|
14
|
+
@property({ type: Boolean }) showPoints: boolean = true
|
|
15
|
+
@property({ type: Number }) pointRadius: number = 4
|
|
16
|
+
|
|
17
|
+
private chartWidth = 0
|
|
18
|
+
private chartHeight = 0
|
|
19
|
+
private resizeObserver?: ResizeObserver
|
|
20
|
+
|
|
21
|
+
static styles = css`
|
|
22
|
+
:host {
|
|
23
|
+
display: block;
|
|
24
|
+
width: 100%;
|
|
25
|
+
height: 100%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.chart-container {
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.trend-line {
|
|
34
|
+
fill: none;
|
|
35
|
+
stroke-linecap: round;
|
|
36
|
+
stroke-linejoin: round;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.data-point {
|
|
40
|
+
fill: white;
|
|
41
|
+
stroke-width: 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.axis line,
|
|
45
|
+
.axis path {
|
|
46
|
+
stroke: #ddd;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.axis text {
|
|
50
|
+
font-size: 10px;
|
|
51
|
+
fill: #666;
|
|
52
|
+
}
|
|
53
|
+
`
|
|
54
|
+
|
|
55
|
+
render() {
|
|
56
|
+
return html`
|
|
57
|
+
<div class="chart-container">
|
|
58
|
+
<svg
|
|
59
|
+
id="trend-chart"
|
|
60
|
+
width=${this.chartWidth || this.width}
|
|
61
|
+
height=${this.chartHeight || this.height}
|
|
62
|
+
viewBox="0 0 ${this.chartWidth || this.width} ${this.chartHeight || this.height}"
|
|
63
|
+
preserveAspectRatio="xMidYMid meet"
|
|
64
|
+
></svg>
|
|
65
|
+
</div>
|
|
66
|
+
`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
connectedCallback() {
|
|
70
|
+
super.connectedCallback()
|
|
71
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const rect = entry.contentRect
|
|
74
|
+
this.chartWidth = rect.width
|
|
75
|
+
this.chartHeight = rect.height
|
|
76
|
+
this.requestUpdate()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
this.resizeObserver.observe(this)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
disconnectedCallback() {
|
|
83
|
+
this.resizeObserver?.disconnect()
|
|
84
|
+
super.disconnectedCallback()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
updated() {
|
|
88
|
+
this.drawTrendChart()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
drawTrendChart() {
|
|
92
|
+
if (!this.data || this.data.length === 0) return
|
|
93
|
+
|
|
94
|
+
const svg = d3.select(this.renderRoot.querySelector('#trend-chart'))
|
|
95
|
+
svg.selectAll('*').remove()
|
|
96
|
+
|
|
97
|
+
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
|
|
98
|
+
const width = this.chartWidth || this.width
|
|
99
|
+
const height = this.chartHeight || this.height
|
|
100
|
+
const chartWidth = width - margin.left - margin.right
|
|
101
|
+
const chartHeight = height - margin.top - margin.bottom
|
|
102
|
+
|
|
103
|
+
// 데이터 파싱
|
|
104
|
+
const parsedData = this.data.map(d => ({
|
|
105
|
+
date: new Date(d[this.dateKey]),
|
|
106
|
+
value: +d[this.valueKey],
|
|
107
|
+
color: d.color || this.lineColor
|
|
108
|
+
}))
|
|
109
|
+
|
|
110
|
+
// 스케일 설정
|
|
111
|
+
const xScale = d3
|
|
112
|
+
.scaleTime()
|
|
113
|
+
.domain(d3.extent(parsedData, d => d.date) as [Date, Date])
|
|
114
|
+
.range([0, chartWidth])
|
|
115
|
+
|
|
116
|
+
const yScale = d3
|
|
117
|
+
.scaleLinear()
|
|
118
|
+
.domain([0, d3.max(parsedData, d => d.value) || 100])
|
|
119
|
+
.range([chartHeight, 0])
|
|
120
|
+
|
|
121
|
+
// SVG 설정
|
|
122
|
+
svg.attr('width', width).attr('height', height)
|
|
123
|
+
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`)
|
|
124
|
+
|
|
125
|
+
// 축 생성
|
|
126
|
+
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat('%m/%d')).ticks(5)
|
|
127
|
+
|
|
128
|
+
const yAxis = d3.axisLeft(yScale).ticks(5)
|
|
129
|
+
|
|
130
|
+
g.append('g').attr('class', 'axis').attr('transform', `translate(0,${chartHeight})`).call(xAxis)
|
|
131
|
+
|
|
132
|
+
g.append('g').attr('class', 'axis').call(yAxis)
|
|
133
|
+
|
|
134
|
+
// 라인 생성기
|
|
135
|
+
const line = d3
|
|
136
|
+
.line<{ date: Date; value: number; color: string }>()
|
|
137
|
+
.x(d => xScale(d.date))
|
|
138
|
+
.y(d => yScale(d.value))
|
|
139
|
+
.curve(d3.curveMonotoneX)
|
|
140
|
+
|
|
141
|
+
// 트렌드 라인 그리기
|
|
142
|
+
g.append('path')
|
|
143
|
+
.datum(parsedData)
|
|
144
|
+
.attr('class', 'trend-line')
|
|
145
|
+
.attr('d', line as any)
|
|
146
|
+
.attr('stroke', this.lineColor)
|
|
147
|
+
.attr('stroke-width', this.strokeWidth)
|
|
148
|
+
|
|
149
|
+
// 데이터 포인트 그리기
|
|
150
|
+
if (this.showPoints) {
|
|
151
|
+
g.selectAll('.data-point')
|
|
152
|
+
.data(parsedData)
|
|
153
|
+
.enter()
|
|
154
|
+
.append('circle')
|
|
155
|
+
.attr('class', 'data-point')
|
|
156
|
+
.attr('cx', d => xScale(d.date))
|
|
157
|
+
.attr('cy', d => yScale(d.value))
|
|
158
|
+
.attr('r', this.pointRadius)
|
|
159
|
+
.attr('stroke', d => d.color)
|
|
160
|
+
.attr('fill', 'white')
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -109,11 +109,18 @@ export class CommonGoogleMap extends LitElement {
|
|
|
109
109
|
// Google Maps 최신 API 사용
|
|
110
110
|
const { Map } = (await google.maps.importLibrary('maps')) as google.maps.MapsLibrary
|
|
111
111
|
|
|
112
|
-
const
|
|
112
|
+
const mapOptions = {
|
|
113
113
|
zoom,
|
|
114
114
|
center,
|
|
115
115
|
mapId: 'DEMO_MAP_ID'
|
|
116
|
-
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// controls 속성이 있으면 지도 옵션에 추가
|
|
119
|
+
if (this.controls) {
|
|
120
|
+
Object.assign(mapOptions, this.controls)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const map = new Map(this.anchor, mapOptions)
|
|
117
124
|
|
|
118
125
|
this.markers && this.markers.forEach(marker => marker.setMap(map))
|
|
119
126
|
|
|
@@ -192,17 +199,40 @@ export class CommonGoogleMap extends LitElement {
|
|
|
192
199
|
// LatLng 객체 생성
|
|
193
200
|
const position = new google.maps.LatLng(lat, lng)
|
|
194
201
|
|
|
202
|
+
// 커스텀 마커 콘텐츠가 있으면 사용
|
|
203
|
+
let markerElement
|
|
204
|
+
if (location.markerContent) {
|
|
205
|
+
// HTML 문자열을 DOM 요소로 변환
|
|
206
|
+
const tempDiv = document.createElement('div')
|
|
207
|
+
tempDiv.innerHTML = location.markerContent
|
|
208
|
+
markerElement = tempDiv.firstElementChild
|
|
209
|
+
} else {
|
|
210
|
+
// 기본 핀 사용
|
|
211
|
+
markerElement = new PinElement({
|
|
212
|
+
background: '#1976d2',
|
|
213
|
+
borderColor: '#1565c0',
|
|
214
|
+
glyphColor: '#ffffff',
|
|
215
|
+
scale: 1.2
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
|
|
195
219
|
// AdvancedMarkerElement 사용
|
|
196
220
|
const marker = new AdvancedMarkerElement({
|
|
197
221
|
position: position,
|
|
198
|
-
map: null // 클러스터에서 관리하므로 지도에 직접 추가하지 않음
|
|
222
|
+
map: null, // 클러스터에서 관리하므로 지도에 직접 추가하지 않음
|
|
223
|
+
content: markerElement
|
|
199
224
|
})
|
|
200
225
|
|
|
201
226
|
marker.addListener('click', () => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
227
|
+
// InfoWindow 대신 커스텀 이벤트 발생
|
|
228
|
+
if (location?.region) {
|
|
229
|
+
this.dispatchEvent(
|
|
230
|
+
new CustomEvent('region-click', {
|
|
231
|
+
detail: { region: location.region },
|
|
232
|
+
bubbles: true,
|
|
233
|
+
composed: true
|
|
234
|
+
})
|
|
235
|
+
)
|
|
206
236
|
}
|
|
207
237
|
})
|
|
208
238
|
|
|
@@ -291,6 +321,14 @@ export class CommonGoogleMap extends LitElement {
|
|
|
291
321
|
this.map.setCenter(this.center)
|
|
292
322
|
}
|
|
293
323
|
|
|
324
|
+
if (changes.has('controls')) {
|
|
325
|
+
// controls가 변경되면 기존 지도의 옵션만 업데이트
|
|
326
|
+
if (this.map && this.controls) {
|
|
327
|
+
// Google Maps API의 setOptions 메서드 사용
|
|
328
|
+
this.map.setOptions(this.controls)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
294
332
|
if (changes.has('polygons')) {
|
|
295
333
|
;(changes.get('polygons') || []).forEach(geofence => geofence.setMap(null))
|
|
296
334
|
;(this.polygons || []).forEach(geofence => geofence.setMap(this.map))
|