Spreadsheet Lens

A spreadsheet as a lens [Foster et al. 2007] of the containing notebook. The cell at B3 is a variable literally named B3; so the view is a pure function of the module. Delete the sheet() call and the underling dataflow keeps working. Edit cells in either the sheet or the enclosing notebook.

edit
A1=
md`### A spreadsheet as a lens over module scope`

A spreadsheet as a lens over module scope

1. The coordinate is the name. A3 below is a variable literally named A3. Position is a parse of the name, so nothing about the layout is stored anywhere.

120
340
95

2. A formula is an ordinary cell body. Select A5: the bar reads d3.sum(A3:C3); what is stored is d3.sum([A3, B3, C3]). The = is Observable's name binding and d3 is already in scope.

555.00

3. A named cell is absolute. taxRate = 0.2 is a definition; C7 = taxRate is a placement — it puts a named cell on the grid, labels it, and never shifts on copy.

taxRate20.0%

4. Copy shifts A1-shaped names only. Copy C9 and paste at E9: the A9 follows, the $A$9 and the taxRate do not. $ pins a component and is legal in a JS identifier.

200
50
$40.00
$40.00

5. A cell can hold anything. Drag the view — it is a live cell, so A12 follows. Markdown is a DOM node, and every builtin is in scope.

A
B
C
D
E
F
G
H
I
J
K
L
M
N
1
2
3
4
5
6
7
8
9
10
11
12
edit

Getting started

import {sheet} from '@tomlarkworthy/spreadsheet'

widget = sheet(runtime, {
  invalidation,     // required for teardown
  module,           // REQUIRED — the module whose scope the sheet projects
  format: {},       // { B3: "0.00" } — presentation state, owned by this cell
  size: {},         // { cols: { B: 140 }, rows: { "3": 40 } } — explicit sizes; unnamed = auto
  persist: true     // rewrite own source when format or size changes
})

Keys. Arrows move; Shift+arrows select a range. Enter or F2 edits in place; typing starts a new value. While editing, = then an arrow picks a reference, Shift+arrow widens it to a range, and a click on the grid does the same. Tab commits to the right, Enter to the row below. Delete clears the selection; Cmd+C / X / V copy, cut and paste with relative addressing; Cmd+Z and Shift+Cmd+Z undo and redo. Drag the handle at the selection corner to fill. Drag a column or row divider to size it; double-click it to return to auto.

edit

How it works

Observable cell syntax already is formula-bar syntax: what a spreadsheet user types after = is the cell body, and the name comes from where they clicked. Recalculation is the reactive runtime, a circular reference is RuntimeError: circular definition, and a reference to a blank cell is an implicit variable ("A5 is not defined") that is taken over the moment A5 is defined — so formulas can be typed in any order.

Names as well as positions. Split definition from placement:

taxRate = 0.2        // definition — the semantic name
B1 = taxRate         // placement — "this lives at B1"

The sheet detects a placement cell (a positional name whose body is a bare reference to a non-positional name), renders the target's value at B1 and labels it. = B1 * 12 is then relative and shifts on copy; = taxRate * 12 is absolute and never shifts. Placement also puts any cell on the grid: D5 = revenuePlot.

Pinning a coordinate. $ is legal in a JS identifier, so $A$1, $A1 and A$1 are names too. The sheet defines each one it sees as an alias to the plain address, and paste leaves a pinned component alone — Excel's semantics, still with nothing stored outside the names.

Ranges. A4:C4 is not JavaScript, so the bar expands it before parsing into [A4, B4, C4] — an array of ordinary references, every one a real dependency — and reads a full-rectangle array literal back as a range for display. d3.sum(A4:C4) is stored as d3.sum([A4, B4, C4]). Written without spaces round the colon; x ? a1 : b2 stays a ternary.

Text. A bare name that resolves to nothing (Total), or words that do not parse (Total revenue), is stored as a string literal; the bar shows the quotes. A broken formula stays broken.

Overflow. Content spills into empty neighbours and clips at the first occupied cell — Excel's rule generalised from text to DOM nodes, done in CSS with nothing stored. A column is auto (the widest text value, capped) unless size names it; DOM values spill rather than widen.

Sparse. Only defined cells are variables and only defined cells have DOM. An empty sheet is zero of both; the grid is painted, not built.

edit
colName = ƒ(n)
edit
colIndex = ƒ(s)
edit
parseAddr = ƒ(name)
edit
formatAddr = ƒ(col, row)
edit
shiftAddr = ƒ(name, dc, dr)
edit
parseRef = ƒ(name)
edit
formatRef = ƒ(col, row, colAbs, rowAbs)
edit
cellRefs = ƒ(src)
edit
rewriteRefs = ƒ(src, fn)
edit
transposeCell = ƒ(src, dc, dr)
edit
formatValue = ƒ(value, spec)
edit
expandRanges = ƒ(…)
edit
collapseRanges = ƒ(src)
edit
sh_css = `.sh-frame { position: relative; box-sizing: border-box; width: 100%; height: 420px; resize: vertical; overflow: hidden; border: 1px solid var(--theme-foreground-faintest, #ddd); border-radius: 6px; background: var(--theme-background, #fff); font: 12px var(--sans-serif, system-ui); } .sh-bar { display: flex; align-items: stretch; gap: 6px; height: 26px; padding: 0 6px; border-bottom: 1px solid var(--theme-foreground-faintest, #ddd); background: var(--theme-background-alt, #fafafa); } .sh-bar .sh-addr { font: 11px var(--code, monospace); min-width: 52px; opacity: 0.7; display: flex; align-items: center; } .sh-bar .sh-eq { display: flex; align-items: center; opacity: 0.5; font: 12px var(--code, monospace); } /* the formula bar is ONE editor-5 CodeMirror for the whole sheet, retargeted on selection -- not cellEditor, which clones a hotbar shell per instance */ .sh-formula { flex: 1 1 auto; min-width: 0; overflow: hidden; position: relative; }Show 98 truncated lines
edit
sheet = ƒ(…)
edit
taxRate = 0.2
edit
edit
edit
A3 = 120
edit
B3 = 340
edit
C3 = 95
edit
edit
A5 = 555
edit
edit
C7 = 0.2
edit
edit
A9 = 200
edit
B9 = 50
edit
C9 = 40
edit
D9 = 40
edit
edit
edit
edit
viewof sheetModule = EventTarget {tag: Symbol()}
edit
bibliography = Object {foster2007lenses: Object}
edit
cite = ƒ(key)
edit

References

  1. Foster, J.N., Greenwald, M.B., Moore, J.T., Pierce, B.C. & Schmitt, A. (2007). Combinators for Bidirectional Tree Transformations: A Linguistic Approach to the View-Update Problem. ACM TOPLAS 29(3).
edit
edit
edit
edit
edit
edit
$A$9 = 200
edit