All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- Author-affiliation country recovery (frontend wire-up). After
every fresh scrape we now batch-query OpenAlex
/authorsfor the author IDs on every paper that has authors but no institution attribution, then walk each author'saffiliations[]and credit the paper to any institution whoseyears[]covers the paper'spublication_year. Result: a large chunk of the Unknown bucket moves back into named countries (~76 % recovery rate on a tested journal).- New module
src/aggregate/author-fallback.js—collectFallbackCandidateAuthorIds(works)+applyAuthorFallback(agg, works, authorsData). - Adds
country.dataInferred+country.topCountriesInferred+country.totalsInferredto the aggregation alongside the rawcountry.data(additive — schema_version stays at 1, old caches keep working). - "Infer Unknown" checkbox in the country card controls. Checked by default when the inferred view is available; flipping it to off instantly restores the raw ground-truth view (no re-aggregation — both datasets are kept in memory). The Unknown legend footnote updates wording to reflect which view is active.
- New OpenAlex client export
fetchAuthorsByIds(ids, opts)— batched filter (openalex:A1|A2|…, 50 IDs per request) sharing the same polite-pool token bucket + 429/5xx retry as every other call.
- New module
- "Last updated" line on the selected-source banner. Shows the cached date plus a relative phrase ("Today" / "5 days ago" / "2 months ago") so visitors can see at a glance how fresh the cached view is. Hidden when no cached timestamp is available.
- Feedback + GitHub Contribute sibling blocks alongside the existing
LinkedIn follow block — same compact visual treatment, all three live
in a single centred row that wraps on narrow viewports.
- Feedback button → Google Form
(
docs.google.com/forms/d/e/1FAIpQLSfFXn89994TW6iv5yI_ghjMpjHxFjaNQk8XdLtg_fw1t__LUQ/viewform). - GitHub button →
github.com/Journal-Trends.
- Feedback button → Google Form
(
- License line. MIT license now called out in three places: a small
centred line under the follow-row, a dedicated footer block, and a
new "License & source code" section in the About modal — all linking
to
opensource.org/licenses/MITand the GitHub org. - Content-mix chart interactivity parity with the country chart. The stacked per-year content-mix chart (work types) now has its own custom HTML legend, hover-highlight + click-to-pin per type, ✕ Clear-pins button, and a year-aware HTML tooltip showing per-type counts + share of the year — matching the country bar chart's UX exactly.
- Y-axis tick formatter unified across every chart via
shortNum(v)insrc/charts/shared.js: 2-significant-digit rounding so we no longer get duplicated ticks like2k, 2k, 3k, 3kwhen Chart.js picks tick positions 1750, 2250, 2750, 3250 etc. Picks up anMsuffix above 999 k. Applied tocountry.js(bar + line + content-mix),institution.js,pps-flagged.js,pps-detector.js,pps-country.js,pps-institution.js. - Mobile nav fits on a single line. ≤ 600 px: hide the SVG brand mark entirely, shrink the wordmark to 1.35 rem, hide the tagline, and tighten button padding so brand + Blog fit horizontally instead of wrapping to a second row.
- FAQ "Achal Agrawal" is now a LinkedIn link (was
<strong>). Both the visible accordion answer and the FAQPage JSON-LD body now point tolinkedin.com/in/achal-agrawal-phd-42331855. - Recently-checked timestamps respect singular vs plural — "1 min ago" / "5 mins ago", "1 hour ago" / "5 hours ago".
- Blog page nav rebuilt to match the SPA's nav exactly (minus the About button — the modal doesn't exist on blog pages). Uses the same rect-based JT SVG logo + topbar-link styling so the cross-link feels like one site.
- Paired with backend v0.11.0 which mirrors the author-fallback in
scripts/seed_delisted.pyso backend-seeded journals carry the inferred view too. Schema stays at v1.
- Search-input placeholder example switched from Nature → Cell. Nature has a ~59 % Unknown country bucket (a lot of its catalogue is older papers + non-research types where OpenAlex has no institution data), which would frame Journal Trends poorly as a first impression. Cell has ~100 % institution coverage on recent samples + 27 K works, so the example demo journal lands cleanly. New placeholder text: "Type journal name or ISSN (e.g. Cell, 0092-8674)".
- Paired with backend v0.10.1 (template re-sync only).
- Content-mix trends chart replaces the plain "Papers published per
year" line chart on the Country tab when the aggregation carries the
new per-year work-type counts. Same canvas, same position; stacked
bar where each year column is sub-divided by OpenAlex
work.type(article / review / letter / editorial / erratum / paratext / …). Stack height per year still equals total papers (same number the old line showed), but composition is now legible at a glance — useful as an integrity signal for journals dominated by editorials or letters. Top-of-chart legend names each type; hover any year column to see the per-type breakdown + total. - Backward-compatible dispatcher: old cached aggregations
(pre-v0.18, no
summary.contentMixByYearfield) fall back to the existing line chart silently. Only newly-scraped journals get the stacked view, until those old ones are re-scraped. - Aggregator captures
work.type— added toWORK_FIELDSinsrc/api/openalex.js. Aggregator emits two new fields undersummary:contentMix(lifetime per-type counts) andcontentMixByYear(per-year per-type counts). - Shared type palette in
src/charts/shared.js—TYPE_ORDER,typeColor(),typeLabel()exported so every chart that uses work-types stays visually consistent. - Blog link in the top nav, sitting between the spacer and About.
Goes to the existing
/blog/unknown-country-coverage.html. - LICENSE file (MIT) for the public-release push.
- Polished README for public visibility.
- FAQ "Who is the team behind this?" — appended a sentence attributing the codebase maintenance to Achal Agrawal with help from Claude Code. Mirrored in the FAQPage JSON-LD so Google Search displays the same answer.
- FAQ "Can I share a link to a specific journal?" — rewritten to
describe the new pretty
/journal/<slug>-<issn>URLs, the per-journal OG-thumbnail preview behaviour on LinkedIn / X / Slack, and the per-chart Share buttons. JSON-LD updated to match.
- Placeholder typing animation in the search input — reverted to
the static
"Type journal name or ISSN (e.g. Nature, 0028-0836)"placeholder. Thesrc/ui/placeholder-typer.jsmodule is deleted. - Standalone Content-mix card that briefly sat between the Scopus card and the tabs — superseded by the stacked trends chart on the Country tab, which shows the same composition information plus time-evolution.
- Paired with backend v0.10.0 (matching
content_mix+contentMixByYearcapture in the Python aggregator). summary.contentMix(lifetime) is still emitted even though no UI renders it — small, possibly useful for downstream API consumers.
-
netlify.toml— proper browser caching for static assets. Netlify's defaultCache-Control: max-age=0, must-revalidateforced a round-trip to the edge for every static file on every page load — making repeat visits unnecessarily slow (200-400 ms per file, ~3-5 sec total over ~12 files). New per-path overrides:/src/*→public, max-age=600, stale-while-revalidate=86400(10 min hard, 24 hr stale-while-revalidate)/data/*→public, max-age=604800, stale-while-revalidate=1209600(1 week hard, 2 weeks stale-while-revalidate — journal_index.json only changes when build_journal_index.py is re-run, monthly)/favicon.svg→public, max-age=86400/og-image.png→public, max-age=86400/blog/*→public, max-age=600, stale-while-revalidate=86400
Expected effect: repeat page loads drop from ~3-5 s to ~0.5 s for the same browser. The single download of
journal_index.jsonis the only significant network cost on first focus; subsequent visits for a week use the browser's local copy.
- After a deploy, users with a warm browser cache may briefly see
the previous version of the affected files (up to the
max-agewindow — 10 min for /src/, 1 week for /data/). Deploy during low-traffic hours to minimise impact. For urgent code changes, a hash-based filename convention would be the proper fix (requires a build step we currently don't have). - Backend unchanged — stays at v0.9.1.
- 'Unknown' country bucket on the country stacked-bar chart. Papers with no institution / country attribution in OpenAlex now land in a reserved 'Unknown' series at the end of the stack (darker grey, distinct from the existing 'Other' tail). The stack now sums to total works on every journal — no more silent data drops where OpenAlex's institutional graph is thin.
- Small footnote below the country chart legend (only shown when 'Unknown' is present): "*Country information not available in OpenAlex. Read how we plan to tackle this."
- New blog post at
/blog/unknown-country-coverage.html— "Investigating missing country information in OpenAlex — and a method to recover some of it". Covers the OpenAlex country-coverage gap, the audit of fallback fields, and the planned author-affiliation- history recovery method (deferred to a future release). - Reserved
UNKNOWN_COLOR(#5d6470, darker thanOTHER_COLOR) insrc/charts/shared.js;seriesColor()handles both aggregate buckets. - Animated placeholder typing in the search input. Cycles through
Nature → 0028-0836 → Science → 0036-8075 on the home page so first-
time visitors discover they can paste either a journal name or an
ISSN. Stops on first focus. New module
src/ui/placeholder-typer.js. Only enabled when the URL doesn't already carry an?issn=deep-link.
- Backward-compatible. Cached aggregations made with v0.16.x don't have the 'Unknown' bucket; charts just render without it — the silent-drop behaviour persists for those journals until they're re-scraped.
- Paired with backend v0.9.0 (matching 'Unknown' bucket in the Python
aggregator + PPS
Status='Genuine'filter).
- Site moved to
https://journaltrends.com(new apex domain on Cloudflare DNS, Let's Encrypt cert via Netlify). All hardcodedhttps://journaltrends.netlify.appreferences updated to the new domain in:index.html:<link rel="canonical">,og:url,og:image,twitter:image, JSON-LDWebApplication.url,SearchAction.urlTemplate,Organization.url,Organization.logo, and the FAQ answer textrobots.txt:Sitemap:directivellms.txt: example deep-linkssrc/ui/share.js: docstring referencing the share-PNG footer
netlify.tomlgains an explicit 301 redirect rule sending everyhttps://journaltrends.netlify.app/*request tohttps://journaltrends.com/:splat. Netlify's "primary domain" setting should install this automatically, but in current accounts it sometimes leaves the.netlify.appsubdomain returning 200; this rule makes the redirect deterministic. Path + query are preserved by:splatso old?issn=…bookmarks,/journal/<slug>-<issn>pretty URLs, and/sitemap.xmlall keep working.
- Paired with backend v0.8.1 which updates
FRONTEND_BASE_URLso/sitemap.xmlemitsjournaltrends.comURLs and per-journal prerendered HTML uses the new domain incanonical+og:image+og:url. - The Netlify
.netlify.appsubdomain stays alive as a 301-redirect shim — old bookmarks / Search-Console history are preserved as permanent-redirect signals to Google (link equity transfers). - Search Console action items (one-time after deploy):
- Add
https://journaltrends.com/as a new URL-prefix property. Verify via DNS TXT record (paste into Cloudflare). - Submit
sitemap.xmlon the new property. - On the old
journaltrends.netlify.appproperty, Settings → Change of Address → journaltrends.com.
- Add
- Journal-name autocomplete. Type any fragment of a journal name in
the search box and a dropdown shows the top 10 matching journals
with their ISSN + publisher. Click a suggestion -> the ISSN is
filled in and Lookup runs automatically. ISSN paste / type still
works exactly as before.
- Bundled
data/journal_index.json(~5.5 MB raw, ~1.45 MB gzipped over the wire — Netlify gzips on the fly): an array of[name, issn, publisher]for 84,346 journals + conferences + repositories withworks_count > 100, sorted by works-count descending so popular journals rank higher on substring match. - Lazy-loaded on first input focus (no first-paint penalty), debounced 150 ms per keystroke, in-memory after first load.
- Fallback: if the index fails to load, autocomplete silently disables and ISSN paste flow still works.
- Bundled
- Input placeholder updated to "Type journal name or ISSN (e.g. Nature, 0028-0836)" so users discover the new flow.
- Per-journal OG share thumbnails start working too — the
backend's v0.8.0 added a
/journal/<slug>-<issn>.pngroute, and every prerendered journal page already setsog:imageto it (server-side; no frontend code change). LinkedIn / X / Slack / WhatsApp previews now show the actual country chart instead of the generic site card. No re-sync oftemplates/journal.htmlneeded.
- Paired with backend v0.8.0 (OG thumbnail renderer +
scripts/build_journal_index.py). - Re-running
build_journal_index.pyand committing the resultingdata/journal_index.jsonis the monthly maintenance for keeping autocomplete coverage fresh as new journals come online.
- CSS + JS didn't load on any
/journal/<slug>-<issn>page. The three asset references inindex.htmlwere relative (src/style/theme.css,src/style/main.css,src/main.js), so on a sub-path URL the browser resolved them against the journal page — e.g.https://journaltrends.netlify.app/journal/foo-1234-5678/src/style/main.css— which 404'd. Result: unstyled HTML + dead SPA on every prerendered journal page. Fix: all three switched to absolute paths (/src/style/…,/src/main.js). Now resolves correctly regardless of the URL path the page is served from.
- Discovered on a real journal page:
https://journaltrends.netlify.app/journal/shodhkosh-journal-of-visual-and-performing-arts-2582-7472. - Paired with backend v0.7.1 (re-sync of
templates/journal.htmlfrom the fixedindex.html). - Search Console submission deferred until both deploys are live.
robots.txtSitemap:directive pointed at the dead old hostname (https://ach4l.pythonanywhere.com/sitemap.xml, from before the v0.5.2 backend rename tojournaltrends.pythonanywhere.com). Crawlers that hitrobots.txtfirst were 404'ing on the sitemap URL entirely. Now points at the Netlify-proxied URL on the verified Search Console property.
- Netlify rewrite
/sitemap.xml→ backend. The sitemap is generated dynamically by Flask from the livestatstable, but Search Console only accepts sitemaps on the same hostname as the verified property. Nowhttps://journaltrends.netlify.app/sitemap.xmlworks (status-200 proxy, identical to the/journal/*pattern).
- "Recently checked" anchors point at the SEO pretty URL form. Was
href="?issn=<issn>"; nowhref="/journal/<slug>-<issn>". Every home-page crawl by Googlebot now seeds 5 fresh internal links to prerendered journal pages — material lift in crawl discovery rate. Uses the sharedslugify()fromsrc/ui/url-state.jsso the URL matches what the backend treats as canonical (no 301 round-trip).
- Backend unchanged. Stays at v0.7.0.
- After this lands, submit
https://journaltrends.netlify.app/sitemap.xmlin Search Console → Sitemaps.
- Pretty per-journal URLs for SEO —
/journal/<slug>-<issn>(e.g./journal/reviews-of-modern-physics-0034-6861). When the user loads any journal, the address bar updates to the pretty form viahistory.replaceState, so every shared link / bookmark carries the journal name. Bots that visit the pretty URL get a real journal- named<title>,<meta description>, OG card, and JSON-LD — rendered server-side by the backend before any JS runs (paired with backend v0.7.0). slugify(name)helper insrc/ui/url-state.js(mirrors the backend's Python implementation so the URL the frontend writes matches what the backend treats as canonical — no 301 round-trip on every load).- Netlify proxy for
/journal/*→ backend, status 200 (rewrite). Keeps the visible URL onjournaltrends.netlify.appwhile the prerendered HTML comes from PA.
readIssnFromUrl()parses both the pretty path (/journal/<slug>-<issn>) and the legacy query (?issn=…). Old bookmarked / shared?issn=links still work; the SPA rewrites them to the pretty form once the journal name is known.writeIssnToUrl(issn, sourceName)gains asourceNamearg — passed at every call site (cache-hit + fresh-fetch paths). Without a name the function falls back to the legacy query form.- Scopus card tweaks (post-0.14.0 polish):
- SCOPUS pill is now orange to match the card's left-edge accent (was blue).
- "Inactive" badge is red (
#fc5c65); "Active" stays green. New.badge-dangerutility class. - ISSN / EISSN values removed from the card body — the journal name + Active/Inactive badge + Coverage carry the relevant information; ISSN duplicates what's already in the URL.
- Footer simplified to just "Scopus snapshot: March 2026"
(was the raw
2026-03plus row counts). InlineformatSnapshot()helper mapsYYYY-MMto Month YYYY.
- Paired with backend v0.7.0 (new
/journal/<slug>-<issn>routetemplates/journal.html+ per-journal sitemap entries +sync_frontend_template.pyto refresh the template).
- Backward compatibility: every old
?issn=…URL still works. - Sitemap on the backend now emits pretty URLs for all 294 cached
journals; the home
/URL stays as-is.
- Scopus indexing card above the tabs. For every loaded journal the
page now shows the journal's status in Elsevier's Scopus extension
list — Source Title, ISSN/EISSN, an Active/Inactive badge, and
Coverage (e.g.
1996-2026or split ranges like1996-2020; 2022-2026). If the journal is in the Discontinued Titles list, a second row shows either "Delisted by Scopus in YYYY" (forindexation_change == 'Discontinuation') or the softer "Journal-change policy noted in YYYY" (the other Elsevier category, which is not a hard delist). New module:src/ui/scopus-card.js, styles inmain.css. - Orange "Delisted by Scopus" vertical marker drawn at the
discontinuation year on four year-axis charts: country stacked bar,
papers-per-year line, institution stacked bar, and the PPS flagged-
share chart. Only fires for
'Discontinuation'rows — not'Journal change policy'. Implemented as a single Chart.js plugin (delistedMarkerPlugininshared.js) plus a shared registry sosetDelistedYear()triggers an in-placechart.update()on every subscribed chart — no full re-render needed when the marker arrives after the initial paint. getCachedStats(issn, { alsoIssns })insrc/api/backend.js— passes extra ISSNs (OpenAlex'sissn[]array) through to the backend as?also=…, so a print/electronic mismatch between the user's pasted ISSN and the Scopus row's ISSN/EISSN still resolves the right journal.
OTHER_COLORstepped one notch darker:#aab2bd→#8c949fso the catch-all reads as more clearly secondary against the coloured series.- Institution line chart skips the "Other" bucket. The stacked
bar still aggregates the long tail into a grey "Other" segment
(per v0.13.0), but the line chart below now omits it — a single
grey line aggregating every tail institution added noise without
insight. Line datasets re-map by
dataset.labelso colours stay aligned with the bar. clearSelection()(Change button) now also hides the Scopus card and clears the delisted-year marker.
- Paired with backend v0.6.0 which adds the
scopus_sources+scopus_discontinuedtables and joins them into/api/stats. - Live lookup, no cache invalidation. The Scopus join runs on
every
/api/statsGET, independent of the per-journal aggregation cache. All ~155 already-cached journals get Scopus info on their next page load — no schema bump, no re-scrape needed. - Source data: Elsevier's monthly "ext_list" workbook (Scopus Sources
- Discontinued Titles sheets). Snapshot version is shown in a footnote inside the card so visitors can tell when it was last refreshed.
- "Other" bucket shown on every trends chart, in light grey.
Country, institution, and the three PPS stacked charts (detector,
country, institution) now always render the aggregated "Other"
catch-all alongside the named series, so each stack reflects the
full picture rather than just the top-N. "Other" gets a reserved
desaturated light grey (
#aab2bd) — present but visually de-emphasised — across bars, legend dots, and tooltip swatches. NewOTHER_COLORconstant andseriesColor(label, i)helper insrc/charts/shared.js. - Institution aggregation now emits an "Other" bucket folding in
every institution past the per-country top 20. Country and PPS
aggregations already had one.
SCHEMA_VERSIONstays1— the field is additive, so the existing journal cache is not invalidated; already-cached journals gain institution "Other" on their next re-scrape.
- FAQ section reworked. Added a new lead question — "Who is the team behind this?" — crediting India Research Watch (IRW) and its founder Achal Agrawal. Rewrote the "which journal to publish", "is it free / what's the catch", "is this journal predatory", and "Scopus / Web of Science / Dimensions" answers. Both the visible accordion and the FAQPage JSON-LD updated 1:1.
- "Follow us on LinkedIn" block moved above the FAQ (was below it), with extra top margin on the FAQ section so the two are clearly separated.
- "Top growth countries" year-on-year trendline chart — the card,
its
src/charts/country-growth.jsmodule, thesrc/main.jswiring, and the now-dead growth computation insrc/aggregate/index.js(country.growth, theGROWTH_*constants, and the early/late-window block) are all gone; the indexed-growth view didn't communicate clearly. Country trends still show year-on-year publication counts plus the papers-per-year line. - The matching "How do I find fast-growing journals?" FAQ entry.
- Backend unchanged (stays at v0.5.1).
- OpenAlex rate-limit handler. A new
RateLimitErrorclass insrc/api/openalex.jsis thrown when the retry loop exhausts on a sustained 429 (vs. transient 5xx). Both the ISSN-lookup flow (src/ui/search.js) and the?issn=URL deep-link flow (src/main.js) catch it and render a friendly multi-paragraph HTML message — "OpenAlex is rate-limiting your IP right now" — pointing visitors at the Recently checked box and at direct?issn=URLs for cached journals (which already serve purely from the backend per v0.11.1). Replaces the bare "Lookup failed: OpenAlex 429 on …" one-liner that previously surfaced. showError()insrc/main.jsgains an{ html: true }mode for rich multi-paragraph error bodies (mirrors the option already insrc/ui/search.js).- Visible FAQ section at the bottom of the page (just above the
Follow us on LinkedIn block) with nine native
<details>accordions covering: who's behind Journal Trends (India Research Watch + Achal Agrawal), which journal to publish in, is it free / what's the catch, how to see trends for a specific journal, what the Problematic Paper Screener is, is a given journal predatory, which countries publish the most, how Journal Trends differs from Scopus / Web of Science / Dimensions, and how to share a per-journal link. Mirrors the FAQPage JSON-LD 1:1 so crawlers index the same copy that visitors see. OrganizationJSON-LD block withsameAs: ["linkedin.com/ company/journal-trends/"]so search engines and knowledge-graph builders can link the brand to its LinkedIn company page.featureList+ richerdescriptionon theWebApplicationJSON-LD, plus analternateNamearray of brand aliases ("JournalTrends", "Where Should I Publish", "OpenAlex Journal Statistics", "Free Journal Analysis Tool").- CSS for the FAQ accordions (
.faq,.faq-title,.faq-item,summary::before+/−markers, accent-coloured border when an item is open, mobile-responsive padding).
- Page
<title>rewritten for SEO target queries: "Journal Trends — Where Should I Publish? Free Journal Statistics, Publication Trends & PPS Flags". <meta description>rewritten around the "compare academic journals before you submit" framing.- Keywords expanded to cover the target search-engine queries ("where should I publish", "which journal should I publish in", "journal statistics", "compare journals before submitting", "predatory journal detection", "fastest growing journals", "paper mill detection", etc.).
- OG + Twitter cards updated to match the new title/description.
- FAQPage JSON-LD expanded from 4 → 9 questions (mirrors the visible accordion 1:1).
- Backend unchanged (stays at v0.5.1).
- The chart-specific OG-image preview on LinkedIn / X / Slack is
still served as the generic site
og-image.png— per-chart OG previews need a backend image-hosting endpoint and are planned for a later release.
- "Follow us on [LinkedIn] for more updates on new features and
analysis." A small centred block right above the footer, with the
LinkedIn glyph (inline SVG in LinkedIn brand blue
#0a66c2) linking to the company page linkedin.com/company/journal-trends (opens in a new tab,rel="noopener"). Lifts a touch on hover with a brand-tinted ring focus state.
- Backend unchanged (stays at v0.5.1).
- "Recently checked" block moved below the ticker (just above the footer). The top of the page is now cleaner — just the brand, the short pitch, and the ISSN input.
- Ticker reworded to " journals containing
papers analyzed using Journal Trends." Two counts: total journals cached and the sum of their
total_works. The papers total is computed client-side from the same/api/statslisting that drives the recent box, so the data is fresh on every page load. - Recent-entry timestamps now in local time via
Intl.DateTime Format. Relative for fresh entries ("just now", "23 min ago", "5 hours ago") and absolute thereafter ("May 18, 14:30" in the current year, with the year appended for older entries). Hovering the timestamp shows a tooltip with the full date + timezone for unambiguity.
- Backend unchanged (stays at v0.5.1).
?issn=…URL deep-links are now backend-first. When a visitor opens a journal URL directly (including clicks from the "Recently checked" box), the page hits the backend cache first and renders from the cached row without ever calling OpenAlex. Only on cache miss does it fall through to the OpenAlex source lookup + Fetch flow. Fixes the case where the recently-seeded journals couldn't be re-opened by visitors whose IP was rate-limited by OpenAlex — the cached data is now reachable purely through the backend.- Recent-box anchors navigate naturally (no SPA interceptor) so they benefit from the same backend-first render path on the next page load.
- Ticker reworded and shrunk:
- Text: "Number of journals analyzed using Journal Trends: ".
- Label font: 0.85rem → 0.78rem.
- Count font: 1.7rem (extra-bold) → 0.95rem (bold).
- Margin/padding tightened so the ticker feels like a tasteful footnote rather than a hero stat.
- Per-journal banner shown on cache-hit doesn't include the publisher
(the backend's
/api/stats/<issn>response doesn't carry it yet). This only affects the inline banner — the cached charts and the share text fall back gracefully. Publisher will return once the backend adds ahost_orgcolumn (planned for a later release). - Backend unchanged (stays at v0.5.1).
- Recently checked box above the ISSN input. Renders the five most
recently-cached journals (by
updated_atfrom the backend) as clickable cards. Each shows journal name + works count + last-update date. Clicking pushes?issn=<issn>and triggers the cache-hit render path. Publisher is intentionally omitted in v0.11.0 — see Notes. - Journals-analysed ticker at the bottom of the page above the footer. Shows the live count of distinct journals cached on the backend in big bold blue ("155 journals analysed"). Updates on the first page load + after every successful POST seeding.
- New
listStats()insrc/api/backend.js— fetches/api/stats, sorts byupdated_atDESC, returns[]on any failure (graceful degradation when the backend is unreachable).
- Cache-hit auto-renders on Lookup.
onSourceSelectednow awaits the backend cache check immediately. On a cache hit the charts render in-place and the URL gains?issn=<issn>— the user no longer has to click Fetch & analyze. On a cache miss the banner still shows so they can decide whether to spend time on a fresh scrape.startFetchbecomes idempotent so the URL-auto-load path doesn't re-render afteronSourceSelectedalready did. - Share text rewritten to a fuller pitch: "Find out country wise, institute wise and author wise publication trends of the journal titled "" published by using this link: ". Publisher is extracted from the source banner (set by the OpenAlex Lookup that always precedes a share).
- Amplitude analytics (script tag, inline init, session-replay
plugin, and the corresponding
cdn.amplitude.com/*.amplitude.com/worker-src blob:CSP entries). Google Analytics 4 remains.
- Publisher in the Recently-checked box is intentionally omitted
in v0.11.0. The backend's
/api/statslisting returns journal name- works count + timestamps but not
host_organization_name; adding it requires a small backend schema migration. Deferred to a future release when the backend bumps.
- works count + timestamps but not
- Per-chart Share button. A small "Share" pill appears in the
top-right corner of every chart card (10 cards total — country bar,
papers/year, country growth, institution stacked + line, author,
and the four PPS cards). Click opens a small popover with:
- Share… — native Web Share API (mobile only, when available); sends the chart PNG + URL + text to the user's chosen app.
- Download image — saves a branded PNG of the chart.
- LinkedIn — opens LinkedIn URL-share dialog.
- X (Twitter) — opens X intent composer pre-filled with the chart name + journal name + URL.
- Save for Instagram — downloads PNG with a hint to open Instagram on a phone and post the saved image.
- Copy link — copies the canonical journal URL to the clipboard.
- Branded PNG composition — exported images aren't raw transparent Chart.js canvases but full-bleed cards on the site's dark-theme background with header (journal name + chart title), the chart in the middle, and footer (Journal Trends wordmark + URL). Self- contained so reposts are recognisable without extra context.
- New
src/ui/share.jsmodule — single delegated click handler, auto-injects share buttons by walking the DOM for any.cardthat contains a<canvas>(idempotent; safe to re-call on re-render).
.cardgainsposition: relativeas the anchor for absolutely- positioned share buttons.- On phones the share button drops its text label and shows just the upload icon to save horizontal space.
- Limitation: LinkedIn and X share via URL only; their crawler
fetches the site's standard
og-image.pngas the preview thumbnail (not the chart). The actual chart-image-as-thumbnail path requires backend image hosting + dynamicog:imageURLs and is planned for v0.11.0. - Backend unchanged (stays at v0.5.1).
- Amplitude analytics (events + session replay + autocapture for
attribution, file downloads, form interactions, page views, sessions,
element interactions, network tracking, web-vitals, frustration
interactions). Loaded from
cdn.amplitude.comright after the GA block. Session replay sample rate set to 100 %.
- CSP loosened for Amplitude:
script-src+https://cdn.amplitude.comconnect-src+https://*.amplitude.com(covers events API + session-replay ingest)worker-srcadded as'self' blob:(session-replay spawns Web Workers from blob URLs)
- Google Analytics 4 (
gtag.js, measurement IDG-0Y9ND25YQW) loaded asynchronously from the very top of<head>so it never blocks render. Page-views fire on load via the standardgtag('config', …)call.
netlify.tomlCSP loosened to allow GA:script-src+'unsafe-inline'(for the inline gtag bootstrap) +https://www.googletagmanager.comconnect-src+https://www.google-analytics.com,https://*.google-analytics.com,https://www.googletagmanager.comimg-src+https://www.google-analytics.com,https://*.google-analytics.com
- Stale
ach4l.pythonanywhere.comURL inllms.txtcorrected tojournaltrends.pythonanywhere.com.
- Year-range modal removed entirely. Every cache-miss fetch now goes directly to "all years" — there's no opt-out. This fixes the v0.2.0–v0.8.x seeding gap where most users defaulted to "Last N years" (the modal's pre-selected option), skipped the backend POST, and never seeded the cache for the next visitor. Now the first visitor pays the full-fetch cost, everyone else gets the cached blob instantly.
- Progress label now shows an ETA. Before each scrape starts the label reads e.g. "Fetching all 48,647 works from OpenAlex (about 2 minutes). Please leave this tab open until the charts appear." ETA assumes ~600 works/sec effective throughput under the polite- pool rate limit. For multi-minute fetches the leave-tab-open hint is included.
- Mobile-responsive PPS tab. Card header rows stack vertically on
narrow viewports (
@media (max-width: 600px)), card-controls wrap to multiple rows, tab strip wraps to multiple rows, canvas heights shrink (300/380/420 → 220/300/340 → 200/260/300 at 380 px), and the legend chip wrap caps at 100 px height with internal scroll so it doesn't push charts below the fold. Same scaling applies to the other tabs that use the same card chrome. - New canvas-height variant classes —
.canvas-wrap-sm(300 px),.canvas-wrap-md(380 px),.canvas-wrap-lg(420 px) — that replace inlinestyle="height:XXXpx"attributes so the heights can respond to viewport via media queries.
- PPS Card 1 — amber
% flaggedline now draws on top of the bars via explicitorder: 0on the line dataset andorder: 2on the bar datasets (Chart.js draws lowerorderlast → in front). Line stroke nudged to 2.5 px and points get a thin dark ring so they pop against the stacked bars.
src/ui/year-range.jsmodule (no longer imported anywhere).- Year-range modal block from
index.html.
- Backend unchanged (stays at v0.5.1).
- Header subtitle PPS link. Display text changed from
"Problematic-Paper-Screener" (hyphenated) to "Problematic Paper
Screener" (no hyphens). Link target switched from the unstable
Oracle APEX URL to the stable
https://www.irit.fr/~Guillaume.Cabanac/problematic-paper-screener. - Default chart mode flipped to Absolute on all five toggle pairs:
country (
country-mode-abs), institution, pps-detector, pps-country, pps-institution. Stacked % is now the alternate. Raw counts read more intuitively on first glance; the percentage view stays one click away.
- Blink-hint animation on the "Stacked %" button: a 3-pulse CSS
animation (
.blink-hintclass,blink-hint-pulsekeyframes) draws the user's eye to the alternate option immediately after each chart first renders. One-shot; never re-fires on country dropdown changes or mode toggles. - New helper
flashHint(elementId)insrc/charts/shared.js— removes the.blink-hintclass, forces a reflow, re-adds it so the CSS animation restarts even if the class was already present.
- Backend unchanged (stays at v0.5.1 on PythonAnywhere).
-
Logo radically simplified. The frame, chart bars, J-curve, and tri-colour T from v0.7 are gone. The mark is now two extrabold letters with vertical split-colour fills: J — top 60 % amber, bottom 40 % blue. T — top 40 % amber, bottom 60 % blue. Each letter is built from rectangles + a per-letter linearGradient with hard colour-stops at the exact split percentage — no font dependency, identical rendering on every system.
-
Wordmark renamed to "Journal Trends" (capitalised, space instead of hyphen). Hyphen span removed. Visible space via
padding-left: 0.35chon.brand-trendsso it survivesdisplay: inline-flexwhitespace stripping. -
Fully responsive brand area via
clamp():.brand-markclamp(36px, 10vw, 88px)(36 → 88 px).brand-nameclamp(1.75rem, 8.5vw, 5rem)(28 → 80 px).brand-taglineclamp(0.65rem, 1.8vw, 0.9rem)(10.4 → 14.4 px).brandgapclamp(8px, 2vw, 18px)Verified to fit a 320 px viewport (oldest iPhone SE) with margin to spare. Topbar gainsflex-wrap: wrapso nav links fall to a new row if the brand needs the width. -
.brand-text,.brand-name,.brand-taglineswitched from<span>to<div>so the wordmark and tagline natively stack vertically, independent of CSS load order. Fixes the "tagline on same line as wordmark" bug seen with stale cached stylesheets. -
Defence against stale CSS.
JournalandTrendscolours are set via inlinestyle="color:…"inindex.htmlso the brand always renders blue+amber even if a visitor's browser is still serving an older cachedmain.css. The matching CSS rules remain as the source of truth. -
Backend URL in
src/api/backend.jsandnetlify.tomlCSP switched fromach4l.pythonanywhere.comto the actual deployed hostjournaltrends.pythonanywhere.com.
- Tagline "Choose journals wisely." under the wordmark in the
brand area. New
.brand-taglinediv inside a.brand-textflex wrapper. - OG image (regenerated) includes the same tagline below the wordmark so social shares carry the brand promise too.
- Compact body padding on small viewports
(
@media (max-width: 600px)→ 16 px;(max-width: 380px)→ 12 px).
- Tour button from the top nav. The tour itself still auto-runs
on first visit and can be replayed by clearing
localStorage.journal-trends.tour_seen; the final step now points at the About link in the nav instead of the removed Tour link.
- Backend unchanged (stays at v0.5.1) — code already deployed to PA
at
https://journaltrends.pythonanywhere.com.
- Logo redesigned to match the provided reference: blue rounded
frame enclosing a curved blue J (vertical descender + quadratic
hook + short foot), three ascending bars (amber, amber, pink) inside
the J's hook area, and a tri-colour T (blue cap + pink upper-stem
- amber lower-stem). Both
favicon.svgand the inline header SVG use the same 64-unit viewBox so they scale identically from 16-px tab favicon through ~36-px header mark to 256-px OG-image mark.
- amber lower-stem). Both
- Wordmark colours swapped.
journalis now blue (tips-pct),trendsis now amber (trend), and the hyphen is a neutral white at 60 % opacity to give the two coloured words breathing room. Mirrors the colour roles in the new mark. .brand-marksize 28 → 36 px so the frame + bars + JT all have room to read..brand-name1.1 → 1.15 rem, kept in proportion with the larger mark.scripts/build_og_image.pyredrawn to match the new mark (concentric rects for the frame stroke, sampled-Bezier for the J curve, tri-colour T). OG mark scaled to 256 px (was 224).
- Backend unchanged (stays at v0.5.1).
-
New JT logo. The mark now spells JT in chart-bar style: J = tall data column + two cascading shorter bars forming the hook, T = horizontal axis bar + centred data column, with an amber "current data point" dot at the top-right of the T. Both
favicon.svgand the inline header SVG share the design, so the browser tab reads "JT" at 16 px. -
Wordmark typography. "journal-trends" is now colour-split:
journalin white (text-strong),-in trend amber,trendsin tips blue. Same palette as the chart cards. Hover brightens both text segments without disturbing the hyphen accent.
- Initial-state intro section ("What you can explore" + example
deep-links) per request. The homepage now goes straight to nav +
header + ISSN input + footer. The
hideIntro()/showIntro()plumbing inmain.jsand the.intro*block inmain.cssare also gone.
scripts/build_og_image.pyupdated to render the new JT mark and the colour-split wordmark;og-image.pngregenerated.
- ISSN-not-found error is now actually helpful. Previously the message was a one-liner ("No OpenAlex source found for ISSN …"), which read as "the journal doesn't exist" even when the journal exists but isn't in OpenAlex under that ISSN (e.g. trade journals, popular press, niche titles, or journals indexed only under their electronic ISSN). The new message explains both failure modes and links to OpenAlex by name and by ISSN-as-text so the user has a concrete next step.
showError(msg, { html: true })mode insrc/ui/search.jsfor richer error bodies..errorCSS now styles<p>,<ul>,<strong>,<em>, and<a>children so multi-paragraph errors with embedded links read cleanly.
- Production frontend URL changed from
boisterous-naiad-3e4962.netlify.apptojournaltrends.netlify.app. Updated every reference:<link rel="canonical">,og:url,og:image(×2), JSON-LDWebApplication.urlandSearchAction.urlTemplate, the four example deep-links inllms.txt, and the URL footer inog-image.png(regenerated viascripts/build_og_image.py).
- Paired with backend v0.5.1, which updates
FRONTEND_BASE_URLso/sitemap.xmlemits the new domain. - If you still hold the old Netlify subdomain, configuring it as a
301 redirect (Netlify dashboard → Domain settings → Add domain alias
- redirect) preserves any external links / search-engine equity.
- Logo + top nav. Inline SVG mark (four ascending bars + amber trend
line) accompanies the wordmark in a new top navigation bar with
Tour and About links. Replaces the floating "?" button.
favicon.svgships at the site root. - About modal — opened from nav (and from a link in the footer disclaimer). Contains a project description, "How it works", source citations, and the full disclaimer.
- Initial-state intro section visible until a journal loads. Lists
what the four tabs explore and links four example journals via
?issn=deep-links (RMP, Nature, Science, AIP Conf. Proceedings). Hidden on render, restored on Change. - Footer disclaimer condensed version next to the attributions, with an inline link to open the full disclaimer.
- Open Graph + Twitter Card meta + generated
og-image.png(1200×630, dark theme, wordmark + tagline). Generator:scripts/build_og_image.py(Pillow). - JSON-LD structured data —
WebApplicationschema with aSearchActiontemplate for ISSN deep-links + aFAQPageschema with four common questions. Helps Google rich snippets and AI agents understand the site. robots.txt(allow all, points to backendSitemap:) andllms.txt(LLM-friendly site description with example deep-links and citations).- Stronger core SEO meta — descriptive
<title>,<meta description>, keywords,canonicalURL,theme-color,preconnecthints.
- Header
<h1>is now a real keyword-rich page heading ("Publication trends for any academic journal") rather than just the brand name. - The tour module now binds to the nav Tour link and updates its final step to point at the nav rather than the removed "?" button.
- Floating
.tour-launch"?" button and its CSS.
- Shareable URLs. Every loaded journal updates the address bar to
?issn=<issn>(viahistory.replaceState). Opening a link with?issn=auto-runs Lookup + Fetch & analyze on boot, so the URL alone deep-links the full chart view. Back/forward navigation between different?issn=values re-loads them. New module:src/ui/url-state.js. - First-time user tour. Six-step in-app walkthrough (welcome → ISSN
input → Lookup/Fetch → tabs → shareable URL → wrap-up) shown once per
browser, gated by
localStorage.journal-trends.tour_seen. A?button in the header re-launches the tour. No third-party library. New module:src/ui/tour.js. - Attribution footer with the OpenAlex citation (Priem, Piwowar & Orr, 2022 — arXiv 2205.01833) and the Problematic Paper Screener citation (Cabanac, Labbé & Magazinov, 2022 — arXiv 2210.04895), both linked.
- Random
mailtoidentifier for OpenAlex polite-pool — generated once per browser (viacrypto.getRandomValues) and stored inlocalStorage.journal-trends.email. The email-prompt modal is removed. Existing user-entered emails from v0.3.x are kept as-is. - PPS status line wording. The amber "stale" framing is gone. The per-entry snapshot version and the current backend dataset version are shown side by side as neutral facts. PPS snapshots are immutable by design: an entry is computed once at insert time and never recomputed. Users wanting the latest PPS view click Change and re-Lookup.
- Email-prompt modal in
index.html(auto-generation replaces it).
- PPS tab with four cards:
- Flagged share, year on year — stacked bar (flagged + unflagged segments)
with secondary-axis line of
% flagged. - Detector composition, year on year — stacked bar of top 15 detectors +
Other, fractional 1/N per flagged paper,
Stacked %/Absolutetoggle. - Country share of flagged papers, year on year — stacked bar, top 15 + Other, fractional 1/N per flagged paper.
- Institution share of flagged papers, year on year — country dropdown → stacked bar of top 15 institutions + Other.
- Flagged share, year on year — stacked bar (flagged + unflagged segments)
with secondary-axis line of
- Status line at the top of the PPS tab:
OpenAlex cached: <date> · PPS dataset (this entry): <ver> · Current PPS dataset: <ver> · <count> papers. Drifted versions are surfaced in amber. - Aggregator now emits
paperMeta[]alongside the aggregation ({doi, year, countries[], institutions[]}per work). It's never cached client- or backend-side — sent only in the POST body, then GC'd.
postStats(issn, …)now accepts and forwardspaperMeta.- Cache-hit path now passes the full PPS bundle into the renderer; cache-miss path renders OpenAlex tabs first, POSTs, then refreshes the PPS tab from the freshly-computed aggregation.
netlify.tomlCSP unchanged — same backend host already allowed.
- Companion release with
journal-trends-backendv0.3.0 (PPS dataset table, in-line PPS aggregation on POST,GET /api/pps/version, 80 MB POST cap).
- Backend-first lookup. On ISSN selection the frontend first hits the
companion service (
journal-trends-backendv0.2.0). If the journal is cached, the email prompt, year-range modal, and OpenAlex fetch are all skipped — charts render directly from the cached aggregation. src/api/backend.js:getCachedStats(issn),postStats(issn, …). Auto-targetshttp://localhost:5000when running on localhost andhttps://ach4l.pythonanywhere.comin production. Override at runtime with?backend=https://…for testing against another host.- Aggregation now carries
schema_version: 1(exported asSCHEMA_VERSIONfromsrc/aggregate/index.js). Backend rejects POSTs with mismatched versions; frontend treats GET responses of stale versions as cache misses.
- Main bootstrap flow split into "backend check → cache miss path". The cache miss path is the old v0.1.x flow (email → year range → OpenAlex → aggregate) followed by an automatic POST to the backend only if the user picked "all years" — partial year-range snapshots are kept local-only.
netlify.tomlCSPconnect-srcnow allows the PythonAnywhere backend.
- Paired release with
journal-trends-backendv0.2.0 (Flask + SQLite, deploys to PythonAnywhere free tier). - PPS-upload feature deferred to v0.3.0 per re-prioritisation.
- Institution tab: new trendline chart under the stacked bar, sharing the
legend, hover/pin state, and the existing
% / Absolutetoggle. - Country tab: new "Top growth countries" card — a multi-line chart of
the top 10 countries by growth, with
Indexed (100 = early window)andAbsolutemodes. - Aggregation now produces
country.growth = { earlyYears, lateYears, topGrowth: [{ code, growthPct, earlyAvg, lateAvg, byYear }] }.
- Bumped
TOP_INSTITUTIONSfrom 10 → 20 (per-country institution leaderboard). - Growth ranking uses Laplace-smoothed ratio
(lateAvg + 0.5) / (earlyAvg + 0.5) − 1so countries that grow from zero are rankable, with a min-total-papers filter (≥5 fractional) to suppress single-paper noise.
- Institution chart no longer carries a leftover singleton
chartreference after splitting bar/line into two Chart.js instances.
0.1.0 — 2026-05-11
- Repository scaffold and SDLC files (README, CHANGELOG, VERSION, .gitignore, netlify.toml).
- HTML shell with ISSN input + Lookup button, email-prompt modal, year-range modal, progress bar, tab container.
- Dark-theme CSS extracted from the AIP
/datavizskill palette. - OpenAlex API client (
src/api/openalex.js):lookupByIssn(issn)—/sources?filter=issn:lookup with ISSN normalisation.getSource(id)— single-source lookup.iterWorks(sourceId, opts)— async generator with cursor pagination, per-requestselect=projection, polite-poolmailto=, token-bucket rate limiting (5 rps), and exponential backoff on 429/5xx.- Non-retryable 4xx errors surface immediately (no retry on bad request).
- IndexedDB cache (
src/storage/cache.js) keyed bysourceId|fromYear|toYear. - Single-pass aggregation (
src/aggregate/):- country fractional counting (top 20 + "Other"),
- institution fractional counting per top-country (top 10 each),
- author presence counting (top 30).
- Three Chart.js views (
src/charts/): country trends, institution trends, author trends — built in the AIP/datavizstyle with shared tooltip, legend pinning/dimming, and stacked-% / absolute mode toggle. - Email-prompt UI that stores user-provided
mailtoinlocalStorage. - Year-range modal that asks "fetch all years / last N years / custom" before scraping.
- v0.1.0 is OpenAlex-only and single-source. Multi-source comparison and Crossref enrichment are planned for later milestones.