Skip to content

Commit

Permalink
feat: mouse wheel for scrollbar (#3606)
Browse files Browse the repository at this point in the history
* fix(scrollbar): fix failed tests

* feat(scrollbar): implement mouse wheel scroll behaviour

* test(scrollbar): add mouse wheel tests

* docs(scrollbar): describe mouse wheel scrolling options

* docs(scrollbar): update demo
  • Loading branch information
Shamann committed Oct 11, 2021
1 parent 5990d3a commit 6731383
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 5 deletions.
24 changes: 24 additions & 0 deletions docs/api/general/interaction.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,30 @@ Chart 和 View 上的 Action 用户控制视图的变化,目前支持的有:
- zoomOut() 放大
- reset() 恢复

#### mousewheel-scroll

- scroll() 鼠标滚轮

```javascript
chart.interaction("plot-mousewheel-scroll");

chart.option('scrollbar', {
type: 'horizontal',
});
```

在鼠标滚动事件(向下滚动或向上滚动)上滚动的图表数据项的数量可以通过如下设置 `wheelDelta` 参数来自定义:

```javascript
chart.interaction("plot-mousewheel-scroll", {
start: [{ trigger: 'plot:mousewheel', action: 'mousewheel-scroll:scroll', arg: { wheelDelta: 5 } }],
});

chart.option('scrollbar', {
type: 'horizontal',
});
```

### Element 的 Action

图表元素 Element 的 Action 大都与状态相关,支持的 Action 有:
Expand Down
7 changes: 7 additions & 0 deletions examples/column/basic/demo/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
},
"screenshot": "https://gw.alipayobjects.com/zos/antfincdn/e%24NnUtHbjm/a9632163-6aa8-4d53-8a2a-c1878426a51b.png"
},
{
"filename": "scrollbar.ts",
"title": {
"zh": "带滚动条的直方图",
"en": "Column chart with a scrollbar"
}
},
{
"filename": "corner-radius.ts",
"title": {
Expand Down
10 changes: 10 additions & 0 deletions examples/column/basic/demo/scrollbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/top2000.json')
groupBy: ['release'],
operations: ['count'],
type: 'aggregate',
})
.transform({
type: 'sort-by',
fields: ['release'],
order: 'ASC',
});

const chart = new Chart({
Expand All @@ -37,5 +42,10 @@ fetch('https://gw.alipayobjects.com/os/antvdemo/assets/data/top2000.json')
type: 'horizontal',
});
chart.interaction('element-visible-filter');
chart.interaction('plot-mousewheel-scroll', {
start: [
{ trigger: 'plot:mousewheel', action: 'mousewheel-scroll:scroll', arg: { wheelDelta: 5 } },
],
});
chart.render();
});
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ import ViewDrag from './interaction/action/view/drag';
import ViewMove from './interaction/action/view/move';
import ScaleTranslate from './interaction/action/view/scale-translate';
import ScaleZoom from './interaction/action/view/scale-zoom';
import MousewheelScroll from './interaction/action/view/mousewheel-scroll';

registerAction('tooltip', TooltipAction);
registerAction('sibling-tooltip', SiblingTooltip);
Expand Down Expand Up @@ -287,6 +288,8 @@ registerAction('reset-button', ButtonAction, {
text: 'reset',
});

registerAction('mousewheel-scroll', MousewheelScroll);

// 注册默认的 Interaction 交互行为
import { registerInteraction } from './core';

Expand Down Expand Up @@ -617,6 +620,10 @@ registerInteraction('sibling-tooltip', {
end: [{ trigger: 'plot:mouseleave', action: 'sibling-tooltip:hide' }],
});

registerInteraction('plot-mousewheel-scroll', {
start: [{ trigger: 'plot:mousewheel', action: 'mousewheel-scroll:scroll' }],
});

// 让 TS 支持 View 原型上添加的创建 Geometry 方法的智能提示
/**
* 往 View 原型上添加的创建 Geometry 的方法
Expand Down
39 changes: 39 additions & 0 deletions src/interaction/action/view/mousewheel-scroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { clamp, size, valuesOfKey } from "@antv/util";
import { COMPONENT_TYPE } from "../../../constant";
import { Action } from '..';
import { LooseObject } from "../../../interface";

function isWheelDown(event: LooseObject) {
const wheelEvent = event.gEvent.originalEvent as WheelEvent;
return wheelEvent.deltaY > 0;
}

const DEFAULT_WHEELDELTA = 1;
class MousewheelScroll extends Action {

public scroll(arg?) {
const { view, event } = this.context;

if (!view.getOptions().scrollbar) {
return;
}

const wheelDelta = arg?.wheelDelta || DEFAULT_WHEELDELTA;
const scrollbarController = view.getController('scrollbar');

const xScale = view.getXScale();
const data = view.getOptions().data;
const dataSize = size(valuesOfKey(data, xScale.field));
const step = size(xScale.values);

const currentRatio = scrollbarController.getValue();
const currentStart = Math.floor((dataSize - step) * currentRatio);

const nextStart = currentStart + (isWheelDown(event) ? wheelDelta : -wheelDelta);
const correction = wheelDelta / (dataSize - step) / 10000;
const nextRatio = clamp(nextStart / (dataSize - step) + correction, 0, 1);
scrollbarController.setValue(nextRatio);
}
}

export default MousewheelScroll;
113 changes: 108 additions & 5 deletions tests/unit/chart/controller/scrollbar-spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Chart } from '../../../../src';
import { createDiv, removeDom } from '../../../util/dom';
import { salesBySubCategory, subSalesBySubCategory, subSalesByArea } from '../../../data/sales';
import { salesBySubCategory, subSalesBySubCategory } from '../../../data/sales';
import { COMPONENT_TYPE } from '../../../../src/constant';
import { Scrollbar as ScrollbarComponent } from '../../../../src/dependents';
import { BBox } from '../../../../src/util/bbox';
import { delay } from '../../../util/delay';
import { near } from '../../../util/math';
import MousewheelScroll from '../../../../src/interaction/action/view/mousewheel-scroll';
import { getClientPoint } from '../../../util/simulate';

describe('Scrollbar', () => {
const container = createDiv();
Expand Down Expand Up @@ -338,8 +340,8 @@ describe('Scrollbar', () => {
expect(scrollbarBBox.width).toBe(coordinateBBox.width);
expect(near(xAxisBBox.maxY, 392 - 16)).toBe(true);
expect(scrollbar.component.get('trackLen')).toBe(coordinateBBox.width);
// @ts-ignore
expect(chart.filteredData.length).toBe(9);
// 32 - default category size
expect(chart.getData().length).toBe(Math.floor(coordinateBBox.width/32));

chart.destroy();
});
Expand Down Expand Up @@ -380,8 +382,8 @@ describe('Scrollbar', () => {
// initial state
expect(scrollbarBBox.height).toBe(8);
expect(scrollbar.component.get('trackLen')).toBe(coordinateBBox.width);
// @ts-ignore
expect(chart.filteredData.length).toBe(9);
// 32 - default category size
expect(chart.getData().length).toBe(Math.floor(coordinateBBox.width/32));

chart.destroy();
});
Expand All @@ -391,6 +393,107 @@ describe('Scrollbar', () => {
});
});

describe('scrollbar mouse wheel scrolling', () => {
const container = createDiv();
const chart = new Chart({
container,
height: 400,
width: 360,
});
chart.animate(false);
chart.data(salesBySubCategory);
chart.axis('subCategory', {
label: {
autoHide: true,
autoRotate: false,
},
});
chart.option('scrollbar', {
type: 'horizontal',
});
chart.scale('sales', {
nice: true,
formatter: (v) => `${Math.floor(v / 10000)}万`,
});
chart.interval().position('subCategory*sales').label('sales');
chart.render();

const spy = jest.spyOn(MousewheelScroll.prototype, 'scroll');
const canvas = chart.canvas;
const el = canvas.get('el');

const createMouseWheelEvent = (options) => new WheelEvent('mousewheel', {
bubbles: true,
cancelable: true,
...options
});

type TestTuple = { expectedWheelDelta: number, options?: any };

const cases: Record<string, TestTuple> = {
['detault']: {
expectedWheelDelta: 1,
},
['advanced /w wheel delta specified']: {
expectedWheelDelta: 5,
options: {
arg: { wheelDelta: 5 },
}
},
['advanced w/o wheel delta specified']: {
expectedWheelDelta: 1,
options: {
}
}
};

test.each(Object.entries(cases))('options: %s', async (_, {expectedWheelDelta, options}) => {

const interactionCfg = options !== undefined && {
start: [{ trigger: 'plot:mousewheel', action: 'mousewheel-scroll:scroll', ...(options.arg && { arg: options.arg }) }],
};

chart.interaction('plot-mousewheel-scroll', interactionCfg);
chart.render();

const scrollForwardEvt = createMouseWheelEvent({
deltaY: 100,
...getClientPoint(canvas, 180, 200)
});
const scrollBackEvt = createMouseWheelEvent({
deltaY: -100,
...getClientPoint(canvas, 180, 200)
});

await delay(50);
el.dispatchEvent(scrollBackEvt);
expect(spy).toHaveBeenCalled();
expect(chart.getData()[0]).toEqual(salesBySubCategory[0]);

const value = (salesBySubCategory.length - chart.getData().length) / expectedWheelDelta;
const numberOfScrolls = Number.isInteger(value) ? value : Math.ceil(value);
for (let i = 1; i < numberOfScrolls; i++) {
el.dispatchEvent(scrollForwardEvt);
await delay(50);
expect(spy).toHaveBeenCalled();
const chartData = chart.getData();
const from = i * expectedWheelDelta;
const to = (from + chartData.length > salesBySubCategory.length ? salesBySubCategory.length : from + chartData.length) - 1;
expect(chartData[0]).toEqual(salesBySubCategory[from]);
expect(chartData[chartData.length - 1]).toEqual(salesBySubCategory[to]);
}

el.dispatchEvent(scrollForwardEvt);
expect(spy).toHaveBeenCalled();
expect(chart.getData()[chart.getData().length - 1]).toEqual(salesBySubCategory[salesBySubCategory.length-1]);
});

afterAll(() => {
chart.destroy();
removeDom(container);
});
});

describe('scrollbar theme', () => {
const container = createDiv();

Expand Down

0 comments on commit 6731383

Please sign in to comment.