Spaces:
Running
Running
elfsong commited on
Commit ·
e218ebb
1
Parent(s): a6af448
fix: restore compact lifecycle layout
Browse files- src/README.md +3 -4
- src/lifecycle_figure.py +121 -142
- src/streamlit_app.py +6 -36
- src/styles/app.css +1 -1
- src/styles/tailwind.css +0 -12
- tests/test_lifecycle_figure.py +13 -65
src/README.md
CHANGED
|
@@ -111,10 +111,9 @@ from all available paper data, and pushes to `Elfsong/hf_paper_lifecycle`.
|
|
| 111 |
This step uses local statistics and does not call Codex.
|
| 112 |
|
| 113 |
The Streamlit Lifecycle view renders snapshots with `lifecycle_figure.py`:
|
| 114 |
-
a
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
phases rank by total papers. Existing snapshots work without regeneration.
|
| 118 |
|
| 119 |
```bash
|
| 120 |
uv run python src/lifecycle_retrieve.py # latest bimonthly snapshot
|
|
|
|
| 111 |
This step uses local statistics and does not call Codex.
|
| 112 |
|
| 113 |
The Streamlit Lifecycle view renders snapshots with `lifecycle_figure.py`:
|
| 114 |
+
a compact curve with topic names labeled directly on it. The five phases show
|
| 115 |
+
up to 3, 4, 4, 3, and 2 topics respectively. Innovation ranks by recent share,
|
| 116 |
+
and other phases rank by total papers. Existing snapshots work without regeneration.
|
|
|
|
| 117 |
|
| 118 |
```bash
|
| 119 |
uv run python src/lifecycle_retrieve.py # latest bimonthly snapshot
|
src/lifecycle_figure.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
-
"""
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import matplotlib
|
|
|
|
| 4 |
import numpy as np
|
| 5 |
from matplotlib import font_manager
|
| 6 |
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
@@ -15,13 +18,6 @@ PHASES_ORDER = [
|
|
| 15 |
"Slope of Enlightenment",
|
| 16 |
"Plateau of Productivity",
|
| 17 |
]
|
| 18 |
-
_COLORS = ["#16a34a", "#dc2626", "#2563eb", "#d97706", "#6b7280"]
|
| 19 |
-
_LABELS_EN = [
|
| 20 |
-
"Innovation\nTrigger", "Peak of Inflated\nExpectations",
|
| 21 |
-
"Trough of\nDisillusionment", "Slope of\nEnlightenment",
|
| 22 |
-
"Plateau of\nProductivity",
|
| 23 |
-
]
|
| 24 |
-
_LABELS_ZH = ["技术萌芽期", "期望膨胀期", "泡沫破裂期", "稳步爬升期", "生产成熟期"]
|
| 25 |
|
| 26 |
|
| 27 |
def _font_settings(lang: bool) -> dict:
|
|
@@ -44,142 +40,125 @@ def _font_settings(lang: bool) -> dict:
|
|
| 44 |
return settings
|
| 45 |
|
| 46 |
|
| 47 |
-
def
|
| 48 |
-
"""
|
| 49 |
-
remaining = " ".join(topic.split())
|
| 50 |
-
lines = []
|
| 51 |
-
while remaining:
|
| 52 |
-
end = 1
|
| 53 |
-
while end <= len(remaining):
|
| 54 |
-
measured, _, _ = renderer.get_text_width_height_descent(
|
| 55 |
-
remaining[:end], font, ismath=False,
|
| 56 |
-
)
|
| 57 |
-
if measured > width:
|
| 58 |
-
break
|
| 59 |
-
end += 1
|
| 60 |
-
if end > len(remaining):
|
| 61 |
-
lines.append(remaining)
|
| 62 |
-
break
|
| 63 |
-
end = max(1, end - 1)
|
| 64 |
-
space = remaining.rfind(" ", 0, end + 1)
|
| 65 |
-
if space > 0:
|
| 66 |
-
end = space
|
| 67 |
-
lines.append(remaining[:end].rstrip())
|
| 68 |
-
remaining = remaining[end:].lstrip()
|
| 69 |
-
return "\n".join(lines) or " "
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def render_hype_cycle(lifecycle_data: dict, lang: bool, topics_per_phase: int = 10):
|
| 73 |
-
"""Show ranked topics on a schematic curve and in five readable columns.
|
| 74 |
-
|
| 75 |
-
Innovation ranks by current share; other phases rank by total paper count.
|
| 76 |
-
The curve's positions illustrate phase membership, not measured visibility.
|
| 77 |
-
"""
|
| 78 |
-
if topics_per_phase < 1:
|
| 79 |
-
raise ValueError("topics_per_phase must be positive")
|
| 80 |
-
|
| 81 |
-
groups = {phase: [] for phase in PHASES_ORDER}
|
| 82 |
-
for topic in lifecycle_data.values():
|
| 83 |
-
if topic["phase"] in groups:
|
| 84 |
-
groups[topic["phase"]].append(topic)
|
| 85 |
-
if not any(groups.values()):
|
| 86 |
-
return None
|
| 87 |
-
for phase, topics in groups.items():
|
| 88 |
-
metric = "current_avg" if phase == PHASES_ORDER[0] else "total_count"
|
| 89 |
-
topics.sort(key=lambda topic: (-topic[metric], topic["topic"]))
|
| 90 |
-
selected = [groups[phase][:topics_per_phase] for phase in PHASES_ORDER]
|
| 91 |
-
|
| 92 |
with matplotlib.rc_context(_font_settings(lang)):
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
ax.annotate(
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
)
|
| 141 |
-
ax.text(index + .5, -.095, f"0{index + 1}", color=color,
|
| 142 |
-
ha="center", va="center", fontsize=11, fontweight="bold")
|
| 143 |
-
|
| 144 |
-
ax.set(xlim=(0, 5), ylim=(-.16, 1.25), xticks=[], yticks=[])
|
| 145 |
-
ax.set_ylabel("关注度" if lang else "Visibility", fontsize=10, color="#475569")
|
| 146 |
-
for spine in ax.spines.values():
|
| 147 |
-
spine.set_visible(False)
|
| 148 |
-
ax.annotate("成熟度" if lang else "Maturity", xy=(5, -.035), xytext=(4.4, -.035),
|
| 149 |
-
fontsize=9, color="#64748b", va="center",
|
| 150 |
-
arrowprops={"arrowstyle": "->", "color": "#64748b", "lw": 1})
|
| 151 |
-
|
| 152 |
-
labels = _LABELS_ZH if lang else _LABELS_EN
|
| 153 |
-
for index, (phase, topics, color) in enumerate(zip(PHASES_ORDER, selected, _COLORS)):
|
| 154 |
-
panel = fig.add_axes([
|
| 155 |
-
left + index * column_width + .005, .3 / height,
|
| 156 |
-
column_width - .01, panel_height / height,
|
| 157 |
-
])
|
| 158 |
-
panel.set(xlim=(0, 1), ylim=(panel_height, 0))
|
| 159 |
-
panel.axis("off")
|
| 160 |
-
panel.plot([0, 1], [.03, .03], color=color, lw=2)
|
| 161 |
-
panel.text(.025, .13, f"0{index + 1} {labels[index]}", fontsize=10,
|
| 162 |
-
color=color, fontweight="bold", va="top", linespacing=1.3)
|
| 163 |
-
count = (
|
| 164 |
-
f"显示 {len(topics)} / {len(groups[phase])} 个主题" if lang else
|
| 165 |
-
f"Showing {len(topics)} / {len(groups[phase])} topics"
|
| 166 |
-
)
|
| 167 |
-
panel.text(.025, .64, count, fontsize=8, color="#64748b", va="top")
|
| 168 |
-
panel.text(.97, .83, "论文" if lang else "Papers", fontsize=7.5,
|
| 169 |
-
color="#64748b", ha="right", va="top")
|
| 170 |
-
y = 1.04
|
| 171 |
-
if not topics:
|
| 172 |
-
panel.text(.025, y, "该阶段暂无主题" if lang else "No topics in this phase",
|
| 173 |
-
fontsize=9, color="#94a3b8", va="top")
|
| 174 |
-
for rank, (topic, label, row_height) in enumerate(
|
| 175 |
-
zip(topics, wrapped[index], row_heights[index]), start=1,
|
| 176 |
-
):
|
| 177 |
-
panel.text(.025, y, f"{rank:02}", fontsize=8.5, color=color,
|
| 178 |
-
fontweight="bold", va="top")
|
| 179 |
-
panel.text(.14, y, label, fontproperties=font, color="#1f2937",
|
| 180 |
-
va="top", linespacing=1.3, gid="topic-label")
|
| 181 |
-
panel.text(.97, y, f"{topic['total_count']:,}", fontsize=8,
|
| 182 |
-
color="#64748b", ha="right", va="top")
|
| 183 |
-
y += row_height
|
| 184 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
return fig
|
|
|
|
| 1 |
+
"""Compact lifecycle curve, independent of the Streamlit app."""
|
| 2 |
+
|
| 3 |
+
from collections import defaultdict
|
| 4 |
|
| 5 |
import matplotlib
|
| 6 |
+
import matplotlib.patheffects as pe
|
| 7 |
import numpy as np
|
| 8 |
from matplotlib import font_manager
|
| 9 |
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
|
|
| 18 |
"Slope of Enlightenment",
|
| 19 |
"Plateau of Productivity",
|
| 20 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
def _font_settings(lang: bool) -> dict:
|
|
|
|
| 40 |
return settings
|
| 41 |
|
| 42 |
|
| 43 |
+
def render_hype_cycle(lifecycle_data: dict, lang: bool):
|
| 44 |
+
"""Render the original hype cycle with up to 16 directly labeled topics."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
with matplotlib.rc_context(_font_settings(lang)):
|
| 46 |
+
phase_groups: dict[str, list] = defaultdict(list)
|
| 47 |
+
for lc in lifecycle_data.values():
|
| 48 |
+
phase_groups[lc["phase"]].append(lc)
|
| 49 |
+
|
| 50 |
+
sort_keys = {
|
| 51 |
+
"Innovation Trigger": lambda x: -x["current_avg"],
|
| 52 |
+
"Peak of Inflated Expectations": lambda x: -x["total_count"],
|
| 53 |
+
"Trough of Disillusionment": lambda x: -x["total_count"],
|
| 54 |
+
"Slope of Enlightenment": lambda x: -x["total_count"],
|
| 55 |
+
"Plateau of Productivity": lambda x: -x["total_count"],
|
| 56 |
+
}
|
| 57 |
+
for phase, key_fn in sort_keys.items():
|
| 58 |
+
if phase in phase_groups:
|
| 59 |
+
phase_groups[phase].sort(key=key_fn)
|
| 60 |
+
|
| 61 |
+
max_per_phase = {
|
| 62 |
+
"Innovation Trigger": 3,
|
| 63 |
+
"Peak of Inflated Expectations": 4,
|
| 64 |
+
"Trough of Disillusionment": 4,
|
| 65 |
+
"Slope of Enlightenment": 3,
|
| 66 |
+
"Plateau of Productivity": 2,
|
| 67 |
+
}
|
| 68 |
+
selected = []
|
| 69 |
+
for phase in PHASES_ORDER:
|
| 70 |
+
selected.extend(phase_groups.get(phase, [])[:max_per_phase[phase]])
|
| 71 |
+
|
| 72 |
+
if not selected:
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
# Hype cycle curve (cubic spline through control points)
|
| 76 |
+
ctrl_x = np.array([0, 0.5, 1, 1.5, 2, 2.3, 2.8, 3.2, 3.8, 4.5, 5.5, 6.5, 7.5, 8.5, 10])
|
| 77 |
+
ctrl_y = np.array([.02, .08, .22, .58, .98, .78, .38, .16, .10, .15, .26, .36, .42, .45, .47])
|
| 78 |
+
cs = CubicSpline(ctrl_x, ctrl_y)
|
| 79 |
+
curve_x = np.linspace(0, 10, 500)
|
| 80 |
+
curve_y = cs(curve_x)
|
| 81 |
+
|
| 82 |
+
phase_ranges = {
|
| 83 |
+
"Innovation Trigger": (0.3, 1.3),
|
| 84 |
+
"Peak of Inflated Expectations": (1.4, 2.6),
|
| 85 |
+
"Trough of Disillusionment": (2.8, 4.3),
|
| 86 |
+
"Slope of Enlightenment": (4.8, 7.2),
|
| 87 |
+
"Plateau of Productivity": (7.5, 9.5),
|
| 88 |
+
}
|
| 89 |
+
phase_colors = {
|
| 90 |
+
"Innovation Trigger": "#16a34a",
|
| 91 |
+
"Peak of Inflated Expectations": "#dc2626",
|
| 92 |
+
"Trough of Disillusionment": "#2563eb",
|
| 93 |
+
"Slope of Enlightenment": "#d97706",
|
| 94 |
+
"Plateau of Productivity": "#6b7280",
|
| 95 |
+
}
|
| 96 |
+
phase_labels = {
|
| 97 |
+
"Innovation Trigger": "技术\n萌芽期" if lang else "Innovation\nTrigger",
|
| 98 |
+
"Peak of Inflated Expectations": "期望\n膨胀期" if lang else "Peak of Inflated\nExpectations",
|
| 99 |
+
"Trough of Disillusionment": "泡沫\n破裂期" if lang else "Trough of\nDisillusionment",
|
| 100 |
+
"Slope of Enlightenment": "稳步\n爬升期" if lang else "Slope of\nEnlightenment",
|
| 101 |
+
"Plateau of Productivity": "生产\n成熟期" if lang else "Plateau of\nProductivity",
|
| 102 |
+
}
|
| 103 |
+
offset_patterns = {
|
| 104 |
+
"Innovation Trigger": [(0.10, "bottom"), (-0.08, "top"), (0.15, "bottom")],
|
| 105 |
+
"Peak of Inflated Expectations": [(0.16, "bottom"), (-0.13, "top"), (0.10, "bottom"), (-0.09, "top")],
|
| 106 |
+
"Trough of Disillusionment": [(0.10, "bottom"), (-0.07, "top"), (0.14, "bottom"), (-0.10, "top")],
|
| 107 |
+
"Slope of Enlightenment": [(0.10, "bottom"), (-0.08, "top"), (0.14, "bottom")],
|
| 108 |
+
"Plateau of Productivity": [(0.10, "bottom"), (-0.08, "top")],
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
fig = Figure(figsize=(10, 5))
|
| 112 |
+
FigureCanvasAgg(fig)
|
| 113 |
+
ax = fig.subplots()
|
| 114 |
+
ax.plot(curve_x, curve_y, color="#d1d5db", linewidth=3.5, zorder=1, solid_capstyle="round")
|
| 115 |
+
ax.fill_between(curve_x, 0, curve_y, alpha=0.03, color="#9ca3af")
|
| 116 |
+
|
| 117 |
+
for bx in [1.35, 2.7, 4.55, 7.35]:
|
| 118 |
+
ax.axvline(bx, color="#e5e7eb", linewidth=0.6, linestyle="--", zorder=0)
|
| 119 |
+
|
| 120 |
+
for phase in PHASES_ORDER:
|
| 121 |
+
x_lo, x_hi = phase_ranges[phase]
|
| 122 |
+
pts = [lc for lc in selected if lc["phase"] == phase]
|
| 123 |
+
if not pts:
|
| 124 |
+
continue
|
| 125 |
+
x_positions = np.linspace(x_lo, x_hi, len(pts) + 2)[1:-1]
|
| 126 |
+
color = phase_colors[phase]
|
| 127 |
+
offsets = offset_patterns[phase]
|
| 128 |
+
|
| 129 |
+
for i, lc in enumerate(pts):
|
| 130 |
+
xp = x_positions[i]
|
| 131 |
+
yp = float(cs(xp))
|
| 132 |
+
dot_size = max(4, min(10, lc["total_count"] / 30))
|
| 133 |
+
ax.plot(xp, yp, "o", color=color, markersize=dot_size, zorder=3,
|
| 134 |
+
markeredgecolor="white", markeredgewidth=0.6)
|
| 135 |
+
offset_y, va = offsets[i % len(offsets)]
|
| 136 |
ax.annotate(
|
| 137 |
+
lc["topic"], xy=(xp, yp), xytext=(xp, yp + offset_y),
|
| 138 |
+
fontsize=7, color=color, fontweight="bold", ha="center", va=va,
|
| 139 |
+
arrowprops=dict(arrowstyle="-", color=color, alpha=0.3, lw=0.5),
|
| 140 |
+
path_effects=[pe.withStroke(linewidth=2.5, foreground="white")],
|
| 141 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
+
for phase in PHASES_ORDER:
|
| 144 |
+
x_lo, x_hi = phase_ranges[phase]
|
| 145 |
+
ax.text((x_lo + x_hi) / 2, -0.10, phase_labels[phase],
|
| 146 |
+
fontsize=7, ha="center", va="top",
|
| 147 |
+
color=phase_colors[phase], fontweight="bold", style="italic")
|
| 148 |
+
|
| 149 |
+
ax.set_xlim(-0.3, 10.3)
|
| 150 |
+
ax.set_ylim(-0.22, 1.20)
|
| 151 |
+
ax.set_ylabel("关注度" if lang else "Visibility", fontsize=10)
|
| 152 |
+
ax.annotate(
|
| 153 |
+
"成熟度" if lang else "Maturity",
|
| 154 |
+
xy=(10.2, -0.03), xytext=(8.5, -0.03), fontsize=9,
|
| 155 |
+
arrowprops=dict(arrowstyle="->", color="#6b7280", lw=1.2),
|
| 156 |
+
color="#6b7280", va="center",
|
| 157 |
+
)
|
| 158 |
+
ax.spines["top"].set_visible(False)
|
| 159 |
+
ax.spines["right"].set_visible(False)
|
| 160 |
+
ax.spines["bottom"].set_visible(False)
|
| 161 |
+
ax.set_xticks([])
|
| 162 |
+
ax.set_yticks([])
|
| 163 |
+
fig.tight_layout()
|
| 164 |
return fig
|
src/streamlit_app.py
CHANGED
|
@@ -3023,11 +3023,6 @@ elif active_tab == "Lifecycle":
|
|
| 3023 |
label_visibility="collapsed", key="lifecycle_select",
|
| 3024 |
)
|
| 3025 |
|
| 3026 |
-
_render_view_heading(
|
| 3027 |
-
"主题生命周期" if lang else "Topic lifecycle",
|
| 3028 |
-
"观察研究主题从萌芽到成熟的阶段变化。" if lang else
|
| 3029 |
-
"Explore how research topics move from early ideas to established practice.",
|
| 3030 |
-
)
|
| 3031 |
_lc_cache_key = f"lifecycle_{selected_snapshot}"
|
| 3032 |
lc_raw = None
|
| 3033 |
if _lc_cache_key in st.session_state:
|
|
@@ -3043,10 +3038,7 @@ elif active_tab == "Lifecycle":
|
|
| 3043 |
lc_data = lc_raw["lifecycle_data_zh"] if lang else lc_raw["lifecycle_data"]
|
| 3044 |
sorted_months = lc_raw["sorted_months"]
|
| 3045 |
|
| 3046 |
-
|
| 3047 |
-
summary_columns[0].metric("论文" if lang else "Papers", f"{lc_raw['n_papers']:,}")
|
| 3048 |
-
summary_columns[1].metric("主题" if lang else "Topics", len(lc_data))
|
| 3049 |
-
summary_columns[2].metric("覆盖月数" if lang else "Months of research", lc_raw["n_months"])
|
| 3050 |
if sorted_months:
|
| 3051 |
st.caption(
|
| 3052 |
f"{lc_raw['n_months']} months ({sorted_months[0]} → {sorted_months[-1]})"
|
|
@@ -3055,33 +3047,11 @@ elif active_tab == "Lifecycle":
|
|
| 3055 |
if not lc_data:
|
| 3056 |
st.warning("Not enough data for lifecycle analysis.")
|
| 3057 |
else:
|
| 3058 |
-
|
| 3059 |
-
|
| 3060 |
-
|
| 3061 |
-
|
| 3062 |
-
|
| 3063 |
-
options=[5, 10, 15, 20], value=10,
|
| 3064 |
-
key="lifecycle_topics_per_phase",
|
| 3065 |
-
)
|
| 3066 |
-
with explanation:
|
| 3067 |
-
st.caption(
|
| 3068 |
-
"萌芽期按近期占比排序,其余阶段按累计论文数排序;数量不足时显示全部。"
|
| 3069 |
-
if lang else
|
| 3070 |
-
"Innovation ranks by recent share; other phases by total papers. "
|
| 3071 |
-
"Phases with fewer topics show all available."
|
| 3072 |
-
)
|
| 3073 |
-
fig = _render_hype_cycle(lc_data, lang, topics_per_phase=topics_per_phase)
|
| 3074 |
-
if fig:
|
| 3075 |
-
import matplotlib.pyplot as plt
|
| 3076 |
-
st.pyplot(fig, use_container_width=True)
|
| 3077 |
-
plt.close(fig)
|
| 3078 |
-
st.caption(
|
| 3079 |
-
"曲线为阶段示意,点位不代表实际关注度或成熟度。"
|
| 3080 |
-
"编号对应下方主题,点大小表示累计论文数。"
|
| 3081 |
-
if lang else
|
| 3082 |
-
"The curve illustrates lifecycle stages, not measured visibility or maturity. "
|
| 3083 |
-
"Numbers match the topic lists; dot size reflects total papers."
|
| 3084 |
-
)
|
| 3085 |
|
| 3086 |
# --- Topic selector & time-series chart ---
|
| 3087 |
tbm = lc_raw.get("topics_by_month_zh" if lang else "topics_by_month") or {}
|
|
|
|
| 3023 |
label_visibility="collapsed", key="lifecycle_select",
|
| 3024 |
)
|
| 3025 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3026 |
_lc_cache_key = f"lifecycle_{selected_snapshot}"
|
| 3027 |
lc_raw = None
|
| 3028 |
if _lc_cache_key in st.session_state:
|
|
|
|
| 3038 |
lc_data = lc_raw["lifecycle_data_zh"] if lang else lc_raw["lifecycle_data"]
|
| 3039 |
sorted_months = lc_raw["sorted_months"]
|
| 3040 |
|
| 3041 |
+
st.metric("Papers", f"{lc_raw['n_papers']:,}")
|
|
|
|
|
|
|
|
|
|
| 3042 |
if sorted_months:
|
| 3043 |
st.caption(
|
| 3044 |
f"{lc_raw['n_months']} months ({sorted_months[0]} → {sorted_months[-1]})"
|
|
|
|
| 3047 |
if not lc_data:
|
| 3048 |
st.warning("Not enough data for lifecycle analysis.")
|
| 3049 |
else:
|
| 3050 |
+
fig = _render_hype_cycle(lc_data, lang)
|
| 3051 |
+
if fig:
|
| 3052 |
+
import matplotlib.pyplot as plt
|
| 3053 |
+
st.pyplot(fig, use_container_width=True)
|
| 3054 |
+
plt.close(fig)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3055 |
|
| 3056 |
# --- Topic selector & time-series chart ---
|
| 3057 |
tbm = lc_raw.get("topics_by_month_zh" if lang else "topics_by_month") or {}
|
src/styles/app.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
| 2 |
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight:initial;--tw-tracking:initial}}}@layer theme{:root,:host{--tw-font-sans:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;--tw-color-amber-50:oklch(98.7% .022 95.277);--tw-color-amber-100:oklch(96.2% .059 95.617);--tw-color-amber-800:oklch(47.3% .137 46.201);--tw-color-emerald-50:oklch(97.9% .021 166.113);--tw-color-emerald-700:oklch(50.8% .118 165.612);--tw-color-blue-50:oklch(97% .014 254.604);--tw-color-blue-100:oklch(93.2% .032 255.585);--tw-color-blue-200:oklch(88.2% .059 254.128);--tw-color-blue-300:oklch(80.9% .105 251.813);--tw-color-blue-500:oklch(62.3% .214 259.815);--tw-color-blue-600:oklch(54.6% .245 262.881);--tw-color-blue-700:oklch(48.8% .243 264.376);--tw-color-rose-50:oklch(96.9% .015 12.422);--tw-color-rose-700:oklch(51.4% .222 16.935);--tw-color-slate-50:oklch(98.4% .003 247.858);--tw-color-slate-100:oklch(96.8% .007 247.896);--tw-color-slate-200:oklch(92.9% .013 255.508);--tw-color-slate-400:oklch(70.4% .04 256.788);--tw-color-slate-500:oklch(55.4% .046 257.417);--tw-color-slate-600:oklch(44.6% .043 257.281);--tw-color-slate-700:oklch(37.2% .044 257.287);--tw-color-slate-800:oklch(27.9% .041 260.031);--tw-color-slate-900:oklch(20.8% .042 265.755);--tw-color-white:#fff;--tw-spacing:.25rem;--tw-text-xs:.75rem;--tw-text-xs--line-height:calc(1 / .75);--tw-text-sm:.875rem;--tw-text-sm--line-height:calc(1.25 / .875);--tw-text-2xl:1.5rem;--tw-text-2xl--line-height:calc(2 / 1.5);--tw-font-weight-normal:400;--tw-font-weight-medium:500;--tw-font-weight-semibold:600;--tw-font-weight-bold:700;--tw-tracking-tight:-.025em;--tw-radius-md:.375rem;--tw-radius-lg:.5rem;--tw-radius-xl:.75rem;--tw-radius-2xl:1rem}}@layer base,components;@layer utilities{.tw\:mt-2{margin-top:calc(var(--tw-spacing) * 2)}.tw\:mt-4{margin-top:calc(var(--tw-spacing) * 4)}.tw\:mb-3{margin-bottom:calc(var(--tw-spacing) * 3)}.tw\:mb-4{margin-bottom:calc(var(--tw-spacing) * 4)}.tw\:flex{display:flex}.tw\:inline-flex{display:inline-flex}.tw\:size-11{width:calc(var(--tw-spacing) * 11);height:calc(var(--tw-spacing) * 11)}.tw\:flex-wrap{flex-wrap:wrap}.tw\:items-center{align-items:center}.tw\:justify-center{justify-content:center}.tw\:gap-2{gap:calc(var(--tw-spacing) * 2)}.tw\:gap-3{gap:calc(var(--tw-spacing) * 3)}.tw\:rounded-xl{border-radius:var(--tw-radius-xl)}.tw\:bg-blue-50{background-color:var(--tw-color-blue-50)}.tw\:text-2xl{font-size:var(--tw-text-2xl);line-height:var(--tw-leading,var(--tw-text-2xl--line-height))}.tw\:text-sm{font-size:var(--tw-text-sm);line-height:var(--tw-leading,var(--tw-text-sm--line-height))}.tw\:text-xs{font-size:var(--tw-text-xs);line-height:var(--tw-leading,var(--tw-text-xs--line-height))}.tw\:font-bold{--tw-font-weight:var(--tw-font-weight-bold);font-weight:var(--tw-font-weight-bold)}.tw\:font-normal{--tw-font-weight:var(--tw-font-weight-normal);font-weight:var(--tw-font-weight-normal)}.tw\:font-semibold{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold)}.tw\:tracking-tight{--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight)}.tw\:text-blue-700{color:var(--tw-color-blue-700)}.tw\:text-slate-400{color:var(--tw-color-slate-400)}.tw\:text-slate-500{color:var(--tw-color-slate-500)}.tw\:text-slate-900{color:var(--tw-color-slate-900)}}[data-testid=stAppViewContainer]{background-color:var(--tw-color-slate-50);color:var(--tw-color-slate-800);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:var(--tw-font-sans)}[data-testid=stHeader],[data-testid=stToolbar]{pointer-events:none}[data-testid=stHeader]{background:0 0}[data-testid=stHeader] :is(button,a){pointer-events:auto}.stMainBlockContainer{max-width:1480px;padding:2.25rem 2.5rem 4rem}[data-testid=stMarkdownContainer] :is(h1,h2,h3,h4){--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900)}[data-testid=stCaptionContainer],[data-testid=stCaptionContainer] p{color:var(--tw-color-slate-500);line-height:1.65}.stDeployButton,[data-testid=stAppDeployButton],footer,#MainMenu,[data-testid=stSidebar],[data-testid=collapsedControl]{display:none}.st-key-app_header{border-bottom:1px solid var(--tw-color-slate-200);margin-bottom:.5rem;padding-bottom:1.4rem}.st-key-app_header [data-testid=stMarkdownContainer]{margin-bottom:0}.paper-brand{min-height:52px}.paper-brand-name{white-space:nowrap;font-size:1.2rem;line-height:1.35}.paper-brand-tagline{letter-spacing:.015em;margin-top:.125rem;font-size:.72rem}.st-key-app_header :is([data-testid=stToggle],[data-testid=stCheckbox]){white-space:nowrap}.st-key-app_header :is([data-testid=stToggle],[data-testid=stCheckbox]) p{font-size:.875rem}[data-testid=stSelectbox] [data-baseweb=select]>div,[data-testid=stMultiSelect] [data-baseweb=select]>div,[data-testid=stDateInput] [data-baseweb=input]{border-radius:var(--tw-radius-xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);min-height:44px;box-shadow:0 1px 2px #0f172a06}[data-testid=stSelectbox] [data-baseweb=select]>div:hover,[data-testid=stDateInput] [data-baseweb=input]:hover{border-color:var(--tw-color-slate-400)}[data-testid=stSelectbox] [data-baseweb=select]>div:focus-within,[data-testid=stMultiSelect] [data-baseweb=select]>div:focus-within,[data-testid=stDateInput] [data-baseweb=input]:focus-within{border-color:var(--tw-color-blue-500);box-shadow:0 0 0 3px var(--tw-color-blue-100)}[data-testid=stWidgetLabel] p{--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-600);font-size:.8rem}.view-heading{margin:.25rem 0 .6rem}[data-testid=stMarkdownContainer] .view-title{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900);margin:0;padding:0;font-size:clamp(1.5rem,2.3vw,2rem);line-height:1.25}.view-description{color:var(--tw-color-slate-500);margin:.5rem 0 0;font-size:.9rem}[data-testid=stMetric]{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);padding:1rem 1.25rem}[data-testid=stMetricLabel] p{--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-500);font-size:.78rem}[data-testid=stMetricValue]{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900);font-size:1.8rem}[data-testid=stMetricValue]>div{font-size:1.8rem}.st-key-lifecycle_chart{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);padding:1.25rem;overflow-x:auto}.st-key-lifecycle_chart [data-testid=stImage],.st-key-lifecycle_chart [data-testid=stElementContainer]:has([data-testid=stImage]){min-width:740px}.st-key-lifecycle_chart [data-testid=stImage] img{width:100%!important}[data-testid=stExpander] details{border-radius:var(--tw-radius-xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200)}[data-testid=stExpander] summary{color:var(--tw-color-slate-700);padding:.8rem 1rem}[data-testid=stPills] button{border-radius:999px;min-height:32px;padding:.3rem .8rem}[data-testid=stPills] button p{font-size:.78rem;font-weight:500}[data-testid=stPills] button[aria-pressed=true]{color:var(--tw-color-blue-700);background:var(--tw-color-blue-50);border-color:var(--tw-color-blue-300)}[data-testid=stPills] button[aria-pressed=true] p{color:var(--tw-color-blue-700)}[data-testid=stAppViewContainer] button:focus-visible,[data-testid=stAppViewContainer] a:focus-visible{outline:2px solid var(--tw-color-blue-500);outline-offset:3px}div[class*=st-key-paper_card_]{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);height:100%;transition:border-color .16s,box-shadow .16s;overflow:hidden;box-shadow:0 2px 4px #0f172a06;border:1px solid var(--tw-color-slate-200)!important;gap:.6rem!important;padding:0 0 .5rem!important}div[class*=st-key-paper_card_]:hover{box-shadow:0 8px 24px #0f172a0f;border-color:var(--tw-color-blue-200)!important}div[class*=st-key-paper_card_] [data-testid=stImage]{aspect-ratio:2;background:var(--tw-color-slate-100);overflow:hidden}div[class*=st-key-paper_card_] [data-testid=stImage] img{object-fit:cover;width:100%;height:100%}div[class*=st-key-paper_card_] [data-testid=stButton]{padding:0 1.1rem}div[class*=st-key-paper_card_] [data-testid=stButton] button{text-align:left;background:0 0;border:0;justify-content:flex-start;width:100%;min-height:4.3rem;padding:0}div[class*=st-key-paper_card_] [data-testid=stButton] button p{color:var(--tw-color-slate-900);-webkit-line-clamp:3;-webkit-box-orient:vertical;font-size:1rem;line-height:1.45;display:-webkit-box;overflow:hidden}div[class*=st-key-paper_card_] [data-testid=stButton] button:hover p{color:var(--tw-color-blue-600)}.paper-card-meta{padding:0 1.1rem .5rem}.paper-authors{color:var(--tw-color-slate-500);-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.5rem;margin-bottom:.8rem;font-size:.78rem;line-height:1.6;display:-webkit-box;overflow:hidden}.card-topics{gap:calc(var(--tw-spacing) * 1.5);flex-wrap:wrap;align-content:flex-start;min-height:3.25rem;display:flex}.topic-badge{border-radius:var(--tw-radius-md);background-color:var(--tw-color-blue-50);--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-blue-700);align-items:center;padding:.25rem .55rem;font-size:.7rem;line-height:1.35;display:inline-flex}.keyword-badge{border-radius:var(--tw-radius-md);background-color:var(--tw-color-amber-50);color:var(--tw-color-amber-800);border:1px solid var(--tw-color-amber-100);align-items:center;padding:.25rem .55rem;font-size:.7rem;line-height:1.35;display:inline-flex}.trending-panel{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);border-left:3px solid var(--tw-color-blue-500);margin-bottom:.75rem;padding:1.25rem 1.5rem}.trending-summary{color:var(--tw-color-slate-600);font-size:.9rem;line-height:1.8}.paper-links{gap:calc(var(--tw-spacing) * 2);flex-wrap:wrap;margin-bottom:1rem;display:flex}.paper-links a{border-radius:var(--tw-radius-lg);background-color:var(--tw-color-slate-50);--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-600);border:1px solid var(--tw-color-slate-200);align-items:center;padding:.45rem .75rem;font-size:.8rem;text-decoration:none;display:inline-flex}.paper-links a:hover{background-color:var(--tw-color-blue-50);color:var(--tw-color-blue-700);border-color:var(--tw-color-blue-200)}[role=dialog]{border-radius:1.25rem}.pros-box,.cons-box{border-radius:.8rem;padding:1rem}.pros-box{background-color:var(--tw-color-emerald-50)}.cons-box{background-color:var(--tw-color-rose-50)}.section-label{margin-bottom:.5rem;font-size:.72rem;font-weight:600}.pros-box .section-label{color:var(--tw-color-emerald-700)}.cons-box .section-label{color:var(--tw-color-rose-700)}.point{color:var(--tw-color-slate-700);padding:.35rem 0;font-size:.85rem;line-height:1.75}.stats-bar{gap:calc(var(--tw-spacing) * 6);border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);flex-wrap:wrap;padding:1rem 1.25rem;display:flex}.stat-item{color:var(--tw-color-slate-500);font-size:.8rem}.stat-value{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);color:var(--tw-color-slate-900);margin-right:.4rem;font-size:1.1rem}@media (max-width:960px){.st-key-app_header{padding-bottom:1rem}.st-key-app_header [data-testid=stHorizontalBlock]{flex-wrap:wrap;gap:1rem .75rem}.st-key-app_header [data-testid=stColumn]{width:auto!important;min-width:0!important}.st-key-app_header [data-testid=stColumn]:first-child{flex:100%}.st-key-app_header [data-testid=stColumn]:nth-child(2){flex:35%}.st-key-app_header [data-testid=stColumn]:nth-child(3){flex:50%}.st-key-app_header [data-testid=stColumn]:nth-child(4){flex:100%}}@media (max-width:760px){.stMainBlockContainer{padding:1.5rem 1rem 3rem}.trending-panel{padding:1rem}.st-key-lifecycle_chart{padding:.6rem}}@media (prefers-reduced-motion:reduce){div[class*=st-key-paper_card_]{transition:none}}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}
|
|
|
|
| 1 |
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
| 2 |
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-font-weight:initial;--tw-tracking:initial}}}@layer theme{:root,:host{--tw-font-sans:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;--tw-color-amber-50:oklch(98.7% .022 95.277);--tw-color-amber-100:oklch(96.2% .059 95.617);--tw-color-amber-800:oklch(47.3% .137 46.201);--tw-color-emerald-50:oklch(97.9% .021 166.113);--tw-color-emerald-700:oklch(50.8% .118 165.612);--tw-color-blue-50:oklch(97% .014 254.604);--tw-color-blue-100:oklch(93.2% .032 255.585);--tw-color-blue-200:oklch(88.2% .059 254.128);--tw-color-blue-300:oklch(80.9% .105 251.813);--tw-color-blue-500:oklch(62.3% .214 259.815);--tw-color-blue-600:oklch(54.6% .245 262.881);--tw-color-blue-700:oklch(48.8% .243 264.376);--tw-color-rose-50:oklch(96.9% .015 12.422);--tw-color-rose-700:oklch(51.4% .222 16.935);--tw-color-slate-50:oklch(98.4% .003 247.858);--tw-color-slate-100:oklch(96.8% .007 247.896);--tw-color-slate-200:oklch(92.9% .013 255.508);--tw-color-slate-400:oklch(70.4% .04 256.788);--tw-color-slate-500:oklch(55.4% .046 257.417);--tw-color-slate-600:oklch(44.6% .043 257.281);--tw-color-slate-700:oklch(37.2% .044 257.287);--tw-color-slate-800:oklch(27.9% .041 260.031);--tw-color-slate-900:oklch(20.8% .042 265.755);--tw-color-white:#fff;--tw-spacing:.25rem;--tw-text-xs:.75rem;--tw-text-xs--line-height:calc(1 / .75);--tw-text-sm:.875rem;--tw-text-sm--line-height:calc(1.25 / .875);--tw-text-2xl:1.5rem;--tw-text-2xl--line-height:calc(2 / 1.5);--tw-font-weight-normal:400;--tw-font-weight-medium:500;--tw-font-weight-semibold:600;--tw-font-weight-bold:700;--tw-tracking-tight:-.025em;--tw-radius-md:.375rem;--tw-radius-lg:.5rem;--tw-radius-xl:.75rem;--tw-radius-2xl:1rem}}@layer base,components;@layer utilities{.tw\:mt-2{margin-top:calc(var(--tw-spacing) * 2)}.tw\:mt-4{margin-top:calc(var(--tw-spacing) * 4)}.tw\:mb-3{margin-bottom:calc(var(--tw-spacing) * 3)}.tw\:mb-4{margin-bottom:calc(var(--tw-spacing) * 4)}.tw\:flex{display:flex}.tw\:inline-flex{display:inline-flex}.tw\:size-11{width:calc(var(--tw-spacing) * 11);height:calc(var(--tw-spacing) * 11)}.tw\:flex-wrap{flex-wrap:wrap}.tw\:items-center{align-items:center}.tw\:justify-center{justify-content:center}.tw\:gap-2{gap:calc(var(--tw-spacing) * 2)}.tw\:gap-3{gap:calc(var(--tw-spacing) * 3)}.tw\:rounded-xl{border-radius:var(--tw-radius-xl)}.tw\:bg-blue-50{background-color:var(--tw-color-blue-50)}.tw\:text-2xl{font-size:var(--tw-text-2xl);line-height:var(--tw-leading,var(--tw-text-2xl--line-height))}.tw\:text-sm{font-size:var(--tw-text-sm);line-height:var(--tw-leading,var(--tw-text-sm--line-height))}.tw\:text-xs{font-size:var(--tw-text-xs);line-height:var(--tw-leading,var(--tw-text-xs--line-height))}.tw\:font-bold{--tw-font-weight:var(--tw-font-weight-bold);font-weight:var(--tw-font-weight-bold)}.tw\:font-normal{--tw-font-weight:var(--tw-font-weight-normal);font-weight:var(--tw-font-weight-normal)}.tw\:font-semibold{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold)}.tw\:tracking-tight{--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight)}.tw\:text-blue-700{color:var(--tw-color-blue-700)}.tw\:text-slate-400{color:var(--tw-color-slate-400)}.tw\:text-slate-500{color:var(--tw-color-slate-500)}.tw\:text-slate-900{color:var(--tw-color-slate-900)}}[data-testid=stAppViewContainer]{background-color:var(--tw-color-slate-50);color:var(--tw-color-slate-800);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:var(--tw-font-sans)}[data-testid=stHeader],[data-testid=stToolbar]{pointer-events:none}[data-testid=stHeader]{background:0 0}[data-testid=stHeader] :is(button,a){pointer-events:auto}.stMainBlockContainer{max-width:1480px;padding:2.25rem 2.5rem 4rem}[data-testid=stMarkdownContainer] :is(h1,h2,h3,h4){--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900)}[data-testid=stCaptionContainer],[data-testid=stCaptionContainer] p{color:var(--tw-color-slate-500);line-height:1.65}.stDeployButton,[data-testid=stAppDeployButton],footer,#MainMenu,[data-testid=stSidebar],[data-testid=collapsedControl]{display:none}.st-key-app_header{border-bottom:1px solid var(--tw-color-slate-200);margin-bottom:.5rem;padding-bottom:1.4rem}.st-key-app_header [data-testid=stMarkdownContainer]{margin-bottom:0}.paper-brand{min-height:52px}.paper-brand-name{white-space:nowrap;font-size:1.2rem;line-height:1.35}.paper-brand-tagline{letter-spacing:.015em;margin-top:.125rem;font-size:.72rem}.st-key-app_header :is([data-testid=stToggle],[data-testid=stCheckbox]){white-space:nowrap}.st-key-app_header :is([data-testid=stToggle],[data-testid=stCheckbox]) p{font-size:.875rem}[data-testid=stSelectbox] [data-baseweb=select]>div,[data-testid=stMultiSelect] [data-baseweb=select]>div,[data-testid=stDateInput] [data-baseweb=input]{border-radius:var(--tw-radius-xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);min-height:44px;box-shadow:0 1px 2px #0f172a06}[data-testid=stSelectbox] [data-baseweb=select]>div:hover,[data-testid=stDateInput] [data-baseweb=input]:hover{border-color:var(--tw-color-slate-400)}[data-testid=stSelectbox] [data-baseweb=select]>div:focus-within,[data-testid=stMultiSelect] [data-baseweb=select]>div:focus-within,[data-testid=stDateInput] [data-baseweb=input]:focus-within{border-color:var(--tw-color-blue-500);box-shadow:0 0 0 3px var(--tw-color-blue-100)}[data-testid=stWidgetLabel] p{--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-600);font-size:.8rem}.view-heading{margin:.25rem 0 .6rem}[data-testid=stMarkdownContainer] .view-title{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900);margin:0;padding:0;font-size:clamp(1.5rem,2.3vw,2rem);line-height:1.25}.view-description{color:var(--tw-color-slate-500);margin:.5rem 0 0;font-size:.9rem}[data-testid=stMetric]{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);padding:1rem 1.25rem}[data-testid=stMetricLabel] p{--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-500);font-size:.78rem}[data-testid=stMetricValue]{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);--tw-tracking:var(--tw-tracking-tight);letter-spacing:var(--tw-tracking-tight);color:var(--tw-color-slate-900);font-size:1.8rem}[data-testid=stMetricValue]>div{font-size:1.8rem}[data-testid=stExpander] details{border-radius:var(--tw-radius-xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200)}[data-testid=stExpander] summary{color:var(--tw-color-slate-700);padding:.8rem 1rem}[data-testid=stPills] button{border-radius:999px;min-height:32px;padding:.3rem .8rem}[data-testid=stPills] button p{font-size:.78rem;font-weight:500}[data-testid=stPills] button[aria-pressed=true]{color:var(--tw-color-blue-700);background:var(--tw-color-blue-50);border-color:var(--tw-color-blue-300)}[data-testid=stPills] button[aria-pressed=true] p{color:var(--tw-color-blue-700)}[data-testid=stAppViewContainer] button:focus-visible,[data-testid=stAppViewContainer] a:focus-visible{outline:2px solid var(--tw-color-blue-500);outline-offset:3px}div[class*=st-key-paper_card_]{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);height:100%;transition:border-color .16s,box-shadow .16s;overflow:hidden;box-shadow:0 2px 4px #0f172a06;border:1px solid var(--tw-color-slate-200)!important;gap:.6rem!important;padding:0 0 .5rem!important}div[class*=st-key-paper_card_]:hover{box-shadow:0 8px 24px #0f172a0f;border-color:var(--tw-color-blue-200)!important}div[class*=st-key-paper_card_] [data-testid=stImage]{aspect-ratio:2;background:var(--tw-color-slate-100);overflow:hidden}div[class*=st-key-paper_card_] [data-testid=stImage] img{object-fit:cover;width:100%;height:100%}div[class*=st-key-paper_card_] [data-testid=stButton]{padding:0 1.1rem}div[class*=st-key-paper_card_] [data-testid=stButton] button{text-align:left;background:0 0;border:0;justify-content:flex-start;width:100%;min-height:4.3rem;padding:0}div[class*=st-key-paper_card_] [data-testid=stButton] button p{color:var(--tw-color-slate-900);-webkit-line-clamp:3;-webkit-box-orient:vertical;font-size:1rem;line-height:1.45;display:-webkit-box;overflow:hidden}div[class*=st-key-paper_card_] [data-testid=stButton] button:hover p{color:var(--tw-color-blue-600)}.paper-card-meta{padding:0 1.1rem .5rem}.paper-authors{color:var(--tw-color-slate-500);-webkit-line-clamp:2;-webkit-box-orient:vertical;min-height:2.5rem;margin-bottom:.8rem;font-size:.78rem;line-height:1.6;display:-webkit-box;overflow:hidden}.card-topics{gap:calc(var(--tw-spacing) * 1.5);flex-wrap:wrap;align-content:flex-start;min-height:3.25rem;display:flex}.topic-badge{border-radius:var(--tw-radius-md);background-color:var(--tw-color-blue-50);--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-blue-700);align-items:center;padding:.25rem .55rem;font-size:.7rem;line-height:1.35;display:inline-flex}.keyword-badge{border-radius:var(--tw-radius-md);background-color:var(--tw-color-amber-50);color:var(--tw-color-amber-800);border:1px solid var(--tw-color-amber-100);align-items:center;padding:.25rem .55rem;font-size:.7rem;line-height:1.35;display:inline-flex}.trending-panel{border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);border-left:3px solid var(--tw-color-blue-500);margin-bottom:.75rem;padding:1.25rem 1.5rem}.trending-summary{color:var(--tw-color-slate-600);font-size:.9rem;line-height:1.8}.paper-links{gap:calc(var(--tw-spacing) * 2);flex-wrap:wrap;margin-bottom:1rem;display:flex}.paper-links a{border-radius:var(--tw-radius-lg);background-color:var(--tw-color-slate-50);--tw-font-weight:var(--tw-font-weight-medium);font-weight:var(--tw-font-weight-medium);color:var(--tw-color-slate-600);border:1px solid var(--tw-color-slate-200);align-items:center;padding:.45rem .75rem;font-size:.8rem;text-decoration:none;display:inline-flex}.paper-links a:hover{background-color:var(--tw-color-blue-50);color:var(--tw-color-blue-700);border-color:var(--tw-color-blue-200)}[role=dialog]{border-radius:1.25rem}.pros-box,.cons-box{border-radius:.8rem;padding:1rem}.pros-box{background-color:var(--tw-color-emerald-50)}.cons-box{background-color:var(--tw-color-rose-50)}.section-label{margin-bottom:.5rem;font-size:.72rem;font-weight:600}.pros-box .section-label{color:var(--tw-color-emerald-700)}.cons-box .section-label{color:var(--tw-color-rose-700)}.point{color:var(--tw-color-slate-700);padding:.35rem 0;font-size:.85rem;line-height:1.75}.stats-bar{gap:calc(var(--tw-spacing) * 6);border-radius:var(--tw-radius-2xl);background-color:var(--tw-color-white);border:1px solid var(--tw-color-slate-200);flex-wrap:wrap;padding:1rem 1.25rem;display:flex}.stat-item{color:var(--tw-color-slate-500);font-size:.8rem}.stat-value{--tw-font-weight:var(--tw-font-weight-semibold);font-weight:var(--tw-font-weight-semibold);color:var(--tw-color-slate-900);margin-right:.4rem;font-size:1.1rem}@media (max-width:960px){.st-key-app_header{padding-bottom:1rem}.st-key-app_header [data-testid=stHorizontalBlock]{flex-wrap:wrap;gap:1rem .75rem}.st-key-app_header [data-testid=stColumn]{width:auto!important;min-width:0!important}.st-key-app_header [data-testid=stColumn]:first-child{flex:100%}.st-key-app_header [data-testid=stColumn]:nth-child(2){flex:35%}.st-key-app_header [data-testid=stColumn]:nth-child(3){flex:50%}.st-key-app_header [data-testid=stColumn]:nth-child(4){flex:100%}}@media (max-width:760px){.stMainBlockContainer{padding:1.5rem 1rem 3rem}.trending-panel{padding:1rem}}@media (prefers-reduced-motion:reduce){div[class*=st-key-paper_card_]{transition:none}}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}
|
src/styles/tailwind.css
CHANGED
|
@@ -85,17 +85,6 @@
|
|
| 85 |
[data-testid="stMetricLabel"] p { @apply tw:text-slate-500 tw:font-medium; font-size: .78rem; }
|
| 86 |
[data-testid="stMetricValue"] { @apply tw:text-slate-900 tw:font-semibold tw:tracking-tight; font-size: 1.8rem; }
|
| 87 |
[data-testid="stMetricValue"] > div { font-size: 1.8rem; }
|
| 88 |
-
.st-key-lifecycle_chart {
|
| 89 |
-
@apply tw:bg-white tw:rounded-2xl;
|
| 90 |
-
border: 1px solid var(--tw-color-slate-200);
|
| 91 |
-
padding: 1.25rem;
|
| 92 |
-
overflow-x: auto;
|
| 93 |
-
}
|
| 94 |
-
.st-key-lifecycle_chart [data-testid="stImage"] { min-width: 740px; }
|
| 95 |
-
.st-key-lifecycle_chart [data-testid="stElementContainer"]:has([data-testid="stImage"]) {
|
| 96 |
-
min-width: 740px;
|
| 97 |
-
}
|
| 98 |
-
.st-key-lifecycle_chart [data-testid="stImage"] img { width: 100% !important; }
|
| 99 |
[data-testid="stExpander"] details {
|
| 100 |
@apply tw:bg-white tw:rounded-xl;
|
| 101 |
border: 1px solid var(--tw-color-slate-200);
|
|
@@ -233,7 +222,6 @@ div[class*="st-key-paper_card_"] [data-testid="stButton"] button:hover p { color
|
|
| 233 |
@media (max-width: 760px) {
|
| 234 |
.stMainBlockContainer { padding: 1.5rem 1rem 3rem; }
|
| 235 |
.trending-panel { padding: 1rem; }
|
| 236 |
-
.st-key-lifecycle_chart { padding: .6rem; }
|
| 237 |
}
|
| 238 |
@media (prefers-reduced-motion: reduce) {
|
| 239 |
div[class*="st-key-paper_card_"] { transition: none; }
|
|
|
|
| 85 |
[data-testid="stMetricLabel"] p { @apply tw:text-slate-500 tw:font-medium; font-size: .78rem; }
|
| 86 |
[data-testid="stMetricValue"] { @apply tw:text-slate-900 tw:font-semibold tw:tracking-tight; font-size: 1.8rem; }
|
| 87 |
[data-testid="stMetricValue"] > div { font-size: 1.8rem; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
[data-testid="stExpander"] details {
|
| 89 |
@apply tw:bg-white tw:rounded-xl;
|
| 90 |
border: 1px solid var(--tw-color-slate-200);
|
|
|
|
| 222 |
@media (max-width: 760px) {
|
| 223 |
.stMainBlockContainer { padding: 1.5rem 1rem 3rem; }
|
| 224 |
.trending-panel { padding: 1rem; }
|
|
|
|
| 225 |
}
|
| 226 |
@media (prefers-reduced-motion: reduce) {
|
| 227 |
div[class*="st-key-paper_card_"] { transition: none; }
|
tests/test_lifecycle_figure.py
CHANGED
|
@@ -5,7 +5,7 @@ import matplotlib
|
|
| 5 |
|
| 6 |
matplotlib.use("Agg")
|
| 7 |
from matplotlib import pyplot as plt
|
| 8 |
-
from matplotlib.text import Annotation
|
| 9 |
|
| 10 |
from src.lifecycle_figure import PHASES_ORDER, _font_settings, render_hype_cycle
|
| 11 |
|
|
@@ -29,87 +29,35 @@ class LifecycleFigureTests(unittest.TestCase):
|
|
| 29 |
|
| 30 |
def topic_labels(self, fig):
|
| 31 |
return [
|
| 32 |
-
text.get_text()
|
| 33 |
-
for
|
| 34 |
-
if
|
|
|
|
| 35 |
]
|
| 36 |
|
| 37 |
-
def
|
| 38 |
data = sample_topics()
|
| 39 |
fig = render_hype_cycle(data, False)
|
| 40 |
labels = self.topic_labels(fig)
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def test_requested_limit_applies_to_each_phase(self):
|
| 50 |
-
for limit in (5, 15, 20):
|
| 51 |
-
with self.subTest(limit=limit):
|
| 52 |
-
fig = render_hype_cycle(sample_topics(), False, topics_per_phase=limit)
|
| 53 |
-
self.assertEqual(len(self.topic_labels(fig)), 5 * limit)
|
| 54 |
-
plt.close(fig)
|
| 55 |
-
|
| 56 |
-
def test_sparse_phases_keep_available_topics_and_show_totals(self):
|
| 57 |
data = {"one": sample_topics(1)["0-0"]}
|
| 58 |
fig = render_hype_cycle(data, False)
|
| 59 |
|
| 60 |
self.assertEqual(self.topic_labels(fig), ["Research topic 0-00"])
|
| 61 |
-
texts = [text.get_text() for ax in fig.axes for text in ax.texts]
|
| 62 |
-
self.assertIn("Showing 1 / 1 topics", texts)
|
| 63 |
-
self.assertEqual(texts.count("No topics in this phase"), 4)
|
| 64 |
self.assertIsNone(render_hype_cycle({}, False))
|
| 65 |
|
| 66 |
-
def test_twenty_long_labels_fit_inside_their_panels_without_overlap(self):
|
| 67 |
-
for lang in (False, True):
|
| 68 |
-
with self.subTest(lang=lang):
|
| 69 |
-
data = sample_topics(20)
|
| 70 |
-
for key, topic in data.items():
|
| 71 |
-
topic["topic"] = (
|
| 72 |
-
f"多模态大语言模型的长上下文推理与高效训练研究 {key}"
|
| 73 |
-
if lang else
|
| 74 |
-
f"Long-context multimodal reasoning and efficient language model training {key}"
|
| 75 |
-
)
|
| 76 |
-
fig = render_hype_cycle(data, lang, topics_per_phase=20)
|
| 77 |
-
fig.canvas.draw()
|
| 78 |
-
renderer = fig.canvas.get_renderer()
|
| 79 |
-
for ax in fig.axes[1:]:
|
| 80 |
-
bounds = ax.get_window_extent(renderer)
|
| 81 |
-
labels = [t for t in ax.texts if t.get_gid() == "topic-label"]
|
| 82 |
-
previous = None
|
| 83 |
-
for text in labels:
|
| 84 |
-
box = text.get_window_extent(renderer)
|
| 85 |
-
self.assertGreaterEqual(box.x0, bounds.x0)
|
| 86 |
-
self.assertLessEqual(box.x1, bounds.x1)
|
| 87 |
-
self.assertGreaterEqual(box.y0, bounds.y0)
|
| 88 |
-
self.assertLessEqual(box.y1, bounds.y1)
|
| 89 |
-
if previous is not None:
|
| 90 |
-
self.assertLess(box.y1, previous.y0)
|
| 91 |
-
previous = box
|
| 92 |
-
plt.close(fig)
|
| 93 |
-
|
| 94 |
def test_language_rendering_does_not_change_global_font_settings(self):
|
| 95 |
before = list(matplotlib.rcParams["font.sans-serif"])
|
| 96 |
render_hype_cycle(sample_topics(1), True)
|
| 97 |
self.assertEqual(matplotlib.rcParams["font.sans-serif"], before)
|
| 98 |
|
| 99 |
-
def test_curve_numbers_do_not_overlap_at_twenty_topics_per_phase(self):
|
| 100 |
-
fig = render_hype_cycle(sample_topics(), False, topics_per_phase=20)
|
| 101 |
-
fig.canvas.draw()
|
| 102 |
-
renderer = fig.canvas.get_renderer()
|
| 103 |
-
numbers = [
|
| 104 |
-
text for text in fig.axes[0].texts
|
| 105 |
-
if isinstance(text, Annotation) and text.get_text().isdigit()
|
| 106 |
-
]
|
| 107 |
-
# Annotation extents include the leader line; compare only the text.
|
| 108 |
-
boxes = [Text.get_window_extent(text, renderer).expanded(1.15, 1.15) for text in numbers]
|
| 109 |
-
for index, box in enumerate(boxes):
|
| 110 |
-
for other in boxes[index + 1:]:
|
| 111 |
-
self.assertFalse(box.overlaps(other), "Topic numbers overlap on the curve")
|
| 112 |
-
|
| 113 |
def test_chinese_font_is_retained_when_figure_is_drawn_after_return(self):
|
| 114 |
if "font.family" not in _font_settings(True):
|
| 115 |
self.skipTest("No CJK font installed")
|
|
|
|
| 5 |
|
| 6 |
matplotlib.use("Agg")
|
| 7 |
from matplotlib import pyplot as plt
|
| 8 |
+
from matplotlib.text import Annotation
|
| 9 |
|
| 10 |
from src.lifecycle_figure import PHASES_ORDER, _font_settings, render_hype_cycle
|
| 11 |
|
|
|
|
| 29 |
|
| 30 |
def topic_labels(self, fig):
|
| 31 |
return [
|
| 32 |
+
text.get_text()
|
| 33 |
+
for text in fig.axes[0].texts
|
| 34 |
+
if isinstance(text, Annotation)
|
| 35 |
+
and text.get_text() not in ("Maturity", "成熟度")
|
| 36 |
]
|
| 37 |
|
| 38 |
+
def test_compact_curve_keeps_original_topic_limits_and_ranking(self):
|
| 39 |
data = sample_topics()
|
| 40 |
fig = render_hype_cycle(data, False)
|
| 41 |
labels = self.topic_labels(fig)
|
| 42 |
|
| 43 |
+
expected = [f"Research topic 0-{rank:02}" for rank in (23, 22, 21)]
|
| 44 |
+
for phase_index, limit in enumerate((4, 4, 3, 2), start=1):
|
| 45 |
+
expected.extend(f"Research topic {phase_index}-{rank:02}" for rank in range(limit))
|
| 46 |
+
self.assertEqual(labels, expected)
|
| 47 |
+
self.assertEqual(len(fig.axes), 1)
|
| 48 |
+
|
| 49 |
+
def test_sparse_phases_keep_available_topics_and_empty_data_has_no_figure(self):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
data = {"one": sample_topics(1)["0-0"]}
|
| 51 |
fig = render_hype_cycle(data, False)
|
| 52 |
|
| 53 |
self.assertEqual(self.topic_labels(fig), ["Research topic 0-00"])
|
|
|
|
|
|
|
|
|
|
| 54 |
self.assertIsNone(render_hype_cycle({}, False))
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
def test_language_rendering_does_not_change_global_font_settings(self):
|
| 57 |
before = list(matplotlib.rcParams["font.sans-serif"])
|
| 58 |
render_hype_cycle(sample_topics(1), True)
|
| 59 |
self.assertEqual(matplotlib.rcParams["font.sans-serif"], before)
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def test_chinese_font_is_retained_when_figure_is_drawn_after_return(self):
|
| 62 |
if "font.family" not in _font_settings(True):
|
| 63 |
self.skipTest("No CJK font installed")
|