Mastering Interactive Features in AntV S2: Click Highlights, Hover, Brush, Resize and More
This article explains how AntV S2 implements a wide range of interactions—such as click and hover highlights, brush selection, dynamic row/column resizing, column hiding, link jumps and custom events—by using canvas rendering, event delegation, state management and JavaScript code examples.
Background
S2 is AntV's solution for multi‑dimensional cross‑analysis tables, primarily used for data analysis. S2 renders tables with canvas (based on the easy‑to‑use, efficient, powerful 2D visualization engine G) and includes many interaction capabilities such as row‑column linked highlight, single/multiple selection highlight, brush highlight, dynamic row/column size adjustment, column header hide, etc.
Difference Between DOM Interaction and Canvas Interaction
Using a cell click as an example, DOM elements can be targeted precisely with CSS3 selectors, while a canvas provides only a single <canvas/> element. To determine which cell was clicked, event delegation combined with mouse coordinates is required.
const cell = document.querySelector('.cell > li:first-child');
cell.addEventListener('click', () => {
console.log('First cell: do not click me!');
});In canvas, the click listener is attached to the canvas element itself:
const canvas = document.querySelector('canvas');
canvas.addEventListener('click', () => {
console.log('Which cell did I click?');
});Event delegation is used to obtain the exact target cell via event.target and, in canvas, by mapping the click position to the virtual cell data structure.
Event Classification
Through event delegation, specific cell events can be captured (implementation located in event‑controller.ts ):
Corner cell click: S2Event.CORNER_CELL_CLICK Column header cell click: S2Event.COL_CELL_CLICK Row header cell click: S2Event.ROW_CELL_CLICK Data cell click: S2Event.DATA_CELL_CLICK Cell double‑click
Cell right‑click
Interaction Classification
Based on the cell event type, interactions can be combined. For example, brush highlight uses the mousedown + mousemove + mouseup sequence on data cells, stores the cell meta in a state machine, and triggers canvas re‑drawing according to the interaction state.
Interaction Type
Name
Applicable Scenario
All Selected
ALL_SELECTED
Copy
Selected
SELECTED
Single/multiple/row‑column batch selection
Unselected
UNSELECTED
Click blank area, ESC reset, even‑click cell
Hover
HOVER
Row‑column linked highlight
Hover Focus
HOVER_FOCUS
Show tooltip
Prepare Select
PREPARE_SELECT
Brush selection
Single‑Cell Highlight
When a data cell is clicked, the cell is not directly highlighted; instead, all non‑selected data cells are dimmed, creating a “spotlight” effect. The cell meta is obtained via cell.getMeta() and the interaction state is changed to InteractionStateName.SELECTED.
this.spreadsheet.on(S2Event.DATA_CELL_CLICK, (event) => {
const cell = this.spreadsheet.getCell(event.target);
const meta = cell.getMeta();
interaction.changeState({
cells: [getCellMeta(cell)],
stateName: InteractionStateName.SELECTED,
});
});The final state object looks like:
const cell = {
id: 'cell-id',
colIndex: 0,
rowIndex: 0,
type: 'cell-type',
};
const state = {
name: InteractionStateName.SELECTED,
cells: [cell],
};All visible data cells are then updated, each cell calling its update() method to adjust fillOpacity based on the current state.
function update() {
const stateName = this.spreadsheet.interaction.getCurrentStateName();
const fillOpacity = stateName === InteractionStateName.SELECTED ? 1 : 0.2;
this.attrs = { fillOpacity };
canvas.draw();
}Hover Highlight (Row‑Column Linked Highlight)
Hovering a data cell highlights the corresponding row and column headers, creating a cross‑highlight effect. The interaction state is set to InteractionStateName.HOVER and a black border is drawn around the hovered cell.
this.spreadsheet.on(S2Event.DATA_CELL_HOVER, (event) => {
const cell = this.spreadsheet.getCell(event.target);
const meta = cell?.getMeta();
interaction.changeState({
cells: [getCellMeta(cell)],
stateName: InteractionStateName.HOVER,
});
this.updateRowColCells(meta);
});Row and column headers are hierarchical; their IDs are compared with the data cell ID to decide whether to highlight.
const allRowHeaderCells = getActiveHoverRowColCells(
rowId,
interaction.getAllRowHeaderCells(),
this.spreadsheet.isHierarchyTreeType(),
);
forEach(allRowHeaderCells, (cell) => {
cell.updateByState(InteractionStateName.HOVER);
});Brush Highlight (Selection)
Brush selection is a drag action that selects all cells within the rectangular area defined by the start and end points. The start point records x/y coordinates and row/column indices; the end point is captured on mouse up.
private getBrushPoint(event) {
const { scrollY, scrollX } = this.spreadsheet.facet.getScrollOffset();
const originalEvent = event.originalEvent;
const point = { x: originalEvent?.layerX, y: originalEvent?.layerY };
const cell = this.spreadsheet.getCell(event.target);
const { colIndex, rowIndex } = cell.getMeta();
return { ...point, colIndex, rowIndex, scrollY, scrollX };
}
private isInBrushRange(meta) {
const { start, end } = this.getBrushRange();
return (
meta.rowIndex >= start.rowIndex && meta.rowIndex <= end.rowIndex &&
meta.colIndex >= start.colIndex && meta.colIndex <= end.colIndex
);
}
this.spreadsheet.on(S2Event.GLOBAL_MOUSE_UP, (event) => {
const range = this.getBrushRange();
this.spreadsheet.interaction.changeState({
cells: this.getSelectedCellMetas(range),
stateName: InteractionStateName.SELECTED,
});
});Dynamic Row/Column Height & Width Adjustment
S2 provides three layout modes (column equal width, row‑column equal width, compact) and also allows dragging row/column headers to resize. A hot‑zone appears when the mouse is near a cell edge; the cursor changes to col-resize or row-resize and a guide line is drawn.
const attrs = { path: '', lineDash: guideLineDash, stroke: guideLineColor, strokeWidth: size };
this.resizeReferenceGroup.addShape('path', { id: RESIZE_START_GUIDE_LINE_ID, attrs });
this.resizeReferenceGroup.addShape('path', { id: RESIZE_END_GUIDE_LINE_ID, attrs });
if (type === ResizeDirectionType.Horizontal) {
startResizeGuideLineShape.attr('path', [['M', offsetX, offsetY], ['L', offsetX, guideLineMaxHeight]]);
endResizeGuideLineShape.attr('path', [['M', offsetX + width, offsetY], ['L', offsetX + width, guideLineMaxHeight]]);
} else {
startResizeGuideLineShape.attr('path', [['M', offsetX, offsetY], ['L', guideLineMaxWidth, offsetY]]);
endResizeGuideLineShape.attr('path', [['M', offsetX, offsetY + height], ['L', guideLineMaxWidth, offsetY + height]]);
}
private getResizeWidthDetail() {
const { start, end } = this.getResizeGuideLinePosition();
const width = Math.floor(end.x - start.x);
switch (this.getResizeInfo().effect) {
case ResizeAreaEffect.Cell:
return {
eventType: S2Event.LAYOUT_RESIZE_COL_WIDTH,
style: { colCfg: { widthByFieldValue: { [this.getResizeInfo().id]: width } } },
};
default:
return null;
}
}Link Jump
Cells can display underlined text to indicate a clickable link. In DOM mode, a normal a tag is used; in canvas mode, an underline shape is drawn manually and click events are listened to.
// Get the bounding box of the text shape
const { minX, maxX, maxY } = this.textShape.getBBox();
// Draw an underline under the text
this.linkFieldShape = renderLine(
this,
{ x1: minX, y1: maxY + 1, x2: maxX, y2: maxY + 1 },
{ stroke: linkFillColor, lineWidth: 1 },
);Column Header Hide
Both pivot and detail tables support hiding column headers. After clicking a column header, a tooltip appears with a “Hide” button. Hidden columns can be grouped; the UI shows expand buttons for each hidden group.
export const getHiddenColumnsThunkGroup = (columns, hiddenColumnFields) => {
if (isEmpty(hiddenColumnFields)) return [];
let prevHiddenIndex = Number.NEGATIVE_INFINITY;
return columns.reduce((result, field, index) => {
if (!hiddenColumnFields.includes(field)) return result;
if (index === prevHiddenIndex + 1) {
const lastGroup = last(result);
lastGroup.push(field);
} else {
result.push([field]);
}
prevHiddenIndex = index;
return result;
}, []);
};When a hidden column is expanded, the configuration is updated by diffing the current hidden fields with the fields that should become visible.
private handleExpandIconClick(node) {
const lastHiddenColumnsDetail = this.spreadsheet.store.get('hiddenColumnsDetail', []);
const { hideColumnNodes = [] } =
lastHiddenColumnsDetail.find(({ displaySiblingNode }) =>
isEqualDisplaySiblingNodeId(displaySiblingNode, node.id),
) || {};
const { hiddenColumnFields: lastHideColumnFields } = this.spreadsheet.options.interaction;
const willDisplayColumnFields = hideColumnNodes.map(this.getHideColumnField);
const hiddenColumnFields = difference(lastHideColumnFields, willDisplayColumnFields);
const hiddenColumnsDetail = lastHiddenColumnsDetail.filter(
({ displaySiblingNode }) => !isEqualDisplaySiblingNodeId(displaySiblingNode, node.id),
);
this.spreadsheet.setOptions({ interaction: { hiddenColumnFields } });
this.spreadsheet.store.set('hiddenColumnsDetail', hiddenColumnsDetail);
}Custom Interaction
Developers can register custom interactions using the S2Event list and the interaction.customInteractions option. The example below shows a custom interaction that displays a tooltip when hovering row or column headers.
import { PivotSheet, BaseEvent, S2Event } from '@antv/s2';
class RowColumnHoverTooltipInteraction extends BaseEvent {
bindEvents() {
this.spreadsheet.on(S2Event.ROW_CELL_HOVER, (event) => this.showTooltip(event));
this.spreadsheet.on(S2Event.COL_CELL_HOVER, (event) => this.showTooltip(event));
}
showTooltip(event) {
const cell = this.spreadsheet.getCell(event.target);
const meta = cell.getMeta();
this.spreadsheet.tooltip.show({
position: { x: event.clientX, y: event.clientY },
content: meta.value,
});
}
}
const s2Options = {
interaction: {
customInteractions: [{ key: 'RowColumnHoverTooltipInteraction', interaction: RowColumnHoverTooltipInteraction }],
},
};Conclusion
The article covered several core interactions of AntV S2, including click highlight, hover highlight, brush selection, dynamic resizing, column hiding, link jumps, and custom interactions. S2 also supports merged cells, custom scroll speed, and many other advanced features.
For more information, visit the official repository and documentation:
GitHub: https://github.com/antvis/s2
Website: https://s2.antv.vision/
Core package: @antv/s2 (https://www.npmjs.com/package/@antv/s2)
React wrapper: @antv/s2-react (https://www.npmjs.com/package/@antv/s2-react)
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
