Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save thepratikguptaa/15555676d8e4d9e9c3deeb81afff947d to your computer and use it in GitHub Desktop.

Select an option

Save thepratikguptaa/15555676d8e4d9e9c3deeb81afff947d to your computer and use it in GitHub Desktop.
GitHub Chart used in my portfolio
"use client"
import * as React from "react"
export function LineChart({
data,
height = 200,
stroke = "#22c55e",
label = "Last 31 days",
}: {
data: { date: string; count: number }[]
height?: number
stroke?: string
label?: string
}) {
const [hoveredPoint, setHoveredPoint] = React.useState<number | null>(null)
if (!data.length) return null
// Responsive dimensions
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
const width = isMobile ? Math.max(350, data.length * 12) : Math.max(600, data.length * 20)
const chartHeight = isMobile ? 160 : height
const max = Math.max(1, ...data.map((d) => d.count))
// Increased top padding to prevent tooltip cutoff
const padding = isMobile
? { top: 40, right: 25, bottom: 30, left: 30 }
: { top: 50, right: 40, bottom: 40, left: 40 }
const innerW = width - padding.left - padding.right
const innerH = chartHeight - padding.top - padding.bottom
const points = data.map((d, i) => {
const x = padding.left + (i / Math.max(1, data.length - 1)) * innerW
const y = padding.top + innerH - (d.count / max) * innerH
return { x, y, count: d.count, date: d.date }
})
const pathData = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
const areaData = `${pathData} L ${points[points.length - 1]?.x || 0} ${chartHeight - padding.bottom} L ${padding.left} ${chartHeight - padding.bottom} Z`
return (
<div className="w-full space-y-3 md:space-y-4">
<div className="space-y-2">
<h3 className="text-lg md:text-xl font-bold">GitHub Activity Graph</h3>
<p className="text-xs md:text-sm text-muted-foreground">
A dynamically generated activity graph to show my GitHub activities of last 31 days.
</p>
</div>
<div className="overflow-x-auto border rounded-lg p-2 md:p-4 bg-card">
<svg width={width} height={chartHeight} className="w-full">
{/* Grid lines */}
{[0, 0.25, 0.5, 0.75, 1].map((ratio) => {
const y = padding.top + innerH - ratio * innerH
return (
<line
key={ratio}
x1={padding.left}
y1={y}
x2={width - padding.right}
y2={y}
stroke="currentColor"
strokeOpacity={0.1}
strokeDasharray="2,2"
/>
)
})}
{/* Area fill */}
<path
d={areaData}
fill={stroke}
fillOpacity={hoveredPoint !== null ? 0.2 : 0.1}
className="transition-all duration-300"
/>
{/* Line */}
<path
d={pathData}
fill="none"
stroke={stroke}
strokeWidth={hoveredPoint !== null ? (isMobile ? 2 : 3) : (isMobile ? 1.5 : 2)}
strokeLinejoin="round"
strokeLinecap="round"
className="transition-all duration-300"
/>
{/* Data points */}
{points.map((point, i) => (
<circle
key={i}
cx={point.x}
cy={point.y}
r={hoveredPoint === i ? (isMobile ? 4 : 6) : (isMobile ? 2 : 3)}
fill={stroke}
className="transition-all duration-200 cursor-pointer hover:drop-shadow-lg"
onMouseEnter={() => setHoveredPoint(i)}
onMouseLeave={() => setHoveredPoint(null)}
onTouchStart={() => setHoveredPoint(i)}
onTouchEnd={() => setTimeout(() => setHoveredPoint(null), 2000)}
>
<title>{`${point.count} contributions on ${new Date(point.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}`}</title>
</circle>
))}
{/* Smart tooltip positioning to prevent cutoff */}
{hoveredPoint !== null && (
<g>
{(() => {
const point = points[hoveredPoint]
const tooltipWidth = isMobile ? 60 : 80
const tooltipHeight = isMobile ? 20 : 25
// Calculate optimal tooltip position
let tooltipX = point.x - tooltipWidth / 2
let tooltipY = point.y - (isMobile ? 30 : 35)
// Prevent left edge cutoff
if (tooltipX < 5) tooltipX = 5
// Prevent right edge cutoff
if (tooltipX + tooltipWidth > width - 5) {
tooltipX = width - tooltipWidth - 5
}
// Prevent top cutoff - move tooltip below point if necessary
if (tooltipY < 5) {
tooltipY = point.y + (isMobile ? 15 : 20)
}
return (
<>
<rect
x={tooltipX}
y={tooltipY}
width={tooltipWidth}
height={tooltipHeight}
fill="rgba(0,0,0,0.9)"
rx={4}
className="animate-in fade-in duration-200"
style={{ filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))' }}
/>
<text
x={tooltipX + tooltipWidth / 2}
y={tooltipY + tooltipHeight / 2 + 3}
fontSize={isMobile ? "10" : "12"}
textAnchor="middle"
fill="white"
className="font-medium"
>
{point.count} commits
</text>
</>
)
})()}
</g>
)}
{/* Y-axis labels - responsive */}
{[0, Math.ceil(max * 0.5), max].map((value) => {
const y = padding.top + innerH - (value / max) * innerH
return (
<text
key={value}
x={padding.left - 5}
y={y + 3}
fontSize={isMobile ? "10" : "12"}
textAnchor="end"
className="fill-muted-foreground"
>
{value}
</text>
)
})}
{/* X-axis */}
<line
x1={padding.left}
y1={chartHeight - padding.bottom}
x2={width - padding.right}
y2={chartHeight - padding.bottom}
stroke="currentColor"
strokeOpacity={0.2}
/>
</svg>
</div>
</div>
)
}
export function BarChart({
data,
height = 220,
barColor = "#22c55e",
label = "By weekday",
}: {
data: number[]
height?: number
barColor?: string
label?: string
}) {
const [hoveredBar, setHoveredBar] = React.useState<number | null>(null)
const [clickedBar, setClickedBar] = React.useState<number | null>(null)
// Responsive dimensions
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
const width = isMobile ? 350 : 500
const chartHeight = isMobile ? 180 : height
const padding = isMobile
? { top: 15, right: 25, bottom: 45, left: 30 }
: { top: 20, right: 40, bottom: 60, left: 40 }
const innerW = width - padding.left - padding.right
const innerH = chartHeight - padding.top - padding.bottom
const max = Math.max(1, ...data)
const barWidth = (innerW / data.length) - (isMobile ? 8 : 16)
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
const dayAbbr = isMobile ? ["S", "M", "T", "W", "T", "F", "S"] : ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
const getBarColor = (index: number, value: number) => {
if (clickedBar === index) return "#16a34a"
if (hoveredBar === index) return "#22c55e"
if (value === 0) return "#374151"
return barColor
}
const getBarOpacity = (index: number) => {
if (hoveredBar !== null && hoveredBar !== index) return 0.6
return 1
}
return (
<div className="w-full space-y-3 md:space-y-4">
<div className="space-y-2">
<h3 className="text-lg md:text-xl font-bold">My Productivity By Day Of The Week</h3>
<p className="text-xs md:text-sm text-muted-foreground">
A visual representation of my productivity based on the number of contributions made on each day of the week.
{clickedBar !== null && (
<span className="block mt-1 font-medium text-primary text-xs md:text-sm">
Selected: {days[clickedBar]} - {data[clickedBar]} contributions
</span>
)}
</p>
</div>
<div className="overflow-x-auto border rounded-lg p-2 md:p-4 bg-card">
<svg width={width} height={chartHeight}>
{/* Grid lines */}
{[0, 0.25, 0.5, 0.75, 1].map((ratio) => {
const y = padding.top + innerH - ratio * innerH
return (
<line
key={ratio}
x1={padding.left}
y1={y}
x2={width - padding.right}
y2={y}
stroke="currentColor"
strokeOpacity={0.1}
strokeDasharray="2,2"
/>
)
})}
{/* Bars */}
{data.map((value, i) => {
const barHeight = (value / max) * innerH
const x = padding.left + i * (barWidth + (isMobile ? 8 : 16)) + (isMobile ? 4 : 8)
const y = padding.top + innerH - barHeight
const isHovered = hoveredBar === i
const isClicked = clickedBar === i
return (
<g key={i}>
{/* Bar shadow for depth */}
<rect
x={x + 1}
y={y + 1}
width={barWidth}
height={barHeight}
fill="rgba(0,0,0,0.1)"
rx={isMobile ? 2 : 4}
/>
{/* Main bar */}
<rect
x={x}
y={y}
width={barWidth}
height={barHeight}
fill={getBarColor(i, value)}
opacity={getBarOpacity(i)}
rx={isMobile ? 2 : 4}
className="cursor-pointer transition-all duration-300 hover:drop-shadow-lg"
style={{
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)',
filter: isClicked ? 'brightness(1.1)' : 'brightness(1)'
}}
onMouseEnter={() => setHoveredBar(i)}
onMouseLeave={() => setHoveredBar(null)}
onTouchStart={() => setHoveredBar(i)}
onTouchEnd={() => setTimeout(() => setHoveredBar(null), 2000)}
onClick={() => setClickedBar(clickedBar === i ? null : i)}
>
<title>{`${days[i]}: ${value} contributions`}</title>
</rect>
{/* Value labels on bars - responsive */}
{value > 0 && (
<text
x={x + barWidth / 2}
y={y - (isMobile ? 4 : 8)}
fontSize={isMobile ? (isHovered ? "11" : "10") : (isHovered ? "14" : "12")}
fontWeight={isHovered ? "bold" : "normal"}
textAnchor="middle"
className="fill-muted-foreground transition-all duration-200"
style={{
transform: isHovered ? 'translateY(-1px)' : 'translateY(0)'
}}
>
{value}
</text>
)}
{/* Day labels - responsive */}
<text
x={x + barWidth / 2}
y={chartHeight - padding.bottom + (isMobile ? 12 : 15)}
fontSize={isMobile ? (isHovered ? "11" : "10") : (isHovered ? "13" : "12")}
fontWeight={isHovered ? "bold" : "normal"}
textAnchor="middle"
className="fill-muted-foreground transition-all duration-200"
>
{dayAbbr[i]}
</text>
{/* Hover indicator line */}
{isHovered && (
<line
x1={x + barWidth / 2}
y1={padding.top}
x2={x + barWidth / 2}
y2={chartHeight - padding.bottom}
stroke={barColor}
strokeWidth={1}
strokeDasharray="3,3"
opacity={0.5}
className="animate-in fade-in duration-200"
/>
)}
</g>
)
})}
{/* Y-axis labels - responsive */}
{[0, Math.ceil(max * 0.5), max].map((value) => {
const y = padding.top + innerH - (value / max) * innerH
return (
<text
key={value}
x={padding.left - 5}
y={y + 3}
fontSize={isMobile ? "10" : "12"}
textAnchor="end"
className="fill-muted-foreground"
>
{value}
</text>
)
})}
{/* X-axis */}
<line
x1={padding.left}
y1={chartHeight - padding.bottom}
x2={width - padding.right}
y2={chartHeight - padding.bottom}
stroke="currentColor"
strokeOpacity={0.2}
/>
</svg>
{/* Interactive legend - responsive */}
<div className="mt-3 md:mt-4 flex justify-center">
<div className="text-xs text-muted-foreground text-center">
{isMobile ? (
<div>
<div>Tap bars to select</div>
{hoveredBar !== null && <div className="mt-1">Selected: {days[hoveredBar]}</div>}
</div>
) : (
<div>
Hover over bars to highlight • Click to select • {hoveredBar !== null ? `Hovering: ${days[hoveredBar]}` : 'Hover to explore'}
</div>
)}
</div>
</div>
</div>
</div>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment