diff --git a/client/.env.production b/client/.env.production index 0f25e687..fb3b71b4 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,4 +1,5 @@ VITE_API_URL="/api" VITE_MISTRAL_URL="/mistral" VITE_APP_MATOMO_BASE_URL="https://piwik.enseignementsup-recherche.pro" -VITE_APP_MATOMO_SITE_ID="36" \ No newline at end of file +VITE_APP_MATOMO_SITE_ID="36" +VITE_APP_ENV="production" \ No newline at end of file diff --git a/client/.env.staging b/client/.env.staging index 9002c0af..49a77f95 100644 --- a/client/.env.staging +++ b/client/.env.staging @@ -1,4 +1,5 @@ VITE_API_URL="/api" VITE_MISTRAL_URL="/mistral" VITE_APP_MATOMO_BASE_URL="https://matomo.staging.dataesr.ovh" -VITE_APP_MATOMO_SITE_ID="4" \ No newline at end of file +VITE_APP_MATOMO_SITE_ID="4" +VITE_APP_ENV="staging" \ No newline at end of file diff --git a/client/src/api/patents/[id]/index.ts b/client/src/api/patents/[id]/index.ts index b40d17e7..ab3f5205 100644 --- a/client/src/api/patents/[id]/index.ts +++ b/client/src/api/patents/[id]/index.ts @@ -32,6 +32,7 @@ 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, @@ -49,9 +50,17 @@ export async function getCpcAggregation(value: string): Promise { aggs: { byCpc: { terms: { - field: "cpc.ss_classe.code.keyword", + field: "cpc.classe.code.keyword", size: 10000, }, + aggs: { + bySectionLabel: { + terms: { + field: "cpc.section.label.keyword", + size: 1, + }, + }, + }, }, }, }; @@ -68,7 +77,7 @@ export async function getCpcAggregation(value: string): Promise { const hits = data?.hits?.hits; const labelsByCode = hits.reduce((acc: any, hit: any) => { - const cpcGroups = hit._source.cpc?.ss_classe ?? []; + const cpcGroups = hit._source.cpc?.classe ?? []; cpcGroups.forEach((cpc: any) => { if (!acc[cpc.code]) { acc[cpc.code] = cpc.label; @@ -77,11 +86,18 @@ export async function getCpcAggregation(value: string): Promise { return acc; }, {}); - const patent = buckets.map((bucket: any) => ({ - code: bucket.key, - doc_count: bucket.doc_count, - label: labelsByCode[bucket.key] || "Label non trouvé", - })); + const patent = buckets.map((bucket: any) => { + console.log(bucket); + const sectionLabel = + bucket.bySectionLabel?.buckets?.[0]?.key || "Label de section non trouvé"; + + return { + code: bucket.key, + doc_count: bucket.doc_count, + label: labelsByCode[bucket.key] || "Label non trouvé", + sectionLabel, + }; + }); return patent; } diff --git a/client/src/components/patent-chart/index.tsx b/client/src/components/patent-chart/index.tsx index d838199f..494e33db 100644 --- a/client/src/components/patent-chart/index.tsx +++ b/client/src/components/patent-chart/index.tsx @@ -2,74 +2,131 @@ import React from "react"; import Highcharts from "highcharts"; import HighchartsReact from "highcharts-react-official"; import HighchartsMore from "highcharts/highcharts-more"; +import { useIntl } from "react-intl"; HighchartsMore(Highcharts); type CpcChartProps = { data: { - label: any; - name: string; + label: string; doc_count: number; code: string; }[]; }; +const sectionLabels: Record = { + A: "organizations.sectionLabels.humanNecessities", + B: "organizations.sectionLabels.performingOperations", + C: "organizations.sectionLabels.chemistryMetallurgy", + D: "organizations.sectionLabels.textilesPaper", + E: "organizations.sectionLabels.fixedConstructions", + F: "organizations.sectionLabels.mechanicalEngineering", + G: "organizations.sectionLabels.physics", + H: "organizations.sectionLabels.electricity", + Y: "organizations.sectionLabels.other", +}; + const CpcChart: React.FC = ({ data }) => { - const chartData = data.map((item) => ({ - name: item.label, - value: item.doc_count, - code: item.code, - })); + const intl = useIntl(); + const colorPalette = [ + "#7AB1E8", + "#21AB8E", + "#f6e157", + "#CE70CC", + "#FF9575", + "#FFCA00", + "#FF732C", + "#E6BE92", + "#AEA397", + ]; + + const groupedData = data.reduce((acc, item) => { + const firstLetter = item.code.charAt(0).toUpperCase(); + if (!acc[firstLetter]) acc[firstLetter] = []; + acc[firstLetter].push({ + name: item.code, + value: item.doc_count, + label: item.label, + }); + return acc; + }, {} as Record); + + const seriesData = Object.entries(groupedData).map( + ([letter, items], index) => ({ + name: + intl.formatMessage({ id: sectionLabels[letter] }) || + `Section ${letter}`, + color: colorPalette[index % colorPalette.length], + data: items.map((item) => ({ + name: item.name, + value: item.value, + label: item.label, + })), + }) + ); + + const values = seriesData.flatMap((series) => + series.data.map((point) => point.value) + ); + const zMin = Math.min(...values); + const zMax = Math.max(...values); const options = { chart: { type: "packedbubble", - layoutAlgorithm: { - splitSeries: false, - }, - height: "100%", backgroundColor: "#f4f4f4", + height: "100%", + }, + title: { + text: intl.formatMessage({ id: "organizations.patents.chart.title" }), }, tooltip: { - pointFormat: "{point.name} (Code: {point.code}): {point.value}", formatter: function () { - return `${this.point.name} (Code: ${this.point.code}): ${this.point.value}`; + const familiesText = intl.formatMessage( + { + id: "organizations.patents.chart.families", + defaultMessage: "{value} famille{plural}", + }, + { + value: this.point.value, + plural: this.point.value > 1 ? "s" : "", + } + ); + return `${this.point.name} - ${this.point.label}: ${familiesText}`; }, }, - 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, + minSize: "45%", + maxSize: "120%", + zMin: zMin, + zMax: zMax, + layoutAlgorithm: { + gravitationalConstant: 0.05, + splitSeries: true, + seriesInteraction: false, + parentNodeLimit: true, + }, dataLabels: { enabled: true, - format: "{point.value}", - }, - cursor: "pointer", - point: { - events: { - click: function () { - window.location.href = `/search/patents?q=${this.code}`; - }, + format: "{point.name}", + style: { + color: "black", + textOutline: "none", + fontWeight: "normal", }, }, }, }, + series: seriesData, }; - return ; + return ( + <> + + + ); }; export default CpcChart; diff --git a/client/src/components/patent-chart/indexA.tsx b/client/src/components/patent-chart/indexA.tsx deleted file mode 100644 index 587bc8e5..00000000 --- a/client/src/components/patent-chart/indexA.tsx +++ /dev/null @@ -1,59 +0,0 @@ -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 0c0a9a54..55b680e0 100644 --- a/client/src/pages/organizations/[id]/components/patents/index.tsx +++ b/client/src/pages/organizations/[id]/components/patents/index.tsx @@ -3,11 +3,11 @@ import { Button, Row, Col, Text } from "@dataesr/dsfr-plus"; 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 { useQuery } from "@tanstack/react-query"; +import { getCpcAggregation } from "../../../../../api/patents/[id]"; import { OrganizationPatentsData } from "../../../../../types/organization"; -// import CpcWordCloud from "../../../../../components/patent-chart/indexA"; +import CpcWordCloud from "../../../../../components/patent-chart"; +import { isInProduction } from "../../../../../utils/helpers"; type OrganizationPatentsProps = { data: OrganizationPatentsData; @@ -28,22 +28,22 @@ export default function OrganizationPatents({ "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 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 prepareCpcGraphData = (patentsData) => { + return patentsData.map((item) => ({ + code: item.code, + doc_count: item.doc_count, + label: item.label, + })); + }; - // const graphData = prepareCpcGraphData(patentsData); + const graphData = prepareCpcGraphData(patentsData); const patentsFilterUrl = `/search/patents?filters=${encodeURIComponent( JSON.stringify(searchFilters) @@ -134,33 +134,21 @@ export default function OrganizationPatents({ {intl.formatMessage({ id: "organizations.patents.nav.year" })} - - {/*
- setProjectGraph("cpc")} - /> - -
-
- setProjectGraph("cpc2")} - /> - -
*/} + {!isInProduction() && ( +
+ setProjectGraph("cpc")} + /> + +
+ )} @@ -175,12 +163,13 @@ export default function OrganizationPatents({ years={patents.byYear.map((year) => year.label)} /> )} - {/* {projectGraph === "cpc" && patentsData && ( - + {!isInProduction() && ( + <> + {projectGraph === "cpc" && patentsData && ( + + )} + )} - {projectGraph === "cpc2" && patentsData && ( - - )} */}
diff --git a/client/src/pages/organizations/[id]/locales/en.json b/client/src/pages/organizations/[id]/locales/en.json index bb168197..135df74f 100644 --- a/client/src/pages/organizations/[id]/locales/en.json +++ b/client/src/pages/organizations/[id]/locales/en.json @@ -42,8 +42,18 @@ "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.patents.nav.cpc": "Distribution by classification", + "organizations.sectionLabels.humanNecessities": "Human Necessities", + "organizations.sectionLabels.performingOperations": "Operations and Transport", + "organizations.sectionLabels.chemistryMetallurgy": "Chemistry and Metallurgy", + "organizations.sectionLabels.textilesPaper": "Textiles", + "organizations.sectionLabels.fixedConstructions": "Fixed Constructions", + "organizations.sectionLabels.mechanicalEngineering": "Mechanical Engineering", + "organizations.sectionLabels.physics": "Physics", + "organizations.sectionLabels.electricity": "Electricity", + "organizations.sectionLabels.other": "Emerging Cross-Sectional Technologies", + "organizations.patents.chart.families": "{value} patent family{plural}", + "organizations.patents.chart.title": "Technological fields identified in the organization's patent families (by section and class)", "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 1ca4e485..5c097db1 100644 --- a/client/src/pages/organizations/[id]/locales/fr.json +++ b/client/src/pages/organizations/[id]/locales/fr.json @@ -28,6 +28,7 @@ "organizations.section.agreements.title": "Accréditations", "organizations.section.awards.title": "Distinctions", "organizations.section.web.title": "Sur le web", + "organizations.patents.nav.cpc": "Répartition par classification CPC", "organizations.section.social-medias.title": "Réseaux sociaux", "organizations.section.identifiers.title": "Identifiants de la structure", "organizations.copy": "Cliquez pour copier dans le presse-papier", @@ -39,7 +40,18 @@ "organizations.header.description.ia-edit-label": "Éditer", "organizations.header.description.ia-edit-hover": "Proposer une description manuelle", "organizations.patents.count": "Familles de brevets répertoriés par scanR", + "organizations.sectionLabels.humanNecessities": "Nécessités courantes de la vie", + "organizations.sectionLabels.performingOperations": "Techniques industrielles diverses; transports", + "organizations.sectionLabels.chemistryMetallurgy": "Chimie; métallurgie", + "organizations.sectionLabels.textilesPaper": "Textiles; papier", + "organizations.sectionLabels.fixedConstructions": "Constructions fixes", + "organizations.sectionLabels.mechanicalEngineering": "Mécanique; éclairage; chauffage; armement; sautage", + "organizations.sectionLabels.physics": "Physique", + "organizations.sectionLabels.electricity": "Electricité", + "organizations.sectionLabels.other": "Technologies émergentes ou celles qui couvrent plusieurs sections", "organizations.patents.search": "Voir la liste des brevets", + "organizations.patents.chart.families": "{value} famille{plural} de brevets", + "organizations.patents.chart.title": "Domaines technogiques identifiés dans les familles de brevets de la structure (par section et classe)", "organizations.patents.nav.year": "Répartition par année", "organizations.patents.year-bars.name": "Familles de brevets", "organizations.projects.count": "financements répertoriés par scanR", diff --git a/client/src/utils/helpers.tsx b/client/src/utils/helpers.tsx new file mode 100644 index 00000000..e4d04320 --- /dev/null +++ b/client/src/utils/helpers.tsx @@ -0,0 +1,3 @@ +export function isInProduction() { + return import.meta.env.VITE_APP_ENV === "production"; +}