Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion browser-extension/src/entrypoints/posts/Home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ function HomePage() {
/>
<div className="flex gap-4">
<ActiveAuthors postComments={allComments} isLoading={isLoading} />
<CategoryDistribution />
<CategoryDistribution
postComments={allComments}
isLoading={isLoading}
/>
</div>
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ function PostDetailPage() {
postComments={post.comments}
isLoading={isLoading}
/>
<CategoryDistribution />
<CategoryDistribution
postComments={post.comments}
isLoading={isLoading}
/>
</div>
</div>
<div className="text-left">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ export const ReportContent = ({
postComments={reportQueryData?.postCommentList ?? []}
isLoading={isLoadingPosts}
/>
<CategoryDistribution />
<CategoryDistribution
postComments={posts?.flatMap((p) => p.comments) ?? []}
isLoading={isLoadingPosts}
/>
</div>
{groupedCommentsByPost.map(([postKey, commentList], index) => {
const post = posts?.find(
Expand Down
152 changes: 142 additions & 10 deletions browser-extension/src/entrypoints/posts/Shared/CategoryDistribution.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,152 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import WorkInProgress from "../WorkInProgress";
import { Spinner } from "@/components/ui/spinner";
import { PostComment } from "@/shared/model/post/Post";
import {
ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Pie, PieChart } from "recharts";
import { Info } from "lucide-react";
import { getCategoryStats } from "@/shared/utils/report-stats";

const CATEGORY_COLORS = [
"oklch(from var(--primary) 0.76 c h)",
"oklch(from var(--primary) 0.68 c h)",
"oklch(from var(--primary) 0.60 c h)",
"var(--primary)",
"oklch(from var(--primary) 0.41 c h)",
"oklch(from var(--primary) 0.33 c h)",
"oklch(from var(--primary) 0.25 c h)",
];

type CategoryDistributionProps = {
postComments: PostComment[];
isLoading: boolean;
};

type ChartDataPoint = {
name: string;
value: number;
fill: string;
};

function CategoryDistribution({
postComments,
isLoading,
}: Readonly<CategoryDistributionProps>) {
const categoryStats = getCategoryStats(postComments);
const total = categoryStats.reduce((sum, s) => sum + s.count, 0);

const dataPoints: ChartDataPoint[] = categoryStats.map((s, i) => ({
name: s.label,
value: s.count,
fill: CATEGORY_COLORS[i % CATEGORY_COLORS.length],
}));

const chartConfig = dataPoints.reduce<ChartConfig>((acc, d) => {
acc[d.name] = { label: d.name, color: d.fill };
return acc;
}, {}) satisfies ChartConfig;

function CategoryDistribution() {
return (
<Card className="w-full relative">
<Card className="w-full relative self-start">
<CardHeader>
<CardTitle className="text-left text-muted-forground font-display font-medium">
<CardTitle className="text-left text-muted-forground font-display font-medium flex items-center gap-1">
Répartition par catégories
<Tooltip>
<TooltipTrigger>
<Info className="size-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-56">
Répartition des commentaires par type d&apos;infraction. Voir le
détail des catégories dans Aide et ressources.
</TooltipContent>
</Tooltip>
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<WorkInProgress />
<p>
Graphique circulaire présentant la répartition des commentaires par
catégorie.
</p>
<CardContent>
{isLoading && <Spinner className="size-8" />}

{!isLoading && dataPoints.length === 0 && (
<p className="text-muted-foreground text-sm">
Aucun commentaire malveillant.
</p>
)}

{!isLoading && dataPoints.length > 0 && (
<div className="flex items-center gap-6">
<ChartContainer
config={chartConfig}
className="aspect-square h-48 shrink-0"
>
<PieChart>
<Pie
data={dataPoints}
dataKey="value"
nameKey="name"
innerRadius="55%"
outerRadius="80%"
strokeWidth={0}
/>
<ChartTooltip
content={
<ChartTooltipContent
indicator="line"
formatter={(value, _name, item) => (
<>
<div
className="w-1 shrink-0 self-stretch rounded-[2px]"
style={{
backgroundColor: (
item.payload as Record<string, unknown>
).fill as string,
}}
/>
<div className="flex flex-1 justify-between gap-2 leading-none">
<span className="text-muted-foreground">
Commentaires malveillants
</span>
<span className="font-mono font-medium tabular-nums">
{total > 0
? `${((Number(value) / total) * 100).toLocaleString("fr-FR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %`
: "0 %"}
</span>
</div>
</>
)}
/>
}
/>
</PieChart>
</ChartContainer>

<ul className="flex flex-col gap-1.5 text-xs min-w-0">
{dataPoints.map((d) => (
<li key={d.name} className="flex items-start gap-2">
<div
className="size-2 mt-0.5 shrink-0 rounded-full"
style={{ backgroundColor: d.fill }}
/>
<span className="flex-1 text-muted-foreground">{d.name}</span>
<span className="ml-auto whitespace-nowrap rounded-md bg-muted px-2 py-0.5 font-medium tabular-nums text-muted-foreground">
{((d.value / total) * 100).toLocaleString("fr-FR", {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}{" "}
%
</span>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
);
Expand Down
32 changes: 32 additions & 0 deletions browser-extension/src/shared/utils/report-stats.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import { getPercentage } from "@/shared/utils/maths";
import { PostComment } from "@/shared/model/post/Post";
import { isCommentHateful } from "@/shared/utils/post-util";
import {
HatefulCategory,
HatefulCategoryLabels,
} from "@/shared/model/HatefulCategory";

export type CategoryStat = {
label: string;
count: number;
};

const HATEFUL_CATEGORIES = Object.values(HatefulCategory).filter(
(c) => c !== HatefulCategory.ABSENCE_DE_CYBERHARCELEMENT,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Etant donnée que le backend renvoie les labels de catégories directement Je ne suis pas sûr que HatefulCategory tel que définit actuellement est beaucoup de sens. Mais bon ce n'est pas gràave pour cette PR.

);

const ABSENCE_LABEL =
HatefulCategoryLabels[HatefulCategory.ABSENCE_DE_CYBERHARCELEMENT];

export const getCategoryStats = (
postComments: readonly PostComment[],
): CategoryStat[] => {
const hatefulComments = postComments.filter(
(c) => c.classification?.[0] && c.classification[0] !== ABSENCE_LABEL,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 isCommentHateful de post-utils.ts devrait être aligné sur cette définition afin d'éviter les désalignement de chiffres.

Les deux pourraît déléguer à une méthode isCategoryHateful(categoryLabel: string): boolean

);
return HATEFUL_CATEGORIES.map((cat) => ({
label: HatefulCategoryLabels[cat],
count: hatefulComments.filter(
(c) => c.classification?.[0] === HatefulCategoryLabels[cat],
).length,
}))
.filter((s) => s.count > 0)
.sort((a, b) => b.count - a.count);
};

export type HatefulAuthorStats = {
/**
Expand Down
Loading