@future-grid/fgp-graph
Version:
fgp-graph is a chart lib based on Dygraphs
494 lines (414 loc) • 19.1 kB
text/typescript
import utils from './utils';
export class GraphInteractions {
private panEnable!: boolean;
private mouseTimer!: number;
private scrollEnable: boolean;
private scrollTimer!: number;
private zoomTimer!: number;
private scaleTimer!: number;
private preDatewindow!: Array<any>;
private needRefresh!: boolean;
private yAxisRangeChanged!: boolean;
constructor(public callback: any, public dateRange?: Array<number>) {
this.panEnable = false;
this.scrollEnable = false;
}
private LOG_SCALE = 10;
private LN_TEN = Math.log(this.LOG_SCALE);
private log10 = (x: number) => {
return Math.log(x) / this.LN_TEN;
};
private pageX = (e: MouseEvent) => {
return !e.pageX || e.pageX < 0 ? 0 : e.pageX;
};
private pageY = (e: MouseEvent) => {
return !e.pageY || e.pageY < 0 ? 0 : e.pageY;
};
private dragGetX_ = (e: MouseEvent, context: any) => {
return this.pageX(e) - context.px;
};
private dragGetY_ = (e: MouseEvent, context: any) => {
return this.pageY(e) - context.py;
};
private endPan = (event: MouseEvent, g: any, context: any) => {
context.dragEndX = this.dragGetX_(event, context);
context.dragEndY = this.dragGetY_(event, context);
let regionWidth = Math.abs(context.dragEndX - context.dragStartX);
let regionHeight = Math.abs(context.dragEndY - context.dragStartY);
if (regionWidth < 2 && regionHeight < 2 &&
g.lastx_ !== undefined && g.lastx_ != -1) {
this.treatMouseOpAsClick(g, event, context);
}
context.regionWidth = regionWidth;
context.regionHeight = regionHeight;
};
private startPan = (event: MouseEvent, g: any, context: any) => {
let i, axis;
context.isPanning = true;
let xRange = g.xAxisRange();
if (g.getOptionForAxis("logscale", "x")) {
context.initialLeftmostDate = this.log10(xRange[0]);
context.dateRange = this.log10(xRange[1]) - this.log10(xRange[0]);
} else {
context.initialLeftmostDate = xRange[0];
context.dateRange = xRange[1] - xRange[0];
}
context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
if (g.getNumericOption("panEdgeFraction")) {
let maxXPixelsToDraw = g.width_ * g.getNumericOption("panEdgeFraction");
let xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
let boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
let boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
let boundedLeftDate = g.toDataXCoord(boundedLeftX);
let boundedRightDate = g.toDataXCoord(boundedRightX);
context.boundedDates = [boundedLeftDate, boundedRightDate];
let boundedValues = [];
let maxYPixelsToDraw = g.height_ * g.getNumericOption("panEdgeFraction");
for (i = 0; i < g.axes_.length; i++) {
axis = g.axes_[i];
let yExtremes = axis.extremeRange;
let boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
let boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
let boundedTopValue = g.toDataYCoord(boundedTopY, i);
let boundedBottomValue = g.toDataYCoord(boundedBottomY, i);
boundedValues[i] = [boundedTopValue, boundedBottomValue];
}
context.boundedValues = boundedValues;
}
// Record the range of each y-axis at the start of the drag.
// If any axis has a valueRange, then we want a 2D pan.
// We can't store data directly in g.axes_, because it does not belong to us
// and could change out from under us during a pan (say if there's a data
// update).
context.is2DPan = false;
context.axes = [];
for (i = 0; i < g.axes_.length; i++) {
axis = g.axes_[i];
let axis_data = { initialTopValue: 0, dragValueRange: 0, unitsPerPixel: 0 };
let yRange = g.yAxisRange(i);
// TODO(konigsberg): These values should be in |context|.
// In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
let logscale = g.attributes_.getForAxis("logscale", i);
if (logscale) {
axis_data.initialTopValue = this.log10(yRange[1]);
axis_data.dragValueRange = this.log10(yRange[1]) - this.log10(yRange[0]);
} else {
axis_data.initialTopValue = yRange[1];
axis_data.dragValueRange = yRange[1] - yRange[0];
}
axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1);
context.axes.push(axis_data);
// While calculating axes, set 2dpan.
if (axis.valueRange) context.is2DPan = true;
}
};
private treatMouseOpAsClick = (g: any, event: MouseEvent, context: any) => {
let clickCallback = g.getFunctionOption('clickCallback');
let pointClickCallback = g.getFunctionOption('pointClickCallback');
let selectedPoint = null;
// Find out if the click occurs on a point.
let closestIdx = -1;
let closestDistance = Number.MAX_VALUE;
// check if the click was on a particular point.
for (let i = 0; i < g.selPoints_.length; i++) {
let p = g.selPoints_[i];
let distance = Math.pow(p.canvasx - context.dragEndX, 2) +
Math.pow(p.canvasy - context.dragEndY, 2);
if (!isNaN(distance) &&
(closestIdx == -1 || distance < closestDistance)) {
closestDistance = distance;
closestIdx = i;
}
}
// Allow any click within two pixels of the dot.
let radius = g.getNumericOption('highlightCircleSize') + 2;
if (closestDistance <= radius * radius) {
selectedPoint = g.selPoints_[closestIdx];
}
if (selectedPoint) {
let e: any = {
cancelable: true,
point: selectedPoint,
canvasx: context.dragEndX,
canvasy: context.dragEndY
};
let defaultPrevented = g.cascadeEvents_('pointClick', e);
if (defaultPrevented) {
// Note: this also prevents click / clickCallback from firing.
return;
}
if (pointClickCallback) {
pointClickCallback.call(g, event, selectedPoint);
}
}
let e: any = {
cancelable: true,
xval: g.lastx_, // closest point by x value
pts: g.selPoints_,
canvasx: context.dragEndX,
canvasy: context.dragEndY
};
if (!g.cascadeEvents_('click', e)) {
if (clickCallback) {
// TODO(danvk): pass along more info about the points, e.g. 'x'
clickCallback.call(g, event, g.lastx_, g.selPoints_);
}
}
}
private offsetToPercentage = (g: any, offsetX: number, offsetY: number) => {
// This is calculating the pixel offset of the leftmost date.
let xOffset = g.toDomCoords(g.xAxisRange()[0], null)[0];
let yar0 = g.yAxisRange(0);
// This is calculating the pixel of the higest value. (Top pixel)
let yOffset = g.toDomCoords(null, yar0[1])[1];
// x y w and h are relative to the corner of the drawing area,
// so that the upper corner of the drawing area is (0, 0).
let x = offsetX - xOffset;
let y = offsetY - yOffset;
// This is computing the rightmost pixel, effectively defining the
// width.
let w = g.toDomCoords(g.xAxisRange()[1], null)[0] - xOffset;
// This is computing the lowest pixel, effectively defining the height.
let h = g.toDomCoords(null, yar0[0])[1] - yOffset;
// Percentage from the left.
let xPct = w == 0 ? 0 : (x / w);
// Percentage from the top.
let yPct = h == 0 ? 0 : (y / h);
// The (1-) part below changes it from "% distance down from the top"
// to "% distance up from the bottom".
return [xPct, (1 - yPct)];
};
private pan = (event: MouseEvent, g: any, context: any, side: string) => {
context.dragEndX = this.dragGetX_(event, context);
context.dragEndY = this.dragGetY_(event, context);
let minDate = context.initialLeftmostDate - (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
if (context.boundedDates) {
minDate = Math.max(minDate, context.boundedDates[0]);
}
let maxDate = minDate + context.dateRange;
if (context.boundedDates) {
if (maxDate > context.boundedDates[1]) {
// Adjust minDate, and recompute maxDate.
minDate = minDate - (maxDate - context.boundedDates[1]);
maxDate = minDate + context.dateRange;
}
}
// y-axis scaling is automatic unless this is a full 2D pan.
if (context.is2DPan) {
let pixelsDragged = context.dragEndY - context.dragStartY;
// Adjust each axis appropriately.
if (side && ("r" == side || "l" == side)) {
let index = (side == 'l' ? 0 : 1);
let axis = g.axes_[index];
let axis_data = context.axes[index];
let unitsDragged = pixelsDragged * axis_data.unitsPerPixel;
let boundedValue = context.boundedValues ? context.boundedValues[index] : null;
// In log scale, maxValue and minValue are the logs of those values.
let maxValue = axis_data.initialTopValue + unitsDragged;
if (boundedValue) {
maxValue = Math.min(maxValue, boundedValue[index]);
}
let minValue = maxValue - axis_data.dragValueRange;
if (boundedValue) {
if (minValue < boundedValue[0]) {
// Adjust maxValue, and recompute minValue.
maxValue = maxValue - (minValue - boundedValue[0]);
minValue = maxValue - axis_data.dragValueRange;
}
}
if (g.attributes_.getForAxis("logscale", index)) {
axis.valueRange = [Math.pow(10, minValue), Math.pow(10, maxValue)];
axis.valueWindow = [Math.pow(10, minValue), Math.pow(10, maxValue)];
axis.extremeRange = [Math.pow(10, minValue), Math.pow(10, maxValue)];
} else {
axis.valueRange = [minValue, maxValue];
axis.valueWindow = [minValue, maxValue];
axis.extremeRange = [minValue, maxValue];
}
g.drawGraph_(true);
} else {
//
let zoomRange = this.dateRange;
if (zoomRange && (minDate < zoomRange[0] || maxDate > zoomRange[1])) {
// console.info("return~~~~", new Date(minDate), new Date(zoomRange[0]), new Date(maxDate), new Date(zoomRange[1]));
return;
}
if (g.getOptionForAxis("logscale", "x")) {
g.dateWindow_ = [new Date(Math.pow(10, minDate)), new Date(Math.pow(10, maxDate))];
} else {
g.dateWindow_ = [new Date(minDate), new Date(maxDate)];
}
g.drawGraph_(false);
}
}
};
private adjustAxis = (axis: any, zoomInPercentage: number, bias: any) => {
let delta = axis[1] - axis[0];
let increment = delta * zoomInPercentage;
let foo = [increment * bias, increment * (1 - bias)];
return [axis[0] + foo[0], axis[1] - foo[1]];
};
private zoom = (g: any, zoomInPercentage: number, xBias: any, yBias: any, direction: string, side: string, e?: Event) => {
xBias = xBias || 0.5;
yBias = yBias || 0.5;
let yAxes = g.axes_;
let newYAxes = [];
for (let i = 0; i < g.numAxes(); i++) {
if(!yAxes[i].valueRange){
yAxes[i].valueRange = [yAxes[i].minyval, yAxes[i].maxyval];
}
newYAxes[i] = this.adjustAxis(yAxes[i].valueRange, zoomInPercentage, yBias);
}
if ('v' == direction) {
if (this.zoomTimer) {
window.clearTimeout(this.zoomTimer);
}
if ('l' == side) {
yAxes[0]['valueRange'] = newYAxes[0];
yAxes[0]['valueWindow'] = newYAxes[0];
yAxes[0]['extremeRange'] = newYAxes[0];
} else if ('r' == side && g.numAxes() == 2) {
yAxes[1]['valueRange'] = newYAxes[1];
yAxes[1]['valueWindow'] = newYAxes[1];
yAxes[1]['extremeRange'] = newYAxes[1];
}
this.zoomTimer = window.setTimeout(()=>{
this.callback(e, g.yAxisRanges(), false);
}, 500);
g.drawGraph_(false);
} else {
if (this.scrollTimer) {
window.clearTimeout(this.scrollTimer);
}
let ranges = g.dateWindow_;
if (ranges[0] instanceof Date) {
ranges[0] = ranges[0].getTime();
ranges[1] = ranges[1].getTime();
}
let newZoomRange = this.adjustAxis(ranges, zoomInPercentage, xBias);
// do not bigger than range data
let zoomRange = this.dateRange;
this.scrollTimer = window.setTimeout(() => {
this.callback(e, g.yAxisRanges(), true);
}, 500);
if (zoomRange && (newZoomRange[0] < zoomRange[0] && newZoomRange[1] > zoomRange[1])) {
return;
} else if (newZoomRange[0] >= newZoomRange[1]) {
return;
} else if (zoomRange && (newZoomRange[0] <= zoomRange[0] && newZoomRange[1] < zoomRange[1])) {
g.updateOptions({
dateWindow: [zoomRange[0], newZoomRange[1]]
});
} else if (zoomRange && (newZoomRange[0] > zoomRange[0] && newZoomRange[1] >= zoomRange[1])) {
g.updateOptions({
dateWindow: [newZoomRange[0], zoomRange[1]]
});
} else {
g.updateOptions({
dateWindow: [newZoomRange[0], newZoomRange[1]]
});
}
}
};
public mouseUp = (e: MouseEvent, g: any, context: any) => {
// call callback on windows mouseup
// console.debug("mouse up");
let currentDatewindow = g.dateWindow_;
if (currentDatewindow[0] instanceof Date) {
currentDatewindow[0] = currentDatewindow[0].getTime();
currentDatewindow[1] = currentDatewindow[1].getTime();
}
context.isPanning = false;
// Dygraph.endPan(event, g, context);
this.endPan(e, g, context);
// call upadte this.panEnable = false;
if (this.panEnable && this.needRefresh && (this.preDatewindow[0] != currentDatewindow[0] || this.preDatewindow[1] != currentDatewindow[1])) {
this.callback(e, g.yAxisRanges(), true);
this.panEnable = false;
} else if (this.yAxisRangeChanged) {
this.callback(e, g.yAxisRanges(), false);
this.panEnable = false;
}
};
public mouseDown = (e: MouseEvent, g: any, context: any) => {
this.preDatewindow = g.dateWindow_;
if (this.preDatewindow[0] instanceof Date) {
this.preDatewindow[0] = this.preDatewindow[0].getTime();
this.preDatewindow[1] = this.preDatewindow[1].getTime();
}
this.panEnable = true;
context.initializeMouseDown(event, g, context);
this.startPan(e, g, context);
// console.debug("mouse down", context);
};
public mouseMove = (e: MouseEvent, g: any, context: any) => {
if(this.scaleTimer){
window.clearTimeout(this.scaleTimer);
}
if (this.panEnable && context.isPanning) {
if (e.offsetX <= (g.plotter_.area.x)) {
this.needRefresh = false;
this.yAxisRangeChanged = true;
this.pan(e, g, context, 'l');
this.callback(e, g.yAxisRanges(), false);
} else if (e.offsetX >= (g.plotter_.area.x + g.plotter_.area.w)) {
this.needRefresh = false;
this.yAxisRangeChanged = true;
this.pan(e, g, context, 'r');
this.callback(e, g.yAxisRanges(), false);
} else {
this.needRefresh = true;
this.pan(e, g, context, 'h');
}
}
};
public mouseOut = (e: MouseEvent, g: any, context: any) => {
// console.debug("mouse out");
if (this.mouseTimer) {
window.clearTimeout(this.mouseTimer);
}
this.scrollEnable = false;
};
public mouseScroll = (e: any, g: any, context: any) => {
if (this.scrollEnable) {
//
let normal;
if (e instanceof WheelEvent) {
normal = e.detail ? e.detail * -1 : e.deltaY / 40;
} else {
normal = e.detail ? e.detail * -1 : e.wheelDelta / 40;
}
// For me the normalized value shows 0.075 for one click. If I took
// that verbatim, it would be a 7.5%.
let percentage = normal / 50;
if (!(e.offsetX && e.offsetY)) {
e.offsetX = e.layerX - e.target.offsetLeft;
e.offsetY = e.layerY - e.target.offsetTop;
}
let percentages = this.offsetToPercentage(g, e.offsetX, e.offsetY);
let xPct = percentages[0];
let yPct = percentages[1];
//
if (e.offsetX <= (g.plotter_.area.x)) {
// left zoom
this.zoom(g, percentage, xPct, yPct, 'v', 'l');
} else if (e.offsetX >= (g.plotter_.area.x + g.plotter_.area.w)) {
// right zoom
this.zoom(g, percentage, xPct, yPct, 'v', 'r');
} else {
// middle zoom
this.zoom(g, percentage, xPct, yPct, 'h', 'm');
}
utils.cancelEvent(e);
}
};
public mouseEnter = (e: MouseEvent, g: any, context: any) => {
if (this.mouseTimer) {
window.clearTimeout(this.mouseTimer);
}
this.mouseTimer = window.setTimeout(() => {
this.scrollEnable = true;
// console.debug("enable scroll zooming~");
}, 1000);
}
}