diff --git a/client/src/api/patents/[id]/index.ts b/client/src/api/patents/[id]/index.ts index b0264542..b40d17e7 100644 --- a/client/src/api/patents/[id]/index.ts +++ b/client/src/api/patents/[id]/index.ts @@ -32,3 +32,56 @@ export async function getPatentById(id: string): Promise { if (!patent) throw new Error("404"); return { ...patent, _id: data?.hits?.hits?.[0]._id }; } +export async function getCpcAggregation(value: string): Promise { + const body: any = { + size: 10000, + query: { + bool: { + must: [ + { + term: { + "applicants.ids.id.keyword": value, + }, + }, + ], + }, + }, + aggs: { + byCpc: { + terms: { + field: "cpc.ss_classe.code.keyword", + size: 10000, + }, + }, + }, + }; + + const res = await fetch(`${patentsIndex}/_search`, { + method: "POST", + body: JSON.stringify(body), + headers: postHeaders, + }); + + const data = await res.json(); + + const buckets = data?.aggregations?.byCpc?.buckets; + const hits = data?.hits?.hits; + + const labelsByCode = hits.reduce((acc: any, hit: any) => { + const cpcGroups = hit._source.cpc?.ss_classe ?? []; + cpcGroups.forEach((cpc: any) => { + if (!acc[cpc.code]) { + acc[cpc.code] = cpc.label; + } + }); + return acc; + }, {}); + + const patent = buckets.map((bucket: any) => ({ + code: bucket.key, + doc_count: bucket.doc_count, + label: labelsByCode[bucket.key] || "Label non trouvé", + })); + + return patent; +} diff --git a/client/src/components/patent-chart/index.tsx b/client/src/components/patent-chart/index.tsx new file mode 100644 index 00000000..d838199f --- /dev/null +++ b/client/src/components/patent-chart/index.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; +import HighchartsMore from "highcharts/highcharts-more"; + +HighchartsMore(Highcharts); + +type CpcChartProps = { + data: { + label: any; + name: string; + doc_count: number; + code: string; + }[]; +}; + +const CpcChart: React.FC = ({ data }) => { + const chartData = data.map((item) => ({ + name: item.label, + value: item.doc_count, + code: item.code, + })); + + const options = { + chart: { + type: "packedbubble", + layoutAlgorithm: { + splitSeries: false, + }, + height: "100%", + backgroundColor: "#f4f4f4", + }, + tooltip: { + pointFormat: "{point.name} (Code: {point.code}): {point.value}", + formatter: function () { + return `${this.point.name} (Code: ${this.point.code}): ${this.point.value}`; + }, + }, + series: [ + { + minSize: 20, + maxSize: 100, + data: chartData.map((item) => ({ + name: item.name, + value: item.value, + code: item.code, + })), + }, + ], + plotOptions: { + packedbubble: { + minSize: 10, + maxSize: 100, + zMin: 0, + zMax: 100, + dataLabels: { + enabled: true, + format: "{point.value}", + }, + cursor: "pointer", + point: { + events: { + click: function () { + window.location.href = `/search/patents?q=${this.code}`; + }, + }, + }, + }, + }, + }; + + return ; +}; + +export default CpcChart; diff --git a/client/src/components/patent-chart/indexA.tsx b/client/src/components/patent-chart/indexA.tsx new file mode 100644 index 00000000..587bc8e5 --- /dev/null +++ b/client/src/components/patent-chart/indexA.tsx @@ -0,0 +1,59 @@ +import React from "react"; +import Highcharts from "highcharts"; +import HighchartsReact from "highcharts-react-official"; +import wordcloud from "highcharts/modules/wordcloud"; + +wordcloud(Highcharts); + +type CpcWordChartProps = { + data: { + label: string; + doc_count: number; + code: string; + }[]; +}; + +const CpcWordCloud: React.FC = ({ data }) => { + const wordCloudData = data.map((item) => ({ + name: item.code, + weight: item.doc_count, + label: item.label, + })); + + const options = { + chart: { + type: "wordcloud", + }, + series: [ + { + type: "wordcloud", + data: wordCloudData, + name: "Occurrences", + }, + ], + tooltip: { + pointFormat: + "{point.label}: {point.name} (Occurrences: {point.weight})", + }, + plotOptions: { + series: { + cursor: "pointer", + point: { + events: { + click: function () { + window.location.href = `/search/patents?q=${this.name}`; + }, + }, + }, + }, + wordcloud: { + minFontSize: 10, + maxFontSize: 50, + }, + }, + }; + + return ; +}; + +export default CpcWordCloud; diff --git a/client/src/pages/organizations/[id]/components/patents/index.tsx b/client/src/pages/organizations/[id]/components/patents/index.tsx index b2041725..31b433da 100644 --- a/client/src/pages/organizations/[id]/components/patents/index.tsx +++ b/client/src/pages/organizations/[id]/components/patents/index.tsx @@ -1,32 +1,72 @@ import { useIntl } from "react-intl"; import { Button, Row, Col, Text } from "@dataesr/dsfr-plus"; -import { OrganizationPatentsData } from "../../../../../types/organization"; import useScreenSize from "../../../../../hooks/useScreenSize"; import YearBars from "../../../../../components/year-bars"; +import { useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import PatentChart from "../../../../../components/patent-chart"; +import { getCpcAggregation } from "../../../../../api/patents/[id]"; +import { OrganizationPatentsData } from "../../../../../types/organization"; +import CpcWordCloud from "../../../../../components/patent-chart/indexA"; type OrganizationPatentsProps = { - data: OrganizationPatentsData, - value: string, - label?: string -} + data: OrganizationPatentsData; + value: string; + label?: string; +}; -export default function OrganizationPatents({ data: patents, value, label }: OrganizationPatentsProps) { +export default function OrganizationPatents({ + data: patents, + value, + label, +}: OrganizationPatentsProps) { const { screen } = useScreenSize(); const intl = useIntl(); - const searchFilters = { 'applicants.ids.id': { values: [{ value: value, label }], type: 'terms' } }; - const patentsFilterUrl = `/search/patents?filters=${encodeURIComponent(JSON.stringify(searchFilters))}`; + const [projectGraph, setProjectGraph] = useState("type"); + + const searchFilters = { + "applicants.ids.id": { values: [{ value, label }], type: "terms" }, + }; + + const patentId = searchFilters["applicants.ids.id"].values[0].value; + const { data: patentsData = [] } = useQuery({ + queryKey: ["patent", patentId], + queryFn: () => getCpcAggregation(patentId), + throwOnError: true, + }); + + const prepareCpcGraphData = (patentsData) => { + return patentsData.map((item) => ({ + code: item.code, + doc_count: item.doc_count, + label: item.label, + })); + }; + + const graphData = prepareCpcGraphData(patentsData); + + const patentsFilterUrl = `/search/patents?filters=${encodeURIComponent( + JSON.stringify(searchFilters) + )}`; if (!patents.patentsCount || patents.patentsCount === 0) { return null; } + if (patents.patentsCount < 5 || ["xs", "sm"].includes(screen)) { return ( <> -
+
- {patents.patentsCount} - {' '} + {patents.patentsCount}{" "} {intl.formatMessage({ id: "organizations.patents.count" })}
@@ -47,44 +87,103 @@ export default function OrganizationPatents({ data: patents, value, label }: Org return ( <> -
+
- {patents.patentsCount} - {' '} + {patents.patentsCount}{" "} {intl.formatMessage({ id: "organizations.patents.count" })}
-
-
+
- {intl.formatMessage({ id: "organizations.activity.fieldset.legend" })} + {intl.formatMessage({ + id: "organizations.activity.fieldset.legend", + })}
- -
+ +
+ setProjectGraph("cpc")} + /> + +
+
+ setProjectGraph("cpc2")} + /> + +
- year.count)} - years={patents.byYear.map((year) => year.label)} - /> + {projectGraph === "type" && ( + year.count)} + years={patents.byYear.map((year) => year.label)} + /> + )} + {projectGraph === "cpc" && patentsData && ( + + )} + {projectGraph === "cpc2" && patentsData && ( + + )}
); -} \ No newline at end of file +} diff --git a/client/src/pages/organizations/[id]/locales/en.json b/client/src/pages/organizations/[id]/locales/en.json index 3d765096..681c487f 100644 --- a/client/src/pages/organizations/[id]/locales/en.json +++ b/client/src/pages/organizations/[id]/locales/en.json @@ -36,10 +36,12 @@ "organizations.header.description.ia-generated-hover": "Learn more", "organizations.header.description.ia-edit-label": "Edit", "organizations.header.description.ia-edit-hover": "Suggest a manual description", - "organizations.patents.count": "patents listed by scanR", + "organizations.patents.count": "patents family listed by scanR", "organizations.patents.search": "See the list of patents", "organizations.patents.nav.year": "Distribution by year", "organizations.patents.year-bars.name": "Patents", + "organizations.patents.nav.cpc-1": "Distribution by classification TEST 1 ", + "organizations.patents.nav.cpc-2": "Distribution by classification TEST 2 ", "organizations.projects.count": "funding listed by scanR", "organizations.projects.search": "See the list of fundings", "organizations.projects.nav.year": "Distribution by year", diff --git a/client/src/pages/organizations/[id]/locales/fr.json b/client/src/pages/organizations/[id]/locales/fr.json index 7a1eeee1..f14e85ed 100644 --- a/client/src/pages/organizations/[id]/locales/fr.json +++ b/client/src/pages/organizations/[id]/locales/fr.json @@ -36,10 +36,12 @@ "organizations.header.description.ia-generated-hover": "En savoir plus", "organizations.header.description.ia-edit-label": "Éditer", "organizations.header.description.ia-edit-hover": "Proposer une description manuelle", - "organizations.patents.count": "brevets répertoriés par scanR", + "organizations.patents.count": "Familles de brevets répertoriés par scanR", "organizations.patents.search": "Voir la liste des brevets", "organizations.patents.nav.year": "Répartition par année", - "organizations.patents.year-bars.name": "Brevets", + "organizations.patents.nav.cpc-1": "Répartition par classification TEST 1 ", + "organizations.patents.nav.cpc-2": "Répartition par classification TEST 2 ", + "organizations.patents.year-bars.name": "Familles de brevets", "organizations.projects.count": "financements répertoriés par scanR", "organizations.projects.search": "Voir la liste des financements", "organizations.projects.nav.year": "Répartition par année", diff --git a/client/src/pages/search/locales/en.json b/client/src/pages/search/locales/en.json index 5fde14dc..27ef229e 100644 --- a/client/src/pages/search/locales/en.json +++ b/client/src/pages/search/locales/en.json @@ -29,7 +29,7 @@ "search.top.authors.result": "{count, plural, =0 {# author} one {# author} other {# authors}}", "search.top.organizations.result": "{count, plural, =0 {# organization} one {# organization} other {# organizations}}", "search.top.projects.result": "{count, plural, =0 {# funding} one {# funding} other {# fundings}}", - "search.top.patents.result": "{count, plural, =0 {# patent} one {# patent} other {# patents}}", + "search.top.patents.result": "{count, plural, =0 {# patent family} one {# patent family} other {# patents families}}", "search.top.publications.filters.result-count": "{count, plural, =0 {# publication} one {# publication} other {# publications}}", "search.top.organizations.filters.result-count": "{count, plural, =0 {# organizations} one {# organization} other {# organizations}}", "search.top.projects.filters.result-count": "{count, plural, =0 {# fundings} one {# funding} other {# fundings}}", diff --git a/client/src/pages/search/locales/fr.json b/client/src/pages/search/locales/fr.json index eb6af905..22537c26 100644 --- a/client/src/pages/search/locales/fr.json +++ b/client/src/pages/search/locales/fr.json @@ -30,7 +30,7 @@ "search.top.authors.result": "{count, plural, =0 {# auteur} one {# auteur} other {# auteurs}}", "search.top.organizations.result": "{count, plural, =0 {# structure} one {# structure} other {# structures}}", "search.top.projects.result": "{count, plural, =0 {# financements} one {# financement} other {# financements}}", - "search.top.patents.result": "{count, plural, =0 {# brevets} one {# brevet} other {# brevets}}", + "search.top.patents.result": "{count, plural, =0 {# familles de brevets} one {# famille de brevet} other {# familles de brevets}}", "search.top.publications.filters.result-count": "{count, plural, =0 {# publication} one {# publication} other {# publications}}", "search.top.organizations.filters.result-count": "{count, plural, =0 {# structures} one {# structure} other {# structures}}", "search.top.projects.filters.result-count": "{count, plural, =0 {# financements} one {# financements} other {# financements}}",