Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,6 +2,7 @@ import streamlit as st
|
|
| 2 |
import pandas as pd
|
| 3 |
import io
|
| 4 |
import html
|
|
|
|
| 5 |
from collections import Counter
|
| 6 |
from datetime import datetime, timedelta, timezone
|
| 7 |
import logging
|
|
@@ -163,15 +164,37 @@ def build_word_frequencies(text_series):
|
|
| 163 |
tokens.append(cleaned)
|
| 164 |
return Counter(tokens)
|
| 165 |
|
| 166 |
-
def
|
| 167 |
if not frequencies:
|
| 168 |
return ""
|
| 169 |
|
| 170 |
sorted_words = sorted(frequencies.items(), key=lambda item: (-item[1], item[0]))[:max_words]
|
| 171 |
max_count = sorted_words[0][1]
|
| 172 |
min_count = sorted_words[-1][1]
|
| 173 |
-
span_html = []
|
| 174 |
palette = ["#50327A", "#2A9D8F", "#3A86AF", "#88C057", "#D4C93D"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
for index, (word, count) in enumerate(sorted_words):
|
| 177 |
if max_count == min_count:
|
|
@@ -179,17 +202,53 @@ def build_wordcloud_html(frequencies, max_words=60):
|
|
| 179 |
else:
|
| 180 |
ratio = (count - min_count) / (max_count - min_count)
|
| 181 |
font_size = int(18 + ratio * 30)
|
|
|
|
| 182 |
color = palette[index % len(palette)]
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
return (
|
| 189 |
-
"<div style='padding:
|
| 190 |
-
"
|
| 191 |
-
|
| 192 |
-
"</
|
|
|
|
|
|
|
| 193 |
)
|
| 194 |
|
| 195 |
def render_admin_approval_panel():
|
|
@@ -740,7 +799,7 @@ def live_chat_board_core():
|
|
| 740 |
st.caption("누적 토의/토론 워드클라우드")
|
| 741 |
frequencies = build_word_frequencies(stats_opinion_df["content"])
|
| 742 |
if frequencies:
|
| 743 |
-
st.markdown(
|
| 744 |
top_words = ", ".join([f"{word}({count})" for word, count in frequencies.most_common(8)])
|
| 745 |
st.caption(f"상위 키워드: {top_words}")
|
| 746 |
else:
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
import io
|
| 4 |
import html
|
| 5 |
+
import math
|
| 6 |
from collections import Counter
|
| 7 |
from datetime import datetime, timedelta, timezone
|
| 8 |
import logging
|
|
|
|
| 164 |
tokens.append(cleaned)
|
| 165 |
return Counter(tokens)
|
| 166 |
|
| 167 |
+
def build_circular_wordcloud_html(frequencies, max_words=75, width=760, height=520):
|
| 168 |
if not frequencies:
|
| 169 |
return ""
|
| 170 |
|
| 171 |
sorted_words = sorted(frequencies.items(), key=lambda item: (-item[1], item[0]))[:max_words]
|
| 172 |
max_count = sorted_words[0][1]
|
| 173 |
min_count = sorted_words[-1][1]
|
|
|
|
| 174 |
palette = ["#50327A", "#2A9D8F", "#3A86AF", "#88C057", "#D4C93D"]
|
| 175 |
+
cx, cy = width / 2, height / 2
|
| 176 |
+
radius = min(width, height) / 2 - 16
|
| 177 |
+
placed_rects = []
|
| 178 |
+
svg_text_nodes = []
|
| 179 |
+
|
| 180 |
+
def overlaps(rect):
|
| 181 |
+
x, y, w, h = rect
|
| 182 |
+
for ox, oy, ow, oh in placed_rects:
|
| 183 |
+
if not (x + w + 3 < ox or ox + ow + 3 < x or y + h + 3 < oy or oy + oh + 3 < y):
|
| 184 |
+
return True
|
| 185 |
+
return False
|
| 186 |
+
|
| 187 |
+
def is_inside_circle(x, y, w, h):
|
| 188 |
+
test_points = [
|
| 189 |
+
(x + 2, y + 2),
|
| 190 |
+
(x + w - 2, y + 2),
|
| 191 |
+
(x + 2, y + h - 2),
|
| 192 |
+
(x + w - 2, y + h - 2),
|
| 193 |
+
]
|
| 194 |
+
for px, py in test_points:
|
| 195 |
+
if math.hypot(px - cx, py - cy) > radius:
|
| 196 |
+
return False
|
| 197 |
+
return True
|
| 198 |
|
| 199 |
for index, (word, count) in enumerate(sorted_words):
|
| 200 |
if max_count == min_count:
|
|
|
|
| 202 |
else:
|
| 203 |
ratio = (count - min_count) / (max_count - min_count)
|
| 204 |
font_size = int(18 + ratio * 30)
|
| 205 |
+
|
| 206 |
color = palette[index % len(palette)]
|
| 207 |
+
text_width = max(26, font_size * (0.62 * len(word) + 0.45))
|
| 208 |
+
text_height = max(22, font_size * 1.05)
|
| 209 |
+
rotation = 90 if index % 11 == 0 else 0
|
| 210 |
+
if rotation == 90:
|
| 211 |
+
text_width, text_height = text_height, text_width
|
| 212 |
+
|
| 213 |
+
placed = False
|
| 214 |
+
for step in range(1, 1400):
|
| 215 |
+
angle = step * 0.33 + index * 0.21
|
| 216 |
+
spiral_radius = min(radius - 8, 2 + step * 0.36)
|
| 217 |
+
x = cx + spiral_radius * math.cos(angle) - text_width / 2
|
| 218 |
+
y = cy + spiral_radius * math.sin(angle) - text_height / 2
|
| 219 |
+
rect = (x, y, text_width, text_height)
|
| 220 |
+
if not is_inside_circle(x, y, text_width, text_height):
|
| 221 |
+
continue
|
| 222 |
+
if overlaps(rect):
|
| 223 |
+
continue
|
| 224 |
+
|
| 225 |
+
placed_rects.append(rect)
|
| 226 |
+
safe_word = html.escape(word)
|
| 227 |
+
if rotation == 90:
|
| 228 |
+
tx, ty = x + text_height * 0.8, y + text_width * 0.2
|
| 229 |
+
svg_text_nodes.append(
|
| 230 |
+
f"<text x='{tx:.1f}' y='{ty:.1f}' fill='{color}' font-size='{font_size}' "
|
| 231 |
+
f"font-weight='700' transform='rotate(90 {tx:.1f} {ty:.1f})'>{safe_word}</text>"
|
| 232 |
+
)
|
| 233 |
+
else:
|
| 234 |
+
tx, ty = x + 2, y + text_height * 0.82
|
| 235 |
+
svg_text_nodes.append(
|
| 236 |
+
f"<text x='{tx:.1f}' y='{ty:.1f}' fill='{color}' font-size='{font_size}' "
|
| 237 |
+
f"font-weight='700'>{safe_word}</text>"
|
| 238 |
+
)
|
| 239 |
+
placed = True
|
| 240 |
+
break
|
| 241 |
+
|
| 242 |
+
if not placed:
|
| 243 |
+
continue
|
| 244 |
|
| 245 |
return (
|
| 246 |
+
"<div style='padding:12px; border:1px solid #e9e9e9; border-radius:10px; background:#ffffff;'>"
|
| 247 |
+
f"<svg viewBox='0 0 {width} {height}' style='width:100%; height:auto; display:block;' "
|
| 248 |
+
"xmlns='http://www.w3.org/2000/svg'>"
|
| 249 |
+
f"<circle cx='{cx:.1f}' cy='{cy:.1f}' r='{radius:.1f}' fill='#ffffff' />"
|
| 250 |
+
+ "".join(svg_text_nodes) +
|
| 251 |
+
"</svg></div>"
|
| 252 |
)
|
| 253 |
|
| 254 |
def render_admin_approval_panel():
|
|
|
|
| 799 |
st.caption("누적 토의/토론 워드클라우드")
|
| 800 |
frequencies = build_word_frequencies(stats_opinion_df["content"])
|
| 801 |
if frequencies:
|
| 802 |
+
st.markdown(build_circular_wordcloud_html(frequencies), unsafe_allow_html=True)
|
| 803 |
top_words = ", ".join([f"{word}({count})" for word, count in frequencies.most_common(8)])
|
| 804 |
st.caption(f"상위 키워드: {top_words}")
|
| 805 |
else:
|