Skip to content

Commit

Permalink
feat(suspensive.org): add bubble chart for contributors page (#1043)
Browse files Browse the repository at this point in the history
# 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
saul-atomrigs and manudeli authored Jul 7, 2024
1 parent 21f1c45 commit 3d3e573
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 7 deletions.
2 changes: 2 additions & 0 deletions docs/suspensive.org/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"dependencies": {
"@codesandbox/sandpack-react": "^2.14.4",
"@codesandbox/sandpack-themes": "^2.0.21",
"d3": "^7.9.0",
"next": "^14.2.3",
"nextra": "^2.13.4",
"nextra-theme-docs": "^2.13.4",
Expand All @@ -36,6 +37,7 @@
"@next/eslint-plugin-next": "^14.2.3",
"@suspensive/eslint-config": "workspace:*",
"@suspensive/tsconfig": "workspace:*",
"@types/d3": "^7.4.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"autoprefixer": "^10.4.19",
Expand Down
98 changes: 98 additions & 0 deletions docs/suspensive.org/src/components/BubbleChart.tsx
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} />
}
1 change: 1 addition & 0 deletions docs/suspensive.org/src/components/index.ts
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'
66 changes: 66 additions & 0 deletions docs/suspensive.org/src/lib/hooks/useFetchContributors.ts
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
3 changes: 2 additions & 1 deletion docs/suspensive.org/src/pages/docs/contributors.en.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Cards, Card } from 'nextra/components'
import Image from 'next/image'
import { BubbleChart } from '../../components'

# Contributors

Thank you to everyone who contributed to Suspensive and we look forward to your continued support.

[![contributors](https://contrib.rocks/image?repo=toss/suspensive)](https://github.com/toss/suspensive/graphs/contributors)
<BubbleChart />

## Maintainers

Expand Down
3 changes: 2 additions & 1 deletion docs/suspensive.org/src/pages/docs/contributors.ko.mdx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Cards, Card } from 'nextra/components'
import Image from 'next/image'
import { BubbleChart } from '../../components'

# 기여자

Suspensive에 기여해주신 모든 분들 감사하고 앞으로도 잘 부탁드립니다.

[![contributors](https://contrib.rocks/image?repo=toss/suspensive)](https://github.com/toss/suspensive/graphs/contributors)
<BubbleChart />

## 메인테이너

Expand Down
33 changes: 33 additions & 0 deletions docs/suspensive.org/src/types/index.ts
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[]
}
Loading

0 comments on commit 3d3e573

Please sign in to comment.