# Version comparison

The version comparison feature allows users to compare two versions of a spreadsheet and visualize differences inline. Changes are highlighted with distinct colors for added, deleted, and modified cells.

<figure><img src="https://67932947-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHWsslZpYESUXxPccoyig%2Fuploads%2FNBCOVPhjrrwVXIgYY4a7%2Fimage.png?alt=media&#x26;token=60389410-e11a-4851-9a52-ff161c7e0823" alt="Spreadsheet version comparison UI"><figcaption><p>Spreadsheet version comparison UI</p></figcaption></figure>

## Overview

Version comparison is useful for:

* Tracking changes between document revisions
* Reviewing edits before accepting them
* Understanding what changed between snapshots
* Collaboration workflows where multiple users edit the same document

## Visual Highlighting

| Change Type  | Background              | Text Color                 | Description                                                             |
| ------------ | ----------------------- | -------------------------- | ----------------------------------------------------------------------- |
| **Added**    | Light green (`#d0fae1`) | Dark green (`#046e38`)     | Cell exists in current version but not in previous                      |
| **Deleted**  | Light red (`#ffdbdb`)   | Dark red (`#b21313`)       | Cell exists in previous version but not in current (with strikethrough) |
| **Modified** | Both colors             | Red for old, green for new | Cell value or format changed                                            |

### Modified Cells

When a cell is modified, both the old and new values are displayed side by side within the cell:

* **Old value**: Red background with the previous formatting applied
* **New value**: Green background with the current formatting applied

This applies to both value changes and format-only changes (e.g., text becoming bold).

## Installation

```bash
npm install @rowsncolumns/version-comparison
```

## Basic Usage

```tsx
import { useState, useCallback, useMemo } from "react";
import { useVersionComparison } from "@rowsncolumns/version-comparison";
import { CanvasGrid, useSpreadsheetState } from "@rowsncolumns/spreadsheet";

const VersionComparisonDemo = () => {
  // Manage comparison mode yourself
  const [isComparing, setIsComparing] = useState(false);

  // Your current spreadsheet data
  const [sheetData, setSheetData] = useState(currentData);

  // Previous version snapshot
  const previousSheetData = useMemo(() => snapshotData, []);

  const { getCellData } = useSpreadsheetState({
    sheetData,
    // ... other props
  });

  // Function to get previous cell data
  const getPreviousCellData = useCallback(
    (sheetId: number, rowIndex: number, columnIndex: number) => {
      return previousSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex] ?? null;
    },
    [previousSheetData]
  );

  // Version comparison hook - pass null when not comparing
  const { getCellDiff } = useVersionComparison({
    getCellData,
    getPreviousCellData: isComparing ? getPreviousCellData : null,
  });

  return (
    <div>
      {/* Comparison controls */}
      <div>
        {!isComparing ? (
          <button onClick={() => setIsComparing(true)}>Compare Versions</button>
        ) : (
          <button onClick={() => setIsComparing(false)}>Exit Comparison</button>
        )}
      </div>

      {/* Grid with comparison highlighting */}
      <CanvasGrid
        // ... standard props
        isComparing={isComparing}
        getCellDiff={getCellDiff}
      />
    </div>
  );
};
```

## API Reference

### useVersionComparison Hook

```typescript
const { getCellDiff } = useVersionComparison(options);
```

#### Options

```typescript
type UseVersionComparisonOptions<T extends CellData = CellData> = {
  /** Function to get current cell data */
  getCellData: (sheetId: number, rowIndex: number, columnIndex: number) => T | null | undefined;
  /** Function to get previous cell data (null = not comparing) */
  getPreviousCellData: ((sheetId: number, rowIndex: number, columnIndex: number) => T | null | undefined) | null;
  /** CellXfs registry for resolving style references in current version */
  cellXfs?: Map<number, CellFormat> | null;
  /** CellXfs registry for previous version (defaults to cellXfs) */
  previousCellXfs?: Map<number, CellFormat> | null;
  /** Shared strings map for current version */
  sharedStrings?: Map<string, string> | null;
  /** Shared strings map for previous version (defaults to sharedStrings) */
  previousSharedStrings?: Map<string, string> | null;
};
```

#### Return Values

| Property      | Type                                                   | Description                  |
| ------------- | ------------------------------------------------------ | ---------------------------- |
| `getCellDiff` | `(sheetId, rowIndex, columnIndex) => CellDiff \| null` | Get diff for a specific cell |

### CellDiff Type

```typescript
type CellDiff = {
  sheetId: number;
  rowIndex: number;
  columnIndex: number;
  state: "added" | "deleted" | "modified";
  detail?: CellDiffDetail;
};

type CellDiffDetail = {
  valueChanged: boolean;
  formatChanged: boolean;
  oldValue?: ExtendedValue;
  newValue?: ExtendedValue;
  oldFormattedValue?: string;
  newFormattedValue?: string;
  oldFormat?: CellFormat;
  newFormat?: CellFormat;
};
```

## Detecting Different Types of Changes

### Value Changes

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
if (diff?.detail?.valueChanged) {
  console.log("Old value:", diff.detail.oldFormattedValue);
  console.log("New value:", diff.detail.newFormattedValue);
}
```

### Format Changes

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
if (diff?.detail?.formatChanged) {
  console.log("Old format:", diff.detail.oldFormat);
  console.log("New format:", diff.detail.newFormat);
}
```

### Format-Only Changes

When a cell's value stays the same but formatting changes (e.g., text becomes bold):

```typescript
const diff = getCellDiff(sheetId, rowIndex, columnIndex);
const isFormatOnlyChange = diff?.detail?.formatChanged && !diff?.detail?.valueChanged;
if (isFormatOnlyChange) {
  // Show both old and new side by side to display formatting difference
}
```

## Working with Shared Strings

If your cell data uses shared strings (`ss` property pointing to a shared strings map), pass the shared strings:

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  sharedStrings, // Map<string, string>
});
```

Cell data with shared string:

```typescript
// Cell uses ss to reference a shared string
const cellData = {
  ss: "0",  // References sharedStrings.get("0")
  fv: "Hello",
};

const sharedStrings = new Map([["0", "Hello"], ["1", "World"]]);
```

If the previous version has a different shared strings map (e.g., from a snapshot):

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  sharedStrings: currentSharedStrings,
  previousSharedStrings: snapshotSharedStrings,
});
```

## Working with Style References

If your cell data uses style IDs instead of inline formats, pass the `cellXfs` registry:

```typescript
const { getCellDiff } = useVersionComparison({
  getCellData,
  getPreviousCellData,
  cellXfs, // Map<number, CellFormat>
  previousCellXfs, // Optional: for previous version styles
});
```

Cell data with style reference:

```typescript
// Cell uses ef.sId to reference a style
const cellData = {
  ue: { sv: "Hello" },
  fv: "Hello",
  ef: { sId: 5 }  // References cellXfs.get(5)
};
```

## Comparison Summary

To display a summary of all changes:

```tsx
const DiffSummary = ({ isComparing, getCellDiff }) => {
  const summary = useMemo(() => {
    if (!isComparing) return { added: 0, modified: 0, deleted: 0 };

    let added = 0, modified = 0, deleted = 0;

    // Scan your data range
    for (let row = 0; row < rowCount; row++) {
      for (let col = 0; col < columnCount; col++) {
        const diff = getCellDiff(sheetId, row, col);
        if (diff?.state === "added") added++;
        else if (diff?.state === "modified") modified++;
        else if (diff?.state === "deleted") deleted++;
      }
    }

    return { added, modified, deleted };
  }, [isComparing, getCellDiff]);

  return (
    <div>
      <span>+{summary.added} added</span>
      <span>~{summary.modified} modified</span>
      <span>-{summary.deleted} deleted</span>
    </div>
  );
};
```

## CanvasGrid Props

| Prop          | Type                                      | Description                      |
| ------------- | ----------------------------------------- | -------------------------------- |
| `isComparing` | `boolean`                                 | Enable comparison mode rendering |
| `getCellDiff` | `(sheetId, row, col) => CellDiff \| null` | Function to get cell diff        |

## Diff Engine Functions

For advanced use cases, you can use the diff engine functions directly:

```typescript
import { areCellValuesEqual, areCellFormatsEqual } from "@rowsncolumns/version-comparison";

// Compare values
const valuesEqual = areCellValuesEqual(
  { nv: 100 },
  { nv: 100 }
); // true

// Compare formats
const formatsEqual = areCellFormatsEqual(
  { textFormat: { bold: true } },
  { textFormat: { bold: false } }
); // false
```

## Best Practices

1. **Snapshot Management**: Store snapshots efficiently - consider only storing changed cells rather than the entire sheet.
2. **Performance**: For large spreadsheets, consider lazy-loading diffs only for visible cells.
3. **User Experience**:
   * Show a clear indicator when comparison mode is active
   * Provide a summary of changes
   * Allow users to navigate between changes
4. **Format Comparison**: Remember that format changes count as modifications even if the value is unchanged.

## Example: Full Implementation

```tsx
import React, { useCallback, useMemo, useState } from "react";
import {
  SpreadsheetProvider,
  CanvasGrid,
  useSpreadsheetState,
} from "@rowsncolumns/spreadsheet";
import { useVersionComparison } from "@rowsncolumns/version-comparison";

const VersionComparisonApp = () => {
  // Manage comparison mode yourself
  const [isComparing, setIsComparing] = useState(false);

  const [sheetData, setSheetData] = useState(currentSheetData);
  const previousSheetData = useMemo(() => snapshotSheetData, []);

  const { getCellData, activeSheetId, ...spreadsheetProps } = useSpreadsheetState({
    sheetData,
    onChangeSheetData: setSheetData,
    // ... other props
  });

  const getPreviousCellData = useCallback(
    (sheetId, rowIndex, columnIndex) =>
      previousSheetData[sheetId]?.[rowIndex]?.values?.[columnIndex] ?? null,
    [previousSheetData]
  );

  // Pass null when not comparing to disable diffing
  const { getCellDiff } = useVersionComparison({
    getCellData,
    getPreviousCellData: isComparing ? getPreviousCellData : null,
    previousCellXfs,
    cellXfs
  });

  return (
    <SpreadsheetProvider>
      <div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
        {/* Comparison toolbar */}
        <div style={{ padding: 8, borderBottom: "1px solid #ddd" }}>
          {!isComparing ? (
            <button onClick={() => setIsComparing(true)}>Compare with Previous</button>
          ) : (
            <button onClick={() => setIsComparing(false)}>Exit Comparison</button>
          )}
        </div>

        {/* Spreadsheet grid */}
        <CanvasGrid
          sheetId={activeSheetId}
          {...spreadsheetProps}
          getCellData={getCellData}
          isComparing={isComparing}
          getCellDiff={getCellDiff}
        />
      </div>
    </SpreadsheetProvider>
  );
};
```

## Related Features

* [Undo/Redo](https://docs.rowsncolumns.app/configuration/features/undo-redo) - Track and revert changes
* [Real-time Data](https://docs.rowsncolumns.app/configuration/features/real-time-data) - Collaborative editing
* [Cell Renderer](https://docs.rowsncolumns.app/configuration/features/cell-renderer) - Custom cell rendering
