@antv/g2
Version:
the Grammar of Graphics in Javascript
361 lines (308 loc) • 9.48 kB
text/typescript
import { deepMix, get, isObject, size, clamp, isNil, noop, throttle, isEmpty, valuesOfKey } from '@antv/util';
import { COMPONENT_TYPE, DIRECTION, LAYER, VIEW_LIFE_CIRCLE } from '../../constant';
import { IGroup, Slider as SliderComponent } from '../../dependents';
import { ComponentOption, Datum, Padding } from '../../interface';
import { BBox } from '../../util/bbox';
import { directionToPosition } from '../../util/direction';
import { isBetween } from '../../util/helper';
import { Writeable } from '../../util/types';
import View from '../view';
import { Controller } from './base';
import { SliderOption, SliderCfg } from '../../interface';
/**
* @ignore
* slider Controller
*/
export default class Slider extends Controller<SliderOption> {
private slider: ComponentOption;
private container: IGroup;
private width: number;
private start: number;
private end: number;
private onChangeFn: (evt: {}) => void = noop;
constructor(view: View) {
super(view);
this.container = this.view.getLayer(LAYER.FORE).addGroup();
this.onChangeFn = throttle(this.onValueChange, 20, {
leading: true,
}) as (evt: {}) => void;
this.width = 0;
this.view.on(VIEW_LIFE_CIRCLE.BEFORE_CHANGE_DATA, this.resetMeasure);
this.view.on(VIEW_LIFE_CIRCLE.BEFORE_CHANGE_SIZE, this.resetMeasure);
}
get name(): string {
return 'slider';
}
public destroy() {
super.destroy();
this.view.off(VIEW_LIFE_CIRCLE.BEFORE_CHANGE_DATA, this.resetMeasure);
this.view.off(VIEW_LIFE_CIRCLE.BEFORE_CHANGE_SIZE, this.resetMeasure);
}
/**
* 初始化
*/
public init() {}
/**
* 渲染
*/
public render() {
this.option = this.view.getOptions().slider;
const { start, end } = this.getSliderCfg();
if (isNil(this.start)) {
this.start = start;
this.end = end;
}
const { data: viewData } = this.view.getOptions();
if (this.option && !isEmpty(viewData)) {
if (this.slider) {
// exist, update
this.slider = this.updateSlider();
} else {
// not exist, create
this.slider = this.createSlider();
// 监听事件,绑定交互
this.slider.component.on('sliderchange', this.onChangeFn);
}
} else {
if (this.slider) {
// exist, destroy
this.slider.component.destroy();
this.slider = undefined;
} else {
// do nothing
}
}
}
/**
* 布局
*/
public layout() {
if (this.option && !this.width) {
this.measureSlider();
setTimeout(() => {
// 初始状态下的 view 数据过滤
if (!this.view.destroyed) {
this.changeViewData(this.start, this.end);
}
}, 0);
}
if (this.slider) {
const width = this.view.coordinateBBox.width;
// 获取组件的 layout bbox
const padding: Padding = this.slider.component.get('padding') as Padding;
const [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding;
const bboxObject = this.slider.component.getLayoutBBox();
const bbox = new BBox(bboxObject.x, bboxObject.y, Math.min(bboxObject.width, width), bboxObject.height).expand(
padding
);
const { minText, maxText } = this.getMinMaxText(this.start, this.end);
const [x1, y1] = directionToPosition(this.view.viewBBox, bbox, DIRECTION.BOTTOM);
const [x2, y2] = directionToPosition(this.view.coordinateBBox, bbox, DIRECTION.BOTTOM);
// 默认放在 bottom
this.slider.component.update({
...this.getSliderCfg(),
x: x2 + paddingLeft,
y: y1 + paddingTop,
width: this.width,
start: this.start,
end: this.end,
minText,
maxText,
});
this.view.viewBBox = this.view.viewBBox.cut(bbox, DIRECTION.BOTTOM);
}
}
/**
* 更新
*/
public update() {
// 逻辑和 render 保持一致
this.render();
}
/**
* 创建 slider 组件
*/
private createSlider(): ComponentOption {
const cfg: any = this.getSliderCfg();
// 添加 slider 组件
const component = new SliderComponent({
container: this.container,
...cfg,
});
component.init();
return {
component,
layer: LAYER.FORE,
direction: DIRECTION.BOTTOM,
type: COMPONENT_TYPE.SLIDER,
};
}
/**
* 更新配置
*/
private updateSlider() {
let cfg = this.getSliderCfg();
if (this.width) {
const { minText, maxText } = this.getMinMaxText(this.start, this.end);
cfg = { ...cfg, width: this.width, start: this.start, end: this.end, minText, maxText };
}
this.slider.component.update(cfg);
return this.slider;
}
/**
* 进行测量操作
*/
private measureSlider() {
const { width } = this.getSliderCfg();
this.width = width;
}
/**
* 清除测量
*/
private resetMeasure = () => {
this.clear();
};
/**
* 生成 slider 配置
*/
private getSliderCfg() {
let cfg: Writeable<SliderCfg> & { x: number; y: number; width: number; minText: string; maxText: string } = {
height: 16,
start: 0,
end: 1,
minText: '',
maxText: '',
x: 0,
y: 0,
width: this.view.coordinateBBox.width,
};
if (isObject(this.option)) {
// 用户配置的数据,优先级更高
const trendCfg = {
data: this.getData(),
...get(this.option, 'trendCfg', {}),
};
// 因为有样式,所以深层覆盖
cfg = deepMix({}, cfg, this.getThemeOptions(), this.option);
// trendCfg 因为有数据数组,所以使用浅替换
cfg = { ...cfg, trendCfg };
}
cfg.start = clamp(Math.min(isNil(cfg.start) ? 0 : cfg.start, isNil(cfg.end) ? 1 : cfg.end), 0, 1);
cfg.end = clamp(Math.max(isNil(cfg.start) ? 0 : cfg.start, isNil(cfg.end) ? 1 : cfg.end), 0, 1);
return cfg;
}
/**
* 从 view 中获取数据,缩略轴使用全量的数据
*/
private getData(): number[] {
const data = this.view.getOptions().data;
const [yScale] = this.view.getYScales();
const groupScales = this.view.getGroupScales();
if (groupScales.length) {
const { field, ticks } = groupScales[0];
return data.reduce((pre, cur) => {
if (cur[field] === ticks[0]) {
pre.push(cur[yScale.field] as number);
}
return pre;
}, []) as number[];
}
return data.map((datum) => datum[yScale.field] || 0);
}
/**
* 获取 slider 的主题配置
*/
private getThemeOptions() {
const theme = this.view.getTheme();
return get(theme, ['components', 'slider', 'common'], {});
}
/**
* 滑块滑动的时候出发
* @param v
*/
private onValueChange = (v: any) => {
const [min, max] = v;
this.start = min;
this.end = max;
this.changeViewData(min, max);
};
/**
* 根据 start/end 和当前数据计算出当前的 minText/maxText
* @param min
* @param max
*/
private getMinMaxText(min: number, max: number) {
const data = this.view.getOptions().data;
const xScale = this.view.getXScale();
const isHorizontal = true;
let values = valuesOfKey(data, xScale.field);
// 如果是 xScale 数值类型,则进行排序
if (xScale.isLinear) {
values = values.sort();
}
const xValues = isHorizontal ? values : values.reverse();
const dataSize = size(data);
if (!xScale || !dataSize) {
return {}; // fix: 需要兼容,否则调用方直接取值会报错
}
const xTickCount = size(xValues);
const minIndex = Math.floor(min * (xTickCount - 1));
const maxIndex = Math.floor(max * (xTickCount - 1));
let minText = get(xValues, [minIndex]);
let maxText = get(xValues, [maxIndex]);
const formatter = this.getSliderCfg().formatter as SliderCfg['formatter'];
if (formatter) {
minText = formatter(minText, data[minIndex], minIndex);
maxText = formatter(maxText, data[maxIndex], maxIndex);
}
return {
minText,
maxText,
};
}
/**
* 更新 view 过滤数据
* @param min
* @param max
*/
private changeViewData(min: number, max: number) {
const data = this.view.getOptions().data;
const xScale = this.view.getXScale();
const dataSize = size(data);
if (!xScale || !dataSize) {
return;
}
const isHorizontal = true;
const values = valuesOfKey(data, xScale.field);
// 如果是 xScale 数值类型,则进行排序
const xScaleValues = this.view.getXScale().isLinear ? values.sort((a, b) => Number(a) - Number(b)) : values;
const xValues = isHorizontal ? xScaleValues : xScaleValues.reverse();
const xTickCount = size(xValues);
const minIndex = Math.floor(min * (xTickCount - 1));
const maxIndex = Math.floor(max * (xTickCount - 1));
// 增加 x 轴的过滤器
this.view.filter(xScale.field, (value: any, datum: Datum) => {
const idx: number = xValues.indexOf(value);
return idx > -1 ? isBetween(idx, minIndex, maxIndex) : true;
});
this.view.render(true);
}
/**
* 覆写父类方法
*/
public getComponents() {
return this.slider ? [this.slider] : [];
}
/**
* 覆盖父类
*/
public clear() {
if (this.slider) {
this.slider.component.destroy();
this.slider = undefined;
}
this.width = 0;
this.start = undefined;
this.end = undefined;
}
}