• #excel
  • #hyperformula
  • #xlsx
  • #nuxt
  • #vue
開発excel-viewer完了

Excel Viewer with HyperFormula - 動的セル編集と再計算の実装ガイド

概要

このドキュメントでは、Excelファイルを単に表示するだけでなく、特定のセルを編集可能な変数として扱い、そのセル変更に応じて他のセル(数式を含む)が自動的に再計算される機能の実装方法を解説します。

実現できること

  • Excelファイルの読み込みと表示
  • 特定のセル(例: C2セル)を編集可能にする
  • セル変更時に、そのセルを参照する数式セルが自動的に再計算される
  • 数値フォーマット(カンマ区切り、パーセント表示)の保持

システムアーキテクチャ

データフロー

初期読み込みフロー

セル編集・再計算フロー

使用技術

1. SheetJS (xlsx)

Excelファイルの読み込みとパースに使用

CDN経由で読み込み (npmインストール不要)

useHead({
  script: [
    {
      src: 'https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js',
      async: true,
    }
  ]
})

2. HyperFormula

Excelの数式エンジン。数式の計算と再計算を担当

npm install hyperformula

データ構造

XLSXセルオブジェクト

シートIDマッピング

HyperFormulaの自動再計算の仕組み

重要なポイント

HyperFormulaは数式の依存関係を自動的に管理し、セル変更時に関連する全ての数式を再計算します。

これにより、開発者が手動で再計算ロジックを書く必要はありません。

具体例:年利率(C2)の変更がH列(運用額合計)に反映される仕組み

なぜ特別な再計算ロジックが不要なのか

1. 初期化時の依存関係解析

HyperFormulaは buildFromSheets() 時に、Excelの全ての数式を解析して依存関係グラフを構築します。

// 数式の例(Excelファイル内)
// F7: =C2*E7        // C2(年利率)を参照
// F8: =C2*E8        // C2(年利率)を参照
// H7: =F7+G7        // F7を参照
// H8: =F8+G8        // F8を参照

// HyperFormulaの初期化
const sheets = {
  'Sheet1': [
    ['', 'B', '年利率', ...],           // 行0
    ['', '', '', ...],                   // 行1
    ['', '', 0.07, ...],                 // 行2: C2=0.07
    ...
    ['', '', '', '', '', '=C2*E7', '...', '=F7+G7'],  // 行6: F7とH7
  ]
}

hfInstance = HyperFormula.buildFromSheets(sheets)
// この時点で HyperFormula は内部的に以下を把握:
// - C2 が変更されたら F7, F8, ... を再計算
// - F7 が変更されたら H7 を再計算
// - F8 が変更されたら H8 を再計算

2. setCellContents() による自動伝播

3. 開発者が書いたコードはシンプル

// これだけで全ての依存セルが再計算される
hfInstance.setCellContents(
  { sheet: hfSheetId, col: C, row: R },
  [[newValue]]
)

// ✓ F7, F8, H7, H8... 全て自動で再計算
// ✓ 開発者は再計算ロジックを書かなくてOK

依存関係の伝播例

数式の読み込みロジック

function convertSheetToHF(worksheet: any) {
  const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1')
  const data: any[][] = []
  
  for (let R = range.s.r; R <= range.e.r; R++) {
    const row: any[] = []
    for (let C = range.s.c; C <= range.e.c; C++) {
      const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
      const cell = worksheet[cellAddress]
      
      if (cell) {
        if (cell.f) {
          // 重要:数式セルは '=' を付けて渡す
          row.push('=' + cell.f)
          // 例: cell.f = 'C2*E7' → HyperFormulaに '=C2*E7' を渡す
          // HyperFormulaはこの時点でC2への依存を記録
        } else {
          row.push(cell.v ?? '')
        }
      } else {
        row.push('')
      }
    }
    data.push(row)
  }
  
  return data
}

HyperFormulaが管理する依存関係グラフ(内部イメージ)

まとめ:開発者がやること・やらないこと

やることやらないこと
✅ Excelファイルを読み込む❌ 依存関係を手動で追跡
✅ 数式をconvertSheetToHF()で変換❌ 再計算順序を考える
setCellContents()で値を更新❌ 依存セルを手動で更新
getCellValue()で結果を取得❌ 数式を手動で評価
✅ 再描画で結果を表示❌ キャッシュ管理

HyperFormulaのすごさ:

  • Excelの数式エンジンを完全に再現
  • 循環参照の検出
  • 最適な計算順序の自動決定
  • パフォーマンス最適化(必要なセルだけ再計算)

実装のポイント

1. XLSXライブラリの読み込みオプション

セルのフォーマット情報を取得するために cellNF: true オプションが必須です。

workbook = XLSX.read(data, {
  type: 'array',
  cellStyles: true,
  cellDates: true,
  cellNF: true  // セルのフォーマット情報を取得(重要!)
})

なぜ必要か:

  • cellNF: true がないと、セルの z プロパティ(フォーマット文字列)が undefined になる
  • パーセント表示や通貨表示などのフォーマットが失われる

2. HyperFormulaの初期化

ワークブック全体をHyperFormula形式に変換して初期化します。

// XLSXのワークシートをHyperFormula形式に変換
function convertSheetToHF(worksheet: any) {
  const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1')
  const data: any[][] = []
  
  for (let R = range.s.r; R <= range.e.r; R++) {
    const row: any[] = []
    for (let C = range.s.c; C <= range.e.c; C++) {
      const cellAddress = XLSX.utils.encode_cell({ r: R, c: C })
      const cell = worksheet[cellAddress]
      
      if (cell) {
        // 数式がある場合は数式を、なければ値を
        if (cell.f) {
          row.push('=' + cell.f)  // 数式の場合は = を付ける
        } else {
          row.push(cell.v ?? '')
        }
      } else {
        row.push('')
      }
    }
    data.push(row)
  }
  
  return data
}

// HyperFormulaインスタンスを作成
const sheets: any = {}
workbook.SheetNames.forEach((sheetName: string) => {
  sheets[sheetName] = convertSheetToHF(workbook.Sheets[sheetName])
})

hfInstance = HyperFormula.buildFromSheets(sheets)

ポイント:

  • 数式セルは cell.f プロパティに数式文字列が入っている
  • HyperFormulaには = 付きの数式文字列を渡す必要がある

3. シートIDの管理

HyperFormulaは独自のシートID管理を行うため、配列インデックスとシートIDをマッピングする必要があります。

let sheetIdMap: Map<number, number> = new Map() // シートインデックス -> HyperFormulaシートID

// シート名→シートIDのマッピングを作成
sheetIdMap.clear()
workbook.SheetNames.forEach((sheetName: string, index: number) => {
  const sheetId = hfInstance.getSheetId(sheetName)
  sheetIdMap.set(index, sheetId)
  console.log(`Sheet "${sheetName}" (index: ${index}) -> HF ID: ${sheetId}`)
})

なぜ必要か:

  • buildFromSheets() はシート名ベースでシートを管理する
  • シート切り替え時などは配列インデックスを使うため、両方のIDを紐付ける必要がある

4. セル値の取得(HyperFormulaから)

通常のセルと数式セルの両方に対応するため、HyperFormulaから値を取得します。

let cellValue = ''
if (hfInstance) {
  try {
    const hfSheetId = sheetIdMap.get(sheetIndex)
    if (hfSheetId !== undefined) {
      const hfValue = hfInstance.getCellValue({ 
        sheet: hfSheetId, 
        col: C, 
        row: R 
      })
      if (hfValue !== null && hfValue !== undefined) {
        cellValue = hfValue.toString()
      }
    }
  } catch (e) {
    // フォールバック: 元の値を使用
    cellValue = cell?.w || cell?.v || ''
  }
}

ポイント:

  • 数式セルは計算済みの値が返される
  • 通常のセルもHyperFormulaから取得することで、処理を統一できる

5. セル編集時の再計算

セル値を変更したら、HyperFormulaに更新を通知して再計算させます。

input.addEventListener('blur', () => {
  const newValue = parseFloat(input.value)
  if (!isNaN(newValue)) {
    // HyperFormulaのセルを更新して再計算
    if (hfInstance) {
      const hfSheetId = sheetIdMap.get(sheetIndex)
      if (hfSheetId !== undefined) {
        hfInstance.setCellContents(
          { sheet: hfSheetId, col: C, row: R },
          [[newValue]]
        )
      }
    }
    
    // workbookのセルも更新(フォーマット情報を保持)
    const updatedCell: any = {
      v: newValue,
      t: 'n'
    }
    
    // 元のセルのフォーマット情報があれば保持
    if (cell?.z) {
      updatedCell.z = cell.z
      // パーセント表示の場合はwもフォーマット
      if (cell.z.includes('%')) {
        updatedCell.w = (newValue * 100).toFixed(2) + '%'
      } else {
        updatedCell.w = newValue.toString()
      }
    }
    
    // スタイル情報も保持
    if (cell?.s) {
      updatedCell.s = cell.s
    }
    
    worksheet[cellAddress] = updatedCell
    
    // 再描画(HyperFormulaの計算結果が反映される)
    displaySheet(sheetIndex)
  }
})

重要なポイント:

  1. HyperFormulaの更新: setCellContents() で新しい値を設定すると、依存する数式が自動的に再計算される
  2. フォーマット情報の保持: z(フォーマット文字列)と s(スタイル情報)を保持しないと、更新後に表示が崩れる
  3. 二重管理: XLSXのworkbookオブジェクトとHyperFormulaの両方を更新する必要がある

6. 数値フォーマットの処理

カンマ区切りとパーセント表示を正しく処理します。

// 数値のフォーマット処理
if (cell && cell.t === 'n') {
  td.classList.add('cell-number')
  const numValue = parseFloat(cellValue)
  if (!isNaN(numValue)) {
    if (cell.z && cell.z.includes('%')) {
      // パーセント表示は小数点以下を残す
      cellValue = (numValue * 100).toFixed(2) + '%'
    } else {
      // 通常の数値はカンマ区切り・小数点なしで表示
      cellValue = Math.round(numValue).toLocaleString('ja-JP')
    }
  }
  td.textContent = cellValue
}

ポイント:

  • Excelでは 0.08 がパーセントフォーマット(z: "0.00%")だと 8.00% と表示される
  • toLocaleString('ja-JP') でカンマ区切りを実現

デバッグのポイント

コンソールログの活用

HyperFormulaの動作を確認するために、以下のログを仕込むと良いです。

// シート変換時
console.log('Converting sheet, range:', worksheet['!ref'])
console.log(`Cell ${cellAddress} has formula:`, formula)

// シートID確認
console.log(`Sheet "${sheetName}" (index: ${index}) -> HF ID: ${sheetId}`)

// セル更新時
console.log(`=== Updating C2 from ${cell?.v} to ${newValue} ===`)
console.log('HyperFormula updated successfully')

// 再計算結果の確認
for (let r = range.s.r; r <= Math.min(range.e.r, 10); r++) {
  for (let c = range.s.c; c <= Math.min(range.e.c, 10); c++) {
    const addr = XLSX.utils.encode_cell({ r, c })
    const hfVal = hfInstance.getCellValue({ sheet: hfSheetId, col: c, row: r })
    console.log(`  ${addr}: ${hfVal}`)
  }
}

トラブルシューティングフロー

よくある問題と解決策

問題1: 再計算されない

原因:

  • シートIDが間違っている
  • setCellContents() の引数が間違っている

解決策:

// シートIDを正しく取得
const hfSheetId = sheetIdMap.get(sheetIndex)

// 正しい形式で更新
hfInstance.setCellContents(
  { sheet: hfSheetId, col: C, row: R },
  [[newValue]]  // 2次元配列で渡す
)

問題2: パーセント表示が失われる

原因:

  • cellNF: true オプションがない
  • セル更新時に z プロパティを保持していない

解決策:

// 読み込み時
XLSX.read(data, { cellNF: true, ... })

// 更新時
updatedCell.z = cell.z  // フォーマット情報を保持

問題3: カンマ区切りが表示されない

原因:

  • 数値フォーマット処理が適用されていない

解決策:

cellValue = Math.round(numValue).toLocaleString('ja-JP')

コンポーネント構造

まとめ

この実装により、以下が実現できました:

  1. ✅ Excelファイルの読み込みと表示
  2. ✅ 特定セルの動的編集
  3. ✅ 数式の自動再計算
  4. ✅ フォーマット情報の保持(パーセント、カンマ区切り)

キーポイント:

  • XLSX: ファイル読み込みとフォーマット情報の取得(cellNF: true 必須)
  • HyperFormula: 数式エンジンとしての再計算
  • 二重管理: XLSXとHyperFormula両方のデータ構造を同期

参考リンク