Skip to content

Commit

Permalink
Create histogram fob adapter component and use with LinkedTimeFobCont…
Browse files Browse the repository at this point in the history
…rollerComponent (tensorflow#5628)

This is a refactoring of the logic used for dragging the linked time fob in the Histogram Card. The previous implementation did not allow for the use cases needed in the Scalar Card. Our goal is to pull out the logic that will be histogram specific into a class that is generalizable. The CardFobAdapter interface created in tensorflow#5623 is the generalization that will be implemented in both the Histogram and Scalar cards to allow for different uses of the draggable fob.
  • Loading branch information
JamesHollyer authored and dna2github committed May 1, 2023
1 parent 0e27c97 commit e3df848
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 181 deletions.
3 changes: 3 additions & 0 deletions tensorboard/webapp/widgets/histogram/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ tf_ng_module(
name = "histogram",
srcs = [
"histogram_component.ts",
"histogram_linked_time_fob_controller.ts",
"histogram_module.ts",
],
assets = [
Expand All @@ -36,6 +37,7 @@ tf_ts_library(
name = "histogram_test",
testonly = True,
srcs = [
"histogram_linked_time_fob_test.ts",
"histogram_test.ts",
"histogram_util_test.ts",
],
Expand All @@ -44,6 +46,7 @@ tf_ts_library(
":types",
"//tensorboard/webapp/angular:expect_angular_core_testing",
"//tensorboard/webapp/angular:expect_angular_platform_browser_animations",
"//tensorboard/webapp/third_party:d3",
"//tensorboard/webapp/widgets/intersection_observer:intersection_observer_testing",
"//tensorboard/webapp/widgets/linked_time_fob",
"//tensorboard/webapp/widgets/linked_time_fob:types",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,12 @@
</svg>
<!-- Disable the feature when in non-offset and non-step mode. -->
<ng-container *ngIf="isLinkedTimeEnabled(linkedTime)">
<linked-time-fob-controller
[axisDirection]="axisDirection"
<histogram-linked-time-fob-controller
[linkedTime]="linkedTime"
[steps]="getSteps()"
[temporalScale]="scales.temporalScale"
(onSelectTimeChanged)="onSelectTimeChanged.emit($event)"
></linked-time-fob-controller>
></histogram-linked-time-fob-controller>
</ng-container>
</div>
<svg #content class="content">
Expand Down
5 changes: 1 addition & 4 deletions tensorboard/webapp/widgets/histogram/histogram_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {fromEvent, Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import * as d3 from '../../third_party/d3';
import {HCLColor} from '../../third_party/d3';
import {AxisDirection} from '../linked_time_fob/linked_time_fob_controller_component';
import {LinkedTime} from '../linked_time_fob/linked_time_types';
import {
Bin,
Expand All @@ -41,7 +40,7 @@ import {

type BinScale = d3.ScaleLinear<number, number>;
type CountScale = d3.ScaleLinear<number, number>;
type TemporalScale =
export type TemporalScale =
| d3.ScaleLinear<number, number>
| d3.ScaleTime<number, number>;
type D3ColorScale = d3.ScaleLinear<HCLColor, string>;
Expand Down Expand Up @@ -102,8 +101,6 @@ export class HistogramComponent implements AfterViewInit, OnChanges, OnDestroy {

@Output() onSelectTimeChanged = new EventEmitter<LinkedTime>();

readonly axisDirection = AxisDirection.VERTICAL;

readonly HistogramMode = HistogramMode;
readonly TimeProperty = TimeProperty;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {Component, EventEmitter, Input, Output} from '@angular/core';
import {AxisDirection} from '../linked_time_fob/linked_time_fob_controller_component';
import {FobCardAdapter, LinkedTime} from '../linked_time_fob/linked_time_types';
import {TemporalScale} from './histogram_component';

@Component({
selector: 'histogram-linked-time-fob-controller',
template: `
<linked-time-fob-controller
[axisDirection]="axisDirection"
[linkedTime]="linkedTime"
[cardAdapter]="this"
(onSelectTimeChanged)="onSelectTimeChanged.emit($event)"
></linked-time-fob-controller>
`,
})
export class HistogramLinkedTimeFobController implements FobCardAdapter {
@Input() steps!: number[];
@Input() linkedTime!: LinkedTime;
@Input() temporalScale!: TemporalScale;
@Output() onSelectTimeChanged = new EventEmitter<LinkedTime>();

readonly axisDirection = AxisDirection.VERTICAL;

getHighestStep(): number {
return this.steps[this.steps.length - 1];
}
getLowestStep(): number {
return this.steps[0];
}
getAxisPositionFromStep(step: number): number {
return this.temporalScale(step);
}
getStepHigherThanAxisPosition(position: number): number {
let stepIndex = 0;
while (
position > this.temporalScale(this.steps[stepIndex]) &&
stepIndex < this.steps.length - 1
) {
stepIndex++;
}
return this.steps[stepIndex];
}
getStepLowerThanAxisPosition(position: number): number {
let stepIndex = this.steps.length - 1;
while (
position < this.temporalScale(this.steps[stepIndex]) &&
stepIndex > 0
) {
stepIndex--;
}
return this.steps[stepIndex];
}
}
182 changes: 182 additions & 0 deletions tensorboard/webapp/widgets/histogram/histogram_linked_time_fob_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/* Copyright 2022 The TensorFlow Authors. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/

import {NO_ERRORS_SCHEMA} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {
Fob,
LinkedTimeFobControllerComponent,
} from '../linked_time_fob/linked_time_fob_controller_component';
import {LinkedTime} from '../linked_time_fob/linked_time_types';
import {TemporalScale} from './histogram_component';
import {HistogramLinkedTimeFobController} from './histogram_linked_time_fob_controller';

describe('HistogramLinkedTimeFobController', () => {
let temporalScaleSpy: jasmine.Spy;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
HistogramLinkedTimeFobController,
LinkedTimeFobControllerComponent,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});

function createComponent(input: {
steps?: number[];
linkedTime?: LinkedTime;
}): ComponentFixture<HistogramLinkedTimeFobController> {
const fixture = TestBed.createComponent(HistogramLinkedTimeFobController);
fixture.componentInstance.steps = input.steps ?? [100, 200, 300, 400];
fixture.componentInstance.linkedTime = input.linkedTime ?? {
start: {step: 200},
end: null,
};
temporalScaleSpy = jasmine.createSpy();
fixture.componentInstance.temporalScale =
temporalScaleSpy as unknown as TemporalScale;
temporalScaleSpy.and.callFake((step: number) => {
// Imitate a 10 to 1 scale.
return step * 10;
});
return fixture;
}

it('returns first element of steps from getLowestStep', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
expect(fixture.componentInstance.getLowestStep()).toBe(100);
});

it('returns final element of steps from getHighestStep', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
expect(fixture.componentInstance.getHighestStep()).toBe(400);
});

describe('getStepHigherThanAxisPosition', () => {
it('gets step higher when position is not on a step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepHigher =
fixture.componentInstance.getStepHigherThanAxisPosition(1500);
expect(stepHigher).toEqual(200);
});
it('gets step on given position when that position is on a step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepHigher =
fixture.componentInstance.getStepHigherThanAxisPosition(3000);
expect(stepHigher).toEqual(300);
});
it('gets highest step when given position is higher than the max step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepHigher =
fixture.componentInstance.getStepHigherThanAxisPosition(8000);
expect(stepHigher).toEqual(400);
});
it('gets lower step when given position is lower than the min step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepHigher =
fixture.componentInstance.getStepHigherThanAxisPosition(10);
expect(stepHigher).toEqual(100);
});
});

describe('getStepLowerThanAxisPosition', () => {
it('gets step lower when position is not on a step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepLower =
fixture.componentInstance.getStepLowerThanAxisPosition(2500);
expect(stepLower).toEqual(200);
});
it('gets step on given position when that position is on a step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepLower =
fixture.componentInstance.getStepLowerThanAxisPosition(3000);
expect(stepLower).toEqual(300);
});
it('gets highest step when given position is higher than the max step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepLower =
fixture.componentInstance.getStepLowerThanAxisPosition(8000);
expect(stepLower).toEqual(400);
});
it('gets lower step when given position is lower than the min step', () => {
let fixture = createComponent({steps: [100, 200, 300, 400]});
let stepLower =
fixture.componentInstance.getStepLowerThanAxisPosition(10);
expect(stepLower).toEqual(100);
});
});

describe('getAxisPositionFromStep', () => {
it('calls the scale function', () => {
let fixture = createComponent({});
expect(fixture.componentInstance.getAxisPositionFromStep(150)).toBe(1500);
expect(temporalScaleSpy).toHaveBeenCalledOnceWith(150);
});
});

describe('interaction with base controller', () => {
it('properly uses scale when setting fob position', () => {
let fixture = createComponent({
linkedTime: {start: {step: 300}, end: null},
});
fixture.detectChanges();
let testController = fixture.debugElement.query(
By.directive(LinkedTimeFobControllerComponent)
).componentInstance;
expect(
testController.startFobWrapper.nativeElement.getBoundingClientRect().top
).toEqual(3000);
});
it('moves the fob to the next highest step when draggin down', () => {
let fixture = createComponent({
steps: [100, 200, 300, 400],
linkedTime: {start: {step: 300}, end: null},
});
fixture.detectChanges();
let testController = fixture.debugElement.query(
By.directive(LinkedTimeFobControllerComponent)
).componentInstance;
testController.startDrag(Fob.START);
const fakeEvent = new MouseEvent('mousemove', {
clientY: 3020,
movementY: 1,
});
testController.mouseMove(fakeEvent);
fixture.detectChanges();
expect(
testController.startFobWrapper.nativeElement.getBoundingClientRect().top
).toEqual(4000);
});
it('moves the fob to the next lowest step when draggin up', () => {
let fixture = createComponent({
steps: [100, 200, 300, 400],
linkedTime: {start: {step: 300}, end: null},
});
fixture.detectChanges();
let testController = fixture.debugElement.query(
By.directive(LinkedTimeFobControllerComponent)
).componentInstance;
testController.startDrag(Fob.START);
const fakeEvent = new MouseEvent('mousemove', {
clientY: 2980,
movementY: -1,
});
testController.mouseMove(fakeEvent);
fixture.detectChanges();
expect(
testController.startFobWrapper.nativeElement.getBoundingClientRect().top
).toEqual(2000);
});
});
});
3 changes: 2 additions & 1 deletion tensorboard/webapp/widgets/histogram/histogram_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import {IntersectionObserverModule} from '../intersection_observer/intersection_
import {LinkedTimeFobModule} from '../linked_time_fob/linked_time_fob_module';
import {ResizeDetectorModule} from '../resize_detector_module';
import {HistogramComponent} from './histogram_component';
import {HistogramLinkedTimeFobController} from './histogram_linked_time_fob_controller';

@NgModule({
declarations: [HistogramComponent],
declarations: [HistogramComponent, HistogramLinkedTimeFobController],
exports: [HistogramComponent],
imports: [
CommonModule,
Expand Down

0 comments on commit e3df848

Please sign in to comment.