Skip to content

Commit

Permalink
chartZoom: listen event from seperate element
Browse files Browse the repository at this point in the history
in case where the range object living and where the event should be listen is different

fix inaccurate zoom origin calculation

use Proxy to handle default options, which does not modify the user provided object.
  • Loading branch information
huww98 committed Jan 28, 2024
1 parent 39c9ce5 commit f28eddc
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 54 deletions.
35 changes: 18 additions & 17 deletions src/chartZoom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,6 @@ export const defaultOptions = {
touchMinPoints: 1,
} as const;

type WithDefaults<T, TDefault> = {x?: T&TDefault, y?: T&TDefault} & typeof defaultOptions;

export function resolveOptions<T, TDefault extends Object>(defaults: TDefault, o?: {x?: T, y?: T}): WithDefaults<T, TDefault> {
if (!o)
o = {}
if (!defaultOptions.isPrototypeOf(o))
Object.setPrototypeOf(o, defaultOptions);
const resolveAxis = (ao?: T) => {
if (ao && !defaults.isPrototypeOf(ao))
Object.setPrototypeOf(ao, defaults);
}
resolveAxis(o.x);
resolveAxis(o.y);
return o as WithDefaults<T, TDefault>;
}

export class ChartZoom {
options: ResolvedOptions;
private touch: ChartZoomTouch;
Expand All @@ -41,7 +25,24 @@ export class ChartZoom {

constructor(el: CapableElement, options?: ChartZoomOptions) {
options = options ?? {};
this.options = resolveOptions(defaultAxisOptions, options);
this.options = new Proxy(options, {
get(target, prop) {
if (prop === 'x' || prop === 'y') {
const op = target[prop];
if (!op)
return op;
return new Proxy(op, {
get(target, prop) {
return (target as any)[prop] ?? (defaultAxisOptions as any)[prop];
}
})
}
if (prop === 'eventElement') {
return target[prop] ?? el;
}
return (target as any)[prop] ?? (defaultOptions as any)[prop];
}
}) as ResolvedOptions;

this.touch = new ChartZoomTouch(el, this.options);
this.mouse = new ChartZoomMouse(el, this.options);
Expand Down
19 changes: 11 additions & 8 deletions src/chartZoom/mouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ export class ChartZoomMouse {
private previousPoint: Point | null = null;

constructor(private el: CapableElement, private options: ResolvedOptions) {
el.style.userSelect = 'none';
el.addEventListener('pointerdown', ev => this.onMouseDown(ev));
el.addEventListener('pointerup', ev => this.onMouseUp(ev));
el.addEventListener('pointermove', ev => this.onMouseMove(ev));
const eventEl = options.eventElement;
eventEl.style.userSelect = 'none';
eventEl.addEventListener('pointerdown', ev => this.onMouseDown(ev));
eventEl.addEventListener('pointerup', ev => this.onMouseUp(ev));
eventEl.addEventListener('pointermove', ev => this.onMouseMove(ev));
}

private point(ev: MouseEvent) {
Expand Down Expand Up @@ -47,17 +48,19 @@ export class ChartZoomMouse {
return;
if ((event.buttons & this.options.panMouseButtons) === 0)
return;
this.el.setPointerCapture(event.pointerId);
const eventEl = this.options.eventElement;
eventEl.setPointerCapture(event.pointerId);
this.previousPoint = this.point(event);
this.el.style.cursor = 'grabbing';
eventEl.style.cursor = 'grabbing';
}

private onMouseUp(event: PointerEvent) {
if (this.previousPoint === null) {
return;
}
const eventEl = this.options.eventElement;
this.previousPoint = null
this.el.releasePointerCapture(event.pointerId);
this.el.style.cursor = '';
eventEl.releasePointerCapture(event.pointerId);
eventEl.style.cursor = '';
}
}
2 changes: 2 additions & 0 deletions src/chartZoom/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ export interface ResolvedOptions {
y?: ResolvedAxisOptions;
panMouseButtons: number;
touchMinPoints: number;
eventElement: CapableElement;
}

export interface ChartZoomOptions {
x?: AxisOptions;
y?: AxisOptions;
panMouseButtons?: number;
touchMinPoints?: number;
eventElement?: CapableElement;
}

export interface CapableElement extends Element, ElementCSSInlineStyle {
Expand Down
9 changes: 5 additions & 4 deletions src/chartZoom/touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ export class ChartZoomTouch {
};

constructor(private el: CapableElement, private options: ResolvedOptions) {
el.addEventListener('touchstart', e => this.onTouchStart(e), { passive: true });
el.addEventListener('touchend', e => this.onTouchEnd(e), { passive: true });
el.addEventListener('touchcancel', e => this.onTouchEnd(e), { passive: true });
el.addEventListener('touchmove', e => this.onTouchMove(e), { passive: true });
const eventEl = options.eventElement;
eventEl.addEventListener('touchstart', e => this.onTouchStart(e), { passive: true });
eventEl.addEventListener('touchend', e => this.onTouchEnd(e), { passive: true });
eventEl.addEventListener('touchcancel', e => this.onTouchEnd(e), { passive: true });
eventEl.addEventListener('touchmove', e => this.onTouchMove(e), { passive: true });

this.update();
}
Expand Down
3 changes: 2 additions & 1 deletion src/chartZoom/wheel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export class ChartZoomWheel {
public scaleUpdated = new EventDispatcher();

constructor(private el: CapableElement, private options: ResolvedOptions) {
el.addEventListener('wheel', ev => this.onWheel(ev));
const eventEl = options.eventElement;
eventEl.addEventListener('wheel', ev => this.onWheel(ev));
}

private onWheel(event: WheelEvent) {
Expand Down
19 changes: 10 additions & 9 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@ import { ColorCommonInstance, ColorSpaceObject, rgb } from 'd3-color';
import { DataPointsBuffer } from './core/dataPointsBuffer';
import { DataPoint } from './core/renderModel';
import { TimeChartPlugin } from './plugins';
import * as zoomOptions from './chartZoom/options';

type ColorSpecifier = ColorSpaceObject | ColorCommonInstance | string

export interface AxisZoomOptions {
export interface AxisZoomOptions extends zoomOptions.AxisOptions {
autoRange?: boolean;
}

export interface ResolvedAxisZoomOptions extends zoomOptions.ResolvedAxisOptions {
autoRange: boolean;
minDomain: number;
maxDomain: number;
minDomainExtent: number;
maxDomainExtent: number;
}

export interface ZoomOptions {
x?: Partial<AxisZoomOptions>;
y?: Partial<AxisZoomOptions>;
x?: AxisZoomOptions;
y?: AxisZoomOptions;
}

export interface ResolvedZoomOptions {
x?: AxisZoomOptions;
y?: AxisZoomOptions;
x?: ResolvedAxisZoomOptions;
y?: ResolvedAxisZoomOptions;
}

interface ScaleBase {
Expand Down
54 changes: 39 additions & 15 deletions src/plugins/chartZoom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ChartZoom, defaultAxisOptions, resolveOptions, } from "../chartZoom";
import { ResolvedOptions as ResolvedChartZoomOptions } from "../chartZoom/options";
import { ChartZoom } from "../chartZoom";
import core from "../core";
import { MinMax } from "../core/renderModel";
import { ResolvedZoomOptions, TimeChartPlugins, ZoomOptions } from "../options";
Expand Down Expand Up @@ -29,13 +28,8 @@ export class TimeChartZoom {
}

private registerZoom(chart: core<TimeChartPlugins>) {
if (this.options.x)
Object.setPrototypeOf(this.options.x, Object.assign(Object.create(defaults), { scale: chart.model.xScale }));
if (this.options.y)
Object.setPrototypeOf(this.options.y, Object.assign(Object.create(defaults), { scale: chart.model.yScale }));

const o = this.options as ResolvedZoomOptions & ResolvedChartZoomOptions;
const z = new ChartZoom(chart.contentBoxDetector.node, o);
const o = this.options;
const z = new ChartZoom(chart.el, o);
chart.model.updated.on(() => {
this.applyAutoRange(o.x, chart.model.xRange);
this.applyAutoRange(o.y, chart.model.yRange);
Expand All @@ -50,17 +44,47 @@ export class TimeChartZoom {
}
}

const defaults = Object.assign(Object.create(defaultAxisOptions) as typeof defaultAxisOptions, {
const defaults = {
autoRange: true,
} as const);
} as const;

export class TimeChartZoomPlugin implements TimeChartPlugin<TimeChartZoom> {
options: ResolvedZoomOptions;
constructor(o?: ZoomOptions) {
this.options = resolveOptions(defaults, o);
constructor(private options?: ZoomOptions) {
}

private resolveOptions(chart: core<TimeChartPlugins>): ResolvedZoomOptions {
const o = this.options ?? {};
return new Proxy(o, {
get: (target, prop) => {
switch (prop) {
case 'x':
case 'y':
const op = target[prop];
if (!op)
return op;
return new Proxy(op, {
get: (target, prop2) => {
if (prop2 === 'scale') {
switch (prop) {
case 'x':
return chart.model.xScale;
case 'y':
return chart.model.yScale;
}
}
return (target as any)[prop2] ?? (defaults as any)[prop2];
}
})
case 'eventElement':
return chart.contentBoxDetector.node;
default:
return (target as any)[prop];
}
}
}) as ResolvedZoomOptions;
}

apply(chart: core<TimeChartPlugins>) {
return new TimeChartZoom(chart, this.options);
return new TimeChartZoom(chart, this.resolveOptions(chart));
}
}

0 comments on commit f28eddc

Please sign in to comment.