Commit f0a534da authored by David Anderson's avatar David Anderson
Browse files

zoom and pan extents are isolated to the implementations and behaviors

parent 411a55f1
......@@ -29,7 +29,6 @@ export class BarChartDemoComponent {
};
let leftAxis: CategoricalAxis<string> = new CategoricalAxis<string>( 'left' );
let topAxis: ContinuousAxis = new ContinuousAxis( 'top' );
topAxis.shouldRenderBorder = false;
barChart.getX2 = ( dp: DataPoint ) => { return dp.data.testScore + dp.data.hitCount; };
me.chart = new ChartCanvas( me.drawspace.nativeElement );
......@@ -55,7 +54,6 @@ export class BarChartDemoComponent {
topAxis.explicitBounds =[ -5, 5 ];
topAxis.label = 'top axis';
leftAxis.canZoom = false;
me.chart.addChart( [ barChart ] );
me.chart.addAxis( [ leftAxis, topAxis ] );
......
......@@ -61,7 +61,6 @@ export class ScatterChartDemoComponent {
me.chart = new ChartCanvas( me.drawspace.nativeElement );
// me.chart.background = 'rgb(255,255,255)';
axis.forEach(a => {
a.registerChart(charts);
a.registerChart( selection );
......@@ -69,10 +68,6 @@ export class ScatterChartDemoComponent {
// a.fill = 'white';
});
// bottomAxis.panExtent = [ 0, 200 ];
// leftAxis.panExtent = [ 0, 200 ];
// rightAxis.panExtent = [ 0, 200 ];
topAxis.getValue = bottomAxis.getValue = ( dp ) => { return dp.data.x; };
rightAxis.getValue = leftAxis.getValue = ( dp ) => { return dp.data.y; };
......@@ -100,7 +95,7 @@ export class ScatterChartDemoComponent {
me.chart.addChart(charts);
me.chart.addChart(selection);
me.chart.addAxis( axis);
var behavior = new GetDataAtMouseEvent(
let getData = new GetDataAtMouseEvent(
'click',
{
chart: charts,
......@@ -116,10 +111,14 @@ export class ScatterChartDemoComponent {
selectionUpdate();
}
},
) ;
me.chart.addBehavior( behavior );
);
let zoom = new ZoomBehavior();
// zoom.limitZoom( axis, 0.5, 1.5 );
let pan = new PanBehavior();
// pan.limitPan(bottomAxis, pointCount * -0.25, pointCount * 1.25 );
me.chart.addBehavior( [new ZoomBehavior(), new PanBehavior()] );
me.chart.addBehavior( [getData, zoom, pan] );
charts[0].alpha = 1;
let dataList = randomData( pointCount );
......
......@@ -4,7 +4,7 @@ import { GistBehavior } from './gistBehavior.class';
import { LimiterType } from "../types/limiterType.type";
export type supportedEventType = 'dblclick' | 'click' | 'mousedown' | 'mousemove' | 'mouseout' | 'mouseover' | 'mouseup' | 'wheel';
export type supportedEventType = 'dblclick' | 'click' | 'mousedown' | 'mousemove' | 'mouseout' | 'mouseover' | 'mouseup';
/**
* Adds a mouse event to a gist chart that will fire when the given event name fires and will call a function with data at the point given
*
......@@ -23,7 +23,6 @@ export class GetDataAtMouseEvent extends GistBehavior {
_activate(): void {
const me = this;
me.eventHandlers.forEach( ( eh ) => {
console.log( eh );
makeList( eh.chart ).forEach( chart => {
if ( chart.isActive ) {
eh.onEvent( chart._implementation.getDataAtCoord( me.currentX, me.currentY ), chart, me.currentEvent );
......
......@@ -152,7 +152,12 @@ export abstract class GistBehavior {
* @abstract
* @memberof GistBehavior
*/
protected abstract _activate(): void;
protected _activate(): void {
const me = this;
me.eventHandlers.forEach(( eh ) => {
eh.onEvent( [], undefined, me.currentEvent );
} );
}
/**
......
......@@ -3,7 +3,8 @@ import { GistBehavior } from './gistBehavior.class';
import { DataPoint } from '../classes/datapoint.class';
import { EventHandler } from '../classes/eventHandler.class';
import { SupportedPanEvent } from "../types/supportedPanEvent.type";
import { LimiterType } from "../types/limiterType.type";
import { makeList } from '../utility/makeList.method';
import { BaseAxis } from '../configs/axis/baseAxis.class';
/* cSpell: words gcbb, togglepan */
......@@ -17,46 +18,45 @@ export class PanBehavior extends GistBehavior {
// private zoomConstant: number = 1.15;
// private zoomMethod: zoomMethod;
private mousedown: boolean = false;
private mousedown: boolean;
private lastPosition!: { x: number, y: number };
private maskingElement!: HTMLElement;
private userSelectValue: string | null = null;
private userSelectValue!: string | null;
private hasWarned: boolean | undefined;
private _mouseDown: boolean = false;
set mouseDown( val: boolean ) {
this._mouseDown = val;
this.mousedown = val;
if ( !val ) {
document.body.removeChild( this.maskingElement );
}
}
get mouseDown() { return this._mouseDown; }
constructor(
eventNames: SupportedPanEvent | SupportedPanEvent[] = [ 'mousedown', 'mouseup', 'mousemove' ],
eventNames: SupportedPanEvent | SupportedPanEvent[] = 'mousedrag' ,
waitTime: number = 15,
limiter: LimiterType = 'throttle' ) {
) {
let givenEvents = makeList( eventNames );
let dragIndex = givenEvents.indexOf( 'mousedrag' );
if ( dragIndex !== -1 ) {
givenEvents.splice( dragIndex, 1, 'mousedown' , 'mousemove' , 'mouseup' );
}
super(
eventNames,
givenEvents,
{
onEvent: ( dpl: DataPoint[], chart: BaseChart, mouseEvent: MouseEvent ) => { this._togglepan( dpl, chart, mouseEvent ); }
}as EventHandler
, waitTime,
limiter
'throttle'
);
this.mousedown = false;
}
_activate(): void {
const me = this;
me.eventHandlers.forEach(( eh ) => {
eh.onEvent( [], undefined, me.currentEvent );
} );
public limitPan( axis: BaseAxis, minValue: any, maxValue: any ) {
if ( !this.hasWarned ) {
window.console.warn( 'limitZoom is still in beta. There are known bugs, but the api should not change' );
this.hasWarned = true;
}
axis._implementation.panExtent = [minValue, maxValue];
}
_togglepan( dpl: DataPoint[], chart: BaseChart | undefined, mouseEvent: MouseEvent ): void {
if ( mouseEvent.type === 'mousedown' ) {
this.mousedown = true;
this.maskingElement = document.body.appendChild( document.createElement( 'div' ) );
this.maskingElement.style.position = 'absolute';
this.maskingElement.style.top = '0';
......@@ -64,7 +64,6 @@ export class PanBehavior extends GistBehavior {
this.maskingElement.style.left = '0';
this.maskingElement.style.right = '0';
this.maskingElement.style.zIndex = '16777271';
this.mouseDown = true;
this.userSelectValue = document.body.style.userSelect;
......@@ -75,7 +74,8 @@ export class PanBehavior extends GistBehavior {
this.lastPosition = { x: mouseEvent.clientX, y: mouseEvent.clientY }
this.maskingElement.addEventListener( 'mouseup', () => {
this.mouseDown = false;
document.body.removeChild( this.maskingElement );
this.mousedown = false;
document.body.style.userSelect = this.userSelectValue;
document.body.style.msUserSelect = this.userSelectValue;
document.body.style.webkitUserSelect = this.userSelectValue;
......@@ -86,7 +86,7 @@ export class PanBehavior extends GistBehavior {
this._pan( dpl, chart, event as MouseEvent);
} )
} )
} else if ( mouseEvent.type === 'wheel' ) {
} else if ( mouseEvent.type === 'wheel' || mouseEvent.type === 'dblclick' ) {
this._pan(dpl, chart, mouseEvent);
}
......@@ -117,20 +117,22 @@ export class PanBehavior extends GistBehavior {
xy = { x: -wheel.deltaX, y: -wheel.deltaY };
me.preventDefault = true;
break;
case 'dblclick':
const gcbb = me.canvas._panClickSpace;
if ( !( mouseEvent.clientX < gcbb.x0 || mouseEvent.clientX > gcbb.x1 ||
mouseEvent.clientY < gcbb.y0 || mouseEvent.clientY > gcbb.y1 ) ) {
const xa: number = ( gcbb.x1 + gcbb.x0 ) / 2;
const ya: number = ( gcbb.y1 + gcbb.y0 ) / 2;
xy = { x: xa - mouseEvent.clientX, y: ya - mouseEvent.clientY };
}
break;
// dblclick does not ever fire now that we have the drop over element
// case 'dblclick':
// const gcbb = me.canvas._panClickSpace;
// if ( !( mouseEvent.clientX < gcbb.x0 || mouseEvent.clientX > gcbb.x1 ||
// mouseEvent.clientY < gcbb.y0 || mouseEvent.clientY > gcbb.y1 ) ) {
// const xa: number = ( gcbb.x1 + gcbb.x0 ) / 2;
// const ya: number = ( gcbb.y1 + gcbb.y0 ) / 2;
// xy = { x: xa - mouseEvent.clientX, y: ya - mouseEvent.clientY };
// }
// break;
}
if ( xy ) {
me.canvas.pan( xy.x, xy.y );
me.canvas.pan( xy.x, xy.y);
}
}
}
\ No newline at end of file
......@@ -2,9 +2,10 @@ import { BaseChart } from '../configs/charts/baseChart.class';
import { GistBehavior } from './gistBehavior.class';
import { DataPoint } from '../classes/datapoint.class';
import { EventHandler } from '../classes/eventHandler.class';
import { SupportedZoomEvent } from "../types/supportedZoomEvent.type";
import { ZoomMethod } from "../types/zoomMethod.type";
import { LimiterType } from "../types/limiterType.type";
import { BaseAxis } from '../configs/axis/baseAxis.class';
import { makeList } from '../utility/makeList.method';
......@@ -18,31 +19,41 @@ export class ZoomBehavior extends GistBehavior {
private zoomConstant: number = 1.15;
private zoomMethod: ZoomMethod;
private hasWarned: boolean = false;
/**
*Creates an instance of ZoomBehavior.
* @param {number} [zoomConstant=1.15] controls how strong the zoom step is
* @param {ZoomMethod} [zoomMethod='multiplication'] multiplication zoom steps are based on the current zoom level resulting in a smoother movement. addition will jump by steps
* @param {number} [waitTime=50] how long in milliseconds to throttle the event
* @memberof ZoomBehavior
*/
constructor(
eventNames: SupportedZoomEvent | SupportedZoomEvent[] = [ 'wheel', 'dblclick' ],
waitTime: number = 50,
limiter: LimiterType = 'throttle',
zoomConstant: number = 1.15,
zoomMethod: ZoomMethod = 'multiplication' ) {
zoomMethod: ZoomMethod = 'multiplication',
waitTime: number = 50,
) {
super(
eventNames,
'wheel',
{
onEvent: ( dpl: DataPoint[], chart: BaseChart, mouseEvent: MouseEvent ) => {
this._zoom( dpl, chart, mouseEvent );
}
} as EventHandler
, waitTime
, limiter
, 'throttle'
);
this.zoomConstant = zoomConstant;
this.zoomMethod = zoomMethod;
}
_activate(): void {
const me = this;
me.eventHandlers.forEach( ( eh ) => {
eh.onEvent( [], undefined, me.currentEvent );
public limitZoom( targets: BaseAxis | Array<BaseAxis>, minZoomLevel: number, maxZoomLevel: number ) {
if ( !this.hasWarned ) {
window.console.warn( 'limitZoom is still in beta. There are known bugs, but the api should not change' );
this.hasWarned = true;
}
makeList( targets ).forEach( axis => {
axis._implementation.scaleExtent = [ minZoomLevel, maxZoomLevel ];
} );
}
......
......@@ -328,9 +328,7 @@ export class ChartCanvas {
if ( zoomLevel ) {
me._currentZoomState = zoomLevel;
me.axisList.forEach( ( axis: BaseAxis ) => {
if ( axis.canZoom ) {
axis.zoom( zoomLevel );
}
axis.zoom( zoomLevel );
} );
me.beginRender();
}
......
......@@ -24,6 +24,10 @@ export abstract class BaseAxis<T = any> {
*/
public readonly id: number;
constructor() {
this.id = uID();
}
/**
* The debug logger. To enable debugger, the isEnabled property must be set to true. It is also advised to set logPrefix to distinguish between different debuggers
*
......@@ -115,6 +119,12 @@ export abstract class BaseAxis<T = any> {
*/
public tickLabelPad: number = 3;
/**
* When set to true, ticks will extend across the charts.
*
* @type {boolean}
* @memberof BaseAxis
*/
public shouldExtendTicksAcrossChart: boolean = false;
/**
* Should the ticks and border be drawn?
......@@ -123,6 +133,13 @@ export abstract class BaseAxis<T = any> {
* @memberof BaseAxis
*/
public shouldRenderTicks: boolean = true;
/**
* Should the border be drawn?
*
* @type {boolean}
* @memberof BaseAxis
*/
public shouldRenderBorder: boolean = true;
/**
......@@ -162,11 +179,6 @@ export abstract class BaseAxis<T = any> {
return value.toString();
}
constructor() {
this.id = uID();
}
/**
* Tells the axis how to get the value of a charts data point
*
......@@ -178,35 +190,9 @@ export abstract class BaseAxis<T = any> {
throw 'GetValue must defined on the axis';
}
// This will documented where it is implemented
abstract valueToWidth( value: T );
/**
* The first number is the minimum zoom level. The second is the max zoom level.
* Defaults to 0 Min Zoom and Infinity max zoom.
*
* @type {number[]}
* @memberof BaseAxis
*/
public scaleExtent: number[] = [ 0, Infinity ];
/**
* This is the value in the scale that the panning extent will allow the user to
* pan.
*
* @type {number[]}
* @memberof BaseAxis
*/
public panExtent: number[] = [ -Infinity, Infinity ];
/**
* Whether is Axis can zoom and pan.
* Default is true.
* @type {boolean}
* @memberof BaseAxis
*/
public canZoom: boolean = true;
/**
* Resets the axis the the default zoom and offset, 1 and 0 respectively.
*
......@@ -228,8 +214,8 @@ export abstract class BaseAxis<T = any> {
*/
public zoom( zoom: number = 1.14, offset: number = 0 ) {
let multiplier: number = 1;
if ( zoom !== this._implementation.currentZoomLevel ) multiplier = zoom / this._implementation.currentZoomLevel;
this._implementation.currentZoomLevel = Math.max( Math.min( zoom, this.scaleExtent[ 1 ] ), this.scaleExtent[ 0 ] );
if ( zoom !== this._implementation.currentZoomLevel ) { multiplier = zoom / this._implementation.currentZoomLevel; }
this._implementation.currentZoomLevel = Math.max( Math.min( zoom, this._implementation.scaleExtent[ 1 ] ), this._implementation.scaleExtent[ 0 ] );
this._implementation.offset *= multiplier;
this._implementation.offset += offset;
Object.keys( this._implementation.charts ).forEach( key => {
......
......@@ -11,14 +11,32 @@ export type AxisType = 'Continuous' | 'Temporal' | 'Categorical' | 'Log';
export abstract class BaseAxisImplementation {
protected axisType!: AxisType;
protected axis!: BaseAxis;
protected chartHeight: number = 0;
protected chartWidth: number = 0;
protected axis!: BaseAxis;
protected strWidth: number = 1;
protected margins: TBLR<number> | undefined;
readonly charts: { [ key: number ]: BaseChart<DataPoint> } = {};
/**
* The first number is the minimum zoom level. The second is the max zoom level.
* Defaults to 0 Min Zoom and Infinity max zoom.
*
* @type {number[]}
* @memberof BaseAxis
*/
public scaleExtent: number[] = [ 0, Infinity ];
/**
* This is the value in the scale that the panning extent will allow the user to
* pan.
*
* @type {number[]}
* @memberof BaseAxis
*/
public panExtent: number[] = [ -Infinity, Infinity ];
/**
* How this axis is oriented in respect to the drawn charts. Once set it should not be changed
*
......@@ -412,8 +430,8 @@ export abstract class BaseAxisImplementation {
let zoomDomain = me.scale.domain();
me.scale.domain( newDomain );
me.scale.range( zoomRange );
let minExtent = Math.min( me.scale( me.axis.panExtent[ 0 ] ), me.scale( me.axis.panExtent[ 1 ] ) );
let maxExtent = Math.max( me.scale( me.axis.panExtent[ 0 ] ), me.scale( me.axis.panExtent[ 1 ] ) );
let minExtent = Math.min( me.scale( me.axis._implementation.panExtent[ 0 ] ), me.scale( me.axis._implementation.panExtent[ 1 ] ) );
let maxExtent = Math.max( me.scale( me.axis._implementation.panExtent[ 0 ] ), me.scale( me.axis._implementation.panExtent[ 1 ] ) );
me.scale.range( newRange );
me.scale.domain( zoomDomain );
maxExtent += ( rangeWidth < 0 ) ? rangeWidth : -rangeWidth;
......@@ -439,8 +457,8 @@ export abstract class BaseAxisImplementation {
let domain: number[] = [ me.scale.invert( startPos / me.currentZoomLevel ), me.scale.invert( endPos / me.currentZoomLevel ) ];
// Make sure the boundaries are correct if there is a pan extent
if ( Math.abs( domain[ 0 ] - domain[ 1 ] ) > Math.abs( me.axis.panExtent[ 0 ] - me.axis.panExtent[ 1 ] ) ) {
domain = me.axis.panExtent;
if ( Math.abs( domain[ 0 ] - domain[ 1 ] ) > Math.abs( me.axis._implementation.panExtent[ 0 ] - me.axis._implementation.panExtent[ 1 ] ) ) {
domain = me.axis._implementation.panExtent;
me.currentZoomLevel = Math.abs( newDomain[ 0 ] - newDomain[ 1 ] ) / Math.abs( domain[ 0 ] - domain[ 1 ] );
}
me.drawScale.domain( domain );
......
export type SupportedPanEvent = 'wheel' | 'dblclick' | 'mousedown' | 'mouseup' | 'mousemove';
\ No newline at end of file
export type SupportedPanEvent = 'wheel' | 'mousedrag';
\ No newline at end of file
export type SupportedZoomEvent = 'wheel' | 'dblclick';
import { EventHandler } from '../classes/eventHandler.class';
import { GistBehavior } from './gistBehavior.class';
import { LimiterType } from "../types/limiterType.type";
export declare type supportedEventType = 'dblclick' | 'click' | 'mousedown' | 'mousemove' | 'mouseout' | 'mouseover' | 'mouseup' | 'wheel';
export declare type supportedEventType = 'dblclick' | 'click' | 'mousedown' | 'mousemove' | 'mouseout' | 'mouseover' | 'mouseup';
/**
* Adds a mouse event to a gist chart that will fire when the given event name fires and will call a function with data at the point given
*
......
......@@ -109,7 +109,7 @@ export declare abstract class GistBehavior {
* @abstract
* @memberof GistBehavior
*/
protected abstract _activate(): void;
protected _activate(): void;
/**
* Allows the implemented behaviors to do any clean up that they need
*
......
......@@ -2,7 +2,7 @@ import { BaseChart } from '../configs/charts/baseChart.class';
import { GistBehavior } from './gistBehavior.class';
import { DataPoint } from '../classes/datapoint.class';
import { SupportedPanEvent } from "../types/supportedPanEvent.type";
import { LimiterType } from "../types/limiterType.type";
import { BaseAxis } from '../configs/axis/baseAxis.class';
/**
* Adds a mouse event to a gist chart that will fire when the given event name fires and will call a function with data at the point given
*
......@@ -14,10 +14,9 @@ export declare class PanBehavior extends GistBehavior {
private lastPosition;
private maskingElement;
private userSelectValue;
private _mouseDown;
mouseDown: boolean;
constructor(eventNames?: SupportedPanEvent | SupportedPanEvent[], waitTime?: number, limiter?: LimiterType);
_activate(): void;
private hasWarned;
constructor(eventNames?: SupportedPanEvent | SupportedPanEvent[], waitTime?: number);
limitPan(axis: BaseAxis, minValue: any, maxValue: any): void;
_togglepan(dpl: DataPoint[], chart: BaseChart | undefined, mouseEvent: MouseEvent): void;
_pan(dpl: DataPoint[], chart: BaseChart | undefined, mouseEvent: MouseEvent): void;
}
import { BaseChart } from '../configs/charts/baseChart.class';
import { GistBehavior } from './gistBehavior.class';
import { DataPoint } from '../classes/datapoint.class';
import { SupportedZoomEvent } from "../types/supportedZoomEvent.type";
import { ZoomMethod } from "../types/zoomMethod.type";
import { LimiterType } from "../types/limiterType.type";
import { BaseAxis } from '../configs/axis/baseAxis.class';
/**
* Adds a mouse event to a gist chart that will fire when the given event name fires and will call a function with data at the point given
*
......@@ -13,7 +13,8 @@ import { LimiterType } from "../types/limiterType.type";
export declare class ZoomBehavior extends GistBehavior {
private zoomConstant;
private zoomMethod;
constructor(eventNames?: SupportedZoomEvent | SupportedZoomEvent[], waitTime?: number, limiter?: LimiterType, zoomConstant?: number, zoomMethod?: ZoomMethod);
_activate(): void;
private hasWarned;
constructor(zoomConstant?: number, zoomMethod?: ZoomMethod, waitTime?: number, limiter?: LimiterType);
limitZoom(targets: BaseAxis | Array<BaseAxis>, minZoomLevel: number, maxZoomLevel: number): void;
_zoom(dpl: DataPoint[], chart: BaseChart | undefined, mouseEvent: MouseEvent): void;
}
......@@ -21,6 +21,7 @@ export declare abstract class BaseAxis<T = any> {
* @memberof BaseAxis
*/
readonly id: number;
constructor();
/**
* The debug logger. To enable debugger, the isEnabled property must be set to true. It is also advised to set logPrefix to distinguish between different debuggers
*
......@@ -98,6 +99,12 @@ export declare abstract class BaseAxis<T = any> {
* @memberof CategoricalAxis
*/
tickLabelPad: number;
/**
* When set to true, ticks will extend across the charts.
*
* @type {boolean}
* @memberof BaseAxis
*/
shouldExtendTicksAcrossChart: boolean;
/**
* Should the ticks and border be drawn?
......@@ -106,6 +113,12 @@ export declare abstract class BaseAxis<T = any> {
* @memberof BaseAxis
*/
shouldRenderTicks: boolean;
/**
* Should the border be drawn?
*
* @type {boolean}
* @memberof BaseAxis
*/
shouldRenderBorder: boolean;
/**
* Registers given charts with this axis, and sets this x/y axis of the chart to this.
......@@ -126,7 +139,6 @@ export declare abstract class BaseAxis<T = any> {
* @memberof BaseAxis
*/
valueToString(value: T, tickIndex?: number, tickArray?: T[]): string;
constructor();
/**
* Tells the axis how to get the value of a charts data point
*
......@@ -136,29 +148,6 @@ export declare abstract class BaseAxis<T = any> {
*/
getValue(dp: DataPoint): T;
abstract valueToWidth(value: T): any;
/**
* The first number is the minimum zoom level. The second is the max zoom level.
* Defaults to 0 Min Zoom and Infinity max zoom.
*