開発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)
}
})
重要なポイント:
- HyperFormulaの更新:
setCellContents()で新しい値を設定すると、依存する数式が自動的に再計算される - フォーマット情報の保持:
z(フォーマット文字列)とs(スタイル情報)を保持しないと、更新後に表示が崩れる - 二重管理: 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')
コンポーネント構造
まとめ
この実装により、以下が実現できました:
- ✅ Excelファイルの読み込みと表示
- ✅ 特定セルの動的編集
- ✅ 数式の自動再計算
- ✅ フォーマット情報の保持(パーセント、カンマ区切り)
キーポイント:
- XLSX: ファイル読み込みとフォーマット情報の取得(
cellNF: true必須) - HyperFormula: 数式エンジンとしての再計算
- 二重管理: XLSXとHyperFormula両方のデータ構造を同期
参考リンク
- SheetJS Documentation
- HyperFormula Documentation
- 実装ファイル:
apps/web/app/pages/excel-viewer.vue