Last active
March 31, 2025 21:56
-
-
Save youngbloodcyb/59c9a0d931d791ac10ac4bf3f5b3a833 to your computer and use it in GitHub Desktop.
Histogram component using shadcn/ui + recharts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client" | |
import { useState, useEffect } from "react" | |
import { | |
Bar, | |
BarChart, | |
CartesianGrid, | |
ResponsiveContainer, | |
Tooltip, | |
XAxis, | |
YAxis, | |
} from "recharts" | |
import { | |
Card, | |
CardContent, | |
CardDescription, | |
CardHeader, | |
CardTitle, | |
} from "@/components/ui/card" | |
import { ChartContainer } from "@/components/ui/chart" | |
import { ScrollArea } from "@/components/ui/scroll-area" | |
// Update the interface | |
interface DataPoint { | |
key: string | |
value: number | null | |
} | |
// Update the tooltip component | |
const CustomTooltip = ({ active, payload }: any) => { | |
if (active && payload && payload.length) { | |
const data = payload[0].payload | |
return ( | |
<div className="bg-background border rounded-lg shadow-md p-4 max-w-xs"> | |
<h3 className="font-medium mb-2">Score Range: {data.range}</h3> | |
<p className="text-sm text-muted-foreground mb-2"> | |
Count: {data.count} | |
</p> | |
<div className="mt-2"> | |
<h4 className="text-sm font-medium mb-1">Items:</h4> | |
<ScrollArea className="h-[200px] w-full"> | |
<ul className="text-xs space-y-1"> | |
{data.items.map((item: string, index: number) => ( | |
<li key={index} className="flex"> | |
<span className="text-primary">{item}</span> | |
<span className="ml-auto text-muted-foreground"> | |
{data.itemValues[item]} | |
</span> | |
</li> | |
))} | |
</ul> | |
</ScrollArea> | |
</div> | |
</div> | |
) | |
} | |
return null | |
} | |
// Update the main component to accept props | |
interface HistogramProps { | |
data: DataPoint[] | |
title?: string | |
description?: string | |
} | |
export default function Histogram({ | |
data, | |
title = "Distribution", | |
description, | |
}: HistogramProps) { | |
const [histogramData, setHistogramData] = useState<any[]>([]) | |
useEffect(() => { | |
processData(data) | |
}, [data]) | |
const processData = (data: DataPoint[]) => { | |
// Filter out null values | |
const validData = data.filter((item) => item.value !== null) | |
// Find min and max values | |
const values = validData.map((item) => item.value as number) | |
const minValue = Math.min(...values) | |
const maxValue = Math.max(...values) | |
// Create buckets (10 point ranges) | |
const bucketSize = 10 | |
const bucketCount = Math.ceil((maxValue - minValue) / bucketSize) + 1 | |
// Initialize buckets | |
const buckets = Array(bucketCount) | |
.fill(0) | |
.map((_, i) => { | |
const bucketMin = | |
Math.floor(minValue / bucketSize) * bucketSize + i * bucketSize | |
const bucketMax = bucketMin + bucketSize - 1 | |
return { | |
range: `${bucketMin}-${bucketMax}`, | |
bucketMin, | |
bucketMax, | |
count: 0, | |
items: [] as string[], | |
itemValues: {} as Record<string, number>, | |
} | |
}) | |
// Fill buckets with data | |
validData.forEach((item) => { | |
const value = item.value as number | |
const bucketIndex = Math.floor( | |
(value - buckets[0].bucketMin) / bucketSize | |
) | |
if (bucketIndex >= 0 && bucketIndex < buckets.length) { | |
buckets[bucketIndex].count++ | |
buckets[bucketIndex].items.push(item.key) | |
buckets[bucketIndex].itemValues[item.key] = value | |
} | |
}) | |
setHistogramData(buckets) | |
} | |
return ( | |
<Card className="w-full"> | |
<CardHeader> | |
<CardTitle>{title}</CardTitle> | |
{description && <CardDescription>{description}</CardDescription>} | |
</CardHeader> | |
<CardContent> | |
<ChartContainer | |
config={{ | |
count: { | |
label: "Count", | |
color: "hsl(var(--primary))", | |
}, | |
}} | |
className="h-[400px]" | |
> | |
<ResponsiveContainer width="100%" height="100%"> | |
<BarChart | |
data={histogramData} | |
margin={{ | |
top: 5, | |
right: 30, | |
left: 20, | |
bottom: 60, | |
}} | |
> | |
<CartesianGrid strokeDasharray="3 3" /> | |
<XAxis | |
dataKey="range" | |
angle={-45} | |
textAnchor="end" | |
height={60} | |
label={{ | |
value: "Value Range", | |
position: "insideBottom", | |
offset: -10, | |
}} | |
/> | |
<YAxis | |
label={{ | |
value: "Count", | |
angle: -90, | |
position: "insideLeft", | |
}} | |
/> | |
<Tooltip content={<CustomTooltip />} /> | |
<Bar | |
dataKey="count" | |
fill="var(--color-count)" | |
radius={[4, 4, 0, 0]} | |
isAnimationActive={true} | |
animationDuration={500} | |
/> | |
</BarChart> | |
</ResponsiveContainer> | |
</ChartContainer> | |
</CardContent> | |
</Card> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment