-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[tensor-widget] Add slicing control to support 3D+ tensors #2565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
bileschi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some high level comments. No big deal and definitely not a blocker. Consider adding bugs to track.
- The slice-spec drop-downs don't close nicely if you open one and then click outside of it. To close it you must first click on it.
- The 'swap with x' functionality is difficult to keep track of. I find myself wishing there were a clearer way to know what I'm looking at. Some sort of highlighting of the 'shape' field in the top bar may help. Perhaps a way to click on the shape itself to set which direction is 'rows' and which direction is 'columns', and leave the others to be slices. It can still be constrained such that the 'rows' dim must be before the 'columns' dim.
- There's a bit more space for the tensor name in the top left. Consider giving a few more characters up there.
| * @return `true` if and only if the slicing dimensions and/or the viewing | ||
| * dimensions differ between the `spec0` and `spec1`. | ||
| */ | ||
| export function dimensionsDiffer( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider the inverse for readability:
compatibleSlicingSpec: which is true if viewing slicingSpec matches except for the {horizontal/vertical}range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.. and the slice index
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I renamed to to areSlicingSpecsCompatible for better clarity.
| spec0: TensorViewSlicingSpec, | ||
| spec1: TensorViewSlicingSpec | ||
| ): boolean { | ||
| if (spec0.viewingDims[0] !== spec1.viewingDims[0]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the canonical way to do this even if the length of the constituent arrays may be less than 1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the length is less than 1, then [0] will give you undefined, which works for the logic here.
| expect(dimensionsDiffer(spec0, spec1)).to.be.false; | ||
| }); | ||
|
|
||
| it('Differen slicing and viewing dimensions are captured', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit differenT
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| private slicingSpec: TensorViewSlicingSpec; | ||
|
|
||
| // Constituent UI components. | ||
| // private rootDiv: HTMLDivElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this commented line supposed to be here? Vestigial?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. Thanks for the catch.
| private readonly shape: Shape, | ||
| private readonly onSlicngSpecChange?: OnSlicingSpecChangeCallback | ||
| ) { | ||
| this.rank = this.shape.length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it could become a problem, as it is now possible to have the rank != this.shape.length, if someone changes one but not the other. Would it be better to replace all occurrences of this.rank with this.shape.length , which isn't that much longer??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes back to how the tensor widget works. If the underlying tensor's rank changes, the UI widget won't reflect that until its render() method is called again. However, when the render() method is called, it'll always create a new SlicingControl instance, which will reflect the up-to-date rank.
| */ | ||
| render(slicingSpec?: TensorViewSlicingSpec) { | ||
| if (slicingSpec != null) { | ||
| this.slicingSpec = JSON.parse(JSON.stringify(slicingSpec)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this stringify-parse loop accomplish?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a way to deep-copy the object. See the accepted answer in https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript
We want to avoid mutating the input argument slicingSpec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but note jeresig’s comment and the actual benchmarks showing that
stringify–parse is (as should be expected) the slowest way to copy:
https://stackoverflow.com/a/5344074/
We already have a lodash dependency; could you please use _.cloneDeep
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The object being deep-copied here is tiny. This method is not called frequently. If the performance becomes and issue in the future, we can consider using lodash. But for now, I think the benefit of one fewer dependency outweighs the tiny performance gain. (TensorWidget is intended to be usable as a standalone package)
| this.slicingSpec.slicingDimsAndIndices[ | ||
| slicingDims.indexOf(i) | ||
| ].index = newIndex; | ||
| dimControl.textContent = `${newIndex}/${dimSize}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider removing the '../${dimSize} here and elsewhere. IMO it adds visual noise and the maximum dimension value is already available from the shape field above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| */ | ||
| private renderDropdownMenuItems( | ||
| dropdown: HTMLDivElement, | ||
| top: number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
consider using a coord.XY pattern here, to collect the two values with similar meaning & use into a common object.
/**
- Represents a position in a planar coordinate frame.
*/
export interface XYCoordinates {
x: number;
y: number;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The top and left arg names correspond nicely to the return type of getBoundingClientRect(). I'd argue that creating a Coordinate-like interface or struct only adds confusion, because coordinates are usually x and y, but in the HTML coordinate system, things are called left, right, top and bottom. It would be confusing if force associate left with x and top with y; it'll also be confusing if you let the members of the interface be called left and top and so forth.
|
@bileschi Thanks for the very helpful review. Listed you as a co-author in this PR if that's okay with you.
|
|
Regarding the point that there is some extra space for tensor name, note that the top-right part of the widget is reserved for health pill, which hasn't been implemented yet, but will be soon. |
| Math.floor(dimSize) != dimSize | ||
| ) { | ||
| // Reject invalid value. | ||
| dimInput.value = `${this.slicingSpec.slicingDimsAndIndices[slicingDims.indexOf(i)].index}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use String(foo) instead of `${foo}`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done throughout this file.
| slicingDims.indexOf(i) | ||
| ].index = newIndex; | ||
| dimControl.textContent = `${newIndex}/${dimSize}`; | ||
| this.onSlicngSpecChange(this.slicingSpec); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/Slicng/Slicing/?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Thanks for the catch.
| */ | ||
| render(slicingSpec?: TensorViewSlicingSpec) { | ||
| if (slicingSpec != null) { | ||
| this.slicingSpec = JSON.parse(JSON.stringify(slicingSpec)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but note jeresig’s comment and the actual benchmarks showing that
stringify–parse is (as should be expected) the slowest way to copy:
https://stackoverflow.com/a/5344074/
We already have a lodash dependency; could you please use _.cloneDeep
instead?





Motivation for features / changes
Technical description of changes
Screenshots of UI changes

The dropdown menu for swapping a viewing dimension with a slicing one:

The div for a slicing dimension becomes a number input box when clicked.
