VioletWizard commited on
Commit
3d4a8d5
·
unverified ·
1 Parent(s): d7bac00

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +70 -11
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 build_wordcloud_html(frequencies, max_words=60):
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
- span_html.append(
184
- f"<span style='display:inline-block; margin:8px 10px; font-size:{font_size}px; "
185
- f"font-weight:700; color:{color}; line-height:1.2;'>{html.escape(word)}</span>"
186
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  return (
189
- "<div style='padding:14px; min-height:280px; border:1px solid #e9e9e9; border-radius:10px; "
190
- "background:#ffffff; display:flex; flex-wrap:wrap; align-items:center; justify-content:center;'>"
191
- + "".join(span_html) +
192
- "</div>"
 
 
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(build_wordcloud_html(frequencies), unsafe_allow_html=True)
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: