Chapters read distribution chart component.
export const ChaptersReadDistributionChart: FC<ChaptersReadDistributionChartProps> = React.memo(function ChaptersReadDistributionChartMemo({ // eslint-disable-next-line react/prop-types matchResults, // eslint-disable-next-line react/prop-types className, }) { const { bins, stats } = useMemo( () => buildHistogram(matchResults), [matchResults], ); const hasData = bins.length > 0; return ( <section aria-label="Chapters read distribution" className={cn( "rounded-2xl border border-slate-200 bg-white/90 p-6 shadow-sm backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/90", className, )} > <header className="mb-4 flex flex-col gap-3"> <div className="flex items-center gap-2"> <span className="bg-linear-to-r inline-flex h-9 min-h-9 w-9 min-w-9 items-center justify-center rounded-full from-blue-500/20 via-purple-500/20 to-fuchsia-500/20 text-blue-500 dark:text-blue-300"> <BarChart2 className="h-4 w-4" aria-hidden="true" /> </span> <div> <h2 className="text-foreground text-lg font-semibold"> Reading Progress Distribution </h2> <p className="text-muted-foreground mt-1 text-sm"> Chapters read per manga based on your matched Kenmei entries. </p> </div> </div> {hasData && ( <div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm"> <Badge variant="secondary" className="bg-emerald-500/15 text-emerald-600 dark:text-emerald-300" > Total: {stats.totalChapters.toLocaleString()} chapters </Badge> <Badge variant="secondary" className="bg-blue-500/15 text-blue-600 dark:text-blue-300" > Average: {stats.averageChapters.toFixed(1)} </Badge> <Badge variant="secondary" className="bg-purple-500/15 text-purple-600 dark:text-purple-300" > Median: {stats.medianChapters.toFixed(1)} </Badge> {stats.modeRange && ( <Badge variant="secondary" className="bg-amber-500/15 text-amber-600 dark:text-amber-300" > Mode: {stats.modeRange} </Badge> )} </div> )} </header> {hasData ? ( <figure className="h-[300px] w-full"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={bins} barCategoryGap={18}> <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" /> <XAxis dataKey="range" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} label={{ value: "Chapters Read", position: "insideBottom", offset: -6, className: "fill-current text-xs text-muted-foreground", }} /> <YAxis allowDecimals={false} tickLine={false} axisLine={false} tick={{ fontSize: 12 }} label={{ value: "Number of Manga", angle: -90, position: "insideLeft", offset: 10, className: "fill-current text-xs text-muted-foreground", }} /> <Tooltip contentStyle={{ backgroundColor: "hsl(var(--muted))", borderRadius: "calc(var(--radius) - 4px)", padding: "0.5rem 0.75rem", border: "none", }} labelStyle={{ color: "hsl(var(--muted-foreground))" }} formatter={(value: number) => `${Number(value).toLocaleString()} manga` } /> <Bar dataKey="count" fill="url(#chaptersGradient)" radius={[8, 8, 0, 0]} animationDuration={800} /> <defs> <linearGradient id="chaptersGradient" x1="0" x2="0" y1="0" y2="1" > <stop offset="0%" stopColor="#3b82f6" stopOpacity={0.95} /> <stop offset="100%" stopColor="#a855f7" stopOpacity={0.75} /> </linearGradient> </defs> </BarChart> </ResponsiveContainer> </figure> ) : ( <div className="text-muted-foreground flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 bg-slate-50/60 p-8 text-center dark:border-slate-800 dark:bg-slate-900/60"> <AlertCircle className="mb-3 h-6 w-6" aria-hidden="true" /> <p className="font-medium">No reading progress data available</p> <p className="mt-1 text-sm"> Chapters read are calculated from Kenmei data once matches are confirmed. </p> </div> )} </section> ); }); Copy
export const ChaptersReadDistributionChart: FC<ChaptersReadDistributionChartProps> = React.memo(function ChaptersReadDistributionChartMemo({ // eslint-disable-next-line react/prop-types matchResults, // eslint-disable-next-line react/prop-types className, }) { const { bins, stats } = useMemo( () => buildHistogram(matchResults), [matchResults], ); const hasData = bins.length > 0; return ( <section aria-label="Chapters read distribution" className={cn( "rounded-2xl border border-slate-200 bg-white/90 p-6 shadow-sm backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/90", className, )} > <header className="mb-4 flex flex-col gap-3"> <div className="flex items-center gap-2"> <span className="bg-linear-to-r inline-flex h-9 min-h-9 w-9 min-w-9 items-center justify-center rounded-full from-blue-500/20 via-purple-500/20 to-fuchsia-500/20 text-blue-500 dark:text-blue-300"> <BarChart2 className="h-4 w-4" aria-hidden="true" /> </span> <div> <h2 className="text-foreground text-lg font-semibold"> Reading Progress Distribution </h2> <p className="text-muted-foreground mt-1 text-sm"> Chapters read per manga based on your matched Kenmei entries. </p> </div> </div> {hasData && ( <div className="text-muted-foreground flex flex-wrap items-center gap-2 text-sm"> <Badge variant="secondary" className="bg-emerald-500/15 text-emerald-600 dark:text-emerald-300" > Total: {stats.totalChapters.toLocaleString()} chapters </Badge> <Badge variant="secondary" className="bg-blue-500/15 text-blue-600 dark:text-blue-300" > Average: {stats.averageChapters.toFixed(1)} </Badge> <Badge variant="secondary" className="bg-purple-500/15 text-purple-600 dark:text-purple-300" > Median: {stats.medianChapters.toFixed(1)} </Badge> {stats.modeRange && ( <Badge variant="secondary" className="bg-amber-500/15 text-amber-600 dark:text-amber-300" > Mode: {stats.modeRange} </Badge> )} </div> )} </header> {hasData ? ( <figure className="h-[300px] w-full"> <ResponsiveContainer width="100%" height="100%"> <BarChart data={bins} barCategoryGap={18}> <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" /> <XAxis dataKey="range" tickLine={false} axisLine={false} tick={{ fontSize: 12 }} label={{ value: "Chapters Read", position: "insideBottom", offset: -6, className: "fill-current text-xs text-muted-foreground", }} /> <YAxis allowDecimals={false} tickLine={false} axisLine={false} tick={{ fontSize: 12 }} label={{ value: "Number of Manga", angle: -90, position: "insideLeft", offset: 10, className: "fill-current text-xs text-muted-foreground", }} /> <Tooltip contentStyle={{ backgroundColor: "hsl(var(--muted))", borderRadius: "calc(var(--radius) - 4px)", padding: "0.5rem 0.75rem", border: "none", }} labelStyle={{ color: "hsl(var(--muted-foreground))" }} formatter={(value: number) => `${Number(value).toLocaleString()} manga` } /> <Bar dataKey="count" fill="url(#chaptersGradient)" radius={[8, 8, 0, 0]} animationDuration={800} /> <defs> <linearGradient id="chaptersGradient" x1="0" x2="0" y1="0" y2="1" > <stop offset="0%" stopColor="#3b82f6" stopOpacity={0.95} /> <stop offset="100%" stopColor="#a855f7" stopOpacity={0.75} /> </linearGradient> </defs> </BarChart> </ResponsiveContainer> </figure> ) : ( <div className="text-muted-foreground flex flex-col items-center justify-center rounded-xl border border-dashed border-slate-200 bg-slate-50/60 p-8 text-center dark:border-slate-800 dark:bg-slate-900/60"> <AlertCircle className="mb-3 h-6 w-6" aria-hidden="true" /> <p className="font-medium">No reading progress data available</p> <p className="mt-1 text-sm"> Chapters read are calculated from Kenmei data once matches are confirmed. </p> </div> )} </section> ); });
Component props.
Rendered chart component.
Chapters read distribution chart component.
Source