-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(suspensive.org): add bubble chart for contributors page (#1043)
# Overview <!-- A clear and concise description of what this pr is about. --> I found the bubble chart idea for the contributors page from here #1012 really interesting, so I decided to give it a try and here's how it looks. Clicking on the bubble will redirect to each contributor's github page. ![Captura de pantalla 2024-07-07 a las 9 59 00 p m](https://github.com/toss/suspensive/assets/82362278/20646b0e-1ea2-45c5-b75a-c22275822b50) ## PR Checklist - [✅] I did below actions if need 1. I read the [Contributing Guide](https://github.com/toss/suspensive/blob/main/CONTRIBUTING.md) 2. I added documents and tests. --------- Co-authored-by: Jonghyeon Ko <jonghyeon@toss.im>
- Loading branch information
1 parent
21f1c45
commit 3d3e573
Showing
8 changed files
with
413 additions
and
7 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import * as d3 from 'd3' | ||
import React, { useEffect, useRef } from 'react' | ||
import useFetchContributors from '../lib/hooks/useFetchContributors' | ||
|
||
type Node = { | ||
name: string | ||
value: number | ||
avatar: string | ||
htmlUrl: string | ||
children?: Node[] | ||
} | ||
|
||
export const BubbleChart = () => { | ||
const result = useFetchContributors() | ||
|
||
if (!result.isSuccess) { | ||
return null | ||
} | ||
|
||
const chartData = result.data.map((contributor) => ({ | ||
name: contributor.author.login, | ||
value: contributor.total, | ||
avatar: contributor.author.avatar_url, | ||
htmlUrl: contributor.author.html_url, | ||
})) | ||
|
||
return ( | ||
<> | ||
<div className="flex w-[100%] items-center justify-center overflow-visible sm:hidden md:hidden lg:hidden"> | ||
<BubbleChartSize chartData={chartData} height={400} width={400} padding={2} /> | ||
</div> | ||
<div className="hidden w-[100%] items-center justify-center overflow-visible sm:flex md:hidden lg:hidden"> | ||
<BubbleChartSize chartData={chartData} height={630} width={630} padding={6} /> | ||
</div> | ||
<div className="hidden w-[100%] items-center justify-center overflow-visible sm:hidden md:flex lg:hidden"> | ||
<BubbleChartSize chartData={chartData} height={560} width={560} padding={4} /> | ||
</div> | ||
<div className="hidden w-[100%] items-center justify-center overflow-visible sm:hidden md:hidden lg:flex"> | ||
<BubbleChartSize chartData={chartData} height={760} width={760} padding={8} /> | ||
</div> | ||
</> | ||
) | ||
} | ||
|
||
const BubbleChartSize = (props: { chartData: Array<Node>; width: number; height: number; padding: number }) => { | ||
const svgRef = useRef<SVGSVGElement | null>(null) | ||
|
||
useEffect(() => { | ||
if (!props.chartData.length) return | ||
|
||
const root = d3 | ||
.hierarchy({ children: props.chartData } as Node) | ||
.sum((d: Node) => (d.value < 100 ? d.value : 100) + 3) | ||
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0)) | ||
|
||
const pack = d3.pack().size([props.width, props.height]).padding(props.padding) | ||
|
||
const nodes = pack(root).descendants().slice(1) | ||
|
||
const svg = d3 | ||
.select(svgRef.current) | ||
.attr('width', props.width) | ||
.attr('height', props.height) | ||
.style('display', 'block') | ||
.style('margin', 'auto') | ||
.style('overflow', 'visible') | ||
|
||
const nodeGroups = svg | ||
.selectAll('g') | ||
.data(nodes) | ||
.enter() | ||
.append('g') | ||
.attr('transform', (d) => `translate(${d.x},${d.y})`) | ||
.style('cursor', 'pointer') | ||
.on('click', (event, d: d3.HierarchyCircularNode<Node>) => { | ||
if (d.data.htmlUrl) { | ||
window.location.href = d.data.htmlUrl | ||
} | ||
}) | ||
.on('mouseover', function () { | ||
d3.select(this).select('image').style('opacity', 0.8) | ||
}) | ||
.on('mouseout', function () { | ||
d3.select(this).select('image').style('opacity', 1) | ||
}) | ||
|
||
nodeGroups | ||
.append('image') | ||
.attr('x', (d) => -d.r * 1) | ||
.attr('y', (d) => -d.r * 1) | ||
.attr('width', (d) => d.r * 2) | ||
.attr('height', (d) => d.r * 2) | ||
.attr('href', (d: d3.HierarchyCircularNode<Node>) => d.data.avatar) | ||
.attr('clip-path', (d) => `circle(${d.r * 1}px at ${d.r * 1}px ${d.r * 1}px)`) | ||
}, [props.chartData, props.height, props.width]) | ||
|
||
return <svg ref={svgRef} /> | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export { Callout } from './Callout' | ||
export { HomePage } from './HomePage' | ||
export { Sandpack } from './Sandpack' | ||
export { BubbleChart } from './BubbleChart' |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { useEffect, useState } from 'react' | ||
import type { GithubRepoContributor } from '../../types' | ||
|
||
type Result = | ||
| { | ||
isLoading: false | ||
isSuccess: true | ||
isError: false | ||
data: GithubRepoContributor[] | ||
error: undefined | ||
} | ||
| { | ||
isLoading: false | ||
isSuccess: false | ||
isError: true | ||
data: undefined | ||
error: Error | ||
} | ||
| { | ||
isLoading: true | ||
isSuccess: false | ||
isError: false | ||
data: undefined | ||
error: undefined | ||
} | ||
|
||
const useFetchContributors = () => { | ||
const [result, setResult] = useState<Result>({ | ||
isSuccess: false, | ||
data: undefined, | ||
error: undefined, | ||
isError: false, | ||
isLoading: true, | ||
}) | ||
|
||
useEffect(() => { | ||
async function fetchContributors(): Promise<void> { | ||
try { | ||
const response = await fetch('https://api.github.com/repos/toss/suspensive/stats/contributors') | ||
if (!response.ok) { | ||
throw new Error('Failed to fetch contributors') | ||
} | ||
const data: GithubRepoContributor[] = (await response.json()) as GithubRepoContributor[] | ||
const filteredContributors = data.filter((contributor) => { | ||
const login = contributor.author.login | ||
return !['github-actions[bot]', 'dependabot[bot]', 'renovate[bot]'].includes(login) | ||
}) | ||
setResult({ | ||
data: filteredContributors, | ||
error: undefined, | ||
isError: false, | ||
isLoading: false, | ||
isSuccess: true, | ||
}) | ||
} catch (error) { | ||
console.error('Error fetching contributors:', error) | ||
} | ||
} | ||
|
||
void fetchContributors() | ||
}, []) | ||
|
||
return result | ||
} | ||
|
||
export default useFetchContributors |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
interface Week { | ||
w: number // Week timestamp in epoch seconds | ||
a: number // Number of additions | ||
d: number // Number of deletions | ||
c: number // Number of commits | ||
} | ||
|
||
interface Author { | ||
login: string | ||
id: number | ||
node_id: string | ||
avatar_url: string | ||
gravatar_id: string | ||
url: string | ||
html_url: string | ||
followers_url: string | ||
following_url: string | ||
gists_url: string | ||
starred_url: string | ||
subscriptions_url: string | ||
organizations_url: string | ||
repos_url: string | ||
events_url: string | ||
received_events_url: string | ||
type: string | ||
site_admin: boolean | ||
} | ||
|
||
export interface GithubRepoContributor { | ||
author: Author | ||
total: number | ||
weeks: Week[] | ||
} |
Oops, something went wrong.