Skip to content

Commit

Permalink
Merge branch 'staging' of https://github.com/dataesr/scanr-ui into st…
Browse files Browse the repository at this point in the history
…aging
  • Loading branch information
ahonestla committed Oct 3, 2024
2 parents fc6b531 + f65a9fd commit a8d6208
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 31 deletions.
53 changes: 53 additions & 0 deletions client/src/api/patents/[id]/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,56 @@ export async function getPatentById(id: string): Promise<Patent> {
if (!patent) throw new Error("404");
return { ...patent, _id: data?.hits?.hits?.[0]._id };
}
export async function getCpcAggregation(value: string): Promise<Patent> {
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;
}
75 changes: 75 additions & 0 deletions client/src/components/patent-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CpcChartProps> = ({ 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: "<b>{point.name}</b> (Code: {point.code}): {point.value}",
formatter: function () {
return `<b>${this.point.name}</b> (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 <HighchartsReact highcharts={Highcharts} options={options} />;
};

export default CpcChart;
59 changes: 59 additions & 0 deletions client/src/components/patent-chart/indexA.tsx
Original file line number Diff line number Diff line change
@@ -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<CpcWordChartProps> = ({ 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}: <b>{point.name}</b> (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 <HighchartsReact highcharts={Highcharts} options={options} />;
};

export default CpcWordCloud;
151 changes: 125 additions & 26 deletions client/src/pages/organizations/[id]/components/patents/index.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="fr-mb-3w" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div
className="fr-mb-3w"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ flexGrow: 1 }}>
<Text className="fr-m-0" bold>
{patents.patentsCount}
{' '}
{patents.patentsCount}{" "}
{intl.formatMessage({ id: "organizations.patents.count" })}
</Text>
</div>
Expand All @@ -47,44 +87,103 @@ export default function OrganizationPatents({ data: patents, value, label }: Org

return (
<>
<div className="fr-mb-3w" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div
className="fr-mb-3w"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<div style={{ flexGrow: 1 }}>
<Text size="lg" className="fr-m-0" bold>
{patents.patentsCount}
{' '}
{patents.patentsCount}{" "}
{intl.formatMessage({ id: "organizations.patents.count" })}
</Text>
</div>
<Button as="a" variant="text" icon="arrow-right-s-line" iconPosition="right" href={patentsFilterUrl}>
<Button
as="a"
variant="text"
icon="arrow-right-s-line"
iconPosition="right"
href={patentsFilterUrl}
>
{intl.formatMessage({ id: "organizations.patents.search" })}
</Button>
</div>
<Row gutters>
<Col xs="12">
<fieldset id="publication-graph-selector" className="fr-segmented fr-segmented--sm">
<fieldset
id="publication-graph-selector"
className="fr-segmented fr-segmented--sm"
>
<legend className="fr-segmented__legend">
{intl.formatMessage({ id: "organizations.activity.fieldset.legend" })}
{intl.formatMessage({
id: "organizations.activity.fieldset.legend",
})}
</legend>
<div className="fr-segmented__elements">
<div className="fr-segmented__element">
<input checked type="radio" id="segmented-patents" />
<label className="fr-label" htmlFor="segmented-patents">
<input
checked={projectGraph === "type"}
onChange={() => setProjectGraph("type")}
type="radio"
id="segmented-patents-1"
/>
<label className="fr-label" htmlFor="segmented-patents-1">
{intl.formatMessage({ id: "organizations.patents.nav.year" })}
</label>
</div>

<div className="fr-segmented__element">
<input
checked={projectGraph === "cpc"}
type="radio"
id="segmented-patents-2"
onChange={() => setProjectGraph("cpc")}
/>
<label className="fr-label" htmlFor="segmented-patents-2">
{intl.formatMessage({
id: "organizations.patents.nav.cpc-1",
})}
</label>
</div>
<div className="fr-segmented__element">
<input
checked={projectGraph === "cpc2"}
type="radio"
id="segmented-patents-3"
onChange={() => setProjectGraph("cpc2")}
/>
<label className="fr-label" htmlFor="segmented-patents-3">
{intl.formatMessage({
id: "organizations.patents.nav.cpc-2",
})}
</label>
</div>
</div>
</fieldset>
</Col>
<Col xs="12" className="fr-pb-6w">
<YearBars
name={intl.formatMessage({ id: "organizations.patents.year-bars.name" })}
height="300px"
counts={patents.byYear.map((year) => year.count)}
years={patents.byYear.map((year) => year.label)}
/>
{projectGraph === "type" && (
<YearBars
name={intl.formatMessage({
id: "organizations.patents.year-bars.name",
})}
height="300px"
counts={patents.byYear.map((year) => year.count)}
years={patents.byYear.map((year) => year.label)}
/>
)}
{projectGraph === "cpc" && patentsData && (
<PatentChart data={graphData} />
)}
{projectGraph === "cpc2" && patentsData && (
<CpcWordCloud data={graphData} />
)}
</Col>
</Row>
<hr />
</>
);
}
}
Loading

0 comments on commit a8d6208

Please sign in to comment.