Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: refactor and optimize core code #833

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 74 additions & 132 deletions src/core.js
Original file line number Diff line number Diff line change
@@ -1,78 +1,58 @@
import {each, callback as call, sign, valueOrDefault} from 'chart.js/helpers';
import {panFunctions, updateRange, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState} from './state';
import {directionEnabled, getEnabledScalesByPoint} from './utils';

function shouldUpdateScaleLimits(scale, originalScaleLimits, updatedScaleLimits) {
const {id, options: {min, max}} = scale;
if (!originalScaleLimits[id] || !updatedScaleLimits[id]) {
return true;
}
const previous = updatedScaleLimits[id];
return previous.min !== min || previous.max !== max;
import { each, callback as call, sign, valueOrDefault } from 'chart.js/helpers';
import { panFunctions, updateRange, zoomFunctions, zoomRectFunctions } from './scale.types';
import { getState } from './state';
import { directionEnabled, getEnabledScalesByPoint } from './utils';

function shouldUpdateScaleLimits(scale, originalLimits, updatedLimits) {
const { id, options: { min, max } } = scale;
const { min: prevMin, max: prevMax } = updatedLimits[id] || {};
return !originalLimits[id] || !updatedLimits[id] || prevMin !== min || prevMax !== max;
}

function removeMissingScales(limits, scales) {
each(limits, (opt, key) => {
if (!scales[key]) {
delete limits[key];
}
Object.keys(limits).forEach(key => {
if (!scales[key]) delete limits[key];
});
}

function storeOriginalScaleLimits(chart, state) {
const {scales} = chart;
const {originalScaleLimits, updatedScaleLimits} = state;

each(scales, function(scale) {
if (shouldUpdateScaleLimits(scale, originalScaleLimits, updatedScaleLimits)) {
originalScaleLimits[scale.id] = {
min: {scale: scale.min, options: scale.options.min},
max: {scale: scale.max, options: scale.options.max},
};
const { scales } = chart;
const { originalScaleLimits, updatedScaleLimits } = state;

Object.values(scales).forEach(({ id, min, max, options }) => {
if (shouldUpdateScaleLimits({ id, options }, originalScaleLimits, updatedScaleLimits)) {
originalScaleLimits[id] = { min: { scale: min, options: options.min }, max: { scale: max, options: options.max } };
}
});

removeMissingScales(originalScaleLimits, scales);
removeMissingScales(updatedScaleLimits, scales);
[originalScaleLimits, updatedScaleLimits].forEach(limits => removeMissingScales(limits, scales));
return originalScaleLimits;
}

function doZoom(scale, amount, center, limits) {
const fn = zoomFunctions[scale.type] || zoomFunctions.default;
call(fn, [scale, amount, center, limits]);
(zoomFunctions[scale.type] || zoomFunctions.default)?.(scale, amount, center, limits);
}

function doZoomRect(scale, amount, from, to, limits) {
const fn = zoomRectFunctions[scale.type] || zoomRectFunctions.default;
call(fn, [scale, amount, from, to, limits]);
(zoomRectFunctions[scale.type] || zoomRectFunctions.default)?.(scale, amount, from, to, limits);
}

function getCenter(chart) {
const ca = chart.chartArea;
return {
x: (ca.left + ca.right) / 2,
y: (ca.top + ca.bottom) / 2,
};
function getCenter({ chartArea: { left, right, top, bottom } }) {
return { x: (left + right) / 2, y: (top + bottom) / 2 };
}

/**
* @param chart The chart instance
* @param {number | {x?: number, y?: number, focalPoint?: {x: number, y: number}}} amount The zoom percentage or percentages and focal point
* @param {string} [transition] Which transition mode to use. Defaults to 'none'
*/
export function zoom(chart, amount, transition = 'none') {
const {x = 1, y = 1, focalPoint = getCenter(chart)} = typeof amount === 'number' ? {x: amount, y: amount} : amount;
const { x = 1, y = 1, focalPoint = getCenter(chart) } = typeof amount === 'number' ? { x: amount, y: amount } : amount;
const state = getState(chart);
const {options: {limits, zoom: zoomOptions}} = state;
const { options: { limits, zoom: zoomOptions } } = state;

storeOriginalScaleLimits(chart, state);

const xEnabled = x !== 1;
const yEnabled = y !== 1;
const enabledScales = getEnabledScalesByPoint(zoomOptions, focalPoint, chart);

each(enabledScales || chart.scales, function(scale) {
each(enabledScales || Object.values(chart.scales), scale => {
if (scale.isHorizontal() && xEnabled) {
doZoom(scale, x, focalPoint, limits);
} else if (!scale.isHorizontal() && yEnabled) {
Expand All @@ -81,30 +61,24 @@ export function zoom(chart, amount, transition = 'none') {
});

chart.update(transition);

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

export function zoomRect(chart, p0, p1, transition = 'none') {
const state = getState(chart);
const {options: {limits, zoom: zoomOptions}} = state;
const {mode = 'xy'} = zoomOptions;
export function zoomRect(chart, { x: x0, y: y0 }, { x: x1, y: y1 }, transition = 'none') {
const { options: { limits, zoom: { mode = 'xy', onZoom } } } = getState(chart);

storeOriginalScaleLimits(chart, state);
const xEnabled = directionEnabled(mode, 'x', chart);
const yEnabled = directionEnabled(mode, 'y', chart);
storeOriginalScaleLimits(chart, getState(chart));

each(chart.scales, function(scale) {
if (scale.isHorizontal() && xEnabled) {
doZoomRect(scale, p0.x, p1.x, limits);
} else if (!scale.isHorizontal() && yEnabled) {
doZoomRect(scale, p0.y, p1.y, limits);
Object.values(chart.scales).forEach(scale => {
const isHorizontal = scale.isHorizontal();
if ((isHorizontal && directionEnabled(mode, 'x', chart)) ||
(!isHorizontal && directionEnabled(mode, 'y', chart))) {
doZoomRect(scale, isHorizontal ? x0 : y0, isHorizontal ? x1 : y1, limits);
}
});

chart.update(transition);

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

export function zoomScale(chart, scaleId, range, transition = 'none') {
Expand All @@ -116,80 +90,57 @@ export function zoomScale(chart, scaleId, range, transition = 'none') {

export function resetZoom(chart, transition = 'default') {
const state = getState(chart);
const originalScaleLimits = storeOriginalScaleLimits(chart, state);

each(chart.scales, function(scale) {
const scaleOptions = scale.options;
if (originalScaleLimits[scale.id]) {
scaleOptions.min = originalScaleLimits[scale.id].min.options;
scaleOptions.max = originalScaleLimits[scale.id].max.options;
} else {
delete scaleOptions.min;
delete scaleOptions.max;
}
const { options: { zoom: { onZoomComplete } } } = state;
const originalLimits = storeOriginalScaleLimits(chart, state);

Object.values(chart.scales).forEach(({ id, options }) => {
const { min, max } = originalLimits[id] || {};
options.min = min?.options;
options.max = max?.options;
if (!min) delete options.min;
if (!max) delete options.max;
});

chart.update(transition);
call(state.options.zoom.onZoomComplete, [{chart}]);
if (onZoomComplete) call(onZoomComplete, [{ chart }]);
}

function getOriginalRange(state, scaleId) {
const original = state.originalScaleLimits[scaleId];
if (!original) {
return;
}
const {min, max} = original;
return valueOrDefault(max.options, max.scale) - valueOrDefault(min.options, min.scale);
const { min, max } = state.originalScaleLimits[scaleId] || {};
return min && max ? valueOrDefault(max.options, max.scale) - valueOrDefault(min.options, min.scale) : undefined;
}

export function getZoomLevel(chart) {
const state = getState(chart);
let min = 1;
let max = 1;
each(chart.scales, function(scale) {

return Object.values(chart.scales).reduce((acc, scale) => {
const origRange = getOriginalRange(state, scale.id);
if (origRange) {
const level = Math.round(origRange / (scale.max - scale.min) * 100) / 100;
min = Math.min(min, level);
max = Math.max(max, level);
acc.min = Math.min(acc.min, level);
acc.max = Math.max(acc.max, level);
}
});
return min < 1 ? min : max;
return acc;
}, { min: 1, max: 1 });
}

function panScale(scale, delta, limits, state) {
const {panDelta} = state;
// Add possible cumulative delta from previous pan attempts where scale did not change
function panScale(scale, delta, limits, { panDelta }) {
const storedDelta = panDelta[scale.id] || 0;
if (sign(storedDelta) === sign(delta)) {
delta += storedDelta;
}
delta += (sign(storedDelta) === sign(delta)) ? storedDelta : 0;
const fn = panFunctions[scale.type] || panFunctions.default;
if (call(fn, [scale, delta, limits])) {
// The scale changed, reset cumulative delta
panDelta[scale.id] = 0;
} else {
// The scale did not change, store cumulative delta
panDelta[scale.id] = delta;
}
panDelta[scale.id] = call(fn, [scale, delta, limits]) ? 0 : delta;
}

export function pan(chart, delta, enabledScales, transition = 'none') {
const {x = 0, y = 0} = typeof delta === 'number' ? {x: delta, y: delta} : delta;
const { x = 0, y = 0 } = typeof delta === 'number' ? { x: delta, y: delta } : delta;
const state = getState(chart);
const {options: {pan: panOptions, limits}} = state;
const {onPan} = panOptions || {};
const { options: { pan: { onPan } = {}, limits }, scales } = state;

storeOriginalScaleLimits(chart, state);

const xEnabled = x !== 0;
const yEnabled = y !== 0;

each(enabledScales || chart.scales, function(scale) {
if (scale.isHorizontal() && xEnabled) {
panScale(scale, x, limits, state);
} else if (!scale.isHorizontal() && yEnabled) {
panScale(scale, y, limits, state);
}
each(enabledScales || chart.scales, scale => {
const delta = scale.isHorizontal() ? x : y;
if (delta) panScale(scale, delta, limits, state);
});

chart.update(transition);
Expand All @@ -198,30 +149,21 @@ export function pan(chart, delta, enabledScales, transition = 'none') {
}

export function getInitialScaleBounds(chart) {
const state = getState(chart);
storeOriginalScaleLimits(chart, state);
const scaleBounds = {};
for (const scaleId of Object.keys(chart.scales)) {
const {min, max} = state.originalScaleLimits[scaleId] || {min: {}, max: {}};
scaleBounds[scaleId] = {min: min.scale, max: max.scale};
}
const { originalScaleLimits } = getState(chart);
storeOriginalScaleLimits(chart, getState(chart));

return scaleBounds;
return Object.keys(chart.scales).reduce((bounds, scaleId) => {
const { min: { scale: minScale } = {}, max: { scale: maxScale } = {} } = originalScaleLimits[scaleId] || {};
bounds[scaleId] = { min: minScale, max: maxScale };
return bounds;
}, {});
}

export function isZoomedOrPanned(chart) {
const scaleBounds = getInitialScaleBounds(chart);
for (const scaleId of Object.keys(chart.scales)) {
const {min: originalMin, max: originalMax} = scaleBounds[scaleId];

if (originalMin !== undefined && chart.scales[scaleId].min !== originalMin) {
return true;
}

if (originalMax !== undefined && chart.scales[scaleId].max !== originalMax) {
return true;
}
}

return false;
}
return Object.entries(chart.scales).some(([scaleId, { min, max }]) => {
const { min: originalMin, max: originalMax } = scaleBounds[scaleId];
return min !== originalMin || max !== originalMax;
});
}