Skip to content

Commit

Permalink
Export zoomRect and make it more configurable (#659)
Browse files Browse the repository at this point in the history
* Implement zoomRect functions

Normal zooming supports customizing zoom behavior using zoomFunctions, but the drag-to-zoom functionality that's implemented by zoomRect bypasses zoomFunctions and calls updateRange correctly.

I have a custom scale implementation that relies on zoomFunctions, so trying to enable drag-to-zoom causes problems. (Specifically, I'm breaking up the axis into viewports so that I can stack multiple series within this chart - somewhat like this example. The scales for the individual portions of the chart shouldn't be directly zoomed, because their layout is managed by a separate, controlling scale, so their zoomFunctions are no-ops.)

Zooming by rectangle appears to be a distinctly different interface than zooming by percentage and focal point, so it seems reasonable to add a zoomRectFunctions to let scales customize that behavior and cover this use case.

* Expose zoomRect; remove incorrect TypeScript type definitions

To go with adding stronger zoomRect functionality, it makes sense to expose the `zoomRect` function directly.

The TypeScript type definitions said that standalone functions like `zoom` were exported, but these don't actually exist.  Fix that.

* Restore incorrectly deleted type definitions

And properly export zoomRect.

* Further update comments

* Restore type tests

* Fix type
  • Loading branch information
joshkel committed Sep 1, 2022
1 parent 7ec113a commit 0259d37
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 21 deletions.
11 changes: 8 additions & 3 deletions docs/guide/developers.md
Expand Up @@ -44,7 +44,7 @@ Returns whether the chart has been zoomed or panned - i.e. whether the initial s

## Custom Scales

You can extend chartjs-plugin-zoom with support for [custom scales](https://www.chartjs.org/docs/latest/developers/axes.html) by using the zoom plugin's `zoomFunctions` and `panFunctions` members. These objects are indexed by scale types (scales' `id` members) and give optional handlers for zoom and pan functionality.
You can extend chartjs-plugin-zoom with support for [custom scales](https://www.chartjs.org/docs/latest/developers/axes.html) by using the zoom plugin's `zoomFunctions`, `zoomRectFunctions`, and `panFunctions` members. These objects are indexed by scale types (scales' `id` members) and give optional handlers for zoom and pan functionality.

```js
import {Scale} from 'chart.js';
Expand All @@ -57,17 +57,22 @@ MyScale.id = 'myScale';
MyScale.defaults = defaultConfigObject;

zoomPlugin.zoomFunctions.myScale = (scale, zoom, center, limits) => false;
zoomPlugin.zoomRectFunctions.myScale = (scale, from, to, limits) => false;
zoomPlugin.panFunctions.myScale = (scale, delta, limits) => false;
// zoomRectFunctions can normally be omitted, since zooming by specific pixel
// coordinates rarely needs special handling.
```

The zoom and pan functions take the following arguments:
The zoom, zoomRect, and pan functions take the following arguments:

| Name | Type | For | Description
| ---- | ---- | --- | ----------
| `scale` | `Scale` | Zoom, Pan | The custom scale instance (usually derived from `Chart.Scale`)
| `zoom` | `number` | Zoom | The zoom fraction; 1.0 is unzoomed, 0.5 means zoomed in to 50% of the original area, etc.
| `center` | `{x, y}` | Zoom | Pixel coordinates of the center of the zoom operation. `{x: 0, y: 0}` is the upper left corner of the chart's canvas.
| `from` | `number` | ZoomRect | Pixel coordinate of the start of the zoomRect operation.
| `to` | `number` | ZoomRect | Pixel coordinate of the end of the zoomRect operation.
| `delta` | `number` | Pan | Pixel amount to pan by
| `limits` | [Limits](./options#limits) | Zoom, Pan | Zoom and pan limits (from chart options)

For examples, see chartjs-plugin-zoom's [default zoomFunctions and panFunctions handling for standard Chart.js axes](https://github.com/chartjs/chartjs-plugin-zoom/blob/v1.0.1/src/scale.types.js#L128).
For examples, see chartjs-plugin-zoom's [default zoomFunctions, zoomRectFunctions, and panFunctions handling for standard Chart.js axes](https://github.com/chartjs/chartjs-plugin-zoom/blob/v1.0.1/src/scale.types.js#L128).
20 changes: 8 additions & 12 deletions src/core.js
@@ -1,5 +1,5 @@
import {each, callback as call, sign, valueOrDefault} from 'chart.js/helpers';
import {panFunctions, updateRange, zoomFunctions} from './scale.types';
import {panFunctions, updateRange, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState} from './state';
import {directionEnabled, getEnabledScalesByPoint} from './utils';

Expand Down Expand Up @@ -43,6 +43,11 @@ function doZoom(scale, amount, center, limits) {
call(fn, [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]);
}

function getCenter(chart) {
const ca = chart.chartArea;
return {
Expand Down Expand Up @@ -80,15 +85,6 @@ export function zoom(chart, amount, transition = 'none') {
call(zoomOptions.onZoom, [{chart}]);
}

function getRange(scale, pixel0, pixel1) {
const v0 = scale.getValueForPixel(pixel0);
const v1 = scale.getValueForPixel(pixel1);
return {
min: Math.min(v0, v1),
max: Math.max(v0, v1)
};
}

export function zoomRect(chart, p0, p1, transition = 'none') {
const state = getState(chart);
const {options: {limits, zoom: zoomOptions}} = state;
Expand All @@ -100,9 +96,9 @@ export function zoomRect(chart, p0, p1, transition = 'none') {

each(chart.scales, function(scale) {
if (scale.isHorizontal() && xEnabled) {
updateRange(scale, getRange(scale, p0.x, p1.x), limits, true);
doZoomRect(scale, p0.x, p1.x, limits);
} else if (!scale.isHorizontal() && yEnabled) {
updateRange(scale, getRange(scale, p0.y, p1.y), limits, true);
doZoomRect(scale, p0.y, p1.y, limits);
}
});

Expand Down
2 changes: 1 addition & 1 deletion src/index.esm.js
@@ -1,4 +1,4 @@
import plugin from './plugin';

export default plugin;
export {pan, zoom, zoomScale, resetZoom} from './core';
export {pan, zoom, zoomRect, zoomScale, resetZoom} from './core';
9 changes: 5 additions & 4 deletions src/plugin.js
@@ -1,8 +1,8 @@
import Hammer from 'hammerjs';
import {addListeners, computeDragRect, removeListeners} from './handlers';
import {startHammer, stopHammer} from './hammer';
import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned} from './core';
import {panFunctions, zoomFunctions} from './scale.types';
import {pan, zoom, resetZoom, zoomScale, getZoomLevel, getInitialScaleBounds, isZoomedOrPanned, zoomRect} from './core';
import {panFunctions, zoomFunctions, zoomRectFunctions} from './scale.types';
import {getState, removeState} from './state';
import {version} from '../package.json';

Expand Down Expand Up @@ -53,6 +53,7 @@ export default {

chart.pan = (delta, panScales, transition) => pan(chart, delta, panScales, transition);
chart.zoom = (args, transition) => zoom(chart, args, transition);
chart.zoomRect = (p0, p1, transition) => zoomRect(chart, p0, p1, transition);
chart.zoomScale = (id, range, transition) => zoomScale(chart, id, range, transition);
chart.resetZoom = (transition) => resetZoom(chart, transition);
chart.getZoomLevel = () => getZoomLevel(chart);
Expand Down Expand Up @@ -107,6 +108,6 @@ export default {
},

panFunctions,

zoomFunctions
zoomFunctions,
zoomRectFunctions,
};
17 changes: 17 additions & 0 deletions src/scale.types.js
Expand Up @@ -29,6 +29,15 @@ function getLimit(state, scale, scaleLimits, prop, fallback) {
return valueOrDefault(limit, fallback);
}

function getRange(scale, pixel0, pixel1) {
const v0 = scale.getValueForPixel(pixel0);
const v1 = scale.getValueForPixel(pixel1);
return {
min: Math.min(v0, v1),
max: Math.max(v0, v1)
};
}

export function updateRange(scale, {min, max}, limits, zoom = false) {
const state = getState(scale.chart);
const {id, axis, options: scaleOpts} = scale;
Expand Down Expand Up @@ -72,6 +81,10 @@ function zoomNumericalScale(scale, zoom, center, limits) {
return updateRange(scale, newRange, limits, true);
}

function zoomRectNumericalScale(scale, from, to, limits) {
updateRange(scale, getRange(scale, from, to), limits, true);
}

const integerChange = (v) => v === 0 || isNaN(v) ? 0 : v < 0 ? Math.min(Math.round(v), -1) : Math.max(Math.round(v), 1);

function existCategoryFromMaxZoom(scale) {
Expand Down Expand Up @@ -158,6 +171,10 @@ export const zoomFunctions = {
default: zoomNumericalScale,
};

export const zoomRectFunctions = {
default: zoomRectNumericalScale,
};

export const panFunctions = {
category: panCategoryScale,
default: panNumericalScale,
Expand Down
1 change: 1 addition & 0 deletions test/specs/api.spec.js
Expand Up @@ -5,6 +5,7 @@ describe('api', function() {
expect(typeof chart.pan).toBe('function');
expect(typeof chart.zoom).toBe('function');
expect(typeof chart.zoomScale).toBe('function');
expect(typeof chart.zoomRect).toBe('function');
expect(typeof chart.resetZoom).toBe('function');
expect(typeof chart.getZoomLevel).toBe('function');
expect(typeof chart.getInitialScaleBounds).toBe('function');
Expand Down
3 changes: 2 additions & 1 deletion test/specs/module.spec.js
Expand Up @@ -7,8 +7,9 @@ describe('module', function() {
expect(window.ChartZoom.id).toBe('zoom');
});

it ('should expose zoomFunctions and panFunctions', function() {
it ('should expose zoomFunctions, zoomRectFunctions, and panFunctions', function() {
expect(window.ChartZoom.zoomFunctions instanceof Object).toBe(true);
expect(window.ChartZoom.zoomRectFunctions instanceof Object).toBe(true);
expect(window.ChartZoom.panFunctions instanceof Object).toBe(true);
});

Expand Down
4 changes: 4 additions & 0 deletions types/index.d.ts
Expand Up @@ -21,6 +21,7 @@ declare module 'chart.js' {
interface Chart<TType extends keyof ChartTypeRegistry = keyof ChartTypeRegistry, TData = DistributiveArray<ChartTypeRegistry[TType]['defaultDataPoint']>, TLabel = unknown> {
pan(pan: PanAmount, scales?: Scale[], mode?: UpdateMode): void;
zoom(zoom: ZoomAmount, mode?: UpdateMode): void;
zoomRect(p0: Point, p1: Point, mode?: UpdateMode): void;
zoomScale(id: string, range: ScaleRange, mode?: UpdateMode): void;
resetZoom(mode?: UpdateMode): void;
getZoomLevel(): number;
Expand All @@ -30,6 +31,7 @@ declare module 'chart.js' {
}

export type ZoomFunction = (scale: Scale, zoom: number, center: Point, limits: LimitOptions) => boolean;
export type ZoomRectFunction = (scale: Scale, from: number, to: number, limits: LimitOptions) => boolean;
export type PanFunction = (scale: Scale, delta: number, limits: LimitOptions) => boolean;

type ScaleFunctions<T> = {
Expand All @@ -40,13 +42,15 @@ type ScaleFunctions<T> = {

declare const Zoom: Plugin & {
zoomFunctions: ScaleFunctions<ZoomFunction>;
zoomRectFunctions: ScaleFunctions<ZoomRectFunction>;
panFunctions: ScaleFunctions<PanFunction>;
};

export default Zoom;

export function pan(chart: Chart, amount: PanAmount, scales?: Scale[], mode?: UpdateMode): void;
export function zoom(chart: Chart, amount: ZoomAmount, mode?: UpdateMode): void;
export function zoomRect(chart: Chart, p0: Point, p1: Point, mode?: UpdateMode): void;
export function zoomScale(chart: Chart, scaleId: string, range: ScaleRange, mode?: UpdateMode): void;
export function resetZoom(chart: Chart, mode?: UpdateMode): void;
export function getZoomLevel(chart: Chart): number;
Expand Down

0 comments on commit 0259d37

Please sign in to comment.