@things-factory/kpi 9.0.21 → 9.0.22

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.
@@ -0,0 +1,198 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ import * as d3 from 'd3';
5
+ let KpiBoxplotChart = class KpiBoxplotChart extends LitElement {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.data = [];
9
+ this.groups = [];
10
+ this.minKey = 'min';
11
+ this.maxKey = 'max';
12
+ this.meanKey = 'mean';
13
+ this.medianKey = 'median';
14
+ this.q1Key = 'q1';
15
+ this.q3Key = 'q3';
16
+ this.valueKey = 'value';
17
+ this.currentGroup = '';
18
+ this.chartWidth = 0;
19
+ this.chartHeight = 0;
20
+ }
21
+ static { this.styles = css `
22
+ :host {
23
+ display: block;
24
+ width: 100%;
25
+ height: 100%;
26
+ }
27
+ svg {
28
+ width: 100%;
29
+ height: 100%;
30
+ display: block;
31
+ }
32
+ `; }
33
+ render() {
34
+ return html `<svg
35
+ id="boxplot"
36
+ width=${this.chartWidth}
37
+ height=${this.chartHeight}
38
+ viewBox="0 0 ${this.chartWidth} ${this.chartHeight}"
39
+ preserveAspectRatio="xMidYMid meet"
40
+ ></svg>`;
41
+ }
42
+ connectedCallback() {
43
+ super.connectedCallback();
44
+ this.resizeObserver = new ResizeObserver(entries => {
45
+ for (const entry of entries) {
46
+ const rect = entry.contentRect;
47
+ this.chartWidth = rect.width;
48
+ this.chartHeight = rect.height;
49
+ this.requestUpdate();
50
+ }
51
+ });
52
+ this.resizeObserver.observe(this);
53
+ }
54
+ disconnectedCallback() {
55
+ this.resizeObserver?.disconnect();
56
+ super.disconnectedCallback();
57
+ }
58
+ updated() {
59
+ this.drawBoxplot();
60
+ }
61
+ drawBoxplot() {
62
+ const svg = d3.select(this.renderRoot.querySelector('#boxplot'));
63
+ svg.selectAll('*').remove();
64
+ const w = this.chartWidth || 300;
65
+ const h = this.chartHeight || 300;
66
+ const margin = { top: 20, right: 20, bottom: 40, left: 40 };
67
+ const plotW = w - margin.left - margin.right;
68
+ const plotH = h - margin.top - margin.bottom;
69
+ // x축: 그룹
70
+ const x = d3.scaleBand().domain(this.groups).range([0, plotW]).padding(0.4);
71
+ // y축: 값
72
+ const allValues = this.data.flatMap(d => [
73
+ d[this.minKey],
74
+ d[this.maxKey],
75
+ d[this.q1Key],
76
+ d[this.q3Key],
77
+ d[this.medianKey],
78
+ d[this.meanKey],
79
+ d[this.valueKey]
80
+ ]);
81
+ const y = d3
82
+ .scaleLinear()
83
+ .domain([d3.min(allValues) ?? 0, d3.max(allValues) ?? 1])
84
+ .nice()
85
+ .range([plotH, 0]);
86
+ const g = svg
87
+ .attr('width', w)
88
+ .attr('height', h)
89
+ .append('g')
90
+ .attr('transform', `translate(${margin.left},${margin.top})`);
91
+ // 축
92
+ g.append('g').call(d3.axisLeft(y));
93
+ g.append('g').attr('transform', `translate(0,${plotH})`).call(d3.axisBottom(x));
94
+ // 박스플롯
95
+ this.data.forEach(d => {
96
+ const gx = x(d.group) ?? 0;
97
+ // 박스
98
+ g.append('rect')
99
+ .attr('x', gx)
100
+ .attr('y', y(d[this.q3Key]))
101
+ .attr('width', x.bandwidth())
102
+ .attr('height', y(d[this.q1Key]) - y(d[this.q3Key]))
103
+ .attr('fill', d.group === this.currentGroup ? '#2196f3' : '#bbb')
104
+ .attr('opacity', 0.5);
105
+ // 중앙선(중앙값)
106
+ g.append('line')
107
+ .attr('x1', gx)
108
+ .attr('x2', gx + x.bandwidth())
109
+ .attr('y1', y(d[this.medianKey]))
110
+ .attr('y2', y(d[this.medianKey]))
111
+ .attr('stroke', '#333')
112
+ .attr('stroke-width', 2);
113
+ // 수염(min-max)
114
+ g.append('line')
115
+ .attr('x1', gx + x.bandwidth() / 2)
116
+ .attr('x2', gx + x.bandwidth() / 2)
117
+ .attr('y1', y(d[this.minKey]))
118
+ .attr('y2', y(d[this.maxKey]))
119
+ .attr('stroke', '#333');
120
+ // min/max
121
+ g.append('line')
122
+ .attr('x1', gx + x.bandwidth() / 4)
123
+ .attr('x2', gx + (x.bandwidth() * 3) / 4)
124
+ .attr('y1', y(d[this.minKey]))
125
+ .attr('y2', y(d[this.minKey]))
126
+ .attr('stroke', '#333');
127
+ g.append('line')
128
+ .attr('x1', gx + x.bandwidth() / 4)
129
+ .attr('x2', gx + (x.bandwidth() * 3) / 4)
130
+ .attr('y1', y(d[this.maxKey]))
131
+ .attr('y2', y(d[this.maxKey]))
132
+ .attr('stroke', '#333');
133
+ // 평균값
134
+ g.append('circle')
135
+ .attr('cx', gx + x.bandwidth() / 2)
136
+ .attr('cy', y(d[this.meanKey]))
137
+ .attr('r', 4)
138
+ .attr('fill', 'orange');
139
+ });
140
+ // 현재 그룹 값 강조
141
+ this.data.forEach(d => {
142
+ if (d.group === this.currentGroup) {
143
+ g.append('circle')
144
+ .attr('cx', x(d.group) + x.bandwidth() / 2)
145
+ .attr('cy', y(d[this.valueKey]))
146
+ .attr('r', 6)
147
+ .attr('fill', '#e91e63')
148
+ .attr('stroke', '#fff')
149
+ .attr('stroke-width', 2);
150
+ }
151
+ });
152
+ }
153
+ };
154
+ __decorate([
155
+ property({ type: Array }),
156
+ __metadata("design:type", Array)
157
+ ], KpiBoxplotChart.prototype, "data", void 0);
158
+ __decorate([
159
+ property({ type: Array }),
160
+ __metadata("design:type", Array)
161
+ ], KpiBoxplotChart.prototype, "groups", void 0);
162
+ __decorate([
163
+ property({ type: String }),
164
+ __metadata("design:type", String)
165
+ ], KpiBoxplotChart.prototype, "minKey", void 0);
166
+ __decorate([
167
+ property({ type: String }),
168
+ __metadata("design:type", String)
169
+ ], KpiBoxplotChart.prototype, "maxKey", void 0);
170
+ __decorate([
171
+ property({ type: String }),
172
+ __metadata("design:type", String)
173
+ ], KpiBoxplotChart.prototype, "meanKey", void 0);
174
+ __decorate([
175
+ property({ type: String }),
176
+ __metadata("design:type", String)
177
+ ], KpiBoxplotChart.prototype, "medianKey", void 0);
178
+ __decorate([
179
+ property({ type: String }),
180
+ __metadata("design:type", String)
181
+ ], KpiBoxplotChart.prototype, "q1Key", void 0);
182
+ __decorate([
183
+ property({ type: String }),
184
+ __metadata("design:type", String)
185
+ ], KpiBoxplotChart.prototype, "q3Key", void 0);
186
+ __decorate([
187
+ property({ type: String }),
188
+ __metadata("design:type", String)
189
+ ], KpiBoxplotChart.prototype, "valueKey", void 0);
190
+ __decorate([
191
+ property({ type: String }),
192
+ __metadata("design:type", String)
193
+ ], KpiBoxplotChart.prototype, "currentGroup", void 0);
194
+ KpiBoxplotChart = __decorate([
195
+ customElement('kpi-boxplot-chart')
196
+ ], KpiBoxplotChart);
197
+ export { KpiBoxplotChart };
198
+ //# sourceMappingURL=kpi-boxplot-chart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-boxplot-chart.js","sourceRoot":"","sources":["../../client/charts/kpi-boxplot-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport * as d3 from 'd3'\n\n@customElement('kpi-boxplot-chart')\nexport class KpiBoxplotChart extends LitElement {\n @property({ type: Array }) data: any[] = []\n @property({ type: Array }) groups: string[] = []\n @property({ type: String }) minKey: string = 'min'\n @property({ type: String }) maxKey: string = 'max'\n @property({ type: String }) meanKey: string = 'mean'\n @property({ type: String }) medianKey: string = 'median'\n @property({ type: String }) q1Key: string = 'q1'\n @property({ type: String }) q3Key: string = 'q3'\n @property({ type: String }) valueKey: string = 'value'\n @property({ type: String }) currentGroup: string = ''\n\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n svg {\n width: 100%;\n height: 100%;\n display: block;\n }\n `\n\n private chartWidth = 0\n private chartHeight = 0\n private resizeObserver?: ResizeObserver\n\n render() {\n return html`<svg\n id=\"boxplot\"\n width=${this.chartWidth}\n height=${this.chartHeight}\n viewBox=\"0 0 ${this.chartWidth} ${this.chartHeight}\"\n preserveAspectRatio=\"xMidYMid meet\"\n ></svg>`\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.resizeObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n const rect = entry.contentRect\n this.chartWidth = rect.width\n this.chartHeight = rect.height\n this.requestUpdate()\n }\n })\n this.resizeObserver.observe(this)\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect()\n super.disconnectedCallback()\n }\n\n updated() {\n this.drawBoxplot()\n }\n\n drawBoxplot() {\n const svg = d3.select(this.renderRoot.querySelector('#boxplot'))\n svg.selectAll('*').remove()\n const w = this.chartWidth || 300\n const h = this.chartHeight || 300\n const margin = { top: 20, right: 20, bottom: 40, left: 40 }\n const plotW = w - margin.left - margin.right\n const plotH = h - margin.top - margin.bottom\n\n // x축: 그룹\n const x = d3.scaleBand().domain(this.groups).range([0, plotW]).padding(0.4)\n // y축: 값\n const allValues = this.data.flatMap(d => [\n d[this.minKey],\n d[this.maxKey],\n d[this.q1Key],\n d[this.q3Key],\n d[this.medianKey],\n d[this.meanKey],\n d[this.valueKey]\n ])\n const y = d3\n .scaleLinear()\n .domain([d3.min(allValues) ?? 0, d3.max(allValues) ?? 1])\n .nice()\n .range([plotH, 0])\n\n const g = svg\n .attr('width', w)\n .attr('height', h)\n .append('g')\n .attr('transform', `translate(${margin.left},${margin.top})`)\n\n // 축\n g.append('g').call(d3.axisLeft(y))\n g.append('g').attr('transform', `translate(0,${plotH})`).call(d3.axisBottom(x))\n\n // 박스플롯\n this.data.forEach(d => {\n const gx = x(d.group) ?? 0\n // 박스\n g.append('rect')\n .attr('x', gx)\n .attr('y', y(d[this.q3Key]))\n .attr('width', x.bandwidth())\n .attr('height', y(d[this.q1Key]) - y(d[this.q3Key]))\n .attr('fill', d.group === this.currentGroup ? '#2196f3' : '#bbb')\n .attr('opacity', 0.5)\n // 중앙선(중앙값)\n g.append('line')\n .attr('x1', gx)\n .attr('x2', gx + x.bandwidth())\n .attr('y1', y(d[this.medianKey]))\n .attr('y2', y(d[this.medianKey]))\n .attr('stroke', '#333')\n .attr('stroke-width', 2)\n // 수염(min-max)\n g.append('line')\n .attr('x1', gx + x.bandwidth() / 2)\n .attr('x2', gx + x.bandwidth() / 2)\n .attr('y1', y(d[this.minKey]))\n .attr('y2', y(d[this.maxKey]))\n .attr('stroke', '#333')\n // min/max\n g.append('line')\n .attr('x1', gx + x.bandwidth() / 4)\n .attr('x2', gx + (x.bandwidth() * 3) / 4)\n .attr('y1', y(d[this.minKey]))\n .attr('y2', y(d[this.minKey]))\n .attr('stroke', '#333')\n g.append('line')\n .attr('x1', gx + x.bandwidth() / 4)\n .attr('x2', gx + (x.bandwidth() * 3) / 4)\n .attr('y1', y(d[this.maxKey]))\n .attr('y2', y(d[this.maxKey]))\n .attr('stroke', '#333')\n // 평균값\n g.append('circle')\n .attr('cx', gx + x.bandwidth() / 2)\n .attr('cy', y(d[this.meanKey]))\n .attr('r', 4)\n .attr('fill', 'orange')\n })\n // 현재 그룹 값 강조\n this.data.forEach(d => {\n if (d.group === this.currentGroup) {\n g.append('circle')\n .attr('cx', x(d.group) + x.bandwidth() / 2)\n .attr('cy', y(d[this.valueKey]))\n .attr('r', 6)\n .attr('fill', '#e91e63')\n .attr('stroke', '#fff')\n .attr('stroke-width', 2)\n }\n })\n }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import { LitElement } from 'lit';
2
+ export declare class KpiRadarChart extends LitElement {
3
+ data: any[];
4
+ categories: string[];
5
+ valueKey: string;
6
+ currentGroup: string;
7
+ static styles: import("lit").CSSResult;
8
+ private chartWidth;
9
+ private chartHeight;
10
+ private resizeObserver?;
11
+ render(): import("lit-html").TemplateResult<1>;
12
+ connectedCallback(): void;
13
+ disconnectedCallback(): void;
14
+ updated(): void;
15
+ drawRadar(): void;
16
+ }
@@ -0,0 +1,138 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { LitElement, html, css } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ import * as d3 from 'd3';
5
+ let KpiRadarChart = class KpiRadarChart extends LitElement {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.data = [];
9
+ this.categories = [];
10
+ this.valueKey = 'value';
11
+ this.currentGroup = '';
12
+ this.chartWidth = 0;
13
+ this.chartHeight = 0;
14
+ }
15
+ static { this.styles = css `
16
+ :host {
17
+ display: block;
18
+ width: 100%;
19
+ height: 100%;
20
+ }
21
+ svg {
22
+ width: 100%;
23
+ height: 100%;
24
+ display: block;
25
+ }
26
+ `; }
27
+ render() {
28
+ return html `<svg
29
+ id="radar"
30
+ width=${this.chartWidth}
31
+ height=${this.chartHeight}
32
+ viewBox="0 0 ${this.chartWidth} ${this.chartHeight}"
33
+ preserveAspectRatio="xMidYMid meet"
34
+ ></svg>`;
35
+ }
36
+ connectedCallback() {
37
+ super.connectedCallback();
38
+ this.resizeObserver = new ResizeObserver(entries => {
39
+ for (const entry of entries) {
40
+ const rect = entry.contentRect;
41
+ this.chartWidth = rect.width;
42
+ this.chartHeight = rect.height;
43
+ this.requestUpdate();
44
+ }
45
+ });
46
+ this.resizeObserver.observe(this);
47
+ }
48
+ disconnectedCallback() {
49
+ this.resizeObserver?.disconnect();
50
+ super.disconnectedCallback();
51
+ }
52
+ updated() {
53
+ this.drawRadar();
54
+ }
55
+ drawRadar() {
56
+ const svg = d3.select(this.renderRoot.querySelector('#radar'));
57
+ svg.selectAll('*').remove();
58
+ const w = this.chartWidth || 300;
59
+ const h = this.chartHeight || 300;
60
+ const r = Math.min(w, h) / 2 - 40;
61
+ const angleSlice = (2 * Math.PI) / (this.categories.length || 1);
62
+ // 데이터 변환: { group, values: [ {category, value} ... ] }
63
+ const groupData = d3
64
+ .groups(this.data, d => d.group)
65
+ .map(([group, values]) => ({
66
+ group,
67
+ values: this.categories.map((cat, i) => {
68
+ const found = values.find(v => v.category === cat);
69
+ return { category: cat, value: found ? found[this.valueKey] : 0 };
70
+ })
71
+ }));
72
+ // 스케일
73
+ const maxValue = d3.max(this.data, d => d[this.valueKey]) || 1;
74
+ const radius = d3.scaleLinear().domain([0, maxValue]).range([0, r]);
75
+ // SVG 기본
76
+ svg.attr('width', w).attr('height', h);
77
+ const g = svg.append('g').attr('transform', `translate(${w / 2},${h / 2})`);
78
+ // 그리드/축
79
+ for (let i = 1; i <= 5; i++) {
80
+ g.append('circle')
81
+ .attr('r', (r / 5) * i)
82
+ .attr('fill', 'none')
83
+ .attr('stroke', '#ccc');
84
+ }
85
+ this.categories.forEach((cat, i) => {
86
+ const angle = i * angleSlice - Math.PI / 2;
87
+ g.append('line')
88
+ .attr('x1', 0)
89
+ .attr('y1', 0)
90
+ .attr('x2', radius(maxValue) * Math.cos(angle))
91
+ .attr('y2', radius(maxValue) * Math.sin(angle))
92
+ .attr('stroke', '#ccc');
93
+ g.append('text')
94
+ .attr('x', (radius(maxValue) + 10) * Math.cos(angle))
95
+ .attr('y', (radius(maxValue) + 10) * Math.sin(angle))
96
+ .attr('text-anchor', 'middle')
97
+ .attr('alignment-baseline', 'middle')
98
+ .attr('font-size', 12)
99
+ .text(cat);
100
+ });
101
+ // 그룹별 폴리곤
102
+ groupData.forEach(gd => {
103
+ // 마지막에 첫 점을 한 번 더 추가
104
+ const closedValues = [...gd.values, gd.values[0]];
105
+ const line = d3
106
+ .lineRadial()
107
+ .radius((d) => radius(d.value))
108
+ .angle((d, i) => i * angleSlice);
109
+ g.append('path')
110
+ .datum(closedValues)
111
+ .attr('d', line)
112
+ .attr('fill', gd.group === this.currentGroup ? 'rgba(33,150,243,0.4)' : 'rgba(200,200,200,0.2)')
113
+ .attr('stroke', gd.group === this.currentGroup ? '#2196f3' : '#aaa')
114
+ .attr('stroke-width', gd.group === this.currentGroup ? 3 : 1);
115
+ });
116
+ }
117
+ };
118
+ __decorate([
119
+ property({ type: Array }),
120
+ __metadata("design:type", Array)
121
+ ], KpiRadarChart.prototype, "data", void 0);
122
+ __decorate([
123
+ property({ type: Array }),
124
+ __metadata("design:type", Array)
125
+ ], KpiRadarChart.prototype, "categories", void 0);
126
+ __decorate([
127
+ property({ type: String }),
128
+ __metadata("design:type", String)
129
+ ], KpiRadarChart.prototype, "valueKey", void 0);
130
+ __decorate([
131
+ property({ type: String }),
132
+ __metadata("design:type", String)
133
+ ], KpiRadarChart.prototype, "currentGroup", void 0);
134
+ KpiRadarChart = __decorate([
135
+ customElement('kpi-radar-chart')
136
+ ], KpiRadarChart);
137
+ export { KpiRadarChart };
138
+ //# sourceMappingURL=kpi-radar-chart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kpi-radar-chart.js","sourceRoot":"","sources":["../../client/charts/kpi-radar-chart.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport * as d3 from 'd3'\n\n@customElement('kpi-radar-chart')\nexport class KpiRadarChart extends LitElement {\n @property({ type: Array }) data: any[] = []\n @property({ type: Array }) categories: string[] = []\n @property({ type: String }) valueKey: string = 'value'\n @property({ type: String }) currentGroup: string = ''\n\n static styles = css`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n svg {\n width: 100%;\n height: 100%;\n display: block;\n }\n `\n\n private chartWidth = 0\n private chartHeight = 0\n private resizeObserver?: ResizeObserver\n\n render() {\n return html`<svg\n id=\"radar\"\n width=${this.chartWidth}\n height=${this.chartHeight}\n viewBox=\"0 0 ${this.chartWidth} ${this.chartHeight}\"\n preserveAspectRatio=\"xMidYMid meet\"\n ></svg>`\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.resizeObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n const rect = entry.contentRect\n this.chartWidth = rect.width\n this.chartHeight = rect.height\n this.requestUpdate()\n }\n })\n this.resizeObserver.observe(this)\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect()\n super.disconnectedCallback()\n }\n\n updated() {\n this.drawRadar()\n }\n\n drawRadar() {\n const svg = d3.select(this.renderRoot.querySelector('#radar'))\n svg.selectAll('*').remove()\n const w = this.chartWidth || 300\n const h = this.chartHeight || 300\n const r = Math.min(w, h) / 2 - 40\n const angleSlice = (2 * Math.PI) / (this.categories.length || 1)\n\n // 데이터 변환: { group, values: [ {category, value} ... ] }\n const groupData = d3\n .groups(this.data, d => d.group)\n .map(([group, values]) => ({\n group,\n values: this.categories.map((cat, i) => {\n const found = values.find(v => v.category === cat)\n return { category: cat, value: found ? found[this.valueKey] : 0 }\n })\n }))\n\n // 스케일\n const maxValue = d3.max(this.data, d => d[this.valueKey]) || 1\n const radius = d3.scaleLinear().domain([0, maxValue]).range([0, r])\n\n // SVG 기본\n svg.attr('width', w).attr('height', h)\n const g = svg.append('g').attr('transform', `translate(${w / 2},${h / 2})`)\n\n // 그리드/축\n for (let i = 1; i <= 5; i++) {\n g.append('circle')\n .attr('r', (r / 5) * i)\n .attr('fill', 'none')\n .attr('stroke', '#ccc')\n }\n this.categories.forEach((cat, i) => {\n const angle = i * angleSlice - Math.PI / 2\n g.append('line')\n .attr('x1', 0)\n .attr('y1', 0)\n .attr('x2', radius(maxValue) * Math.cos(angle))\n .attr('y2', radius(maxValue) * Math.sin(angle))\n .attr('stroke', '#ccc')\n g.append('text')\n .attr('x', (radius(maxValue) + 10) * Math.cos(angle))\n .attr('y', (radius(maxValue) + 10) * Math.sin(angle))\n .attr('text-anchor', 'middle')\n .attr('alignment-baseline', 'middle')\n .attr('font-size', 12)\n .text(cat)\n })\n\n // 그룹별 폴리곤\n groupData.forEach(gd => {\n // 마지막에 첫 점을 한 번 더 추가\n const closedValues = [...gd.values, gd.values[0]]\n const line = d3\n .lineRadial()\n .radius((d: any) => radius(d.value))\n .angle((d, i) => i * angleSlice)\n g.append('path')\n .datum(closedValues)\n .attr('d', line as any)\n .attr('fill', gd.group === this.currentGroup ? 'rgba(33,150,243,0.4)' : 'rgba(200,200,200,0.2)')\n .attr('stroke', gd.group === this.currentGroup ? '#2196f3' : '#aaa')\n .attr('stroke-width', gd.group === this.currentGroup ? 3 : 1)\n })\n }\n}\n"]}
@@ -7,6 +7,8 @@ import './kpi-history-viewer';
7
7
  import './kpi-list-summary';
8
8
  import './kpi-value-entry';
9
9
  import './kpi-alert-panel';
10
+ import '../../charts/kpi-radar-chart';
11
+ import '../../charts/kpi-boxplot-chart';
10
12
  export declare class KpiDashboardPage extends PageView {
11
13
  static styles: import("lit").CSSResult[];
12
14
  categories: any[];
@@ -16,6 +18,15 @@ export declare class KpiDashboardPage extends PageView {
16
18
  showHistoryModal: boolean;
17
19
  modalHistories: any[];
18
20
  modalKpiName: string;
21
+ private get sampleCategories();
22
+ private get sampleGroups();
23
+ private get sampleSeriesData();
24
+ private get sampleRadarData();
25
+ private get sampleRadarCategories();
26
+ private get sampleRadarGroups();
27
+ private get sampleBoxplotData();
28
+ private get sampleBoxplotGroups();
29
+ private get sampleCurrentGroup();
19
30
  connectedCallback(): void;
20
31
  pageUpdated(changes: any, lifecycle: any): void;
21
32
  fetchCategories(): Promise<void>;
@@ -13,6 +13,8 @@ import './kpi-history-viewer';
13
13
  import './kpi-list-summary';
14
14
  import './kpi-value-entry';
15
15
  import './kpi-alert-panel';
16
+ import '../../charts/kpi-radar-chart';
17
+ import '../../charts/kpi-boxplot-chart';
16
18
  let KpiDashboardPage = class KpiDashboardPage extends PageView {
17
19
  constructor() {
18
20
  super(...arguments);
@@ -36,6 +38,35 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
36
38
  flex: 1;
37
39
  padding: 24px;
38
40
  }
41
+ .sample-charts-section {
42
+ display: flex;
43
+ gap: 40px;
44
+ margin-bottom: 48px;
45
+ align-items: flex-start;
46
+ }
47
+ .sample-chart-card {
48
+ background: #fff;
49
+ border-radius: 12px;
50
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
51
+ border: 1px solid #ececec;
52
+ padding: 24px 32px;
53
+ min-width: 340px;
54
+ flex: 1;
55
+ display: flex;
56
+ flex-direction: column;
57
+ align-items: stretch;
58
+ min-height: 500px;
59
+ }
60
+ .sample-chart-title {
61
+ font-size: 1.1rem;
62
+ font-weight: bold;
63
+ margin-bottom: 12px;
64
+ }
65
+ .sample-chart-container {
66
+ width: 100%;
67
+ height: 340px;
68
+ min-height: 340px;
69
+ }
39
70
  .category-section {
40
71
  margin-bottom: 40px;
41
72
  }
@@ -78,6 +109,124 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
78
109
  }
79
110
  `
80
111
  ]; }
112
+ // 샘플 데이터
113
+ get sampleCategories() {
114
+ return ['생산성', '품질', '안전', '환경', '비용', '일정'];
115
+ }
116
+ get sampleGroups() {
117
+ return ['A', 'B', 'C'];
118
+ }
119
+ get sampleSeriesData() {
120
+ // 각 카테고리별로 10개 이상의 다양한 값(평균, 분산, 이상치 포함)
121
+ // 생산성: 평균 높고 분산 큼, 이상치 포함
122
+ const 생산성 = [95, 92, 90, 88, 85, 80, 78, 75, 70, 60, 100]; // 100은 이상치
123
+ // 품질: 평균 높고 분산 작음
124
+ const 품질 = [90, 89, 88, 87, 86, 85, 84, 83, 82, 80, 70];
125
+ // 안전: 평균 중간, 이상치 포함
126
+ const 안전 = [92, 91, 90, 89, 88, 87, 86, 85, 84, 65, 60]; // 60은 이상치
127
+ // 환경: 낮은 값에 몰림, 분산 큼
128
+ const 환경 = [95, 90, 85, 80, 75, 70, 65, 60, 60, 60, 55];
129
+ // 비용: 전체적으로 낮음, 이상치 포함
130
+ const 비용 = [80, 78, 76, 74, 72, 70, 68, 66, 64, 62, 50]; // 50은 이상치
131
+ // 일정: 분산 큼, 이상치 포함
132
+ const 일정 = [90, 88, 86, 84, 82, 80, 78, 76, 74, 60, 100]; // 100은 이상치
133
+ const categories = this.sampleCategories;
134
+ const groups = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'];
135
+ const all = [];
136
+ categories.forEach((cat, i) => {
137
+ let arr = [];
138
+ switch (cat) {
139
+ case '생산성':
140
+ arr = 생산성;
141
+ break;
142
+ case '품질':
143
+ arr = 품질;
144
+ break;
145
+ case '안전':
146
+ arr = 안전;
147
+ break;
148
+ case '환경':
149
+ arr = 환경;
150
+ break;
151
+ case '비용':
152
+ arr = 비용;
153
+ break;
154
+ case '일정':
155
+ arr = 일정;
156
+ break;
157
+ }
158
+ arr.forEach((v, idx) => {
159
+ all.push({ group: groups[idx] ?? `G${idx + 1}`, category: cat, value: v });
160
+ });
161
+ });
162
+ return all;
163
+ }
164
+ get sampleRadarData() {
165
+ // 카테고리별로 min, max, avg, A(자신의 그룹)만 반환
166
+ const categories = this.sampleCategories;
167
+ const data = this.sampleSeriesData;
168
+ const minData = { group: 'Min' };
169
+ const maxData = { group: 'Max' };
170
+ const avgData = { group: 'Avg' };
171
+ const aData = { group: 'A' };
172
+ categories.forEach(category => {
173
+ const values = data.filter(d => d.category === category).map(d => d.value);
174
+ minData[category] = Math.min(...values);
175
+ maxData[category] = Math.max(...values);
176
+ avgData[category] = values.reduce((a, b) => a + b, 0) / values.length;
177
+ aData[category] = data.find(d => d.category === category && d.group === 'A')?.value ?? avgData[category];
178
+ });
179
+ // kpi-radar-chart가 기대하는 구조로 변환: [{group, category, value} ...]
180
+ const result = [];
181
+ categories.forEach(category => {
182
+ result.push({ group: 'Min', category, value: minData[category] });
183
+ result.push({ group: 'Max', category, value: maxData[category] });
184
+ result.push({ group: 'Avg', category, value: avgData[category] });
185
+ result.push({ group: 'A', category, value: aData[category] });
186
+ });
187
+ return result;
188
+ }
189
+ get sampleRadarCategories() {
190
+ return this.sampleCategories;
191
+ }
192
+ get sampleRadarGroups() {
193
+ return this.sampleGroups;
194
+ }
195
+ get sampleBoxplotData() {
196
+ // 카테고리별로 그룹별 value의 분포 계산
197
+ const categories = this.sampleCategories;
198
+ const data = this.sampleSeriesData;
199
+ return categories.map(category => {
200
+ const values = data.filter(d => d.category === category).map(d => d.value);
201
+ const sorted = [...values].sort((a, b) => a - b);
202
+ const min = sorted[0];
203
+ const max = sorted[sorted.length - 1];
204
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
205
+ const median = sorted.length % 2 === 0
206
+ ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
207
+ : sorted[Math.floor(sorted.length / 2)];
208
+ const q1 = sorted[Math.floor(sorted.length / 4)];
209
+ const q3 = sorted[Math.floor((sorted.length * 3) / 4)];
210
+ // value: A그룹의 실제값(강조)
211
+ const value = data.find(d => d.category === category && d.group === 'A')?.value ?? mean;
212
+ return {
213
+ group: category, // x축: 카테고리
214
+ min,
215
+ q1,
216
+ median,
217
+ q3,
218
+ max,
219
+ mean,
220
+ value
221
+ };
222
+ });
223
+ }
224
+ get sampleBoxplotGroups() {
225
+ return this.sampleCategories;
226
+ }
227
+ get sampleCurrentGroup() {
228
+ return 'A';
229
+ }
81
230
  connectedCallback() {
82
231
  super.connectedCallback();
83
232
  this.fetchCategories();
@@ -301,6 +450,42 @@ let KpiDashboardPage = class KpiDashboardPage extends PageView {
301
450
  return html `<div>${this.error}</div>`;
302
451
  return html `
303
452
  <div class="dashboard-root">
453
+ <!-- 샘플 차트 섹션 -->
454
+ <div class="sample-charts-section">
455
+ <div class="sample-chart-card">
456
+ <div class="sample-chart-title">그룹별 KPI 비교 (Radar)</div>
457
+ <div class="sample-chart-container">
458
+ <kpi-radar-chart
459
+ .data=${this.sampleRadarData ?? []}
460
+ .categories=${this.sampleRadarCategories ?? []}
461
+ .currentGroup=${this.sampleCurrentGroup}
462
+ ></kpi-radar-chart>
463
+ </div>
464
+ <div style="margin-top:12px;color:#666;font-size:0.98em;">
465
+ ※ <b>Radar 차트</b>는 각 카테고리별로 최소(Min), 최대(Max), 평균(Avg), 그리고 이 프로젝트의 값을 한눈에
466
+ 비교할 수 있도록 시각화합니다.<br />
467
+ <b>진한 파란색</b> 다각형이 이 프로젝트의 성과이며, 회색 다각형은 기준값(최소/최대/평균)입니다.
468
+ </div>
469
+ </div>
470
+ <div class="sample-chart-card">
471
+ <div class="sample-chart-title">그룹별 분포 (Boxplot)</div>
472
+ <div class="sample-chart-container">
473
+ <kpi-boxplot-chart
474
+ .data=${this.sampleBoxplotData ?? []}
475
+ .groups=${this.sampleBoxplotGroups ?? []}
476
+ .currentGroup=${this.sampleCurrentGroup}
477
+ ></kpi-boxplot-chart>
478
+ </div>
479
+ <div style="margin-top:12px;color:#666;font-size:0.98em;">
480
+ ※ <b>Boxplot(박스플롯)</b>은 각 카테고리별로 값의 분포(최소, 1사분위, 중앙값, 3사분위, 최대, 평균, 이상치
481
+ 등)를 보여줍니다.<br />
482
+ <b>박스</b>는 중앙 50% 구간, <b>수염</b>은 전체 범위, <b>굵은 검정색 가로선</b>은 중앙값(메디안),
483
+ <b>주황색 원</b>은 평균값(Mean)을 의미합니다.<br />
484
+ <b>이 프로젝트의 값</b>은 <b>진한 오렌지색 원</b>으로 별도 강조되어 표시되며, 중앙값/평균과 다를 수
485
+ 있습니다.<br />
486
+ </div>
487
+ </div>
488
+ </div>
304
489
  ${this.showHistoryModal
305
490
  ? html `
306
491
  <div