Skip to content

Commit d258790

Browse files
authored
Merge pull request #507 from massimoaria/develop
Develop
2 parents 28b1f18 + 208da4c commit d258790

12 files changed

+186
-139
lines changed

DESCRIPTION

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: bibliometrix
22
Type: Package
33
Title: Comprehensive Science Mapping Analysis
4-
Version: 4.3.2
4+
Version: 4.3.3
55
Authors@R: c(
66
person(given = "Massimo",
77
family = "Aria",
@@ -53,13 +53,13 @@ Imports: stats,
5353
stringi,
5454
stringr,
5555
tidyr,
56-
tidytext
56+
tidytext,
57+
visNetwork
5758
Suggests:
5859
knitr,
5960
rmarkdown,
6061
testthat (>= 3.0.0),
6162
shinycssloaders,
62-
visNetwork,
6363
wordcloud2
6464
RoxygenNote: 7.3.2
6565
NeedsCompilation: no

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import(shiny)
6464
import(stats)
6565
import(stringi)
6666
import(tidytext)
67+
import(visNetwork)
6768
importFrom(DT,DTOutput)
6869
importFrom(DT,datatable)
6970
importFrom(DT,renderDT)

NEWS

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
bibliometrix v4.3.3 (Release date: 2025-03-17)
2+
3+
Features:
4+
* Improved the function plotThematicEvolution()
5+
* Solved issue with synonyms in conceptualStructure()
6+
17
bibliometrix v4.3.0 (Release date: 2024-06-30)
28

39
Features:

R/conceptualStructure.R

+8-22
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ conceptualStructure<-function(M,field="ID", ngrams=1, method="MCA", quali.supp=N
7777
# names(SUPP)=names(M)[quanti.supp]
7878
# row.names(SUPP)=tolower(row.names(M))
7979
# }
80-
binary=FALSE
81-
if (method=="MCA"){binary=TRUE}
80+
binary <- FALSE
81+
if (method=="MCA"){binary <- TRUE}
8282

8383
switch(field,
8484
ID={
@@ -153,41 +153,28 @@ conceptualStructure<-function(M,field="ID", ngrams=1, method="MCA", quali.supp=N
153153
)
154154

155155

156-
colnames(CW)=tolower(colnames(CW))
157-
rownames(CW)=tolower(rownames(CW))
156+
colnames(CW) <- label <- tolower(colnames(CW))
157+
rownames(CW) <- tolower(rownames(CW))
158158
p=dim(CW)[2]
159159
quali=NULL
160160
quanti=NULL
161-
# Perform Multiple Correspondence Analysis (MCA)
162-
# if (!is.null(quali.supp)){
163-
# ind=which(row.names(QSUPP) %in% row.names(CW))
164-
# QSUPP=as.data.frame(QSUPP[ind,])
165-
# CW=cbind(CW,QSUPP)
166-
# quali=(p+1):dim(CW)[2]
167-
# names(CW)[quali]=names(M)[quali.supp]
168-
# }
169-
# if (!is.null(quanti.supp)){
170-
# ind=which(row.names(SUPP) %in% row.names(CW))
171-
# SUPP=as.data.frame(SUPP[ind,])
172-
# CW=cbind(CW,SUPP)
173-
# quanti=(p+1+length(quali)):dim(CW)[2]
174-
# names(CW)[quanti]=names(M)[quanti.supp]
175-
# }
176161

177162
results <- factorial(CW,method=method,quanti=quanti,quali=quali)
178163
res.mca <- results$res.mca
179164
df <- results$df
165+
row.names(df) <- label
166+
# row.names(df) <- gsub("\\."," ",row.names(df))
180167
docCoord <- results$docCoord
181168
df_quali <- results$df_quali
182169
df_quanti <- results$df_quanti
183170

184171
### Total Citations of documents
185-
if ("TC" %in% names(M) & method!="MDS"){docCoord$TC=as.numeric(M[toupper(rownames(docCoord)),"TC"])}
172+
if ("TC" %in% names(M) & method!="MDS"){docCoord$TC <- as.numeric(M[toupper(rownames(docCoord)),"TC"])}
186173

187174

188175
# Selection of optimal number of clusters (gap statistics)
189176
#a=fviz_nbclust((df), kmeans, method = "gap_stat",k.max=k.max)['data']$data$y
190-
km.res=hclust(dist(df),method="average")
177+
km.res <- hclust(dist(df),method="average")
191178

192179
if (clust=="auto"){
193180
clust=min((length(km.res$height)-which.max(diff(km.res$height))+1),k.max)
@@ -430,7 +417,6 @@ conceptualStructure<-function(M,field="ID", ngrams=1, method="MCA", quali.supp=N
430417
factorial<-function(X,method,quanti=NULL,quali=NULL){
431418
df_quali=data.frame()
432419
df_quanti=data.frame()
433-
434420
switch(method,
435421
### CORRESPONDENCE ANALYSIS ###
436422
CA={

R/plotThematicEvolution.R

+98-86
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
utils::globalVariables(c("group"))
2-
#' Plot a Thematic Evolution Analysis
1+
utils::globalVariables(c("group", "CL1", "CL2", "value", "visEdges", "visIgraphLayout", "visNetwork", "visOptions",
2+
"visPhysics"))
3+
#' Plot Thematic Evolution Network
34
#'
4-
#' It plot a Thematic Evolution Analysis performed using the \code{\link{thematicEvolution}} function.
5+
#' Visualizes the thematic evolution of clusters over time using a temporal network layout.
6+
#' Nodes are positioned along the x-axis according to time slices and vertically to minimize overlap.
7+
#' Edges represent links between themes across time periods, with customizable weights and styles.
8+
#' Nodes and Edges objects are the results of a Thematic Evolution Analysis performed using the \code{\link{thematicEvolution}} function.
59
#'
610
#' @param Nodes is a list of nodes obtained by \code{\link{thematicEvolution}} function.
711
#' @param Edges is a list of edges obtained by \code{\link{thematicEvolution}} function.
8-
#' @param measure is a character. It can be \code{measure=("inclusion","stability", "weighted")}.
9-
#' @param min.flow is numerical. It indicates the minimum value of measure to plot a flow.
10-
#' @return a sankeyPlot
12+
#' @param min.flow Numeric. Minimum threshold for edge weight (i.e., flow) to be included in the network visualization. Default is `0`.
13+
#' @param measure Character. The method to define edge weight: `"inclusion"`, `"stability"`, or `"weighted"` (default is `"weighted"`).
14+
#' @param node_shape Character. Shape of the nodes in the network. Options are `"dot"` (default), `"box"`, `"circle"`, or `"ellipse"`.
15+
#' @param label_size Numeric. Font size of the node labels. Default is `15`.
16+
#' @param edge_scale Numeric. Scaling factor for edge width. Default is `10`.
17+
#' @param node_scale Numeric. Scaling factor for node size. Default is `30`.
18+
#'
19+
#' @return A `visNetwork` object displaying a time-structured thematic evolution network.
1120
#'
1221
#'
1322
#' @examples
@@ -27,91 +36,94 @@ utils::globalVariables(c("group"))
2736
#'
2837
#' @export
2938

30-
plotThematicEvolution <- function (Nodes, Edges, measure = "inclusion", min.flow = 0){
39+
plotThematicEvolution <- function(Nodes,
40+
Edges,
41+
min.flow = 0,
42+
measure = "weighted", #measure=("inclusion","stability", "weighted")
43+
node_shape = "box", # "box", "circle", "ellipse"
44+
label_size = 5,
45+
edge_scale = 10,
46+
node_scale = 30) {
3147

32-
48+
switch(measure,
49+
inclusion = {
50+
edge_weight_var = "Inclusion"
51+
},
52+
stability = {
53+
edge_weight_var = "Stability"
54+
},
55+
weighted = {
56+
edge_weight_var = "Inc_Weighted"
57+
}
58+
)
3359

34-
Kx <- length(unique(Nodes$group))
35-
Ky <- nrow(Nodes)
36-
Nodes <-Nodes %>%
37-
mutate(
38-
coordX=rep(seq(from= 0, to= 1, by= 1/(Kx-0.8)),as.numeric(table(group))),
39-
coordY= rep(0.1, Ky)
40-
)
60+
Edges$Stability <- Edges$Stability*10
4161

42-
43-
################
44-
switch(measure, inclusion = {
45-
Edges = Edges[-c(4, 5)]
46-
}, stability = {
47-
Edges = Edges[-c(3, 4)]
48-
}, weighted = {
49-
Edges = Edges[, -c(3, 5)]
50-
})
51-
names(Edges)[3] = "weight"
52-
Edges = Edges[Edges$weight >= min.flow, ]
53-
Edges$weight = Edges$weight * 100
62+
# 1. X coordinate by time
63+
unique_slices <- sort(unique(Nodes$slice))
64+
x_positions <- normalize_to_minus1_1(setNames((seq_along(unique_slices)-1), unique_slices))
65+
Nodes <- Nodes %>%
66+
mutate(x = x_positions[as.character(slice)])
5467

55-
Nodes$id <- (1:nrow(Nodes))-1
56-
57-
## identify and remove nodes with empty edges
58-
ind <- setdiff(Nodes$id,unique(c(Edges$from,Edges$to)))
59-
if(length(ind)>0) {
60-
Nodes <- Nodes[-(ind+1),]
61-
Nodes$idnew <- (1:nrow(Nodes))-1
62-
## replace old edge ids with new ones
63-
for (i in 1:nrow(Nodes)){
64-
old <- Nodes$id[i]
65-
new <- Nodes$idnew[i]
66-
Edges$from[Edges$from==old] <- new
67-
Edges$to[Edges$to==old] <- new
68-
}
69-
}
68+
# 2. Y coordinates to avoid overlap
69+
Nodes<- Nodes %>%
70+
group_by(slice) %>%
71+
arrange(name) %>%
72+
mutate(y = normalize_to_minus1_1(seq(from = 100, by = 100, length.out = n()))) %>%
73+
ungroup()
7074

71-
#Edges$color <- "lightgrey"
75+
# 4. Prepare nodes for visNetwork
76+
Nodes_vis <- Nodes %>%
77+
mutate(shape = node_shape,
78+
size = sum * node_scale,
79+
value = sum) %>%
80+
select(id, label, group, color, x, y, shape, size, value)
7281

73-
# plotly margins
74-
m <- list(
75-
l = 50,
76-
r = 50,
77-
b = 100,
78-
t = 100,
79-
pad = 4
80-
)
82+
# Add fonts as a list for each line (label below)
83+
#Nodes_vis$font <- map(seq_len(nrow(Nodes_vis)), ~ list(size = label_size, vadjust = -0.1))
84+
85+
if (node_shape %in% c("dot", "square")) {
86+
vadjust <- -2 * (Nodes_vis$size / (max(Nodes_vis$size) - min(Nodes_vis$size))) * label_size
87+
font_sizes <- rep(label_size, nrow(Nodes_vis))
88+
} else {
89+
# Scala il font in base al valore del nodo (sum) tra min e max specificati
90+
min_font <- 10
91+
max_font <- label_size*4
92+
font_sizes <- min_font + (Nodes_vis$value - min(Nodes_vis$value)) / (max(Nodes_vis$value) - min(Nodes_vis$value)) * (max_font - min_font)
93+
vadjust <- rep(0, nrow(Nodes_vis))
94+
}
95+
96+
Nodes_vis$font <- purrr::map2(font_sizes, vadjust, ~ list(size = .x, vadjust = .y))
8197

82-
plotly::plot_ly(
83-
type = "sankey",
84-
arrangement = "snap",
85-
node = list(
86-
label = Nodes$name,
87-
x = Nodes$coordX,
88-
y = Nodes$coordY,
89-
color = Nodes$color,
90-
pad = 4), # 10 Pixel
91-
link = list(
92-
source = Edges$from,
93-
target = Edges$to,
94-
value = Edges$weight
95-
#,color = Edges$color
96-
)
97-
) %>%
98-
layout(margin = m) %>%
99-
plotly::add_annotations(x = Nodes$coordX,
100-
y = 1.08,
101-
text = factor(Nodes$group) ,
102-
showarrow=F,xanchor = "center",
103-
font = list(color = 'Dark',
104-
family = "TimesNewRoman",
105-
size = 18)) %>%
106-
config(displaylogo = FALSE,
107-
modeBarButtonsToRemove = c(
108-
'toImage',
109-
'sendDataToCloud',
110-
'pan2d',
111-
'select2d',
112-
'lasso2d',
113-
'toggleSpikelines',
114-
'hoverClosestCartesian',
115-
'hoverCompareCartesian'
116-
))
98+
Nodes_vis$fixed.x <- TRUE
99+
100+
# 5. Prepare edges
101+
edges_vis <- Edges %>%
102+
mutate(width = !!sym(edge_weight_var) * edge_scale,
103+
value = !!sym(edge_weight_var)) %>%
104+
dplyr::filter(value>=min.flow)
105+
106+
edges_vis$color <- lapply(1:nrow(edges_vis), function(i) list(color = "#D3D3D3", highlight = "#35343370", hover = "#35343370"))
107+
108+
## layout
109+
coords = as.matrix(Nodes %>% select(x,y))
110+
111+
# 6. Build the network
112+
VIS <- visNetwork(Nodes_vis, edges_vis, type="full", smooth=TRUE) %>%
113+
#visNodes(scaling=list(min = 10, max = node_scale)) %>%
114+
visEdges(smooth = list(type="horizontal"), arrows = "to", scaling=list(min = 1, max = edge_scale)) %>%
115+
visIgraphLayout(layout = "layout.norm", layoutMatrix = coords, type = "full") %>%
116+
#visOptions(highlightNearest = TRUE, nodesIdSelection = FALSE) %>%
117+
visPhysics(enabled = FALSE) %>%
118+
visOptions(highlightNearest = list(enabled = TRUE, hover = TRUE, degree = 1,
119+
algorithm = "all", hideColor = "rgba(200, 200, 200, 0.90)"), nodesIdSelection = FALSE) %>%
120+
#visNetwork::visOptions(highlightNearest =list(enabled = T, hover = T, degree=max(as.numeric(unique_slices))), nodesIdSelection = T) %>%
121+
visNetwork::visInteraction(dragNodes = TRUE, navigationButtons = F, hideEdgesOnDrag = TRUE, zoomSpeed = 0.4) %>%
122+
visNetwork::visOptions(manipulation = FALSE, height ="100%", width = "100%")
123+
124+
return(VIS)
125+
}
126+
127+
normalize_to_minus1_1 <- function(v) {
128+
2 * (v - min(v)) / (max(v) - min(v)) - 1
117129
}

R/thematicEvolution.R

+11-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ thematicEvolution <- function(M, field = "ID", years, n = 250, minFreq = 2, size
6666
net[[k]] <- resk$net
6767
}
6868
#dev.off()
69-
par(mfrow = c(1, (K - 1)))
69+
#par(mfrow = c(1, (K - 1)))
7070
if (K < 2) {
7171
print("Error")
7272
return()
@@ -142,6 +142,16 @@ thematicEvolution <- function(M, field = "ID", years, n = 250, minFreq = 2, size
142142
Nodes<-rbind(Nodes,left_join(subset(nodes,nodes$slice==i), res[[i]]$clusters[c("color","name")], by="name"))
143143
}
144144
################
145+
# Preparing data for plot
146+
Nodes$id <- 0:(nrow(Nodes)-1)
147+
Nodes <- Nodes %>% left_join(
148+
rbind(INC[, -c(1, 2)] %>% select(CL1,sum) %>% rename(label=CL1),
149+
INC[, -c(1, 2)] %>% select(CL2,sum)%>% rename(label=CL2)) %>%
150+
group_by(label) %>% reframe(sum=max(sum)), by="label"
151+
)
152+
Nodes <- Nodes %>% group_by(slice) %>%
153+
mutate(sum=sum/sum(sum,na.rm=T)) %>% ungroup()
154+
###############
145155

146156
params <- list(field = field,
147157
years = years,

R/thematicMap.R

+2-4
Original file line numberDiff line numberDiff line change
@@ -64,20 +64,18 @@ thematicMap <- function(M, field="ID", n=250, minfreq=5, ngrams=1, stemming=FALS
6464
switch(field,
6565
ID={
6666
NetMatrix <- biblioNetwork(M, analysis = "co-occurrences", network = "keywords", n = n, sep = ";", remove.terms=remove.terms, synonyms = synonyms)
67-
TERMS=tolower(M$ID)
67+
TERMS <- tolower(M$ID)
6868
},
6969
DE={
7070
NetMatrix <- biblioNetwork(M, analysis = "co-occurrences", network = "author_keywords", n = n, sep = ";", remove.terms=remove.terms, synonyms = synonyms)
71-
TERMS=tolower(M$DE)
71+
TERMS <- tolower(M$DE)
7272
},
7373
TI={
74-
#if(!("TI_TM" %in% names(values$M))){values$M=termExtraction(values$M,Field="TI",verbose=FALSE, stemming = input$stemming)}
7574
M=termExtraction(M,Field="TI", ngrams=ngrams, verbose=FALSE, stemming = stemming, remove.terms=remove.terms, synonyms = synonyms)
7675
NetMatrix <- biblioNetwork(M, analysis = "co-occurrences", network = "titles", n = n, sep = ";")
7776

7877
},
7978
AB={
80-
#if(!("AB_TM" %in% names(values$M))){values$M=termExtraction(values$M,Field="AB",verbose=FALSE, stemming = input$stemming)}
8179
M=termExtraction(M,Field="AB", ngrams=ngrams, verbose=FALSE, stemming = stemming, remove.terms=remove.terms, synonyms = synonyms)
8280
NetMatrix <- biblioNetwork(M, analysis = "co-occurrences", network = "abstracts", n = n, sep = ";")
8381

R/zzz.R

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#' @import tidytext
1818
#' @import openalexR
1919
#' @import ca
20+
#' @import visNetwork
2021
#' @importFrom purrr map2_dfr
2122
#' @importFrom purrr map_dfr
2223
#' @importFrom purrr map_df

0 commit comments

Comments
 (0)