3方式比較で決めた逼迫度クリップとISO週番号・SVGゲージの実装詳細

2026年2月23日

逼迫度の定義とエッジケースの技術的な壁

「残り工数と残り稼働時間の比率」を出すだけなら、割り算1回で済む。しかし実運用で使える指標にするには、いくつもの壁がある。

まず、未完了タスクだけを集計する定義が必要。完了済みタスクまで含めると、残工数が膨れ上がって実態と乖離する。次に、稼働時間がゼロになるケース。週末やまだ稼働時間を未設定の状態では、分母がゼロになってpressureが発散する。さらに、週番号の一貫性。日本でよく使われるカレンダーの「第何週」は曖昧で、年末年始に週が重複したり消えたりする。

そして可視化。数値だけでは逼迫感が伝わりにくく、SVG半円ゲージで視覚的に示す必要がある。ゲージの上限をどこに設定するかで、「パッと見の印象」が大きく変わる。

この記事では、逼迫度指標の3方式比較、ISO 8601準拠の週番号算出、SVG半円ゲージの描画ロジック、localStorage運用設計までの実装詳細を示す。

候補方式の比較

逼迫度(pressure)の算出アプローチとして、以下の3方式を検討した。

| 属性 | 単純比率 | 上限スケーリング | 正規化補正 | |------|----------|------------------|------------| | 実装コスト | 低い | 中 | 高い | | エッジケース耐性 | 低い | 高い | 中 | | 説明性 | 高い | 高い(補足要) | 低い | | 推奨用途 | 検証段階 | 本番表示 | 分析用途 |

方式A: 単純比率方式

pressure = remainingWork / remainingAvailable をそのまま使う方式。計算が単純で直感的に理解しやすい。しかし、remainingAvailableが0に近づくとpressureが発散し、ゲージ描画やユーザー解釈で混乱を招く。ISO 8601の週定義を使って稼働時間を一貫管理しても、週末やまだ稼働設定していない週では分母がゼロになる問題が残る。

方式B: 上限スケーリング方式(採用)

pressureを上限値(150%)でクリップし、表示用のゲージ比率はクリップ後の値をスケーリングする方式。稼働時間ゼロや極端な残工数でもUIが破綻しない。150%で頭打ちにすることで「過負荷だが表示は安定」という状態を実現できる。クリップ値の150%は経験則だが、「100%超=赤信号、150%=振り切り」という段階的な危険度が伝わりやすい。

方式C: 正規化+ヒューリスティック補正方式

過去の週平均稼働を参照してremainingAvailableを補正する方式。突発的な稼働時間減少による誤検知を低減できるが、過去データ依存のため新規ユーザーやデータ欠損時に不安定になる。実装が複雑で説明性が低下するため、ツールのコンセプト(シンプルに危険を察知する)と合わない。

なぜ上限スケーリングを選んだか

方式Aはゼロ除算で破綻し、方式Cは過去データなしの初期状態で誤動作する。方式Bは計算量O(n)(タスク数nの線形走査)で実装可能かつ、UIの安定性と説明性を両立できる。

実装の詳細

逼迫度の計算フロー

  1. タスク群を走査し、未完了(estimatedHours > actualHours)のみ抽出
  2. remainingWork = Σ(estimatedHours - actualHours) を合算
  3. remainingAvailable = totalAvailable - elapsedHours を算出
  4. remainingAvailable ≤ 0 なら pressure = 1.5(上限クリップ)
  5. それ以外は pressure = min(remainingWork / remainingAvailable, 1.5)
  6. ゲージ比率 ratio = pressure / 1.5
数値例:
  タスクA: estimated 10h, actual 4h → 未完了 6h
  タスクB: estimated 5h, actual 5h  → 完了(除外)
  タスクC: estimated 8h, actual 2h  → 未完了 6h
  remainingWork = 6 + 6 = 12h

  週の totalAvailable = 40h(8h × 5日)
  経過 elapsedHours = 16h(火曜終業)
  remainingAvailable = 40 - 16 = 24h

  pressure = 12 / 24 = 0.5
  pressure_display = min(0.5, 1.5) = 0.5
  ゲージ比率 = 0.5 / 1.5 ≒ 0.333

ISO 8601週番号の算出

「木曜日を含む週が第1週」「月曜起点」というISO 8601の定義に準拠する。

ISO 8601 週番号の算出手順:
  1. 対象年の1月4日(jan4)を求める
     → 1月4日は必ず第1週に属する(ISO定義)
  2. jan4の曜日からその週の月曜日(monday1)を求める
     → monday1 = jan4 - (jan4の曜日 - 1)
     (日曜=0の場合は調整が必要)
  3. 対象日とmonday1の差分日数を7で割る
     → week = floor((targetDate - monday1) / 7) + 1

年跨ぎの例:
  2026年12月31日(水曜)
  → 2027年の第1木曜 = 1月1日
  → 12月31日は2027年のW01に属する

タイムゾーンの影響を排除するため、日付計算はすべてUTCベースで行う。new Date(Date.UTC(year, month, day)) を使い、ローカルタイムとのずれを防いでいる。

SVG半円ゲージの描画ロジック

半円(180度)のゲージをstroke-dasharrayとstroke-dashoffsetで描画する。

SVG半円ゲージの計算:
  直径 D = 120px
  半円の弧長 = π × D / 2 = π × 60 ≒ 188.5px

  stroke-dasharray = "188.5"(弧長全体)
  stroke-dashoffset = 188.5 × (1 - ratio)

  ratio = 0.333 の場合:
    dashoffset = 188.5 × (1 - 0.333) = 188.5 × 0.667 ≒ 125.7
    → 弧の約33%が塗られる

  ratio = 1.0(pressure ≥ 1.5)の場合:
    dashoffset = 0
    → 弧が完全に塗られる(振り切り状態)

transform: rotate(-90deg) で開始角度を12時方向に調整し、左から右に弧が伸びるようにしている。

localStorageの運用設計

週データはMarkdown形式でシリアライズしてlocalStorageに保存する。JSON形式に比べて人間が読めるため、デバッグやエクスポート時の利便性が高い。

Markdownシリアライズ例:
  # W08 (2026-02-16 〜 2026-02-22)
  - totalAvailable: 40
  - elapsedHours: 32
  ## Tasks
  - [ ] タスクA: 10h / 4h
  - [x] タスクB: 5h / 5h
  - [ ] タスクC: 8h / 2h

localStorageの容量制限(通常5MB)を考慮し、保存する週データは直近12週分に限定している。保存失敗時はMarkdownエクスポートを促すUIを表示する。

検証結果

ケース1: 通常週の中間(火曜終業)

典型的な週中の確認。残工数と残稼働が両方存在する状況。

入力値:

  • タスク合計未完了: 12h
  • totalAvailable: 40h
  • elapsedHours: 16h

計算結果:

  • remainingAvailable: 24h
  • pressure: 12 / 24 = 0.5
  • ゲージ比率: 0.333

解釈: 逼迫度50%は中程度。ゲージは約1/3の充填で、視覚的にも余裕がある状態。金曜まで間に合うペースだが、新規タスクの割り込みに注意。

ケース2: 稼働時間ゼロ(エッジケース)

週の稼働時間が未設定、または休暇で0hの場合。

入力値:

  • タスク合計未完了: 8h
  • totalAvailable: 0h
  • elapsedHours: 0h

計算結果:

  • remainingAvailable: 0 → 特別処理
  • pressure: 1.5(上限クリップ)
  • ゲージ比率: 1.0(満杯)

解釈: 稼働時間がゼロなのにタスクが残っている状態は「完全に過負荷」として扱う。ゲージは振り切り表示となり、ユーザーに稼働時間の設定を促すUIを併記する。分母ゼロによる発散はクリップで回避。

エッジケース: 年末の週番号跨ぎ

入力値:

  • 対象日: 2026年12月31日(木曜)

計算結果:

  • 2027年の1月4日 → 日曜
  • 2027年のW01月曜日 → 12月29日(月曜)
  • 12月31日は12月29日から2日後 → W01に属する
  • 表示: "W01 (2027)"

解釈: 12月31日が翌年のW01に属するのはISO 8601の正しい挙動。ユーザーが混乱しないよう、年表示を併記する設計とした。

よくある質問(FAQ)

Q: 週の稼働時間を途中で変更した場合の反映は

変更は即時反映される。remainingAvailableが再計算され、ゲージもアニメーション付きで更新されるため、変化が視覚的に伝わる。過去週のデータは変更されない。

Q: 上限150%の根拠は何か

150%は「残り稼働の1.5倍のタスクが残っている」状態を意味する。100%超で赤信号、150%で振り切りという2段階の危機感を表現するために設定した経験則。将来的には設定画面で調整可能にしており、チームの稼働パターンに合わせた値をA/Bテストで探索できる設計としている。

Q: データはサーバーに送信される?

すべてブラウザ内で処理される。タスクや稼働時間のデータがサーバーに送信されることはない。localStorageに保存されるが、ブラウザ内に閉じた保存であり、外部からアクセスされることはない。

Q: localStorageが容量超過した場合はどうなるか

保存失敗時はアラートを表示し、Markdownエクスポートでのバックアップを促す。直近12週分を超える古いデータは自動的に削除する設計としているため、通常の運用で容量超過になることはまれ。

まとめ

Weekloadの逼迫度指標は、上限スケーリング方式(150%クリップ)により計算の安定性とUIの破綻防止を両立している。ISO 8601準拠の週番号で年末年始の曖昧さを排除し、SVG半円ゲージのdashoffset制御で視覚的な逼迫感を表現した。

実際に試したいときはWeekload 週間工数管理を使ってみて。数値の可視化という共通テーマでは、梁の安全審判員もSVGたわみ図で計算結果を視覚化している。


不具合や要望があれば、お問い合わせページから気軽に教えてほしい。

M

Mahiro

Mahiro Appの開発者。ISO週番号と逼迫度スコアの設計思想を技術的に解説した記事の著者。

運営者情報を見る

© 2026 Mahiro App