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)
を計算し、表とバー表示を行います。
- <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に切り出しておくことで、再利用しやすくなります。
- テーブル内で「完了/進行中/予定」をバー表示する
続いて、<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 行分のデータが以下のような場合:
{ 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セルに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