devextreme
Version:
HTML5 JavaScript Component Suite for Responsive Web Development
639 lines (615 loc) • 29.4 kB
JavaScript
/**
* DevExtreme (cjs/viz/chart_components/zoom_and_pan.js)
* Version: 24.2.6
* Build date: Mon Mar 17 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
*/
"use strict";
exports.default = void 0;
var _type = require("../../core/utils/type");
var _extend = require("../../core/utils/extend");
var _utils = require("../core/utils");
var _wheel = require("../../common/core/events/core/wheel");
var transformEvents = _interopRequireWildcard(require("../../common/core/events/transform"));
var _drag = require("../../common/core/events/drag");
function _getRequireWildcardCache(e) {
if ("function" != typeof WeakMap) {
return null
}
var r = new WeakMap,
t = new WeakMap;
return (_getRequireWildcardCache = function(e) {
return e ? t : r
})(e)
}
function _interopRequireWildcard(e, r) {
if (!r && e && e.__esModule) {
return e
}
if (null === e || "object" != typeof e && "function" != typeof e) {
return {
default: e
}
}
var t = _getRequireWildcardCache(r);
if (t && t.has(e)) {
return t.get(e)
}
var n = {
__proto__: null
},
a = Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var u in e) {
if ("default" !== u && {}.hasOwnProperty.call(e, u)) {
var i = a ? Object.getOwnPropertyDescriptor(e, u) : null;
i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]
}
}
return n.default = e, t && t.set(e, n), n
}
function _extends() {
return _extends = Object.assign ? Object.assign.bind() : function(n) {
for (var e = 1; e < arguments.length; e++) {
var t = arguments[e];
for (var r in t) {
({}).hasOwnProperty.call(t, r) && (n[r] = t[r])
}
}
return n
}, _extends.apply(null, arguments)
}
const EVENTS_NS = ".zoomAndPanNS";
const DRAG_START_EVENT_NAME = _drag.start + EVENTS_NS;
const DRAG_EVENT_NAME = _drag.move + EVENTS_NS;
const DRAG_END_EVENT_NAME = _drag.end + EVENTS_NS;
const PINCH_START_EVENT_NAME = transformEvents.pinchstart + EVENTS_NS;
const PINCH_EVENT_NAME = transformEvents.pinch + EVENTS_NS;
const PINCH_END_EVENT_NAME = transformEvents.pinchend + EVENTS_NS;
const SCROLL_BAR_START_EVENT_NAME = "dxc-scroll-start" + EVENTS_NS;
const SCROLL_BAR_MOVE_EVENT_NAME = "dxc-scroll-move" + EVENTS_NS;
const SCROLL_BAR_END_EVENT_NAME = "dxc-scroll-end" + EVENTS_NS;
const GESTURE_TIMEOUT = 300;
const MIN_DRAG_DELTA = 5;
const _min = Math.min;
const _max = Math.max;
const _abs = Math.abs;
function canvasToRect(canvas) {
return {
x: canvas.left,
y: canvas.top,
width: canvas.width - canvas.left - canvas.right,
height: canvas.height - canvas.top - canvas.bottom
}
}
function checkCoords(rect, coords) {
const x = coords.x;
const y = coords.y;
return x >= rect.x && x <= rect.width + rect.x && y >= rect.y && y <= rect.height + rect.y
}
function sortAxes(axes, onlyAxisToNotify) {
if (onlyAxisToNotify) {
axes = axes.sort(((a, b) => {
if (a === onlyAxisToNotify) {
return -1
}
if (b === onlyAxisToNotify) {
return 1
}
return 0
}))
}
return axes
}
function getFilteredAxes(axes) {
return axes.filter((a => !a.getTranslator().getBusinessRange().isEmpty()))
}
function isAxisAvailablePanning(axes) {
return axes.some((axis => !axis.isExtremePosition(false) || !axis.isExtremePosition(true)))
}
function axisZoom(axis, onlyAxisToNotify, getRange, getParameters, actionField, scale, e) {
const silent = onlyAxisToNotify && axis !== onlyAxisToNotify;
const range = getRange(axis);
const {
stopInteraction: stopInteraction,
correctedRange: correctedRange
} = axis.checkZoomingLowerLimitOvercome(actionField, scale, range);
const result = axis.handleZooming(stopInteraction ? null : correctedRange, getParameters(silent), e, actionField);
stopInteraction && axis.handleZoomEnd();
return {
stopInteraction: stopInteraction,
result: result
}
}
function zoomAxes(e, axes, getRange, zoom, params, onlyAxisToNotify) {
axes = sortAxes(axes, onlyAxisToNotify);
let zoomStarted = false;
const getParameters = silent => ({
start: !!silent,
end: !!silent
});
getFilteredAxes(axes).some((axis => {
const translator = axis.getTranslator();
const scale = translator.getMinScale(zoom);
const {
stopInteraction: stopInteraction,
result: result
} = axisZoom(axis, onlyAxisToNotify, getRange(_extends({
scale: scale,
translator: translator,
axis: axis
}, params)), getParameters, "zoom", scale, e);
zoomStarted = !stopInteraction;
return onlyAxisToNotify && result.isPrevented
}));
return zoomStarted
}
function cancelEvent(e) {
if (e.originalEvent) {
cancelEvent(e.originalEvent)
}
if (false !== e.cancelable) {
e.cancel = true
}
}
var _default = exports.default = {
name: "zoom_and_pan",
init: function() {
const chart = this;
const renderer = this._renderer;
function getAxesCopy(zoomAndPan, actionField) {
let axes = [];
const options = zoomAndPan.options;
const actionData = zoomAndPan.actionData;
if (options.argumentAxis[actionField]) {
axes.push(chart.getArgumentAxis())
}
if (options.valueAxis[actionField]) {
axes = axes.concat(actionData.valueAxes)
}
return axes
}
function startAxesViewportChanging(zoomAndPan, actionField, e) {
const axes = getAxesCopy(zoomAndPan, actionField);
getFilteredAxes(axes).some((axis => axis.handleZooming(null, {
end: true
}, e, actionField).isPrevented)) && cancelEvent(e)
}
function axesViewportChanging(zoomAndPan, actionField, e, offsetCalc, centerCalc) {
function zoomAxes(axes, criteria, coordField, e, actionData) {
let zoom = {
zoomed: false
};
criteria && getFilteredAxes(axes).forEach((axis => {
const options = axis.getOptions();
const viewport = axis.visualRange();
const scale = axis.getTranslator().getEventScale(e);
const translate = -offsetCalc(e, actionData, coordField, scale);
zoom = (0, _extend.extend)(true, zoom, axis.getTranslator().zoom(translate, scale, axis.getZoomBounds()));
const range = axis.adjustRange((0, _utils.getVizRangeObject)([zoom.min, zoom.max]));
const {
stopInteraction: stopInteraction,
correctedRange: correctedRange
} = axis.checkZoomingLowerLimitOvercome(actionField, scale, range);
if (!(0, _type.isDefined)(viewport) || viewport.startValue.valueOf() !== correctedRange.startValue.valueOf() || viewport.endValue.valueOf() !== correctedRange.endValue.valueOf()) {
axis.handleZooming(stopInteraction ? null : correctedRange, {
start: true,
end: true
}, e, actionField);
if (!stopInteraction) {
zoom.zoomed = true;
zoom.deltaTranslate = translate - zoom.translate
}
} else if ("touch" === e.pointerType && "discrete" === options.type) {
const isMinPosition = axis.isExtremePosition(false);
const isMaxPosition = axis.isExtremePosition(true);
const zoomInEnabled = scale > 1 && !stopInteraction;
const zoomOutEnabled = scale < 1 && (!isMinPosition || !isMaxPosition);
const panningEnabled = 1 === scale && !(isMinPosition && (translate < 0 && !options.inverted || translate > 0 && options.inverted) || isMaxPosition && (translate > 0 && !options.inverted || translate < 0 && options.inverted));
zoom.enabled = zoomInEnabled || zoomOutEnabled || panningEnabled
}
}));
return zoom
}
function storeOffset(e, actionData, zoom, coordField) {
if (zoom.zoomed) {
actionData.offset[coordField] = (e.offset ? e.offset[coordField] : actionData.offset[coordField]) + zoom.deltaTranslate
}
}
function storeCenter(center, actionData, zoom, coordField) {
if (zoom.zoomed) {
actionData.center[coordField] = center[coordField] + zoom.deltaTranslate
}
}
const rotated = chart.option("rotated");
const actionData = zoomAndPan.actionData;
const options = zoomAndPan.options;
let argZoom = {};
let valZoom = {};
if (!actionData.fallback) {
argZoom = zoomAxes(chart._argumentAxes, options.argumentAxis[actionField], rotated ? "y" : "x", e, actionData);
valZoom = zoomAxes(actionData.valueAxes, options.valueAxis[actionField], rotated ? "x" : "y", e, actionData);
chart._requestChange(["VISUAL_RANGE"]);
storeOffset(e, actionData, argZoom, rotated ? "y" : "x");
storeOffset(e, actionData, valZoom, rotated ? "x" : "y")
}
const center = centerCalc(e);
storeCenter(center, actionData, argZoom, rotated ? "y" : "x");
storeCenter(center, actionData, valZoom, rotated ? "x" : "y");
if (!argZoom.zoomed && !valZoom.zoomed) {
actionData.center = center
}
return argZoom.zoomed || valZoom.zoomed || actionData.fallback || argZoom.enabled || valZoom.enabled
}
function finishAxesViewportChanging(zoomAndPan, actionField, e, offsetCalc) {
function zoomAxes(axes, coordField, actionData, onlyAxisToNotify) {
let zoomStarted = false;
const scale = e.scale || 1;
const getRange = axis => {
const zoom = axis.getTranslator().zoom(-offsetCalc(e, actionData, coordField, scale), scale, axis.getZoomBounds());
return {
startValue: zoom.min,
endValue: zoom.max
}
};
const getParameters = silent => ({
start: true,
end: silent
});
getFilteredAxes(axes).forEach((axis => {
zoomStarted = !axisZoom(axis, onlyAxisToNotify, getRange, getParameters, actionField, scale, e).stopInteraction
}));
return zoomStarted
}
const rotated = chart.option("rotated");
const actionData = zoomAndPan.actionData;
const options = zoomAndPan.options;
let zoomStarted = true;
if (actionData.fallback) {
zoomStarted &= options.argumentAxis[actionField] && zoomAxes(chart._argumentAxes, rotated ? "y" : "x", actionData, chart.getArgumentAxis());
zoomStarted |= options.valueAxis[actionField] && zoomAxes(actionData.valueAxes, rotated ? "x" : "y", actionData)
} else {
const axes = getAxesCopy(zoomAndPan, actionField);
getFilteredAxes(axes).forEach((axis => {
axis.handleZooming(null, {
start: true
}, e, actionField)
}));
zoomStarted = axes.length
}
zoomStarted && chart._requestChange(["VISUAL_RANGE"])
}
function prepareActionData(coords, action) {
const axes = chart._argumentAxes.filter((axis => checkCoords(canvasToRect(axis.getCanvas()), coords)));
return {
fallback: chart._lastRenderingTime > 300,
cancel: !axes.length || !(0, _type.isDefined)(action),
action: action,
curAxisRect: axes.length && canvasToRect(axes[0].getCanvas()),
valueAxes: axes.length && chart._valueAxes.filter((axis => checkCoords(canvasToRect(axis.getCanvas()), coords))),
offset: {
x: 0,
y: 0
},
center: coords,
startCenter: coords
}
}
function getPointerCoord(rect, e) {
const rootOffset = renderer.getRootOffset();
return {
x: _min(_max(e.pageX - rootOffset.left, rect.x), rect.width + rect.x),
y: _min(_max(e.pageY - rootOffset.top, rect.y), rect.height + rect.y)
}
}
function calcCenterForPinch(e) {
const rootOffset = renderer.getRootOffset();
const x1 = e.pointers[0].pageX;
const x2 = e.pointers[1].pageX;
const y1 = e.pointers[0].pageY;
const y2 = e.pointers[1].pageY;
return {
x: _min(x1, x2) + _abs(x2 - x1) / 2 - rootOffset.left,
y: _min(y1, y2) + _abs(y2 - y1) / 2 - rootOffset.top
}
}
function calcCenterForDrag(e) {
const rootOffset = renderer.getRootOffset();
return {
x: e.pageX - rootOffset.left,
y: e.pageY - rootOffset.top
}
}
function calcOffsetForDrag(e, actionData, coordField) {
return e.offset[coordField] - actionData.offset[coordField]
}
function preventDefaults(e) {
if (false !== e.cancelable) {
e.preventDefault();
e.stopPropagation()
}
chart._stopCurrentHandling()
}
const zoomAndPan = {
dragStartHandler: function(e) {
const options = zoomAndPan.options;
const isTouch = "touch" === e.pointerType;
const wantPan = options.argumentAxis.pan || options.valueAxis.pan;
const wantZoom = options.argumentAxis.zoom || options.valueAxis.zoom;
const panKeyPressed = (0, _type.isDefined)(options.panKey) && e[(0, _utils.normalizeEnum)(options.panKey) + "Key"];
const dragToZoom = options.dragToZoom;
let action;
e._cancelPreventDefault = true;
if (isTouch) {
if (options.allowTouchGestures && wantPan) {
const cancelPanning = !zoomAndPan.panningVisualRangeEnabled() || zoomAndPan.skipEvent;
action = cancelPanning ? null : "pan"
}
} else if (dragToZoom && wantPan && panKeyPressed || !dragToZoom && wantPan) {
action = "pan"
} else if (dragToZoom && wantZoom) {
action = "zoom"
}
const actionData = prepareActionData(calcCenterForDrag(e), action);
if (actionData.cancel) {
zoomAndPan.skipEvent = false;
if (false !== e.cancelable) {
e.cancel = true
}
return
}
zoomAndPan.actionData = actionData;
if ("zoom" === action) {
actionData.startCoords = getPointerCoord(actionData.curAxisRect, e);
actionData.rect = renderer.rect(0, 0, 0, 0).attr(options.dragBoxStyle).append(renderer.root)
} else {
startAxesViewportChanging(zoomAndPan, "pan", e)
}
},
dragHandler: function(e) {
const rotated = chart.option("rotated");
const options = zoomAndPan.options;
const actionData = zoomAndPan.actionData;
const isTouch = "touch" === e.pointerType;
e._cancelPreventDefault = true;
if (!actionData || isTouch && !zoomAndPan.panningVisualRangeEnabled()) {
return
}
if ("zoom" === actionData.action) {
preventDefaults(e);
const curCanvas = actionData.curAxisRect;
const startCoords = actionData.startCoords;
const curCoords = getPointerCoord(curCanvas, e);
const zoomArg = options.argumentAxis.zoom;
const zoomVal = options.valueAxis.zoom;
const rect = {
x: _min(startCoords.x, curCoords.x),
y: _min(startCoords.y, curCoords.y),
width: _abs(startCoords.x - curCoords.x),
height: _abs(startCoords.y - curCoords.y)
};
if (!zoomArg || !zoomVal) {
if (!zoomArg && !rotated || !zoomVal && rotated) {
rect.x = curCanvas.x;
rect.width = curCanvas.width
} else {
rect.y = curCanvas.y;
rect.height = curCanvas.height
}
}
actionData.rect.attr(rect)
} else if ("pan" === actionData.action) {
axesViewportChanging(zoomAndPan, "pan", e, calcOffsetForDrag, (e => e.offset));
const deltaOffsetY = Math.abs(e.offset.y - actionData.offset.y);
const deltaOffsetX = Math.abs(e.offset.x - actionData.offset.x);
if (isTouch && (deltaOffsetY > 5 && deltaOffsetY > Math.abs(actionData.offset.x) || deltaOffsetX > 5 && deltaOffsetX > Math.abs(actionData.offset.y))) {
return
}
preventDefaults(e)
}
},
dragEndHandler: function(e) {
const rotated = chart.option("rotated");
const options = zoomAndPan.options;
const actionData = zoomAndPan.actionData;
const isTouch = "touch" === e.pointerType;
const getRange = _ref => {
let {
translator: translator,
startCoord: startCoord,
curCoord: curCoord
} = _ref;
return () => [translator.from(startCoord), translator.from(curCoord)]
};
const getCoords = (curCoords, startCoords, field) => ({
curCoord: curCoords[field],
startCoord: startCoords[field]
});
const needToZoom = (axisOption, coords) => axisOption.zoom && _abs(coords.curCoord - coords.startCoord) > 5;
const panIsEmpty = actionData && "pan" === actionData.action && !actionData.fallback && 0 === actionData.offset.x && 0 === actionData.offset.y;
if (!actionData || isTouch && !zoomAndPan.panningVisualRangeEnabled() || panIsEmpty) {
return
}!isTouch && preventDefaults(e);
if ("zoom" === actionData.action) {
const curCoords = getPointerCoord(actionData.curAxisRect, e);
const argumentCoords = getCoords(curCoords, actionData.startCoords, rotated ? "y" : "x");
const valueCoords = getCoords(curCoords, actionData.startCoords, rotated ? "x" : "y");
const argumentAxesZoomed = needToZoom(options.argumentAxis, argumentCoords) && zoomAxes(e, chart._argumentAxes, getRange, true, argumentCoords, chart.getArgumentAxis());
const valueAxesZoomed = needToZoom(options.valueAxis, valueCoords) && zoomAxes(e, actionData.valueAxes, getRange, true, valueCoords);
if (valueAxesZoomed || argumentAxesZoomed) {
chart._requestChange(["VISUAL_RANGE"])
}
actionData.rect.dispose()
} else if ("pan" === actionData.action) {
finishAxesViewportChanging(zoomAndPan, "pan", e, calcOffsetForDrag)
}
zoomAndPan.actionData = null
},
pinchStartHandler: function(e) {
const actionData = prepareActionData(calcCenterForPinch(e), "zoom");
if (actionData.cancel) {
cancelEvent(e);
return
}
zoomAndPan.actionData = actionData;
startAxesViewportChanging(zoomAndPan, "zoom", e)
},
pinchHandler: function(e) {
if (!zoomAndPan.actionData) {
return
}
axesViewportChanging(zoomAndPan, "zoom", e, ((e, actionData, coordField, scale) => calcCenterForPinch(e)[coordField] - actionData.center[coordField] + (actionData.center[coordField] - actionData.center[coordField] * scale)), calcCenterForPinch);
preventDefaults(e)
},
pinchEndHandler: function(e) {
if (!zoomAndPan.actionData) {
return
}
finishAxesViewportChanging(zoomAndPan, "zoom", e, ((e, actionData, coordField, scale) => actionData.center[coordField] - actionData.startCenter[coordField] + (actionData.startCenter[coordField] - actionData.startCenter[coordField] * scale)));
zoomAndPan.actionData = null
},
mouseWheelHandler: function(e) {
const options = zoomAndPan.options;
const rotated = chart.option("rotated");
const getRange = _ref2 => {
let {
translator: translator,
coord: coord,
scale: scale,
axis: axis
} = _ref2;
return () => {
const zoom = translator.zoom(-(coord - coord * scale), scale, axis.getZoomBounds());
return {
startValue: zoom.min,
endValue: zoom.max
}
}
};
const coords = calcCenterForDrag(e);
let axesZoomed = false;
let targetAxes;
if (options.valueAxis.zoom) {
targetAxes = chart._valueAxes.filter((axis => checkCoords(canvasToRect(axis.getCanvas()), coords)));
if (0 === targetAxes.length) {
const targetCanvas = chart._valueAxes.reduce(((r, axis) => {
if (!r && axis.coordsIn(coords.x, coords.y)) {
r = axis.getCanvas()
}
return r
}), null);
if (targetCanvas) {
targetAxes = chart._valueAxes.filter((axis => checkCoords(canvasToRect(axis.getCanvas()), {
x: targetCanvas.left,
y: targetCanvas.top
})))
}
}
axesZoomed |= zoomAxes(e, targetAxes, getRange, e.delta > 0, {
coord: rotated ? coords.x : coords.y
})
}
if (options.argumentAxis.zoom) {
const canZoom = chart._argumentAxes.some((axis => {
if (checkCoords(canvasToRect(axis.getCanvas()), coords) || axis.coordsIn(coords.x, coords.y)) {
return true
}
return false
}));
axesZoomed |= canZoom && zoomAxes(e, chart._argumentAxes, getRange, e.delta > 0, {
coord: rotated ? coords.y : coords.x
}, chart.getArgumentAxis())
}
if (axesZoomed) {
chart._requestChange(["VISUAL_RANGE"]);
if (targetAxes && isAxisAvailablePanning(targetAxes) || !targetAxes && zoomAndPan.panningVisualRangeEnabled()) {
preventDefaults(e)
}
}
},
cleanup: function() {
renderer.root.off(EVENTS_NS);
zoomAndPan.actionData && zoomAndPan.actionData.rect && zoomAndPan.actionData.rect.dispose();
zoomAndPan.actionData = null;
renderer.root.css({
"touch-action": ""
})
},
setup: function(options) {
zoomAndPan.cleanup();
if (!options.argumentAxis.pan) {
renderer.root.on(SCROLL_BAR_START_EVENT_NAME, cancelEvent)
}
if (options.argumentAxis.none && options.valueAxis.none) {
return
}
zoomAndPan.options = options;
if ((options.argumentAxis.zoom || options.valueAxis.zoom) && options.allowMouseWheel) {
renderer.root.on(_wheel.name + EVENTS_NS, zoomAndPan.mouseWheelHandler)
}
if ((options.argumentAxis.zoom || options.valueAxis.zoom) && options.allowTouchGestures) {
renderer.root.on(PINCH_START_EVENT_NAME, {
passive: false
}, zoomAndPan.pinchStartHandler).on(PINCH_EVENT_NAME, {
passive: false
}, zoomAndPan.pinchHandler).on(PINCH_END_EVENT_NAME, zoomAndPan.pinchEndHandler)
}
renderer.root.on(DRAG_START_EVENT_NAME, {
immediate: true,
passive: false
}, zoomAndPan.dragStartHandler).on(DRAG_EVENT_NAME, {
immediate: true,
passive: false
}, zoomAndPan.dragHandler).on(DRAG_END_EVENT_NAME, zoomAndPan.dragEndHandler);
renderer.root.on(SCROLL_BAR_START_EVENT_NAME, (function(e) {
zoomAndPan.actionData = {
valueAxes: [],
offset: {
x: 0,
y: 0
},
center: {
x: 0,
y: 0
}
};
preventDefaults(e);
startAxesViewportChanging(zoomAndPan, "pan", e)
})).on(SCROLL_BAR_MOVE_EVENT_NAME, (function(e) {
preventDefaults(e);
axesViewportChanging(zoomAndPan, "pan", e, calcOffsetForDrag, (e => e.offset))
})).on(SCROLL_BAR_END_EVENT_NAME, (function(e) {
preventDefaults(e);
finishAxesViewportChanging(zoomAndPan, "pan", e, calcOffsetForDrag);
zoomAndPan.actionData = null
}))
},
panningVisualRangeEnabled: function() {
return isAxisAvailablePanning(chart._valueAxes) || isAxisAvailablePanning(chart._argumentAxes)
}
};
this._zoomAndPan = zoomAndPan
},
members: {
_setupZoomAndPan: function() {
this._zoomAndPan.setup(this._themeManager.getOptions("zoomAndPan"))
}
},
dispose: function() {
this._zoomAndPan.cleanup()
},
customize: function(constructor) {
constructor.addChange({
code: "ZOOM_AND_PAN",
handler: function() {
this._setupZoomAndPan()
},
isThemeDependent: true,
isOptionChange: true,
option: "zoomAndPan"
})
}
};
module.exports = exports.default;
module.exports.default = exports.default;