🐷

Vue 3 + TypeScript で「完了/進行中/予定」を割合バーで見せる

に公開

バックエンドから各ステータスの件数(完了・進行中 など)だけが返ってきていても、フロント側の工夫で「割合つきの横棒グラフ」として見せることができます。

この記事では、以下のようなテーブルを Vue3(<script setup>)で実装する手順をまとめます。

行:任意の項目の組み合わせ(例:商品 × 拠点 など)
列:
進捗率(%)
完了/進行中/予定の本数を、横棒の長さで可視化
必要なら 1 セルに 3 色のバーとしてまとめる

サンプルコードは業務に依存しない形にしてあるので、そのまま自分のプロジェクトに持ち込んでアレンジできます。

想定シナリオ
バックエンドからは、以下のような集計結果だけが返ってくるとします。

interface RawCountRow {
    itemA: string   // 例: 商品名
    itemB: string   // 例: 拠点名
    total: number   // 工程の総数
    completed: number   // 完了件数
    inProgress: number  // 進行中件数
}

フロントエンドでは、この値から

予定件数 (planned = total - completed - inProgress)
進捗率 (rate = (completed + inProgress) / total * 100)

を計算し、表とバー表示を行います。

  1. <script setup> 側の実装
    まずは、Vue 3 の <script setup> 部分です。
    日付や画面遷移など、特定業務に依存しそうな処理は極力省き、進捗表示に必要な部分だけ に絞っています。
<script setup lang="ts">
import { ref, computed } from 'vue'

/**
 * バックエンドから返ってくる集計結果の型
 * total / completed / inProgress だけを持つシンプルな想定
 */
interface RawCountRow {
  itemA: string
  itemB: string
  total: number
  completed: number
  inProgress: number
}

/**
 * サンプルデータ
 * 実際には API レスポンスをここにセットするイメージ
 */
const rawRows = ref<RawCountRow[]>([
  { itemA: 'サンプルA', itemB: '拠点1', total: 4, completed: 2, inProgress: 1 },
  { itemA: 'サンプルB', itemB: '拠点2', total: 4, completed: 1, inProgress: 2 },
  { itemA: 'サンプルB', itemB: '拠点1', total: 2, completed: 0, inProgress: 1 }
])

/**
 * ドロップダウンの候補(itemA / itemB)を重複なしで抽出
 */
const itemAList = computed<string[]>(() => {
  const set = new Set<string>()
  for (const r of rawRows.value) set.add(r.itemA)
  return Array.from(set)
})
const itemBList = computed<string[]>(() => {
  const set = new Set<string>()
  for (const r of rawRows.value) set.add(r.itemB)
  return Array.from(set)
})

/**
 * ドロップダウンの選択状態
 * 空文字列は「全て」を表す
 */
    const selectedItemA = ref<string>('')
    const selectedItemB = ref<string>('')

/**
 * テーブル 1 行分の型
 * planned / rate はフロント側で計算する
 */
interface ProgressRow {
  itemA: string
  itemB: string
  total: number
  completed: number
  inProgress: number
  planned: number   // = total - completed - inProgress
  rate: number      // = (completed + inProgress) / total * 100
}

/**
 * RawCountRow → ProgressRow
 * planned と rate を計算してマッピングする
 */
const allProgressRows = computed<ProgressRow[]>(() => {
  return rawRows.value.map((r) => {
    const planned = Math.max(r.total - r.completed - r.inProgress, 0)
    const rate = r.total
      ? Math.round(((r.completed + r.inProgress) / r.total) * 100)
      : 0

    return {
      itemA: r.itemA,
      itemB: r.itemB,
      total: r.total,
      completed: r.completed,
      inProgress: r.inProgress,
      planned,
      rate
    }
  })
})

/**
 * ドロップダウンの選択で絞り込み
 */
const filteredRows = computed<ProgressRow[]>(() => {
  return allProgressRows.value.filter((row) => {
    const matchA =
      !selectedItemA.value || selectedItemA.value === '' || row.itemA === selectedItemA.value
    const matchB =
      !selectedItemB.value || selectedItemB.value === '' || row.itemB === selectedItemB.value
    return matchA && matchB
  })
})

/**
 * 今回重要となる点:比率 → 幅%に変換するユーティリティ
 *
 * part / total の割合から "33.3%" のような文字列を返す
 * total が 0 や負数の場合は "0%" を返してガードする
 */
const getWidthPercent = (part: number, total: number): string => {
  if (!total || total <= 0) return '0%'
  const pct = (part / total) * 100
  return `${pct.toFixed(1)}%`
}
</script>

ポイントは、バックエンドを一切意識させずに使えるようにしている ことです。

「total / completed / inProgress が来れば動く」だけの汎用コンポーネントplannedとrateはフロントで計算するので、既存 API に手を入れなくて良い比率→幅%への変換をgetWidthPercentに切り出しておくことで、再利用しやすくなります。

  1. テーブル内で「完了/進行中/予定」をバー表示する
    続いて、<template> です。まずは、完了/進行中/予定を別々の列 で表示するパターンを設定します。
テーブルヘッダ
<thead>
  <tr>
    <th>項目A</th>
    <th>項目B</th>
    <th>進捗率</th>
    <th>完了</th>
    <th>進行中</th>
    <th>予定</th>
  </tr>
</thead>
テーブル本体(3 列に分けてバー表示)
<tbody>
  <tr v-for="row in filteredRows" :key="row.itemA + '|' + row.itemB">
    <!-- 項目A -->
    <td class="border-r border-gray-300 bg-white px-6 py-2">
      {{ row.itemA }}
    </td>

    <!-- 項目B -->
    <td class="border-r border-gray-300 bg-white px-6 py-2">
      {{ row.itemB }}
    </td>

    <!-- 進捗率(数値だけ) -->
    <td class="border-r border-gray-300 bg-gray-200 px-4 py-2 text-center">
      {{ row.rate }}%
    </td>

    <!-- 完了:幅は completed / total -->
    <td class="border-r border-gray-300 px-4 py-2 bg-white">
      <div class="w-full h-4 bg-gray-100 rounded">
        <div
          class="h-4 bg-green-300 rounded"
          :style="{ width: getWidthPercent(row.completed, row.total) }"
        ></div>
      </div>
      <div class="text-xs text-center mt-1">
        {{ row.completed }} 件
      </div>
    </td>

    <!-- 進行中:幅は inProgress / total -->
    <td class="border-r border-gray-300 px-4 py-2 bg-white">
      <div class="w-full h-4 bg-gray-100 rounded">
        <div
          class="h-4 bg-blue-300 rounded"
          :style="{ width: getWidthPercent(row.inProgress, row.total) }"
        ></div>
      </div>
      <div class="text-xs text-center mt-1">
        {{ row.inProgress }} 件
      </div>
    </td>

    <!-- 予定:幅は planned / total -->
    <td class="border-r border-gray-300 px-4 py-2 bg-white">
      <div class="w-full h-4 bg-gray-100 rounded">
        <div
          class="h-4 bg-red-300 rounded"
          :style="{ width: getWidthPercent(row.planned, row.total) }"
        ></div>
      </div>
      <div class="text-xs text-center mt-1">
        {{ row.planned }} 件
      </div>
    </td>
  </tr>
</tbody>

Tailwind を使っている前提ですが、クラス指定は好みに応じて変えてOKです。
getWidthPercent のおかげで、どのセルでも同じ書き方でバー幅を指定できています。

  1. 挙動イメージ
    例えば 1 行分のデータが以下のような場合:

{ itemA: 'サンプルA', itemB: '拠点1', total: 4, completed: 2, inProgress: 1, planned: 1 }

それぞれのバー幅はこうなります。
完了バー幅 = 2 / 4 = 50%
進行中バー幅 = 1 / 4 = 25%
予定バー幅 = 1 / 4 = 25%

結果として、テーブル上では
緑バー:横幅 50%
青バー:横幅 25%
赤バー:横幅 25%
となり、それぞれの下に
「2 件」
「1 件」
「1 件」
といった件数が表示されます。

  1. 1セルに3色のバーをまとめるバリエーション
    UI デザインによっては、列を増やしたくないので、
    1 つの横棒に「完了/進行中/予定」を3色で詰め込みたいというケースもあると思います。
    その場合は、セルをflexにして3つのdivを横並びにし幅だけ変えるようにすればOKです。
<td class="border-r border-gray-300 px-4 py-2 bg-white">
  <div class="w-full h-4 flex rounded overflow-hidden border">
    <div
      class="h-4 bg-green-300"
      :style="{ width: getWidthPercent(row.completed, row.total) }"
    ></div>
    <div
      class="h-4 bg-blue-300"
      :style="{ width: getWidthPercent(row.inProgress, row.total) }"
    ></div>
    <div
      class="h-4 bg-red-300"
      :style="{ width: getWidthPercent(row.planned, row.total) }"
    ></div>
  </div>
  <div class="text-[10px] text-gray-600 mt-1 text-center">
    完了: {{ row.completed }} / 進行中: {{ row.inProgress }} / 予定: {{ row.planned }}
  </div>
</td>

このパターンを使う場合は、ヘッダを

<th>進捗内訳</th>

など 1 列にまとめておくと、テーブル全体がすっきりします。

Discussion