Skip to content

Commit 1ff357c

Browse files
committed
v1.16: Articles can also be filtered by author affiliation now
1 parent 6e3da81 commit 1ff357c

File tree

3 files changed

+42
-38
lines changed

3 files changed

+42
-38
lines changed

CHANGELOG

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
17.08.2023 v1.16
2+
+ Articles can also be filtered by author affiliation now (OpenAlex seems to provide most complete affiliations)
3+
14
14.05.2023 v1.15
2-
+ Retrieve "Incoming suggestions" also with OpenAlex (OA) now. Results in 1 extra API call per input article (disable by running "vm.getCitationsOA = false" in browser console).
5+
+ Retrieve "Outgoing suggestions" also with OpenAlex (OA) now. Results in 1 extra API call per input article (disable by running "vm.getCitationsOA = false" in browser console).
36
+ Added "↻" / "↺" link to rotate the citation network by 90° and stopped automatic rotation when fullscreen is toggled as it did not always fit shape of network.
47
+ Added option to color-group author nodes in co-authorship network by either their first or last article. Show which article they're color-grouped by in author-node-tooltip.
58
* Network settings are now saved with each network (i.e. tab) separately and stored locally when "Autosave results locally" is on or a JSON of a network is downloaded (Citation network settings: "Node color", "Node size", "Show source article node", "↻" / "↺"; Co-authorship network settings: "Node color", "Show first names", "only authors with a minimum [2-10] input & suggested articles are shown"); consistent with "Number shown" for "Incoming suggestions" and "Outgoing suggestions" since v1.1.
@@ -21,7 +24,7 @@
2124
+ It's now possible to download a network as a JSON file and reload it through the "File" button.
2225
+ Added white background to networks' canvas so they can now properly be saved by right-click → "Open/Save Image".
2326
+ Added option to also show first names in node labels for Co-authorship network.
24-
+ Added "Incoming suggestions" to OpenAlex experimentally (activate by running "vm.getCitationsOA = true" in browser console).
27+
+ Added "Outgoing suggestions" to OpenAlex experimentally (activate by running "vm.getCitationsOA = true" in browser console).
2528
* "Number shown" for "Incoming suggestions" and "Outgoing suggestions" is now saved with each network (i.e. tab) separately and stored locally when "Autosave results locally" is on or a JSON of network is downloaded.
2629
* Authors are now again distinguished by their names in the Co-authorship network and not by their hidden internal OpenAlex (OA) / Semantic Scholar (S2) ID due to seemingly many duplicates.
2730
! Fixed first/last name detection when name was formatted "last name, first name".

index.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ <h2 class="title is-4">Input articles ({{ currentGraph.input.length }})</h2>
268268
</template>
269269
</p>
270270
<p v-if="props.row.abstract" v-html="formatTags(props.row.abstract)" class="content"></p>
271-
<template v-if="currentGraph.source.referenceContexts && currentGraph.source.referenceContexts[props.row.id] && currentGraph.source.referenceContexts[props.row.id].length">
271+
<template v-if="currentGraph.source.referenceContexts?.[props.row.id]?.length">
272272
<h4 class="title is-6">Reference contexts in source article, {{ currentGraph.source.authors[0].LN + ' ' + currentGraph.source.year }} (experimental):</h4>
273273
<ul style="list-style-type: disc">
274274
<li v-for="context in currentGraph.source.referenceContexts[props.row.id]">{{ context }} -{{ currentGraph.source.authors[0].LN + ' ' + currentGraph.source.year }}</li>
@@ -295,7 +295,7 @@ <h4 class="title is-6">Reference contexts in source article, {{ currentGraph.sou
295295
</b-table-column>
296296
<b-table-column field="authors" label="First Author" v-slot="props">
297297
<b-tooltip dashed multilined :label="authorString(props.row.authors)" position="is-left" type="is-dark">
298-
{{ props.row.authors[0] && props.row.authors[0].LN }}
298+
{{ props.row.authors[0]?.LN }}
299299
</b-tooltip>
300300
</b-table-column>
301301
<b-table-column field="year" label="Year" meta="Hover year to see journal" sortable>
@@ -377,11 +377,11 @@ <h4 class="title is-6">Reference contexts in source article, {{ currentGraph.sou
377377
<b-tab-item value="incomingSuggestions" :disabled="currentGraph.incomingSuggestions === undefined" v-if="currentGraph.incomingSuggestions === undefined || currentGraph.incomingSuggestions.length">
378378
<template slot="header">
379379
<h2 class="title is-4">Incoming suggestions
380-
(<template v-if="currentGraph.incomingSuggestions && currentGraph.incomingSuggestions.length">▲ {{ maxIncomingSuggestions }}</template>
380+
(<template v-if="currentGraph.incomingSuggestions?.length">▲ {{ maxIncomingSuggestions }}</template>
381381
<a v-else class="button is-loading" style="display:inline; border: 0"></a>)
382382
</h2>
383383
</template>
384-
<div class="content" v-if="currentGraph.incomingSuggestions && currentGraph.incomingSuggestions.length">
384+
<div class="content" v-if="currentGraph.incomingSuggestions?.length">
385385
<p>
386386
<a @click="indexFAQ = 'incoming-suggestions'; showFAQ = true;" href="#incoming-suggestions">Incoming suggestions</a> are the most cited references by the input articles that are still missing, thus they have high in-degrees. They tend to be older than most input articles. Number shown:
387387
<select v-model="maxIncomingSuggestions" @change="changeCurrentNetworkSettings(true)">
@@ -446,7 +446,7 @@ <h2 class="title is-4">Incoming suggestions
446446
</b-table-column>
447447
<b-table-column field="authors" label="First Author" v-slot="props">
448448
<b-tooltip dashed multilined :label="authorString(props.row.authors)" position="is-left" type="is-dark">
449-
{{ props.row.authors[0] && props.row.authors[0].LN }}
449+
{{ props.row.authors[0]?.LN }}
450450
</b-tooltip>
451451
</b-table-column>
452452
<b-table-column field="year" label="Year" meta="Hover year to see journal" sortable>
@@ -528,10 +528,10 @@ <h2 class="title is-4">Incoming suggestions
528528
<b-tab-item value="outgoingSuggestions" :disabled="currentGraph.outgoingSuggestions === undefined" v-if="currentGraph.outgoingSuggestions === undefined || currentGraph.outgoingSuggestions.length">
529529
<template slot="header">
530530
<h2 class="title is-4">Outgoing suggestions
531-
(<template v-if="currentGraph.outgoingSuggestions && currentGraph.outgoingSuggestions.length">▼ {{ maxOutgoingSuggestions }}</template>
531+
(<template v-if="currentGraph.outgoingSuggestions?.length">▼ {{ maxOutgoingSuggestions }}</template>
532532
<a v-else class="button is-loading" style="display:inline; border: 0"></a>)
533533
</template>
534-
<div class="content" v-if="currentGraph.outgoingSuggestions && currentGraph.outgoingSuggestions.length">
534+
<div class="content" v-if="currentGraph.outgoingSuggestions?.length">
535535
<p>
536536
<a @click="indexFAQ = 'outgoing-suggestions'; showFAQ = true;" href="#outgoing-suggestions">Outgoing suggestions</a> are citing the most input articles, thus they have high out-degrees. They tend to be newer than most input articles. Number shown:
537537
<select v-model="maxOutgoingSuggestions" @change="changeCurrentNetworkSettings(true)">
@@ -596,7 +596,7 @@ <h2 class="title is-4">Outgoing suggestions
596596
</b-table-column>
597597
<b-table-column field="authors" label="First Author" v-slot="props">
598598
<b-tooltip dashed multilined :label="authorString(props.row.authors)" position="is-left" type="is-dark">
599-
{{ props.row.authors[0] && props.row.authors[0].LN }}
599+
{{ props.row.authors[0]?.LN }}
600600
</b-tooltip>
601601
</b-table-column>
602602
<b-table-column field="year" label="Year" meta="Hover year to see journal" sortable>

index.js

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
/* Local Citation Network v1.15 (GPL-3) */
1+
/* Local Citation Network v1.16 (GPL-3) */
22
/* by Tim Woelfle */
33
/* https://timwoelfle.github.io/Local-Citation-Network */
44

55
/* global fetch, localStorage, vis, Vue, Buefy */
66

77
'use strict'
88

9-
const localCitationNetworkVersion = 1.15
9+
const localCitationNetworkVersion = 1.16
1010

1111
const arrSum = arr => arr.reduce((a, b) => a + b, 0)
1212
const arrAvg = arr => arrSum(arr) / arr.length
@@ -56,7 +56,7 @@ function semanticScholarPaper (id, getCitations, getReferenceContexts) {
5656

5757
function semanticScholarResponseToArticleArray (data) {
5858
return data.filter(Boolean).map(article => {
59-
const doi = article.externalIds && article.externalIds.DOI && article.externalIds.DOI.toUpperCase()
59+
const doi = article.externalIds?.DOI?.toUpperCase()
6060

6161
return {
6262
id: article.paperId,
@@ -67,15 +67,15 @@ function semanticScholarResponseToArticleArray (data) {
6767
const cutPoint = (author.name.lastIndexOf(',') !== -1) ? author.name.lastIndexOf(',') : author.name.lastIndexOf(' ')
6868
return {
6969
id: author.authorId,
70-
orcid: author.externalIds && author.externalIds.ORCID,
70+
orcid: author.externalIds?.ORCID,
7171
url: author.url,
7272
LN: author.name.substr(cutPoint + 1),
7373
FN: author.name.substr(0, cutPoint),
7474
affil: (author.affiliations || []).join(', ') || undefined
7575
}
7676
}),
7777
year: article.year,
78-
journal: (article.journal && article.journal.name) || article.venue,
78+
journal: article.journal?.name || article.venue,
7979
references: (article.references) ? article.references.map(x => x.paperId) : [],
8080
citations: (article.citations) ? article.citations.map(x => x.paperId).filter(Boolean) : [],
8181
citationsCount: article.citationCount,
@@ -92,8 +92,10 @@ async function openAlexWrapper (ids, responseFunction, isLoadingProgress = false
9292
const responses = []
9393
ids = ids.map(id => {
9494
if (!id) return undefined
95+
// OpenAlex usually formats ids as URLs (e.g. https://openalex.org/W2741809807 / https://doi.org/10.7717/peerj.4375 / https://pubmed.ncbi.nlm.nih.gov/29456894)
96+
// Supported ids: https://docs.openalex.org/api-entities/works/work-object#id
9597
else if (id.includes('https://')) return id
96-
else if (id.toLowerCase().match(/openalex:|doi:|mag:|pmid:|pmcid:/)) return id.toLowerCase()
98+
else if (id.toLowerCase().match(/doi:|mag:|openalex:|pmid:|pmcid:/)) return id.toLowerCase()
9799
else if (id.includes('/')) return 'doi:' + id
98100
else return 'openalex:' + id
99101
})
@@ -144,18 +146,16 @@ function openAlexResponseToArticleArray (data) {
144146
const display_name = authorship.author.display_name || ''
145147
const cutPoint = (display_name.lastIndexOf(',') !== -1) ? display_name.lastIndexOf(',') : display_name.lastIndexOf(' ')
146148
return {
147-
id: authorship.author.id && authorship.author.id.replace('https://openalex.org/', ''),
148-
orcid: authorship.author.orcid && authorship.author.orcid.replace('https://orcid.org/', ''),
149+
id: authorship.author.id?.replace('https://openalex.org/', ''),
150+
orcid: authorship.author.orcid?.replace('https://orcid.org/', ''),
149151
LN: display_name.substr(cutPoint + 1),
150152
FN: display_name.substr(0, cutPoint),
151153
affil: (authorship.institutions || []).map(institution => institution.display_name + (institution.country_code ? ' (' + institution.country_code + ')' : '')).join(', ') || undefined
152154
}
153155
}),
154156
year: article.publication_year,
155-
journal: (article.primary_location && article.primary_location.source && (
156-
article.primary_location.source.display_name +
157-
((article.primary_location.source.host_organization_name && !article.primary_location.source.display_name.includes(article.primary_location.source.host_organization_name)) ? ' (' + article.primary_location.source.host_organization_name + ')' : '')
158-
)) ?? undefined,
157+
journal: article.primary_location?.source?.display_name +
158+
((article.primary_location.source.host_organization_name && !article.primary_location.source.display_name.includes(article.primary_location.source.host_organization_name)) ? ' (' + article.primary_location.source.host_organization_name + ')' : ''),
159159
references: (article.referenced_works || []).map(x => x.replace('https://openalex.org/', '')),
160160
citations: (article.citations) ? article.citations.results.map(x => x.id.replace('https://openalex.org/', '')) : [],
161161
citationsCount: article.cited_by_count,
@@ -208,20 +208,20 @@ function crossrefWorks (id) {
208208

209209
function crossrefResponseToArticleArray (data) {
210210
return data.filter(Boolean).map(article => {
211-
const doi = article.DOI && article.DOI.toUpperCase()
211+
const doi = article.DOI?.toUpperCase()
212212

213213
return {
214214
id: doi,
215215
numberInSourceReferences: data.indexOf(article) + 1,
216216
doi: doi,
217217
title: String(article.title), // most of the time title is an array with length=1, but I've also seen pure strings
218-
authors: (article.author && article.author.length) ? article.author.map(x => ({
218+
authors: (article.author?.length) ? article.author.map(x => ({
219219
orcid: x.ORCID,
220220
LN: x.family || x.name,
221221
FN: x.given,
222-
affil: (x.affiliation && x.affiliation.length) ? x.affiliation.map(aff => aff.name).join(', ') : (typeof (x.affiliation) === 'string' ? x.affiliation : undefined)
222+
affil: (x.affiliation?.length) ? x.affiliation.map(aff => aff.name).join(', ') : (typeof (x.affiliation) === 'string' ? x.affiliation : undefined)
223223
})) : [{ LN: article.author || undefined }],
224-
year: article.issued['date-parts'] && article.issued['date-parts'][0] && article.issued['date-parts'][0][0],
224+
year: article.issued['date-parts']?.[0]?.[0],
225225
journal: String(article['container-title']),
226226
// Crossref "references" array contains null positions for references it doesn't have DOIs for, thus preserving the original number of references
227227
references: (typeof article.reference === 'object') ? article.reference.map(x => (x.DOI) ? x.DOI.toUpperCase() : undefined) : [],
@@ -268,7 +268,7 @@ function openCitationsMetadata (id) {
268268

269269
function openCitationsResponseToArticleArray (data) {
270270
return data.filter(Boolean).map(article => {
271-
const doi = article.doi && article.doi.toUpperCase()
271+
const doi = article.doi?.toUpperCase()
272272

273273
return {
274274
id: doi,
@@ -341,7 +341,7 @@ function initCitationNetwork (app) {
341341
group: article[app.citationNetworkNodeColor],
342342
value: arrSum([['in', 'both'].includes(app.citationNetworkNodeSize) ? app.inDegree(article.id) : 0, ['out', 'both'].includes(app.citationNetworkNodeSize) ? app.outDegree(article.id) : 0]),
343343
shape: (app.currentGraph.source.id === article.id) ? 'diamond' : (app.inputArticlesIds.includes(article.id) ? 'dot' : (app.incomingSuggestionsIds.includes(article.id) ? 'triangle' : 'triangleDown')),
344-
label: (article.authors[0] && article.authors[0].LN) + '\n' + article.year
344+
label: article.authors[0]?.LN + '\n' + article.year
345345
}))
346346

347347
// Create network
@@ -518,8 +518,8 @@ function initAuthorNetwork (app, minPublications = undefined) {
518518
if (authorId1 === authorId2) return false
519519

520520
// Is there already a link for this pair? If so, make it stronger
521-
if (links[authorId1] && links[authorId1][authorId2]) return links[authorId1][authorId2]++
522-
if (links[authorId2] && links[authorId2][authorId1]) return links[authorId2][authorId1]++
521+
if (links[authorId1]?.[authorId2]) return links[authorId1][authorId2]++
522+
if (links[authorId2]?.[authorId1]) return links[authorId2][authorId1]++
523523

524524
// Create new link
525525
if (!links[authorId1]) links[authorId1] = {}
@@ -763,11 +763,11 @@ const vm = new Vue({
763763
},
764764
// The following are settings and their default values
765765
maxIncomingSuggestions: {
766-
get: function () { return this.currentGraph.maxIncomingSuggestions ?? Math.min(10, this.currentGraph.incomingSuggestions && this.currentGraph.incomingSuggestions.length) },
766+
get: function () { return this.currentGraph.maxIncomingSuggestions ?? Math.min(10, this.currentGraph.incomingSuggestions?.length) },
767767
set: function (x) { this.$set(this.currentGraph, 'maxIncomingSuggestions', x) }
768768
},
769769
maxOutgoingSuggestions: {
770-
get: function () { return this.currentGraph.maxOutgoingSuggestions ?? Math.min(10, this.currentGraph.outgoingSuggestions && this.currentGraph.outgoingSuggestions.length) },
770+
get: function () { return this.currentGraph.maxOutgoingSuggestions ?? Math.min(10, this.currentGraph.outgoingSuggestions?.length) },
771771
set: function (x) { this.$set(this.currentGraph, 'maxOutgoingSuggestions', x) }
772772
},
773773
citationNetworkNodeColor: {
@@ -1155,6 +1155,7 @@ const vm = new Vue({
11551155
}
11561156

11571157
network.selectNodes(selectedNodeIds)
1158+
// Only highlight connected nodes in citationNetwork
11581159
const connectedNodes = (!this.showAuthorNetwork) ? network.getConnectedNodes(selectedNodeIds) : []
11591160

11601161
// Code loosely adapted from: https://github.com/visjs/vis-network/blob/master/examples/network/exampleApplications/neighbourhoodHighlight.html
@@ -1201,11 +1202,11 @@ const vm = new Vue({
12011202
this.currentGraph.input.sort(this.sortInDegree)
12021203
this.selectedInputArticle = this.currentGraph.input[0]
12031204

1204-
if (this.currentGraph.incomingSuggestions && this.currentGraph.incomingSuggestions.length) {
1205+
if (this.currentGraph.incomingSuggestions?.length) {
12051206
this.currentGraph.incomingSuggestions.sort(this.sortInDegree)
12061207
this.selectedIncomingSuggestionsArticle = this.currentGraph.incomingSuggestions[0]
12071208
}
1208-
if (this.currentGraph.outgoingSuggestions && this.currentGraph.outgoingSuggestions.length) {
1209+
if (this.currentGraph.outgoingSuggestions?.length) {
12091210
this.currentGraph.outgoingSuggestions.sort(this.sortOutDegree)
12101211
this.selectedOutgoingSuggestionsArticle = this.currentGraph.outgoingSuggestions[0]
12111212
}
@@ -1343,9 +1344,9 @@ const vm = new Vue({
13431344
const re = new RegExp(this.filterString, 'gi')
13441345
switch (this.filterColumn) {
13451346
case 'titleAbstract':
1346-
return articles.filter(article => String(article.numberInSourceReferences).match(new RegExp(this.filterString, 'y')) || (article.title && article.title.match(re)) || (article.abstract && article.abstract.match(re)))
1347+
return articles.filter(article => String(article.numberInSourceReferences).match(new RegExp(this.filterString, 'y')) || (article.title?.match(re)) || (article.abstract?.match(re)))
13471348
case 'authors':
1348-
return articles.filter(article => this.authorString(article.authors).match(re))
1349+
return articles.filter(article => this.authorString(article.authors).match(re) || article.authors.map(author => author.affil?.match(re)).some(Boolean))
13491350
case 'year':
13501351
return articles.filter(article => String(article.year).match(re))
13511352
case 'journal':
@@ -1355,10 +1356,10 @@ const vm = new Vue({
13551356
}
13561357
},
13571358
authorString: function (authors) {
1358-
return (authors && authors.length) ? authors.map(x => ((x.FN) ? (x.FN) + ' ' : '') + x.LN).join(', ') : ''
1359+
return (authors?.length) ? authors.map(x => ((x.FN) ? (x.FN) + ' ' : '') + x.LN).join(', ') : ''
13591360
},
13601361
authorStringShort: function (authors) {
1361-
return (authors && authors.length > 5) ? this.authorString(authors.slice(0, 5).concat({ LN: '(' + (authors.length - 5) + ' more)' })) : this.authorString(authors)
1362+
return (authors?.length > 5) ? this.authorString(authors.slice(0, 5).concat({ LN: '(' + (authors.length - 5) + ' more)' })) : this.authorString(authors)
13621363
},
13631364
clickToggleAutosave: function () {
13641365
this.autosaveResults = !this.autosaveResults

0 commit comments

Comments
 (0)