Created
August 8, 2025 09:55
-
-
Save thepratikguptaa/15555676d8e4d9e9c3deeb81afff947d to your computer and use it in GitHub Desktop.
GitHub Chart used in my portfolio
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 * 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