datagrok-selection
Manipulate row selection (`df.selection`) on a Datagrok DataFrame via the datagrok_exec tool — set, clear, invert, add to, remove from, intersect, and read the selection mask. Also covers current row
SKILL.mdpackages/Grokky/dockerfiles/claude-runtime/plugin/skills/datagrok-selection/SKILL.md
GitHubdatagrok-selection
The active DataFrame is t; row selection lives at t.selection (a DG.BitSet).
The number-one trap: t.selection.setAll(false) clears the selection.
setAll(true) selects every row. That is the opposite of filter, where
setAll(true) is the cleared / no-filter state.
Quick reference
| Need | Code |
|---|---|
| select all rows | t.selection.setAll(true) |
| clear the selection | t.selection.setAll(false) |
| invert | t.selection.invert() |
| select by predicate (replace) | t.selection.init(i => pred(i)) |
| select by index list (replace) | t.selection.setAll(false, false); for (const i of idx) t.selection.set(i,true,false); t.selection.fireChanged(); |
| add by predicate (union) | t.selection.or(DG.BitSet.create(t.rowCount, i => pred(i))) |
| remove indices | for (const i of idx) t.selection.set(i,false,false); t.selection.fireChanged(); |
| intersect with predicate | t.selection.and(DG.BitSet.create(t.rowCount, i => pred(i))) |
| count selected | t.selection.trueCount |
| indices of selected rows | t.selection.getSelectedIndexes() (returns Int32Array) |
is row i selected? |
t.selection.get(i) |
| any row selected? | t.selection.anyTrue |
| selected rows as a new DF | t.clone(t.selection) |
| filter → selection (select visible) | t.selection.copyFrom(t.filter) |
| selection → filter (hide unselected) | t.filter.copyFrom(t.selection) |
| move current row | t.currentRowIdx = i (validate 0 <= i < t.rowCount) |
| current row object / cell | t.currentRow, t.currentCell |
| mouse-over row | t.mouseOverRowIdx |
| subscribe to selection changes | view.subs.push(t.onSelectionChanged.subscribe(() => …)) |
The polarity table — opposite of filter
Selection and filter use the same DG.BitSet class, but the meaning of true
differs, so "clear" goes in opposite directions:
| Action | Filter (df.filter) |
Selection (df.selection) |
|---|---|---|
| clear (return to default) | setAll(true) (show all) |
setAll(false) (none selected) |
meaning of bit === true |
row passes (visible) | row is selected |
meaning of bit === false |
row hidden | row not selected |
The pair to memorize, side by side:
t.filter.setAll(true); // clear filter → every row visible
t.selection.setAll(false); // clear selection → none selected
Predicate polarity matches across skills: in both t.filter.init(pred) and
t.selection.init(pred), returning true includes the row. Only the clear
idiom flips.
Selection vs current row vs mouse-over
Three independent row pointers — none is "the selected row" in the singular:
| Concept | API | Cardinality | Event |
|---|---|---|---|
| Selection | t.selection (BitSet) |
many rows (set) | t.onSelectionChanged |
| Current row | t.currentRowIdx / t.currentRow |
one row (focus) | t.onCurrentRowChanged |
| Mouse-over | t.mouseOverRowIdx |
one row (transient) | t.onMouseOverRowChanged |
Setting t.currentRowIdx = 5 does not select row 5; selecting rows does
not move the current row. When a user says "highlight row 5", the intent is
ambiguous — pick the path matching context ("scroll to" → current row;
"mark" → selection) or surface the ambiguity.
Reading selection state
| Need | Code |
|---|---|
| rows currently selected | t.selection.trueCount |
| rows not selected | t.selection.falseCount |
| total rows | t.selection.length (same as t.rowCount) |
| indices of selected rows | t.selection.getSelectedIndexes() → Int32Array |
is row i selected? |
t.selection.get(i) |
| any selected? | t.selection.anyTrue |
| any unselected? | t.selection.anyFalse |
| next / prev set bit | t.selection.findNext(i, true) / findPrev(i, true) |
// How many rows are currently selected?
return {selected: t.selection.trueCount, unselected: t.selection.falseCount, total: t.rowCount};
getSelectedIndexes() caches its result. The cache invalidates on public
mutators (set, init, setAll, and/or/xor/andNot, invert, copyFrom).
If you mutate via getBuffer(), call fireChanged() to invalidate.
Programmatic selection — modes
The four modes map directly to BitSet operations:
| Mode | BitSet op | Pattern |
|---|---|---|
| replace | init(pred) / copyFrom(other) |
new selection replaces current |
| add | or(other) |
union with current |
| remove | andNot(other) |
subtract from current |
| intersect | and(other) |
keep only rows in both |
Predicate
t.selection.init(pred) is the fast, buffer-direct path for replace:
// Select rows where age > 40 (replace).
const age = t.getCol('age');
t.selection.init(i => (age.get(i)) > 40);
return {selected: t.selection.trueCount};
For add / remove / intersect with a predicate, build a fresh BitSet and combine:
// Add to the current selection (union, don't replace).
const act = t.getCol('activity');
t.selection.or(DG.BitSet.create(t.rowCount, i => (act.get(i)) > 7));
Index list
t.selection.set(i, x, notify) writes one bit. Pass notify=false in a
loop, then call fireChanged() once — otherwise N writes fire N events:
// Select explicit indices (replace). Zero the buffer, set bits with notify=false,
// fire one change at the end.
const idx = [0, 3, 5, 7, 11];
t.selection.setAll(false, false);
for (const i of idx) t.selection.set(i, true, false);
t.selection.fireChanged();
Anti-pattern (laggy UI):
// BAD — fires N events.
for (const i of indices) t.selection.set(i, true);
From another BitSet
t.selection.copyFrom(other); // replace
t.selection.or(other); // add
t.selection.andNot(other); // remove
t.selection.and(other); // intersect
Clearing & inverting
// Deselect everything. setAll(FALSE), not TRUE.
t.selection.setAll(false);
// Flip the current selection.
t.selection.invert();
t.selection.setAll(true) selects every row — including rows currently
hidden by the filter. For "select all currently-visible rows", copy from the
filter (see below).
Selected rows as a new DataFrame
const subset = t.clone(t.selection);
Same shape as t.clone(t.filter) — different mask:
| Idiom | Mask used | Source DF modified? |
|---|---|---|
t.clone(t.filter) |
t.filter |
no — clone of visible rows |
t.clone(t.selection) |
t.selection |
no — clone of selected rows |
// Open selected rows as a new table view.
const subset = t.clone(t.selection);
grok.shell.addTableView(subset);
If the selection is empty, clone returns a zero-row DataFrame (no error).
Guard with t.selection.anyTrue first for user-facing flows.
Cross-skill bridges — selection ↔ filter
| Idiom | Effect | Phrasing it answers |
|---|---|---|
t.filter.copyFrom(t.selection) |
hide non-selected rows in place | "show only", "filter to the selection" |
t.selection.copyFrom(t.filter) |
mark every visible row as selected | "select all currently-visible rows" |
Critical disambiguation:
- "select rows [by / where / top N]" → set
t.selectionviat.selection.init(pred)or index-list pattern. - "extract" / "copy" / "open as new table" / "give me a new table" →
t.clone(t.selection)(new DataFrame; original unchanged).
t.selection.copyFrom(t.filter) fires onSelectionChanged but does NOT fire
a filter event; the reverse holds for t.filter.copyFrom(t.selection).
Current row
// Focus row 5 (the platform usually does this on click; rarely needed in scripts).
if (5 >= 0 && 5 < t.rowCount)
t.currentRowIdx = 5;
t.currentRow (a Row proxy) and t.currentCell (a Cell) follow
currentRowIdx — read them on demand. Setting currentRowIdx = -1 is the
valid "no current row" state.
Describing the current selection
No built-in "describe selection" — hand-roll a structured report: count, capped index list, sample row from the first selected index.
// Describe the current selection.
const sel = t.selection;
const all = sel.getSelectedIndexes();
const MAX_INDEXES = 50;
const MAX_SAMPLE_COLS = 12;
const indexes = Array.from(all.slice(0, MAX_INDEXES));
const out = {
count: sel.trueCount,
total: t.rowCount,
indexes,
currentRowIdx: t.currentRowIdx,
sample: undefined,
};
if (all.length > 0) {
const firstIdx = all[0];
const cols = t.columns;
const sample = {};
const colCap = Math.min(cols.length, MAX_SAMPLE_COLS);
for (let c = 0; c < colCap; c++) {
const col = cols.byIndex(c);
sample[col.name] = col.get(firstIdx);
}
out.sample = sample;
}
return out;
Selection event lifecycle
One event — unlike filter:
| Event | When |
|---|---|
t.onSelectionChanged |
Any mutation on t.selection (set, init, setAll, and/or/xor/andNot, invert, copyFrom). Same observable as t.selection.onChanged. |
// Log selection count whenever it changes.
const sub = t.onSelectionChanged.subscribe(() => {
console.log(`selected: ${t.selection.trueCount}`);
});
view.subs.push(sub);
In a viewer / widget, push onto viewer.subs[] so the subscription cancels on
detach (DG.Widget does this automatically).
Coalescing. Every BitSet mutator takes notify: boolean = true. Pass
false in a loop and call fireChanged() once at the end:
for (let i = 0; i < indices.length; i++)
t.selection.set(indices[i], true, false); // notify = false
t.selection.fireChanged(); // one coalesced event
Common patterns
// Toggle selection on a single row.
const i = 5;
t.selection.set(i, !t.selection.get(i));
// Top N by a column — sort, slice, set.
const idx = Array.from(t.getSortedOrder(['activity'], [false])).slice(0, 5);
t.selection.setAll(false, false);
for (const i of idx) t.selection.set(i, true, false);
t.selection.fireChanged();
// "Select what I see" → narrow → "filter to what I just selected" pipeline.
t.selection.copyFrom(t.filter); // visible → selection
const flag = t.getCol('flag');
t.selection.and(DG.BitSet.create(t.rowCount, i => flag.get(i) === 'ok')); // narrow
t.filter.copyFrom(t.selection); // push back to filter
Anti-patterns
t.selection.setAll(true)to "clear" the selection. That selects every row — the opposite of what filter's clear does. Uset.selection.setAll(false).t.filter.setAll(false)to "clear" the filter. Hides every row. Uset.filter.setAll(true). The cross-skill pair must be memorized together.t.rows.select(rowPred)for "replace selection" in perf-critical code. Functional, but iterates theRowListand per-bitset— materially slower thant.selection.init(pred), which is buffer-direct.t.selection.init(...)to add a condition to the selection.initzeros the buffer first. To extend, OR inDG.BitSet.create(t.rowCount, pred).- Per-bit
setin a loop withoutnotify=false. N writes → N events → laggy UI. Alwaysset(i, x, false)in the loop andfireChanged()at the end. t.selection.setAll(true)when the user means "select all visible". That includes filtered-out rows. Uset.selection.copyFrom(t.filter).- Combining selections across DataFrames of different row counts.
dfA.selection.and(dfB.selection)only makes sense if rows are aligned 1:1. Cross-table sync by key columns isgrok.data.linkTables(...). t.rows.removeWhereIdx(pred)when asked to "remove from the selection". That deletes rows from the DF entirely. To narrow the selection, useandNotorset(i, false, false)in a loop.
Out of scope
- Filter operations.
t.filterlives indatagrok-filtering. Same BitSet API, opposite clear polarity — see the polarity table above. - Viewer-driven interactive selection. Lasso, grid click, shift-extend,
ctrl-toggle — dispatched via
BitSet.handleClick(rowPred, mouseEvent, ...). Covered bydatagrok-viewers. - Cross-table selection sync.
grok.data.linkTables(t1, t2, k1, k2, [SYNC_TYPE.SELECTION_TO_SELECTION, ...])establishes long-lived sync. - Generic DataFrame cloning. For "copy of this DF with these columns" (no
selection involved), see
datagrok-df-and-columns.