ホットスポットプロファイルの可視化を GitHub Actions で GitHub Pages にデプロイしたメモ

# 概要
[前回](https://qiita.com/hibohiboo/items/bba79362884bccaa27d7)はホットスポットプロファイルの可視化を行った。
既に[GitHubPages](https://qiita.com/hibohiboo/items/efff5fe687076ab44561)があるので、GitHubActionsを使ってデプロイする。

[デプロイしたページ](https://hibohiboo.github.io/odyssage/crime-scene-hotspots/)

[ソースコード](https://github.com/hibohiboo/odyssage/blob/84d66619f6b7b8b60ddb296da85804f73653482d/.github/workflows/github-pages.yml)

# ディレクトリ構造

GitHub Pagesの作成にはastroを使っている。
今回は、astroをビルドしたフォルダにGitHub Actionsを使ってhotspot.jsonを追加する仕組みとする。

```diff
- docs
  - astro
    - dist ← astroをビルドして生成されるフォルダ。このフォルダをGitHub Pagesとしてデプロイする
      - crimes-scene-hotspots ← publicからコピーされたディレクトリ
        - index.html 
+       - hotspots.json ← github actionsで生成するJSONファイル
    - public
      - crimes-scene-hotspots
        - index.html 
```


# ソースコード

## GitHub Actions
hotspotプロファイルの作成を除いた部分は割愛している。

```yaml:.github/workflows/github-pages.yml
# 省略
env:
  BUILD_PATH: './docs/astro' 
jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          # デフォルトでは 1 に設定されており、最新のコミット履歴しか参照できない
          # 0 に変更することで、リポジトリの完全なコミット履歴を参照できるようになる
          fetch-depth: 0    
      # 省略 
      - name: Build with Astro
        run: |
          npm run astro build \
            --site "${{ steps.pages.outputs.origin }}" \
            --base "${{ steps.pages.outputs.base_path }}"
        working-directory: ${{ env.BUILD_PATH }}
      
      - name: Set up JDK 21
        uses: actions/setup-java@v4.5.0
        with:
          distribution: 'temurin'
          java-version: '21'

      - name: Download Code Maat
        run: wget https://github.com/adamtornhill/code-maat/releases/download/v1.0.4/code-maat-1.0.4-standalone.jar
        working-directory: ./docs/code-maat

      - name: create git log 
        run: git log --all --numstat --date=short --pretty=format:'--%h--%ad--%aN' --no-renames --after=2023-01-01 > ./logfile.log
        working-directory: ./docs/code-maat

      - name: Run Code Maat
        run: java -jar code-maat-1.0.4-standalone.jar -l logfile.log -c git2 -c git2 -a revisions > ./revisions.csv
        working-directory: ./docs/code-maat

      - name: Run Cloc
        run: docker run --rm -v .:/tmp aldanial/cloc --unix --by-file --csv --quiet --timeout 10 --vcs=git --exclude-dir=docs,.vscode,.github --not-match-f=\.json --report-file=./docs/code-maat/complexity.csv

      - uses: actions/setup-python@v5
        with:
          python-version: '3.13' 

      - run: python csv_as_enclosure_json.py --structure complexity.csv --weights revisions.csv > ../astro/dist/crime-scene-hotspots/hotspots.json
        working-directory: ./docs/code-maat
    # 省略
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ${{ env.BUILD_PATH }}/dist
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    needs: build
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

```

## ホットスポットの可視化

[crime-scene-hotspots.html](https://github.com/adamtornhill/maat-scripts/blob/python3/transform/crime-scene-hotspots.html) を参考に、 d3をv3からv7にマイグレーションしたものを使用する。

```html:docs/astro/crime-scene-hotspots/index.html
<!DOCTYPE html>
<meta charset="utf-8" />
<style>

  .node {
    cursor: pointer;
  }

  .node:hover {
    stroke: #000;
    stroke-width: 1.5px;
  }

  .node--root {
    stroke: #777;
    stroke-width: 2px;
  }

  .node--leaf {
    fill: white;
    stroke: #777;
    stroke-width: 1px;
  }

  .label {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-anchor: middle;
    fill: white;
    text-shadow: 0 1px 0 #000, 1px 0 0 #000, -1px 0 0 #000, 0 -1px 0 #000;
  }

  .label,
  .node--root,
  .node--leaf {
    pointer-events: none;
  }
</style>
<body>
  <script type="module">
    import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
  
    const margin = 10,
      outerDiameter = 960,
      innerDiameter = outerDiameter - margin - margin;
  
    const x = d3.scaleLinear().range([0, innerDiameter]);
    const y = d3.scaleLinear().range([0, innerDiameter]);
    const color = d3.scaleLinear()
      .domain([-1, 5])
      .range(['hsl(185,60%,99%)', 'hsl(187,40%,70%)'])
      .interpolate(d3.interpolateHcl);
  
    const pack = d3.pack()
      .padding(2)
      .size([innerDiameter, innerDiameter]);
  
    const svg = d3
      .select('body')
      .append('svg')
      .attr('width', outerDiameter)
      .attr('height', outerDiameter)
      .append('g')
      .attr('transform', 'translate(' + margin + ',' + margin + ')');
  
    const d3Json = await d3.json('hotspots.json');
    const root = d3.hierarchy(d3Json).sum(d => d.size);
    const nodes = pack(root).descendants();
    let focus = root;
  
    svg
      .append('g')
      .selectAll('circle')
      .data(nodes)
      .enter()
      .append('circle')
      .attr('class', function (d) {
        return d.parent
          ? d.children
            ? 'node'
            : 'node node--leaf'
          : 'node node--root';
      })
      .attr('transform', function (d) {
        return 'translate(' + d.x + ',' + d.y + ')';
      })
      .attr('r', function (d) {
        return d.r;
      })
      .style('fill', function (d) {
        return d.weight > 0.0
          ? 'darkred'
          : d.children
          ? color(d.depth)
          : 'WhiteSmoke';
      })
      .style('fill-opacity', function (d) {
        return d.weight;
      })
      .on('click', function (event, d) {
        return zoom(event, focus === d ? root : d);
      });
  
    svg
      .append('g')
      .selectAll('text')
      .data(nodes)
      .enter()
      .append('text')
      .attr('class', 'label')
      .attr('transform', function (d) {
        return `translate(${d.x}, ${d.y})`;
      })
      .style('fill-opacity', function (d) {
        return d.parent === root ? 1 : 0;
      })
      .style('display', function (d) {
        return d.parent === root ? null : 'none';
      })
      .text(function (d) {
        return d.data.name;
      });
  
    d3.select(window).on('click', function (event) {
      zoom(event, root);
    });
  
    function zoom(event, d) {
      const focus0 = focus;
      focus = d;
  
      const k = innerDiameter / d.r / 2;
      x.domain([d.x - d.r, d.x + d.r]);
      y.domain([d.y - d.r, d.y + d.r]);
      event.stopPropagation();
  
      const transition = svg
        .selectAll("text,circle")
        .transition()
        .duration(event.altKey ? 7500 : 750)
        .attr("transform", function (d) {
          return `translate(${x(d.x)}, ${y(d.y)})`;
        });
  
      transition.filter("circle").attr("r", function (d) {
        return k * d.r;
      });
  
      transition
        .filter("text")
        .filter(function (d) {
          return d.parent === focus || d.parent === focus0;
        })
        .style("fill-opacity", function (d) {
          return d.parent === focus ? 1 : 0;
        })
        .on("start", function (d) {
          if (d.parent === focus) this.style.display = "inline";
        })
        .on("end", function (d) {
          if (d.parent !== focus) this.style.display = "none";
        });
    }
  
    d3.select(self.frameElement).style("height", outerDiameter + "px");
  </script>
</body>

```


# 参考
[GitHubPagesに開発ドキュメントをデプロイする総集編](https://qiita.com/hibohiboo/items/efff5fe687076ab44561)
[ホットスポットプロファイルの可視化を試してみたメモ](https://qiita.com/hibohiboo/items/bba79362884bccaa27d7)
[GitHub Actionsでコミット履歴を参照する](https://qiita.com/h_tyokinuhata/items/bd5488264f0e6aa41c89)