diff --git a/.claude/knowledge/api-map.md b/.claude/knowledge/api-map.md new file mode 100644 index 0000000000..bf7af55135 --- /dev/null +++ b/.claude/knowledge/api-map.md @@ -0,0 +1,635 @@ +# API map + +Canonical "for X, use Y" entries. Each row points at the JS-API symbol that +package authors should call, plus a note on common wrong choices. + +## Routing + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Reading the URL path inside an app function | A string input parameter annotated `{meta.url: true}` (header form) or `{metaUrl: true}` (decorator form). The runtime binds the path string into that parameter. | `window.location` / `document.location` — bypasses the function-level routing model. | js-api/src/decorators/functions.ts:101 | +| Reading URL query parameters | Additional named string inputs on the same function — they are bound by name from the query string. | Manual `URLSearchParams` parsing. | help/develop/how-to/apps/routing.md:27 | +| Setting a view's URL state | `view.path = '/segment1/segment2'` (relative to function base path). | `view.basePath = ...` — DEPRECATED (`@deprecated use path instead` in JS-API). | js-api/src/views/view.ts:170-171 | +| Making a view participate in URL dispatch | Override `View.acceptsPath(urlPath): boolean` and `View.handlePath(urlPath): void` in your subclass. | Manual hashchange listeners. | js-api/src/views/view.ts:173-178 | +| Marking a function as an app | Header annotation `//meta.role: app`. | Naming convention alone — the role tag is what the platform indexes. | packages/Alation/src/package.ts:32 | +| Overriding a package's URL segment | `package.json` → `"meta": { "url": "/your/segment" }`. | Hard-coding URLs in client code. | packages/UsageAnalysis/package.json | +| Overriding an app function's URL segment | Function-level header annotation `//meta.url: /alias` (use `/` alone to make it the package default). | Computing aliases at runtime. | packages/NodeJSDemo/src/package.ts:17-19 | +| Setting the active view | `grok.shell.v = view` (setter). | Manually swapping DOM nodes. | js-api/src/shell.ts:71 | +| Adding a new TableView from a DataFrame | `grok.shell.addTableView(table, dockType?, width?)` (returns `TableView`). | `new TableView(...)` — no public constructor. | js-api/src/shell.ts:258 | +| Loading a built-in demo dataset (for tutorials/scaffolds) | `grok.data.testData(name)` where `name` is a `DemoDatasetName` (`wells`, `demog`, `biosensor`, `random walk`, `geo`, `molecules`, `dose-response`). | Arbitrary strings — type system rejects them. | js-api/src/data.ts:153, js-api/src/const.ts:765-773 | +| Adding a scatter plot to a view | `view.addViewer(DG.VIEWER.SCATTER_PLOT, options)` or `view.addViewer(DG.Viewer.scatterPlot(options))`. | `view.scatterPlot(options)` — JSDoc-marked deprecated. | js-api/src/views/view.ts:559-564 | + +## User settings storage + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Persisting per-user key-value state across sessions | `grok.userSettings.add(name, key, value, isPrivate?)` (default `isPrivate=true`). Synchronous, returns `void`. | `grok.dapi.userDataStorage.*` — `@deprecated`, async legacy API. `localStorage` — bypasses the server; not synced across devices. | js-api/src/user_settings_storage.ts:23, js-api/src/dapi.ts:168 | +| Reading a single stored value | `grok.userSettings.getValue(name, key, isPrivate?)` → `string \| undefined` (synchronous). Wrap with `JSON.parse` for objects. | `await`ing the call — methods are synchronous, the await is harmless but misleading. | js-api/src/user_settings_storage.ts:62 | +| Reading the whole record under a name | `grok.userSettings.get(name, isPrivate?)` → `{[key: string]: string} \| undefined`. Iterate with `Object.keys`. | Treating the result as a JS `Map` (the article calls it that, but the type is a plain record). | js-api/src/user_settings_storage.ts:52 | +| Bulk-saving many entries at once | `grok.userSettings.addAll(name, data, isPrivate?)` (merge) or `grok.userSettings.put(name, data, isPrivate?)` (replace). `data` is `{[key: string]: string}`. | A `Map` instance — type is a plain object record. | js-api/src/user_settings_storage.ts:33,43 | +| Deleting one stored value | `grok.userSettings.delete(name, key, isPrivate?)` with the actual key. | Re-`put`-ing the whole record minus one key. | js-api/src/user_settings_storage.ts:72 | +| Wiping a whole storage namespace | Call `delete(name, null as any, isPrivate?)` (TS-typed as `string`, runtime accepts null) — Dart routes to the no-key endpoint. | Iterating keys and deleting one-by-one — extra round-trips. | js-api/src/user_settings_storage.ts:72, js-api/src/datagrok/build/web/grok_shared.dart.js:93263 | +| Sharing settings across all users | Pass `isPrivate: false` on every call. | A separate "shared/" key prefix in the private store — won't be visible to other users. | js-api/src/user_settings_storage.ts:21,31,41,50,60,70 | +| Storing structured objects | `grok.userSettings.add(name, key, JSON.stringify(obj))` then `JSON.parse(grok.userSettings.getValue(name, key))` on read. | Passing a non-string `value` — TS rejects, runtime stringifies inconsistently. | help/develop/how-to/data/user-settings-storage.md:36-47 | +| Storing values longer than 5000 characters | Split into N chunks, each under 5000 chars; store a metadata key with `{parts: N}`. See `ClinicalCase/src/utils/layout-utils.ts`. | A single `add` with the full payload — silently fails or rejects. | js-api/src/user_settings_storage.ts:9, packages/ClinicalCase/src/utils/layout-utils.ts:26-67 | + +## Access data + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Running a parameterized query from JS | `await grok.data.query(':', {param: value})` → `Promise` (or any scalar/typed result via `query`). Synchronous wrapper around the function dispatcher. | A 4th polling-interval argument — the article shows it but the signature only accepts 3 positional args (the 4th is silently ignored). The 3rd `adHoc` arg is `@deprecated` — omit it. | js-api/src/data.ts:255-262 | +| Calling any registered function (including a query) | `grok.functions.call(':', params?)` → `Promise`. Equivalent to `Package:Function()` typed in the console. | `eval` for parameterless functions when `call` is clearer; raw HTTP. | packages/ApiTests/src/functions/functions.ts:66 | +| Reading a file from a file share | `grok.functions.eval(\`OpenServerFile(":Home/data.csv")\`).then(t => t[0])` — returns `Promise`. Take `[0]`. | Manual `fetch` of internal URLs — bypasses the credentials/permissions layer. | packages/ApiSamples/scripts/scripting/grok-scripting.js:1, js-api/src/datagrok/build/web/grok_shared.dart.js:85499 | +| Proxying an external HTTP request through Datagrok | `grok.dapi.fetchProxy(url, fetchInit?, maxAge?)` → `Promise`. Same shape as `fetch`. Use `maxAge` (seconds) on GET/HEAD to enable server-side caching. | Direct `fetch(externalUrl)` from package code — hits CORS, no caching, no auth. | js-api/src/dapi.ts:189-209 | +| Loading a CSV from any URL into a DataFrame | `grok.data.loadTable(csvUrl)` → `Promise`. Compose with `_package.webRoot` for package-bundled assets. | Hand-rolled `fetch` + `parseCsv` when `loadTable` will do it in one call. | js-api/src/data.ts:173-175 | +| Parsing an in-memory CSV string | `grok.data.parseCsv(csv, options?)` → `DataFrame` (synchronous). Pair with `file.readAsString()` to customize headers/separator. | `DataFrame.fromCsv(csv)` when you need `CsvImportOptions`. | js-api/src/data.ts:166-168 | +| Reading the bytes/text of a `file`-typed input | `await file.readAsString()` → `Promise`; `await file.readAsBytes()` → `Promise`. Methods are on the `FileInfo` instance, no args. | `grok.dapi.files.readAsText(file)` — works, but the FileInfo method is the more direct route inside a function whose input is typed `file`. | js-api/src/entities/table-info.ts:107-114 | +| File operations on a file-share path | `grok.dapi.files.{readAsText, readAsBytes, writeAsText, exists, list, rename, move, delete}(target, …)`. `target` may be a path string, a `FileInfo`, or a file-share connection GUID. All async. | Mixing path string and FileInfo arguments inconsistently across calls — keep one shape per code path. | js-api/src/dapi.ts:1287-1290,1418-1465, packages/ApiSamples/scripts/dapi/files.js | +| Loading a generic demo dataset (one of seven) | `grok.data.testData('')` → `DataFrame` (synchronous). Names: `wells`, `demog`, `biosensor`, `random walk`, `geo`, `molecules`, `dose-response`. | `grok.data.demo.()` is the typed shortcut for the same data — pick whichever reads better. | js-api/src/data.ts:153, js-api/src/const.ts:765-773 | +| Loading a named demo file by relative path | `grok.data.getDemoTable('sensors/eeg.csv')` → `Promise`. Path is relative to the demo root. | `grok.data.testData(...)` — that's for the seven generic generators, not file-on-disk demos. | js-api/src/data.ts:157-159 | +| Opening a registered table by GUID | `grok.data.openTable(id)` → `Promise`. | `grok.data.files.openTable(path)` — different method, takes a file path, not a GUID. Two `openTable` overloads exist on different objects. | js-api/src/data.ts:222 | +| Resolving package-bundled asset URLs | `${_package.webRoot}` (the trailing slash is included by the getter). | Hard-coded `/packages//...` URLs — break under `meta.url` overrides. | js-api/src/entities/misc.ts:79-84 | +| Declaring a package data connection | JSON file under `connections/`. Required: `parameters`, `dataSource`. Optional: `name` (defaults to filename), `description`, `tags`, `credentials.parameters`. | Embedding `login`/`password` inside `parameters` or `connString` — bypasses credentials management. | help/develop/how-to/db/access-data.md:34-114, packages/Chembl/connections/chembl.json | +| Picking a `dataSource` connector value | One of: `Postgres`, `PostgresDart`, `MariaDB`, `MySQL`, `MS SQL`, `Oracle`, `Snowflake`, `ClickHouse`, `Neo4j`, `Access`, `Files` (set is open — see `help/access/databases/connectors/`). | Lower-case or alternate spellings (`postgres`, `mssql`) — lookup is exact. | packages/DBTests/connections/*.json | +| Declaring a SQL query | `.sql` under `queries/`. Header annotations: `--name:`, `--connection: :`, `--description:`, `--input: `. `--end` separates multiple queries in one file (omit if one query per file). Omit `--connection` if the package has only one. | Multiple queries per file when you also want a `.js` post-process or `.layout` — those associate by single basename. | packages/Chembl/queries/queries.sql:1-39, help/develop/how-to/db/access-data.md:189-213 | +| Storing per-package credentials after deploy | POST `$(GROK_HOST)/api/credentials/for/$(PACKAGE_NAME).$(CONNECTION_NAME)` with JSON body `{"login":"…","password":"…"}` and headers `Authorization: $(API_KEY)`, `Content-Type: application/json`. | Committing creds in `connections/*.json` — they get redacted but the workflow assumes none are stored locally. | help/develop/how-to/db/access-data.md:116 | +| Storing package-level credentials (NOT bound to a connection) | POST `$(GROK_HOST)/api/credentials/for/$(PACKAGE_NAME)` — same headers/body shape as the dotted form; the entity name without a dot targets the package itself. Body keys are arbitrary (`{"apiKey":"..."}`, `{"accessKeyId":"...","secretAccessKey":"..."}`, etc.). | Storing API keys in package settings — settings are world-readable; credentials respect the owner group. | help/develop/how-to/packages/manage-credentials.md:22-40, packages/NLP/aws/nlp-user.py:47-53 | +| Reading the current package's credentials from JS | `const c = await _package.getCredentials(); if (c == null) { /* not set, or caller not in owner group */ } else { c.parameters[''] }`. Returns `Promise`. | `c.openParameters` for actual use — that's the redacted display copy. Use `parameters` to obtain real secrets. | js-api/src/entities/misc.ts:163-166, js-api/src/entities/data-connection.ts:396-415, packages/NLP/src/package.ts:104-115 | +| Reading credentials of any entity (incl. a connection) from JS | `await grok.dapi.credentials.forEntity(entity)` → `Promise`. Pair with `grok.dapi.credentials.save(c)` after mutating `c.parameters`. | Direct `fetch` to `/api/credentials/...` from package code when running in-platform — the dapi wrapper handles auth. | js-api/src/dapi.ts:624-642 | +| Creating a service user for programmatic credential rotation | UI path `Manage \| Users \| Actions \| Add Service User` — generates a login and an API key; embed the API key in the `Authorization` header of rotation scripts (see NLP). | Reusing a human user's API key in a rotation cron — service users isolate the trust boundary. | help/develop/how-to/packages/manage-credentials.md:42-44, packages/NLP/aws/nlp-user.py:50-53 | +| Sharing data connections | Share the QUERY — its connection (and any web/OpenAPI queries) auto-share. Sharing the connection alone does NOT propagate to its queries. | Sharing connection then assuming queries follow — the rights flow is one-way. | help/develop/how-to/db/access-data.md:218-222 | + +## Database in Docker container + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Connecting to a DB that ships inside a package's Docker container | A connection JSON under `connections/` with `parameters.server: "${:}"`. Use canonical PascalCase casing — `DBTests` package → `Dbtests:Dbtests`. | The Datagrok UI's "New Connection" dialog — Docker-bound connections can only be declared programmatically. | help/develop/how-to/db/db-in-docker.md:14-22, packages/DBTests/connections/postgres-test-docker.json:6 | +| Selecting the database/schema inside the container | `parameters.db: ""` — must match the value of `POSTGRES_DB` (or equivalent) baked into the Dockerfile. | Embedding the database into `connString` — `connString` short-circuits all other parameters, including the Docker placeholder. | packages/DBTests/connections/postgres-test-docker.json:7, packages/DBTests/dockerfiles/Dockerfile:4 | +| Specifying the container's port | OMIT `port` from `parameters` entirely — Datagrok resolves it from the running container. | Setting `port` to whatever you put in `EXPOSE` — conflicts with dynamic resolution. | help/develop/how-to/db/db-in-docker.md:16-17 | +| Wiring the container's friendly name when the package has multiple Dockerfiles | Use `${:-}` — friendly name is `-` when `dockerfiles/` has subfolders each with a Dockerfile. | Using just the folder name. | help/develop/how-to/db/db-in-docker.md:21-22 | +| Exposing the DB port from the Dockerfile | Exactly one `EXPOSE ` directive. | Multiple `EXPOSE` lines — the platform allows only one exposed port per image. | help/develop/how-to/packages/docker-containers.md:20, packages/DBTests/dockerfiles/Dockerfile:6 | +| Seeding initial data into the containerized DB | Postgres: `COPY *.sql /docker-entrypoint-initdb.d/` in the Dockerfile (image's standard init hook). Add `HEALTHCHECK CMD pg_isready ...` so the platform sees a ready signal. | Running an initialization script via the Datagrok function dispatcher post-deploy — racy and fires only once. | packages/DBTests/dockerfiles/Dockerfile:5,8-9 | +| Tuning startup behavior of a DB container | `dockerfiles//container.json` next to the Dockerfile. For DB workloads: `on_demand: true` (lazy start), `shutdown_timeout: ` (idle shutdown), `cpu`/`memory`/`storage` caps. | Hard-coding resource limits in the Dockerfile — they're config, not image content. | help/develop/how-to/packages/docker-containers.md:41-86, packages/DBTests/dockerfiles/container.json | +| Carrying the credentials that match the seeded DB | Connection JSON `credentials.parameters: { login, password }` — values must match the Dockerfile's `ENV POSTGRES_USER`/`ENV POSTGRES_PASSWORD`. | Embedding credentials in `parameters.connString` or in `parameters.server` — bypasses the credentials store. | packages/DBTests/connections/postgres-test-docker.json:9-13, packages/DBTests/dockerfiles/Dockerfile:2-3 | + +## Docker container HTTP/WebSocket integrations + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Resolving a package's container handle by friendly name | `(await grok.dapi.docker.dockerContainers.filter('').first())` returns a `DG.DockerContainer`; use its `.id` for proxy calls. Friendly name is `` (single-Dockerfile) or `-` (multi-folder). | Hard-coding a container UUID — IDs change per deploy. Also: `grok.dapi.dockerfiles.*` — that namespace doesn't exist (DRIFT-049). | js-api/src/dapi.ts:1011, packages/Reinvent4/src/package.ts:51, packages/NodeJSDemo/src/app/todo_app.ts:59 | +| Sending an HTTP request to a package's container | `await grok.dapi.docker.dockerContainers.fetchProxy(containerId, path, params?)` — same shape as `fetch`; `params` is `RequestInit`. Default method `GET`; `credentials` is forced to `'include'`. | `fetch(, ...)` — the container is not reachable directly; only the platform proxy is. Or `grok.dapi.fetchProxy(...)` — that's the unrelated CORS-bypass shim for arbitrary URLs. | js-api/src/dapi.ts:1083-1089, packages/MolTrack/src/services/moltrack-docker-service.ts:28-73, packages/Reinvent4/src/package.ts:61 | +| Opening a WebSocket to a package's container | `await grok.dapi.docker.dockerContainers.webSocketProxy(containerId, path, timeout=60000)` — resolves only after the server emits a `"CONNECTED"` text message. Raise `timeout` for `on_demand: true` containers (cold-start latency). | The synchronous `webSocketProxySync(...)` if you can `await`; the socket isn't ready to send when sync returns. | js-api/src/dapi.ts:1100-1145, packages/Notebooks/src/package.js:446 | +| Programmatic container lifecycle | `dockerContainers.run(id, awaitStart=true)` to start (await ready); `dockerContainers.stop(id, awaitStop=false)` to stop. Pre-check `container.status !== 'started' && container.status !== 'checking'` before calling `run`. | Calling `run` unconditionally — wastes seconds on already-started containers. | js-api/src/dapi.ts:1057-1069, packages/ApiTests/src/packages/docker.ts:9-10,22-23 | +| Reading container runtime logs | `dockerContainers.getContainerLogs(containerId, limit=10000)` returns the last `limit` lines of stdout/stderr. | Mounting log files via `grok.dapi.files.readAsText` — logs aren't surfaced through the file share. | js-api/src/dapi.ts:1204-1206, packages/ApiTests/src/packages/docker.ts:24 | +| Reading image build logs | `(await grok.dapi.docker.dockerImages.filter('').first()).logs` — the `logs` property on the `DockerImage` entity is the build-log accessor. Also visible in the `Manage > Dockers` UI's Property pane. | A standalone `dockerImages.getBuildLogs(id)` method — does NOT exist. `DockerImagesDataSource` only declares `revalidate(imageId)` and inherits `HttpDataSource` (filter/list). | js-api/src/dapi.ts:1024-1040, packages/ApiTests/src/packages/docker.ts:14-18 | +| Sizing a container before publish | `dockerfiles/[/]container.json` next to the Dockerfile. Defaults — `cpu: 0.25`, `gpu: 0`, `memory: 512`, `storage: 21`, `shm_size: 64`, `on_demand: false`, `shutdown_timeout: null`. Set `on_demand: true` for rarely-used workloads to save cluster resources. | Tuning resource limits in the Dockerfile — they're config, not image content. The file is optional; defaults apply if absent. | help/develop/how-to/packages/docker-containers.md:46-72, packages/Boltz1/dockerfiles/boltz/container.json | +| Passing entities/connections into the container as env vars | `container.json` with `"environmentVars": { "VAR": "#{x.:}" }` — platform JSON-serializes the resolved entity at start time. Use `#{x.}` to inject a same-package connection (credentials carried). | The article's `"env": {...}` field name — production packages and the Dart model both use `"environmentVars"` (DRIFT-050). Cross-package credentials — only same-package entities are reachable. | help/develop/how-to/packages/docker-containers.md:53-58,79-81, packages/MolTrack/dockerfiles/container.json, packages/NodeJSDemo/dockerfiles/todo-app/container.json | +| Exposing a port from the Dockerfile | Exactly one `EXPOSE ` line — the platform routes proxy traffic to that single port. | Multiple `EXPOSE` directives — only one port per image is supported. | help/develop/how-to/packages/docker-containers.md:20, packages/Admetica/dockerfiles/Dockerfile:50 | + +## Plugin Postgres database (db-in-plugin) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Declaring a custom Postgres DB owned by the plugin | A directory `/databases//` with `*.sql` migration files. The connection is auto-registered as `:`; no `connections/*.json` needed. | A `connections/*.json` file pointing at the platform Postgres — bypasses the auto-registration and migration framework. A Docker-bundled DB (`db-in-docker.md`) when you only need a schema in the platform's own DB. | help/develop/how-to/db/db-in-plugin.md:14-16, packages/HitTriage/databases/hitdesign | +| Naming the directory | A short lowercase identifier — it is reused as the Postgres SCHEMA name AND the connection name. Examples: `hitdesign`, `plts`, `moltrack`, `todo`. | Mixed case or hyphens — every canonical package uses lowercase. | packages/HitTriage/databases/hitdesign, packages/Plates/databases/plts, packages/MolTrack/databases/moltrack | +| Numbering migrations | 4-digit zero-padded prefix + underscore + descriptive name: `0000_init.sql`, `0001_add_thing.sql`. Files apply in lexicographical order. | A 3-digit prefix or no prefix — works (NodeJSDemo / MolTrack do it) but loses headroom past `999`. Inconsistent widths in the same directory — they sort wrong (`9_x.sql` > `10_x.sql`). | help/develop/how-to/db/db-in-plugin.md:28-30, packages/Biologics/databases/biologics | +| Granting query access to the auto-created DB user | At the END of `0000_init.sql`, emit `GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA TO :LOGIN;` and `GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA TO :LOGIN;`. The `:LOGIN` placeholder is substituted with the platform's connection user at deploy. | Granting to `CURRENT_USER` alone — that's the migration-time owner, not the runtime query user. Omitting GRANTs entirely — queries hit "permission denied". | packages/HitTriage/databases/hitdesign/0000_init.sql:18-20, packages/MolTrack/databases/moltrack/0000.sql:277-279 | +| Referencing the plugin DB from a query file | `--connection: :` (e.g. `--connection: HitTriage:hitdesign`). | `--connection: ` only — works via case-insensitive single-connection fallback but the canonical convention is the fully-qualified form. | packages/HitTriage/queries/locks.sql:4, packages/Plates/queries/plates-crud.sql:2 | +| Calling a plugin-DB query from JS | `await grok.data.query(':', {param: value})` — same as any other query. | Hand-rolling SQL execution via raw HTTP — no auth, no connection pooling. | help/develop/how-to/db/db-in-plugin.md:78, js-api/src/data.ts:255-262 | +| Evolving the schema after first deploy | Add a NEW `NNNN_.sql` file with strictly additive changes (new columns, new tables, new indexes). Republish in `--release` mode so all users receive it. | DROP COLUMN, ALTER COLUMN with type change, RENAME — no rollback, will break older clients still mid-session. Editing an already-published migration file — applied state diverges across users. | help/develop/how-to/db/db-in-plugin.md:24-37 | +| Picking between plugin Postgres vs. Docker DB | Plugin Postgres — small CRUD state, low ops overhead, shared instance, schema-namespaced. | Docker DB — when you need a different DBMS (e.g. MariaDB, ClickHouse), data isolation, or full DBA control. See `db-in-docker.md`. | help/develop/how-to/db/db-in-plugin.md:5-10 | + +## Register identifiers (semantic-type detection + handlers) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Declaring a regex-detected semantic type from a package | `package.json` → `"meta": { "semanticTypes": [{ "semType": "", "description": "...", "parsers": [{"regexp": "..."}] }] }`. Multiple parsers per type are allowed. | Calling `DG.SemanticValue.registerRegExpDetector` from your own init code when the static `meta.semanticTypes` shape will do — declarative wins for discoverability. | help/develop/how-to/db/register-identifiers.md:13-31, packages/Chemspace/package.json:67-77 | +| Imperatively registering a semantic-type detector at runtime | `DG.SemanticValue.registerRegExpDetector(semType, regexp, description?)`. Used by the db-explorer library when an entry point declares `matchRegexp`. | Building your own MutationObserver scan — the platform already detects matches anywhere on the page. | js-api/src/grid.ts:1375-1377 | +| Custom rendering for a semantic type (tooltip / card / context-panel / cell) | Subclass `DG.ObjectHandler`, implement `get type()`, `isApplicable(x)` (guard with `x instanceof DG.SemanticValue && x.semType === ''`), and one or more of `renderTooltip`, `renderCard`, `renderProperties`, `renderMarkup`, `renderIcon`, `renderView`. Register once via `DG.ObjectHandler.register(new MyHandler())`. | Modifying the DOM directly — the platform owns the surfaces and will overwrite. | js-api/ui.ts:1651-1773, help/develop/how-to/db/register-identifiers.md:39-82 | +| Booting handlers on package load | `@grok.decorators.autostart() static init() { ... }` inside `PackageFunctions` (preferred). Or header form `//meta.role: autostart` on a `function init()` export. | The compound `//meta.role: init, autostart` form from the article — undocumented; canonical packages use single-token `autostart`. Doing it from a regular function call — never gets executed. | packages/Chembl/src/package.ts:15-19, packages/Chembl/src/package.g.ts:4 | +| Booting a DB-tied identifier explorer (recommended for any identifier that maps to a DB row) | `DBExplorer.initFromConfig(config)` from `@datagrok-libraries/db-explorer/src/db-explorer`. NULL-CHECK the return — null means the connection wasn't found or the schema couldn't load. | Hand-rolled `DG.ObjectHandler` subclasses for each identifier when the data is purely DB-driven — db-explorer eliminates the boilerplate. | libraries/db-explorer/src/db-explorer.ts:210-243, packages/Chembl/src/handlers.ts:7-12 | +| Loading the explorer config from a JSON file shipped with the package | `DBExplorer.initFromConfigPath(_package, 'db-explorer/db-explorer-config.json')`. Path is relative to package files. | Reading the file yourself with `fetch` — `_package.files.readAsText` already routes through the platform. | libraries/db-explorer/src/db-explorer.ts:245-251 | +| Picking the connection target in `DBExplorerConfig` | `connectionName` (matches `name` or `shortName`); `nqName: ':'` (fully-qualified — required when multiple connections share the short name); `dataSourceName: 'postgres'` etc. (additional disambiguator). | A single-segment `nqName` like `'CHEMBL'` (article example) — the precise namespace-qualified lookup only fires when the value contains `:`. | libraries/db-explorer/src/db-explorer.ts:65-70, packages/Chembl/src/explorer-config.ts:7 | +| Defining an entry-point semantic type in `DBExplorerConfig` | `entryPoints: { '': { table, column, regexpExample?: {example, nonVariablePart, regexpMarkup}, matchRegexp? } }`. Provide `matchRegexp` to register a fresh detector; omit it if the semType is already registered elsewhere. | The `schama?: string` field on the type — typo in `types.ts`, never consumed. Don't put a per-entryPoint schema there; override `schemaName` at the top level instead. | libraries/db-explorer/src/types.ts:58-64, packages/Chembl/src/explorer-config.ts:8-22 | +| Pulling extra columns from related tables for tooltip/card rendering | `joinOptions: [{ fromTable, columnName, tableName, onColumn, select: ['col_a', 'col_b'], fromSchema?, onSchema? }]`. Aliasing happens at the SQL layer — pick the columns the cards should show. | Writing a separate query per joined table — `joinOptions` builds a single composite query. | libraries/db-explorer/src/types.ts:33-41, help/develop/how-to/db/register-identifiers.md:276-295 | +| Defining FK-like relationships not present in DB metadata | `explicitReferences: [{ table, column, refTable, refColumn, schema?, refSchema? }]`. Used when the source DB lacks foreign keys, or for cross-schema links. | Faking the relationship via a manual `joinOptions` entry — `explicitReferences` builds the "Links" drill-down panel; `joinOptions` only feeds the card. | libraries/db-explorer/src/types.ts:43-50, help/develop/how-to/db/register-identifiers.md:298-307 | +| Forcing a specific renderer per (table, column) | `customRenderers: [{ table, column, renderer: 'molecule' \| 'helm' \| 'imageURL' \| 'rawImage' }]`. Lookup is exact-string; unknown values fall through to text. | Other renderer names — only those four are supported. | libraries/db-explorer/src/renderer.ts:683-693, libraries/db-explorer/src/types.ts:25 | +| Adding a content-aware renderer (e.g. SMILES detection) | `exp.addCustomRenderer((tableName, colName, value) => boolean, (value, connection) => HTMLElement)`. First arg picks rows; second produces the element. Built-ins exposed for direct use — `moleculeRenderer`, `textRenderer` from `@datagrok-libraries/db-explorer/src/renderer`. | Stuffing the same logic into `customRenderers` when the trigger isn't `(table, column)` — that config matches by exact column name only. | libraries/db-explorer/src/db-explorer.ts:177-183, packages/Chembl/src/handlers.ts:13-16 | +| Configuring identifiers with no code at all (single connection) | UI flow — Browse > Databases > right-click > **Configure Identifiers...** > pick schema > accordion editor (Identifiers, Joins, Explicit References, Header Names, Unique Columns, Custom Selected Columns, Renderers) > Save. Use **Import/Export JSON** at the bottom to round-trip between environments. | Maintaining a parallel TS config when no version control is needed — the editor's JSON IS the source of truth in that mode. | help/develop/how-to/db/register-identifiers.md:131-200 | + +## Define semantic-type detectors (data-shape, written in JS) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Detecting a custom semType from data SHAPE (column type, name, statistics, value patterns) | A method on `PackageDetectors extends DG.Package` in `/detectors.js`, tagged `//meta.role: semTypeDetector` + `//input: column col` + `//output: string semType`, signature `(col) => string \| null`. | `package.json` `meta.semanticTypes` (regex-only, can't gate on `col.type`/`col.min`/`col.max`/`col.name`). `DG.SemanticValue.registerRegExpDetector` from autostart (same regex-only limit). Defining the class in `package.ts` — `detectors.js` is a separate upload bundle. | help/develop/how-to/functions/define-semantic-type-detectors.md:11-19, packages/BiostructureViewer/detectors.js, js-api/src/const.ts:367-370 | +| Scaffolding a new detector | `cd && grok add detector ` — creates `detectors.js` (with the `PackageDetectors` class wrapper) if missing, then appends a `detect` method based on `tools/entity-template/sem-type-detector.js`. | Hand-writing the class wrapper from memory — the `PackageDetectors` suffix is mechanical and the CLI gets the casing right. | tools/bin/commands/add.ts:268-290, tools/entity-template/sem-type-detector.js | +| Sampling a string column to test a value predicate efficiently | `DG.Detector.sampleCategories(col, (s) => /pattern/.test(s), min=5, max=10, ratio=1, minStringLength=1)` — checks up to `max` evenly-spaced category samples and returns `true` only if `ratio` of them pass. SILENT FALSE for non-STRING columns. | Iterating `col.toList()` or `col.categories` in a loop — O(N), runs on every imported dataframe. Iterating without skipping empty strings — they'll match `^.*$`-style patterns and pollute null-only columns. | js-api/src/data.ts:278-313 | +| Assigning the detected semType | Return the type string (e.g. `return 'Magnitude';`) OR set `col.semType = '';` and return it. Both work — platform reads return value first, falls back to `col.semType`. | Setting only `col.semType` and returning `null` — the platform treats `null` as "no match" and the type assignment may be overwritten by a later detector. Returning a string from a detector that didn't actually match — false positive sticks until session reset. | help/develop/how-to/functions/define-semantic-type-detectors.md:27-32, packages/BiostructureViewer/detectors.js:19-30 | +| Attaching units alongside a semType | `col.meta.units = '';` — writes the `units` tag (`DG.TAGS.UNITS`). Use when one semType covers multiple physical formats (e.g., `Molecule3D` for `pdb` vs `pdbqt`). Renderers branch on units to pick the right parser. | Sticking the unit suffix into the semType (e.g. `Molecule3D-pdb`) — breaks downstream registry lookups that match exact-string semTypes. | js-api/src/dataframe/column-helpers.ts:232-234, packages/BiostructureViewer/detectors.js:19,26 | +| Skipping the platform's auto-test for a detector that doesn't fit the standard fixture | `//meta.skipTest: ` on the detector function. | Removing the detector to avoid the test failure — disables runtime detection too. Leaving the test failing — gates the package's CI. | help/develop/how-to/functions/define-semantic-type-detectors.md:90-99, packages/BiostructureViewer/detectors.js:9-13 | +| Pointing the auto-test at a custom fixture CSV | `//meta.testData: ` plus optional `//meta.testDataColumnName: ` to assert the detector picks that specific column out of the fixture. The fixture must contain exactly one column the detector should match. | A CSV with multiple matching columns — defeats the false-positive check. | help/develop/how-to/functions/define-semantic-type-detectors.md:120-127 | + +## Custom file viewers + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a custom file viewer | Header annotations on a `(file: DG.FileInfo) => DG.View \| Promise` function — `//meta.role: fileViewer`, `//meta.fileViewer: [,…]`, `//input: file file`, `//output: view v`. The platform invokes it from the file-share browser when the user opens a matching file. | A `cellRenderer` for an in-grid preview, or a `fileHandler` (which imports the file as `DataFrame[]` instead of opening a custom view). The two roles are separate registrations. | help/develop/how-to/files/create-custom-file-viewers.md:5-9, packages/EpsViewer/src/package.g.ts:6-7 | +| Decorator-form file viewer | `@grok.decorators.fileViewer({fileViewer: 'eps', fileViewerCheck?: ':'}) static method(file: DG.FileInfo): DG.View` inside `PackageFunctions`. `fileViewer` is REQUIRED. | Forgetting `fileViewer` — the decorator typings make it required; runtime ignores the registration without it. | js-api/src/decorators/functions.ts:220-223,342-348, packages/EpsViewer/src/package.ts:122-145 | +| Declaring multiple extensions for one viewer | Comma-separated string after `meta.fileViewer:` — whitespace tolerated. `mol,sdf,cif` and `mp3, wav, flac` both parse. | Repeating `//meta.fileViewer:` lines for each extension — only the LAST line is read. Use a single comma-separated value. | packages/BiostructureViewer/src/package.g.ts:113, packages/Media/src/package.ts:73 | +| Picking among multiple viewers for the same extension | Add `meta.fileViewerCheck: :` (header form) or `fileViewerCheck: ':'` (decorator key). The platform calls `(content)` and uses this viewer only when it returns true. Input typing — `string content` for text payloads, `blob`/`Uint8Array` for binary. | Maintaining one mega-viewer that branches on file content — `fileViewerCheck` lets the platform pick before instantiation, keeping each viewer focused. | js-api/src/decorators/functions.ts:220-223, packages/Plates/src/package.ts:56,176, packages/MetabolicGraph/src/package.ts:103-108 | +| Reading the file content inside the viewer | `await file.readAsString()` for text → `Promise`; `await file.readAsBytes()` for binary → `Promise`. Methods are on the `DG.FileInfo` input, no args. | `grok.dapi.files.readAsText(file)` — works, but the FileInfo method is the more direct route inside a function whose input is typed `file`. | js-api/src/entities/table-info.ts:107-114 | +| Building the view body | `const view = DG.View.create(); view.append(host); view.name = file.name; return view;` — `host` is any `HTMLElement` (typically `ui.div([], 'd4-something')`). Optional `DG.View.create('css-class')` to scope styles. | Returning the host element directly — the platform expects a `DG.View`, not a DOM node. | js-api/src/views/view.ts:243-251, packages/EpsViewer/src/package.ts:124-145 | +| Sync vs async viewer | Sync — return `DG.View` immediately; chain `.readAsBytes().then(bytes => mutate(view))` to populate after the read. Async — `async` function + `await file.readAs...()` before `return view`. Use sync when you want the view to appear before the bytes load. | Returning a Promise just because the read is async — sync + chain is the article's recommended pattern for fast first-paint. | help/develop/how-to/files/create-custom-file-viewers.md:20-31, packages/FileEditors/src/package.g.ts:13-15 | +| Companion check function for `fileViewerCheck` | Plain Datagrok function with `//input: string content` (or `blob content` for binary) and `//output: bool result`. Pure — no side effects. Keep cheap (the platform may run several checks per file). Reference: `Plates:checkFileIsPlate`, `Chem:checkJsonMpoProfile`, `Metabolicgraph:escherFileViewerCheck`. | Reading the whole file inside the check, or making async server calls — the platform short-circuits on truthy return so heavy work delays viewer instantiation. | packages/Plates/src/package.ts:226, packages/Chem/src/package.ts:2879, packages/MetabolicGraph/src/package.ts:103-108 | + +## Custom file exporters + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a custom file exporter | Header annotations on a `() => void` function — `//meta.role: fileExporter` and `//description: `. Or decorator form `@grok.decorators.fileExporter({description: ''})` on a `static` method inside `PackageFunctions`. The platform attaches it to the global file Export top-menu at startup. | A `fileExporter` decorator without a `description` — `tools/bin/commands/check.ts:358` flags it as an error ("File exporters should have a description parameter"). A `meta.fileExporter: ` annotation — no such key exists; extension is decided at download time, not registration time. | help/develop/how-to/files/file-exporters.md:12-15, packages/Chem/src/package.ts:526-531, js-api/src/decorators/functions.ts:350-356 | +| Naming the menu entry | Pass the human-readable label as `description`. Convention — `As ...` (Chem `As SDF...`, Bio `As FASTA...`) when the exporter shows a follow-up dialog; `Save as ` (Arrow `Save as Parquet`, `Save as Feather`) when it downloads directly. | The function name itself — the menu uses `description`, not the symbol. Empty string — fails the `grok check` linter. | packages/Arrow/src/package.ts:64,71, packages/Chem/src/package.ts:526-528, packages/Bio/src/package.ts:1404 | +| Reading the active table inside the exporter | `grok.shell.t` (returns the active `DG.DataFrame` or `null`). Guard for `null` before iterating columns. The exporter is invoked only from a table view, but defensive null-handling is still warranted. | A `DG.DataFrame` input parameter — file-exporter functions are forbidden from declaring inputs; `tools/bin/utils/func-generation.ts:340` defines `inputs: []`. | packages/Chem/src/package.ts:529, packages/Arrow/src/package.ts:66, js-api/src/shell.ts (Shell.t getter) | +| Triggering the browser download | `DG.Utils.download(filename, content, contentType?)` — wraps `Blob` + `URL.createObjectURL` + anchor click. `content: BlobPart` accepts string, `Uint8Array`, `ArrayBuffer`, `Blob`. `contentType` defaults to `'application/octet-stream'`. | The hand-rolled `document.createElement('a')` + `data:text/plain;charset=utf-8,encodeURIComponent(...)` snippet from the article — silently corrupts non-text payloads past ~2MB on most browsers. | js-api/src/utils.ts:230-237, packages/Arrow/src/package.ts:68,75 | +| Composing the output filename | `.` is the convention — `table.name + '.sdf'`, `table.name + '.parquet'`, `table.name + '.feather'`. | Hard-coded filenames — multiple exporter invocations would overwrite each other in the user's Downloads folder. | packages/Chem/package.js (article snippet line 50), packages/Arrow/src/package.ts:68,75 | +| Returning a value from an exporter | Return nothing. The signature is fixed `fileExporter(): void`; `tools/bin/utils/const.ts:131-136`. | Returning a `DG.DataFrame` (the platform ignores it) or a `Promise` — the platform awaits void; downstream code never sees the resolved value. | js-api/src/const.ts:485-489, tools/bin/utils/func-generation.ts:340-346 | +| Showing a follow-up dialog (format options, column picker) | Build a `ui.dialog(title)` inside the exporter, attach inputs, and call `DG.Utils.download(...)` from the dialog's OK handler. References: `Chem:saveAsSdfDialog` in `packages/Chem/src/utils/sdf-utils.ts`; `Bio:saveAsFastaUI` in `packages/Bio/src/utils/save-as-fasta.ts`. | Inlining the dialog in `package.ts` for non-trivial format options — keep the exporter wrapper one-liner, factor the dialog into a util. | packages/Chem/src/package.ts:526-531, packages/Bio/src/package.ts:1404-1407 | + +## Custom folder content preview + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a custom folder content preview | Header annotations on a `(folder: DG.FileInfo, files: DG.FileInfo[]) => DG.Widget \| undefined \| Promise<...>` function — `//meta.role: folderViewer`, `//input: file folder`, `//input: list files`, `//output: widget`. Or decorator form `@grok.decorators.folderViewer(config?)` (config is OPTIONAL, generic `FunctionOptions` — no specialized `FolderViewerOptions`) on a `static` method inside `PackageFunctions`. The platform invokes the function whenever the user opens a folder in the file-share browser. | A `fileViewer` (which is per-file, not per-folder) or a `fileHandler` (which imports a single file's content as DataFrames). The folderViewer role is distinct — it sees the folder + its file list and returns a custom widget. | help/develop/how-to/files/folder-content-preview.md:5-7,12-15, js-api/src/const.ts:419,593-597, js-api/src/decorators/functions.ts:294-300 | +| Inspecting the folder | `folder.name` and `folder.fullPath` on the `DG.FileInfo` input — use `folder.fullPath` to read child files via `grok.dapi.files.readAsText(\`${folder.fullPath}/\`)`. Plates checks `folder.name?.toLowerCase().includes('plate')`. | Computing the path from URL — `FileInfo` already carries the resolved fullPath. | packages/ClinicalCase/src/package.ts:208, packages/Plates/src/package.ts:50-52 | +| Detecting sentinel files in the folder | Iterate `files: DG.FileInfo[]` with `.some(f => f.fileName.toLowerCase() === '')`. Both the article and production ClinicalCase detect `dm.csv` (the SDTM Demographics domain filename) — `files.some((f) => f.fileName.toLowerCase() === 'dm.csv')`. | Filename comparison without `.toLowerCase()` — folder uploads may carry mixed case; the canonical pattern always lowercases. | help/develop/how-to/files/folder-content-preview.md:20, packages/ClinicalCase/src/package.ts:207 | +| Returning the widget | `DG.Widget.fromRoot(htmlElement)` — wrap any `HTMLElement` (typically a `ui.div([...])` containing buttons / panels). Static factory at `js-api/src/widgets/base.ts:374-376`. | Returning the HTMLElement directly — the platform expects a `DG.Widget`, not a DOM node. Returning a plain `{root}` object — bypasses the Widget contract. | js-api/src/widgets/base.ts:374-376, packages/ClinicalCase/src/package.ts:211-226 | +| Signaling "no preview applies" | Return `undefined` (or fall through with no `return`) when the folder doesn't match the preview's criteria. The platform falls back to its default folder view. | Returning a degenerate empty widget — the platform will render it instead of falling back. Throwing — uncaught exceptions surface as errors in the user's view. | help/develop/how-to/files/folder-content-preview.md:5-7, packages/ClinicalCase/src/package.ts:228, packages/Plates/src/package.ts:51-52 | +| Returning a `DG.ViewBase` instead of a `DG.Widget` | Override the codegen output type with `@grok.decorators.folderViewer({outputs: [{'name': 'result', 'type': 'dynamic'}]})` — the auto-emitted `package.g.ts` then carries `//output: dynamic result` instead of `//output: widget result`. Used by `Plates:platesFolderPreview` to return a full view. | Plain `@grok.decorators.folderViewer()` when the body returns a `ViewBase` — the codegen-default `widget` output type rejects it. | packages/Plates/src/package.ts:48-49, packages/Plates/src/package.g.ts:17-23, tools/bin/utils/func-generation.ts:420-426 | +| Wiring the preview to launch an app | Inside the widget body, attach `ui.button('Run X', async () => { ... grok.functions.call(':'); })`. Production pattern — ClinicalCase loads each domain's CSV via `grok.data.files.openTable`, then `grok.functions.call('Clinicalcase:clinicalCaseApp')` from the button handler. | The article's stub button calls `grok.shell.info('Folder contains SDTM data')` purely to demonstrate the registration mechanism — replace with the real launcher in shipped code. | help/develop/how-to/files/folder-content-preview.md:21-22, packages/ClinicalCase/src/package.ts:215-225 | + +## Custom file handlers + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a custom file handler (importer) | Header annotations on a `(content: string \| Uint8Array) => DG.DataFrame[]` function — `//meta.role: fileHandler`, `//meta.ext: [,…]`, `//input: string content` (or `//input: list bytes` for binary), `//output: list tables`. The platform invokes it whenever a user opens a file with a matching extension and treats the returned list as the imported tables. | A `fileViewer` for tabular content (no DataFrame import) or a `fileExporter` (the inverse direction). The three roles are separate registrations. | help/develop/how-to/files/file-handlers.md:5-21, packages/Bio/src/package.g.ts:486-503 | +| Decorator-form file handler | `@grok.decorators.fileHandler({ext: 'jdx', fileViewerCheck?: ':', outputs?: [{name: 'tables', type: 'list'}]}) static async myHandler(content: string \| Uint8Array)` inside `PackageFunctions`. `ext` is REQUIRED; the decorator's `config` parameter is non-optional (unlike `fileViewer(config?)`). | Header form when writing fresh `package.ts` — every modern canonical package uses the decorator and lets codegen emit `package.g.ts`. | js-api/src/decorators/functions.ts:225-228,358-364, packages/nmrium/src/package.ts:20-46 | +| Declaring multiple extensions for one handler | Single `meta.ext: ,,…` value with comma-separated extensions; whitespace tolerated. Decorator form: `{ext: 'sdf,mol'}`. Production examples: `fasta, fna, ffn, faa, frn, fa, fst` (Bio FASTA), `sdf,mol` (Chem SDF), `bam, bai` (Bio BAM). | Repeating `//meta.ext:` lines per extension — only the LAST line is read. Per-extension decorators only when the bodies differ (nmrium pattern: `jdx` and `dx` need separate functions because `addNmriumView(extension, ...)` keys on the extension). | packages/Bio/src/package.g.ts:491,501, packages/Chem/src/package.g.ts:797, packages/nmrium/src/package.ts:20-46 | +| Reading binary file content (Uint8Array input) | Override the handler input with `inputs: [{name: 'bytes', type: 'list'}]` in the decorator (or `//input: list bytes` in header form). Default codegen uses `{name: 'content', type: 'string'}`. Used by `Chem:importSdf(bytes: Uint8Array)`, `Chem:importBam(bytes)`, every Chem `import*` for binary. | Receiving a string input then `TextDecoder`-decoding to bytes — corrupts the payload because the platform already UTF-8-decoded the bytes once. | packages/Chem/src/package.g.ts:793-809, packages/Bio/src/package.g.ts:496-503, tools/bin/utils/func-generation.ts:348-355 | +| Disambiguating handlers when two packages claim the same extension | `meta.fileViewerCheck: :` (header form) or `fileViewerCheck: ':'` (decorator key). The platform calls `(content)` and uses this handler only when it returns true. Same mechanism shared with `fileViewer`. | A mega-handler that branches on content — the platform short-circuits before instantiation when the check is registered. | js-api/src/decorators/functions.ts:225-228, packages/nmrium/src/package.ts:20-40 | +| Building the returned `DataFrame[]` from CSV-like content | `[grok.data.parseCsv(content, options?)]` for one DataFrame; concat several when the file contains multiple tables. The handler is the canonical place to call `parseCsv` because the platform has already loaded the bytes. | Calling `grok.data.loadTable` (re-fetches via URL) or hand-rolling `fetch` — wasteful, and the handler runs in a context where the content is already in memory. | js-api/src/data.ts:166-168, help/develop/how-to/files/file-handlers.md:17-20 | +| Returning `[]` from a handler that opens a custom view as a side effect | Inside the handler call `grok.shell.addView(v)` (or chain another mechanism that adds the visualization), then `return []`. The platform accepts an empty list — it just doesn't open an extra TableView. Reference: `Nmrium:nmriumFileHandler` calls `addNmriumView()` then returns `[]`. | A null return — the signature requires an array; null trips the platform's iteration over the result. | packages/nmrium/src/package.ts:12-46 | +| Picking a TypeScript signature inside a class-based package | Decorator form — `@grok.decorators.fileHandler({ext: 'jdx', outputs: [{name: 'tables', type: 'list'}]}) static async jdxFileHandler(bytes: string): Promise`. The auto-emitted `package.g.ts` carries `//meta.role: fileHandler` + `//input: string fileContent` + `//output: list result`. | Top-level `function` exports in modern packages — `PackageFunctions` is the convention; codegen needs the static class shape. | packages/nmrium/src/package.ts:19-46 | + +## Custom cell renderers + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a class as a custom grid cell renderer | `@grok.decorators.cellRenderer({name?, description?, cellType, columnTags?, virtual?})` directly on the `extends DG.GridCellRenderer` class. `cellType` REQUIRED; `virtual` is BOOLEAN (`virtual: true`). The `FuncGeneratorPlugin` picks it up from `package.ts` and emits the entry to `package.g.ts`. | A separate factory function with `//meta.role: cellRenderer` headers when a class decorator will do — modern packages decorate the renderer class directly. The commented-out method-decorator version at `js-api/src/decorators/functions.ts:318-324` is intentionally NOT exported. | js-api/src/decorators/functions.ts:62-80, packages/PowerGrid/src/cell-types/svg-cell-renderer.ts:6-12, packages/Curves/src/fit/fit-renderer.ts:311-315 | +| Registering via a factory method inside `PackageFunctions` (alternate path) | `@grok.decorators.func({meta: {role: 'cellRenderer', cellType: '', virtual: 'true'}, tags: ['cellRenderer'], outputs: [{type: 'grid_cell_renderer', name: 'result'}], name: ''}) static () { return new MyRenderer(); }`. NOTE `meta.virtual` MUST be the STRING `'true'` here (the `meta` interface is typed `Record`). | Boolean `true` inside `@grok.decorators.func({meta: {virtual: true}})` — the type system rejects it; codegen also fails. | packages/PowerGrid/src/package.ts:85-98, js-api/src/decorators/functions.ts:117-145 | +| Header-form fallback (when reading auto-emitted `package.g.ts`) | `//tags: cellRenderer` + `//meta.role: cellRenderer` + `//meta.cellType: ` + `//output: grid_cell_renderer result`, optional `//meta.virtual: true`, optional `//meta.columnTags: ` over a `function () { return new MyRenderer(); }`. | Hand-writing `package.g.ts` — the codegen owns it. The article's first example shows the bare-function form because that's what users see in `package.g.ts`, not what they should author in `package.ts`. | help/develop/how-to/grid/custom-cell-renderers.md:14-22, packages/PowerGrid/src/package.g.ts:116-125 | +| Subclassing `DG.GridCellRenderer` | Implement `get name()`, `get cellType()`, `render(g, x, y, w, h, gridCell, cellStyle)`. Optional `renderSettings(gridColumn): Element \| null`, `defaultWidth`, `defaultHeight`, `getDefaultSize`, mouse / keyboard handlers. | Calling `GridCellRenderer.register(...)` directly — the platform invokes it via the registered factory; calling it twice corrupts the registry. | js-api/src/grid.ts:1276-1325, packages/PowerGrid/src/sparklines/piechart.ts:192-217 | +| Marking a renderer as virtual (summary / synthesised column) | `virtual: true` (class decorator) / `virtual: 'true'` (`@grok.decorators.func({meta})`) / `//meta.virtual: true` (header). Pair with `cellType: ''` so the platform offers it in the "summary column" picker. | Setting `virtual: true` on a renderer that visualises an existing cell value — would let the user create a phantom column with no source data. | packages/PowerGrid/src/package.ts:85-98 (virtual), packages/PowerGrid/src/cell-types/svg-cell-renderer.ts:6-12 (NOT virtual) | +| Surfacing a chart-style renderer in the summary-column picker | `meta.gridChart: 'true'` (function-decorator form) — paired with `meta.virtual: 'true'` and `cellType: ''`. The class-decorator option type does NOT expose `gridChart` (`functions.ts:72-80`); the generic `Meta` interface does (`functions.ts:147`). For a class-decorator chart, drop to `@grok.decorators.func({meta: {gridChart: 'true', virtual: 'true', cellType: '', role: 'cellRenderer'}, ...})` inside `PackageFunctions`. | `@grok.decorators.cellRenderer({cellType, virtual: true, gridChart: 'true'})` — TS rejects the extra key; without `@ts-ignore` the meta silently drops. Treating `gridChart` as a synonym for `virtual` — they are independent tags; only the pair places the renderer in the chart picker. | js-api/src/decorators/functions.ts:72-80,117-149, packages/PowerGrid/src/package.ts:34-160, packages/PowerGrid/src/package.g.ts:109,120,131 | +| Picking the auto-emitted output parameter name | Read what your registration form actually produces: class decorator → `//output: grid_cell_renderer renderer` (codegen default `name: 'renderer'` at `func-generation.ts:327`); function decorator with explicit `outputs: [{name: 'result'}]` → `//output: grid_cell_renderer result`. The platform binds by TYPE; the name only matters for drift-detection or hand-copying. | Hard-coding `result` (or `renderer`) in a fingerprint check without accepting both — class-decorator-authored sparklines will fail the check; function-decorator ones will fail the inverse. | tools/bin/utils/func-generation.ts:323-330, packages/PowerGrid/src/package.g.ts:87,98 (renderer) vs :118,129 (result) | +| Matching by column tags instead of cellType | `columnTags: 'quality=Macromolecule, units=fasta'` (decorator) or `//meta.columnTags: foo=bar,units=kg` (header). Comma-separated `key=value` pairs; whitespace tolerated. | Per-cell branching inside `render` — the platform already filters by tags before invoking the renderer. | help/develop/advanced/decorators.md:62-70, packages/PowerGrid/src/package.g.ts:188 | +| Reading the active dataframe inside `render` | `gridCell.grid.dataFrame` — already bound to the cell's grid. `gridCell.cell.row.idx` for the row index; `gridCell.gridColumn.settings` for per-column settings. | `grok.shell.t` — points at the active TableView's dataframe, which may differ from the rendered column's parent (same trap as the column-tooltip rule — DG-FACT-427: use `col.dataFrame`, not `grok.shell.tv.dataFrame`). | packages/PowerGrid/src/sparklines/piechart.ts:218-223 | +| Building summary settings UI for a renderer | Override `renderSettings(gridColumn): Element \| null` (default returns `null`). Shown in the column header / property panel. | A separate dialog wired through a custom function — `renderSettings` is the documented hook. | js-api/src/grid.ts:1287, packages/PowerGrid/src/sparklines/* (each chart renderer overrides settings) | + +## Custom column tooltips + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a custom column tooltip | Header annotations on a `(col: DG.Column) => DG.Widget \| undefined \| Promise<...>` function — `//meta.role: tooltip`, `//input: column col { semType: }`, `//output: widget result`. The platform invokes the function whenever a user hovers a column whose `semType` matches; the returned widget is rendered inside the column-header tooltip. | A `cellRenderer` (per-cell, not per-column) or a `DG.ObjectHandler.renderTooltip` (per-semantic-value, more general but more boilerplate). The `tooltip` role is the targeted "column-header tooltip" hook. | help/develop/how-to/grid/column-tooltip.md:5-7, js-api/src/const.ts:586-591, packages/Bio/src/package.g.ts:16-22 | +| Decorator-form column tooltip | Generic `@grok.decorators.func({meta: {role: 'tooltip'}}) static method(@grok.decorators.param({options: {semType: ''}}) col: DG.Column): DG.Widget` inside `PackageFunctions`. There is NO specialized `@grok.decorators.tooltip()` (unlike `fileViewer`/`fileHandler`/`folderViewer`). | Inventing a `@grok.decorators.tooltip()` call — the surface does not exist in `js-api/src/decorators/functions.ts`; the codegen has no `funcGenerators.tooltip` either. Use the generic `func` decorator with `meta: {role: 'tooltip'}`. | packages/Bio/src/package.ts:146-159, packages/Chem/src/package.ts:263-268, js-api/src/decorators/functions.ts | +| Binding the tooltip to a semantic type | `semType: ` constraint on the `col` input — header form `//input: column col { semType: }`; decorator form `@grok.decorators.param({options: {semType: ''}})`. The platform fires the tooltip only for matching columns. | Filtering by semType inside the function body — wastes invocations and the platform has already filtered for you. | packages/Bio/src/package.ts:151 (`Macromolecule`), packages/Chem/src/package.ts:268 (`Molecule`) | +| Building the tooltip widget from the right dataframe | Pass `col` directly into your widget constructor, or use `col.dataFrame` if you need the parent. Article's example: `await col.dataFrame.plot.fromType('WebLogo', {sequenceColumnName: col.name}) as unknown as DG.Widget`. Bio: `new MacromoleculeColumnWidget(col, ...)`. Chem iterates `col.categories` directly. | `grok.shell.tv.dataFrame.plot.fromType(...)` or `grok.shell.t` — pulls from the active TableView's dataframe, which may not be the column's parent. The `sequenceColumnName` lookup then resolves on the wrong table. See DG-FACT-427. | help/develop/how-to/grid/column-tooltip.md:13-24, packages/Bio/src/package.ts:152, packages/Chem/src/package.ts:273-277 | +| Skipping the tooltip for some column states | `return undefined` (or fall through with no `return`). The platform falls back to its default tooltip. Used by Chem to skip when any of the first 100 categories is a SMARTS pattern. | Returning a degenerate empty widget — the platform will render it instead of falling back to the default. Throwing — uncaught exceptions surface as errors in the user's view. | packages/Chem/src/package.ts:267-277 | +| Adding the legacy `tags: tooltip` token | Optional. Bio adds `tags: ['tooltip']` to the decorator (auto-emits `//tags: tooltip`); Chem omits it and relies on `meta.role: tooltip` alone — both work. The function-role descriptor uses `header: 'tags'` so the role is also written to the `tags` bucket on the server side regardless. | Treating `tags: tooltip` as required — it isn't; `meta.role: tooltip` is sufficient. | packages/Bio/src/package.ts:146-149, packages/Chem/src/package.ts:263-266, js-api/src/const.ts:589 | + +## Customize a grid (formatting / ordering / visibility / resizing / color-coding) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Looking up one grid column | `view.grid.col(name)` (shortcut for `view.grid.columns.byName(name)`) or `view.grid.columns.byIndex(i)`. Returns `GridColumn \| null`. Names are case-insensitive. | `view.grid.cols(...)` — does not exist; the plural is `view.grid.columns` (a `GridColumnList`, not a method). | js-api/src/grid.ts:836-838,955-957 | +| Setting a grid column's display format | `view.grid.col(name).format = ''` (e.g. `'scientific'`, `'#.0000'`, `'dd.MM.yyyy'`). Applies to numeric and datetime columns only — string columns silently ignore. Per-column setting overrides the global Settings → Format defaults. Sorting/filtering uses the unchanged underlying value. | A separate `column.meta.format = ...` when you only need a per-grid override — that path writes the dataframe-level `format` tag and propagates to every view. | js-api/src/grid.ts:723-724, js-api/src/dataframe/column-helpers.ts:221-223, help/develop/how-to/grid/customize-grid.md:32-34 | +| Reordering visible columns inside ONE grid | `view.grid.columns.setOrder(['age','sex','race'])`. Unmentioned columns appear after the listed ones. Affects only this grid. | `df.columns.setOrder([...])` — that's the dataframe-level reorder; touches every TableView and the on-disk projection (different blast radius). | js-api/src/grid.ts:840-845 | +| Reordering columns inside the dataframe (and every view) | `df.columns.setOrder(['age','sex','race'])`. Mutates the DataFrame's column list itself. | Doing it grid-by-grid via `grid.columns.setOrder` for every view — duplicate work. Pick the dataframe form when the change is logical, not visual. | js-api/src/dataframe/column-list.ts:121-126 | +| Showing only a subset of columns in one grid | `view.grid.columns.setVisible(['age','sex','race'])` — hides the rest. | A list of single-column `gridColumn.visible = false` calls — works but takes N round-trips. | js-api/src/grid.ts:847-852, js-api/src/grid.ts:734-736 | +| Hiding a column at the dataframe level (so every view inherits) | Rename the column with a leading tilde — `data.columns.byName('age').name = '~age'`. Affects every TableView/Grid built from the dataframe. To re-show, rename back — `setVisible(['~age'])` does NOT undo the rename. | Calling `setVisible(['~age'])` to undo a tilde rename — does not work; the rename is the change. | help/develop/how-to/grid/customize-grid.md:130-140 | +| Resizing a grid column | `gridColumn.width = ` (number). Works on `byName`, `byIndex`, and `rowHeader`. | `setWidthType(type, extra?)` for one-off pixel values — that takes a `ColumnWidthType` enum (Minimal / Compact / Optimal / Maximal); use it for policy-based sizing instead. | js-api/src/grid.ts:712-715,805-809 | +| Resizing the row-index column | `view.grid.columns.rowHeader.width = ` — `rowHeader` is a getter that returns `byIndex(0)`, so the two are interchangeable. | `byIndex(1)` thinking the row header is "before" the data — index 0 IS the row header; the first dataframe column lives at index 1. | js-api/src/grid.ts:820-824 | +| Filtering rows down to a chosen subset (and order) in a grid | `grid.setRowOrder([1, 56, 3, 6, 4])` — displays ONLY the listed indexes, in the listed order. Hides every unlisted row. | Using this when you actually want all rows in custom order — only listed rows show. Use `sortIndexes` for "keep all, reorder". | js-api/src/grid.ts:1042-1048 | +| Sorting all rows by a JS comparer | `grid.sortIndexes((a, b) => fa(a) - fa(b))`. Internally builds a full `Int32Array` of `table.rowCount`, sorts via the comparer, then `setRowOrder`s it. All rows stay visible. | `setRowOrder` with a manually-sorted partial list — drops rows. | js-api/src/grid.ts:1027-1035 | +| Sorting rows by one or more column values | `grid.sort(['age'])` (ASC) or `grid.sort(['disease', 'weight'], [true, false])` (per-column ASC/DESC). Returns `Grid` for chaining. `grid.sort([], [])` clears the sort. | Setting `column.valueComparer` for a one-off sort when you don't need it elsewhere — comparers are global to the column and affect aggregations / group-bys / charts. | js-api/src/grid.ts:1019-1025, packages/Chem/src/package.ts:2001 | +| Persistent custom row order on a column (used everywhere, not just sort) | `column.valueComparer = (a, b) => …` — setter on the underlying `Column`. Picked up by every visualization, aggregation, group-by, and chart category ordering. Pair with `column.getSortedOrder()` → `Int32Array` to apply to a grid via `setRowOrder`. | A grid-only sort when the order should hold across charts — use the comparer + `getSortedOrder` instead. | js-api/src/dataframe/column.ts:412-418 | +| Painting category cells with custom colors (per grid) | `view.grid.col(name).categoryColors = {'M': DG.Color.red, 'F': DG.Color.fromHtml('#800080')}` — `{[category: string]: number}` map of ARGB integers. Categories not in the map get default colors. Empty `{}` enables coloring with all defaults. | Raw 32-bit hex like `0xFF0000FF` — that's BLUE in ARGB (alpha is the high byte; `DG.Color.blue = 0xFF0000FF`). Use `DG.Color.` constants or `DG.Color.fromHtml('#…')` to avoid the byte-order trap. | js-api/src/grid.ts:741-742, js-api/src/color.ts:141-183, help/develop/how-to/grid/customize-grid.md:166-170 | +| Reading the active color-coding mode of a column | `column.meta.colors.getType()` → one of `'Off' \| 'Categorical' \| 'Conditional' \| 'Linear'`. Returns `'Categorical'` as a legacy fallback when the `COLOR_CODING_CATEGORICAL` tag exists but `COLOR_CODING_TYPE` doesn't. | Reading the raw tag string — the `COLOR_CODING_TYPE` constant maps to the dot-prefixed key `.color-coding-type`, not the all-caps name. Use the helper. | js-api/src/dataframe/column-helpers.ts:55-61, js-api/src/const.ts:295-298,797-802 | +| Setting linear color-coding on a numeric / datetime column | `column.meta.colors.setLinear([DG.Color.orange, DG.Color.green])` (range = palette as ARGB ints) or `setLinear()` for the default palette. Optional second arg `{min, max, belowMinColor?, aboveMaxColor?}` to fix the scale. The setter writes the `COLOR_CODING_TYPE` tag implicitly — do NOT also set the tag manually. | Hand-writing `column.tags['.color-coding-type'] = 'Linear'` — works, but skips the helper's range serialization (`JSON.stringify`) and out-of-range color writes. | js-api/src/dataframe/column-helpers.ts:75-84, help/develop/how-to/grid/customize-grid.md:189-220 | +| Setting categorical color-coding | `column.meta.colors.setCategorical({'New York': DG.Color.orange})` — partial map; missing categories get defaults. Optional second arg `{fallbackColor, matchType?}`. Only valid for categorical-typed columns. | Per-grid `categoryColors` (DG-FACT-108) when the coloring should propagate to other charts and grids — `meta.colors` lives on the dataframe column. | js-api/src/dataframe/column-helpers.ts:103-111 | +| Setting conditional color-coding (range rules) | `column.meta.colors.setConditional({'<100': DG.Color.green, '100-200': '#ff0000', '20-170': '#00FF00'})` — keys are range expressions (see `help/access/databases/databases.md#parameterized-queries`); values are ARGB ints OR `#rrggbb` strings (the helper auto-converts numbers via `DG.Color.toHtml`). Values outside the rules render uncolored. Calling with `null`/no args writes only the type tag and the platform generates rules from column statistics. | Setting `setLinear` when discrete buckets are needed — different rendering. | js-api/src/dataframe/column-helpers.ts:113-122, help/develop/how-to/grid/customize-grid.md:189-205 | +| Disabling color-coding | `column.meta.colors.setDisabled()` — writes `COLOR_CODING_TYPE = 'Off'`. Cells render with default backgrounds. | Removing the tag manually — `setDisabled()` is the documented hook and survives any future tag-name changes. | js-api/src/dataframe/column-helpers.ts:124-126 | +| Restricting which users can edit a column from the UI | `column.setTag('editableBy', 'login1, login2')` (or `column.tags['editableBy'] = 'login1'`). Comma-separated user/group LOGIN names; applies at column OR dataframe scope. Pair with `setTag('pinIfEditable', 'true')` to keep an editor's columns pinned. (DG-FACT-248) | A `column.editable = false` flag — no such property; edit-gating is tag-driven and server-enforced. Hard-coding usernames into the package code instead of the tag — bypasses the platform's group + permission lookups. | js-api/src/api/ddt.api.g.ts:266-268, help/visualize/viewers/grid.md:590-591 | + +## Custom package settings editors + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a custom UI for the package "Settings" pane | A function annotated with role `packageSettingsEditor` — `//meta.role: packageSettingsEditor` (header) or `meta: {role: 'packageSettingsEditor'}` + `tags: ['packageSettingsEditor']` (decorator). Returns `DG.Widget` (or `Promise`). The widget renders in the right-side context panel when a user opens the package. | Building a custom dialog wired to a button — the platform already has a "Settings" affordance and indexes editors by role; an out-of-band dialog won't be discovered. The deprecated approach of overriding `Package.getSettings()` for UI purposes — that method is for reading values, not rendering. | js-api/src/const.ts:392-394,502-507, packages/HitTriage/src/package.ts:350-358 | +| Writing the editor in `package.ts` (modern) | Generic `@grok.decorators.func({name, meta: {role: 'packageSettingsEditor'}, tags: ['packageSettingsEditor']})` over a `static` method inside `PackageFunctions` that takes `@grok.decorators.param({name: 'propList', type: 'object'}) properties: DG.Property[]` and returns `DG.Widget` / `Promise`. Codegen emits matching `package.g.ts`. | The specialized `@grok.decorators.packageSettingsEditor()` decorator — it exists at `js-api/src/decorators/functions.ts:310` but no production package actually uses it; production prefers the generic `func` decorator form. | packages/HitTriage/src/package.ts:350-358 | +| Receiving the package's declared properties inside the editor | A function input `propList: DG.Property[]` (header form: `//input: object propList`). The platform passes the array of properties declared in `package.json` (or the codegen-driven properties block). Each entry has `.name`, `.category`, `.get(scope)`, `.set(scope, value)`. | Reading `_package.getProperties()` directly — works, but you lose the platform's "scope" parameter and the wired-up save flow. The article's no-arg form `packageSettingsEditor(): DG.Widget` is documented as canonical but no shipping production package uses it (DG-FACT-DRIFT-047) — prefer the `propList` form. | packages/HitTriage/src/packageSettingsEditor.ts:66-95, packages/HitTriage/src/package.g.ts:127-134 | +| Building inputs that bind to properties | For each `prop` in `propList`, build a `DG.InputBase` (e.g., `ui.input.string`, `ui.input.userGroups`); subscribe `input.onInput` and call `prop.set(null, value)` to stage the new value. The platform persists staged values when the user clicks the "Save" button on the Settings pane. | Calling `_package.setSetting(key, value)` synchronously inside `onInput` — bypasses the staged-then-save flow and writes immediately. | packages/HitTriage/src/packageSettingsEditor.ts:78-83 | +| Filtering which properties the editor renders | Filter `propList` by category — `propList.filter((p) => p.category === '')`. Categories come from `package.json`'s property declarations. Lets one editor handle a subset of settings while letting the auto-UI handle the rest. | Returning a widget with no inputs at all — silently breaks the user's ability to edit those properties. | packages/HitTriage/src/packageSettingsEditor.ts:67 | +| Subclassing `DG.Widget` for the editor body | `class MySettingsEditor extends DG.Widget { constructor() { super(ui.div()); /* append children to this.root */ } }`. Override `get type(): string` to return a stable identifier (e.g., `'PowerPackSettingsEditor'`) — used for serialization. | Returning `ui.div(...)` directly without wrapping in `DG.Widget` — the platform expects a `DG.Widget` instance per the function's `output: widget` annotation. | packages/PowerPack/src/settings-editor.ts:11-40, help/develop/how-to/packages/custom-package-settings-editors.md:20-24 | + +## Data enrichments + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Shipping a one-click DB-join enrichment with a plugin | A JSON file under `/enrichments/.json` with keys `name`, `connection`, `keySchema`, `keyTable`, `keyColumn`, `fields[]`, `joins[]`. The platform discovers the folder at publish time; PowerPack must be installed in the target instance to surface the "Add enrichment" UI. | A `connections/*.json` + a hand-rolled query — bypasses the column-context-panel one-click flow and forces users to remember the query name. The runtime save shape (with `keyDb` instead of `connection`) — that's for files written by `saveEnrichment` after edit, not for bundled enrichments. | help/develop/how-to/packages/data-enrichments.md:13-23, packages/Chembl/enrichments/activities_assays.json | +| Naming the target connection inside an enrichment | Fully-qualified nqName `:` (e.g. `Chembl:Chembl`, `Dbtests:PostgresTest`). Same form as `--connection:` in query files. | A bare connection name like `PostgresTest` — the loader requires the namespace prefix because PowerPack's runtime maps it through `nqNameToPath` (replaces `:` with `_`) before deriving the storage folder. | packages/PowerPack/src/db-explorer.ts:634-637, packages/Chembl/enrichments/activities_assays.json:3 | +| Listing the columns the enrichment fetches | `fields: ["..", ...]` — three-segment dot-qualified names, both source-table and joined-table columns mixed in the same array. Include the `keyTable.keyColumn` itself so the result row carries the lookup key (every shipping enrichment does). | Mixing fully-qualified and bare names — every entry must be three segments. There is no per-field alias / rename — column names land verbatim in the result table. | help/develop/how-to/packages/data-enrichments.md:36-40, packages/Chembl/enrichments/compound_core_with_properties.json:7-15 | +| Choosing a `joinType` | `"left"` for "always keep the source row, fill missing right-table values with null". Article also documents `inner` and `right`; both are valid but no production enrichment in the public repo uses them. | Capitalised forms (`"LEFT"`, `"Left"`) — every shipping example is lowercase. | help/develop/how-to/packages/data-enrichments.md:69, packages/Chembl/enrichments # all 17 lowercase "left" | +| Wiring multi-column joins | Pass parallel-indexed arrays — `leftTableKeys: ["customerid", "year"]` matches `rightTableKeys: ["customerid", "year"]` positionally. | Single-string keys when the join is composite — every key array entry must be a string column name; arrays must be the same length. (No public-repo example uses composite keys; verify against the running PowerPack before relying on this.) | help/develop/how-to/packages/data-enrichments.md:40-49 | +| Restricting which columns can be enriched | Implicit — PowerPack's column context panel only offers "Add enrichment" for `STRING`, `BIG_INT`, `INT`, `FLOAT` columns. `BOOL` and `DATE_TIME` are rejected with an inline message. | Designing an enrichment whose `keyColumn` is a boolean or datetime — the user will never see the entry-point button for that column. | packages/PowerPack/src/db-explorer.ts:396-398 | +| Triggering the enrichment from JS instead of the UI | `grok.functions.call('PowerPack:runEnrichment', {conn, schema, table, column, name, df, db, localColumn?})` — the function PowerPack registers with `meta.role: transform`. Same effect as the column-context-panel "Apply" link. | A hand-rolled JOIN query — duplicates the work that `runEnrichment` already orchestrates (key probing, missing-value handling, result-column merging). | packages/PowerPack/src/package.ts:393-399, packages/PowerPack/src/db-explorer.ts:691-699 | + +## Home page widgets (dashboard role) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a home-page (welcome-screen) widget | A zero-arg `() => DG.Widget` function annotated with `//meta.role: dashboard` + `//output: widget result` (header form) or, since there IS a specialized decorator, `@grok.decorators.dashboard(config?: DashboardOptions)` over a `static` method inside `PackageFunctions`. The platform indexes registered functions by `meta.role` and renders every `dashboard` widget on the welcome screen ("Home page" view). | The generic `@grok.decorators.func({meta: {role: 'dashboard'}})` form when the specialized decorator exists — production uses `@grok.decorators.dashboard(...)`. A `panel` role (which is column/object-context-driven, not home-page-driven). | help/develop/how-to/packages/home-page-widgets.md:11-15, js-api/src/const.ts:396-398,509-513, js-api/src/decorators/functions.ts:286-292, packages/PowerPack/src/package.ts:138-155 | +| Decorator-form home-page widget | `@grok.decorators.dashboard({name: 'Spotlight', order: '-1', meta: {showName: 'false'}})` over a `static dashboardName(): DG.Widget` method inside `PackageFunctions`. `DashboardOptions extends FunctionOptions` and adds `order?: string` (placement) and `test?: string` (smoke-test invocation string surfaced as `//test:` in `package.g.ts`). Codegen emits matching `//meta.role: dashboard` + `//output: widget result` block. | A `@grok.decorators.func` with `meta: {role: 'dashboard'}` — works but bypasses the typed `DashboardOptions` (`order`, `test`). | js-api/src/decorators/functions.ts:195-198,216-218,286-292, packages/PowerPack/src/package.ts:138-155, packages/UsageAnalysis/src/package.ts:397-415 | +| Controlling widget placement on the home page | The function-level decorator option `order: ''` (string) — emitted as `//meta.order: ` in `package.g.ts`. Lower numbers (including negatives) sort first; PowerPack's Spotlight uses `order: '-1'` (top), Community uses `order: '6'` (further down). | A Widget instance property declared via `super.addProperty('order', DG.TYPE.STRING, '1')` — that's a per-widget settings field (visible in the gear-icon settings panel), NOT the placement control. The article example (line 41) does this and is misleading. See DRIFT-054. | packages/PowerPack/src/package.ts:142,150, packages/PowerPack/src/package.g.ts:21,29 | +| Restricting widget visibility | The function-level decorator option `meta: {canView: 'Developers,Administrators'}` — emitted as `//meta.canView: Developers,Administrators` in `package.g.ts`. UsageAnalysis uses this to hide developer-internal dashboards from non-admin users. Or `meta: {showName: 'false'}` to suppress the widget header (used by Spotlight). | Hand-rolling a permissions check inside the widget body — the `canView` meta is what the platform indexes for the home-page render decision. | packages/UsageAnalysis/src/package.ts:397-415, packages/PowerPack/src/package.ts:138-147 | +| Subclassing `DG.Widget` for the widget body | `class MyHomeWidget extends DG.Widget { get type(): string { return 'MyHomeWidget'; } caption: string; constructor() { super(ui.panel([], {style: {display: 'flex'}})); this.caption = super.addProperty('caption', DG.TYPE.STRING, 'My widget'); /* append children to this.root */ } }`. The `super(...)` argument becomes `this.root: HTMLElement`. | Returning `ui.panel(...)` directly without wrapping in `DG.Widget` — the platform expects a `DG.Widget` instance per the function's `output: widget` annotation. Using `DG.Widget.fromRoot(...)` works for a one-off but loses the gear-icon settings hook (no `addProperty` access). | js-api/src/widgets/base.ts:231-257,322-325,345-365, packages/PowerPack/src/widgets/recent-projects-widget.ts:8-19, packages/PowerPack/src/widgets/community-widget.ts:6-28 | +| Declaring tunable widget settings (gear-icon panel) | Inside the constructor, `this. = super.addProperty(name, DG.TYPE., defaultValue)` — the call returns the default value, so the field initializer and registration happen in one line. Example: `this.caption = super.addProperty('caption', DG.TYPE.STRING, 'Recent projects')`. | A bare class field with no `addProperty` call — the platform won't expose it in the gear-icon settings panel. Adding a Widget property to control placement (`order`) — placement is a function-level decorator option (DG-FACT-140); the Widget property is per-instance UI state. | js-api/src/widgets/base.ts:345-365, packages/PowerPack/src/widgets/recent-projects-widget.ts:13,17, packages/PowerPack/src/widgets/community-widget.ts:11,26 | +| Overriding the widget type identifier | `get type(): string { return 'MyHomeWidget'; }` — used for serialization / widget registry. Default is `'Unknown'` (`js-api/src/widgets/base.ts:233`). Production canonical examples all override (RecentProjects, Community, Spotlight). | Skipping the override — leaves the widget with `type: 'Unknown'`, which clouds layout serialization. | js-api/src/widgets/base.ts:233, packages/PowerPack/src/widgets/recent-projects-widget.ts:9-11, packages/PowerPack/src/widgets/community-widget.ts:7-9 | + +## Work with package files + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Holding the package handle | A module-level `export const _package = new DG.Package();` (or `new ();`) inside `src/package.ts`. Every `webRoot` / `files.*` call dereferences this symbol. | A new `DG.Package()` per call — production never re-instantiates; the dart-bound pointer is set on first construction by the platform. | js-api/src/entities/misc.ts:59-70, packages/Chem/src/package.ts:1, packages/Bio/src/package.ts | +| Picking the right reserved folder for bundled data | `files/` for arbitrary data (text/JSON/binary, plus CSV when consumed via the FilesDataSource API), `tables/` for CSV-only data that you'll URL-fetch with `grok.data.loadTable`. Both allow subdirectories. `files/` is the only one wired to `_package.files`. | A custom folder when an existing convention applies — the platform special-cases `files/` (uploads to AppData on publish) and `tables/`. Custom folders are legal but lose the AppData wiring. | help/develop/how-to/packages/work-with-package-files.md:12-14, packages/Bio/files, packages/HitTriage/files, packages/ClinicalCase/tables | +| Fetching a static asset (image, README, .csv shipped under `tables/`) by URL | `_package.webRoot + ''` — webRoot already ends with `/`, so concat without a leading slash. Works for any URL-fetchable resource bundled in the source tree. Example: `\`url(${_package.webRoot}images/night-sky.png)\``. | A hard-coded `/packages//...` URL — webRoot encodes deployment-specific routing; concatenating manually breaks under non-default deploys. Doubling the slash (`webRoot + '/x.png'` → `…//x.png`). | js-api/src/entities/misc.ts:79-88, help/develop/how-to/packages/work-with-package-files.md:35-48, packages/HitTriage/src/app/pep-triage-views/info-view.ts:22,32 | +| Loading a CSV from `tables/` into a TableView | `grok.data.loadTable(\`${_package.webRoot}tables/test.csv\`).then((t) => grok.shell.addTableView(t))`. `loadTable` accepts any URL → `Promise`. | `_package.files.readCsv('test.csv')` for files placed in `tables/` — `_package.files` is rooted at `System:AppData/` (mirrors `files/`), so it cannot see `tables/`. | js-api/src/data.ts:170-175, js-api/src/shell.ts:258, help/develop/how-to/packages/work-with-package-files.md:42-43 | +| Listing files shipped in `files/` (recursive or flat) | `await _package.files.list('templates/', true)` → `FileInfo[]`. First arg is the relative path (under `files/`); second arg `recursive=true` for nested. Use `.name` / `.fullPath` on each entry. Optional 3rd arg is a `searchPattern` substring. | Iterating with raw fetch + manifest parsing — the platform exposes the AppData connection through this method. | js-api/src/dapi.ts:1340-1348, packages/HitTriage/src/app/utils.ts:162, packages/HitTriage/src/app/accordeons/new-template-accordeon.ts:21 | +| Reading a JSON file shipped in `files/` | `JSON.parse(await _package.files.readAsText('template.json'))` — there is no per-file `readAsJson`. (Bulk variant `_package.files.readFilesAsJson(folder, recursive?, ext?)` exists for whole folders.) | A `readAsBytes` + `TextDecoder` round-trip — `readAsText` already returns a string. | js-api/src/dapi.ts:1418-1421,1386-1395, help/develop/how-to/packages/work-with-package-files.md:64 | +| Reading a CSV file shipped in `files/` | `_package.files.readCsv('df.csv')` → `Promise` (convenience that wraps `DataFrame.fromCsv(await readAsText)`). The article uses the explicit form `DG.DataFrame.fromCsv(await _package.files.readAsText('df.csv'))` — both work. Pass `CsvImportOptions` as 2nd arg if needed. | `grok.data.loadTable` against an `_package.files`-rooted path — `loadTable` takes a URL, not an AppData connection path. | js-api/src/dapi.ts:1423-1427, help/develop/how-to/packages/work-with-package-files.md:63 | +| Reading binary data shipped in `files/` | `_package.files.readAsBytes('test.dat')` → `Promise`. Pair with `Blob`/`DataView` for downstream parsing. | `readAsText` for binary content — silently corrupts on UTF-8 decode. | js-api/src/dapi.ts:1429-1434, help/develop/how-to/packages/work-with-package-files.md:65 | +| Reading a `.d42` archive of dataframes | `(await _package.files.readBinaryDataFrames('project.d42'))[0]` → first DataFrame in the archive (the method returns `Promise`). `.d42` is Datagrok's binary serialization format — preferred over CSV for round-tripping typed columns. | `readAsBytes` + manual decode — `.d42` is platform-internal; let the platform parse it. | js-api/src/dapi.ts:1436-1440, help/develop/how-to/packages/work-with-package-files.md:66 | +| Naming the API class for `_package.files` | `DG.FilesDataSource`. The deprecated alias `DG.FileSource` (`export const FileSource = FilesDataSource`) is retained only for backward compatibility. | `DG.FileSource` in new code — its TypeScript definition is the same class, but the name will eventually be removed. | js-api/src/dapi.ts:1292,1472-1475 | + +## Custom script handlers + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a function as a script handler | A function annotated with `meta.role: scriptHandler` + `meta.scriptHandler.language: ` + `meta.scriptHandler.extensions: ` (comma-separated, no leading dot) + a single `funccall scriptCall` input. Header form (in `package.g.ts`) or decorator form `@grok.decorators.func({meta: {role: 'scriptHandler', 'scriptHandler.language': '', 'scriptHandler.extensions': ''}})` over a `static async (scriptCall: DG.FuncCall): Promise` inside `PackageFunctions`. The platform indexes registered functions by `meta.role` and dispatches script execution to the matching handler when a script's `#language:` header matches. | Hand-rolling a script-runner UI hook — the platform already has the script-creation flow indexed by language; an out-of-band runner won't be reachable from the "New Script" menu. The role string `scriptRunner` or similar — the only recognised role token is the literal `scriptHandler` (`js-api/src/const.ts:420`). | js-api/src/const.ts:420, js-api/src/api/ddt.api.g.ts:395-424, packages/Pyodide/src/package.ts:347-369, packages/Pyodide/src/package.g.ts:15-26 | +| Naming the handler's single input parameter | `funccall scriptCall` — the type token is `funccall` (lowercase, single word) and the canonical parameter name is `scriptCall`. The platform passes a populated `DG.FuncCall` whose `.inputs` reflect the script's own `#input:` header; write outputs via `scriptCall.outputs[] = `. | The article's example name `call` — works at the JS level but creates a name mismatch between header annotation (`scriptCall`) and parameter symbol; production canonical is `scriptCall`. | packages/Pyodide/src/package.g.ts:15,24, packages/Pyodide/src/package.ts:361-369 | +| Pointing the handler at a vectorization function | `meta.scriptHandler.vectorizationFunction: :` — nqName form. The target must be a registered package function with `//input: script script` + `//output: string result` (signature `(script: DG.Script) => string`). | A bare function name without the package prefix — the platform resolves nqNames by namespace and silently skips a malformed reference at handler-construction. | packages/Pyodide/src/package.g.ts:9-13,21, js-api/src/api/ddt.api.g.ts:410-414 | +| Choosing the in-editor syntax-highlighting mode | `meta.scriptHandler.codeEditorMode: ` — values from CodeMirror 5's mode catalog (`python`, `clojure`, `javascript`, …). Defaults to the `language` value when unset. | A CodeMirror 6 extension name — the platform's editor is CodeMirror 5 (`help/develop/how-to/scripts/custom-script-handlers.md:34`). | js-api/src/api/ddt.api.g.ts:422-424, packages/Pyodide/src/package.g.ts:20 | +| Setting the comment-start character for the language | `meta.scriptHandler.commentStart: ` — single character that begins a line comment. The platform uses it for the boilerplate `#name: / #description: / #input:` block synthesised when the user creates a new script. Defaults to `#` if unset. | Multi-character openers (e.g. `;;` for Clojure) — the platform reads a single character (`grok_shared.dart.js:121493`); use the language's single-char alternative (`;` for Clojure, `--` is two chars and won't work without testing). | js-api/src/api/ddt.api.g.ts:407-408, packages/Pyodide/src/package.g.ts:18 | +| Bundling the language icon for the "New Script" UI | `meta.icon: files/.` — package-source-relative path. Conventionally placed under `files/` so the asset is published alongside the package. | A URL or absolute path — the icon must be a published asset; the platform resolves the relative path against the package's webRoot. | packages/Pyodide/src/package.g.ts:22 | +| Seeding a "New Script" with starter code | `meta.scriptHandler.templateScript: ` — embed real newlines as the two-character escape `\n` (the YAML-style annotation parser unescapes them). The full Datagrok header (`#name: / #language: / #input: / #output:`) is part of the template, not auto-generated. | Hand-editing `package.g.ts` to insert real newlines — codegen will regenerate the file and overwrite. Use `\n` in the decorator's meta string. | packages/Pyodide/src/package.g.ts:19, packages/Pyodide/src/package.ts:354 | +| Overriding the UI label for the language | `meta.scriptHandler.friendlyName: ` — defaults to the `language` value. Useful when the language identifier is short/lowercase but the UI should show a capitalised name. | Renaming the `language` field — the language string is matched against script `#language:` headers at runtime, so changing it breaks existing scripts. | js-api/src/api/ddt.api.g.ts:403-405 | +| Plugging in a custom text→Script parser | `meta.scriptHandler.parserFunction: :` — nqName of a function that converts plain text into a `DG.Script` instance. Useful when the language has a non-default header convention. | Falling back to the platform's default parser for languages whose comment syntax doesn't match Datagrok's `#name:` line — the platform parses the header by stripping `commentStart` from each line; a custom parser is needed only when that's insufficient. | js-api/src/api/ddt.api.g.ts:416-417 | + +## Context actions (right-click + Actions pane) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a context-specific action | A function annotated with `//meta.action: ` (header form) or `meta: {action: ''}` in `@grok.decorators.func({...})` (decorator form). The function MUST declare exactly one input, and that input MUST carry a `{semType: }` annotation — the semantic type is the dispatch key, the `meta.action` text is the menu label. The platform offers the action wherever the right-clicked item's semantic type matches. | A function with `meta.action` but no `semType` on the input — it registers but is never offered as a context action (no dispatch surface). Manually attaching event handlers to the grid — the platform owns the right-click menu plumbing. | help/develop/how-to/ui/context-actions.md:5-9, js-api/src/api/ddt.api.g.ts:329-330, packages/Chem/src/package.ts:2010-2014, packages/Chem/src/package.g.ts:843-849 | +| Picking the input parameter type for the desired invocation surface | `semantic_value { semType: }` (→ `DG.SemanticValue`) for CELL right-click + per-row Actions pane — gives full cell context (`value.value`, `value.cell.column`, `value.cell.column.meta.units`, `value.cell.rowIndex`). `column { semType: }` (→ `DG.Column`) for column-HEADER right-click. `list { type: numerical }` (→ `DG.Column[]`) for multi-column selection. Match the type to where you want the action to surface. | `string { semType: }` (the article's example) — works but loses cell/column context, forces a `tv.dataFrame.columns.bySemType(...)` lookup that silently picks the wrong column when multiple columns share the semantic type. Production never uses `string` for context actions. | help/develop/how-to/ui/context-actions.md:13-17, packages/Chem/src/package.g.ts:765-849, packages/Bio/src/package.g.ts:391-396,510-516, packages/PowerGrid/src/package.g.ts:170-183 | +| Hiding the action from the Actions accordion pane (cell right-click only) | `//meta.exclude-actions-panel: true` (header form) or `meta: {action: '...', 'exclude-actions-panel': 'true'}` (decorator form). Use for sub-actions that would clutter the panel (e.g., 5+ `Copy as ` variants — keep only the parent `Copy as...` chooser in the panel). | Removing `meta.action` to hide from the panel — that also removes the action from the cell right-click menu. The two surfaces are scoped independently via these toggles. | js-api/src/api/ddt.api.g.ts:434, packages/Chem/src/package.g.ts:864,873,882, packages/Chem/src/package.ts:2066,2079,2092 | +| Hiding the action from the cell right-click menu (Actions pane only) | `//meta.exclude-current-value-menu: true` (header form) or `meta: {action: '...', 'exclude-current-value-menu': 'true'}` (decorator form). Use for actions that open a chooser/dialog and shouldn't crowd the cell right-click. Chem's parent `Copy as...` action uses this. | Skipping the toggle — the action will appear in BOTH the right-click menu and the Actions pane (the default). | js-api/src/api/ddt.api.g.ts:436, packages/Chem/src/package.g.ts:855, packages/Chem/src/package.ts:2042-2046 | +| Adding an open dataframe as a substructure filter from a Molecule cell action | `tv.getFiltersGroup({createDefaultFilters: false}).updateOrAdd({type: DG.FILTER_TYPE.SUBSTRUCTURE, column: molCol.name, columnName: molCol.name, molBlock}, false)`. Branch on `value.cell.column.meta.units == DG.chem.Notation.Smiles` and convert SMILES → MolBlock via `DG.chem.convertMolNotation` (preserves orientation); else use `molToMolblock(molecule, getRdKitModule())`. Guard with `if (grok.shell.tv == null)` and `if (value.cell?.column == null)`. | `.add({...})` (the article's call) — re-creates a duplicate filter row each invocation; users expect a single live filter. Bare `FILTER_TYPE.SUBSTRUCTURE` (article uses an undocumented import) — use `DG.FILTER_TYPE.SUBSTRUCTURE`. | packages/Chem/src/package.ts:2015-2040, js-api/src/views/view.ts:405, js-api/CLAUDE.md:151-154 | + +## Custom viewers (DG.JsViewer) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Building a dataframe-bound JS viewer | Subclass `DG.JsViewer` (`extends Viewer`). Constructor takes no args, `_root` is `ui.box()`, `subs: Subscription[]` is initialized to `[]`, `props: ObjectPropertyBag` is wired automatically. Class name SHOULD end in `Viewer`. | `DG.Viewer` directly — that's the dart-side base; `JsViewer` is the public extension surface for JS subclasses. Hand-rolled HTML containers — `ui.box()` participates in Datagrok's docking/resize system. | js-api/src/viewer.ts:376-404 | +| Registering a viewer property in the constructor | `this.(propertyName, defaultValue?, options?)` where type is one of `int / float / string / stringList / bool / dateTime / columnList`, plus the helpers `choices(name, default, choices[])` and `column(dataPropertyName)` (the latter auto-appends `ColumnName`). Each call returns the initial value AND registers the property in the context panel. | `this.props.add(...)` directly — the typed helpers wrap `addProperty` with the right TYPE token. Storing properties in private fields without registering them — they won't appear in the context panel and won't persist with the layout. | js-api/src/viewer.ts:457-499 | +| Marking a property as column-bound (Data tab) | Suffix the property name with `ColumnName` AND register it as a `string` (not the column's dtype): `this.fooColumnName = this.string('fooColumnName', 'someCol')`. Article-bullet alternative `this.column('foo', {options})` auto-appends the suffix. | `this.int('valueColumnName', 'age')` — the article's worked-example typo (`int` defaults are `number\|null`; column NAMES are strings regardless of underlying dtype). | js-api/src/viewer.ts:455-459, help/develop/how-to/viewers/develop-custom-viewer.md:127-129 | +| Re-rendering on dataframe state change | Push `DG.debounce(this.dataFrame.selection.onChanged, 50).subscribe((_) => this.render())`-style subscriptions into `this.subs` from `onTableAttached()`. Mirror for `.filter.onChanged` and `ui.onSizeChanged(this.root)`. | Subscribing without pushing to `subs` — leaks the subscription past `detach()`. Re-rendering on every event without `DG.debounce(..., 50)` — causes thrash on rapid filter changes. | js-api/src/viewer.ts:397-398,429-432, help/develop/how-to/viewers/develop-custom-viewer.md:103-110 | +| Reacting to a property edit | Override `onPropertyChanged(property)`, call `super.onPropertyChanged(property)` first, then branch on `property.name` (the parameter is a `Property`, not a string). Re-validate column type with `this.dataFrame.getCol(name).type !== property.propertyType`. | A bare reassignment + manual `render()` — the platform updates the field automatically; `onPropertyChanged` is for SIDE EFFECTS beyond re-render. | js-api/src/viewer.ts:412-444, help/develop/how-to/viewers/develop-custom-viewer.md:210-232 | +| Auto-grouping a property into a context-panel tab | Name it so the platform's auto-routing picks the tab — `*ColumnName` → Data, `*color` → Colors, `*axis*` → Axes, `legend*` → Legend, `*margin*` → Margins, `*marker*` → Markers, `title`/`description` → Description, else Misc. Override with `{category: ''}` in options. | Hard-coding tab labels by hand for properties whose names already match the convention — redundant noise. | help/develop/how-to/viewers/develop-custom-viewer.md:159-168, js-api/src/viewer.ts:409 | +| Adding the standard "rowSource" dropdown | Call `this.addRowSourceAndFormula()` from the constructor — registers `rowSource` (choices: `All / Filtered / Selected / SelectedOrCurrent / FilteredSelected / MouseOverGroup / CurrentRow / MouseOverRow`) plus a `formulaFilter` property under the Data tab. | Re-implementing the enum manually with `this.string('rowSource', 'Filtered', {choices: [...]})` — duplicates the platform's canonical list and risks divergence from future additions. | js-api/src/viewer.ts:406-410 | +| Registering a viewer with the platform | Either (a) annotated factory `//name: //tags: viewer //output: viewer result` returning `new MyViewer()` from `package.ts`, or (b) `@grok.decorators.viewer({name, description, icon, toolbox, trellisable, viewerPath})` over the class (datagrok-tools ≥ 4.12.x; codegen emits the function into committed `package.g.ts`). | `grok.shell.registerViewer(...)` from regular package code — async-loaded packages won't have the type registered when callers ask. Use it only from a `//meta.role: autostart` function when synchronous registration is required. | js-api/src/decorators/functions.ts:36-45, js-api/src/shell.ts:295, packages/Charts/src/viewers/sankey/sankey.ts:53-58 | +| Setting the viewer's top-menu placement | `meta.viewerPath` (header form) / `viewerPath` (decorator form), format `Subcategory \| Friendly Viewer Name`. Default is `Add > JavaScript Viewers > > `. | A `meta.viewerPath` value that doesn't include the viewer's own friendly name — the menu entry will collide with sibling subcategory items. | help/develop/how-to/viewers/develop-custom-viewer.md:495-515, js-api/src/decorators/functions.ts:42 | +| Setting the viewer's dock position | Header-only `//meta.viewerPosition: ` (values: `top / bottom / left / right / fill / auto`). | The decorator form — `viewerPosition` is NOT in the typed signature (`functions.ts:36-43`); decorator users must drop to header annotation in `package.g.ts` codegen. | help/develop/how-to/viewers/develop-custom-viewer.md:515, packages/PowerGrid/src/package.g.ts:225 | +| Marking the viewer as embeddable inside a Trellis plot | `meta.trellisable: true` (header form) / `trellisable: true` (decorator form). | Hardcoding `DG.VIEWER.TRELLIS_PLOT` checks at runtime — the platform consults this flag automatically. | js-api/src/decorators/functions.ts:41, js-api/src/const.ts (VIEWER.TRELLIS_PLOT) | +| Building a scripting viewer (Python/R/Julia) | Place `.py / .R / .jl` script under `/scripts/`. Header MUST include `# tags: viewers` (registers in Script Browser at `/scripts?q=%23viewers`), `# output: graphics`, `# language: `, dataframe + column inputs (`# input: column splitColumnName {type: categorical}`); optional `# sample: .csv`. Body returns/produces a graphics object (e.g., `plt.show()` for matplotlib). | A regular script with `# output: dataframe` — without `output: graphics` the platform won't treat it as a viewer. Omitting `tags: viewers` — works as a one-off script but won't show in the Script Browser viewer filter. | help/develop/how-to/viewers/develop-custom-viewer.md:413-477 | +| Constraining a scripting-viewer column input | `# input: column {type: }` — restricts the dropdown to compatible columns and seeds a sane initial value. | Trying to type-restrict to a SemanticType in a scripting viewer header — column type tokens accept the data-type taxonomy, not semantic types. | help/develop/how-to/viewers/develop-custom-viewer.md:449-455 | +| Showing a multi-row tooltip from a viewer element | `ui.tooltip.showRowGroup(this.dataFrame, i => , event.x, event.y)` on `mouseover`, paired with `ui.tooltip.hide()` on `mouseout`. The predicate selects every dataframe row the hovered element represents; the platform highlights that group in every other open viewer for free. | Manually building a `
` tooltip with HTML and absolute positioning — bypasses Datagrok's row-group highlight bus and won't cross-link to other viewers. | js-api/ui.ts:1606-1608, help/develop/how-to/viewers/develop-custom-viewer.md:371-374 | +| Selecting rows in response to a click inside a viewer | `this.dataFrame.selection.handleClick(i => , event)` from a `mousedown` handler — pass the raw `MouseEvent` so the platform honors Ctrl/Shift/Meta semantics. | Calling `dataFrame.selection.set(i, true)` directly in a loop — overwrites the user's selection regardless of modifier keys and breaks Ctrl-click extend / Shift-click range conventions. | js-api/src/dataframe/bit-set.ts:220-222, help/develop/how-to/viewers/develop-custom-viewer.md:375-379 | +| Aggregating a viewer's data while respecting the user's filter | `this.dataFrame.groupBy([splitCol]).whereRowMask(this.dataFrame.filter).add(aggType, valueCol, 'result').aggregate()` inside `render(computeData=true)`. | Skipping `whereRowMask(this.dataFrame.filter)` — the viewer renders aggregations over the full frame and silently ignores filter changes. Also passing `this.dataFrame.filter` to the `groupBy([...])` argument — that arg is the LIST of grouping COLUMNS, not a row mask. | js-api/src/dataframe/stats.ts:350-354, help/develop/how-to/viewers/develop-custom-viewer.md:256-260 | + +## Manipulate viewers + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Attaching ANY viewer (native or custom) to a TableView | `view.addViewer(v: ViewerType \| string \| Viewer, options?)` — pass either the type string (`DG.VIEWER.HISTOGRAM`) or a pre-built `Viewer` instance; optional `options` is applied via `setOptions` after attach. Returns the attached `Viewer`. | `view.histogram(opts)` / `view.scatterPlot(opts)` / `view.barChart(opts)` / etc. — every wrapper on `View` carries JSDoc `deprecated: use addViewer(Viewer.(options))` and is slated for removal in 1.21. The article's primary examples (lines 32-36) still feature these. | js-api/src/views/view.ts:418-427,444-587 | +| Creating a NATIVE viewer instance to dock independently | `DG.Viewer.fromType(viewerType, table, options=null)` (sync) or one of the typed factories `DG.Viewer.scatterPlot(table, options?)`, `DG.Viewer.histogram(table, options?)`, `DG.Viewer.barChart(table, options?)`, etc. (typed return — `ScatterPlotViewer`, `HistogramViewer`, …). | `new Viewer(...)` — no public constructor for native viewers. | js-api/src/viewer.ts:128-130,215-305 | +| Creating a CUSTOM (package-provided) viewer instance | `await dataFrame.plot.fromType(viewerType, options=null)` — returns `Promise`; await it because the owning package may need to load. | `DG.Viewer.fromType(...)` — synchronous; only works for the 25 CORE viewers and silently fails for package-provided types. | js-api/src/dataframe/data-frame.ts:557-559 | +| One-call create-and-attach (DataFrame entrypoint) | `dataFrame.plot.(options?)` — the 10 sync helpers `scatter`, `grid`, `tile`, `form`, `histogram`, `bar`, `heatMap`, `box`, `line`, `network`. Each returns a typed `Viewer` you then pass to `view.addViewer(viewer)`. | `dataFrame.plot.barChart(...)` / `.boxPlot(...)` / `.lineChart(...)` — those names don't exist on `DataFramePlotHelper` (they're the names on `DG.Viewer` statics). The helper uses the SHORT names (`bar`, `box`, `line`). | js-api/src/dataframe/data-frame.ts:551-572 | +| Reading a viewer's current settings | `viewer.getOptions(includeDefaults: boolean = false)` → `{id, type, look}`. Default `false` omits properties at default (cleaner serialization); pass `true` for an exhaustive snapshot. The mutable property bag is on `.look` — `view.grid.getOptions(true).look` gives the grid's complete property map. | Reading `getOptions()` and expecting a flat key/value map — the wrapper has three top-level fields. | js-api/src/viewer.ts:158-160 | +| Writing viewer settings at runtime | `viewer.setOptions({propertyName: value, …})` — flat property map, not the `{look: {...}}` wrapper. Optional `type` field swaps the viewer kind. | Wrapping the map in `{look: {...}}` — `setOptions` writes property names directly. | js-api/src/viewer.ts:146-148 | +| Inspecting available properties of an attached viewer | `viewer.getProperties(): Property[]`. Each `Property` has `name`, `propertyType`, `semType`, `description`, `defaultValue`, `choices: string[]`, and `columnTypeFilter: ColumnType \| 'numerical' \| 'categorical' \| null`. | `viewer.getProperty(name)` blindly — works (`js-api/src/viewer.ts:436-438`) but returns `undefined` silently for typos; `getProperties` is the discovery surface. Also `p.columnFilter` — RENAMED to `p.columnTypeFilter` (DRIFT-078). | js-api/src/viewer.ts:166-168, js-api/src/entities/property.ts:248-251 | +| Docking a viewer relative to its TableView | `view.dockManager.dock(viewer, DG.DOCK_TYPE.RIGHT)` — the per-view manager (`view.dockManager`) docks viewers INSIDE the view; they move/close with the view. Default position when omitted is `LEFT` (not `FILL`). | `grok.shell.dockManager.dock(viewer, DG.DOCK_TYPE.RIGHT)` when you want the viewer tied to the data view — the shell-level manager docks at the root, independent of any view. | js-api/src/docking.ts:135-137, js-api/src/views/view.ts:440-442 | +| Docking a viewer floating at the platform root | `grok.shell.dockManager.dock(element, DG.DOCK_TYPE.RIGHT, refNode?, title?, ratio=0.5)`. | A second `view.dockManager.dock(...)` thinking it's "global" — that one is scoped to the view's window tree. | js-api/src/shell.ts:361-363 | +| Picking a dock position string | `DG.DOCK_TYPE.{LEFT, RIGHT, TOP, DOWN, FILL}` — always reference the constant. | Inlining `'top'` as a literal string — `DG.DOCK_TYPE.TOP` resolves to `'up'`, NOT `'top'` (DRIFT-079). The enum NAME is `TOP` but the VALUE is `'up'`. Article's prose at line 263 understates this. | js-api/src/const.ts:776-782 | +| Rendering a viewer inside a custom view or dialog | `DG.Viewer.scatterPlot(table)` (or any typed factory), then append `viewer.root` to the container — `grok.shell.newView('foo').append(v.root)` or `ui.div([…, DG.Viewer.scatterPlot(table)])` inside `ui.dialog()`. | `view.addViewer(...)` for non-TableView containers — `addViewer` is a method on `TableView` and adds the viewer to the dataset's docking model, not to an arbitrary HTMLElement container. | js-api/src/viewer.ts:215-305, help/develop/how-to/viewers/manipulate-viewers.md:230-246 | +| Running custom code every time a viewer is constructed (incl. layout/project restore) | Set `initializationFunction: ':'` in the viewer's options. The named function takes a single `viewer`-typed input and runs on every construction. Production canonical: `Chem` `activityCliffsInitFunction` registered with `@grok.decorators.func` over `static async (sp: DG.ScatterPlotViewer)`. | `viewer.onAfterDrawScene.subscribe(...)` written from package init — fires only for the live instance, not when the viewer is reconstructed from a saved layout. | packages/Chem/src/package.ts:1218,1238-1256, js-api/src/interfaces/d4.d.ts:77,231,261 | +| Choosing between sync and async viewer creation | Sync (`DG.Viewer.fromType` / `view.addViewer(viewerInstance)`) for the 25 CORE viewers in `DG.CORE_VIEWER` (`Viewer.getViewerTypes({core: true})`). Async (`dataFrame.plot.fromType` / `await` on the result) for package-provided types — `GLOBE`, `GOOGLE_MAP`, `WORD_CLOUD`, `TIMELINES`, `RADAR_VIEWER`, `SURFACE_PLOT`, `SCAFFOLD_TREE`. | A bare `view.addViewer('Globe')` followed by an immediate `setOptions({...})` — addViewer accepts the string, but the package may still be loading; property writes can race the viewer's true construction. Pre-build with `await dataFrame.plot.fromType('Globe', opts)` instead. | js-api/src/const.ts:670-733, js-api/src/viewer.ts:135-139, js-api/src/dataframe/data-frame.ts:557-559 | + +## Show formula lines + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Adding a reference line that travels with the data | `df.meta.formulaLines.addLine({formula:'${y} = ...', ...})` — persists into the `.formula-lines` tag, renders on EVERY supporting viewer attached to that frame. | `viewer.meta.formulaLines.addLine(...)` for a line you want consistent across multiple plots — viewer-scoped storage only renders on that one viewer. | js-api/src/dataframe/formula-helpers.ts:11-21, js-api/src/helpers.ts:109-112 | +| Adding a viewer-scoped line | `viewer.meta.formulaLines.addLine({...})` — writes to `viewer.props['formulaLines']`; isolated to that viewer instance and travels with layouts. | `df.meta.formulaLines.addLine(...)` when you only want it on this one chart — it would also appear on every other supporting viewer. | js-api/src/viewer.ts:835-857 | +| Bulk loading multiple lines/bands | `helper.addAll([{type:'line', formula:'…'}, {type:'band', formula:'…', column2:'…'}])` — single re-serialize of the storage list. | A loop of `addLine`/`addBand` for N items — each call parses + re-stringifies the WHOLE list (O(N²)). | js-api/src/helpers.ts:104-107 | +| Hiding all formula lines without losing them | Set `showDataframeFormulaLines: false` and/or `showViewerFormulaLines: false` on the viewer's options (`view.scatterPlot({...})` constructor or `viewer.setOptions({showDataframeFormulaLines:false})`). Both default `true`. | `helper.clear()` to "turn them off" — that's destructive, you'd have to re-add them. | js-api/src/interfaces/d4.ts:597,602,909,914,1199,1204,1605,1610,1932,1937,2331,2336 | +| Hiding a single line | `helper.updateAt(idx, {...item, visible: false})`. | `removeAt(idx)` — see DRIFT-081, the impl wipes the whole list, not just that index. | js-api/src/helpers.ts:119-123 | +| Removing one line by index | `helper.removeWhere((_, i) => i === idx)` — predicate filter is correct. | `helper.removeAt(idx)` — implementation `slice(idx, idx + count - 1)` returns `[]` when `count=1` (default). DRIFT-081. | js-api/src/helpers.ts:125-131 | +| Generic add when type is data-driven | `helper.add({type: 'line' | 'band', formula:'…', ...})` — discriminator `type` is REQUIRED here (no default). | Forgetting `type` — the item persists but renders neither as a line nor as a band. | js-api/src/helpers.ts:99-102, help/develop/how-to/viewers/show-formula-lines.md:75-79 | +| Setting a line on Trellis Plot | Same `viewer.meta.formulaLines.addLine(...)` API; the helper transparently routes to `innerViewerLook` storage so the inner viewers pick it up. | Hand-writing `viewer.props['formulaLines']` for a TRELLIS_PLOT — bypasses the inner-look indirection and gets ignored at render. | js-api/src/viewer.ts:838-851 | +| Reading current line list | `helper.items: FormulaLine[]` — re-parses storage on every read; cache the array if you iterate. | Calling `df.getTag('.formula-lines')` directly — returns the raw JSON string, you'd have to parse it yourself. | js-api/src/helpers.ts:90-97 | +| Choosing line style | One of `'solid' \| 'dotted' \| 'dashed' \| 'longdash' \| 'dotdash'` (line-only field; bands have no `style`). | Other dash patterns or CSS `border-style` literals — anything outside the five values is silently coerced to `'solid'`. | help/develop/how-to/viewers/show-formula-lines.md:135, packages/ApiSamples/scripts/data-frame/metadata/formula-lines.js:34 | +| Picking opacity | Number on `[0..100]` (NOT `[0..1]`). Default `100`. | CSS-style `0.7` for 70% opacity — passes the type check, renders almost-invisible. | js-api/src/helpers.ts:16, help/develop/how-to/viewers/show-formula-lines.md:129 | +| Stacking lines / bands relative to chart | `zIndex` integer; chart itself is `0`, negatives render under, positives over, ties broken by insertion order. Default `100` (above chart). | `z-index` (CSS) — the field name is `zIndex` (camelCase). | js-api/src/helpers.ts:17, help/develop/how-to/viewers/show-formula-lines.md:128 | + +## Custom views + +| For… | Use | Don't use | Source | +|---|---|---|---| +| One-shot ad-hoc view (no URL routing, no state save/load) | `grok.shell.newView(name, [el1, el2, …])` — returns a fully docked `View` with the elements appended to its root. | `new DG.View(...)` — `View` constructor takes a Dart handle, not a name; for a fresh view, the factory is `View.create(options)` and the registration step is `grok.shell.addView(v)`. | js-api/src/shell.ts:234-242 | +| A view shipped from a package (URL-routable, state-serializable) | Subclass `DG.ViewBase` and override the surface you need: `type`, `getIcon()`, `saveStateMap()`, `loadStateMap(stateMap)`, `handlePath(path)`, `acceptsPath(path)`, plus `name` / `helpUrl` / `path` getters. Registration is via a function annotated `tags: view`, `input: map params`, `input: string path`, `output: view result` that returns `new MySubclass(params, path)`. | `grok.shell.newView(...)` for anything that needs URL state or restore-from-layout — the `View` it returns is a leaf that does not dispatch path. | js-api/src/views/view.ts:45-204, packages/Notebooks/src/package.js:27-135 | +| Wiring a custom view's URL state | Override `acceptsPath(urlPath): boolean` and `handlePath(urlPath): void`; let `path` getter return the synthesized URL. | Listening on `window.hashchange` from the constructor — bypasses Datagrok's per-view dispatcher. | js-api/src/views/view.ts:170-178 | +| Persisting a custom view inside a project / layout | Override `saveStateMap()` to return a JSON-serializable object and `loadStateMap(stateMap)` to rebuild from it. The platform calls these on project save / restore. | Holding state in module-level variables — won't be re-applied when the project is reopened. | js-api/src/views/view.ts:163-167 | +| Custom view tab icon | Override `getIcon(): HTMLElement \| null` and return an `` / `` element. The platform reads this each time the view tab renders. | Calling `setIcon(icon)` from the constructor — that path goes through Dart and is meant for the imperative shell ("set the icon I just decided"); for a static per-class icon, `getIcon()` is the contract. | js-api/src/views/view.ts:158-161, packages/Notebooks/src/package.js:101-107 | + +## Layouts + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Capturing the current view's visual state | `view.saveLayout({saveWithData?: false})` → `ViewLayout` (only meaningful on `TableView`-shaped views; no-op on plain `View`). Pass `saveWithData: true` to inline the underlying data into the resulting layout (the `.layout` files Chem ships do this). | `JSON.stringify(view)` or hand-rolled snapshot — bypasses the platform's viewer-state serializer and won't reapply. | js-api/src/views/view.ts:305-307 | +| Reapplying a captured layout | `view.loadLayout(layout, pickupColumnTags?: boolean)` — `pickupColumnTags=true` copies the layout's stored column tags onto the destination frame during apply. | Calling `loadLayout` on a non-table view — it silently does nothing or throws. Walking `viewStateMap` and re-creating viewers by hand. | js-api/src/views/view.ts:297-299 | +| Round-tripping a layout through JSON (file-share / userSettings storage) | `DG.ViewLayout.fromJson(jsonString)` (full doc with `columns` metadata) and `layout.toJson()`. Pair with `_package.files.readAsText('demo_files/x.layout')` for package-bundled `.layout` files. | `JSON.parse(jsonString)` + manual reconstruction — the Dart-side parser is the only thing that knows the schema. | js-api/src/entities/view-layout.ts:22-23, 46-48 | +| Reapplying just the visual state (drop column metadata) | `DG.ViewLayout.fromViewState(layout.viewState)` — rebuilds a layout from the bare `viewState` JSON only. Useful when the destination columns are guaranteed to match by position/name, no `columns`-metadata matching needed. | `fromJson` for state-only restores — carries unnecessary column metadata that can pollute the destination. | js-api/src/entities/view-layout.ts:26-28, 30-36 | +| Listing / finding / saving server-side layouts | `grok.dapi.layouts.{list, find, first, save, delete, count}` (inherited from `HttpDataSource`) plus chained `.filter(w).order(field).page(n)`. `save(layout)` is the JS equivalent of `View | Layout | Save to Gallery`. | A separate `userSettings` namespace for layouts you want shared — gallery is the central store and respects entity permissions. | js-api/src/dapi.ts:111-113, 244-380, 645-660 | +| Asking which gallery layouts can apply to a frame | `grok.dapi.layouts.getApplicable(df)` → `Promise` (server walks the matching algorithm: name+type, `layout-id` tag, or `quality` tag). | Calling `list()` and filtering client-side by name — misses the tag-based matches that don't share column names. | js-api/src/dapi.ts:657-659 | +| Forcing a layout to apply across datasets with different column names | Set `column.tags['layout-id'] = ''` on both source and destination columns BEFORE saving the layout / before calling `getApplicable`. | Renaming destination columns to match the layout's source — fragile and surprises end users. | js-api/src/const.ts:283 | +| Annotating a column so a layout matches by semantic type | Set `column.semType = ''` (writes the `quality` tag, `DG.TAGS.SEMTYPE = 'quality'`). | Free-form tag values that don't map to a registered semantic type — match works but other platform features (cell renderers, context actions) won't fire. | js-api/src/const.ts:318 | +| Attaching arbitrary metadata to a saved layout | `layout.setUserDataValue(key, value)` / `layout.getUserDataValue(key)` — string-only, persisted with the layout entity. | Stuffing structured metadata into `viewState` — gets clobbered on next `saveLayout`. | js-api/src/entities/view-layout.ts:38-44 | + +## JS API entry points + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Importing the JS API in a package source file | `import * as grok from 'datagrok-api/grok'`, `import * as ui from 'datagrok-api/ui'`, `import * as DG from 'datagrok-api/dg'` — the three canonical entry points. | A single `import 'datagrok-api'` (no default export) or per-class imports from `'datagrok-api/dg'` — the namespace import is what lines up with the help articles' `grok.*` / `ui.*` / `DG.*` references. | js-api/grok.ts, js-api/dg.ts, js-api/ui.ts | +| Discovering the right `grok.` for a task | The `grok` namespace — IntelliSense-driven entry: `functions, events, dapi, shell, settings, data, userSettings, ai, log` plus re-exported `chem.*`, `ml.*`, `decorators.*`. Pick the namespace closest to the noun (data → `data`, server entities → `dapi`, UI → `shell` or `ui`). | Reaching into `DG.*` first when a `grok.*` shortcut exists — the article's note says drop to `DG` only "when you need more control". | js-api/grok.ts:11-38 | +| Subscribing to a platform event | `grok.events.on.subscribe(handler)` — RxJS `Observable`. Per-entity events live on the entity (`dataFrame.onValuesChanged.subscribe(...)`). For event-name discovery use Inspector (Alt+I) → Client Log → click an event → context-panel snippet. | Polling with `setInterval` or hand-wired DOM event listeners on platform widgets. | js-api/src/events.ts:45-60, js-api/grok.ts:14 | +| Registering a globally-callable function from JS | `grok.functions.register({signature, run, tags?, isAsync?, namespace?, options?})` — `signature` uses Datagrok type names (e.g., `'Widget jsWidget()'`, `'List jsSuggestCountryName(String text)'`, `'({int x, int y}) foo(string bar)'` for multi-output). | Class decorators (`@grok.decorators.func`) for ad-hoc / dynamic registration — those are for compile-time package functions, not for code that registers at runtime (e.g., from an init hook). | js-api/src/functions.ts:106-138 | +| Defining a custom domain class with native context actions / rendering / drag-drop / tooltips | Extend `DG.ObjectHandler` (NOT `DG.EntityMeta` — see DG-FACT-DRIFT-087) and call `DG.ObjectHandler.register(handler)` from an autostart init function. Override `isApplicable`, `renderTooltip`, `renderCard`, `renderProperties`, etc. | The non-existent `DG.EntityMeta` symbol referenced in the js-api overview article. | js-api/ui.ts:1651-1773 | +| Wrapping a raw DOM element so it can be returned from a function with signature `Widget` | `new ui.Widget(domElement)` — `Widget` is exported from `datagrok-api/ui` (re-exported from `js-api/src/widgets/base.ts:231`). | Returning the raw DOM node — the platform expects a `Widget` instance for signatures typed `Widget`. | js-api/src/widgets/base.ts:231, js-api/ui.ts | + +## REST API (external integrations) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Authenticating against the REST API | `Authorization: Bearer ` header on every request. Copy the API key from user profile → "API Key" link. The Python client passes the supplied string verbatim, so the caller must include the `Bearer ` prefix. (DG-FACT-229) | An `Authorization` header without the `Bearer ` prefix, or a `?token=` query string. | help/develop/packages/rest-api.md:12-18, python-api/datagrok_api/http_client.py:5-9 | +| Picking the API base URL | `/api` (e.g. `https://public.datagrok.ai/api`). All endpoint paths under `/public/v1/...` are relative to this. (DG-FACT-230, DG-FACT-231) | The plain deployment URL without `/api`, or the UI path `/p/...`. | help/develop/packages/rest-api.md:22 | +| Referring to a Datagrok entity by name in a REST call | The Grok name with colons replaced by periods (`Demo:Files:cars.csv` → `Demo.Files.cars.csv`). The Python client does this with `id.replace(':', '.')` on every resource. (DG-FACT-232) | The raw Grok-name form with `:` separators — the route won't match. | python-api/datagrok_api/resources/tables.py:30, python-api/datagrok_api/resources/functions.py:67 | +| Invoking a Datagrok function from outside the platform | `POST /public/v1/functions/{name}/call` with parameters as a JSON body. Note the `functions/` segment — the help article omits it (see DG-FACT-DRIFT-090). | `POST /public/v1/{name}/call` — what the article literally says, but produces 404 against a real server. | python-api/datagrok_api/resources/functions.py:67-68 | +| Discovering the full REST endpoint surface | The OpenAPI spec at `/public/api.yaml` (e.g. `https://public.datagrok.ai/api/public/api.yaml`). | The help article alone — it documents only a curated subset (files / tables / dashboards / functions) and misses real surfaces like `/connections`, `/users`, `/groups`, `/entities/{id}/shares`. | help/develop/packages/rest-api.md:70 | +| Driving the REST API from Python | `from datagrok_api import DatagrokClient; grok = DatagrokClient(base_url, api_key)`, then `grok.tables.upload(...)`, `grok.functions.call(name, params)`, `grok.files.upload(...)`, etc. | Hand-rolling `requests.post(...)` — you'd reimplement the colon→period rewrite, header handling, and CSV serialization that the client already does. | python-api/datagrok_api/datagrok_client.py:97-98 | +| Driving the REST API from the shell | `grok s` CLI — same surface, see the `server-management.md` article. | Ad-hoc `curl` for repeated workflows — fine for one-offs, but you'll re-derive the auth header and identifier rewriting on every call. | help/develop/server-management.md | + +## Cheminformatics + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Substructure search over a column of molecules | `await grok.chem.searchSubstructure(column, pattern, { molBlockFailover? })` → `Promise` of input column length, 1=hit, 0=miss-or-unparseable. (DG-FACT-238) | `{ substructLibrary: true }` from the article — the setting is not in the TS type and is ignored (DG-FACT-DRIFT-093). Hand-rolled `RDKit.get_mol` loops — bypasses the cache that survives across calls on the same Column. | js-api/src/chem.ts:766-775 | +| Top-N most similar molecules sorted by score | `await grok.chem.findSimilar(column, molecule, { limit?, cutoff? })` → `Promise` with `molecule`/`score`/`index` columns, sorted descending by score. Defaults `{ limit: Number.MAX_VALUE, cutoff: 0.0 }` — pass a `limit` to cap, `cutoff` to drop low-similarity rows. (DG-FACT-239) | Calling `getSimilarities` and sorting in JS — `findSimilar` already does both server-side. | js-api/src/chem.ts:726-737 | +| Per-row similarity scores aligned with the input column | `await grok.chem.getSimilarities(column, molecule)` → `Promise` (single Column, not a DataFrame; `null` when `molecule === ''`). (DG-FACT-240) | Treating the result as a DataFrame as the article phrases it — it's a single Column. Using `findSimilar` and re-sorting back to original order — extra work for the same data. | js-api/src/chem.ts:699-708 | +| Priming the per-column fingerprint cache without scoring | `await grok.chem.getSimilarities(column, '')` (empty molecule) — builds the Morgan-fingerprint cache for the Column and returns `null`. Subsequent calls on the same Column reuse it. (DG-FACT-240) | A real-but-arbitrary molecule string — wastes the scoring pass. | js-api/src/chem.ts:699-708 | +| Selecting a diverse subset of molecules | `await grok.chem.diversitySearch(column, { limit })` → `Promise` with one column `molecule`. Default limit is `Number.MAX_VALUE`. (DG-FACT-DRIFT-091) | The article's `diversitySearch(column, metric, limit=10)` signature — `metric` parameter doesn't exist, default isn't 10. There is no `METRIC_TANIMOTO` constant. | js-api/src/chem.ts:750-756 | +| Finding the Maximum Common Substructure across a column | `await grok.chem.mcs(table, columnName, returnSmarts?, exactAtomSearch?, exactBondSearch?)` → `Promise` (SMILES, or SMARTS if `returnSmarts=true`). NOTE: takes a DataFrame plus column NAME, not a Column. (DG-FACT-DRIFT-092) | The article's `mcs(column)` form — wrong shape, will throw or produce a wrong result. | js-api/src/chem.ts:797-806 | +| R-group decomposition around a core | `await grok.chem.rGroup(table, columnName, coreSmiles)` → `Promise` of R-groups (column prefix `R`). | Hand-rolling RDKit `RGroupDecomposition` in JS — server pathway is faster and respects the platform cache. | js-api/src/chem.ts:785-789 | +| Computing molecular descriptors in bulk | `await grok.chem.descriptors(table, columnName, descriptorNames[])` mutates `table` in place by adding descriptor columns; awaitable returns the same `table`. Discover available descriptors via `await grok.chem.descriptorsTree()`. | Per-row JS calls — descriptor calculation is RDKit-Python on the server; a single bulk call amortizes the round-trip. | js-api/src/chem.ts:817-828 | +| Rendering a molecule to SVG (lightweight, sync) | `grok.chem.svgMol(molString, width=300, height=200, options?): HTMLDivElement` — returns the div immediately and fills its `innerHTML` after a dynamic OpenChemLib import. Accepts SMILES or molfile (auto-detected by `M END`). (DG-FACT-241) | `canvasMol` for static UI — heavier (RDKit + server round-trip) and async. | js-api/src/chem.ts:837-851 | +| Rendering a molecule to a `` with optional scaffold highlight | `await grok.chem.canvasMol(x, y, w, h, canvasEl, molString, scaffoldSmarts \| null, options?)` — scaffold is highlighted when passed as SMARTS; molecule string in any RDKit-supported notation. Async (delegates to `Chem:canvasMol`). (DG-FACT-241) | Mixing in `svgMol` for cell renderers — inconsistent rendering with the rest of the platform's RDKit pipeline. | js-api/src/chem.ts:858-868 | +| Picking a renderer for grid cells / `customRenderers` | `'molecule'` (SMILES/Molfile via `grok.chem.drawMolecule`) — the platform-blessed string for chemistry cells. | `'rawImage'` / `'imageURL'` — those are for image columns, not molecules; they bypass the RDKit pipeline. | js-api/src/chem.ts:870-877 | +| Validating a SMILES string client-side | `grok.chem.checkSmiles(s): boolean` — synchronous, returns whether the string parses as SMILES. | Round-tripping through `searchSubstructure` to test parseability. | js-api/src/chem.ts:896 | +| Custom client-side cheminformatics inside a package | OpenChemLib JS (rendering, atom/bond manipulation) or RDKit-WASM (parity with Python RDKit, runs in a Web Worker). Both are in-browser. | Calling out to a server endpoint when the data fits in memory client-side — defeats Datagrok's "compute in the browser" model. | help/develop/domains/chem/cheminformatics.md:142-168 | +| Custom server-side cheminformatics | RDKit-Python embedded via the platform's Scripting feature — the Chem package ships a battery of these in `packages/Chem/scripts/`. | Inventing a new transport — Scripting already handles the JS↔Python boundary. | help/develop/domains/chem/cheminformatics.md:152-160 | + +## Docking (AutoDock) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Docking a single ligand or column of ligands from a package | `await grok.functions.call('Docking:getAutodockResults', { table, ligands, target, poses })` — the vector function declared with `outputs: [{name:'result', type:'dataframe', options:{action:'join(table)'}}]` joins poses + binding-energy columns back to the input frame on call. (DG-FACT-244, DG-FACT-247) | Hand-driving `IAutoDockService.dockLigand` per row — bypasses the function-cache (`Docking:dockLigandCached`, monthly invalidation `0 0 1 * *`) and the `processAutodockResults`/`detectSemanticTypes` post-processing that gives the columns their semantic types. | packages/Docking/src/package.ts:87-122 | +| Acquiring the AutoDock service from outside the Docking package | `import {getAutoDockService} from '@datagrok-libraries/bio/src/pdb/auto-dock-service'; const svc = await getAutoDockService();` — the bio-lib helper finds `Docking:getAutoDockService` via `DG.Func.find` and throws `Package 'Docking' must be installed for AutoDock service.` if absent. (DG-FACT-246) | Direct `new AutoDockService()` from the Docking package — that class is package-internal and not re-exported; bio-lib's helper is the supported boundary. | libraries/bio/src/pdb/auto-dock-service.ts:73-81 | +| Gating on container readiness before issuing dock requests | `await svc.awaitStatus('started', 30000)` (default 30 s timeout) before any `dockLigand`/`dockLigandColumn`. The `ready` getter is cheap (returns true while status is `'started'` or `'checking'`) but doesn't wait. (DG-FACT-246) | Calling `dockLigand` when `svc.ready === false` — request goes to a stopped container; expect a non-200 from `fetchProxy`. | packages/Docking/src/utils/auto-dock-service.ts:90-96, libraries/bio/src/pdb/auto-dock-service.ts:53 | +| Listing available targets | `await grok.functions.call('Docking:getConfigFiles')` → `string[]` of folder names under `System:AppData/Docking/targets` that contain a `.gpf` file. The `runAutodock` dialog wires this directly via `choices: 'Docking:getConfigFiles'`. (DG-FACT-242) | Walking `grok.dapi.files.list('System:AppData/Docking/targets', true)` and filtering yourself — duplicates the public function and skips the `.gpf` filter that defines what counts as a valid target. | packages/Docking/src/package.ts:52-62,133 | +| Adding a new docking target | Place `.pdbqt` (preferred) or `.pdb` plus `.gpf` in a new folder at `System:AppData/Docking/targets//`. The folder name becomes the target choice in the AutoDock dialog. Use `AutoDock tools` to prepare the macromolecule + grid parameter file. (DG-FACT-242, DG-FACT-243) | Storing the receptor or `.gpf` outside the target folder, mixing more than one receptor per folder, or omitting the `.gpf` (the dialog will silently exclude the folder from `getConfigFiles`). | help/develop/domains/chem/docking.md:13-25, packages/Docking/src/package.ts:362-370 | +| Choosing atomic map types in a custom GPF | Include all atom types your ligand library actually contains. Article-recommended baseline for "big ligand dataset": `A C HD N NA OA SA CL` (matches `BACE1.gpf`). The package's `buildDefaultAutodockGpf` fallback uses `A C N F NA OA HD SA` (adds F, drops CL) — so chlorinated ligands fail under the default. (DG-FACT-243) | A reduced map set "to make grids small" — AutoDock fails per-ligand if any required atom type is missing. | packages/Docking/files/targets/BACE1/BACE1.gpf:5, packages/Docking/src/utils/auto-dock-service.ts:46-69 | +| Reading docking output columns by name | `BINDING_ENERGY_COL = 'binding energy'`, `POSE_COL = 'pose'`. Per-row AutoDock properties (`intermolecular (1)`, `electrostatic`, `ligand fixed`, `ligand moving`, `total internal (2)`, `torsional free (3)`, `unbound systems (4)`) are parsed out of the pose's PDBQT REMARK block by `getRemarksFromPdb`. Human-readable descriptions live in `AUTODOCK_PROPERTY_DESCRIPTIONS`. (DG-FACT-244) | Hard-coding column names without importing the constants — they're shared with the AutoDock context-panel widget; renaming would silently break the widget's `value.includes(BINDING_ENERGY_COL)` discriminator. | packages/Docking/src/utils/constants.ts:6-29 | +| Showing the binding pocket for a pose | `await currentTable.plot.fromType('Biostructure', { dataJson: BiostructureDataJson.fromData(receptorData), ligandColumnName: .name, zoom: true })` — the BiostructureViewer plot type renders the receptor (Molstar engine) and zooms to the ligand. Used as the AutoDock context-panel widget. (DG-FACT-244) | Constructing an NGL or raw Molstar viewer by hand — duplicates what BiostructureViewer already wires (ligand-column linking, zoom, error handling for missing receptor data). | packages/Docking/src/package.ts:210-217 | +| Selecting the dock-batch size for a single dialog run | Default `poses = 10` (dialog `initialValue: '10'`). API-level default for `IAutoDockService.dockLigand`/`dockLigandColumn` is `30` — pick higher for offline batch jobs, leave at 10 for interactive UI flows. (DG-FACT-245) | Setting `poses = 1` "to go faster" — AutoDock's pose-clustering needs ≥3 to produce meaningful binding-energy ordering. | packages/Docking/src/package.ts:134, packages/Docking/src/utils/auto-dock-service.ts:127,160 | + +## Custom filters + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Implementing a custom dataframe-filter widget | Subclass `DG.Filter` (`js-api/src/widgets/filter.ts:15`); implement `applyFilter()` and `get filterSummary(): string` (both abstract). Constructor: `super()` with no args, then `this.root = ui.divV([...], '')` and `this.subs = []`. (DG-FACT-265) | `DG.Widget` directly — bypasses the FilterGroup wiring (`onRowsFiltering` subscription, indicator/controls slots, isFiltering wiring). | js-api/src/widgets/filter.ts:15-119, packages/Widgets/src/filters/radio-button-filter.ts:12-20 | +| Registering a filter function (header form) | `//meta.role: filter` AND `//output: filter result` on the package function. (DG-FACT-266) | Just `meta.role: filter` without the `filter` output type — the platform won't index it as a filter producer. | packages/Widgets/src/package.g.ts:4-10 | +| Registering a filter function (decorator form, method-decorator) | `@grok.decorators.func({ outputs: [{type: 'filter', name: 'result'}], meta: {role: 'filter'} })` on a static method that returns `new YourFilter()`. (DG-FACT-266) | Forgetting either the role meta or the filter-typed output — the build still emits a function entry, but it isn't routed into FilterGroup. | packages/Widgets/src/package.ts:15-23 | +| Registering a filter function (class-decorator form, datagrok-tools ^4.12) | `@grok.decorators.filter({ name?, description?, semType? })` on the `class extends DG.Filter`. Implicitly applies role+output. The generated `package.g.ts` IS committed to the repo. (DG-FACT-267) | Adding any other field to the class decorator — only those three are accepted (`js-api/src/decorators/functions.ts:54-60`). For `name`/`description` heavy-lifting, fall back to the method-decorator form. | js-api/src/decorators/functions.ts:47-60, help/develop/advanced/decorators.md:46-53 | +| Adding a registered filter to a TableView | `view.addViewer(DG.Viewer.filters({filters: [{type: ':', columnName: '
'}]}))`. (DG-FACT-268) | `view.filters({filters: [...]})` — JSDoc-marked `@deprecated` (`js-api/src/views/view.ts:489`). Still functional and used in tutorial samples, but expect removal. | js-api/src/views/view.ts:489-494, packages/ApiSamples/scripts/ui/viewers/filters/custom-filters.js:8 | +| Cross-package filter activation | `await grok.functions.call(':')` BEFORE `view.filters({filters: [{type: ':', ...}]})` — the filters call is synchronous and assumes the function is already registered. (DG-FACT-269) | Synchronous `view.filters({...})` against a not-yet-loaded package's filter — silently no-ops or throws "function not found". | packages/ApiSamples/scripts/ui/viewers/filters/custom-filters.js:5-10 | +| Writing the filter mask in `applyFilter()` | Iterate `this.dataFrame!.rowCount` and call `filter.set(i, false, false)` for rows that fail your predicate; finish with `this.dataFrame!.filter.fireChanged()`. Only flip bits to FALSE — TRUE bits get re-set by other filters in the chain. (DG-FACT-270) | Setting bits to TRUE — overwrites earlier filters' work; breaks collaborative filtering. Calling `applyFilter()` directly from a UI handler — bypasses the chain. | js-api/src/widgets/filter.ts:71-75, packages/Widgets/src/filters/radio-button-filter.ts:53-63 | +| Triggering re-filter on UI change | `this.dataFrame!.rows.requestFilter()` from the input change handler. The platform fires `onRowsFiltering`, which invokes `applyFilter()` on every filter in the group. (DG-FACT-271) | Calling `this.applyFilter()` directly — leaves the rest of the chain stale; results vary depending on what other filters are active. | packages/Widgets/src/filters/radio-button-filter.ts:73, packages/Widgets/src/filters/multi-value-filter.ts:88 | +| Hooking dataframe attach | Override `attach(dataFrame)` and call `super.attach(dataFrame)` first — base subscribes to `onRowsFiltering` and pushes to `this.subs`. After super, set `this.column` / `this.columnName` (default to a sensible column if unset), then `this.render()`. (DG-FACT-272) | Skipping `super.attach` — your filter never receives the filter-event callback. Building UI in the constructor before `attach` — `this.dataFrame` is null at that point. | js-api/src/widgets/filter.ts:91-103 | +| Persisting filter state across layout reload | Default `saveState()` returns `{column, columnName}`. Override to add UI selection. Override `applyState(state)` and call `super.applyState(state)` then `this.render()`. (DG-FACT-273) | Storing UI state in module-level globals — lost on layout reload; conflicts when the same filter type is used twice in one view. | js-api/src/widgets/filter.ts:78-89 | +| Declaring filter activity for short-circuit optimisation | Override `get isFiltering(): boolean`. Return `super.isFiltering && ` to honour user-disable; return `true` to ignore disable; return cheap-active for short-circuit. (DG-FACT-274) | Always returning `true` blindly — the platform can't skip your `applyFilter()` when nothing's selected, costing one full-frame iteration per redraw. | js-api/src/widgets/filter.ts:57-63 | +| Filter caption / header decorations | Override `get caption()` (default `columnName ?? ''`). Use `this.indicator` (left, hidden by default — set `display: ''` to expose) and `this.controls` (right, FilterGroup-managed visibility). (DG-FACT-275) | Building your own header DOM inside `this.root` — duplicates the FilterGroup-provided header and produces two captions. | js-api/src/widgets/filter.ts:18-43 | + +## Hit triage (lightweight, DB-backed) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Pulling the source frame from a packaged DB query | `await grok.data.query(':', {: })` → `Promise`. Namespace is the package's `friendlyName` (e.g., `ChEMBL:CompoundActivityDetailsForTarget`), NOT the directory name. Prefer the typed wrapper `.queries.(...)` from the generated `package-api.ts` when one exists. (DG-FACT-250 step 1) | `grok.functions.call(':_', …)` — the article shows this for ChEMBL; the underscore-prefixed name and fullName-cased namespace are obsolete codegen artifacts. (DG-FACT-DRIFT-097) | js-api/src/data.ts:255-262, packages/Chembl/src/package-api.ts:14-15 | +| Adding scratch annotation/decision columns to the working frame | `data.columns.addNewString('Comments')`, `addNewInt`, `addNewFloat`, `addNewBool`, `addNewDateTime`, `addNewVirtual` — append a typed column in one call. (DG-FACT-250 step 2) | Building a `DG.Column.fromList(...)` and stitching with `columns.add(col)` — works, but `addNewString`/etc. is the one-liner. | js-api/src/dataframe/column-list.ts:189 | +| Gating who can edit a triage column from the UI | `column.setTag('editableBy', 'login1, login2')` — comma-separated user OR group login names. Tag applies at column OR dataframe scope. Pair with `setTag('pinIfEditable', 'true')` to keep editor columns visible. (DG-FACT-248) | A `column.editable = false` property — there is no such flag; edit-gating is tag-driven and server-enforced. | js-api/src/api/ddt.api.g.ts:266-271, help/visualize/viewers/grid.md:590-591 | +| Engaging molecule/macromolecule cell renderers on a freshly-built frame | `await grok.data.detectSemanticTypes(data)` BEFORE `grok.shell.addTableView(data)`. Async. Frames from `loadTable`/`query` have already been detected; frames built with `DataFrame.create/fromColumns` have NOT. (DG-FACT-249) | Adding the view first — semantic-type cell renderers won't engage until the next reload. | js-api/src/data.ts:273-275 | +| Auto-applying a saved layout that fits the current frame | `let layouts = await grok.dapi.layouts.getApplicable(data); tv.loadLayout(layouts[0]);` — `getApplicable` matches by column name+type, `layout-id` tag, or `quality`/`semType` tag (DG-FACT-218). Set `column.tags['layout-id'] = ''` on both source and destination for cross-target portability. (DG-FACT-250 step 6) | Calling `list()` and filtering client-side by name — misses tag-based matches that don't share column names. Hard-coding a specific layout GUID — breaks when the layout is re-saved. | js-api/src/dapi.ts:657-659, js-api/src/views/view.ts:297 | + +## Info panels (context-panel widgets) + +| For… | Use | Don't use | Source | +|---|---|---|---| +| Registering a JS info-panel function (decorator form, RECOMMENDED) | `@grok.decorators.panel({name, description, condition?, meta: {role: ''}?})` over a `static` method returning `DG.Widget` or `Promise`. Codegen appends `panel` to `meta.role` automatically — don't include it manually. (DG-FACT-276, DG-FACT-281) | `@grok.decorators.func` with `meta.role: 'panel'` hand-written — works, but `decorators.panel` is the dedicated decorator and clearer to readers. | js-api/src/decorators/functions.ts:278-284, packages/Admetica/src/package.ts:30-40 | +| Registering a JS info-panel function (header form) | `//name: ` + `//input: ` + `//output: widget result` + `//meta.role: panel` (or `//meta.role: widgets,panel` for combined widget+panel) + `//condition: `. (DG-FACT-276, DG-FACT-277) | Omitting `//output: widget result` — without it, codegen doesn't index the function as a widget producer and the panel never renders. | packages/Bio/src/package.g.ts:52-58, packages/Admetica/src/package.g.ts:9-17 | +| Registering a server-side panel script | Script header `# name: ` + `# language: ` + `# meta.role: panel` + `# input: ` + `# output: ` + `# condition: `. Conditions are Grok-script REGARDLESS of script language. (DG-FACT-276, DG-FACT-278, DG-FACT-279) | A JS panel for server-only computation — round-trips data through the client unnecessarily. Use a script when the computation is server-resident. | help/develop/how-to/ui/add-info-panel.md:32-86 | +| Returning a widget from a JS panel function | `return new DG.Widget(rootElement)` or `return DG.Widget.fromRoot(rootElement)`. The `rootElement` is your `HTMLElement` (e.g., `ui.divText(...)`, `ui.divV([...])`). For richer widgets subclass `DG.Widget` and pass the root via `super(rootElement)` in the constructor. (DG-FACT-277) | A raw `HTMLElement` — `widget` output type wraps via `DG.Widget`; returning an element directly may render but skips the property/subscription wiring. | js-api/src/widgets/base.ts:231-273, packages/Admetica/src/package.ts:30-40 | +| Returning a server-rendered plot/image from a panel script | `# output: graphics pic` and assign the rendered binary in the script body (e.g., R/Python plot). (DG-FACT-278) | Embedding the image as a base64 string into a `widget` output — bloats payloads; `graphics` is the dedicated path. | help/develop/how-to/ui/add-info-panel.md:222-232, js-api/src/const.ts:99 | +| Returning an interactive viewer (pre-configured) from a panel script | `# output: viewer plot` and build the viewer inside the script body, e.g., `plot = table.ScatterPlot("height", "weight", "age", "sex"); plot.showRegressionLine = true`. (DG-FACT-278) | A static `graphics` snapshot when the user might want to brush/zoom — viewer outputs participate in the live grid linkage. | help/develop/how-to/ui/add-info-panel.md:191-203 | +| Returning a dataframe that joins into the source table | `# output: dataframe predictions {action: join()}` — Datagrok adds the result frame as virtual columns on the source. Match the row order to the source frame. (DG-FACT-278) | Returning a `dataframe` without `{action: join(...)}` — it renders as a separate object the user must open manually. | help/develop/how-to/ui/add-info-panel.md:260-274, help/datagrok/concepts/functions/func-params-annotation.md:782-839 | +| Adding action buttons to a panel (server-side scripts) | `# output: string actions {action: markup}` and assign Grok-script markup such as `actions = '#{button("Flag as suspicious", "http.Post(myserver, row.transactionId)")}'`. (DG-FACT-278) | Hand-rolling a JS widget with `