You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Biblioshiny — OpenAlex search: exact match (stemming off): the OpenAlex query builder gained an Exact match checkbox. By default OpenAlex applies stemming to search filters, so bone also retrieves bones and surgery also retrieves surgeries, which can inflate the result count well beyond what the user intended. When the box is checked, Biblioshiny queries the .exact variant of the search filter (title.search.exact, abstract.search.exact, title_and_abstract.search.exact, default.search.exact), the API counterpart of the Enable stemming switch on the OpenAlex website. Quoted phrases, parentheses and the AND/OR/NOT operators work in both modes, and Author/Concept rows are unaffected since they resolve to entity IDs.
Biblio AI — Gemini 3.5 Flash Lite: added gemini-3.5-flash-lite to the model list. It is Google's recommended replacement for Gemini 2.5 Flash — higher quality at the same price and a faster throughput. gemini_ai() sends it with thinkingConfig$thinkingLevel = "minimal", which is what keeps 2.5-Flash-class latency, and caps its output at the model's 65536-token ceiling. Verified against both a free-tier and a paid API key, so it is listed under Free tier (free-tier models also work with paid keys).
OpenAlex CSV import — new web-export format: convert2df(dbsource = "openalex") now reads the new OpenAlex Export to CSV layout (human-readable headers such as Work ID, Title, Author, Source, Author IDs, Institution) in addition to the legacy dotted-path column names (id, authorships.author.display_name, ...). The paper identifier was renamed by OpenAlex from id to Work ID.
OpenAlex CSV — field validation & fault tolerance: csvOA2df() now validates the available metadata. It stops with a clear, actionable message when a required field is missing (Work ID, Title, Author, Source, Year) and warns about missing recommended fields, while the import still proceeds. In Biblioshiny these are surfaced as a dialog / notification, so an incomplete OpenAlex CSV no longer crashes the app.
completeMetadata() — OpenAlex enrichment by Work ID: records are now looked up on OpenAlex by their Work ID (id_oa) when the DOI is absent (typical of minimal OpenAlex CSV exports), instead of being skipped. The OpenAlex pass is no longer disabled for OpenAlex-sourced collections, so fields omitted from a partial CSV export (abstract, affiliations, references, ...) can be downloaded from the OpenAlex API.
Bug Fixes and Improvements
Biblioshiny — the authors' references updated: the Team's Publications page gained the 2026 Journal of Informetrics paper on Biblioshiny and the SAAS workflow, the 2026 Scientometrics paper describing the multi-phase reference matching algorithm, and three science-mapping studies that were missing (Angelelli et al. 2025 on PLS-SEM, Aria et al. 2024 on gender dysphoria research, Ciavolino et al. 2022 on PLS-SEM). The two new methodological papers were also added to the help pages they document: the reference matching paper heads the References of the Reference Matching page and appears under Historiograph and Co-citation Network, whose results depend on that matching, and the SAAS paper is cited on the SAAS Workflow page next to the 2017 one.
Biblio AI no longer refuses to run on a working connection (#570): the check that guards an analysis called is_online(), a name that no longer exists in the app — utils.R renamed the helper is_Online() when #558 was fixed, and this one call site was not updated. Nothing failed visibly, because httr2 (which Biblioshiny loads) exports a function with the old name, so R silently resolved the call to curl::has_internet(). That probes connectivity in general and returns FALSE behind a proxy or with filtered DNS even when the service is perfectly reachable, and Biblio AI then refused to start with "Biblio AI requires an active internet connection to work" on a working network. The call now uses the app's own helper, and it probes the Gemini endpoint itself rather than the network at large: what matters at that point is whether the service about to be called can be reached. The message names the host that could not be reached and mentions proxies and firewalls.
check_online() treats an HTTP error status as reachable: the probe opened the URL with url() and took any failure as offline, so a host answering 401, 403 or 404 was reported as unreachable — the root of the Gemini endpoint answers 404, which would have blocked Biblio AI for everyone. The question being asked is whether the host responds, not whether the request succeeds, so the probe now performs a HEAD request through httr2 and only a transport failure (DNS, refused connection, timeout) counts as offline. The CRAN download counter and the other callers are unaffected.
Biblioshiny now starts in non-UTF-8 locales (#589): the app failed to launch on Windows systems set to a Chinese, Japanese, Korean or Cyrillic locale, with "Error sourcing ... ui.R" and "unexpected INCOMPLETE_STRING". Shiny reads the app sources with sourceUTF8(), but in an MBCS locale R ignores encoding = "UTF-8" and decodes the files with the system codepage, so every non-ASCII character was corrupted — an em dash became a two-character sequence — and when the corruption fell inside a string literal the file no longer parsed. RStudio hid the problem because it forces the session to UTF-8; running the same code from VS Code or a plain R console did not. All 643 non-ASCII characters in the Biblioshiny sources are now written as \u escapes, or as numeric character references in the help panels, whose HTML is rendered through HTML() anyway. The sources are pure ASCII and parse identically in any locale. The change is confined to how the characters are spelled in the source: all 13,457 character constants of the app were compared before and after, and every one of them has the same value. A test now keeps the sources ASCII.
Biblioshiny — the startup check now names the packages that are missing (#624): when a dependency could not be loaded, Biblioshiny stopped with "some packages are missing, please check your internet connection and try again". The check knew exactly which packages had failed and threw the information away, so users were sent looking for a connectivity problem they did not have, and had no way to tell which of the 36 dependencies was the one at fault. The message now lists the packages that could not be loaded and gives the install.packages() call that installs exactly those, with the version requirements stripped so the line can be pasted as it is. install.packages() signals most of its failures as a warning and returns normally, so a package that could not be installed used to fall through silently; the reason is now captured and reported. The two call sites no longer wrap the check in suppressMessages(), which was hiding these diagnostics along with the notice about a dependency being older than required — package startup chatter is silenced inside the check itself, so nothing noisy is added.
Biblioshiny — the collaboration world map no longer crashes when no country pair reaches the edge threshold (#586): the map filtered the country pairs on Min Edges and then assigned the arc widths, which on an empty result meant writing a single value into a zero-row data frame — "replacement has 1 row, data has 0", and a greyed-out session. Since the default Min Edges is 2, any collection whose countries never collaborated more than once hit it: a small collection (the 4 PubMed records of the issue), a merged one, or simply a field of research with little international co-authorship. Reproduced on the package's own 8-record WoS fixture. The choropleth is still meaningful without the arcs, so the map is now drawn with the countries coloured and no collaboration lines.
metaTagExtraction(Field = "AU1_CO") no longer fails on a collection without affiliations (#586): a Lens.org export never carries the addresses, so neither C1 nor RP exists. Recycling the missing field through paste() collapsed it to a single element, so RP[i] was NA from the second document on, gregexpr() returned NA and the sum() guarding the country match fed NA to if() — "missing value where TRUE/FALSE needed". Both fields are now normalised to full-length vectors and the country matches are guarded against NA, so a collection with no address simply gets AU1_CO = NA instead of aborting. This is what made the Most Relevant Countries panel fail on Lens collections.
Biblioshiny — country analyses say when the collection has no affiliation (#586): a Lens.org export contains no address at all, and a PubMed or OpenAlex export may omit them depending on the fields selected when the file was downloaded. No country can be derived from such a collection, and the four country panels — Corresponding Author's Countries, Country Scientific Production, the country collaboration network and the collaboration world map — used to fail somewhere deeper with a technical message, or with an error object the browser rendered as [object Object]. They now check the affiliations first and state plainly that country-level analyses are not available for that collection.
mergeDbSources() no longer leaves stale country fields behind (#586): AU_CO and AU1_CO are derived from the addresses, and after a merge they described only the sources where they had already been extracted — the documents coming from the other collections kept an empty value, which every country measure reads as no country. Merging a collection carrying AU_CO with one that does not therefore under-counted international collaboration: on the package fixtures a Brazilian paper co-authored abroad was reported as single-country. Both fields are now dropped at merge time and recomputed from C1/RP by the analyses that need them, which is the state a freshly converted collection is in anyway.
mergeDbSources() no longer truncates the author initials (#590): when the collections came from more than one database, the merge rebuilt every author name as the surname plus the first letter of a single token. SELVANATHAN EA became SELVANATHAN E and LAW CCH became LAW C, so authors who differ only after the first initial were merged into one and the merged collection collapsed homonyms more aggressively than either of its inputs — measured on the two collections attached to the issue, all 488 author names carrying two or more initials lost them. A collection imported with Author Name Format = Fullname had its full names pushed back to initials by the same code, undoing at merge time the choice made at import and forcing users to restore the AU column by hand afterwards. Every converter already writes author names in the WoS form, so the only thing that genuinely differs between databases is the punctuation — Scopus writes the full names as REID, MONIQUE, WoS as REID MONIQUE — and that is now all the merge harmonises. No author name is rewritten any more: every name in the merged collection is one that was already in one of the sources. Note that the collections to be merged should be imported with the same Author Name Format, so that their author fields are comparable.
BibTeX import — authors are no longer split where the exporter wrapped the line (#590): WoS writes its BibTeX fields wrapped at about 72 characters, and bib2df() joined the continuation lines with ;, the separator between authors. In a BibTeX file the author separator is always and, never the end of a line, so whenever the wrap fell inside a name the author was split in two: Selvanathan,\n Saroja became the two authors SELVANATHAN and SAROJA, one a bare surname and the other a bare given name. The affected records carried phantom authors that no analysis could match, while the real author lost part of his production and, in the author networks, part of his co-authorships. Measured on the 208-record collection attached to the issue, 110 of its Author fields are wrapped and 113 of its 932 author slots (12%) were phantom; after the fix 2 remain, both genuine single-name authors. AU is now unwrapped before being split on and, so BibTeX files whose author lists fit on one line — Scopus exports among them — are left byte for byte unchanged.
Biblioshiny — the journal ranking filter matches on the ISSN, and reports what it recognised (#643): the uploaded ranking list was compared against the collection on the journal name alone, exact after trimming and upper-casing. Any difference in punctuation, in an abbreviation or between & and and dropped the journal, and since unmatched sources are labelled Not Ranked the loss was indistinguishable from a journal the list genuinely does not rank. The file may now carry one or more ISSN columns — every column whose header mentions ISSN (ISSN, eISSN, ISSN-L, Print ISSN, ...) is used as a matching key, wherever it sits in the file — and journals are matched on the identifier first, falling back to the name. ISSN-only matching was not an option: WoS exports carry SN/EI, the Scopus and OpenAlex CSV paths carry ISSN, and some exports carry no ISSN at all, so the name has to stay as a fallback. After the upload a notification reports how many sources and documents were recognised and on which key. A plain two-column file behaves exactly as before, and a file missing a name or ranking column is now refused with a message instead of aborting the observer and greying out the session.
WoS import — cited references written as "Surname, Initials" are now parsed (#640): some Web of Science plaintext exports write the cited author as Aaker, JL, 1997, J MARKETING RES, ..., where the classic format has Aria M, 2017, J INFORMETR, .... The extra comma shifts every field of the reference by one, so the positional parsers read the initials where they expect the year and the year where they expect the source. Nothing then matched: histNetwork() reported "Matrix is empty!!", localCitations() returned zeros for every paper and author, and CR_AU lost the initials, silently merging different authors who share a surname. Measured on the 549-record collection attached to the issue, only 3.4% of its 43,104 references parsed a real year; after the fix 98.5% do, local citations go from 0 to 3,486 across 345 documents, and the historiograph is a 549x549 network with 3,447 edges instead of a crash. isi2df() now rejoins the two name fields, but only where the year sits exactly where the shift would put it, so references already in the classic format are left byte for byte unchanged.
WoS import — the repeated DOI tag no longer swallows the DOI (#640): WoS writes some references as ... DOI DOI 10.1234/x, and the multi-DOI form DOI [DOI 10.1234/X, 10.1234/x] leaves the same duplication behind once convert2df() strips the brackets. Splitting the reference on "DOI" then yields an empty string, so those references lost their DOI entirely and could never be matched. The duplicate tag is now removed at import. This affects classic-format collections too: 10 of the 254 references in the package's own WoS fixture carried it.
histNetwork() no longer crashes on a collection with no local citation (#640): when no reference matches any document of the collection, the LCR column stays empty and cocMatrix() returns NA after printing "Matrix is empty!!". The network was then built from that scalar and the function died on "Error in matrix(0, nrow(WLCR), length(missingLABEL)) : non-numeric matrix extent" — the same message produced by the single-term bug below, which is why the two had to be fixed together. The empty case is now returned with the shape the populated one has, a square all-zero LABEL x LABELNetMatrix, so histPlot() and Biblioshiny get a valid empty historiograph instead of an aborted session; with verbose = TRUE the reason is stated explicitly.
cocMatrix() no longer collapses to a vector on a single-term field (#640): the two final subsettings — WF[, !is.na(uniqueField)] and the removal of the "NA" column — used [ without drop = FALSE, so a collection whose field resolves to exactly one distinct term got back a plain vector instead of a document-by-term matrix. Every consumer then broke on nrow()/colnames(): in histNetwork() a collection with a single locally cited reference aborted with "Error in matrix(0, nrow(WLCR), length(missingLABEL)) : non-numeric matrix extent", and biblioNetwork(), conceptualStructure(), couplingMap(), threeFieldsPlot(), fieldByYear(), rpys(), sourceGrowth() and keywordAssoc() all share the same code path. Both subsettings now keep their two dimensions, for type = "matrix" and type = "sparse" alike.
Biblioshiny — the filtered collection can be exported from the Filtered Data window (#625): the Data window on the Filters page listed the filtered records but offered no way to save them, so the export described in the Info page looked as if it did not exist. It did — under Data → Import or Load → Export Collection, which exports the collection in its current, filtered state — but nowhere near where a user would look for it. The window now carries its own format selector (Excel / R Data) and a Save Filtered Data button. The over-32767-character warning on the CR column applies here too, and both exports share one helper with the rest of the app.
mergeDbSources() now says that it empties CR, and documents why (#639): merging collections from different databases renames CR to CR_raw and blanks CR for every document. That is deliberate — each database writes its references in its own format, so the same cited work appears under different strings and a shared CR column would count it more than once — but nothing said so: the roxygen page did not mention it and verbose = TRUE printed only the duplicate count, so users reasonably concluded that the merge had corrupted their data. mergeDbSources() now reports the blanking, names CR_raw as the place the original strings are kept, and lists the analyses that become unavailable (co-citation, bibliographic coupling, historiograph, local citations); the same explanation was added to the function's @details and @return. Behaviour is unchanged, and a merge of collections from a single database still leaves CR untouched and prints nothing.
Biblioshiny — Life Cycle: Add to report can be clicked more than once (#631): the report handler reshaped residuals and base_yearin place, writing the tabular version back into the shared values$DLC. The conversion is not idempotent, so the second click on the same results aborted the observer with "Column name Year must not be duplicated" (and base_year failed the same way, its "." column being gone), leaving the session greyed out and the collection unusable without a restart. The reshaping now happens on a local copy; values$DLC is left as computed, which is what the plots, the summary panel and the download handlers read.
Biblioshiny — OpenAlex search: Boolean operators between rows on different fields: in the query builder, a row's operator was applied only when the row searched the same field as the row above it; when it opened a new field the operator was silently dropped. A NOT row therefore became an inclusion — a NOT rat row on Title, added below a Title and Abstract row, searched forrat instead of excluding it and returned a plausible but wrong result count. Cross-field NOT is now translated into the form the API accepts: title.search: NOT rat for the search fields, authorships.author.id: !A123 for Author/Concept rows. Cross-field OR has no API counterpart — OpenAlex only ANDs the filters of a request together — so it is still combined with AND, but the app now says so in a notification and suggests putting both terms in a single row instead (e.g. one All Fields row with bone OR cartilage). The same notification covers a NOT between two Author or Concept rows, which resolve into a single OR-ed list of IDs and so cannot exclude one term.
Biblioshiny — the Gemini API key check no longer depends on a single model: setGeminiAPI() validated the key by sending a generateContent request to a hard-coded gemini-2.5-flash. When Google stopped serving the 2.5 family to newly created API keys, that probe started returning "HTTP 404: This model models/gemini-2.5-flash is no longer available to new users", so every new user was told their key had been refused — whatever model they had selected in Settings, and on a paid key too. The key is now validated against the model catalogue (ListModels), which is model-agnostic and cannot be broken by a model retirement; a transport failure is reported as a connection error instead of blaming the key. If the model selected in Settings is not in the catalogue of that specific key, the success message says so, instead of letting the user discover it as an HTTP 404 at the first analysis.
Biblioshiny — default Gemini model updated: fresh installations defaulted to gemini-2.5-flash-lite, which recently created API keys cannot use. The default (in the model selector, in loadGeminiModel() and in the Content Analysis fallback) is now gemini-3.5-flash-lite. The two Gemini 2.5 entries remain selectable, for keys that still have access to them, under a Legacy group that states the limitation.
Biblioshiny — plot export no longer requires pandoc: saving a Three-Field Plot (and any other network/plotly export) aborted with "Saving a widget with selfcontained = TRUE requires pandoc" on installations without pandoc, since htmlwidgets::saveWidget()/visSave() route self-contained output through rmarkdown::pandoc_self_contained_html(). These pages are temporary scratch files, screenshotted locally and deleted immediately, so they are now written with selfcontained = FALSE: the export works with no pandoc installed and the resulting PNG is unchanged. Affected: plotlySankey2png(), plot2png(type = "vis") and plot2pngGemini(), which also cleans up after itself now.
Biblioshiny — the Gemini API key status was never displayed: the Settings panel declares uiOutput("apiStatus"), but the server assigned output$status <- renderText(...) from inside renderUI() — an output id that does not exist in the UI — and returned no UI at all. Every message was silently discarded, so a user pasting an invalid key saw nothing happen. The status is now a reactive value rendered by a single output, colour-coded for validating/success/error.
Biblioshiny — actionable message for "AQ." Gemini keys: Google AI Studio has started issuing some accounts OAuth-style tokens prefixed with AQ. instead of classic AIzaSy... API keys. They are rejected by the generativelanguage.googleapis.com REST endpoint Biblio AI calls (HTTP 401, "Expected OAuth 2 access token"), so they can never work. setGeminiAPI() now reports the real error returned by Google and, for AQ. keys, explains how to obtain a usable key. Empty/short keys are also rejected before spending a network round trip, and the validated key is trimmed before being saved, so a key pasted with a trailing newline no longer breaks the next launch.
Package functions now work without library(bibliometrix) (#629): internal calls to data() did not specify package = "bibliometrix", so the package datasets (bibtag, countries, stopwords, logo) were only found when the package was attached. Calling e.g. bibliometrix::convert2df(..., dbsource = "generic", format = "bibtex") in a fresh session raised "data set 'bibtag' not found" followed by "undefined columns selected". All 16 affected call sites are now package-qualified, so bibliometrix:: usage works without attaching the package. Affected functions: bib2df(), dimensions2df(), csvOA2df(), metaTagExtraction(), biblioAnalysis(), collabByRegionPlot(), termExtraction(), bradford(), rpys(), histPlot(), conceptualStructure(), fieldByYear(), couplingMap(), thematicMap(), authorProdOverTime().
metaTagExtraction() — empty RP/C1 strings treated as missing: a Correspondence Address column present but empty in a Scopus CSV yielded RP == "" (not NA), which wiped out all affiliations in AU_UN() (crashing biblioAnalysis() with "invalid argument to unary operator") and silently overwrote valid C1 data in AU1_CO() (losing the corresponding-author country). Empty or whitespace-only RP/C1 values are now normalized to NA on entry, restoring the intended fallback on C1.
OpenAlex CSV import no longer crashes on sparse collections: missing optional columns are now created as NA_character_ (a logical NA broke strsplit() in biblioAnalysis()), and the DT (document type) column is always present (defaults to ARTICLE, since the web CSV export does not include the work type).
Biblioshiny — Missing Data audit: wcTable() now treats an all-NA Science Categories (WC) column as missing, fixing a "missing value where TRUE/FALSE needed" crash on import of OpenAlex collections without subject categories.
Biblioshiny — Metadata completion modal: eligibility now counts records with a DOI or an OpenAlex Work ID, and OpenAlex enrichment is offered for OpenAlex collections (previously deselected by default). Labels and help text updated accordingly.
Biblioshiny — Import Info & References: added a dedicated OpenAlex section documenting the minimum and recommended metadata to select when exporting, and the need to enrich the collection via the OpenAlex API to obtain cited references.
missingData() no longer fails on collections without a TC column: the collection-level rule that flags an all-zero citation column as fully missing assigned missing_counts["TC"] unconditionally. Since sum(as.numeric(NULL)) is 0, the branch fired even when the data frame had no TC column at all, appending a spurious element to the counts vector and aborting with "arguments imply differing number of rows". The assignment is now guarded on the column being present. completeMetadata(), which calls missingData() on entry, was affected by the same crash.
Technical Improvements
Test suite realigned with the OpenAlex Work ID refactoring: the completeMetadata() unit tests still built the old by_doi enrichment payload (now by_key, since lookups may be keyed by DOI or Work ID) and stubbed .enrich_from_openalex() without its new key_type argument, so R CMD check failed on all three platforms. The OpenAlex tests now cover the current contract — lookup by id_oa with key_type = "id" — and stub the client so they run offline instead of issuing a real API request.
CI: actions/checkout bumped from v4 to v5; v4 targets the Node.js 20 runtime, deprecated on GitHub Actions runners.