pf_app/ui/vendor
Paul Trowbridge 41e76978f4 Let a blank tree label take the click as well as draw the caret
The class fix rendered the `+` but the node still would not open, and the
caret did not light on hover. `tbody th:empty { pointer-events: none }`
makes the padding cells left of a tree label inert, and a label cell whose
group key is null or "" is `:empty` too — its caret is a `::before`, which
does not count as content — so the cell received no pointer events at all.

Exclude the tree classes from that rule. `row-hover.css` reads `:empty` the
same way in six border selectors; those are a hover tint, left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 00:19:15 -04:00
..
0001-apply-depth-fields-on-config-update.patch Carry a patch for the engine dropping depth on a config update 2026-09-17 03:29:28 -04:00
perspective-dev-client-5.4.0.tgz Let a blank tree label take the click as well as draw the caret 2026-09-22 00:19:15 -04:00
perspective-dev-server-5.4.0.tgz Vendor a Perspective build that applies depth on a config update 2026-09-17 10:47:09 -04:00
perspective-dev-viewer-5.4.0.tgz Let a blank tree label take the click as well as draw the caret 2026-09-22 00:19:15 -04:00
perspective-dev-viewer-datagrid-5.4.0.tgz Let a blank tree label take the click as well as draw the caret 2026-09-22 00:19:15 -04:00
PROVENANCE.txt Let a blank tree label take the click as well as draw the caret 2026-09-22 00:19:15 -04:00
README.md Let a blank tree label take the click as well as draw the caret 2026-09-22 00:19:15 -04:00
rebuild-perspective.sh Let a tree node with a blank group key expand 2026-09-22 00:08:56 -04:00

Vendored Perspective

pf_app runs a patched build of Perspective. Upstream's C++ engine has always implemented column-axis expand/collapse — t_ctx2::set_depth(HEADER_COLUMN, …) and open/close(HEADER_COLUMN, idx) are fully written — but nothing above C++ could reach it: set_column_pivot_depth() was never called, and View<t_ctx2>::expand/collapse hardcoded HEADER_ROW. The patch is wiring, not new engine logic.

It buys two things the released packages cannot do at all:

  • split_by_depth in ViewConfig, the split_by counterpart to group_by_depth
  • expand_column() / collapse_column(), so one column branch can fold to its subtotal while its siblings stay expanded — the Excel behaviour

Source: https://github.com/fleetside72/perspective, branch expand-blank-tree-nodes — the tip of the stack, carrying column-axis-expand-collapse and apply-depth-on-config-update beneath it. See PROVENANCE.txt for the exact commit these tarballs were built from.

Why tarballs and not npm

The feature is not released upstream. Until it is, the four packages are built from the fork and committed here as npm tarballs. npm install expands them exactly as it expands anything from the registry — no special tooling, and pf.sh deploy works unchanged. A deploy machine needs node and nothing else: no emscripten, no cmake, no protoc, no Rust.

All four move together, never a subset. Perspective couples loader, package versions, data format and apache-arrow; vendoring a partial set reintroduces exactly the drift that causes trouble.

Changing the engine

./rebuild-perspective.sh          # builds the fork, repacks, rewrites PROVENANCE.txt
cd .. && npm install ./vendor/perspective-dev-*-5.4.0.tgz   # by path — see below
git add vendor package-lock.json && git commit

Push the fork first — the script warns if the source tree is dirty, because a tarball built from uncommitted code has no recoverable source.

The build itself needs cmake >= 3.29.5, protoc >= 22 (its version silently selects which protobuf source tree gets cloned), pnpm, and the Rust nightly the repo pins. Roughly 40 minutes cold. Only ever on a machine changing the engine.

npm install alone does not pick up a rebuilt tarball

The version in the filename never changes, and package-lock.json pins an integrity hash per package — so npm satisfies the dependency from its own cache and the freshly built tarball on disk is never read. Nothing fails; the old code is simply still there. Re-resolve by path instead, which recomputes the hashes and rewrites the lock:

cd ui
rm -rf node_modules/@perspective-dev
npm install ./vendor/perspective-dev-{client,server,viewer,viewer-datagrid}-5.4.0.tgz

Check the install rather than trusting it — the packages ship their TypeScript sources, so the change is readable without unminifying anything:

grep -n is_label node_modules/@perspective-dev/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts

cargo check on this patch was clean — the 125 errors it reports without protoc installed are unresolved generated protobuf modules, none of them in view_config.rs.

applyDepth() in Forecast.jsx reads the config back after restoring it and falls back to the imperative view.set_depth() when the value did not stick. With the patched engine installed that fallback should no longer run; it is kept for now so an unpatched engine still works, and can be removed once the declarative path is confirmed.

Getting rid of this

This is a fork, with the maintenance that implies. The exit is upstream taking the change — the patch is small and additive, and the engine work is already theirs. When a release ships it, delete this directory and put normal version ranges back in ui/package.json.

Applied: expand a tree node whose group key is blank

In the vendored tarballs as of the 2026-09-22 build, from branch expand-blank-tree-nodes (which is apply-depth-on-config-update plus this). Upstream bug, in packages/viewer-datagrid/src/ts/style_handlers/table_cell/row_header.ts.

A tree row's header is the label cell padded on its left by one blank cell per level above it, and cell_style_row_header told padding from label by testing the cell's own text. A node whose group key is null or '' therefore read as padding: no psp-tree-label class, so no + caret — and because createClickListener in event_handlers/header_click.ts gates expand/collapse on that same class, no way to open the node at all. The node was in the traversal and its children existed; only the affordance was missing. On gs.osm_skinny that is partgroup, which has 93k null and 111k empty rows.

The fix tells them apart structurally — the label is the last defined entry in row_header — which is how is_collapse two lines below already reads the following row. Flat rollup (group_rollup_mode: 'flat') writes each level into its own cell with no padding, so there is nothing structural to test there and it keeps the text test.

The class was only half of it. tbody th:empty { pointer-events: none } in regular_table.css makes the padding cells inert, and a blank label cell is :empty as well — a ::before caret is not content — so the fixed node drew its +, then ignored the click and would not even take the hover. The rule now excludes the tree classes, which is the distinction it was reaching for: a padding cell never carries one.

row-hover.css uses :empty the same way in six border selectors. Those are a hover tint and are left alone; a row whose first level is blank loses a border highlight, and nothing else.

Separate from the data question: null and '' group as two distinct rows that look identical, so a COALESCE(NULLIF(col,''), '(none)') at load time is still worth doing on its own merits.

Applied: depth fields on a config update

0001-apply-depth-fields-on-config-update.patch is in the vendored tarballs as of the 2026-09-17 build, from branch apply-depth-on-config-update. It fixes an upstream bug in rust/perspective-client/src/rust/config/view_config.rs: ViewConfig::apply_update applies ten fields and neither group_by_depth nor split_by_depth is among them. So a depth can be set when a view is created (table.view({ group_by_depth: 1 }) — which is what the fork's own depth_test.mjs exercises) but never through restore(), which is how a viewer changes its own configuration. The field arrives, deserializes, and is discarded before the engine sees it.

group_by_depth and the omission are both upstream; the fork mirrored split_by_depth alongside it faithfully, including the omission.

Note the semantics, from server.cpp:

ctx1->set_depth(row_pivot_depth - 1);                        // one-sided
ctx2->set_depth(t_header::HEADER_ROW, row_pivot_depth - 1);  // two-sided

The config field counts levels to show; view.set_depth() counts the boundary below them. So group_by_depth: n equals set_depth(n - 1), and Forecast.jsx sends d + 1.

To apply:

cd $PSP_DIR                       # default ~/perspective
git apply /path/to/pf_app/ui/vendor/0001-apply-depth-fields-on-config-update.patch
./ui/vendor/rebuild-perspective.sh
cd ui && npm install

Host tools this tree needs

The repo pins its Rust toolchain (nightly-2026-06-01), Emscripten (4.0.9) and Binaryen (132) exactly, and pins nothing for the host tools — no packageManager, no .nvmrc. So npm i -g pnpm and pip install cmake both give versions this tree was not written against. The script's preflight now rejects them, but for the record:

  • pnpm 10.x. 11+ rejects a repeated --if-present, which sh_perspective.mjs:189 emits once per package in scope.
  • cmake 3.x, not 4. Both satisfy the >= 3.29.5 floor; CMake 4 removed support for cmake_minimum_required < 3.5, which the C++ dependencies still declare, and fails partway through the Arrow build.
  • protoc >= 22 (33.2 known good).
  • pnpm pack, not npm pack. These packages depend on each other as workspace:^, and only pnpm rewrites that to a concrete version. An npm pack tarball is rejected at install: Unsupported URL Type "workspace:".

Two commits on the build branch are local build concessions rather than fixes, and should be dropped before the real patch goes anywhere upstream: removing the postinstall:playwright step (it runs playwright install --with-deps, which apt-installs system libraries and so needs root), and filling in the allowBuilds block pnpm 12 demanded.