Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 46 additions & 17 deletions assets/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -747,18 +747,18 @@ function formatDetailRow(rowData) {
function initDataTable(data) {
const tableData = data.map(row => [
null,
{ display: truncateText(row.title_o || row.author_o, 50), search: `${row.title_o || ''} ${row.author_o || ''} ${row.journal_o || ''}` },
{ display: truncateText(row.title_o || row.author_o, 50), search: `${row.title_o || ''} ${searchAuthorNames(row.author_o)} ${row.journal_o || ''}` },
row.year_o || '-',
{ display: truncateText(row.title_r || row.author_r, 50), search: `${row.title_r || ''} ${row.author_r || ''} ${row.journal_r || ''}` },
{ display: truncateText(row.title_r || row.author_r, 50), search: `${row.title_r || ''} ${searchAuthorNames(row.author_r)} ${row.journal_r || ''}` },
row.year_r || '-',
{ display: getOutcomeBadgeShort(row.outcome), search: `${row.outcome || ''} ${row.outcome_quote || ''}` },
{ display: getOutcomeBadgeShort(row.outcome), search: row.outcome_quote || '' },
truncateText(row.type, 15) || '-',
{ display: formatDOI(row.doi_o, true), search: row.doi_o || '' },
{ display: formatUrlOrDoiShort(row.url_r, row.doi_r), search: `${row.url_r || ''} ${row.doi_r || ''}` }
{ display: formatUrlOrDoiShort(row.url_r, row.doi_r), search: row.doi_r || '' }
]);

dataTable = $('#flora-table').DataTable({
data: tableData, responsive: false, autoWidth: false, pageLength: 25, dom: 'lrtip',
data: tableData, responsive: false, autoWidth: false, deferRender: true, pageLength: 25, dom: 'lrtip',
lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "All"]],
order: [[4, 'desc']],
language: { search: "Search:", searchPlaceholder: "Filter studies (searches full references)...", zeroRecords: "No matching reference pairs", emptyTable: "No matching reference pairs" },
Expand All @@ -773,7 +773,7 @@ function initDataTable(data) {
createdRow: (row, d, dataIndex) => { $(row).attr('data-index', dataIndex); row.querySelector('.study-details').setAttribute('aria-label', 'Show evidence for ' + (data[dataIndex].title_o || 'original report')); }
});

dataTable.on('order.dt', () => { if (bmInitialized) updateBrowseResults(); syncBrowseUrl(); });
dataTable.on('order.dt', () => { if (bmInitialized) updateBrowseResults(false); syncBrowseUrl(); });

$('#flora-table tbody').on('click', 'button.study-details', function() {
const tr = $(this).closest('tr');
Expand All @@ -787,12 +787,19 @@ function initDataTable(data) {

// ===== Browse mobile =====
const BM_PAGE_SIZE = 20;
const bmSortCollator = new Intl.Collator(undefined, {numeric:true});
let bmFiltered = []; let bmPage = 0; let bmInitialized = false;
const bmSearchText = new WeakMap();

function bmSearchableText(row) {
return [row.title_o, row.author_o, row.journal_o, row.year_o, row.doi_o,
row.title_r, row.author_r, row.journal_r, row.year_r, row.doi_r, row.url_r,
row.outcome, row.outcome_quote, row.type].filter(Boolean).join(' | ').toLowerCase();
if (!bmSearchText.has(row)) {
// Keep quotations searchable, but omit raw outcome values and non-DOI URLs.
const urlDoi = /^https?:\/\/(?:dx\.)?doi\.org\//i.test(row.url_r || '') ? row.url_r : '';
bmSearchText.set(row, normalizedQuery([row.title_o, searchAuthorNames(row.author_o), row.journal_o, row.year_o, row.doi_o,
row.title_r, searchAuthorNames(row.author_r), row.journal_r, row.year_r, row.doi_r, urlDoi,
row.outcome_quote, row.type].filter(Boolean).join(' | ')));
}
return bmSearchText.get(row);
}
function bmHref(doi, url) { return doi ? safeHref(String(doi).startsWith('http') ? doi : 'https://doi.org/' + encodeURI(doi)) : safeHref(url); }
function bmAuthorYear(authorData, year) { const a = shortAuthors(authorData); const y = year ? `(${year})` : ''; return [a, y].filter(Boolean).join(' '); }
Expand Down Expand Up @@ -851,30 +858,52 @@ function setupBrowseMobile(data) {
const prev = document.getElementById('bm-prev');
const next = document.getElementById('bm-next');
let debounceTimer;
input.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => bmApplySearch(input.value), 150); });
input.addEventListener('input', () => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => bmApplySearch(input.value), 200); });
prev.addEventListener('click', () => { if (bmPage > 0) { bmPage--; bmRender(); document.querySelector('.app-main').scrollTo({ top: 0, behavior: 'smooth' }); } });
next.addEventListener('click', () => { bmPage++; bmRender(); document.querySelector('.app-main').scrollTo({ top: 0, behavior: 'smooth' }); });
input.value = browseQuery; bmApplySearch(browseQuery);
}

function normalizedQuery(query) {
return String(query || '').toLowerCase().replace(/https?:\/\/(?:dx\.)?doi\.org\//g, '').replace(/\bdoi:\s*/g, '').trim();
return String(query || '').toLowerCase().normalize('NFD').replace(/\p{M}/gu, '')
.replace(/[‘’]/g, "'").replace(/[“”]/g, '"')
.replace(/https?:\/\/(?:dx\.)?doi\.org\//g, '').replace(/\bdoi:\s*/g, '').replace(/\s+/g, ' ').trim();
}
// Search all names, without indexing Crossref keys, ORCIDs or affiliations.
function searchAuthorNames(raw) {
if (!raw) return '';
try {
const authors = typeof raw === 'string' ? JSON.parse(raw) : raw;
const name = a => typeof a === 'string' ? a : a && (a.name || [a.given, a.family].filter(Boolean).join(' ')) || '';
return (Array.isArray(authors) ? authors : [authors]).map(name).join(', ');
} catch { return String(raw); }
}
let lastSearchQuery, lastSearchTokens = [];
function searchTokens(query) {
if (query !== lastSearchQuery) {
lastSearchQuery = query;
lastSearchTokens = (normalizedQuery(query).match(/"[^"\n]+"|\S+/g) || []).map(token => token.replace(/^"|"$/g, ''));
}
return lastSearchTokens;
}
function matchesSearchTokens(text, tokens) {
return tokens.every(token => text.includes(token));
}
function matchesBrowseQuery(row, query) {
const text = bmSearchableText(row);
const tokens = normalizedQuery(query).match(/"[^"\n]+"|\S+/g) || [];
return tokens.every(token => text.includes(token.replace(/^"|"$/g, '')));
const tokens = searchTokens(query);
return !tokens.length || matchesSearchTokens(bmSearchableText(row), tokens);
}
let updatingBrowse = false;
function updateBrowseResults() {
function updateBrowseResults(redraw = true) {
if (updatingBrowse) return;
updatingBrowse = true;
bmFiltered = getChartData();
if (dataTable) {
const [column, direction] = dataTable.order()[0] || [4, 'desc'];
const field = {1:'title_o',2:'year_o',3:'title_r',4:'year_r',5:'outcome',6:'type',7:'doi_o',8:'doi_r'}[column] || 'year_r';
bmFiltered.sort((a,b) => String(a[field] || '').localeCompare(String(b[field] || ''), undefined, {numeric:true}) * (direction === 'desc' ? -1 : 1));
dataTable.draw(false);
bmFiltered.sort((a,b) => bmSortCollator.compare(String(a[field] || ''), String(b[field] || '')) * (direction === 'desc' ? -1 : 1));
// order.dt also fires during a draw: refresh cards/charts without a second draw.
if (redraw) dataTable.draw(false);
}
bmPage = 0;
if (bmInitialized) bmRender();
Expand Down
58 changes: 45 additions & 13 deletions assets/citation-impact.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@
});
}

function buildSearchIndex(index) {
// The citation snapshot's display names stop after six authors. The
// already-loaded FLoRA pairs retain full names; collect all records for
// each original and keep the snapshot names as a fallback.
const authorsByDoi = new Map();
for (const row of fullRowData) {
const doi = normalizeDoi(row.doi_o);
const names = searchAuthorNames(row.author_o);
if (!doi || !names) continue;
if (!authorsByDoi.has(doi)) authorsByDoi.set(doi, new Set());
authorsByDoi.get(doi).add(names);
}
return index.map(s => {
// Denominator is citations since the first replication, not lifetime
// citations — citing works published before any replication existed
// could never have co-cited one.
const cocit_prop = s.n_citations_post_first_rep > 0 ? s.n_cocitations / s.n_citations_post_first_rep : 0;
const authors = [...(authorsByDoi.get(normalizeDoi(s.doi)) || [])];
const searchText = normalizedQuery([s.title, searchAuthorNames(s.author), ...authors, s.year, s.venue, s.doi].filter(Boolean).join(' | '));
return { ...s, cocit_prop, searchText };
});
}

async function init() {
if (CI.loaded || CI.loading) return;
CI.loading = true;
Expand All @@ -83,13 +106,7 @@
if (!metaRes.ok || !aggRes.ok || !idxRes.ok) { showPlaceholder(); return; }
CI.meta = await metaRes.json();
CI.agg = await aggRes.json();
CI.index = (await idxRes.json()).map(s => {
// Denominator is citations since the first replication, not lifetime
// citations — citing works published before any replication existed
// could never have co-cited one.
const cocit_prop = s.n_citations_post_first_rep > 0 ? s.n_cocitations / s.n_citations_post_first_rep : 0;
return { ...s, cocit_prop };
});
CI.index = buildSearchIndex(await idxRes.json());

// Optional: ETWFE results (may not exist on first run)
try {
Expand Down Expand Up @@ -386,14 +403,12 @@
}

function renderTable() {
const q = (document.getElementById('search-input').value || '').toLowerCase();
const q = document.getElementById('search-input').value || '';
const tokens = searchTokens(q);
const outFilter = document.getElementById('filter-outcome').value;

let rows = CI.index.filter(s => {
if (q) {
const hay = `${s.title} ${s.author} ${s.doi}`.toLowerCase();
if (!hay.includes(q)) return false;
}
if (!matchesSearchTokens(s.searchText, tokens)) return false;
if (outFilter && !(s.outcome_mix && s.outcome_mix[outFilter])) return false;
return true;
});
Expand All @@ -416,6 +431,10 @@
});

const total = rows.length;
const empty = document.getElementById('ci-search-empty');
empty.hidden = total > 0;
document.getElementById('ci-search-empty-message').textContent = q.trim() ? `No originals match “${q.trim()}” with the current outcome selection.` : 'No originals match the current outcome selection.';
document.getElementById('ci-clear-search').hidden = !q.trim();
const pages = Math.max(1, Math.ceil(total / CI.perPage));
if (CI.page > pages) CI.page = 1;
const start = (CI.page - 1) * CI.perPage;
Expand All @@ -438,6 +457,7 @@
}).join('');

const pag = document.getElementById('pagination');
if (!total) { pag.innerHTML = ''; return; }
const winSize = 5;
const winStart = Math.max(1, CI.page - Math.floor(winSize / 2));
const winEnd = Math.min(pages, winStart + winSize - 1);
Expand Down Expand Up @@ -604,7 +624,12 @@
CI.outcome = btn.dataset.value; renderAggregate();
});
});
document.getElementById('search-input').addEventListener('input', () => { CI.page = 1; renderTable(); });
const input = document.getElementById('search-input');
let searchTimer;
input.addEventListener('input', () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => { CI.page = 1; renderTable(); }, 200); });
document.getElementById('ci-clear-search').addEventListener('click', () => {
clearTimeout(searchTimer); input.value = ''; input.dispatchEvent(new Event('input')); input.focus();
});
document.getElementById('filter-outcome').addEventListener('change', () => { CI.page = 1; renderTable(); });
function sortBy(col) {
if (CI.sortCol === col) {
Expand Down Expand Up @@ -708,6 +733,13 @@
// Lazy-load when the user opens the tab
document.getElementById('citation-tab').addEventListener('shown.bs.tab', ensureInit);

// A DOI deep link can load citation data before the main CSV finishes.
document.addEventListener('flora-ready', () => {
if (!CI.index) return;
CI.index = buildSearchIndex(CI.index);
if (CI.loaded) renderTable();
});

setupStudyTypeSelect('ci-study-type', kind => { ciKind = kind; renderCiKindGate(kind); });

// Open a study popup straight away if arrived via /citations/?doi=…
Expand Down
3 changes: 2 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ <h2 class="card-header">Browse all originals</h2>

<label for="search-input" class="search-label">Search originals</label>
<div class="search-row">
<input id="search-input" type="search" placeholder="Title, author or DOI" />
<input id="search-input" type="search" placeholder="Title, author, year, journal, DOI or DOI URL" />
<select id="filter-outcome" aria-label="Filter originals by any recorded replication outcome">
<option value="">All outcomes</option>
<option value="failed">Has failed replication</option>
Expand All @@ -648,6 +648,7 @@ <h2 class="card-header">Browse all originals</h2>
</select>
</div>

<div id="ci-search-empty" class="browse-empty" role="status" hidden><p id="ci-search-empty-message"></p><button type="button" id="ci-clear-search" class="browse-download">Clear search</button></div>
<div class="table-wrap">
<table id="originals-table">
<thead>
Expand Down
Loading
Loading