Create a Custom Viewer
SkillMediaCreate a custom JavaScript viewer extending DG.JsViewer with properties and rendering
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Create a Custom Viewer skill
What this skill tells your AI
The instructions your AI receives, as published by datagrok-ai/public in .claude/skills/create-custom-viewer/SKILL.md and read by ahel’s review.
Help the user develop a custom interactive viewer for Datagrok by extending DG.JsViewer.
Usage
/create-custom-viewer [viewer-name] [--library <d3|echarts|plotly>]
Instructions
1. Scaffold the viewer
From the package directory:
grok add viewer <ViewerName>
This creates a viewer class file. The naming convention is to add a Viewer postfix to the class name (e.g., AwesomeViewer).
2. Define the viewer class
Create a subclass of DG.JsViewer in a separate file (e.g., src/awesome-viewer.ts):
import * as DG from 'datagrok-api/dg';
import * as ui from 'datagrok-api/ui';
export class AwesomeViewer extends DG.JsViewer {
constructor() {
super();
// Register properties (appear in the context panel)
this.splitColumnName = this.string('splitColumnName', 'site');
this.valueColumnName = this.int('valueColumnName', 'age');
this.valueAggrType = this.string('valueAggrType', 'avg', { choices: ['avg', 'count', 'sum'] });
this.color = this.string('color', 'steelblue', { choices: ['darkcyan', 'seagreen', 'steelblue'] });
this.initialized = false;
}
onTableAttached() {
this.init();
this.subs.push(DG.debounce(this.dataFrame.selection.onChanged, 50).subscribe((_) => this.render()));
this.subs.push(DG.debounce(this.dataFrame.filter.onChanged, 50).subscribe((_) => this.render()));
this.subs.push(DG.debounce(ui.onSizeChanged(this.root), 50).subscribe((_) => this.render(false)));
this.render();
}
detach() {
this.subs.forEach(sub => sub.unsubscribe());
}
onPropertyChanged(property) {
super.onPropertyChanged(property);
if (this.initialized)
this.render();
}
render(computeData = true) {
// Rendering logic here
}
}
3. Register the viewer
In src/package.ts, add the annotated function:
import {AwesomeViewer} from './awesome-viewer';
//name: AwesomeViewer
//description: Creates an awesome viewer
//tags: viewer
//meta.icon: images/icon.svg
//meta.toolbox: true
//meta.trellisable: true
//output: viewer result
export function awesome() {
return new AwesomeViewer();
}
Or use the decorator approach (requires datagrok-tools >= 4.12.x):
@grok.decorators.viewer({
icon: 'images/icon.png',
toolbox: true,
})
export class AwesomeViewer extends DG.JsViewer { /* ... */ }
4. Property types and naming conventions
Available property types in the constructor:
this.int(name, defaultValue, options)-- integerthis.float(name, defaultValue, options)-- floating pointthis.string(name, defaultValue, options)-- stringthis.stringList(name, defaultValue, options)-- string arraythis.bool(name, defaultValue, options)-- booleanthis.dateTime(name, defaultValue, options)-- datetime
Property grouping in the UI is determined by naming:
Datatab: properties ending withColumnNameColorstab: properties ending withcolorAxestab: properties containingaxisLegendtab: properties starting withlegendMarginstab: properties containingmarginMisctab: everything else
A viewer registered with //meta.trellisable: true can also name the properties that belong on the
trellis plot's control panel — the strip beside the inner-viewer selector, where a scatter plot
offers X, Y, Color and Size:
get trellisProperties(): string[] { return ['mergeColumnSeries', 'logX', 'logY']; }
The first four are shown, in the order given, and unknown names are skipped. A viewer that declares none gets no strip; its properties are still editable in the context panel, under a tab named after the viewer.
5. Data preparation with filter support
Always respect the dataframe filter when preparing data:
render(computeData = true) {
if (computeData) {
this.data.length = 0;
this.aggregatedTable = this.dataFrame
.groupBy([this.splitColumnName])
.whereRowMask(this.dataFrame.filter)
.add(this.valueAggrType, this.valueColumnName, 'result')
.aggregate();
// Process aggregated data...
}
// Render using this.root as the container
}
6. Events and interactivity
Add tooltips and selection handling to visual elements:
// Row group tooltips on hover
element.on('mouseover', (event, d) => ui.tooltip.showRowGroup(this.dataFrame, i => {
return d.category === this.dataFrame.getCol(this.splitColumnName).get(i);
}, event.x, event.y));
element.on('mouseout', () => ui.tooltip.hide());
// Selection on click
element.on('mousedown', (event, d) => {
this.dataFrame.selection.handleClick(i => {
return d.category === this.dataFrame.getCol(this.splitColumnName).get(i);
}, event);
});
7. External dependencies
Add libraries (e.g., D3, ECharts) to package.json dependencies. Do NOT add platform-provided externals (datagrok-api, rxjs, cash-dom, dayjs, wu, openchemlib/full) to your bundle.
8. Build and test
npm run build
grok publish dev
Test with: grok.shell.addTableView(grok.data.demo.demog()).addViewer('AwesomeViewer');
Behavior
- Ask for the viewer name and what it should visualize if not specified.
- Always include filter and selection event subscriptions for proper interactivity.
- Add subscriptions to
this.subsso they are cleaned up when the viewer is detached. - Use
DG.debounceon frequently firing events (selection, filter, resize) for performance. - Separate data computation from rendering to avoid recomputing on resize.
- Follow Datagrok coding conventions: no excessive comments, no curly brackets for one-line if/for, catch/else-if on new line.
- Suggest the decorator approach for registration when using datagrok-tools >= 4.12.x.
Signals
- GitHub stars
- 72
- Forks
- 32
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
create-custom-viewer- Source
- github.com/datagrok-ai/public