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.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Mastering Interactive Features in AntV S2: Click Highlights, Hover, Brush, Resize and More

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)

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

FrontendJavaScriptAntV S2Canvas Interaction
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.