Skip to content

Commit

Permalink
Refactor to logical entities (#452)
Browse files Browse the repository at this point in the history
* Refactor to logical entities

* Remove monolith

* Complete refactoring

* Add tolerance

* Fix drag-zoom, complete let/const

* Better helper names, shorten wheel

* Remove duplicate drag rect calculations
  • Loading branch information
kurkle authored Apr 16, 2021
1 parent b4c9462 commit c5c7d47
Show file tree
Hide file tree
Showing 13 changed files with 695 additions and 674 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ plugins: ['html', 'es']
rules:
complexity: ["warn", 10]
max-statements: ["warn", 30]
no-var: "warn"
prefer-const: ["warn", {"destructuring": "all"}]
1 change: 1 addition & 0 deletions samples/.eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ globals:
rules:
indent: ["error", "tab", {flatTernaryExpressions: true}]
no-new: "off"
no-var: "off"
115 changes: 115 additions & 0 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {each, callback as call} from 'chart.js/helpers';
import {panFunctions, zoomFunctions} from './scale.types';
import {directionEnabled, getEnabledScalesByPoint} from './utils';

function storeOriginalOptions(chart) {
const originalOptions = chart.$zoom._originalOptions;
each(chart.scales, function(scale) {
if (!originalOptions[scale.id]) {
originalOptions[scale.id] = {min: scale.options.min, max: scale.options.max};
}
});
each(originalOptions, function(opt, key) {
if (!chart.scales[key]) {
delete originalOptions[key];
}
});
}

function zoomScale(scale, zoom, center, zoomOptions) {
call(zoomFunctions[scale.type], [scale, zoom, center, zoomOptions]);
}

/**
* @param chart The chart instance
* @param {number} percentZoomX The zoom percentage in the x direction
* @param {number} percentZoomY The zoom percentage in the y direction
* @param {{x: number, y: number}} focalPoint The x and y coordinates of zoom focal point. The point which doesn't change while zooming. E.g. the location of the mouse cursor when "drag: false"
* @param {object} zoomOptions The zoom options
* @param {string} [whichAxes] `xy`, 'x', or 'y'
* @param {boolean} [useTransition] Whether to use `zoom` transition
*/
export function doZoom(chart, percentZoomX, percentZoomY, focalPoint, zoomOptions, whichAxes, useTransition) {
const ca = chart.chartArea;
if (!focalPoint) {
focalPoint = {
x: (ca.left + ca.right) / 2,
y: (ca.top + ca.bottom) / 2,
};
}

if (zoomOptions.enabled) {
storeOriginalOptions(chart);
// Do the zoom here
const zoomMode = typeof zoomOptions.mode === 'function' ? zoomOptions.mode({chart: chart}) : zoomOptions.mode;

// Which axes should be modified when fingers were used.
let _whichAxes;
if (zoomMode === 'xy' && whichAxes !== undefined) {
// based on fingers positions
_whichAxes = whichAxes;
} else {
// no effect
_whichAxes = 'xy';
}

const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint.x, focalPoint.y, chart);
each(enabledScales || chart.scales, function(scale) {
if (scale.isHorizontal() && directionEnabled(zoomMode, 'x', chart) && directionEnabled(_whichAxes, 'x', chart)) {
zoomOptions.scaleAxes = 'x';
zoomScale(scale, percentZoomX, focalPoint, zoomOptions);
} else if (!scale.isHorizontal() && directionEnabled(zoomMode, 'y', chart) && directionEnabled(_whichAxes, 'y', chart)) {
// Do Y zoom
zoomOptions.scaleAxes = 'y';
zoomScale(scale, percentZoomY, focalPoint, zoomOptions);
}
});

chart.update(useTransition ? 'zoom' : 'none');

call(zoomOptions.onZoom, [chart]);
}
}

export function resetZoom(chart) {
storeOriginalOptions(chart);
const originalOptions = chart.$zoom._originalOptions;
each(chart.scales, function(scale) {

const scaleOptions = scale.options;
if (originalOptions[scale.id]) {
scaleOptions.min = originalOptions[scale.id].min;
scaleOptions.max = originalOptions[scale.id].max;
} else {
delete scaleOptions.min;
delete scaleOptions.max;
}
});
chart.update();
}

function panScale(scale, delta, panOptions) {
call(panFunctions[scale.type], [scale, delta, panOptions]);
}

export function doPan(chart, deltaX, deltaY, panOptions, panningScales) {
storeOriginalOptions(chart);
if (panOptions.enabled) {
const panMode = typeof panOptions.mode === 'function' ? panOptions.mode({chart}) : panOptions.mode;

each(panningScales || chart.scales, function(scale) {
if (scale.isHorizontal() && directionEnabled(panMode, 'x', chart) && deltaX !== 0) {
panOptions.scaleAxes = 'x';
panScale(scale, deltaX, panOptions);
} else if (!scale.isHorizontal() && directionEnabled(panMode, 'y', chart) && deltaY !== 0) {
panOptions.scaleAxes = 'y';
panScale(scale, deltaY, panOptions);
}
});

chart.update('none');

call(panOptions.onPan, [chart]);
}
}

153 changes: 153 additions & 0 deletions src/hammer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Hammer from 'hammerjs';
import {doPan, doZoom} from './core';
import {getEnabledScalesByPoint} from './utils';

function createEnabler(chart, panOptions) {
return function(recognizer, event) {
if (!panOptions || !panOptions.enabled) {
return false;
}
if (!event || !event.srcEvent) { // Sometimes Hammer queries this with a null event.
return true;
}
const requireModifier = panOptions.modifierKey
&& (event.pointerType === 'mouse');
if (requireModifier && !event.srcEvent[panOptions.modifierKey + 'Key']) {
if (typeof panOptions.onPanRejected === 'function') {
panOptions.onPanRejected({
chart: chart,
event: event
});
}
return false;
}
return true;
};
}

export function startHammer(chart, options) {
const node = chart.canvas;
const {pan: panOptions, zoom: zoomOptions} = options;

const mc = new Hammer.Manager(node);
if (zoomOptions && zoomOptions.enabled) {
mc.add(new Hammer.Pinch());
}
if (panOptions && panOptions.enabled) {
mc.add(new Hammer.Pan({
threshold: panOptions.threshold,
enable: createEnabler(chart, panOptions)
}));
}

// Hammer reports the total scaling. We need the incremental amount
let currentPinchScaling;
const handlePinch = function(e) {
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'; // x axis
} else {
xy = 'y'; // y axis
}

doZoom(chart, diff, diff, center, zoomOptions, xy);

if (typeof zoomOptions.onZoom === 'function') {
zoomOptions.onZoom({chart: chart});
}

// Keep track of overall scale
currentPinchScaling = e.scale;
};

mc.on('pinchstart', function() {
currentPinchScaling = 1; // reset tracker
});
mc.on('pinch', handlePinch);
mc.on('pinchend', function(e) {
handlePinch(e);
currentPinchScaling = null; // reset
if (typeof zoomOptions.onZoomComplete === 'function') {
zoomOptions.onZoomComplete({chart: chart});
}
});

let currentDeltaX = null;
let currentDeltaY = null;
let panning = false;
let panningScales = null;
const handlePan = function(e) {
if (currentDeltaX !== null && currentDeltaY !== null) {
panning = true;
const deltaX = e.deltaX - currentDeltaX;
const deltaY = e.deltaY - currentDeltaY;
currentDeltaX = e.deltaX;
currentDeltaY = e.deltaY;
doPan(chart, deltaX, deltaY, panOptions, panningScales);
}
};

mc.on('panstart', function(e) {
if (panOptions.enabled) {
const rect = e.target.getBoundingClientRect();
const x = e.center.x - rect.left;
const y = e.center.y - rect.top;
panningScales = getEnabledScalesByPoint(panOptions, x, y, chart);
}

currentDeltaX = 0;
currentDeltaY = 0;
handlePan(e);
});
mc.on('panmove', handlePan);
mc.on('panend', function() {
currentDeltaX = null;
currentDeltaY = null;
setTimeout(function() {
panning = false;
}, 500);
if (typeof panOptions.onPanComplete === 'function') {
panOptions.onPanComplete({chart: chart});
}
});

chart.$zoom._ghostClickHandler = function(e) {
if (panning && e.cancelable) {
e.stopImmediatePropagation();
e.preventDefault();
}
};
node.addEventListener('click', chart.$zoom._ghostClickHandler);

chart._mc = mc;
}

export function stopHammer(chart) {
const mc = chart._mc;
if (mc) {
mc.remove('pinchstart');
mc.remove('pinch');
mc.remove('pinchend');
mc.remove('panstart');
mc.remove('pan');
mc.remove('panend');
mc.destroy();
}
}
Loading

0 comments on commit c5c7d47

Please sign in to comment.