aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts')
-rw-r--r--tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts150
1 files changed, 150 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts b/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts
new file mode 100644
index 0000000000..bf9f7b70e2
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts
@@ -0,0 +1,150 @@
+module Plottable {
+export class DragZoomLayer extends Components.SelectionBoxLayer {
+ private _dragInteraction: Interactions.Drag;
+ private _doubleClickInteraction: Interactions.DoubleClick;
+ private xDomainToRestore: any[];
+ private yDomainToRestore: any[];
+ private isZoomed = false;
+ private easeFn: (t: number) => number = d3.ease("cubic-in-out");
+ private _animationTime = 750;
+
+ /* Constructs a SelectionBoxLayer with an attached DragInteraction and ClickInteraction.
+ * On drag, it triggers an animated zoom into the box that was dragged.
+ * On double click, it zooms back out to the original view, before any zooming.
+ * The zoom animation uses an easing function (default d3.ease("cubic-in-out")) and is customizable.
+ * Usage: Construct the selection box layer and attach x and y scales, and then add the layer
+ * over the plot you are zooming on using a Component Group.
+ * TODO(danmane) - merge this into Plottable
+ */
+ constructor(xScale: QuantitativeScale<number | { valueOf(): number }>,
+ yScale: QuantitativeScale<number | { valueOf(): number }>) {
+ super();
+ this.xScale(xScale);
+ this.yScale(yScale);
+ this._dragInteraction = new Interactions.Drag();
+ this._dragInteraction.attachTo(this);
+ this._doubleClickInteraction = new Interactions.DoubleClick();
+ this._doubleClickInteraction.attachTo(this);
+ this.setupCallbacks();
+ }
+
+ private setupCallbacks() {
+ let dragging = false;
+ this._dragInteraction.onDragStart((startPoint: Point) => {
+ this.bounds({
+ topLeft: startPoint,
+ bottomRight: startPoint,
+ });
+ });
+ this._dragInteraction.onDrag((startPoint, endPoint) => {
+ this.bounds({topLeft: startPoint, bottomRight: endPoint});
+ this.boxVisible(true);
+ dragging = true;
+ });
+ this._dragInteraction.onDragEnd((startPoint, endPoint) => {
+ this.boxVisible(false);
+ this.bounds({topLeft: startPoint, bottomRight: endPoint});
+ if (dragging) {
+ this.zoom();
+ }
+ dragging = false;
+ });
+
+ this._doubleClickInteraction.onDoubleClick(this.unzoom.bind(this));
+ }
+
+ /* Set the time (in ms) over which the zoom will interpolate.
+ * 0 implies no interpolation. (ie zoom is instant)
+ */
+ public animationTime(): number;
+ public animationTime(animationTime: number): DragZoomLayer;
+ public animationTime(animationTime?: number): any {
+ if (animationTime == null) {
+ return this._animationTime;
+ }
+ if (animationTime < 0) {
+ throw new Error("animationTime cannot be negative");
+ }
+ this._animationTime = animationTime;
+ return this;
+ }
+
+ /* Set the easing function, which determines how the zoom interpolates over time. */
+ public ease(fn: (t: number) => number): DragZoomLayer {
+ if (typeof(fn) !== "function") {
+ throw new Error("ease function must be a function");
+ }
+ if (fn(0) !== 0 || fn(1) !== 1) {
+ Utils.Window.warn("Easing function does not maintain invariant f(0)==0 && f(1)==1. Bad behavior may result.");
+ }
+ this.easeFn = fn;
+ return this;
+ }
+
+ // Zoom into extent of the selection box bounds
+ private zoom() {
+ let x0: number = this.xExtent()[0].valueOf();
+ let x1: number = this.xExtent()[1].valueOf();
+ let y0: number = this.yExtent()[1].valueOf();
+ let y1: number = this.yExtent()[0].valueOf();
+
+ if (x0 === x1 || y0 === y1) {
+ return;
+ }
+
+ if (!this.isZoomed) {
+ this.isZoomed = true;
+ this.xDomainToRestore = this.xScale().domain();
+ this.yDomainToRestore = this.yScale().domain();
+ }
+ this.interpolateZoom(x0, x1, y0, y1);
+ }
+
+ // Restore the scales to their state before any zoom
+ private unzoom() {
+ if (!this.isZoomed) {
+ return;
+ }
+ this.isZoomed = false;
+ this.interpolateZoom(this.xDomainToRestore[0], this.xDomainToRestore[1],
+ this.yDomainToRestore[0], this.yDomainToRestore[1]);
+ }
+
+ // If we are zooming, disable interactions, to avoid contention
+ private isZooming(isZooming: boolean) {
+ this._dragInteraction.enabled(!isZooming);
+ this._doubleClickInteraction.enabled(!isZooming);
+ }
+
+ private interpolateZoom(x0f: number, x1f: number, y0f: number, y1f: number) {
+ let x0s: number = this.xScale().domain()[0].valueOf();
+ let x1s: number = this.xScale().domain()[1].valueOf();
+ let y0s: number = this.yScale().domain()[0].valueOf();
+ let y1s: number = this.yScale().domain()[1].valueOf();
+
+ // Copy a ref to the ease fn, so that changing ease wont affect zooms in progress
+ let ease = this.easeFn;
+ let interpolator = (a: number, b: number, p: number) => d3.interpolateNumber(a, b)(ease(p));
+
+ this.isZooming(true);
+ let start = Date.now();
+ let draw = () => {
+ let now = Date.now();
+ let passed = now - start;
+ let p = this._animationTime === 0 ? 1 : Math.min(1, passed / this._animationTime);
+ let x0 = interpolator(x0s, x0f, p);
+ let x1 = interpolator(x1s, x1f, p);
+ let y0 = interpolator(y0s, y0f, p);
+ let y1 = interpolator(y1s, y1f, p);
+ this.xScale().domain([x0, x1]);
+ this.yScale().domain([y0, y1]);
+ if (p < 1) {
+ Utils.DOM.requestAnimationFramePolyfill(draw);
+ } else {
+ this.isZooming(false);
+ }
+ };
+ draw();
+ }
+}
+}