light-chart
Version:
Charts for mobile visualization.
251 lines (222 loc) • 8.05 kB
JavaScript
const Util = require('../util/common');
const Helper = require('./helper');
const Interaction = require('./base');
const Chart = require('../chart/chart');
const FilterPlugin = require('../plugin/filter');
const PressTooltipMixin = require('./mixin/press-tooltip');
const updateScaleMixin = require('./mixin/update-scale');
class Pinch extends Interaction {
getDefaultCfg() {
const defaultCfg = super.getDefaultCfg();
return Util.mix({}, defaultCfg, {
startEvent: 'pinchstart',
processEvent: 'pinch',
endEvent: 'pinchend',
resetEvent: 'touchend',
pressThreshold: 9, // Minimal movement that is allowed while pressing
pressTime: 251, // Minimal press time in ms
mode: 'x',
currentPinchScaling: null,
originValues: null,
minScale: null,
maxScale: null,
limitRange: {},
sensitivity: 1,
_pinchCumulativeDelta: 0,
_timestamp: 0
});
}
constructor(cfg, chart) {
super(cfg, chart);
const self = this;
const { hammer } = self;
hammer.get('pinch').set({ // open pinch recognizer
enable: true
});
chart.registerPlugins([ FilterPlugin, {
changeData() {
self.limitRange = {};
self.originTicks = null;
},
clear() {
self.limitRange = {};
self.originTicks = null;
}
}]);
Util.mix(self, PressTooltipMixin, updateScaleMixin);
self._bindPress();
}
start() {
if (this.pressed) return;
this.currentPinchScaling = 1;
}
process(e) {
if (this.pressed) return;
this._handlePinch(e);
}
end(e) {
if (this.pressed) return;
this._handlePinch(e);
this.currentPinchScaling = null; // reset
this.pinchCumulativeDelta = 0;
}
_handlePinch(e) {
const currentPinchScaling = this.currentPinchScaling;
const diff = 1 / (currentPinchScaling) * e.scale;
const rect = e.target.getBoundingClientRect();
const offsetX = e.center.x - rect.left;
const offsetY = e.center.y - rect.top;
const center = {
x: offsetX,
y: offsetY
};
// fingers position difference
const x = Math.abs(e.pointers[0].clientX - e.pointers[1].clientX);
const y = Math.abs(e.pointers[0].clientY - e.pointers[1].clientY);
// diagonal fingers will change both (xy) axes
const p = x / y;
let xy;
if (p > 0.3 && p < 1.7) {
xy = 'xy';
} else if (x > y) {
xy = 'x';
} else {
xy = 'y';
}
const lastTimestamp = this._timestamp;
const now = +new Date();
if ((now - lastTimestamp) > 16) {
this._doZoom(diff, center, xy);
this._timestamp = now;
}
// Keep track of overall scale
this.currentPinchScaling = e.scale;
}
_doZoom(diff, center, whichAxes) {
const self = this;
const { mode, chart, limitRange } = self;
// Which axe should be modified when figers were used.
let _whichAxes;
if (mode === 'xy' && whichAxes !== undefined) {
// based on fingers positions
_whichAxes = whichAxes;
} else {
_whichAxes = 'xy';
}
const data = chart.get('data');
if (Util.directionEnabled(mode, 'x') && Util.directionEnabled(_whichAxes, 'x')) { // x
const xScale = chart.getXScale();
const xField = xScale.field;
if (!limitRange[xField]) {
limitRange[xField] = Helper.getLimitRange(data, xScale);
}
if (xScale.isCategory) { // 横轴为分类类型
self._zoomCatScale(xScale, diff, center);
} else if (xScale.isLinear) {
self._zoomLinearScale(xScale, diff, center, 'x');
}
const xDef = Helper.getColDef(chart, xField);
this.xRange = Helper.getFieldRange(xDef, limitRange[xField], xScale.type);
}
if (Util.directionEnabled(mode, 'y') && Util.directionEnabled(_whichAxes, 'y')) { // y
const yScales = chart.getYScales();
Util.each(yScales, yScale => {
const yField = yScale.field;
if (!limitRange[yField]) {
limitRange[yField] = Helper.getLimitRange(data, yScale);
}
yScale.isLinear && self._zoomLinearScale(yScale, diff, center, 'y');
});
const yDef = Helper.getColDef(chart, yScales[0].field);
this.yRange = Helper.getFieldRange(yDef, limitRange[yScales[0].field], yScales[0].type);
}
chart.repaint();
}
_zoomLinearScale(scale, zoom, center, flag) {
const chart = this.chart;
const { min, max, field } = scale;
const valueRange = max - min;
const limitRange = this.limitRange;
const originRange = limitRange[field].max - limitRange[field].min;
const coord = chart.get('coord');
let newDiff = valueRange * (zoom - 1);
if (this.minScale && zoom < 1) { // zoom in
const maxRange = originRange / this.minScale;
newDiff = Math.max(valueRange - maxRange, newDiff);
}
if (this.maxScale && zoom >= 1) { // zoom out
const minRange = originRange / this.maxScale;
newDiff = Math.min(valueRange - minRange, newDiff);
}
const offsetPoint = coord.invertPoint(center);
const percent = flag === 'x' ? offsetPoint.x : offsetPoint.y;
const minDelta = newDiff * percent;
const maxDelta = newDiff * (1 - percent);
const newMax = max - maxDelta;
const newMin = min + minDelta;
this.updateLinearScale(field, newMin, newMax);
}
// 针对分类类型
_zoomCatScale(scale, zoom, center) {
let pinchCumulativeDelta = this._pinchCumulativeDelta;
const sensitivity = this.sensitivity;
pinchCumulativeDelta = zoom > 1 ? pinchCumulativeDelta + 1 : pinchCumulativeDelta - 1;
this._pinchCumulativeDelta = pinchCumulativeDelta;
const { field, values } = scale;
const chart = this.chart;
const coord = chart.get('coord');
if (!this.originTicks) {
this.originTicks = scale.ticks;
}
const originValues = this.limitRange[field];
const originValuesLen = originValues.length;
const minScale = this.minScale || 1;
const maxScale = this.maxScale || 5;
const minCount = parseInt(originValuesLen / maxScale);
const maxCount = parseInt(originValuesLen / minScale);
const currentLen = values.length;
if (pinchCumulativeDelta > 0 && currentLen <= minCount) {
return null;
}
if (pinchCumulativeDelta < 0 && currentLen >= maxCount) {
return null;
}
const lastLabelIndex = originValuesLen - 1;
const firstValue = values[0];
const lastValue = values[currentLen - 1];
let minIndex = originValues.indexOf(firstValue);
let maxIndex = originValues.indexOf(lastValue);
const chartCenter = (coord.start.x + coord.end.x) / 2;
const centerPointer = center.x;
if (Math.abs(pinchCumulativeDelta) > sensitivity) {
const deltaCount = Math.max(1, parseInt(currentLen * Math.abs(zoom - 1)));
if (pinchCumulativeDelta < 0) {
if (centerPointer >= chartCenter) {
if (minIndex <= 0) {
maxIndex = Math.min(lastLabelIndex, maxIndex + deltaCount);
} else {
minIndex = Math.max(0, minIndex - deltaCount);
}
} else if (centerPointer < chartCenter) {
if (maxIndex >= lastLabelIndex) {
minIndex = Math.max(0, minIndex - deltaCount);
} else {
maxIndex = Math.min(lastLabelIndex, maxIndex + deltaCount);
}
}
this._pinchCumulativeDelta = 0;
} else if (pinchCumulativeDelta > 0) {
if (centerPointer >= chartCenter) {
minIndex = minIndex < maxIndex ? minIndex = Math.min(maxIndex, minIndex + deltaCount) : minIndex;
} else if (centerPointer < chartCenter) {
maxIndex = maxIndex > minIndex ? maxIndex = Math.max(minIndex, maxIndex - deltaCount) : maxIndex;
}
this._pinchCumulativeDelta = 0;
}
const newValues = originValues.slice(minIndex, maxIndex + 1);
this.updateCatScale(field, newValues, this.originTicks, originValues, minIndex, maxIndex);
}
}
}
Chart.registerInteraction('pinch', Pinch);
module.exports = Pinch;