Skip to content

Commit cea8f37

Browse files
committed
feat: dynamic language strip with full legend and faceted counts
- Language bar and percentages update when type/topic filters change - All languages shown in clickable legend (not just top 10) - Active language highlighted in both bar and legend
1 parent 7c74300 commit cea8f37

2 files changed

Lines changed: 53 additions & 23 deletions

File tree

apps/web/index.html

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,14 @@
136136
cursor: pointer;
137137
}
138138
.lang-bar:hover { opacity: 0.85; }
139+
.lang-bar.lang-active { outline: 2px solid #2D2D2D; outline-offset: -2px; }
139140
.lang-legend {
140141
display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px;
141142
font-size: 0.72rem; color: #9CA3AF;
142143
}
143-
.lang-legend-item { display: flex; align-items: center; gap: 5px; }
144+
.lang-legend-item { display: flex; align-items: center; gap: 5px; transition: color 0.15s; }
145+
.lang-legend-item:hover { color: #2D2D2D; }
146+
.lang-legend-active { color: #F97B6D; font-weight: 600; }
144147
.lang-dot { width: 8px; height: 8px; border-radius: 2px; flex-shrink: 0; }
145148

146149
/* Explore table */
@@ -585,6 +588,7 @@ <h1>The largest classified corpus of Word documents</h1>
585588
if (data.facets) {
586589
renderTypesGrid(data.facets.types);
587590
renderTopicsGrid(data.facets.topics);
591+
renderLangStrip(data.facets.languages);
588592
}
589593
} catch {
590594
$('explore-count').textContent = 'API unavailable';
@@ -636,37 +640,35 @@ <h1>The largest classified corpus of Word documents</h1>
636640
});
637641
}
638642

639-
// ---------- render stats ----------
640-
function renderStats(stats) {
641-
const h = stats.hero;
642-
$('stat-docs').textContent = fmt(h.total_documents);
643-
$('stat-langs').textContent = h.languages;
644-
$('stat-taxonomy').textContent = `${h.types} \u00d7 ${h.topics}`;
645-
$('stat-conf').textContent = (h.avg_confidence * 100).toFixed(0) + '%';
646-
647-
renderTypesGrid(stats.types);
648-
renderTopicsGrid(stats.topics);
649-
650-
// Language strip
651-
const topLangs = stats.languages.slice(0, 10);
652-
const otherPct = stats.languages.slice(10).reduce((s, l) => s + l.percentage, 0);
643+
function renderLangStrip(languages) {
644+
const topLangs = languages.slice(0, 10);
645+
const otherPct = languages.slice(10).reduce((s, l) => s + l.percentage, 0);
653646

647+
// Bar: top 10 + "other" segment
654648
let langHtml = '';
655-
let legendHtml = '';
656649
topLangs.forEach((l, i) => {
657650
const color = i < 5 ? CORAL_SHADES[i] : GRAY_SHADES[i - 5];
658-
langHtml += `<div class="lang-bar" style="flex:${l.percentage};background:${color}" data-lang="${l.code}" title="${l.name} ${l.percentage}%">${l.code.toUpperCase()}</div>`;
659-
legendHtml += `<div class="lang-legend-item"><span class="lang-dot" style="background:${color}"></span>${l.name} ${l.percentage}%</div>`;
651+
const active = filters.lang === l.code;
652+
langHtml += `<div class="lang-bar${active ? ' lang-active' : ''}" style="flex:${l.percentage};background:${color}" data-lang="${l.code}" title="${l.name} ${l.percentage}%">${l.code.toUpperCase()}</div>`;
660653
});
661654
if (otherPct > 0) {
662-
langHtml += `<div class="lang-bar" style="flex:${otherPct};background:#d0d0d0;color:#888;font-size:0.6rem">+${stats.languages.length - 10}</div>`;
655+
langHtml += `<div class="lang-bar" style="flex:${otherPct};background:#d0d0d0;color:#888;font-size:0.6rem">+${languages.length - 10}</div>`;
663656
}
664657

658+
// Legend: ALL languages, clickable
659+
let legendHtml = '';
660+
languages.forEach((l, i) => {
661+
const color = i < 5 ? CORAL_SHADES[i] : i < 10 ? GRAY_SHADES[i - 5] : '#d0d0d0';
662+
const active = filters.lang === l.code;
663+
legendHtml += `<div class="lang-legend-item${active ? ' lang-legend-active' : ''}" data-lang="${l.code}" style="cursor:pointer"><span class="lang-dot" style="background:${color}"></span>${l.name} ${l.percentage}%</div>`;
664+
});
665+
665666
$('lang-row').innerHTML = langHtml;
666667
$('lang-legend').innerHTML = legendHtml;
667668

668-
document.querySelectorAll('.lang-bar[data-lang]').forEach(bar => {
669-
bar.addEventListener('click', function() {
669+
// Click handlers for both bars and legend items
670+
document.querySelectorAll('[data-lang]').forEach(el => {
671+
el.addEventListener('click', function() {
670672
filters.lang = filters.lang === this.dataset.lang ? '' : this.dataset.lang;
671673
currentPage = 1;
672674
updateActiveStates();
@@ -676,6 +678,19 @@ <h1>The largest classified corpus of Word documents</h1>
676678
});
677679
}
678680

681+
// ---------- render stats ----------
682+
function renderStats(stats) {
683+
const h = stats.hero;
684+
$('stat-docs').textContent = fmt(h.total_documents);
685+
$('stat-langs').textContent = h.languages;
686+
$('stat-taxonomy').textContent = `${h.types} \u00d7 ${h.topics}`;
687+
$('stat-conf').textContent = (h.avg_confidence * 100).toFixed(0) + '%';
688+
689+
renderTypesGrid(stats.types);
690+
renderTopicsGrid(stats.topics);
691+
renderLangStrip(stats.languages);
692+
}
693+
679694
function renderSkeletons() {
680695
$('types-grid').innerHTML = Array(10).fill(
681696
'<div class="skeleton skeleton-cell" style="width:100%"></div>'

apps/web/worker/src/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ function buildFilters(url: URL): { where: string; params: unknown[]; paramIndex:
195195
function buildFiltersExcluding(url: URL, exclude: string): { where: string; params: unknown[] } {
196196
const type = exclude === "type" ? "" : (url.searchParams.get("type") || "");
197197
const topic = exclude === "topic" ? "" : (url.searchParams.get("topic") || "");
198-
const lang = url.searchParams.get("lang") || "";
198+
const lang = exclude === "lang" ? "" : (url.searchParams.get("lang") || "");
199199
const minConf = parseFloat(url.searchParams.get("min_confidence") || "0");
200200

201201
const conditions: string[] = ["document_type IS NOT NULL"];
@@ -223,8 +223,9 @@ async function handleDocuments(url: URL, env: Env, origin: string): Promise<Resp
223223
// Facet queries: exclude own dimension so all options remain visible with counts
224224
const typeFacet = buildFiltersExcluding(url, "type");
225225
const topicFacet = buildFiltersExcluding(url, "topic");
226+
const langFacet = buildFiltersExcluding(url, "lang");
226227

227-
const [countResult, rows, typeCounts, topicCounts] = await Promise.all([
228+
const [countResult, rows, typeCounts, topicCounts, langCounts] = await Promise.all([
228229
sql.query(`SELECT COUNT(*)::int AS total FROM documents WHERE ${where}`, params),
229230
sql.query(
230231
`SELECT id, original_filename AS filename, document_type, document_topic,
@@ -246,6 +247,13 @@ async function handleDocuments(url: URL, env: Env, origin: string): Promise<Resp
246247
GROUP BY document_topic ORDER BY count DESC`,
247248
topicFacet.params
248249
),
250+
sql.query(
251+
`SELECT COALESCE(NULLIF(language, 'unknown'), 'unknown') AS code, COUNT(*)::int AS count
252+
FROM documents WHERE ${langFacet.where}
253+
GROUP BY COALESCE(NULLIF(language, 'unknown'), 'unknown')
254+
ORDER BY count DESC LIMIT 20`,
255+
langFacet.params
256+
),
249257
]);
250258

251259
const total = countResult[0].total as number;
@@ -259,13 +267,20 @@ async function handleDocuments(url: URL, env: Env, origin: string): Promise<Resp
259267
confidence: r.classification_confidence,
260268
}));
261269

270+
const langTotal = (langCounts as { code: string; count: number }[]).reduce((s, l) => s + l.count, 0);
262271
const facets = {
263272
types: (typeCounts as { id: string; count: number }[]).map(t => ({
264273
id: t.id, label: TYPE_LABELS[t.id] || t.id, count: t.count,
265274
})),
266275
topics: (topicCounts as { id: string; count: number }[]).map(t => ({
267276
id: t.id, label: TOPIC_LABELS[t.id] || t.id, count: t.count,
268277
})),
278+
languages: (langCounts as { code: string; count: number }[]).map(l => ({
279+
code: l.code,
280+
name: LANG_NAMES[l.code] || l.code,
281+
count: l.count,
282+
percentage: Math.round((1000 * l.count) / langTotal) / 10,
283+
})),
269284
};
270285

271286
return json({ documents, total, page, pages: Math.ceil(total / limit), facets }, 200, origin);

0 commit comments

Comments
 (0)