Skip to content

Commit

Permalink
Merge commit '7ded1996f267d9e88301f9d06c1aea43e33a61f6' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonestla committed Jan 15, 2025
2 parents 2cbcc98 + 7ded199 commit 53a79da
Show file tree
Hide file tree
Showing 13 changed files with 310 additions and 56 deletions.
11 changes: 10 additions & 1 deletion client/src/api/trends/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ export const CONFIG = {
"entity-fishing": {
field: "domains.id_name.keyword",
},
"open-alex": {
"open-alex-topics": {
field: "topics.display_name.keyword",
},
"open-alex-subfields": {
field: "topics.subfield.display_name.keyword",
},
"open-alex-fields": {
field: "topics.field.display_name.keyword",
},
"open-alex-domains": {
field: "topics.domain.display_name.keyword",
},
}
73 changes: 73 additions & 0 deletions client/src/api/trends/publications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,76 @@ export async function getCitationsTrends({ model, query, years, filters, normali
const trends = citationsTrends(aggregation, years, normalized)
return trends
}

export async function getPublicationsEvolution({ model, query, years, filters, normalized }: TrendsArgs) {
const body: any = {
size: 0,
query: {
bool: {
must: [
{ range: { year: { gte: years[0] } } },
{
query_string: {
query: query || "*",
fields: FIELDS,
},
},
],
},
},
aggs: {
years: {
terms: { field: "year", size: years.length },
aggs: {
model: { terms: { field: CONFIG[model].field, size: 60000 / years.length } },
},
},
},
}

if (filters && filters.length > 0) body.query.bool.filter = filters

const res = await fetch(`${publicationsIndex}/_search`, {
method: "POST",
body: JSON.stringify(body),
headers: postHeaders,
})

if (res.status !== 200) {
console.error(`Elasticsearch error: ${res.status}`)
return null
}

const json = await res.json()
const aggregation: TrendsAggregation = json?.aggregations?.["years"]?.buckets

if (!aggregation?.length) {
console.error(`Elasticsearch error: no aggregation found for years ${years[0]}-${years[years.length - 1]}`)
return null
}

const _evolution = aggregation.reduce((acc, bucket) => {
acc[bucket.key] = bucket?.model?.buckets.slice(0, 5).map((item) => ({
id: item.key.split("###")[0],
label: item.key.split("###")?.[1] || item.key.split("###")[0],
count: item.doc_count,
}))
return acc
}, [])

const evolution = Object.entries(_evolution).reduce((acc, [year, items]) => {
items.forEach((item) => {
acc[item.id] = {
label: item.label,
count: { ...acc?.[item.id]?.count, [year]: (acc?.[item.id]?.count?.[year] || 0) + item.count },
}
})
return acc
}, {})

console.log("years", years)
console.log("aggregation", aggregation)
console.log("items", evolution)

return evolution
}
92 changes: 46 additions & 46 deletions client/src/api/trends/trends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,7 @@ const MAX_ITEMS = 15

type TrendsAggregation = Array<ElasticBucket & { [x: string]: ElasticAggregation }>

export function citationsTrends(aggregation: ElasticBuckets, years: Array<number>, normalized: boolean) {
// Items citations count by year
const _items: Record<string, Record<string, any>> = aggregation.reduce((acc, item) => {
years.forEach((year) => {
const id = item.key.split("###")[0]
const label = item.key.split("###")?.[1] || id
const citationsCount = item?.[`citationsIn${year}`]?.value
acc[id] = {
...acc?.[id],
id: id,
label: acc?.[id]?.label || label,
count: { ...acc?.[id]?.count, ...(citationsCount && { [year]: (acc?.[id]?.count?.[year] || 0) + citationsCount }) },
sum: (acc?.[id]?.sum || 0) + citationsCount,
}
})
return acc
}, {})
const items = Object.values(_items)

const trends = computeTrends(items, years, normalized)
return trends
}

export function publicationsTrends(aggregation: TrendsAggregation, years: Array<number>, normalized: boolean) {
// Items count by year
const _items: Record<string, Record<string, any>> = aggregation.reduce((acc, bucket) => {
bucket?.model?.buckets.forEach((item) => {
const id = item.key.split("###")[0]
const label = item.key.split("###")?.[1] || id
acc[id] = {
...acc?.[id],
id: id,
label: acc?.[id]?.label || label,
count: { ...acc?.[id]?.count, [bucket.key]: (acc?.[id]?.count?.[bucket.key] || 0) + item.doc_count },
sum: (acc?.[id]?.sum || 0) + item.doc_count,
}
})
return acc
}, {})
const items = Object.values(_items)

const trends = computeTrends(items, years, normalized)
return trends
}

export function computeTrends(data: Array<any>, years: Array<number>, normalized: boolean) {
function computeTrends(data: Array<any>, years: Array<number>, normalized: boolean) {
const min_year = years[0]
const max_year = years[years.length - 1]

Expand Down Expand Up @@ -102,3 +57,48 @@ export function computeTrends(data: Array<any>, years: Array<number>, normalized

return trends
}

export function publicationsTrends(aggregation: TrendsAggregation, years: Array<number>, normalized: boolean) {
// Items count by year
const _items: Record<string, Record<string, any>> = aggregation.reduce((acc, bucket) => {
bucket?.model?.buckets.forEach((item) => {
const id = item.key.split("###")[0]
const label = item.key.split("###")?.[1] || id
acc[id] = {
...acc?.[id],
id: id,
label: acc?.[id]?.label || label,
count: { ...acc?.[id]?.count, [bucket.key]: (acc?.[id]?.count?.[bucket.key] || 0) + item.doc_count },
sum: (acc?.[id]?.sum || 0) + item.doc_count,
}
})
return acc
}, {})
const items = Object.values(_items)

const trends = computeTrends(items, years, normalized)
return trends
}

export function citationsTrends(aggregation: ElasticBuckets, years: Array<number>, normalized: boolean) {
// Items citations count by year
const _items: Record<string, Record<string, any>> = aggregation.reduce((acc, item) => {
years.forEach((year) => {
const id = item.key.split("###")[0]
const label = item.key.split("###")?.[1] || id
const citationsCount = item?.[`citationsIn${year}`]?.value
acc[id] = {
...acc?.[id],
id: id,
label: acc?.[id]?.label || label,
count: { ...acc?.[id]?.count, ...(citationsCount && { [year]: (acc?.[id]?.count?.[year] || 0) + citationsCount }) },
sum: (acc?.[id]?.sum || 0) + citationsCount,
}
})
return acc
}, {})
const items = Object.values(_items)

const trends = computeTrends(items, years, normalized)
return trends
}
82 changes: 82 additions & 0 deletions client/src/pages/trends/components/evolution-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Container } from "@dataesr/dsfr-plus"
import useEvolution from "../../hooks/useEvolution"
import { rangeArray } from "../../../../utils/helpers"
import { useIntl } from "react-intl"
import AnalyticsGraph from "../../../../components/analytics-graph"
import useTrends from "../../hooks/useTrends"

function LineChart({ data, source }) {
const intl = useIntl()
const {
trendsYears: { min, max },
} = useEvolution()
const range = rangeArray(min, max)

if (!data) return null

const highchartsOptions = {
chart: {
type: "line",
height: "500px",
},
xAxis: {
accessibility: {
description: intl.formatMessage({ id: "trends.line-chart.xAxis.accessibility.description" }, { min: min, max: max }),
},
tickInterval: 1, // one year
},
yAxis: {
accessibility: {
description: intl.formatMessage(
{ id: "trends.line-chart.yAxis.accessibility.description" },
{
source: intl.formatMessage({ id: `trends.select-source.${source}` }).toLowerCase(),
}
),
},
title: {
text: intl.formatMessage(
{ id: "trends.line-chart.yAxis.title.text" },
{
source: intl.formatMessage({ id: `trends.select-source.${source}` }).toLowerCase(),
}
),
},
},
plotOptions: {
series: {
pointStart: min,
pointInterval: 1, // one year
},
},
legend: { enabled: true },
series: Object.values(data).map((d: any) => ({
name: d.label,
data: range.map((year) => d?.count?.[year] || 0),
marker: { enabled: false },
})),
}

return (
<AnalyticsGraph
title={intl.formatMessage({ id: "trends.line-chart.title" }, { label: data.label })}
description={intl.formatMessage(
{ id: `trends.line-chart.description.${source}` },
{
label: data.label,
}
)}
options={highchartsOptions}
/>
)
}

export default function TrendsEvolutionChart() {
const { trends: evolution } = useEvolution()
const { trends } = useTrends()

console.log("evolution", evolution)
console.log("trends", trends)

return <Container className="fr-mt-5w">{<LineChart data={evolution} source="publications" />}</Container>
}
31 changes: 28 additions & 3 deletions client/src/pages/trends/components/select-model/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { Container, Listbox, ListboxItem } from "@dataesr/dsfr-plus"
import Modal from "../../../../components/modal"
import { useIntl } from "react-intl"
import { useTrendsContext } from "../../context"
import { getOpenAlexMapping } from "../../config/openalex"

export default function TrendsSelectModelModal() {
const intl = useIntl()
const { model, setModel } = useTrendsContext()
const id = "trends-options-select-model-modal"

const openAlexMapping = getOpenAlexMapping()
console.log(openAlexMapping)

return (
<Modal id={id} size="lg" title={intl.formatMessage({ id: "trends.select-model.modal.title" })}>
<Container fluid className="fr-mb-4w">
Expand All @@ -29,11 +33,32 @@ export default function TrendsSelectModelModal() {
{intl.formatMessage({ id: "trends.select-model.entity-fishing" })}
</ListboxItem>
<ListboxItem
key={"open-alex"}
key={"open-alex-topics"}
startContent={<span className={`fr-mr-3v fr-icon--lg fr-icon-book-2-line`} />}
description={intl.formatMessage({ id: "trends.select-model.open-alex-topics.description" })}
>
{intl.formatMessage({ id: "trends.select-model.open-alex-topics" })}
</ListboxItem>
<ListboxItem
key={"open-alex-subfields"}
startContent={<span className={`fr-mr-3v fr-icon--lg fr-icon-book-2-line`} />}
description={intl.formatMessage({ id: "trends.select-model.open-alex-subfields.description" })}
>
{intl.formatMessage({ id: "trends.select-model.open-alex-subfields" })}
</ListboxItem>
<ListboxItem
key={"open-alex-fields"}
startContent={<span className={`fr-mr-3v fr-icon--lg fr-icon-book-2-line`} />}
description={intl.formatMessage({ id: "trends.select-model.open-alex-fields.description" })}
>
{intl.formatMessage({ id: "trends.select-model.open-alex-fields" })}
</ListboxItem>
<ListboxItem
key={"open-alex-domains"}
startContent={<span className={`fr-mr-3v fr-icon--lg fr-icon-book-2-line`} />}
description={intl.formatMessage({ id: "trends.select-model.open-alex.description" })}
description={intl.formatMessage({ id: "trends.select-model.open-alex-domains.description" })}
>
{intl.formatMessage({ id: "trends.select-model.open-alex" })}
{intl.formatMessage({ id: "trends.select-model.open-alex-domains" })}
</ListboxItem>
</Listbox>
</Container>
Expand Down
16 changes: 16 additions & 0 deletions client/src/pages/trends/config/openalex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// import { useSuspenseQuery } from "@tanstack/react-query"
import { useMemo } from "react"
import data from "./openalex_topic_mapping_table.json"

export function getOpenAlexMapping() {
// const { data } = useSuspenseQuery({
// queryKey: ["bso", "locals"],
// queryFn: () => fetch("./openalex_topic_mapping_table.json").then((response) => (response.ok ? response.json() : {})),
// })

const values = useMemo(() => {
return data
}, [data])

return values
}

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions client/src/pages/trends/config/years.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const CURRENT_YEAR = new Date().getFullYear()
export const CURRENT_YEAR = /*new Date().getFullYear()*/ 2024
export const MAX_YEAR = CURRENT_YEAR - 1
export const MIN_YEAR = MAX_YEAR - 10
export const MIN_YEAR = 2013
2 changes: 1 addition & 1 deletion client/src/pages/trends/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function TrendsContext({ children }) {
const [view, setView] = useState<string>(TRENDS_VIEWS[0].id)
const [model, setModel] = useState<string>("entity-fishing")
const [source, setSource] = useState<string>("publications")
const [normalized, setNormalized] = useState<boolean>(true)
const [normalized, setNormalized] = useState<boolean>(false)
const [focus, setFocus] = useState<string>("")

return (
Expand Down
Loading

0 comments on commit 53a79da

Please sign in to comment.