kakaotalk-chat-analyzer 0.22.6 → 0.23.0
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/README.md +3 -1
- package/dist/src/report-charts.d.ts +1 -1
- package/dist/src/report-charts.js +265 -71
- package/dist/src/report-charts.js.map +1 -1
- package/dist/src/report-innovation.js +5 -5
- package/dist/src/report-llm-deck.js +7 -7
- package/dist/src/report-story.js +5 -5
- package/dist/src/report-story.js.map +1 -1
- package/dist/src/report-styles.d.ts +1 -1
- package/dist/src/report-styles.js +1663 -1275
- package/dist/src/report-styles.js.map +1 -1
- package/dist/src/report-ux.js +1 -1
- package/dist/src/report.js +19 -19
- package/dist/src/report.js.map +1 -1
- package/dist/src/version.d.ts +2 -2
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ npx kcachat@latest "C:\경로\KakaoTalk_Chat_....csv"
|
|
|
106
106
|
|
|
107
107
|
## 리포트에서 볼 수 있는 것
|
|
108
108
|
|
|
109
|
-
브라우저만 있으면 되는 **단일 HTML**입니다. **대화 원문은 파일에 넣지 않습니다.** 상단 **섹션 메뉴**로 ⓪~⑥ 구역을 바로 이동할 수
|
|
109
|
+
브라우저만 있으면 되는 **단일 HTML**입니다. **대화 원문은 파일에 넣지 않습니다.** 상단 **섹션 메뉴**로 ⓪~⑥ 구역을 바로 이동할 수 있으며, 모든 콘텐츠는 **v3 통합 카드 시스템**으로 일관되게 배치됩니다.
|
|
110
110
|
|
|
111
111
|
| 구역 | 내용 |
|
|
112
112
|
|------|------|
|
|
@@ -262,6 +262,8 @@ cd kakaotalk-chat-analyzer && npm install && npm run build && npm test
|
|
|
262
262
|
|
|
263
263
|
| 버전 | 요약 |
|
|
264
264
|
|------|------|
|
|
265
|
+
| **0.23.0** | **v3 통합 카드 시스템** — 리포트 레이아웃 전면 재설계 · 12개 CSS 토큰 · 5계층 카드 시스템 · OLED 글라스 6종 확장 · 히어로 재설계 · #s-charts 구조화 · 헤딩 계층 정비 · 컬러 토큰화 |
|
|
266
|
+
| **0.22.6** | **프리미엄 리디자인** — 랜딩 페이지·리포트 CSS 전면 개편 · **글라스 2.0** 테마 · **벤토 그리드** 기능 카드 · **인터랙티브 마우스 트래킹** · 접근성 개선 (**focus-visible**, 대비 강화, reduced motion) · light theme 보완 |
|
|
265
267
|
| **0.22.3** | **5개 신규 지표**(질문·독백·세션·피크일·도메인) · **3개 LLM 섹션 활성화**(관계·시대·그날) · **소셜 네트워크 그래프** · **감정 롤러코스터** · **토픽 트랜드** · 참여자 **10명+더보기** · 모바일 **ECharts 최적화** · 네비 번호 연속성 · `insightBullets` 버그 수정 |
|
|
266
268
|
| **0.22.2** | **JSON 필드명 노출** 버그 수정·LLM 출력 필터링 강화·리포트 렌더링 품질 개선 |
|
|
267
269
|
| **0.22.1** | **LLM JSON Grammar Constraint** 핫픽스(Child Process)·**템플릿 잔여물 필터링**·**Few-shot 프롬프트**·리포트 출력 품질 개선 |
|
|
@@ -81,4 +81,4 @@ export declare function serializeChartPayload(data: ReportData): string;
|
|
|
81
81
|
export declare function buildChartPayload(data: ReportData): ChartPayload;
|
|
82
82
|
export declare function serializeExplorerPayload(data: ReportData): string;
|
|
83
83
|
export declare function renderChartDeck(data: ReportData): string;
|
|
84
|
-
export declare const CHARTS_INIT_SCRIPT = "\n (function () {\n var kcaDyadBoot = function () {};\n function run() {\n var dataEl = document.getElementById(\"kca-chart-data\");\n if (!dataEl) return;\n if (typeof echarts === \"undefined\") return;\n var data;\n try { data = JSON.parse(dataEl.textContent || \"{}\"); } catch (e) { return; }\n\n function cssVar(name, fallback) {\n try {\n var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n return v || fallback;\n } catch (e) { return fallback; }\n }\n function isDarkTheme() {\n return document.documentElement.getAttribute(\"data-theme\") === \"dark\" ||\n (!document.documentElement.getAttribute(\"data-theme\") &&\n window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches);\n }\n var dark, text, muted, accent, accent2, heatLo, heatHi, wdColors;\n\n function baseOpt() {\n return {\n textStyle: { color: text, fontFamily: \"Pretendard, Apple SD Gothic Neo, sans-serif\" },\n tooltip: { trigger: \"axis\", backgroundColor: dark ? \"#1c2128\" : \"#fff\", borderColor: \"transparent\" },\n };\n }\n\n var charts = [];\n var dyadChartStarted = false;\n function disposeCharts() {\n dyadChartStarted = false;\n charts.forEach(function (c) {\n try { c.dispose(); } catch (e) {}\n });\n charts.length = 0;\n }\n function markDyadReady(el) {\n el.classList.remove(\"is-loading\");\n el.classList.add(\"is-ready\");\n el.setAttribute(\"aria-busy\", \"false\");\n var sk = el.querySelector(\".chart-skeleton\");\n if (sk) sk.remove();\n }\n function initDyadChart(data) {\n if (!data.interaction || !data.interaction.aliases.length) return null;\n var el = document.getElementById(\"chart-dyad\");\n if (!el) return null;\n var ix = data.interaction;\n var dg = layout(el);\n var heat = [];\n var maxV = 1;\n for (var ri = 0; ri < ix.matrix.length; ri += 1) {\n for (var ci = 0; ci < ix.matrix[ri].length; ci += 1) {\n var v = ix.matrix[ri][ci];\n if (v > maxV) maxV = v;\n if (v > 0) heat.push([ci, ri, v]);\n }\n }\n var splitFill = dark ? [\"rgba(255,255,255,0.03)\", \"rgba(255,255,255,0.06)\"] : [\"rgba(0,0,0,0.02)\", \"rgba(0,0,0,0.05)\"];\n var chart = init(\"chart-dyad\", Object.assign(baseOpt(), {\n animation: false,\n tooltip: { position: \"top\", formatter: function (p) { var v = p.value[2]; return ix.aliases[p.value[1]] + \" \u2192 \" + ix.aliases[p.value[0]] + \": \" + v; } },\n grid: { left: Math.max(dg.leftCat, 80), right: dg.right, top: dg.top, bottom: Math.max(dg.bottom, 72) },\n xAxis: {\n type: \"category\",\n data: ix.aliases,\n axisLabel: { color: muted, fontSize: dg.fs, rotate: 28, margin: 10 },\n splitArea: { show: true, areaStyle: { color: splitFill } },\n },\n yAxis: {\n type: \"category\",\n data: ix.aliases,\n inverse: true,\n axisLabel: { color: muted, fontSize: dg.fs },\n splitArea: { show: true, areaStyle: { color: splitFill } },\n },\n visualMap: {\n min: 0,\n max: maxV,\n calculable: true,\n orient: \"horizontal\",\n left: \"center\",\n bottom: 0,\n itemHeight: dg.w < 380 ? 72 : 88,\n inRange: { color: [heatLo, dark ? \"#2a9d8f\" : \"#7ecfc2\", dark ? \"#3ee8c5\" : \"#0f6b5c\", heatHi] },\n },\n series: [{\n type: \"heatmap\",\n data: heat,\n progressive: 0,\n animation: false,\n label: {\n show: true,\n color: text,\n fontSize: dg.w < 380 ? 8 : 10,\n formatter: function (p) { var v = p.value[2]; return v > 0 ? String(v) : \"\"; },\n },\n emphasis: {\n label: { show: true, color: text, fontSize: dg.w < 380 ? 9 : 11 },\n itemStyle: { shadowBlur: 12, shadowColor: dark ? \"rgba(62,232,197,0.5)\" : \"rgba(15,107,92,0.35)\" },\n },\n itemStyle: { borderWidth: 0 },\n }],\n }));\n if (chart) markDyadReady(el);\n return chart;\n }\n function bootDyadWhenVisible(data) {\n if (!data || !data.interaction || !data.interaction.aliases.length) return;\n var el = document.getElementById(\"chart-dyad\");\n if (!el || dyadChartStarted) return;\n function startDyad() {\n if (dyadChartStarted) return;\n dyadChartStarted = true;\n requestAnimationFrame(function () { initDyadChart(data); });\n }\n if (typeof IntersectionObserver === \"undefined\") {\n startDyad();\n return;\n }\n var dyIo = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) { return e.isIntersecting; })) {\n dyIo.disconnect();\n startDyad();\n }\n }, { rootMargin: \"200px 0px\", threshold: 0.05 });\n dyIo.observe(el);\n setTimeout(function () {\n if (dyadChartStarted) return;\n var r = el.getBoundingClientRect();\n if (r.top < window.innerHeight + 200 && r.bottom > 0) startDyad();\n }, 200);\n }\n kcaDyadBoot = bootDyadWhenVisible;\n function resizeAll() {\n charts.forEach(function (c) {\n try { c.resize(); } catch (e) {}\n });\n }\n function layout(el) {\n var w = (el && el.clientWidth) || 400;\n if (w < 380) {\n return { w: w, left: 28, right: 8, top: 14, bottom: 44, fs: 9, rot: 40, leftCat: 56, bottomRot: 42 };\n }\n if (w < 640) {\n return { w: w, left: 40, right: 10, top: 18, bottom: 34, fs: 10, rot: 26, leftCat: 72, bottomRot: 32 };\n }\n return { w: w, left: 48, right: 14, top: 22, bottom: 28, fs: 11, rot: 0, leftCat: 96, bottomRot: 28 };\n }\n function init(id, opt) {\n var el = document.getElementById(id);\n if (!el) return null;\n try {\n var chart = echarts.init(el, null, { renderer: \"canvas\" });\n chart.setOption(opt);\n charts.push(chart);\n if (typeof ResizeObserver !== \"undefined\") {\n var ro = new ResizeObserver(function () {\n requestAnimationFrame(function () {\n try { chart.resize(); } catch (e) {}\n });\n });\n ro.observe(el);\n }\n return chart;\n } catch (err) {\n console.error(\"[kca-chart]\", id, err);\n el.setAttribute(\"data-chart-failed\", \"1\");\n el.innerHTML = '<p style=\"margin:0;padding:12px;font-size:12px;color:var(--muted);text-align:center\">\uCC28\uD2B8\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC5B4\uC694. \uC0C8\uB85C\uACE0\uCE68\uD558\uAC70\uB098 \uB124\uD2B8\uC6CC\uD06C(CDN)\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.</p>';\n return null;\n }\n }\n var chartHooksInstalled = false;\n var resizeListenersBound = false;\n var kwSortBound = false;\n\n function paintCharts() {\n dark = isDarkTheme();\n text = cssVar(\"--ink\", dark ? \"#e9eef5\" : \"#141a1f\");\n muted = cssVar(\"--muted\", dark ? \"#8b98a8\" : \"#5c6670\");\n accent = cssVar(\"--accent\", dark ? \"#3ee8c5\" : \"#0f6b5c\");\n accent2 = cssVar(\"--accent2\", dark ? \"#818cf8\" : \"#4f46e5\");\n heatLo = cssVar(\"--chart-heat-lo\", dark ? \"#1a2744\" : \"#d4e4f4\");\n heatHi = cssVar(\"--chart-heat-hi\", dark ? \"#5ee8ff\" : \"#1e4fd6\");\n wdColors = [\n cssVar(\"--chart-wd-0\", dark ? \"#818cf8\" : \"#4f46e5\"),\n cssVar(\"--chart-wd-1\", dark ? \"#3ee8c5\" : \"#0f6b5c\"),\n cssVar(\"--chart-wd-2\", dark ? \"#34d399\" : \"#059669\"),\n cssVar(\"--chart-wd-3\", dark ? \"#2dd4bf\" : \"#0d9488\"),\n cssVar(\"--chart-wd-4\", dark ? \"#38bdf8\" : \"#0284c7\"),\n cssVar(\"--chart-wd-5\", dark ? \"#fbbf24\" : \"#d97706\"),\n cssVar(\"--chart-wd-6\", dark ? \"#fb923c\" : \"#ea580c\"),\n ];\n\n function hourBarColor(h) {\n if (h <= 5) return dark ? \"#6366f1\" : \"#4f46e5\";\n if (h <= 11) return dark ? \"#3ee8c5\" : \"#0f6b5c\";\n if (h <= 17) return dark ? \"#fbbf24\" : \"#d97706\";\n return dark ? \"#3b82f6\" : \"#1d4ed8\";\n }\n if (data.hourly && document.getElementById(\"chart-hours\")) {\n var hoursEl = document.getElementById(\"chart-hours\");\n var hg = layout(hoursEl);\n init(\"chart-hours\", Object.assign(baseOpt(), {\n grid: { left: hg.left, right: hg.right, top: hg.top, bottom: hg.bottom },\n xAxis: { type: \"category\", data: data.hourly.map(function (_, h) { return h + \"\uC2DC\"; }), axisLabel: { color: muted, fontSize: hg.fs, rotate: hg.rot, interval: hg.w < 480 ? 2 : 0 } },\n yAxis: { type: \"value\", axisLabel: { color: muted }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n series: [{\n type: \"bar\",\n data: data.hourly.map(function (v, h) {\n return { value: v, itemStyle: { color: hourBarColor(h), borderRadius: [4, 4, 0, 0] } };\n }),\n markArea: {\n silent: true,\n itemStyle: { color: dark ? \"rgba(99,102,241,0.12)\" : \"rgba(79,70,229,0.08)\" },\n data: [[{ xAxis: \"0\uC2DC\" }, { xAxis: \"5\uC2DC\" }], [{ xAxis: \"22\uC2DC\" }, { xAxis: \"23\uC2DC\" }]],\n },\n }],\n }));\n }\n\n if (data.weekdays && document.getElementById(\"chart-weekday\")) {\n var wdEl = document.getElementById(\"chart-weekday\");\n var wg = layout(wdEl);\n var wdCounts = data.weekdays.map(function (w) { return w.count; });\n var wdMax = Math.max.apply(null, wdCounts.concat([1]));\n init(\"chart-weekday\", Object.assign(baseOpt(), {\n grid: { left: wg.leftCat, right: wg.right, top: wg.top, bottom: wg.bottom },\n xAxis: { type: \"value\", axisLabel: { color: muted, fontSize: wg.fs } },\n yAxis: { type: \"category\", data: data.weekdays.map(function (w) { return w.label; }), axisLabel: { color: muted, fontSize: wg.fs } },\n series: [{\n type: \"bar\",\n data: data.weekdays.map(function (w, i) {\n var c = wdColors[i % 7];\n return {\n value: w.count,\n itemStyle: {\n color: c,\n borderRadius: [0, 6, 6, 0],\n shadowBlur: w.count >= wdMax ? 10 : 0,\n shadowColor: w.count >= wdMax ? (dark ? \"rgba(62,232,197,0.45)\" : \"rgba(15,107,92,0.35)\") : \"transparent\",\n },\n };\n }),\n }],\n }));\n }\n\n if (data.monthly && document.getElementById(\"chart-monthly\")) {\n var moEl = document.getElementById(\"chart-monthly\");\n var mg = layout(moEl);\n var monthLabels = data.monthly.map(function (m) {\n if (mg.w < 480) {\n var p = m.label.split(\"-\");\n return p.length >= 2 ? Number(p[1]) + \"\uC6D4\" : m.label;\n }\n return m.label;\n });\n init(\"chart-monthly\", Object.assign(baseOpt(), {\n grid: { left: mg.left, right: mg.right, top: mg.top, bottom: mg.bottom },\n xAxis: { type: \"category\", data: monthLabels, axisLabel: { color: muted, fontSize: mg.fs, rotate: mg.bottomRot } },\n yAxis: { type: \"value\", axisLabel: { color: muted, fontSize: mg.fs }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n series: [{ type: \"line\", smooth: true, data: data.monthly.map(function (m) { return m.count; }), areaStyle: { opacity: 0.12 }, lineStyle: { width: 2, color: accent2 }, itemStyle: { color: accent2 } }],\n }));\n }\n\n if (data.daily && document.getElementById(\"chart-daily-heat\")) {\n var heatEl = document.getElementById(\"chart-daily-heat\");\n var dg = layout(heatEl);\n var heatMax = Math.max.apply(null, data.daily.map(function (d) { return d.count; }).concat([1]));\n var heat = data.daily.map(function (d) { return [d.date, d.count]; });\n var burstSet = {};\n (data.burstDates || []).forEach(function (d) { burstSet[d] = true; });\n var daySpan = data.daily.length;\n var useBarFallback = daySpan > 0 && daySpan < 90;\n if (useBarFallback) {\n var labels = data.daily.map(function (d) {\n var p = d.date.split(\"-\");\n return p.length === 3 ? Number(p[1]) + \"/\" + Number(p[2]) : d.date;\n });\n init(\"chart-daily-heat\", Object.assign(baseOpt(), {\n grid: { left: dg.left, right: dg.right, top: dg.top, bottom: Math.max(dg.bottom, 52), containLabel: true },\n tooltip: { trigger: \"axis\" },\n xAxis: { type: \"category\", data: labels, axisLabel: { color: muted, fontSize: dg.fs, rotate: labels.length > 20 ? 40 : 0, interval: labels.length > 40 ? Math.floor(labels.length / 20) : 0 } },\n yAxis: { type: \"value\", axisLabel: { color: muted }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n visualMap: { show: false, min: 0, max: heatMax, inRange: { color: dark ? [\"#161b22\", \"#0e4429\", \"#006d32\", \"#26a641\", \"#39d353\"] : [\"#ebedf0\", \"#9be9a8\", \"#40c463\", \"#30a14e\", \"#216e39\"] } },\n series: [{\n type: \"bar\",\n data: data.daily.map(function (d) { return d.count; }),\n itemStyle: { borderRadius: [3, 3, 0, 0], borderWidth: 0 },\n emphasis: { itemStyle: { shadowBlur: 10, shadowColor: dark ? \"rgba(62,232,197,0.45)\" : \"rgba(15,107,92,0.35)\" } },\n }],\n }));\n } else {\n var cellH = dg.w < 380 ? 12 : dg.w < 640 ? 14 : 16;\n var cellW = dg.w < 380 ? 12 : 14;\n init(\"chart-daily-heat\", Object.assign(baseOpt(), {\n tooltip: { position: \"top\" },\n visualMap: { min: 0, max: heatMax, calculable: true, orient: \"horizontal\", left: \"center\", bottom: 0, textStyle: { color: muted, fontSize: dg.fs }, itemWidth: dg.w < 380 ? 10 : 14, itemHeight: dg.w < 380 ? 60 : 80, inRange: { color: dark ? [\"#161b22\", \"#0e4429\", \"#006d32\", \"#26a641\", \"#39d353\"] : [\"#ebedf0\", \"#9be9a8\", \"#40c463\", \"#30a14e\", \"#216e39\"] } },\n calendar: { top: dg.w < 380 ? 28 : 36, left: dg.left, right: dg.right, cellSize: [cellW, cellH], range: data.daily.length ? [data.daily[0].date, data.daily[data.daily.length - 1].date] : undefined, itemStyle: { borderWidth: 0, borderColor: \"transparent\" }, dayLabel: { color: muted, fontSize: dg.fs }, monthLabel: { color: muted, fontSize: dg.fs } },\n series: [{ type: \"heatmap\", coordinateSystem: \"calendar\", data: heat, emphasis: { itemStyle: { shadowBlur: 8, shadowColor: dark ? \"rgba(62,232,197,0.5)\" : \"rgba(33,110,57,0.45)\" } } }],\n }));\n }\n }\n\n if (data.keywords && document.getElementById(\"chart-kw-cloud\")) {\n var cloudEl = document.getElementById(\"chart-kw-cloud\");\n var cg = layout(cloudEl);\n var cloud = data.keywords.slice(0, 100).map(function (k) {\n return { name: k.label, value: k.count };\n });\n var sizeLo = cg.w < 380 ? 10 : 12;\n var sizeHi = cg.w < 380 ? 34 : cg.w < 640 ? 46 : 56;\n init(\"chart-kw-cloud\", {\n textStyle: baseOpt().textStyle,\n tooltip: { show: true },\n series: [{\n type: \"wordCloud\",\n shape: \"circle\",\n gridSize: cg.w < 380 ? 8 : 6,\n sizeRange: [sizeLo, sizeHi],\n rotationRange: [-45, 45],\n textStyle: {\n fontFamily: \"Pretendard, Apple SD Gothic Neo, sans-serif\",\n color: function () {\n var palette = dark ? [\"#3ee8c5\", \"#818cf8\", \"#fbbf24\", \"#fb923c\", \"#f472b6\"] : [\"#0f6b5c\", \"#4f46e5\", \"#b8860b\", \"#c45c2a\", \"#be185d\"];\n return palette[Math.floor(Math.random() * palette.length)];\n },\n },\n data: cloud,\n }],\n });\n }\n\n if (data.sentiment && document.getElementById(\"chart-sentiment\")) {\n var sentEl = document.getElementById(\"chart-sentiment\");\n var sg = layout(sentEl);\n var s = data.sentiment;\n init(\"chart-sentiment\", Object.assign(baseOpt(), {\n tooltip: { trigger: \"item\", formatter: \"{b}: {c}%\" },\n legend: { bottom: 0, textStyle: { color: muted, fontSize: sg.fs } },\n series: [{\n type: \"pie\",\n radius: sg.w < 380 ? [\"32%\", \"58%\"] : [\"35%\", \"62%\"],\n center: [\"50%\", \"46%\"],\n data: [\n { name: \"\uAE0D\uC815\", value: s.positivePercent },\n { name: \"\uC911\uB9BD\", value: s.neutralPercent },\n { name: \"\uBD80\uC815\", value: s.negativePercent },\n ],\n label: { color: text, fontSize: sg.fs },\n }],\n }));\n }\n\n if (data.topicsThemes && data.topicsThemes.length && document.getElementById(\"chart-topics\")) {\n var topEl = document.getElementById(\"chart-topics\");\n var tg = layout(topEl);\n var topics = data.topicsThemes.slice(0, 12);\n init(\"chart-topics\", Object.assign(baseOpt(), {\n grid: { left: Math.max(tg.leftCat, tg.w < 380 ? 72 : 96), right: tg.right, top: tg.top, bottom: tg.bottom },\n xAxis: { type: \"value\", axisLabel: { color: muted, fontSize: tg.fs, formatter: \"{value}%\" } },\n yAxis: {\n type: \"category\",\n data: topics.map(function (t) { return t.title; }).reverse(),\n axisLabel: { color: text, fontSize: tg.fs },\n },\n series: [{\n type: \"bar\",\n data: topics.map(function (t) { return t.messagePercent; }).reverse(),\n itemStyle: {\n borderRadius: [0, 6, 6, 0],\n color: function (p) { return p.dataIndex % 2 === 0 ? accent : accent2; },\n },\n }],\n }));\n }\n\n if (data.topicTrend && data.topicTrend.length && document.getElementById(\"chart-topic-trend\")) {\n var ttEl = document.getElementById(\"chart-topic-trend\");\n var tg = layout(ttEl);\n var periods = data.topicTrend.map(function (t) { return t.period; });\n var allNames = [];\n var nameSet = {};\n data.topicTrend.forEach(function (t) {\n t.topics.forEach(function (topic) {\n if (!nameSet[topic.name]) {\n nameSet[topic.name] = true;\n allNames.push(topic.name);\n }\n });\n });\n var series = allNames.map(function (name, idx) {\n return {\n name: name,\n type: \"line\",\n stack: \"Total\",\n areaStyle: { opacity: 0.18 },\n lineStyle: { width: 1.5 },\n symbol: \"circle\",\n symbolSize: 4,\n emphasis: { focus: \"series\" },\n data: data.topicTrend.map(function (t) {\n var found = t.topics.find(function (topic) { return topic.name === name; });\n return found ? found.value : 0;\n }),\n };\n });\n var ttColors = [\n dark ? \"#818cf8\" : \"#4f46e5\",\n dark ? \"#3ee8c5\" : \"#0f6b5c\",\n dark ? \"#34d399\" : \"#059669\",\n dark ? \"#2dd4bf\" : \"#0d9488\",\n dark ? \"#38bdf8\" : \"#0284c7\",\n dark ? \"#fbbf24\" : \"#d97706\",\n dark ? \"#fb923c\" : \"#ea580c\",\n dark ? \"#f472b6\" : \"#be185d\",\n ];\n init(\"chart-topic-trend\", Object.assign(baseOpt(), {\n grid: { left: tg.leftCat, right: tg.right, top: tg.top, bottom: Math.max(tg.bottom, 60) },\n tooltip: { trigger: \"axis\", backgroundColor: dark ? \"#1c2128\" : \"#fff\" },\n legend: { type: \"scroll\", bottom: 0, textStyle: { color: muted, fontSize: tg.fs }, pageIconColor: accent, pageTextStyle: { color: muted } },\n xAxis: { type: \"category\", data: periods, axisLabel: { color: muted, fontSize: tg.fs } },\n yAxis: { type: \"value\", axisLabel: { color: muted, fontSize: tg.fs }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n color: ttColors,\n series: series,\n }));\n }\n\n if (data.domains && document.getElementById(\"chart-domains\")) {\n var domEl = document.getElementById(\"chart-domains\");\n var domg = layout(domEl);\n init(\"chart-domains\", Object.assign(baseOpt(), {\n tooltip: { trigger: \"item\" },\n series: [{\n type: \"treemap\",\n data: data.domains.map(function (d) { return { name: d.label, value: d.count }; }),\n label: {\n color: text,\n fontSize: 11,\n formatter: function (p) {\n var name = p.name;\n if (domg.w < 480 && name.length > 12) return name.slice(0, 10) + \"...\";\n if (name.length > 20) return name.slice(0, 18) + \"...\";\n return name;\n }\n },\n itemStyle: { borderColor: dark ? \"#0d1117\" : \"#fff\", gapWidth: 2 },\n }],\n }));\n }\n\n if (data.interaction && document.getElementById(\"chart-network\")) {\n var netEl = document.getElementById(\"chart-network\");\n var ng = layout(netEl);\n var ix = data.interaction;\n var aliases = ix.aliases;\n var matrix = ix.matrix;\n var msgCounts = ix.messageCounts || [];\n var maxNode = 15;\n var indices = aliases.map(function (_, i) { return i; })\n .sort(function (a, b) { return (msgCounts[b] || 0) - (msgCounts[a] || 0); })\n .slice(0, maxNode);\n var idxMap = {};\n indices.forEach(function (idx, i) { idxMap[idx] = i; });\n var nodes = indices.map(function (idx) {\n var msgCount = msgCounts[idx] || 0;\n var totalReplies = 0;\n for (var ci = 0; ci < matrix.length; ci += 1) {\n totalReplies += (matrix[ci] && matrix[ci][idx]) || 0;\n totalReplies += (matrix[idx] && matrix[idx][ci]) || 0;\n }\n var size = Math.max(20, Math.min(80, Math.sqrt(msgCount) * 2));\n return {\n name: aliases[idx],\n value: totalReplies,\n symbolSize: size,\n label: { show: true, fontSize: ng.w < 380 ? 9 : 11 }\n };\n });\n var links = [];\n var maxLink = 1;\n for (var ri = 0; ri < indices.length; ri += 1) {\n for (var ci = 0; ci < indices.length; ci += 1) {\n if (ri === ci) continue;\n var src = indices[ri];\n var tgt = indices[ci];\n var v = (matrix[src] && matrix[src][tgt]) || 0;\n if (v >= 50) {\n if (v > maxLink) maxLink = v;\n links.push({\n source: aliases[src],\n target: aliases[tgt],\n value: v,\n lineStyle: { width: Math.max(1, Math.min(8, v / maxLink * 6)), curveness: 0.2 }\n });\n }\n }\n }\n if (nodes.length > 0 && links.length > 0) {\n init(\"chart-network\", Object.assign(baseOpt(), {\n tooltip: {\n formatter: function (p) {\n if (p.dataType === \"edge\") {\n return p.data.source + \" \u2192 \" + p.data.target + \": \" + p.data.value;\n }\n return p.data.name + \"<br/>\uBA54\uC2DC\uC9C0: \" + (msgCounts[indices[p.dataIndex]] || 0) + \"<br/>\uC751\uB2F5: \" + p.data.value;\n }\n },\n series: [{\n type: \"graph\",\n layout: \"force\",\n data: nodes,\n links: links,\n roam: true,\n force: {\n repulsion: ng.w < 380 ? 200 : 300,\n edgeLength: ng.w < 380 ? 60 : 100,\n gravity: 0.1\n },\n label: {\n show: true,\n color: text,\n fontSize: ng.w < 380 ? 9 : 11\n },\n lineStyle: {\n color: dark ? \"rgba(62,232,197,0.4)\" : \"rgba(15,107,92,0.35)\",\n curveness: 0.2\n },\n emphasis: {\n focus: \"adjacency\",\n lineStyle: { width: 4 }\n }\n }]\n }));\n }\n }\n\n kcaDyadBoot(data);\n }\n\n function bindKwSortOnce() {\n if (kwSortBound) return;\n var freqEl = document.getElementById(\"kw-ranked-freq\");\n var distEl = document.getElementById(\"kw-ranked-distinct\");\n if (!freqEl || !distEl) return;\n kwSortBound = true;\n var listF = data.keywords || [];\n var listD = data.keywordsDistinctive || listF;\n document.querySelectorAll(\"[data-kw-sort]\").forEach(function (btn) {\n btn.addEventListener(\"click\", function () {\n var mode = btn.getAttribute(\"data-kw-sort\");\n document.querySelectorAll(\"[data-kw-sort]\").forEach(function (b) {\n var on = b === btn;\n b.classList.toggle(\"is-active\", on);\n b.setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n });\n freqEl.hidden = mode !== \"freq\";\n distEl.hidden = mode !== \"distinct\";\n var src = mode === \"distinct\" ? listD : listF;\n var cloudEl = document.getElementById(\"chart-kw-cloud\");\n if (cloudEl && typeof echarts !== \"undefined\") {\n var inst = echarts.getInstanceByDom(cloudEl);\n if (inst) {\n inst.setOption({\n series: [{\n data: src.slice(0, 100).map(function (k) { return { name: k.label, value: k.count }; }),\n }],\n });\n }\n }\n });\n });\n }\n\n function installChartHooks() {\n if (chartHooksInstalled) return;\n chartHooksInstalled = true;\n function onThemeChange() {\n disposeCharts();\n paintCharts();\n }\n var themeObs = new MutationObserver(function () { setTimeout(onThemeChange, 60); });\n themeObs.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\"] });\n var mqOsTheme = window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\");\n function onOsThemeChange() {\n if (document.documentElement.getAttribute(\"data-theme\")) return;\n onThemeChange();\n }\n if (mqOsTheme && mqOsTheme.addEventListener) {\n mqOsTheme.addEventListener(\"change\", onOsThemeChange);\n } else if (mqOsTheme && mqOsTheme.addListener) {\n mqOsTheme.addListener(onOsThemeChange);\n }\n var mqWide = window.matchMedia && window.matchMedia(\"(min-width: 900px)\");\n if (mqWide && mqWide.addEventListener) {\n mqWide.addEventListener(\"change\", function () { setTimeout(resizeAll, 80); });\n } else if (mqWide && mqWide.addListener) {\n mqWide.addListener(function () { setTimeout(resizeAll, 80); });\n }\n }\n\n paintCharts();\n installChartHooks();\n bindKwSortOnce();\n if (!resizeListenersBound) {\n resizeListenersBound = true;\n requestAnimationFrame(resizeAll);\n setTimeout(resizeAll, 150);\n window.addEventListener(\"resize\", resizeAll);\n window.addEventListener(\"load\", resizeAll);\n }\n }\n function whenVisible() {\n var anchor = document.getElementById(\"s-viz\") || document.querySelector(\".chart-box\");\n if (!anchor || typeof IntersectionObserver === \"undefined\") {\n run();\n return;\n }\n var started = false;\n var io = new IntersectionObserver(function (entries) {\n if (started) return;\n if (entries.some(function (e) { return e.isIntersecting; })) {\n started = true;\n io.disconnect();\n run();\n }\n }, { rootMargin: \"480px 0px\", threshold: 0.01 });\n io.observe(anchor);\n setTimeout(function () {\n if (started) return;\n var r = anchor.getBoundingClientRect();\n if (r.top < window.innerHeight + 320) {\n started = true;\n io.disconnect();\n run();\n }\n }, 200);\n setTimeout(function () {\n if (started) return;\n started = true;\n try { io.disconnect(); } catch (e) {}\n run();\n }, 300);\n }\n function bootCharts() {\n if (typeof echarts === \"undefined\") return false;\n whenVisible();\n return true;\n }\n if (!bootCharts()) {\n window.addEventListener(\"load\", function () {\n var tries = 0;\n (function wait() {\n if (bootCharts()) return;\n if (++tries > 120) {\n document.querySelectorAll(\".chart-box\").forEach(function (el) {\n if (!el.querySelector(\"canvas\")) {\n el.innerHTML = '<p style=\"margin:0;padding:12px;font-size:12px;color:var(--muted);text-align:center\">ECharts CDN\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.</p>';\n }\n });\n return;\n }\n setTimeout(wait, 50);\n })();\n });\n }\n })();\n";
|
|
84
|
+
export declare const CHARTS_INIT_SCRIPT = "\n (function () {\n var kcaDyadBoot = function () {};\n function run() {\n var dataEl = document.getElementById(\"kca-chart-data\");\n if (!dataEl) return;\n if (typeof echarts === \"undefined\") return;\n var data;\n try { data = JSON.parse(dataEl.textContent || \"{}\"); } catch (e) { return; }\n\n function cssVar(name, fallback) {\n try {\n var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n return v || fallback;\n } catch (e) { return fallback; }\n }\n function isDarkTheme() {\n return document.documentElement.getAttribute(\"data-theme\") === \"dark\" ||\n (!document.documentElement.getAttribute(\"data-theme\") &&\n window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\").matches);\n }\n var dark, text, muted, accent, accent2, heatLo, heatHi, wdColors;\n\n function baseOpt() {\n return {\n textStyle: { color: text, fontFamily: \"Pretendard, Apple SD Gothic Neo, sans-serif\" },\n tooltip: { trigger: \"axis\", backgroundColor: dark ? \"#1c2128\" : \"#fff\", borderColor: \"transparent\" },\n };\n }\n\n var charts = [];\n var dyadChartStarted = false;\n function disposeCharts() {\n dyadChartStarted = false;\n charts.forEach(function (c) {\n try { c.dispose(); } catch (e) {}\n });\n charts.length = 0;\n }\n function markDyadReady(el) {\n el.classList.remove(\"is-loading\");\n el.classList.add(\"is-ready\");\n el.setAttribute(\"aria-busy\", \"false\");\n var sk = el.querySelector(\".chart-skeleton\");\n if (sk) sk.remove();\n }\n function initDyadChart(data) {\n if (!data.interaction || !data.interaction.aliases.length) return null;\n var el = document.getElementById(\"chart-dyad\");\n if (!el) return null;\n var ix = data.interaction;\n var dg = layout(el);\n var heat = [];\n var maxV = 1;\n for (var ri = 0; ri < ix.matrix.length; ri += 1) {\n for (var ci = 0; ci < ix.matrix[ri].length; ci += 1) {\n var v = ix.matrix[ri][ci];\n if (v > maxV) maxV = v;\n if (v > 0) heat.push([ci, ri, v]);\n }\n }\n var splitFill = dark ? [\"rgba(255,255,255,0.03)\", \"rgba(255,255,255,0.06)\"] : [\"rgba(0,0,0,0.02)\", \"rgba(0,0,0,0.05)\"];\n var chart = init(\"chart-dyad\", Object.assign(baseOpt(), {\n animation: false,\n tooltip: { position: \"top\", formatter: function (p) { var v = p.value[2]; return ix.aliases[p.value[1]] + \" \u2192 \" + ix.aliases[p.value[0]] + \": \" + v; } },\n grid: { left: Math.max(dg.leftCat, 80), right: dg.right, top: dg.top, bottom: Math.max(dg.bottom, 72) },\n xAxis: {\n type: \"category\",\n data: ix.aliases,\n axisLabel: { color: muted, fontSize: dg.fs, rotate: 28, margin: 10 },\n splitArea: { show: true, areaStyle: { color: splitFill } },\n },\n yAxis: {\n type: \"category\",\n data: ix.aliases,\n inverse: true,\n axisLabel: { color: muted, fontSize: dg.fs },\n splitArea: { show: true, areaStyle: { color: splitFill } },\n },\n visualMap: {\n min: 0,\n max: maxV,\n calculable: true,\n orient: \"horizontal\",\n left: \"center\",\n bottom: 0,\n itemHeight: dg.w < 380 ? 72 : 88,\n inRange: { color: [heatLo, dark ? \"#2a9d8f\" : \"#7ecfc2\", dark ? \"#3ee8c5\" : \"#0f6b5c\", heatHi] },\n },\n series: [{\n type: \"heatmap\",\n data: heat,\n progressive: 0,\n animation: false,\n label: {\n show: true,\n color: text,\n fontSize: dg.w < 380 ? 8 : 10,\n formatter: function (p) { var v = p.value[2]; return v > 0 ? String(v) : \"\"; },\n },\n emphasis: {\n label: { show: true, color: text, fontSize: dg.w < 380 ? 9 : 11 },\n itemStyle: { shadowBlur: 12, shadowColor: dark ? \"rgba(62,232,197,0.5)\" : \"rgba(15,107,92,0.35)\" },\n },\n itemStyle: { borderWidth: 0 },\n }],\n }));\n if (chart) markDyadReady(el);\n return chart;\n }\n function bootDyadWhenVisible(data) {\n if (!data || !data.interaction || !data.interaction.aliases.length) return;\n var el = document.getElementById(\"chart-dyad\");\n if (!el || dyadChartStarted) return;\n function startDyad() {\n if (dyadChartStarted) return;\n dyadChartStarted = true;\n requestAnimationFrame(function () { initDyadChart(data); });\n }\n if (typeof IntersectionObserver === \"undefined\") {\n startDyad();\n return;\n }\n var dyIo = new IntersectionObserver(function (entries) {\n if (entries.some(function (e) { return e.isIntersecting; })) {\n dyIo.disconnect();\n startDyad();\n }\n }, { rootMargin: \"200px 0px\", threshold: 0.05 });\n dyIo.observe(el);\n setTimeout(function () {\n if (dyadChartStarted) return;\n var r = el.getBoundingClientRect();\n if (r.top < window.innerHeight + 200 && r.bottom > 0) startDyad();\n }, 200);\n }\n kcaDyadBoot = bootDyadWhenVisible;\n function resizeAll() {\n charts.forEach(function (c) {\n try { c.resize(); } catch (e) {}\n });\n }\n function layout(el) {\n var w = (el && el.clientWidth) || 400;\n if (w < 380) {\n return { w: w, left: 28, right: 8, top: 14, bottom: 44, fs: 9, rot: 40, leftCat: 56, bottomRot: 42 };\n }\n if (w < 640) {\n return { w: w, left: 40, right: 10, top: 18, bottom: 34, fs: 10, rot: 26, leftCat: 72, bottomRot: 32 };\n }\n return { w: w, left: 48, right: 14, top: 22, bottom: 28, fs: 11, rot: 0, leftCat: 96, bottomRot: 28 };\n }\n function init(id, opt) {\n var el = document.getElementById(id);\n if (!el) return null;\n try {\n var chart = echarts.init(el, null, { renderer: \"canvas\" });\n chart.setOption(opt);\n charts.push(chart);\n if (typeof ResizeObserver !== \"undefined\") {\n var ro = new ResizeObserver(function () {\n requestAnimationFrame(function () {\n try { chart.resize(); } catch (e) {}\n });\n });\n ro.observe(el);\n }\n return chart;\n } catch (err) {\n console.error(\"[kca-chart]\", id, err);\n el.setAttribute(\"data-chart-failed\", \"1\");\n el.innerHTML = '<p style=\"margin:0;padding:12px;font-size:12px;color:var(--muted);text-align:center\">\uCC28\uD2B8\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC5B4\uC694. \uC0C8\uB85C\uACE0\uCE68\uD558\uAC70\uB098 \uB124\uD2B8\uC6CC\uD06C(CDN)\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.</p>';\n return null;\n }\n }\n var chartHooksInstalled = false;\n var resizeListenersBound = false;\n var kwSortBound = false;\n\n function paintCharts() {\n dark = isDarkTheme();\n text = cssVar(\"--ink\", dark ? \"#e9eef5\" : \"#141a1f\");\n muted = cssVar(\"--muted\", dark ? \"#8b98a8\" : \"#5c6670\");\n accent = cssVar(\"--accent\", dark ? \"#3ee8c5\" : \"#0f6b5c\");\n accent2 = cssVar(\"--accent2\", dark ? \"#818cf8\" : \"#4f46e5\");\n heatLo = cssVar(\"--chart-heat-lo\", dark ? \"#1a2744\" : \"#d4e4f4\");\n heatHi = cssVar(\"--chart-heat-hi\", dark ? \"#5ee8ff\" : \"#1e4fd6\");\n wdColors = [\n cssVar(\"--chart-wd-0\", dark ? \"#818cf8\" : \"#4f46e5\"),\n cssVar(\"--chart-wd-1\", dark ? \"#3ee8c5\" : \"#0f6b5c\"),\n cssVar(\"--chart-wd-2\", dark ? \"#34d399\" : \"#059669\"),\n cssVar(\"--chart-wd-3\", dark ? \"#2dd4bf\" : \"#0d9488\"),\n cssVar(\"--chart-wd-4\", dark ? \"#38bdf8\" : \"#0284c7\"),\n cssVar(\"--chart-wd-5\", dark ? \"#fbbf24\" : \"#d97706\"),\n cssVar(\"--chart-wd-6\", dark ? \"#fb923c\" : \"#ea580c\"),\n ];\n\n function hourBarColor(h) {\n if (h <= 5) return dark ? \"#6366f1\" : \"#4f46e5\";\n if (h <= 11) return dark ? \"#3ee8c5\" : \"#0f6b5c\";\n if (h <= 17) return dark ? \"#fbbf24\" : \"#d97706\";\n return dark ? \"#3b82f6\" : \"#1d4ed8\";\n }\n if (data.hourly && document.getElementById(\"chart-hours\")) {\n var hoursEl = document.getElementById(\"chart-hours\");\n var hg = layout(hoursEl);\n init(\"chart-hours\", Object.assign(baseOpt(), {\n grid: { left: hg.left, right: hg.right, top: hg.top, bottom: hg.bottom },\n xAxis: { type: \"category\", data: data.hourly.map(function (_, h) { return h + \"\uC2DC\"; }), axisLabel: { color: muted, fontSize: hg.fs, rotate: hg.rot, interval: hg.w < 480 ? 2 : 0 } },\n yAxis: { type: \"value\", axisLabel: { color: muted }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n series: [{\n type: \"bar\",\n data: data.hourly.map(function (v, h) {\n return { value: v, itemStyle: { color: hourBarColor(h), borderRadius: [4, 4, 0, 0] } };\n }),\n markArea: {\n silent: true,\n itemStyle: { color: dark ? \"rgba(99,102,241,0.12)\" : \"rgba(79,70,229,0.08)\" },\n data: [[{ xAxis: \"0\uC2DC\" }, { xAxis: \"5\uC2DC\" }], [{ xAxis: \"22\uC2DC\" }, { xAxis: \"23\uC2DC\" }]],\n },\n }],\n }));\n }\n\n if (data.weekdays && document.getElementById(\"chart-weekday\")) {\n var wdEl = document.getElementById(\"chart-weekday\");\n var wg = layout(wdEl);\n var wdCounts = data.weekdays.map(function (w) { return w.count; });\n var wdMax = Math.max.apply(null, wdCounts.concat([1]));\n init(\"chart-weekday\", Object.assign(baseOpt(), {\n grid: { left: wg.leftCat, right: wg.right, top: wg.top, bottom: wg.bottom },\n xAxis: { type: \"value\", axisLabel: { color: muted, fontSize: wg.fs } },\n yAxis: { type: \"category\", data: data.weekdays.map(function (w) { return w.label; }), axisLabel: { color: muted, fontSize: wg.fs } },\n series: [{\n type: \"bar\",\n data: data.weekdays.map(function (w, i) {\n var c = wdColors[i % 7];\n return {\n value: w.count,\n itemStyle: {\n color: c,\n borderRadius: [0, 6, 6, 0],\n shadowBlur: w.count >= wdMax ? 10 : 0,\n shadowColor: w.count >= wdMax ? (dark ? \"rgba(62,232,197,0.45)\" : \"rgba(15,107,92,0.35)\") : \"transparent\",\n },\n };\n }),\n }],\n }));\n }\n\n if (data.monthly && document.getElementById(\"chart-monthly\")) {\n var moEl = document.getElementById(\"chart-monthly\");\n var mg = layout(moEl);\n var monthLabels = data.monthly.map(function (m) {\n if (mg.w < 480) {\n var p = m.label.split(\"-\");\n return p.length >= 2 ? Number(p[1]) + \"\uC6D4\" : m.label;\n }\n return m.label;\n });\n init(\"chart-monthly\", Object.assign(baseOpt(), {\n grid: { left: mg.left, right: mg.right, top: mg.top, bottom: mg.bottom },\n xAxis: { type: \"category\", data: monthLabels, axisLabel: { color: muted, fontSize: mg.fs, rotate: mg.bottomRot } },\n yAxis: { type: \"value\", axisLabel: { color: muted, fontSize: mg.fs }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n series: [{ type: \"line\", smooth: true, data: data.monthly.map(function (m) { return m.count; }), areaStyle: { opacity: 0.12 }, lineStyle: { width: 2, color: accent2 }, itemStyle: { color: accent2 } }],\n }));\n }\n\n if (data.daily && document.getElementById(\"chart-daily-heat\")) {\n var heatEl = document.getElementById(\"chart-daily-heat\");\n var dg = layout(heatEl);\n var heatMax = Math.max.apply(null, data.daily.map(function (d) { return d.count; }).concat([1]));\n var heat = data.daily.map(function (d) { return [d.date, d.count]; });\n var burstSet = {};\n (data.burstDates || []).forEach(function (d) { burstSet[d] = true; });\n var daySpan = data.daily.length;\n var useBarFallback = daySpan > 0 && daySpan < 90;\n if (useBarFallback) {\n var labels = data.daily.map(function (d) {\n var p = d.date.split(\"-\");\n return p.length === 3 ? Number(p[1]) + \"/\" + Number(p[2]) : d.date;\n });\n init(\"chart-daily-heat\", Object.assign(baseOpt(), {\n grid: { left: dg.left, right: dg.right, top: dg.top, bottom: Math.max(dg.bottom, 52), containLabel: true },\n tooltip: { trigger: \"axis\" },\n xAxis: { type: \"category\", data: labels, axisLabel: { color: muted, fontSize: dg.fs, rotate: labels.length > 20 ? 40 : 0, interval: labels.length > 40 ? Math.floor(labels.length / 20) : 0 } },\n yAxis: { type: \"value\", axisLabel: { color: muted }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n visualMap: { show: false, min: 0, max: heatMax, inRange: { color: dark ? [\"#161b22\", \"#0e4429\", \"#006d32\", \"#26a641\", \"#39d353\"] : [\"#ebedf0\", \"#9be9a8\", \"#40c463\", \"#30a14e\", \"#216e39\"] } },\n series: [{\n type: \"bar\",\n data: data.daily.map(function (d) { return d.count; }),\n itemStyle: { borderRadius: [3, 3, 0, 0], borderWidth: 0 },\n emphasis: { itemStyle: { shadowBlur: 10, shadowColor: dark ? \"rgba(62,232,197,0.45)\" : \"rgba(15,107,92,0.35)\" } },\n }],\n }));\n } else {\n var cellH = dg.w < 380 ? 12 : dg.w < 640 ? 14 : 16;\n var cellW = dg.w < 380 ? 12 : 14;\n init(\"chart-daily-heat\", Object.assign(baseOpt(), {\n tooltip: { position: \"top\" },\n visualMap: { min: 0, max: heatMax, calculable: true, orient: \"horizontal\", left: \"center\", bottom: 0, textStyle: { color: muted, fontSize: dg.fs }, itemWidth: dg.w < 380 ? 10 : 14, itemHeight: dg.w < 380 ? 60 : 80, inRange: { color: dark ? [\"#161b22\", \"#0e4429\", \"#006d32\", \"#26a641\", \"#39d353\"] : [\"#ebedf0\", \"#9be9a8\", \"#40c463\", \"#30a14e\", \"#216e39\"] } },\n calendar: { top: dg.w < 380 ? 28 : 36, left: dg.left, right: dg.right, cellSize: [cellW, cellH], range: data.daily.length ? [data.daily[0].date, data.daily[data.daily.length - 1].date] : undefined, itemStyle: { borderWidth: 0, borderColor: \"transparent\" }, dayLabel: { color: muted, fontSize: dg.fs }, monthLabel: { color: muted, fontSize: dg.fs } },\n series: [{ type: \"heatmap\", coordinateSystem: \"calendar\", data: heat, emphasis: { itemStyle: { shadowBlur: 8, shadowColor: dark ? \"rgba(62,232,197,0.5)\" : \"rgba(33,110,57,0.45)\" } } }],\n }));\n }\n }\n\n if (data.keywords && document.getElementById(\"chart-kw-cloud\")) {\n var cloudEl = document.getElementById(\"chart-kw-cloud\");\n var cg = layout(cloudEl);\n var cloud = data.keywords.slice(0, 100).map(function (k) {\n return { name: k.label, value: k.count };\n });\n var sizeLo = cg.w < 380 ? 10 : 12;\n var sizeHi = cg.w < 380 ? 34 : cg.w < 640 ? 46 : 56;\n init(\"chart-kw-cloud\", {\n textStyle: baseOpt().textStyle,\n tooltip: { show: true },\n series: [{\n type: \"wordCloud\",\n shape: \"circle\",\n gridSize: cg.w < 380 ? 8 : 6,\n sizeRange: [sizeLo, sizeHi],\n rotationRange: [-45, 45],\n textStyle: {\n fontFamily: \"Pretendard, Apple SD Gothic Neo, sans-serif\",\n color: function () {\n var palette = dark ? [\"#3ee8c5\", \"#818cf8\", \"#fbbf24\", \"#fb923c\", \"#f472b6\"] : [\"#0f6b5c\", \"#4f46e5\", \"#b8860b\", \"#c45c2a\", \"#be185d\"];\n return palette[Math.floor(Math.random() * palette.length)];\n },\n },\n data: cloud,\n }],\n });\n }\n\n if (data.sentiment && document.getElementById(\"chart-sentiment\")) {\n var sentEl = document.getElementById(\"chart-sentiment\");\n var sg = layout(sentEl);\n var s = data.sentiment;\n init(\"chart-sentiment\", Object.assign(baseOpt(), {\n tooltip: { trigger: \"item\", formatter: \"{b}: {c}%\" },\n legend: { bottom: 0, textStyle: { color: muted, fontSize: sg.fs } },\n series: [{\n type: \"pie\",\n radius: sg.w < 380 ? [\"32%\", \"58%\"] : [\"35%\", \"62%\"],\n center: [\"50%\", \"46%\"],\n data: [\n { name: \"\uAE0D\uC815\", value: s.positivePercent },\n { name: \"\uC911\uB9BD\", value: s.neutralPercent },\n { name: \"\uBD80\uC815\", value: s.negativePercent },\n ],\n label: { color: text, fontSize: sg.fs },\n }],\n }));\n }\n\n if (data.topicsThemes && data.topicsThemes.length && document.getElementById(\"chart-topics\")) {\n var topEl = document.getElementById(\"chart-topics\");\n var tg = layout(topEl);\n var topics = data.topicsThemes.slice(0, 12);\n init(\"chart-topics\", Object.assign(baseOpt(), {\n grid: { left: Math.max(tg.leftCat, tg.w < 380 ? 72 : 96), right: tg.right, top: tg.top, bottom: tg.bottom },\n xAxis: { type: \"value\", axisLabel: { color: muted, fontSize: tg.fs, formatter: \"{value}%\" } },\n yAxis: {\n type: \"category\",\n data: topics.map(function (t) { return t.title; }).reverse(),\n axisLabel: { color: text, fontSize: tg.fs },\n },\n series: [{\n type: \"bar\",\n data: topics.map(function (t) { return t.messagePercent; }).reverse(),\n itemStyle: {\n borderRadius: [0, 6, 6, 0],\n color: function (p) { return p.dataIndex % 2 === 0 ? accent : accent2; },\n },\n }],\n }));\n }\n\n if (data.topicTrend && data.topicTrend.length && document.getElementById(\"chart-topic-trend\")) {\n var ttEl = document.getElementById(\"chart-topic-trend\");\n var tg = layout(ttEl);\n var periods = data.topicTrend.map(function (t) { return t.period; });\n var allNames = [];\n var nameSet = {};\n data.topicTrend.forEach(function (t) {\n t.topics.forEach(function (topic) {\n if (!nameSet[topic.name]) {\n nameSet[topic.name] = true;\n allNames.push(topic.name);\n }\n });\n });\n var series = allNames.map(function (name, idx) {\n return {\n name: name,\n type: \"line\",\n stack: \"Total\",\n areaStyle: { opacity: 0.18 },\n lineStyle: { width: 1.5 },\n symbol: \"circle\",\n symbolSize: 4,\n emphasis: { focus: \"series\" },\n data: data.topicTrend.map(function (t) {\n var found = t.topics.find(function (topic) { return topic.name === name; });\n return found ? found.value : 0;\n }),\n };\n });\n var ttColors = [\n dark ? \"#818cf8\" : \"#4f46e5\",\n dark ? \"#3ee8c5\" : \"#0f6b5c\",\n dark ? \"#34d399\" : \"#059669\",\n dark ? \"#2dd4bf\" : \"#0d9488\",\n dark ? \"#38bdf8\" : \"#0284c7\",\n dark ? \"#fbbf24\" : \"#d97706\",\n dark ? \"#fb923c\" : \"#ea580c\",\n dark ? \"#f472b6\" : \"#be185d\",\n ];\n init(\"chart-topic-trend\", Object.assign(baseOpt(), {\n grid: { left: tg.leftCat, right: tg.right, top: tg.top, bottom: Math.max(tg.bottom, 60) },\n tooltip: { trigger: \"axis\", backgroundColor: dark ? \"#1c2128\" : \"#fff\" },\n legend: { type: \"scroll\", bottom: 0, textStyle: { color: muted, fontSize: tg.fs }, pageIconColor: accent, pageTextStyle: { color: muted } },\n xAxis: { type: \"category\", data: periods, axisLabel: { color: muted, fontSize: tg.fs } },\n yAxis: { type: \"value\", axisLabel: { color: muted, fontSize: tg.fs }, splitLine: { lineStyle: { color: dark ? \"rgba(255,255,255,0.06)\" : \"rgba(0,0,0,0.06)\" } } },\n color: ttColors,\n series: series,\n }));\n }\n\n if (data.domains && document.getElementById(\"chart-domains\")) {\n var domEl = document.getElementById(\"chart-domains\");\n var domg = layout(domEl);\n init(\"chart-domains\", Object.assign(baseOpt(), {\n tooltip: { trigger: \"item\" },\n series: [{\n type: \"treemap\",\n data: data.domains.map(function (d) { return { name: d.label, value: d.count }; }),\n label: {\n color: text,\n fontSize: 11,\n formatter: function (p) {\n var name = p.name;\n if (domg.w < 480 && name.length > 12) return name.slice(0, 10) + \"...\";\n if (name.length > 20) return name.slice(0, 18) + \"...\";\n return name;\n }\n },\n itemStyle: { borderColor: dark ? \"#0d1117\" : \"#fff\", gapWidth: 2 },\n }],\n }));\n }\n\n if (data.interaction && document.getElementById(\"chart-network\")) {\n var netEl = document.getElementById(\"chart-network\");\n var ng = layout(netEl);\n var ix = data.interaction;\n var aliases = ix.aliases;\n var matrix = ix.matrix;\n var msgCounts = ix.messageCounts || [];\n\n // \uC751\uB2F5 \uCD1D\uB7C9(\uC218\uC2E0+\uBC1C\uC2E0) \uAE30\uC900 \uC815\uB82C \u2014 \uC751\uB2F5 \uB124\uD2B8\uC6CC\uD06C\uC5D0 \uC801\uD569\n var maxNode = 15;\n var replyCounts = aliases.map(function (_, idx) {\n var total = 0;\n for (var ci = 0; ci < matrix.length; ci += 1) {\n total += (matrix[ci] && matrix[ci][idx]) || 0;\n total += (matrix[idx] && matrix[idx][ci]) || 0;\n }\n return total;\n });\n var indices = aliases.map(function (_, i) { return i; })\n .sort(function (a, b) { return (replyCounts[b] || 0) - (replyCounts[a] || 0); })\n .slice(0, maxNode);\n\n // \uBAA8\uB4E0 \uC5E3\uC9C0 \uC218\uC9D1 (\uC784\uACC4\uAC12 \uC5C6\uC774)\n var allLinks = [];\n var maxLink = 1;\n for (var ri = 0; ri < indices.length; ri += 1) {\n for (var ci = 0; ci < indices.length; ci += 1) {\n if (ri === ci) continue;\n var src = indices[ri];\n var tgt = indices[ci];\n var v = (matrix[src] && matrix[src][tgt]) || 0;\n if (v > 0) {\n if (v > maxLink) maxLink = v;\n allLinks.push({ source: aliases[src], target: aliases[tgt], value: v });\n }\n }\n }\n\n // \uC0C1\uC704 \uC751\uB2F5 \uC778\uC0AC\uC774\uD2B8 \uC0DD\uC131\n var insightEl = document.getElementById(\"chart-network-insight\");\n if (insightEl) {\n var sortedLinks = allLinks.slice().sort(function (a, b) { return b.value - a.value; });\n function esc(s) { return String(s).replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\"); }\n var insightItems = sortedLinks.slice(0, 3).map(function (l) {\n return '<span class=\"network-insight-item\"><strong>' + esc(l.source) + '</strong> <span class=\"network-arrow\">\u2192</span> <strong>' + esc(l.target) + '</strong> <span class=\"network-count\">' + esc(l.value) + '\uD68C</span></span>';\n });\n if (insightItems.length > 0) {\n insightEl.innerHTML = '<span class=\"network-insight-label\">\uD83D\uDCA1 \uC0C1\uC704 \uC751\uB2F5 \uD750\uB984</span>' + insightItems.join('<span class=\"network-insight-sep\">\u00B7</span>') + '<span class=\"network-insight-note\"> (\uC804\uCCB4 \uAE30\uC900)</span>';\n }\n }\n\n // \uAE30\uBCF8 \uC784\uACC4\uAC12: \uB370\uC774\uD130 \uAE30\uBC18 \uC790\uB3D9 \uC124\uC815 (\uCD5C\uB300\uAC12\uC758 2%, \uCD5C\uC18C 2)\n var defaultThreshold = Math.max(2, Math.min(10, Math.floor(maxLink * 0.02)));\n var currentThreshold = defaultThreshold;\n var networkChart = null;\n\n function buildNetworkChart(threshold) {\n // \uC784\uACC4\uAC12\uC73C\uB85C \uD544\uD130\uB9C1\n var links = [];\n for (var i = 0; i < allLinks.length; i += 1) {\n if (allLinks[i].value >= threshold) links.push(allLinks[i]);\n }\n // \uD45C\uC2DC\uD560 \uC0C1\uC704 N\uAC1C\uB9CC (\uCD5C\uB300 60\uAC1C \u2014 \uAC00\uB3C5\uC131 \uBCF4\uC7A5)\n if (links.length > 60) {\n links.sort(function (a, b) { return b.value - a.value; });\n links = links.slice(0, 60);\n }\n\n var emptyEl = document.getElementById(\"chart-network-empty\");\n if (emptyEl) emptyEl.hidden = links.length > 0 || allLinks.length === 0;\n\n var isMobile = ng.w < 380;\n var isSmall = ng.w < 600;\n\n // \uB178\uB4DC: \uC751\uB2F5 \uCD1D\uB7C9 \uAE30\uBC18 \uD06C\uAE30, \uC6D0\uD615 \uBC30\uCE58\n var nodes = indices.map(function (idx) {\n var totalReplies = replyCounts[idx] || 0;\n // \uBA74\uC801 \uC778\uC2DD \uACE0\uB824 sqrt \uC2A4\uCF00\uC77C\uB9C1, \uCD5C\uC18C 22px ~ \uCD5C\uB300 68px\n var size = Math.max(22, Math.min(68, Math.sqrt(totalReplies + 1) * 3.2));\n return {\n name: aliases[idx],\n value: totalReplies,\n symbolSize: size,\n category: 0,\n label: { show: true }\n };\n });\n\n if (links.length === 0 && allLinks.length > 0) {\n // \uC5E3\uC9C0 \uC5C6\uC73C\uBA74 \uB178\uB4DC\uB9CC \uD45C\uC2DC (\uBE48 \uC6D0\uD615 \uB2E4\uC774\uC5B4\uADF8\uB7A8)\n links = [];\n }\n\n if (nodes.length === 0) return;\n\n // \uC5E3\uC9C0 \uC2A4\uD0C0\uC77C: \uBE44\uB840 \uB450\uAED8 + \uBC29\uD5A5 \uD654\uC0B4\uD45C\n var styledLinks = links.map(function (l) {\n return {\n source: l.source,\n target: l.target,\n value: l.value,\n lineStyle: {\n width: Math.max(1, Math.min(10, (l.value / maxLink) * 8)),\n curveness: 0.3,\n opacity: Math.max(0.2, Math.min(0.8, (l.value / maxLink)))\n }\n };\n });\n\n // \uAE30\uC874 \uCC28\uD2B8 \uC778\uC2A4\uD134\uC2A4 \uC788\uC73C\uBA74 setOption\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8, \uC5C6\uC73C\uBA74 init\n if (networkChart) {\n networkChart.setOption(Object.assign(baseOpt(), {\n animation: true,\n animationDuration: 600,\n animationEasing: \"cubicOut\",\n tooltip: {\n trigger: \"item\",\n formatter: function (p) {\n if (p.dataType === \"edge\") {\n var arrowColor = dark ? \"#5ee8ff\" : \"#0f6b5c\";\n return '<div style=\"font-weight:600;margin-bottom:4px\">' + p.data.source + ' <span style=\"color:' + arrowColor + '\">\u2192</span> ' + p.data.target + '</div><div>\uC751\uB2F5: <strong>' + p.data.value + '\uD68C</strong></div>';\n }\n var idx = aliases.indexOf(p.data.name);\n if (idx < 0) idx = 0;\n return '<div style=\"font-weight:600;margin-bottom:4px\">' + p.data.name + '</div><div>\uBA54\uC2DC\uC9C0: <strong>' + (msgCounts[idx] || 0) + '\uAC74</strong></div><div>\uC751\uB2F5 \uCD1D\uB7C9: <strong>' + (replyCounts[idx] || 0) + '\uD68C</strong></div>';\n }\n },\n series: [{\n type: \"graph\",\n layout: \"circular\",\n circular: { rotateLabel: false },\n data: nodes,\n links: styledLinks,\n roam: isMobile ? false : \"scale\",\n draggable: false,\n edgeSymbol: [\"none\", \"arrow\"],\n edgeSymbolSize: [0, isMobile ? 6 : 9],\n symbol: \"circle\",\n itemStyle: {\n borderColor: dark ? \"#1c2128\" : \"#fff\",\n borderWidth: 2,\n color: dark ? \"#5ee8ff\" : \"#0f6b5c\"\n },\n label: {\n show: true,\n position: \"right\",\n color: text,\n fontSize: isMobile ? 9 : (isSmall ? 10 : 11),\n fontWeight: 600,\n distance: 8,\n formatter: function (p) {\n var name = p.name;\n if (isMobile && name.length > 5) return name.slice(0, 4) + \"..\";\n if (isSmall && name.length > 7) return name.slice(0, 6) + \"..\";\n if (name.length > 10) return name.slice(0, 8) + \"..\";\n return name;\n }\n },\n labelLayout: { hideOverlap: true },\n lineStyle: {\n color: dark ? \"rgba(94,232,255,0.5)\" : \"rgba(15,107,92,0.45)\",\n curveness: 0.3,\n opacity: 0.55\n },\n emphasis: {\n focus: \"adjacency\",\n scale: 1.4,\n label: { fontSize: isMobile ? 11 : 13, fontWeight: 700 },\n itemStyle: { shadowBlur: 18, shadowColor: dark ? \"rgba(94,232,255,0.45)\" : \"rgba(15,107,92,0.3)\" },\n lineStyle: { width: 5, opacity: 1 }\n },\n blur: {\n itemStyle: { opacity: 0.12 },\n lineStyle: { opacity: 0.04 },\n label: { opacity: 0.15 }\n }\n }]\n }), { notMerge: false });\n } else {\n networkChart = init(\"chart-network\", Object.assign(baseOpt(), {\n animation: true,\n animationDuration: 600,\n animationEasing: \"cubicOut\",\n tooltip: {\n trigger: \"item\",\n formatter: function (p) {\n if (p.dataType === \"edge\") {\n var arrowColor2 = dark ? \"#5ee8ff\" : \"#0f6b5c\";\n return '<div style=\"font-weight:600;margin-bottom:4px\">' + p.data.source + ' <span style=\"color:' + arrowColor2 + '\">\u2192</span> ' + p.data.target + '</div><div>\uC751\uB2F5: <strong>' + p.data.value + '\uD68C</strong></div>';\n }\n var idx = aliases.indexOf(p.data.name);\n if (idx < 0) idx = 0;\n return '<div style=\"font-weight:600;margin-bottom:4px\">' + p.data.name + '</div><div>\uBA54\uC2DC\uC9C0: <strong>' + (msgCounts[idx] || 0) + '\uAC74</strong></div><div>\uC751\uB2F5 \uCD1D\uB7C9: <strong>' + (replyCounts[idx] || 0) + '\uD68C</strong></div>';\n }\n },\n series: [{\n type: \"graph\",\n layout: \"circular\",\n circular: { rotateLabel: false },\n data: nodes,\n links: styledLinks,\n roam: isMobile ? false : \"scale\",\n draggable: false,\n edgeSymbol: [\"none\", \"arrow\"],\n edgeSymbolSize: [0, isMobile ? 6 : 9],\n symbol: \"circle\",\n itemStyle: {\n borderColor: dark ? \"#1c2128\" : \"#fff\",\n borderWidth: 2,\n color: dark ? \"#5ee8ff\" : \"#0f6b5c\"\n },\n label: {\n show: true,\n position: \"right\",\n color: text,\n fontSize: isMobile ? 9 : (isSmall ? 10 : 11),\n fontWeight: 600,\n distance: 8,\n formatter: function (p) {\n var name = p.name;\n if (isMobile && name.length > 5) return name.slice(0, 4) + \"..\";\n if (isSmall && name.length > 7) return name.slice(0, 6) + \"..\";\n if (name.length > 10) return name.slice(0, 8) + \"..\";\n return name;\n }\n },\n labelLayout: { hideOverlap: true },\n lineStyle: {\n color: dark ? \"rgba(94,232,255,0.5)\" : \"rgba(15,107,92,0.45)\",\n curveness: 0.3,\n opacity: 0.55\n },\n emphasis: {\n focus: \"adjacency\",\n scale: 1.4,\n label: { fontSize: isMobile ? 11 : 13, fontWeight: 700 },\n itemStyle: { shadowBlur: 18, shadowColor: dark ? \"rgba(94,232,255,0.45)\" : \"rgba(15,107,92,0.3)\" },\n lineStyle: { width: 5, opacity: 1 }\n },\n blur: {\n itemStyle: { opacity: 0.12 },\n lineStyle: { opacity: 0.04 },\n label: { opacity: 0.15 }\n }\n }]\n }));\n }\n }\n\n // \uCD08\uAE30 \uB80C\uB354\uB9C1\n buildNetworkChart(currentThreshold);\n\n // \uC784\uACC4\uAC12 \uC2AC\uB77C\uC774\uB354 \uBC14\uC778\uB529\n var thresholdSlider = document.getElementById(\"network-threshold\");\n var thresholdVal = document.getElementById(\"network-threshold-val\");\n if (thresholdSlider && thresholdVal) {\n thresholdSlider.value = String(defaultThreshold);\n thresholdVal.textContent = String(defaultThreshold);\n thresholdSlider.addEventListener(\"input\", function () {\n var val = parseInt(this.value, 10);\n thresholdVal.textContent = String(val);\n });\n thresholdSlider.addEventListener(\"change\", function () {\n var val = parseInt(this.value, 10);\n currentThreshold = val;\n buildNetworkChart(val);\n });\n }\n }\n\n kcaDyadBoot(data);\n }\n\n function bindKwSortOnce() {\n if (kwSortBound) return;\n var freqEl = document.getElementById(\"kw-ranked-freq\");\n var distEl = document.getElementById(\"kw-ranked-distinct\");\n if (!freqEl || !distEl) return;\n kwSortBound = true;\n var listF = data.keywords || [];\n var listD = data.keywordsDistinctive || listF;\n document.querySelectorAll(\"[data-kw-sort]\").forEach(function (btn) {\n btn.addEventListener(\"click\", function () {\n var mode = btn.getAttribute(\"data-kw-sort\");\n document.querySelectorAll(\"[data-kw-sort]\").forEach(function (b) {\n var on = b === btn;\n b.classList.toggle(\"is-active\", on);\n b.setAttribute(\"aria-pressed\", on ? \"true\" : \"false\");\n });\n freqEl.hidden = mode !== \"freq\";\n distEl.hidden = mode !== \"distinct\";\n var src = mode === \"distinct\" ? listD : listF;\n var cloudEl = document.getElementById(\"chart-kw-cloud\");\n if (cloudEl && typeof echarts !== \"undefined\") {\n var inst = echarts.getInstanceByDom(cloudEl);\n if (inst) {\n inst.setOption({\n series: [{\n data: src.slice(0, 100).map(function (k) { return { name: k.label, value: k.count }; }),\n }],\n });\n }\n }\n });\n });\n }\n\n function installChartHooks() {\n if (chartHooksInstalled) return;\n chartHooksInstalled = true;\n function onThemeChange() {\n disposeCharts();\n paintCharts();\n }\n var themeObs = new MutationObserver(function () { setTimeout(onThemeChange, 60); });\n themeObs.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\"] });\n var mqOsTheme = window.matchMedia && window.matchMedia(\"(prefers-color-scheme: dark)\");\n function onOsThemeChange() {\n if (document.documentElement.getAttribute(\"data-theme\")) return;\n onThemeChange();\n }\n if (mqOsTheme && mqOsTheme.addEventListener) {\n mqOsTheme.addEventListener(\"change\", onOsThemeChange);\n } else if (mqOsTheme && mqOsTheme.addListener) {\n mqOsTheme.addListener(onOsThemeChange);\n }\n var mqWide = window.matchMedia && window.matchMedia(\"(min-width: 900px)\");\n if (mqWide && mqWide.addEventListener) {\n mqWide.addEventListener(\"change\", function () { setTimeout(resizeAll, 80); });\n } else if (mqWide && mqWide.addListener) {\n mqWide.addListener(function () { setTimeout(resizeAll, 80); });\n }\n }\n\n paintCharts();\n installChartHooks();\n bindKwSortOnce();\n if (!resizeListenersBound) {\n resizeListenersBound = true;\n requestAnimationFrame(resizeAll);\n setTimeout(resizeAll, 150);\n window.addEventListener(\"resize\", resizeAll);\n window.addEventListener(\"load\", resizeAll);\n }\n }\n function whenVisible() {\n var anchor = document.getElementById(\"s-viz\") || document.querySelector(\".chart-box\");\n if (!anchor || typeof IntersectionObserver === \"undefined\") {\n run();\n return;\n }\n var started = false;\n var io = new IntersectionObserver(function (entries) {\n if (started) return;\n if (entries.some(function (e) { return e.isIntersecting; })) {\n started = true;\n io.disconnect();\n run();\n }\n }, { rootMargin: \"480px 0px\", threshold: 0.01 });\n io.observe(anchor);\n setTimeout(function () {\n if (started) return;\n var r = anchor.getBoundingClientRect();\n if (r.top < window.innerHeight + 320) {\n started = true;\n io.disconnect();\n run();\n }\n }, 200);\n setTimeout(function () {\n if (started) return;\n started = true;\n try { io.disconnect(); } catch (e) {}\n run();\n }, 300);\n }\n function bootCharts() {\n if (typeof echarts === \"undefined\") return false;\n whenVisible();\n return true;\n }\n if (!bootCharts()) {\n window.addEventListener(\"load\", function () {\n var tries = 0;\n (function wait() {\n if (bootCharts()) return;\n if (++tries > 120) {\n document.querySelectorAll(\".chart-box\").forEach(function (el) {\n if (!el.querySelector(\"canvas\")) {\n el.innerHTML = '<p style=\"margin:0;padding:12px;font-size:12px;color:var(--muted);text-align:center\">ECharts CDN\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.</p>';\n }\n });\n return;\n }\n setTimeout(wait, 50);\n })();\n });\n }\n })();\n";
|
|
@@ -87,45 +87,45 @@ export function renderChartDeck(data) {
|
|
|
87
87
|
const showDailyHeat = !hasCalendarHeatmap(data);
|
|
88
88
|
const showMonthly = showMonthlyChart(data);
|
|
89
89
|
const topicChart = themeCount > 0
|
|
90
|
-
? `<article class="viz-card span-12">
|
|
90
|
+
? `<article class="viz-card kca-card--chart span-12">
|
|
91
91
|
<h3>대화 테마 · c-TF-IDF</h3>
|
|
92
92
|
<p class="viz-hint">막대 = <strong>의미 주제</strong> 신호 비중(근사 %). 월별 메시지량은 「기간 비교」·아래 주제 카드의 월별 화제를 보세요.</p>
|
|
93
93
|
<div id="chart-topics" class="chart-box" role="img" aria-label="주제 테마 차트"></div>
|
|
94
94
|
</article>`
|
|
95
95
|
: "";
|
|
96
|
-
return `<section id="s-viz" class="viz-hero anim-enter" style="--enter-delay:0.055s" aria-label="인터랙티브 차트">
|
|
96
|
+
return `<section id="s-viz" class="kca-section viz-hero anim-enter" style="--enter-delay:0.055s" aria-label="인터랙티브 차트">
|
|
97
97
|
<h2>📊 인터랙티브 차트</h2>
|
|
98
98
|
<p>ECharts 기반 — 막대·히트맵·워드클라우드에 마우스를 올리면 수치를 확인할 수 있어요. 키워드 <strong>${formatNumber(kw)}</strong>개(메시지 등장 횟수 기준).</p>
|
|
99
99
|
</section>
|
|
100
100
|
<div class="viz-grid anim-enter" style="--enter-delay:0.06s">
|
|
101
|
-
<article class="viz-card span-8">
|
|
101
|
+
<article class="viz-card kca-card--chart span-8">
|
|
102
102
|
<h3>키워드 워드클라우드</h3>
|
|
103
103
|
<p class="viz-hint">글자 크기 = 메시지 등장 빈도. Kiwi·BM25로 뽑은 본문 키워드입니다.</p>
|
|
104
104
|
<div id="chart-kw-cloud" class="chart-box tall" role="img" aria-label="키워드 워드클라우드"></div>
|
|
105
105
|
</article>
|
|
106
|
-
<article class="viz-card span-4">
|
|
106
|
+
<article class="viz-card kca-card--chart span-4">
|
|
107
107
|
<h3>시간대 분포</h3>
|
|
108
108
|
<p class="viz-hint">0~23시 메시지량</p>
|
|
109
109
|
<div id="chart-hours" class="chart-box compact" role="img" aria-label="시간대 차트"></div>
|
|
110
110
|
</article>
|
|
111
111
|
${showDailyHeat
|
|
112
|
-
? `<article class="viz-card span-6">
|
|
112
|
+
? `<article class="viz-card kca-card--chart span-6">
|
|
113
113
|
<h3>일별 활동 히트맵</h3>
|
|
114
114
|
<p class="viz-hint">활동 기간만 표시 · 급증일 강조</p>
|
|
115
115
|
<div id="chart-daily-heat" class="chart-box" role="img" aria-label="일별 히트맵"></div>
|
|
116
116
|
</article>`
|
|
117
117
|
: ""}
|
|
118
|
-
<article class="viz-card span-6">
|
|
118
|
+
<article class="viz-card kca-card--chart span-6">
|
|
119
119
|
<h3>요일 분포</h3>
|
|
120
120
|
<p class="viz-hint">요일별 메시지량</p>
|
|
121
121
|
<div id="chart-weekday" class="chart-box compact" role="img" aria-label="요일 차트"></div>
|
|
122
122
|
${showMonthly
|
|
123
|
-
? `<h3
|
|
123
|
+
? `<h3 class="viz-sub-title">월별 추이</h3>
|
|
124
124
|
<p class="viz-hint">월 단위 합계</p>
|
|
125
125
|
<div id="chart-monthly" class="chart-box compact" role="img" aria-label="월별 차트"></div>`
|
|
126
126
|
: ""}
|
|
127
127
|
</article>
|
|
128
|
-
<article class="viz-card span-12">
|
|
128
|
+
<article class="viz-card kca-card--chart span-12">
|
|
129
129
|
<h3>키워드 순위 · 메시지 등장 횟수</h3>
|
|
130
130
|
<p class="viz-hint">막대 길이 = 1위 대비 비율 · 전체 ${formatNumber(kw)}개 · 워드클라우드는 위 카드</p>
|
|
131
131
|
<div class="kw-sort-toggle" role="group" aria-label="키워드 정렬">
|
|
@@ -137,15 +137,25 @@ export function renderChartDeck(data) {
|
|
|
137
137
|
<div id="kw-ranked-distinct" hidden>${renderKeywordRankedList(data.keywordsDistinctive)}</div>
|
|
138
138
|
</div>
|
|
139
139
|
</article>
|
|
140
|
-
<article class="viz-card span-12">
|
|
140
|
+
<article class="viz-card kca-card--chart span-12">
|
|
141
141
|
<h3>공유 도메인</h3>
|
|
142
142
|
<p class="viz-hint">링크 호스트 상위</p>
|
|
143
143
|
<div id="chart-domains" class="chart-box" role="img" aria-label="도메인 차트"></div>
|
|
144
144
|
</article>
|
|
145
|
-
${data.interaction ? `<article class="viz-card span-12">
|
|
145
|
+
${data.interaction ? `<article class="viz-card kca-card--chart span-12">
|
|
146
146
|
<h3>응답 관계 네트워크</h3>
|
|
147
|
-
<p class="viz-hint"
|
|
148
|
-
<div id="chart-network" class="
|
|
147
|
+
<p class="viz-hint">누가 누구에게 답하는지 <strong>원형 아크 다이어그램</strong>으로 보여줍니다. 화살표 = 응답 방향, 선 굵기 = 응답 횟수, 노드 크기 = 응답 총량입니다.</p>
|
|
148
|
+
<div id="chart-network-insight" class="network-insight" aria-live="polite"></div>
|
|
149
|
+
<div class="network-controls">
|
|
150
|
+
<label class="network-filter-label">
|
|
151
|
+
<span>최소 응답</span>
|
|
152
|
+
<input type="range" id="network-threshold" min="1" max="500" value="3" class="network-threshold-slider" aria-label="최소 응답 횟수 필터">
|
|
153
|
+
<span id="network-threshold-val" class="network-threshold-value">3</span>
|
|
154
|
+
<span>회 이상</span>
|
|
155
|
+
</label>
|
|
156
|
+
</div>
|
|
157
|
+
<div id="chart-network" class="chart-box chart-box--network" role="img" aria-label="응답 관계 네트워크"></div>
|
|
158
|
+
<div id="chart-network-empty" class="network-empty" hidden aria-hidden="true">표시할 응답 관계가 없습니다. 최소 응답 횟수를 낮춰보세요.</div>
|
|
149
159
|
</article>` : ""}
|
|
150
160
|
${topicChart}
|
|
151
161
|
${renderTopicTrendCard(data)}
|
|
@@ -154,7 +164,7 @@ export function renderChartDeck(data) {
|
|
|
154
164
|
function renderTopicTrendCard(data) {
|
|
155
165
|
if (data.topicTrend.length < 2)
|
|
156
166
|
return "";
|
|
157
|
-
return `<article class="viz-card span-12">
|
|
167
|
+
return `<article class="viz-card kca-card--chart span-12">
|
|
158
168
|
<h3>토픽 트랜드 · 월별 키워드</h3>
|
|
159
169
|
<p class="viz-hint">월별 상위 키워드 등장 횟수 추이. 스택드 에어리어 차트입니다.</p>
|
|
160
170
|
<div id="chart-topic-trend" class="chart-box" role="img" aria-label="토픽 트랜드 차트"></div>
|
|
@@ -662,28 +672,23 @@ export const CHARTS_INIT_SCRIPT = `
|
|
|
662
672
|
var aliases = ix.aliases;
|
|
663
673
|
var matrix = ix.matrix;
|
|
664
674
|
var msgCounts = ix.messageCounts || [];
|
|
675
|
+
|
|
676
|
+
// 응답 총량(수신+발신) 기준 정렬 — 응답 네트워크에 적합
|
|
665
677
|
var maxNode = 15;
|
|
666
|
-
var
|
|
667
|
-
|
|
668
|
-
.slice(0, maxNode);
|
|
669
|
-
var idxMap = {};
|
|
670
|
-
indices.forEach(function (idx, i) { idxMap[idx] = i; });
|
|
671
|
-
var nodes = indices.map(function (idx) {
|
|
672
|
-
var msgCount = msgCounts[idx] || 0;
|
|
673
|
-
var totalReplies = 0;
|
|
678
|
+
var replyCounts = aliases.map(function (_, idx) {
|
|
679
|
+
var total = 0;
|
|
674
680
|
for (var ci = 0; ci < matrix.length; ci += 1) {
|
|
675
|
-
|
|
676
|
-
|
|
681
|
+
total += (matrix[ci] && matrix[ci][idx]) || 0;
|
|
682
|
+
total += (matrix[idx] && matrix[idx][ci]) || 0;
|
|
677
683
|
}
|
|
678
|
-
|
|
679
|
-
return {
|
|
680
|
-
name: aliases[idx],
|
|
681
|
-
value: totalReplies,
|
|
682
|
-
symbolSize: size,
|
|
683
|
-
label: { show: true, fontSize: ng.w < 380 ? 9 : 11 }
|
|
684
|
-
};
|
|
684
|
+
return total;
|
|
685
685
|
});
|
|
686
|
-
var
|
|
686
|
+
var indices = aliases.map(function (_, i) { return i; })
|
|
687
|
+
.sort(function (a, b) { return (replyCounts[b] || 0) - (replyCounts[a] || 0); })
|
|
688
|
+
.slice(0, maxNode);
|
|
689
|
+
|
|
690
|
+
// 모든 엣지 수집 (임계값 없이)
|
|
691
|
+
var allLinks = [];
|
|
687
692
|
var maxLink = 1;
|
|
688
693
|
for (var ri = 0; ri < indices.length; ri += 1) {
|
|
689
694
|
for (var ci = 0; ci < indices.length; ci += 1) {
|
|
@@ -691,53 +696,242 @@ export const CHARTS_INIT_SCRIPT = `
|
|
|
691
696
|
var src = indices[ri];
|
|
692
697
|
var tgt = indices[ci];
|
|
693
698
|
var v = (matrix[src] && matrix[src][tgt]) || 0;
|
|
694
|
-
if (v
|
|
699
|
+
if (v > 0) {
|
|
695
700
|
if (v > maxLink) maxLink = v;
|
|
696
|
-
|
|
697
|
-
source: aliases[src],
|
|
698
|
-
target: aliases[tgt],
|
|
699
|
-
value: v,
|
|
700
|
-
lineStyle: { width: Math.max(1, Math.min(8, v / maxLink * 6)), curveness: 0.2 }
|
|
701
|
-
});
|
|
701
|
+
allLinks.push({ source: aliases[src], target: aliases[tgt], value: v });
|
|
702
702
|
}
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
705
|
+
|
|
706
|
+
// 상위 응답 인사이트 생성
|
|
707
|
+
var insightEl = document.getElementById("chart-network-insight");
|
|
708
|
+
if (insightEl) {
|
|
709
|
+
var sortedLinks = allLinks.slice().sort(function (a, b) { return b.value - a.value; });
|
|
710
|
+
function esc(s) { return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); }
|
|
711
|
+
var insightItems = sortedLinks.slice(0, 3).map(function (l) {
|
|
712
|
+
return '<span class="network-insight-item"><strong>' + esc(l.source) + '</strong> <span class="network-arrow">→</span> <strong>' + esc(l.target) + '</strong> <span class="network-count">' + esc(l.value) + '회</span></span>';
|
|
713
|
+
});
|
|
714
|
+
if (insightItems.length > 0) {
|
|
715
|
+
insightEl.innerHTML = '<span class="network-insight-label">💡 상위 응답 흐름</span>' + insightItems.join('<span class="network-insight-sep">·</span>') + '<span class="network-insight-note"> (전체 기준)</span>';
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 기본 임계값: 데이터 기반 자동 설정 (최대값의 2%, 최소 2)
|
|
720
|
+
var defaultThreshold = Math.max(2, Math.min(10, Math.floor(maxLink * 0.02)));
|
|
721
|
+
var currentThreshold = defaultThreshold;
|
|
722
|
+
var networkChart = null;
|
|
723
|
+
|
|
724
|
+
function buildNetworkChart(threshold) {
|
|
725
|
+
// 임계값으로 필터링
|
|
726
|
+
var links = [];
|
|
727
|
+
for (var i = 0; i < allLinks.length; i += 1) {
|
|
728
|
+
if (allLinks[i].value >= threshold) links.push(allLinks[i]);
|
|
729
|
+
}
|
|
730
|
+
// 표시할 상위 N개만 (최대 60개 — 가독성 보장)
|
|
731
|
+
if (links.length > 60) {
|
|
732
|
+
links.sort(function (a, b) { return b.value - a.value; });
|
|
733
|
+
links = links.slice(0, 60);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
var emptyEl = document.getElementById("chart-network-empty");
|
|
737
|
+
if (emptyEl) emptyEl.hidden = links.length > 0 || allLinks.length === 0;
|
|
738
|
+
|
|
739
|
+
var isMobile = ng.w < 380;
|
|
740
|
+
var isSmall = ng.w < 600;
|
|
741
|
+
|
|
742
|
+
// 노드: 응답 총량 기반 크기, 원형 배치
|
|
743
|
+
var nodes = indices.map(function (idx) {
|
|
744
|
+
var totalReplies = replyCounts[idx] || 0;
|
|
745
|
+
// 면적 인식 고려 sqrt 스케일링, 최소 22px ~ 최대 68px
|
|
746
|
+
var size = Math.max(22, Math.min(68, Math.sqrt(totalReplies + 1) * 3.2));
|
|
747
|
+
return {
|
|
748
|
+
name: aliases[idx],
|
|
749
|
+
value: totalReplies,
|
|
750
|
+
symbolSize: size,
|
|
751
|
+
category: 0,
|
|
752
|
+
label: { show: true }
|
|
753
|
+
};
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
if (links.length === 0 && allLinks.length > 0) {
|
|
757
|
+
// 엣지 없으면 노드만 표시 (빈 원형 다이어그램)
|
|
758
|
+
links = [];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (nodes.length === 0) return;
|
|
762
|
+
|
|
763
|
+
// 엣지 스타일: 비례 두께 + 방향 화살표
|
|
764
|
+
var styledLinks = links.map(function (l) {
|
|
765
|
+
return {
|
|
766
|
+
source: l.source,
|
|
767
|
+
target: l.target,
|
|
768
|
+
value: l.value,
|
|
769
|
+
lineStyle: {
|
|
770
|
+
width: Math.max(1, Math.min(10, (l.value / maxLink) * 8)),
|
|
771
|
+
curveness: 0.3,
|
|
772
|
+
opacity: Math.max(0.2, Math.min(0.8, (l.value / maxLink)))
|
|
713
773
|
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
// 기존 차트 인스턴스 있으면 setOption으로 업데이트, 없으면 init
|
|
778
|
+
if (networkChart) {
|
|
779
|
+
networkChart.setOption(Object.assign(baseOpt(), {
|
|
780
|
+
animation: true,
|
|
781
|
+
animationDuration: 600,
|
|
782
|
+
animationEasing: "cubicOut",
|
|
783
|
+
tooltip: {
|
|
784
|
+
trigger: "item",
|
|
785
|
+
formatter: function (p) {
|
|
786
|
+
if (p.dataType === "edge") {
|
|
787
|
+
var arrowColor = dark ? "#5ee8ff" : "#0f6b5c";
|
|
788
|
+
return '<div style="font-weight:600;margin-bottom:4px">' + p.data.source + ' <span style="color:' + arrowColor + '">→</span> ' + p.data.target + '</div><div>응답: <strong>' + p.data.value + '회</strong></div>';
|
|
789
|
+
}
|
|
790
|
+
var idx = aliases.indexOf(p.data.name);
|
|
791
|
+
if (idx < 0) idx = 0;
|
|
792
|
+
return '<div style="font-weight:600;margin-bottom:4px">' + p.data.name + '</div><div>메시지: <strong>' + (msgCounts[idx] || 0) + '건</strong></div><div>응답 총량: <strong>' + (replyCounts[idx] || 0) + '회</strong></div>';
|
|
793
|
+
}
|
|
730
794
|
},
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
795
|
+
series: [{
|
|
796
|
+
type: "graph",
|
|
797
|
+
layout: "circular",
|
|
798
|
+
circular: { rotateLabel: false },
|
|
799
|
+
data: nodes,
|
|
800
|
+
links: styledLinks,
|
|
801
|
+
roam: isMobile ? false : "scale",
|
|
802
|
+
draggable: false,
|
|
803
|
+
edgeSymbol: ["none", "arrow"],
|
|
804
|
+
edgeSymbolSize: [0, isMobile ? 6 : 9],
|
|
805
|
+
symbol: "circle",
|
|
806
|
+
itemStyle: {
|
|
807
|
+
borderColor: dark ? "#1c2128" : "#fff",
|
|
808
|
+
borderWidth: 2,
|
|
809
|
+
color: dark ? "#5ee8ff" : "#0f6b5c"
|
|
810
|
+
},
|
|
811
|
+
label: {
|
|
812
|
+
show: true,
|
|
813
|
+
position: "right",
|
|
814
|
+
color: text,
|
|
815
|
+
fontSize: isMobile ? 9 : (isSmall ? 10 : 11),
|
|
816
|
+
fontWeight: 600,
|
|
817
|
+
distance: 8,
|
|
818
|
+
formatter: function (p) {
|
|
819
|
+
var name = p.name;
|
|
820
|
+
if (isMobile && name.length > 5) return name.slice(0, 4) + "..";
|
|
821
|
+
if (isSmall && name.length > 7) return name.slice(0, 6) + "..";
|
|
822
|
+
if (name.length > 10) return name.slice(0, 8) + "..";
|
|
823
|
+
return name;
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
labelLayout: { hideOverlap: true },
|
|
827
|
+
lineStyle: {
|
|
828
|
+
color: dark ? "rgba(94,232,255,0.5)" : "rgba(15,107,92,0.45)",
|
|
829
|
+
curveness: 0.3,
|
|
830
|
+
opacity: 0.55
|
|
831
|
+
},
|
|
832
|
+
emphasis: {
|
|
833
|
+
focus: "adjacency",
|
|
834
|
+
scale: 1.4,
|
|
835
|
+
label: { fontSize: isMobile ? 11 : 13, fontWeight: 700 },
|
|
836
|
+
itemStyle: { shadowBlur: 18, shadowColor: dark ? "rgba(94,232,255,0.45)" : "rgba(15,107,92,0.3)" },
|
|
837
|
+
lineStyle: { width: 5, opacity: 1 }
|
|
838
|
+
},
|
|
839
|
+
blur: {
|
|
840
|
+
itemStyle: { opacity: 0.12 },
|
|
841
|
+
lineStyle: { opacity: 0.04 },
|
|
842
|
+
label: { opacity: 0.15 }
|
|
843
|
+
}
|
|
844
|
+
}]
|
|
845
|
+
}), { notMerge: false });
|
|
846
|
+
} else {
|
|
847
|
+
networkChart = init("chart-network", Object.assign(baseOpt(), {
|
|
848
|
+
animation: true,
|
|
849
|
+
animationDuration: 600,
|
|
850
|
+
animationEasing: "cubicOut",
|
|
851
|
+
tooltip: {
|
|
852
|
+
trigger: "item",
|
|
853
|
+
formatter: function (p) {
|
|
854
|
+
if (p.dataType === "edge") {
|
|
855
|
+
var arrowColor2 = dark ? "#5ee8ff" : "#0f6b5c";
|
|
856
|
+
return '<div style="font-weight:600;margin-bottom:4px">' + p.data.source + ' <span style="color:' + arrowColor2 + '">→</span> ' + p.data.target + '</div><div>응답: <strong>' + p.data.value + '회</strong></div>';
|
|
857
|
+
}
|
|
858
|
+
var idx = aliases.indexOf(p.data.name);
|
|
859
|
+
if (idx < 0) idx = 0;
|
|
860
|
+
return '<div style="font-weight:600;margin-bottom:4px">' + p.data.name + '</div><div>메시지: <strong>' + (msgCounts[idx] || 0) + '건</strong></div><div>응답 총량: <strong>' + (replyCounts[idx] || 0) + '회</strong></div>';
|
|
861
|
+
}
|
|
734
862
|
},
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
863
|
+
series: [{
|
|
864
|
+
type: "graph",
|
|
865
|
+
layout: "circular",
|
|
866
|
+
circular: { rotateLabel: false },
|
|
867
|
+
data: nodes,
|
|
868
|
+
links: styledLinks,
|
|
869
|
+
roam: isMobile ? false : "scale",
|
|
870
|
+
draggable: false,
|
|
871
|
+
edgeSymbol: ["none", "arrow"],
|
|
872
|
+
edgeSymbolSize: [0, isMobile ? 6 : 9],
|
|
873
|
+
symbol: "circle",
|
|
874
|
+
itemStyle: {
|
|
875
|
+
borderColor: dark ? "#1c2128" : "#fff",
|
|
876
|
+
borderWidth: 2,
|
|
877
|
+
color: dark ? "#5ee8ff" : "#0f6b5c"
|
|
878
|
+
},
|
|
879
|
+
label: {
|
|
880
|
+
show: true,
|
|
881
|
+
position: "right",
|
|
882
|
+
color: text,
|
|
883
|
+
fontSize: isMobile ? 9 : (isSmall ? 10 : 11),
|
|
884
|
+
fontWeight: 600,
|
|
885
|
+
distance: 8,
|
|
886
|
+
formatter: function (p) {
|
|
887
|
+
var name = p.name;
|
|
888
|
+
if (isMobile && name.length > 5) return name.slice(0, 4) + "..";
|
|
889
|
+
if (isSmall && name.length > 7) return name.slice(0, 6) + "..";
|
|
890
|
+
if (name.length > 10) return name.slice(0, 8) + "..";
|
|
891
|
+
return name;
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
labelLayout: { hideOverlap: true },
|
|
895
|
+
lineStyle: {
|
|
896
|
+
color: dark ? "rgba(94,232,255,0.5)" : "rgba(15,107,92,0.45)",
|
|
897
|
+
curveness: 0.3,
|
|
898
|
+
opacity: 0.55
|
|
899
|
+
},
|
|
900
|
+
emphasis: {
|
|
901
|
+
focus: "adjacency",
|
|
902
|
+
scale: 1.4,
|
|
903
|
+
label: { fontSize: isMobile ? 11 : 13, fontWeight: 700 },
|
|
904
|
+
itemStyle: { shadowBlur: 18, shadowColor: dark ? "rgba(94,232,255,0.45)" : "rgba(15,107,92,0.3)" },
|
|
905
|
+
lineStyle: { width: 5, opacity: 1 }
|
|
906
|
+
},
|
|
907
|
+
blur: {
|
|
908
|
+
itemStyle: { opacity: 0.12 },
|
|
909
|
+
lineStyle: { opacity: 0.04 },
|
|
910
|
+
label: { opacity: 0.15 }
|
|
911
|
+
}
|
|
912
|
+
}]
|
|
913
|
+
}));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// 초기 렌더링
|
|
918
|
+
buildNetworkChart(currentThreshold);
|
|
919
|
+
|
|
920
|
+
// 임계값 슬라이더 바인딩
|
|
921
|
+
var thresholdSlider = document.getElementById("network-threshold");
|
|
922
|
+
var thresholdVal = document.getElementById("network-threshold-val");
|
|
923
|
+
if (thresholdSlider && thresholdVal) {
|
|
924
|
+
thresholdSlider.value = String(defaultThreshold);
|
|
925
|
+
thresholdVal.textContent = String(defaultThreshold);
|
|
926
|
+
thresholdSlider.addEventListener("input", function () {
|
|
927
|
+
var val = parseInt(this.value, 10);
|
|
928
|
+
thresholdVal.textContent = String(val);
|
|
929
|
+
});
|
|
930
|
+
thresholdSlider.addEventListener("change", function () {
|
|
931
|
+
var val = parseInt(this.value, 10);
|
|
932
|
+
currentThreshold = val;
|
|
933
|
+
buildNetworkChart(val);
|
|
934
|
+
});
|
|
741
935
|
}
|
|
742
936
|
}
|
|
743
937
|
|