Skip to content

Latest commit

 

History

History
1118 lines (985 loc) · 53.5 KB

File metadata and controls

1118 lines (985 loc) · 53.5 KB

Changelog

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.

[0.19.0] — 2026-05-28

Added

  • Author-affiliation country recovery (frontend wire-up). After every fresh scrape we now batch-query OpenAlex /authors for the author IDs on every paper that has authors but no institution attribution, then walk each author's affiliations[] and credit the paper to any institution whose years[] covers the paper's publication_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.jscollectFallbackCandidateAuthorIds(works) + applyAuthorFallback(agg, works, authorsData).
    • Adds country.dataInferred + country.topCountriesInferred + country.totalsInferred to the aggregation alongside the raw country.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.
  • "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.
  • 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/MIT and 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.

Changed

  • Y-axis tick formatter unified across every chart via shortNum(v) in src/charts/shared.js: 2-significant-digit rounding so we no longer get duplicated ticks like 2k, 2k, 3k, 3k when Chart.js picks tick positions 1750, 2250, 2750, 3250 etc. Picks up an M suffix above 999 k. Applied to country.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 to linkedin.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.

Notes

  • Paired with backend v0.11.0 which mirrors the author-fallback in scripts/seed_delisted.py so backend-seeded journals carry the inferred view too. Schema stays at v1.

[0.18.1] — 2026-05-27

Changed

  • 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)".

Notes

  • Paired with backend v0.10.1 (template re-sync only).

[0.18.0] — 2026-05-27

Added

  • 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.contentMixByYear field) 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 to WORK_FIELDS in src/api/openalex.js. Aggregator emits two new fields under summary: contentMix (lifetime per-type counts) and contentMixByYear (per-year per-type counts).
  • Shared type palette in src/charts/shared.jsTYPE_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.

Changed

  • 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.

Removed

  • Placeholder typing animation in the search input — reverted to the static "Type journal name or ISSN (e.g. Nature, 0028-0836)" placeholder. The src/ui/placeholder-typer.js module 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.

Notes

  • Paired with backend v0.10.0 (matching content_mix + contentMixByYear capture in the Python aggregator).
  • summary.contentMix (lifetime) is still emitted even though no UI renders it — small, possibly useful for downstream API consumers.

[0.17.1] — 2026-05-26

Changed

  • netlify.toml — proper browser caching for static assets. Netlify's default Cache-Control: max-age=0, must-revalidate forced 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.svgpublic, max-age=86400
    • /og-image.pngpublic, 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.json is the only significant network cost on first focus; subsequent visits for a week use the browser's local copy.

Notes

  • After a deploy, users with a warm browser cache may briefly see the previous version of the affected files (up to the max-age window — 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.

[0.17.0] — 2026-05-25

Added

  • '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 than OTHER_COLOR) in src/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.

Notes

  • 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).

[0.16.1] — 2026-05-25

Changed

  • Site moved to https://journaltrends.com (new apex domain on Cloudflare DNS, Let's Encrypt cert via Netlify). All hardcoded https://journaltrends.netlify.app references updated to the new domain in:
    • index.html: <link rel="canonical">, og:url, og:image, twitter:image, JSON-LD WebApplication.url, SearchAction.urlTemplate, Organization.url, Organization.logo, and the FAQ answer text
    • robots.txt: Sitemap: directive
    • llms.txt: example deep-links
    • src/ui/share.js: docstring referencing the share-PNG footer
  • netlify.toml gains an explicit 301 redirect rule sending every https://journaltrends.netlify.app/* request to https://journaltrends.com/:splat. Netlify's "primary domain" setting should install this automatically, but in current accounts it sometimes leaves the .netlify.app subdomain returning 200; this rule makes the redirect deterministic. Path + query are preserved by :splat so old ?issn=… bookmarks, /journal/<slug>-<issn> pretty URLs, and /sitemap.xml all keep working.

Notes

  • Paired with backend v0.8.1 which updates FRONTEND_BASE_URL so /sitemap.xml emits journaltrends.com URLs and per-journal prerendered HTML uses the new domain in canonical + og:image + og:url.
  • The Netlify .netlify.app subdomain 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):
    1. Add https://journaltrends.com/ as a new URL-prefix property. Verify via DNS TXT record (paste into Cloudflare).
    2. Submit sitemap.xml on the new property.
    3. On the old journaltrends.netlify.app property, Settings → Change of Address → journaltrends.com.

[0.16.0] — 2026-05-25

Added

  • 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 with works_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.
  • 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>.png route, and every prerendered journal page already sets og:image to 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 of templates/journal.html needed.

Notes

  • Paired with backend v0.8.0 (OG thumbnail renderer + scripts/build_journal_index.py).
  • Re-running build_journal_index.py and committing the resulting data/journal_index.json is the monthly maintenance for keeping autocomplete coverage fresh as new journals come online.

[0.15.2] — 2026-05-24

Fixed

  • CSS + JS didn't load on any /journal/<slug>-<issn> page. The three asset references in index.html were 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.

Notes

  • 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.html from the fixed index.html).
  • Search Console submission deferred until both deploys are live.

[0.15.1] — 2026-05-24

Fixed

  • robots.txt Sitemap: directive pointed at the dead old hostname (https://ach4l.pythonanywhere.com/sitemap.xml, from before the v0.5.2 backend rename to journaltrends.pythonanywhere.com). Crawlers that hit robots.txt first were 404'ing on the sitemap URL entirely. Now points at the Netlify-proxied URL on the verified Search Console property.

Added

  • Netlify rewrite /sitemap.xml → backend. The sitemap is generated dynamically by Flask from the live stats table, but Search Console only accepts sitemaps on the same hostname as the verified property. Now https://journaltrends.netlify.app/sitemap.xml works (status-200 proxy, identical to the /journal/* pattern).

Changed

  • "Recently checked" anchors point at the SEO pretty URL form. Was href="?issn=<issn>"; now href="/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 shared slugify() from src/ui/url-state.js so the URL matches what the backend treats as canonical (no 301 round-trip).

Notes

  • Backend unchanged. Stays at v0.7.0.
  • After this lands, submit https://journaltrends.netlify.app/sitemap.xml in Search Console → Sitemaps.

[0.15.0] — 2026-05-23

Added

  • 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 via history.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 in src/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 on journaltrends.netlify.app while the prerendered HTML comes from PA.

Changed

  • 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 a sourceName arg — 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-danger utility 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-03 plus row counts). Inline formatSnapshot() helper maps YYYY-MM to Month YYYY.

Notes

  • Paired with backend v0.7.0 (new /journal/<slug>-<issn> route
    • templates/journal.html + per-journal sitemap entries + sync_frontend_template.py to 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.

[0.14.0] — 2026-05-23

Added

  • 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-2026 or split ranges like 1996-2020; 2022-2026). If the journal is in the Discontinued Titles list, a second row shows either "Delisted by Scopus in YYYY" (for indexation_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 in main.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 (delistedMarkerPlugin in shared.js) plus a shared registry so setDelistedYear() triggers an in-place chart.update() on every subscribed chart — no full re-render needed when the marker arrives after the initial paint.
  • getCachedStats(issn, { alsoIssns }) in src/api/backend.js — passes extra ISSNs (OpenAlex's issn[] 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.

Changed

  • OTHER_COLOR stepped one notch darker: #aab2bd#8c949f so 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.label so colours stay aligned with the bar.
  • clearSelection() (Change button) now also hides the Scopus card and clears the delisted-year marker.

Notes

  • Paired with backend v0.6.0 which adds the scopus_sources + scopus_discontinued tables and joins them into /api/stats.
  • Live lookup, no cache invalidation. The Scopus join runs on every /api/stats GET, 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.

[0.13.0] — 2026-05-22

Added

  • "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. New OTHER_COLOR constant and seriesColor(label, i) helper in src/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_VERSION stays 1 — the field is additive, so the existing journal cache is not invalidated; already-cached journals gain institution "Other" on their next re-scrape.

Changed

  • 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.

Removed

  • "Top growth countries" year-on-year trendline chart — the card, its src/charts/country-growth.js module, the src/main.js wiring, and the now-dead growth computation in src/aggregate/index.js (country.growth, the GROWTH_* 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.

Notes

  • Backend unchanged (stays at v0.5.1).

[0.12.0] — 2026-05-22

Added

  • OpenAlex rate-limit handler. A new RateLimitError class in src/api/openalex.js is 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() in src/main.js gains an { html: true } mode for rich multi-paragraph error bodies (mirrors the option already in src/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.
  • Organization JSON-LD block with sameAs: ["linkedin.com/ company/journal-trends/"] so search engines and knowledge-graph builders can link the brand to its LinkedIn company page.
  • featureList + richer description on the WebApplication JSON-LD, plus an alternateName array 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).

Changed

  • 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).

Notes

  • 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.

[0.11.3] — 2026-05-21

Added

  • "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.

Notes

  • Backend unchanged (stays at v0.5.1).

[0.11.2] — 2026-05-20

Changed

  • "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/stats listing 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.

Notes

  • Backend unchanged (stays at v0.5.1).

[0.11.1] — 2026-05-20

Changed

  • ?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.

Notes

  • 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 a host_org column (planned for a later release).
  • Backend unchanged (stays at v0.5.1).

[0.11.0] — 2026-05-20

Added

  • Recently checked box above the ISSN input. Renders the five most recently-cached journals (by updated_at from 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() in src/api/backend.js — fetches /api/stats, sorts by updated_at DESC, returns [] on any failure (graceful degradation when the backend is unreachable).

Changed

  • Cache-hit auto-renders on Lookup. onSourceSelected now 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. startFetch becomes idempotent so the URL-auto-load path doesn't re-render after onSourceSelected already 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).

Removed

  • 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.

Notes

  • Publisher in the Recently-checked box is intentionally omitted in v0.11.0. The backend's /api/stats listing 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.

[0.10.0] — 2026-05-20

Added

  • 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.js module — single delegated click handler, auto-injects share buttons by walking the DOM for any .card that contains a <canvas> (idempotent; safe to re-call on re-render).

Changed

  • .card gains position: relative as 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.

Notes

  • Limitation: LinkedIn and X share via URL only; their crawler fetches the site's standard og-image.png as the preview thumbnail (not the chart). The actual chart-image-as-thumbnail path requires backend image hosting + dynamic og:image URLs and is planned for v0.11.0.
  • Backend unchanged (stays at v0.5.1).

[0.9.2] — 2026-05-18

Added

  • 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.com right after the GA block. Session replay sample rate set to 100 %.

Changed

  • CSP loosened for Amplitude:
    • script-src + https://cdn.amplitude.com
    • connect-src + https://*.amplitude.com (covers events API + session-replay ingest)
    • worker-src added as 'self' blob: (session-replay spawns Web Workers from blob URLs)

[0.9.1] — 2026-05-18

Added

  • Google Analytics 4 (gtag.js, measurement ID G-0Y9ND25YQW) loaded asynchronously from the very top of <head> so it never blocks render. Page-views fire on load via the standard gtag('config', …) call.

Changed

  • netlify.toml CSP loosened to allow GA:
    • script-src + 'unsafe-inline' (for the inline gtag bootstrap) + https://www.googletagmanager.com
    • connect-src + https://www.google-analytics.com, https://*.google-analytics.com, https://www.googletagmanager.com
    • img-src + https://www.google-analytics.com, https://*.google-analytics.com

Fixed

  • Stale ach4l.pythonanywhere.com URL in llms.txt corrected to journaltrends.pythonanywhere.com.

[0.9.0] — 2026-05-17

Changed

  • 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.

Added

  • 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 inline style="height:XXXpx" attributes so the heights can respond to viewport via media queries.

Fixed

  • PPS Card 1 — amber % flagged line now draws on top of the bars via explicit order: 0 on the line dataset and order: 2 on the bar datasets (Chart.js draws lower order last → in front). Line stroke nudged to 2.5 px and points get a thin dark ring so they pop against the stacked bars.

Removed

  • src/ui/year-range.js module (no longer imported anywhere).
  • Year-range modal block from index.html.

Notes

  • Backend unchanged (stays at v0.5.1).

Changed

  • 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.

Added

  • Blink-hint animation on the "Stacked %" button: a 3-pulse CSS animation (.blink-hint class, blink-hint-pulse keyframes) 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) in src/charts/shared.js — removes the .blink-hint class, forces a reflow, re-adds it so the CSS animation restarts even if the class was already present.

Notes

  • Backend unchanged (stays at v0.5.1 on PythonAnywhere).

[0.8.0] — 2026-05-17

Changed

  • 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.35ch on .brand-trends so it survives display: inline-flex whitespace stripping.

  • Fully responsive brand area via clamp(): .brand-mark clamp(36px, 10vw, 88px) (36 → 88 px) .brand-name clamp(1.75rem, 8.5vw, 5rem) (28 → 80 px) .brand-tagline clamp(0.65rem, 1.8vw, 0.9rem) (10.4 → 14.4 px) .brand gap clamp(8px, 2vw, 18px) Verified to fit a 320 px viewport (oldest iPhone SE) with margin to spare. Topbar gains flex-wrap: wrap so nav links fall to a new row if the brand needs the width.

  • .brand-text, .brand-name, .brand-tagline switched 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. Journal and Trends colours are set via inline style="color:…" in index.html so the brand always renders blue+amber even if a visitor's browser is still serving an older cached main.css. The matching CSS rules remain as the source of truth.

  • Backend URL in src/api/backend.js and netlify.toml CSP switched from ach4l.pythonanywhere.com to the actual deployed host journaltrends.pythonanywhere.com.

Added

  • Tagline "Choose journals wisely." under the wordmark in the brand area. New .brand-tagline div inside a .brand-text flex 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).

Removed

  • 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.

Notes

  • Backend unchanged (stays at v0.5.1) — code already deployed to PA at https://journaltrends.pythonanywhere.com.

[0.7.0] — 2026-05-16

Changed

  • 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.svg and 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.
  • Wordmark colours swapped. journal is now blue (tips-pct), trends is 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-mark size 28 → 36 px so the frame + bars + JT all have room to read.
  • .brand-name 1.1 → 1.15 rem, kept in proportion with the larger mark.
  • scripts/build_og_image.py redrawn 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).

Notes

  • Backend unchanged (stays at v0.5.1).

Added

  • 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.svg and the inline header SVG share the design, so the browser tab reads "JT" at 16 px.

  • Wordmark typography. "journal-trends" is now colour-split: journal in white (text-strong), - in trend amber, trends in tips blue. Same palette as the chart cards. Hover brightens both text segments without disturbing the hyphen accent.

Removed

  • 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 in main.js and the .intro* block in main.css are also gone.

Changed

  • scripts/build_og_image.py updated to render the new JT mark and the colour-split wordmark; og-image.png regenerated.

[0.5.2] — 2026-05-16

Changed

  • 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.

Added

  • showError(msg, { html: true }) mode in src/ui/search.js for richer error bodies.
  • .error CSS now styles <p>, <ul>, <strong>, <em>, and <a> children so multi-paragraph errors with embedded links read cleanly.

Changed

  • Production frontend URL changed from boisterous-naiad-3e4962.netlify.app to journaltrends.netlify.app. Updated every reference: <link rel="canonical">, og:url, og:image (×2), JSON-LD WebApplication.url and SearchAction.urlTemplate, the four example deep-links in llms.txt, and the URL footer in og-image.png (regenerated via scripts/build_og_image.py).

Notes

  • Paired with backend v0.5.1, which updates FRONTEND_BASE_URL so /sitemap.xml emits 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.

[0.5.0] — 2026-05-15

Added

  • 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.svg ships 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 dataWebApplication schema with a SearchAction template for ISSN deep-links + a FAQPage schema with four common questions. Helps Google rich snippets and AI agents understand the site.
  • robots.txt (allow all, points to backend Sitemap:) and llms.txt (LLM-friendly site description with example deep-links and citations).
  • Stronger core SEO meta — descriptive <title>, <meta description>, keywords, canonical URL, theme-color, preconnect hints.

Changed

  • 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.

Removed

  • Floating .tour-launch "?" button and its CSS.

[0.4.0] — 2026-05-15

Added

  • Shareable URLs. Every loaded journal updates the address bar to ?issn=<issn> (via history.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.

Changed

  • Random mailto identifier for OpenAlex polite-pool — generated once per browser (via crypto.getRandomValues) and stored in localStorage.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.

Removed

  • Email-prompt modal in index.html (auto-generation replaces it).

[0.3.0] — 2026-05-14

Added

  • PPS tab with four cards:
    1. Flagged share, year on year — stacked bar (flagged + unflagged segments) with secondary-axis line of % flagged.
    2. Detector composition, year on year — stacked bar of top 15 detectors + Other, fractional 1/N per flagged paper, Stacked % / Absolute toggle.
    3. Country share of flagged papers, year on year — stacked bar, top 15 + Other, fractional 1/N per flagged paper.
    4. Institution share of flagged papers, year on year — country dropdown → stacked bar of top 15 institutions + Other.
  • 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.

Changed

  • postStats(issn, …) now accepts and forwards paperMeta.
  • 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.toml CSP unchanged — same backend host already allowed.

Notes

  • Companion release with journal-trends-backend v0.3.0 (PPS dataset table, in-line PPS aggregation on POST, GET /api/pps/version, 80 MB POST cap).

[0.2.0] — 2026-05-13

Added

  • Backend-first lookup. On ISSN selection the frontend first hits the companion service (journal-trends-backend v0.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-targets http://localhost:5000 when running on localhost and https://ach4l.pythonanywhere.com in production. Override at runtime with ?backend=https://… for testing against another host.
  • Aggregation now carries schema_version: 1 (exported as SCHEMA_VERSION from src/aggregate/index.js). Backend rejects POSTs with mismatched versions; frontend treats GET responses of stale versions as cache misses.

Changed

  • 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.toml CSP connect-src now allows the PythonAnywhere backend.

Notes

  • Paired release with journal-trends-backend v0.2.0 (Flask + SQLite, deploys to PythonAnywhere free tier).
  • PPS-upload feature deferred to v0.3.0 per re-prioritisation.

[0.1.1] — 2026-05-11

Added

  • Institution tab: new trendline chart under the stacked bar, sharing the legend, hover/pin state, and the existing % / Absolute toggle.
  • Country tab: new "Top growth countries" card — a multi-line chart of the top 10 countries by growth, with Indexed (100 = early window) and Absolute modes.
  • Aggregation now produces country.growth = { earlyYears, lateYears, topGrowth: [{ code, growthPct, earlyAvg, lateAvg, byYear }] }.

Changed

  • Bumped TOP_INSTITUTIONS from 10 → 20 (per-country institution leaderboard).
  • Growth ranking uses Laplace-smoothed ratio (lateAvg + 0.5) / (earlyAvg + 0.5) − 1 so countries that grow from zero are rankable, with a min-total-papers filter (≥5 fractional) to suppress single-paper noise.

Fixed

  • Institution chart no longer carries a leftover singleton chart reference after splitting bar/line into two Chart.js instances.

0.1.0 — 2026-05-11

Added

  • 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 /dataviz skill 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-request select= projection, polite-pool mailto=, 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 by sourceId|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 /dataviz style with shared tooltip, legend pinning/dimming, and stacked-% / absolute mode toggle.
  • Email-prompt UI that stores user-provided mailto in localStorage.
  • Year-range modal that asks "fetch all years / last N years / custom" before scraping.

Notes

  • v0.1.0 is OpenAlex-only and single-source. Multi-source comparison and Crossref enrichment are planned for later milestones.