[LTスライド&原稿]Figma APIを使ってGitHubの芝っぽい活動履歴を作る

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/baf6e2c0-92d2-9d45-ad89-2d1d5063a533.jpeg)

この記事は2021年1月20日に開催された[Web API LT会](https://rakus.connpass.com/event/198587/)というイベントでのLTスライドと原稿（+コード）です。

## 自己紹介

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/fabe15e3-edd7-20e5-2ad2-1dd1b4664ab6.jpeg)

Figma APIを使ってGitHubの芝っぽい活動履歴を作る、と題してLTをします。

Increments株式会社で働いている綿貫佳祐です。

とってもざっくりした経歴はこんな感じで、エンジニアではなくデザイナーです。

connpassの概要に「エンジニア向けのイベントページです。」って書いてありましたが、今日は参加させていただきました。どうぞお手柔らかによろしくお願いします笑

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/25a9a6d5-63ba-2325-e7f1-345a227009bc.jpeg)

私の在籍しているIncrementsだとQiitaが一番有名なサービスで、今日この中にも使ってくださっている人がいるかもしれません。後はQiita TeamとQiita Jobsというプロダクトも作っていて、これら全般のデザインを担当しています。

## GitHubの“芝”ってありますよね

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/56b2d1f0-b81b-fd45-4589-5f5b03f19e16.jpeg)

早速本題に入って行くんですが、みなさんGitHubの「芝」ってありますよね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/57c7d7c4-bf5f-75a1-d37e-e270fbc8f480.jpeg)

これです。正しくはContributions calendarって言うらしいんですが、おそらくご存じの通り書いたコードの量や頻度が視覚化されているわけですよね。

これが絶対の指標ってこともないんですが、日々どれくらい開発しているかをざっくり把握するのには役立ちます。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/eea95abe-0280-c6fd-981d-b1af91a32ee8.jpeg)

それで、デザイナー版の芝もあったら良いなーってずっと思ってたんですよ。

## Figma APIって知ってますか？

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/b3e1b19f-f554-861e-5c59-d849b2196f15.jpeg)

そしてやっとタイトルのFigma APIの話なんですが、こちらも知ってるとか使ったことある人いますか？

自分の周りだとあんまり使い込んでる人はいないかなーと思うんですが、結構色々やれるんですよ。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/c04f5694-c0fb-f33a-6572-87854cc8ea8e.jpeg)

例えばコマンドラインからFigmaデータにコメントを入れる、とかも一応可能なんですね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/546849ee-e607-ceb0-7116-7bcf449e6616.jpeg)

で、APIのページを色々読んでたら、「コレ、Figmaの活動履歴で芝生やせるんじゃね？」って思ったので作ってみました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/8d1285f7-a796-6be9-76a9-ac990ff7f7f2.jpeg)

こんな感じです。

思惑通り視覚化出来たんですが……スカスカなのが明確になっちゃいました笑

## 概要

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/73b6a712-d057-fb54-be0e-7040d0a22838.jpeg)

全体像をかいつまんで説明すると、Figmaのデータって有料プランなら過去のバージョン履歴が全て保存されているんですね。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/2edb5e3c-ae59-b4e8-e4d9-7212090e13c7.jpeg)

それで、APIを叩いて取得出来る情報の中に「各バージョン情報のcreated_at」があります。

GitでcommitするとDateも保存されているのをイメージしてください。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/4c7c554b-35f3-8a5c-662d-eb9cc0a10ae2.jpeg)

そんなこんなでサイトをビルドするときにFigmaのAPIを叩いてレスポンスを受け取り、スタイリングを施してグラフ化しているという寸法です。

## 詳細

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/473e37a5-d6d9-c2ac-a671-a2d0483e2d11.jpeg)

もう少し詳細に話していきます。

Figmaのデータの構造はTeam → Project → Fileと階層になっていて、さらに各Fileの中にVersion Historyが埋め込まれています。

Incrementsのデータでいうと Increments → Qiita → DesktopやMobileのモックアップがある、といった具合です。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/a586f429-3460-20cd-3240-473b2065244a.jpeg)

Figma APIでもこの流れに沿ってデータを取得するイメージで、Team情報のエンドポイントからAPIにアクセスするとProjectの一覧が手に入ります。この中にはProject名やidなどが格納されています。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/5a8a1387-d1b6-367d-1392-858c7517097e.jpeg)

そしてidを使って各ProjectのエンドポイントからAPIにアクセス。すると今度はFile一覧が手に入る。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/f012cde6-4c3e-c37a-cd12-c02f9ec06976.jpeg)

後はもうお分かりかもしれませんが手に入れたFile情報からVersion Historyを取得するわけです。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/43b7e16f-2e88-5e14-5eed-3b8cc295c888.jpeg)

自分のTeam IDさえあればProject→File→Version Historyと段階を踏むことで全ての活動履歴を取得が出来るので、あとはそれを良い感じに加工してスタイリングすれば完成って寸法です。

## おわりに

かなり駆け足になってしまいましたが作ったものについては以上です。

自分はGatsbyを使ってこのサイトを作っていますが、ぶっちゃけfetchとループ処理くらいしかしていないので言語とかフレーム問わず作れるんじゃないかなと思います。

実際のコードを載せておきます。
（ただし私自身かなり手探りで実装しており、クソコードを晒しているだろうと自覚しています。もし参考にする際はご注意ください。）

```javascript:gatsby-node.js
const fetch = require(`node-fetch`)

exports.sourceNodes = async ({
  actions: { createNode },
  createContentDigest,
}) => {
  const token = { 'X-FIGMA-TOKEN': process.env.GATSBY_FIGMA_TOKEN }
  const team = await fetch(`https://api.figma.com/v1/teams/${process.env.GATSBY_FIGMA_TEAM_ID}/projects`, { headers: token })
  const result = await team.json()
  const projects = await result.projects
  const files = await Promise.all(
    projects.map(async (project) => {
      const response = await fetch(`https://api.figma.com/v1/projects/${project.id}/files`, { headers: token })
      const result = await response.json()
      return result.files
    })
  )
  const versions = await Promise.all(
    files.flat().map(async (file) => {
      const response = await fetch(`https://api.figma.com/v1/files/${file.key}/versions`, { headers: token })
      const result = await response.json()
      return result.versions
    })
  )

  const allVersions = versions.flat()

  createNode({
    id: `figma`,
    parent: null,
    children: [],
    content: allVersions,
    internal: {
      type: `Figma`,
      contentDigest: createContentDigest(allVersions),
    },
  })
}
```

```jsx:components/FigmaActivity.js
import React from 'react'
import { useStaticQuery, graphql } from 'gatsby'

import { css } from '@emotion/react'
import styled from '@emotion/styled'
import { getSize } from '../styles/Size'
import hexToRgba from 'hex-rgba'
import { color, typography } from '../styles/Theme'

const wrap = css`
  display: flex;
  justify-content: center;
  grid-column: span 12;
  @media (max-width: 848px) {
    grid-column: 1 / -1;
    overflow: scroll;
  }
`

const root = css`
  align-content: center;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  height: ${getSize(28.5)};
  justify-content: flex-start;
  list-style: none;
  width: 100%;
  @media (max-width: 848px) {
    align-content: flex-start;
  }
`

const Chart = styled.li`
  background-color: ${(props) => (props.value ? hexToRgba(color.blue[60], (props.value / props.max) * 100) : color.gray[0])};
  border: 1px solid ${color.divider.onSurface};
  border-radius: ${getSize(0.5)};
  height: ${getSize(3)};
  margin: ${getSize(0.5)};
  position: relative;
  width: ${getSize(3)};
`

const ChartText = styled.span`
  display: none;
  ${Chart}:hover & {
    background-color: ${color.gray[90]};
    border-radius: ${getSize(1)};
    display: flex;
    left: 50%;
    padding: ${getSize(2)} ${getSize(4)};
    position: absolute;
    top: ${getSize(-12)};
    transform: translateX(-50%);
    z-index: 1;
    ${typography.body3};
    &::after {
      border-left: ${getSize(2.5)} solid transparent;
      border-right: ${getSize(2.5)} solid transparent;
      border-top: ${getSize(2)} solid ${color.gray[90]};
      bottom: ${getSize(-2)};
      content: '';
      left: 50%;
      position: absolute;
      transform: translateX(-50%);
    }
  }
`

const chartValue = css`
  color: ${color.text.onBackground.highEmphasis};
  font-weight: 600;
  white-space: nowrap;
`

const chartDate = css`
  color: ${color.text.onBackground.mediumEmphasis};
  margin-left: ${getSize(2)};
  white-space: nowrap;
`

export const FigmaActivity = ({ ...props }) => {
  const data = useStaticQuery(graphql`
    {
      allFigma {
        nodes {
          content {
            created_at
          }
        }
      }
    }
  `)

  const contents = data.allFigma.nodes[0].content
  const versionsCreatedAt = contents.flat().map(
    (content) => content.created_at.slice(0, 10) //先頭から10文字を取得することで「時間」を切り捨てて1日ずつのデータにしている
  )
  // data.allFigma.nodes より下のデータが何故か配列が何度も入れ子になっているのでflat()などを使って取り出している

  const days = 365
  const today = new Date()
  const period = [...Array(days)].map(() => new Intl.DateTimeFormat('sv-SE', { timeZone: 'UTC' }).format(today.setDate(today.getDate() - 1)))
  // yyyy-mm-dd形式にできるのが'sv-SE'という形式だったのでIntl.DateTimeFormatでは'ja-JP'ではなく'sv-SE'を使う

  const concatedversionsCreatedAt = period.concat(versionsCreatedAt)
  // versionsCreatedAtにはversion historyが記録されている日だけが格納されているので、periodと合成して全ての日のリストを作成する

  const allContributes = []
  concatedversionsCreatedAt.map((versionCreatedAt) => (allContributes[versionCreatedAt] = allContributes[versionCreatedAt] ? allContributes[versionCreatedAt] + 1 : 1))
  // 日毎の、1回以上version historyを記録した回数の配列

  const contributes = Object.entries(allContributes).sort().slice(-days)
  // 日付が昇順にも降順にもなっていないのでソートした後スライス

  const counter = []
  contributes.map(([_, value]) => counter.push(value - 1))
  const max = Math.max(...counter)
  // contributesからスコアだけ抜き出した配列を作った後、最大値を取得

  const charts = contributes.map(([key, value]) => (
    <Chart key={key} value={value - 1} max={max}>
      <ChartText>
        <span css={chartValue}>
          {value - 1} {value <= 1 ? 'contribution' : 'contributions'}
        </span>
        <span css={chartDate}>on {key}</span>
      </ChartText>
    </Chart>
  ))

  return (
    <div css={wrap} className={props.className}>
      <ol css={root}>{charts}</ol>
    </div>
  )
}
```

以上で私のLTを終わります、ありがとうございました。