diff --git a/client/src/api/networks/network/communities.ts b/client/src/api/networks/network/communities.ts index 69601e99..2cc6f4bb 100644 --- a/client/src/api/networks/network/communities.ts +++ b/client/src/api/networks/network/communities.ts @@ -8,6 +8,11 @@ import { openAiLabeledClusters } from "./mistralai" import { COLORS } from "../_utils/constants" import { GetColorName } from "hex-color-to-color-name" import { configGetItemUrl } from "./config" +import { CONFIG } from "./config" +import { nodeGetId } from "./network" + +const CURRENT_YEAR = new Date().getFullYear() +const RECENT_YEARS = [CURRENT_YEAR - 1, CURRENT_YEAR] const communityGetAttribute = (graph: Graph, community: number, name: string): Array | Array => graph.reduceNodes( @@ -51,6 +56,18 @@ const communityGetCitationsByYear = (aggs: ElasticAggregations): Record key.startsWith("citationsIn")) .reduce((acc, [key, value]) => ({ ...acc, [key.slice(-4)]: value?.value }), {}) +const communityGetCitationsCount = (aggs: ElasticAggregations): number => + Object.values(communityGetCitationsByYear(aggs)).reduce((acc, value) => acc + value, 0) + +const communityGetCitationsRecent = (aggs: ElasticAggregations): number => + Object.entries(communityGetCitationsByYear(aggs)).reduce( + (acc, [key, value]) => (RECENT_YEARS.includes(Number(key)) ? acc + value : acc), + 0 + ) + +const communityGetCitationsScore = (aggs: ElasticAggregations): number => + communityGetCitationsRecent(aggs) / communityGetPublicationsCount(aggs) + const communityGetDomains = (aggs: ElasticAggregations): Record => aggs?.domains?.buckets.reduce((acc, bucket) => ({ ...acc, [labelClean(bucket.key)]: bucket.doc_count }), {}) @@ -66,6 +83,27 @@ const communityGetPublications = (hits: ElasticHits): Array + hits.reduce((acc, hit) => { + const field = CONFIG[model].field.split(".")[0] + const citationsByYear = hit?.["counts_by_year"]?.reduce( + (acc, citations) => ({ + ...acc, + [citations.year]: citations.cited_by_count, + }), + {} + ) + hit?.[field].forEach((node) => { + const id = nodeGetId(node[CONFIG[model].field.split(".")[1]]) + acc[id] = { + ...acc?.[id], + publicationsCount: acc?.[id]?.publicationsCount ? acc[id].publicationsCount + 1 : 1, + citationsByYear: citationsByYear, + } + }) + return acc + }, {}) + export default async function communitiesCreate(graph: Graph, computeClusters: boolean): Promise { const query: string = graph.getAttribute("query") const model: string = graph.getAttribute("model") @@ -88,6 +126,26 @@ export default async function communitiesCreate(graph: Graph, computeClusters: b const hits = await networkSearchHits({ model, query, filters, links: communityGetLinks(graph, index) }) const aggs = await networkSearchAggs({ model, query, filters, links: communityGetLinks(graph, index) }) + if (hits) { + const nodesInfos = communityGetNodesInfos(hits, model) + graph.forEachNode((key) => { + if (!Object.keys(nodesInfos).includes(key)) return + const nodeInfos = nodesInfos[key] + const publicationsCount = nodeInfos.publicationsCount + const citationsCount = nodeInfos?.citationsByYear + ? Object.values(nodeInfos?.citationsByYear).reduce((acc: number, value: number) => acc + value, 0) + : 0 + const citationsRecent = nodeInfos?.citationsByYear + ? (nodeInfos.citationsByYear?.[CURRENT_YEAR - 1] || 0) + (nodeInfos.citationsByYear?.[CURRENT_YEAR] || 0) + : 0 + const citationsScore = citationsRecent / (publicationsCount || 1) + graph.setNodeAttribute(key, "publicationsCount", publicationsCount) + graph.setNodeAttribute(key, "citationsCount", citationsCount) + graph.setNodeAttribute(key, "citationsRecent", citationsRecent) + graph.setNodeAttribute(key, "citationsScore", citationsScore) + }) + } + const community = { cluster: index + 1, label: COLORS?.[index] ? GetColorName(COLORS[index]) : `Unnamed ${index + 1}`, @@ -96,9 +154,12 @@ export default async function communitiesCreate(graph: Graph, computeClusters: b nodes: communityGetNodes(graph, index), maxYear: communityGetMaxYear(graph, index), ...(aggs && { - publicationsCount: communityGetPublicationsCount(aggs), publicationsByYear: communityGetPublicationsByYear(aggs), + publicationsCount: communityGetPublicationsCount(aggs), citationsByYear: communityGetCitationsByYear(aggs), + citationsCount: communityGetCitationsCount(aggs), + citationsRecent: communityGetCitationsRecent(aggs), + citationsScore: communityGetCitationsScore(aggs), domains: communityGetDomains(aggs), oaPercent: communityGetOaPercent(aggs), }), diff --git a/client/src/api/networks/network/config.ts b/client/src/api/networks/network/config.ts index 7d82f313..934e1cbb 100644 --- a/client/src/api/networks/network/config.ts +++ b/client/src/api/networks/network/config.ts @@ -1,8 +1,11 @@ import { NetworkConfig } from "../../../types/network" import { COLORS } from "../_utils/constants" -const CONFIG = { +export const CONFIG = { authors: { + field: "authors.id_name", + aggregation: "authors.id_name.keyword", + co_aggregation: "co_authors.keyword", url: (key: string) => `/authors/${key}`, terminology: { item: "author", @@ -16,6 +19,9 @@ const CONFIG = { }, }, institutions: { + field: "affiliations.id_name", + aggregation: "affiliations.id_name.keyword", + co_aggregation: "co_institutions.keyword", url: (key: string) => `/organizations/${key}`, terminology: { item: "institution", @@ -29,6 +35,9 @@ const CONFIG = { }, }, structures: { + field: "affiliations.id_name", + aggregation: "affiliations.id_name.keyword", + co_aggregation: "co_structures.keyword", url: (key: string) => `/organizations/${key}`, terminology: { item: "structure", @@ -42,6 +51,9 @@ const CONFIG = { }, }, domains: { + field: "domains.id_name", + aggregation: "domains.id_name.keyword", + co_aggregation: "co_domains.keyword", url: (_: string, label: string) => `/search/publications?q="${label.replace(/ /g, "+")}"`, terminology: { item: "domain", @@ -55,6 +67,9 @@ const CONFIG = { }, }, software: { + field: "software.id_name", + aggregation: "software.id_name.keyword", + co_aggregation: "co_software.keyword", url: (_: string, label: string) => `/search/publications?q="${label.replace(/ /g, "+")}"`, terminology: { item: "software", diff --git a/client/src/api/networks/network/network.ts b/client/src/api/networks/network/network.ts index 94fdbf06..c82704e6 100644 --- a/client/src/api/networks/network/network.ts +++ b/client/src/api/networks/network/network.ts @@ -11,7 +11,7 @@ export const GRAPH_MAX_ORDER = 300 export const GRAPH_MAX_COMPONENTS = 5 const nodeConcatMaxYear = (nodeMaxYear: number, maxYear: number) => (nodeMaxYear ? Math.max(nodeMaxYear, maxYear) : maxYear) -const nodeGetId = (id: string) => { +export const nodeGetId = (id: string) => { const nodeId = id.split("###")[0] return isNaN(+nodeId) ? nodeId : String(+nodeId) } @@ -82,10 +82,14 @@ export default async function networkCreate( x: attr.x, y: attr.y, label: attr.label, - cluster: attr.community + 1, + cluster: attr?.community + 1, weights: { Weight: attr.weight, Degree: graph.degree(key) }, scores: { "Last activity": attr?.maxYear }, page: configGetItemUrl(model, key, attr.label), + ...(attr?.publicationsCount !== undefined && { publicationsCount: attr?.publicationsCount }), + ...(attr?.citationsCount !== undefined && { citationsCount: attr?.citationsCount }), + ...(attr?.citationsRecent !== undefined && { citationsRecent: attr?.citationsRecent }), + ...(attr?.citationsScore !== undefined && { citationsScore: attr?.citationsScore }), })), links: graph.mapEdges((_, attr, source, target) => ({ source_id: source, diff --git a/client/src/api/networks/search/search.ts b/client/src/api/networks/search/search.ts index ce274dc8..392aee3c 100644 --- a/client/src/api/networks/search/search.ts +++ b/client/src/api/networks/search/search.ts @@ -7,6 +7,7 @@ import { NetworkSearchHitsArgs, ElasticAggregations, } from "../../../types/network" +import { CONFIG } from "../network/config" import networkCreate from "../network/network" import configCreate from "../network/config" import infoCreate from "../network/info" @@ -34,7 +35,7 @@ const networkSearchBody = (model: string, query?: string | unknown): NetworkSear }, aggs: { [model]: { - terms: { field: `co_${model}.keyword`, size: DEFAULT_SIZE }, + terms: { field: CONFIG[model].co_aggregation, size: DEFAULT_SIZE }, aggs: { max_year: { max: { field: "year" } } }, }, }, @@ -84,7 +85,7 @@ export async function networkSearchHits({ model, query, filters, links }: Networ const linksFilter = { terms: { [`co_${model}.keyword`]: links } } const body = { size: DEFAULT_SIZE, - _source: HIT_FIELDS, + _source: [...HIT_FIELDS, CONFIG[model].field], query: { bool: { must: [ diff --git a/client/src/pages/networks/components/clusters.tsx b/client/src/pages/networks/components/clusters.tsx index ebedfcc8..c2c2d2dd 100644 --- a/client/src/pages/networks/components/clusters.tsx +++ b/client/src/pages/networks/components/clusters.tsx @@ -35,12 +35,9 @@ type ClustersSectionArgs = { function ClusterItem({ currentTab, community, setFocusItem }: ClusterItemArgs) { const intl = useIntl() + const currentYear = new Date().getFullYear() const [showNodesModal, setShowNodesModal] = useState(false) const [showPublicationsModal, setShowPublicationsModal] = useState(false) - const currentYear = new Date().getFullYear() - const citationScore = (community: NetworkCommunity) => - (community?.citationsByYear?.[currentYear] + community?.citationsByYear?.[currentYear - 1]) / - (community?.publicationsCount || 1) const oaColor = (percent: number) => (percent >= 40.0 ? (percent >= 70.0 ? "success" : "yellow-moutarde") : "warning") return ( @@ -78,9 +75,9 @@ function ClusterItem({ currentTab, community, setFocusItem }: ClusterItemArgs) { {`${intl.formatMessage( { id: "networks.section.clusters.citations" }, - { count: community.citationsByYear?.[currentYear] + community.citationsByYear?.[currentYear - 1] } + { count: community.citationsRecent } )} (${currentYear - 1}-${currentYear})`} - {`Citation score: ${citationScore(community).toFixed(1)}`} + {`Citation score: ${community.citationsScore.toFixed(1)}`} diff --git a/client/src/pages/networks/hooks/useExportData.ts b/client/src/pages/networks/hooks/useExportData.ts index 6c871094..4eaadff0 100644 --- a/client/src/pages/networks/hooks/useExportData.ts +++ b/client/src/pages/networks/hooks/useExportData.ts @@ -21,7 +21,7 @@ const XSLXFormatter = (network: any) => { const publicationsList = network.clusters?.reduce((acc, cluster) => { cluster?.publications.forEach((publication) => { - acc = [...acc, { id: publication.id, title: publication.title, cluster: cluster.cluster }] + acc = [...acc, { id: publication.id, title: publication.title, cluster: cluster.cluster, clusterLabel: cluster.label }] }) return acc }, []) @@ -43,15 +43,25 @@ const exportNetwork = (network: NetworkData) => ({ id: item.id, label: item.label || "", cluster: item.cluster, - degree: item?.weights?.Degree || null, - weight: item?.weights?.Weight || null, + ...(network.clusters.length && { + clusterLabel: network.clusters.find((cluster) => cluster.cluster === item.cluster).label, + }), + publicationsCount: item?.publicationsCount, + citationsCount: item?.citationsCount, + citationsRecent: item?.citationsRecent, + citationsScore: item?.citationsScore, + degree: item?.weights?.Degree, + weight: item?.weights?.Weight, })), links: network.links, clusters: network.clusters.map((cluster) => ({ + id: cluster.cluster, label: cluster.label, - cluster: cluster.cluster, nodesCount: cluster.nodes.length, publicationsCount: cluster?.publicationsCount, + citationsCount: cluster?.citationsCount, + citationsRecent: cluster?.citationsRecent, + citationsScore: cluster?.citationsScore, publications: cluster?.publications, })), }) diff --git a/client/src/types/network.ts b/client/src/types/network.ts index 39fc8bc0..edac3a26 100644 --- a/client/src/types/network.ts +++ b/client/src/types/network.ts @@ -20,6 +20,10 @@ export type NetworkItem = { weights: Record scores: Record page?: string + publicationsCount?: number + citationsCount?: number + citationsRecent?: number + citationsScore?: number } export type NetworkLinks = Array export type NetworkLink = { @@ -40,10 +44,13 @@ export type NetworkCommunity = { url?: string }> maxYear?: number + publicationsByYear?: Record publicationsCount?: number publications?: Array> - publicationsByYear?: Record citationsByYear?: Record + citationsCount?: number + citationsRecent?: number + citationsScore?: number domains?: Record oaPercent?: number }