请先上传或载入会话以加载语义可视化。
-
@@ -797,7 +779,6 @@
载入会话
const TOPIC_VIZ_COLORS=['#2563eb','#1d4ed8','#0ea5e9','#10b981','#f97316','#f43f5e'];
const TOPIC_VIZ_CONFIG={
topic:{
- listSelector:'[data-topicviz-list="topic"]',
messageSelector:'[data-topicviz-message="topic"]',
keywordHintSelector:'[data-topicviz-keyword-hint="topic"]',
scatterHintSelector:'[data-topicviz-scatter-hint="topic"]',
@@ -805,7 +786,6 @@
载入会话
scatterId:'topic_viz_scatter_topic'
},
field:{
- listSelector:'[data-topicviz-list="field"]',
messageSelector:'[data-topicviz-message="field"]',
keywordHintSelector:'[data-topicviz-keyword-hint="field"]',
scatterHintSelector:'[data-topicviz-scatter-hint="field"]',
@@ -817,8 +797,7 @@
载入会话
active:'topic',
fetchedSession:'',
loading:false,
- data:{ topic:null, field:null },
- selected:{ topic:null, field:null }
+ data:{ topic:null, field:null }
};
/* ===================== 统计与图表工具 ===================== */
@@ -2566,87 +2545,76 @@
${category}
TOPIC_VIZ_STATE.fetchedSession='';
TOPIC_VIZ_STATE.loading=false;
TOPIC_VIZ_STATE.data={ topic:null, field:null };
- TOPIC_VIZ_STATE.selected={ topic:null, field:null };
TOPIC_VIZ_DIMENSIONS.forEach(dim=>{
clearTopicVizCharts(dim);
- const cfg=getTopicVizConfig(dim);
- if(cfg){
- const listEl=document.querySelector(cfg.listSelector);
- if(listEl){ listEl.innerHTML=''; }
- }
const message = SESSION_ID ? '语义可视化数据加载中…' : '请先上传或载入会话以加载语义可视化。';
setTopicVizMessage(dim, message);
});
}
-function updateTopicVizListActive(dim){
- const cfg = getTopicVizConfig(dim);
- const list = document.querySelector(cfg.listSelector);
- if(!list) return;
- const selected = TOPIC_VIZ_STATE.selected[dim] || '';
- list.querySelectorAll('.topic-viz-item').forEach(btn=>{
- const label = btn.dataset.label || '';
- const isActive = label === selected;
- btn.classList.toggle('topic-viz-item-active', isActive);
- btn.setAttribute('aria-pressed', isActive ? 'true' : 'false');
- });
-}
-
-function renderTopicVizKeywords(dim, category){
+function renderTopicVizKeywords(dim){
const cfg = getTopicVizConfig(dim);
- const chartEl = getTopicVizChartEl(dim, 'words');
- if(!chartEl) return;
+ const container = getTopicVizChartEl(dim, 'words');
+ if(!container) return;
const hintEl = cfg ? document.querySelector(cfg.keywordHintSelector) : null;
- if(!window.Plotly){
- chartEl.innerHTML='
Plotly 未加载
';
- if(hintEl){ hintEl.textContent=''; }
- return;
- }
- if(!category || !Array.isArray(category.top_words) || !category.top_words.length){
- try{ Plotly.purge(chartEl); }catch(e){}
- chartEl.innerHTML='
该分类暂未提取出关键词。
';
+ container.innerHTML='';
+ const data = TOPIC_VIZ_STATE.data[dim];
+ const categories = Array.isArray(data?.categories) ? data.categories : [];
+ if(!categories.length){
+ container.innerHTML='
暂无可展示的分类关键词。
';
if(hintEl){ hintEl.textContent=''; }
return;
}
- const items = category.top_words.slice(0, 12);
- const labels = items.map(item=>item?.token||'未命名');
- const counts = items.map(item=>safeNumber(item?.count||0));
- const weights = items.map(item=>safeNumber(item?.weight||0));
- const yLabels = labels.slice().reverse();
- const xValues = counts.slice().reverse();
- const percentTexts = weights.slice().reverse().map(w=>{
- if(!Number.isFinite(w) || w<=0) return '';
- return `${(w*100).toFixed(1)}%`;
- });
- const totalTokens = safeNumber(category.total_tokens||0);
- const totalDocs = safeNumber(category.count||0);
+ const sorted = categories.slice().sort((a,b)=>safeNumber(b?.count||0)-safeNumber(a?.count||0));
+ const limit = Math.min(sorted.length, 12);
+ const topCategories = sorted.slice(0, limit);
+ const grid=document.createElement('div');
+ grid.className='topic-viz-keywords-grid';
+ let totalDocs=0;
+ topCategories.forEach(cat=>{
+ const docCount = safeNumber(cat?.count||0);
+ totalDocs += docCount;
+ const card=document.createElement('article');
+ card.className='topic-viz-keyword-card';
+ const header=document.createElement('div');
+ header.className='topic-viz-keyword-header';
+ const title=document.createElement('h5');
+ title.className='topic-viz-keyword-title';
+ title.textContent=cat?.label||'未命名';
+ const meta=document.createElement('div');
+ meta.className='topic-viz-keyword-meta';
+ meta.textContent=`文献 ${docCount}`;
+ header.append(title, meta);
+ const wordList=document.createElement('div');
+ wordList.className='topic-viz-keyword-list';
+ const words = Array.isArray(cat?.top_words) ? cat.top_words.slice(0,6) : [];
+ if(words.length){
+ words.forEach(word=>{
+ const chip=document.createElement('span');
+ chip.className='topic-viz-keyword-chip';
+ const token = (word?.token||'').trim()||'未命名';
+ const weight = safeNumber(word?.weight||0);
+ chip.innerHTML=`
${escapeHtml(token)}${weight>0?`
${(weight*100).toFixed(1)}%`:''}`;
+ wordList.appendChild(chip);
+ });
+ }else{
+ const empty=document.createElement('div');
+ empty.className='topic-viz-keyword-meta';
+ empty.textContent='暂无关键词';
+ wordList.appendChild(empty);
+ }
+ card.append(header, wordList);
+ grid.appendChild(card);
+ });
+ container.appendChild(grid);
if(hintEl){
- const tokensText = totalTokens>0 ? `词频总计 ${totalTokens}` : '词频不足';
- hintEl.textContent = `文献 ${totalDocs} 篇 · ${tokensText}`;
- }
- const layout={
- margin:{l:110,r:24,t:30,b:40},
- height:Math.max(320, yLabels.length*28 + 160),
- paper_bgcolor:'rgba(255,255,255,0)',
- plot_bgcolor:'rgba(255,255,255,0)',
- xaxis:{title:'出现次数', rangemode:'tozero'},
- yaxis:{automargin:true},
- font:{family:'"Inter",system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif', color:'#0f172a'}
- };
- const trace={
- type:'bar',
- orientation:'h',
- x:xValues,
- y:yLabels,
- text:percentTexts,
- textposition:'outside',
- marker:{color:'#2563eb', opacity:0.9},
- hovertemplate:'
%{y}词频:%{x}
占比:%{text}
'
- };
- try{
- Plotly.react(chartEl, [trace], layout, {displaylogo:false, responsive:false, modeBarButtonsToRemove:['autoScale2d','select2d','lasso2d']});
- }catch(e){
- chartEl.innerHTML='
关键词图表渲染失败
';
+ const totalText = totalDocs>0 ? `覆盖文献 ${totalDocs}` : '';
+ const parts=[`展示前 ${topCategories.length} 个分类`];
+ if(categories.length>topCategories.length){
+ parts.push(`共 ${categories.length} 类`);
+ }
+ if(totalText) parts.push(totalText);
+ hintEl.textContent=parts.join(' · ');
}
}
@@ -2739,52 +2707,9 @@
${category}
}
}
-function renderTopicVizList(dim){
- const cfg = getTopicVizConfig(dim);
- const listEl = document.querySelector(cfg.listSelector);
- if(!listEl) return;
- listEl.innerHTML='';
- const data = TOPIC_VIZ_STATE.data[dim];
- const categories = Array.isArray(data?.categories) ? data.categories : [];
- if(!categories.length){
- listEl.innerHTML='
暂无分类数据。
';
- TOPIC_VIZ_STATE.selected[dim]=null;
- clearTopicVizCharts(dim);
- return;
- }
- categories.forEach(cat=>{
- const btn=document.createElement('button');
- btn.type='button';
- btn.className='topic-viz-item';
- btn.dataset.topicvizItem=dim;
- btn.dataset.label=cat.label||'';
- btn.innerHTML=`
${escapeHtml(cat.label||'未分类')}${safeNumber(cat.count||0)}`;
- btn.addEventListener('click',()=>selectTopicVizCategory(dim, cat.label));
- listEl.appendChild(btn);
- });
- const desired = TOPIC_VIZ_STATE.selected[dim] && categories.some(cat=>cat.label===TOPIC_VIZ_STATE.selected[dim])
- ? TOPIC_VIZ_STATE.selected[dim]
- : categories[0].label;
- selectTopicVizCategory(dim, desired);
-}
-
-function selectTopicVizCategory(dim, label){
- const data = TOPIC_VIZ_STATE.data[dim];
- const categories = Array.isArray(data?.categories) ? data.categories : [];
- if(!categories.length){
- TOPIC_VIZ_STATE.selected[dim]=null;
- clearTopicVizCharts(dim);
- return;
- }
- const target = categories.find(cat=>cat.label===label) || categories[0];
- TOPIC_VIZ_STATE.selected[dim] = target ? target.label : null;
- updateTopicVizListActive(dim);
- renderTopicVizKeywords(dim, target || null);
-}
-
function renderTopicVizPanel(dim){
- renderTopicVizList(dim);
renderTopicVizScatter(dim);
+ renderTopicVizKeywords(dim);
}
async function fetchTopicVisuals(options={}){
diff --git a/static/style.css b/static/style.css
index 10bf34a..5e2ac29 100644
--- a/static/style.css
+++ b/static/style.css
@@ -68,25 +68,23 @@
.topic-viz-message{font-size:0.85rem;color:#475569;background:rgba(248,250,252,0.85);border:1px dashed rgba(148,163,184,0.45);border-radius:1rem;padding:0.85rem 1rem;}
.topic-viz-message.hidden{display:none;}
.topic-viz-layout{display:grid;gap:1.5rem;}
-@media (min-width:1024px){.topic-viz-layout{grid-template-columns:280px minmax(0,1fr);}}
-.topic-viz-sidebar{display:flex;flex-direction:column;gap:1rem;border-radius:1.2rem;background:rgba(248,250,252,0.9);border:1px solid rgba(148,163,184,0.35);padding:1rem;box-shadow:inset 0 0 0 1px rgba(148,163,184,0.15);}
-.topic-viz-sidebar-header{display:flex;flex-direction:column;gap:0.35rem;}
-.topic-viz-sidebar-title{font-size:0.9rem;font-weight:600;color:#0f172a;}
-.topic-viz-sidebar-hint{font-size:0.75rem;color:#64748b;}
-.topic-viz-list{display:flex;flex-direction:column;gap:0.5rem;max-height:360px;overflow:auto;padding-right:0.25rem;}
-.topic-viz-item{display:flex;align-items:center;justify-content:space-between;gap:0.75rem;padding:0.45rem 0.75rem;border-radius:0.85rem;background:rgba(148,163,184,0.12);border:1px solid transparent;font-size:0.82rem;color:#475569;transition:background-color .15s ease,color .15s ease,border-color .15s ease;cursor:pointer;}
-.topic-viz-item:hover{background:rgba(148,163,184,0.22);color:#1e3a8a;}
-.topic-viz-item:focus-visible{outline:2px solid rgba(37,99,235,0.45);outline-offset:2px;}
-.topic-viz-item-active,.topic-viz-item[aria-pressed="true"]{background:linear-gradient(135deg,#2563eb 0%,#1d4ed8 100%);color:#fff;box-shadow:0 16px 28px -24px rgba(37,99,235,0.65);}
-.topic-viz-item-count{font-family:"JetBrains Mono","Fira Code",monospace;font-size:0.75rem;opacity:0.8;}
-.topic-viz-content{display:grid;gap:1.5rem;}
-@media (min-width:768px){.topic-viz-content{grid-template-columns:repeat(2,minmax(0,1fr));}}
+@media (min-width:1024px){.topic-viz-layout{grid-template-columns:repeat(2,minmax(0,1fr));align-items:start;}}
.topic-viz-card{display:flex;flex-direction:column;gap:0.85rem;border-radius:1.2rem;background:rgba(248,250,252,0.9);border:1px solid rgba(148,163,184,0.35);padding:1rem;box-shadow:0 24px 45px -40px rgba(15,23,42,0.55);min-height:100%;}
.topic-viz-card-header{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:0.5rem;}
.topic-viz-card-title{font-size:0.95rem;font-weight:600;color:#0f172a;}
.topic-viz-hint{font-size:0.75rem;color:#64748b;}
-.topic-viz-chart,.topic-viz-scatter{position:relative;width:100%;min-height:280px;border-radius:1rem;background:rgba(255,255,255,0.85);border:1px solid rgba(148,163,184,0.3);padding:0.5rem;}
-@media (max-width:768px){.topic-viz-chart,.topic-viz-scatter{min-height:240px;}}
+.topic-viz-scatter{position:relative;width:100%;min-height:320px;border-radius:1rem;background:rgba(255,255,255,0.85);border:1px solid rgba(148,163,184,0.3);padding:0.5rem;}
+@media (max-width:768px){.topic-viz-scatter{min-height:260px;}}
+.topic-viz-keywords{position:relative;width:100%;border-radius:1rem;background:rgba(255,255,255,0.85);border:1px solid rgba(148,163,184,0.3);padding:1rem;display:flex;flex-direction:column;gap:1rem;min-height:280px;}
+@media (max-width:768px){.topic-viz-keywords{min-height:240px;}}
+.topic-viz-keywords-grid{display:grid;gap:0.85rem;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));}
+.topic-viz-keyword-card{display:flex;flex-direction:column;gap:0.45rem;padding:0.75rem;border-radius:0.9rem;background:rgba(248,250,252,0.9);border:1px solid rgba(148,163,184,0.35);box-shadow:inset 0 0 0 1px rgba(148,163,184,0.12);}
+.topic-viz-keyword-header{display:flex;align-items:center;justify-content:space-between;gap:0.5rem;}
+.topic-viz-keyword-title{font-size:0.85rem;font-weight:600;color:#0f172a;}
+.topic-viz-keyword-meta{font-size:0.72rem;color:#64748b;}
+.topic-viz-keyword-list{display:flex;flex-wrap:wrap;gap:0.35rem;}
+.topic-viz-keyword-chip{display:inline-flex;align-items:center;gap:0.25rem;padding:0.28rem 0.6rem;border-radius:9999px;background:rgba(37,99,235,0.12);color:#1d4ed8;font-size:0.75rem;font-weight:500;}
+.topic-viz-keyword-chip strong{font-weight:600;color:#0f172a;}
.topic-viz-empty{display:flex;align-items:center;justify-content:center;min-height:220px;padding:1rem;border-radius:1rem;border:1px dashed rgba(148,163,184,0.45);color:#64748b;background:rgba(248,250,252,0.75);font-size:0.85rem;text-align:center;}
.chart-type-toggle{ display:inline-flex; align-items:center; gap:0.35rem; padding:0.25rem; border-radius:9999px; background:rgba(148,163,184,0.18);
border:1px solid rgba(148,163,184,0.35); }