Skip to content
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

Refactoring of the parallel-coordinates element. #2223

Merged
merged 21 commits into from
Jun 17, 2019

Conversation

erzel
Copy link
Contributor

@erzel erzel commented May 10, 2019

  • Converted the code into typescript and introduced several cooperating classes to implement the behavior.
  • Changed 'schema' to be an object containing all columns (visible and non visible) with a boolean associated with each one to denote whether it is currently displayed. The old format of the schema is
    preserved under the deprecated field 'visibleSchema' of 'schema'. The goal is eventually to make everyone use 'schema' and to remove 'visibleSchema'.

@erzel
Copy link
Contributor Author

erzel commented May 10, 2019

This PR should also take care of b/129365646 (errorneous brush behavior when linear-scale / log-scale axis contains just one point).

@wchargin
Copy link
Contributor

Thanks for the PR! I’m OOO until next Wednesday and won’t be able to
take a look until then.

@erzel
Copy link
Contributor Author

erzel commented May 10, 2019

@stephanwlee , @wchargin PTAL.
Thanks!

Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not completely read the code yet. The high level feedbacks, I believe, can be addressed and it can cause more changes.

result = schema.hparamColumns.length + schema.metricColumns.findIndex(
c => (c.metricInfo.name === metricName));
}
console.assert(result !== undefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, result cannot be undefined. Is this strictly required?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a sanity check. I agree that it's currently clear that it can't happen, but it may not be so clear after the code evolves in future changes. I typically use asserts to check assumptions (and document them) on the internal logic of the program. So these are typically conditions that one doesn't expect to see fail.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it may not be so clear after the code evolves in future changes.

This reminds me of saying that goes like 'don't design for tomorrow's problem'.

I typically use asserts to check assumptions (and document them) on the internal logic of the program

Especially since this is not an input validation, that'd be a job of a test, not some assertion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my intended use of asserts here is not clear. Let me try to clarify:

I use asserts to both verify and document invariants on the internal logic of the code that I believe must hold regardless of user's input. If an invariant does not hold, there is a bug and the code needs to be changed to fix it. I use asserts here as I would use a "CHECK" macro in (google) C++ or "Preconditions.CheckState" in Java. For example, to check that an enum's value is one of its predefined members or that an API is being used correctly (open is called on a file before we try to access it). I think assert shouldn't be called for checking a condition on (some function of) the user's input or to check that some external API didn't return an error (like a file wasn't opened successfully)

So when an invariant on the code's logic fails, I'd log as much information as possible for the developer to allow reproducing the bug and also notify the user that it happened. The user can then file a bug report with the above information so that the bug can be fixed.

This reminds me of saying that goes like 'don't design for tomorrow's problem'.
Especially since this is not an input validation, that'd be a job of a test, not some assertion.

Like tests, these assertions safeguard against future changes that would break past assumptions. But, they document these assumptions in a much more direct fashion (one reads the code and immediately sees them), they require a lot less overhead to write, and they run during a real execution of the program so they are more helpful in fixing bugs. The downside obviously, is that they have a performance overhead. Also, of course, I'm not suggesting we should use assertions instead of tests, but I just claim these asserts have their place as well.

Having said all that, in this particular case, I don't feel strongly about having the assert there, since the distance between where 'result' is computed and the assertion is very small and so it's clear from the code that it holds. I agree you could argue that it's overkill, so I'm removing it.

Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really appreciate the change. Thanks!

I still have not yet completed by first pass. Before I complete it, let me ask this: would it be possible to break tf-hparams-parallel-coords-plot.ts into few files? I don't think you will need to do tons of type refactoring. Preferably file per big item (Interaction vs. Axis. vs. Line) would be nice.

this._axesManager.dragEnd(/*transitionDuration=*/ 500);
this._linesManager.recomputeControlPoints(LineType.Foreground,
/* transitionDuration=*/ 500);
window.setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I didn't know about that. I agree that doing this with the event is a more explicit, but I'd rather not fix something that's not really broken. Also this would make the code dependent on D3 implementing SVG transitions using CSS transitions.

* scale types.
*/
export enum ScaleType {
Linear = "LINEAR",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ENUMs tend to be ALL_CAPS.
Are you using these string values somewhere? If not, just use whatever TypeScript gives you.

export enum ScaleType {
  LINEAR,
  LOGARITHMIC,
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to ALL_CAPS. I am setting these to the strings so that they can be freely interchanged with the current (pre-typescript) values I already use in options.columns[].scale.


private _lower: number;
private _upper: number;
private _lowerOpen: boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stylistic nit: missing semi-colon.
Also, can you put the properties near the top of the class? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Style guides for other languages, like C++, typically call for putting the private parts of a class at the end, so that it's easier to read the public interface (which I guess is read more than the private one). What's your reasoning here for having the private properties first?

if (sessionGroupSel === undefined) {
sessionGroupSel = d3.selectAll(null);
}
console.assert(sessionGroupSel.size() <= 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so when sessionGroupSel is undefined, this assertion fails, right? If so, why is it an optional parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that when sessionGroupSel is undefined, it's set in line 640 to be the empty selection which would pass the assertion.

}

/**
* @returns the sessionGroup object this handle references or null if
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of mixing Closure type with TypeScript, I would make the TypeScript type more concrete.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm not sure I understand. I actually didn't know that I was using a "closure type" -- I was simply trying to follow the TypeScript doc convention I found here:
https://typedoc.org/guides/doccomments/

Unfortunately, I don't have a 'SessionGroup' type definition so I don't write 'SessionGroup | null' here.
One could add it, but the downside is we'll need to remember to change it every time we change the underlying protocol buffer (SessionGroup is a JavaScript representation of the proto defined in api.proto obtained using proto3's proto to JSON mapping).

But I may have misundestood your intention. Can you clarify what you mean by concrete type here?

* dragging an axis and contains the logic for re-ordering the axes
* during dragging.
*/
class AxesManager {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of calling this a manager, I would choose a name like AxesGroup or something like that. Word util or manager are often used to mean a place where random functions end up. Since this is also responsible for rendering axes, I would call it something more appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to AxesCollection and LinesCollection. I think AxesGroup is fine, but LinesGroup sounded a bit ambigious for me and I wanted the same suffix for both Axes and Lines. What do you think?


// Update the DOM.
this._parentsSel = this._parentsSel
.data(Array.from(visibleColIndices), /*key=*/ (colIndex => colIndex));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the key part strictly required? It looks too simple for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Without it, D3 joins by array order:
"If a key function is not specified, then the first datum in data is assigned to the first selected element, the second datum to the second selected element, and so on.",
whereas here if a DOM element from the previous state represented column 5 (say), I'd like to continue to use it to represent column 5, and not change it to some other column.

@erzel
Copy link
Contributor Author

erzel commented May 15, 2019

I still have not yet completed by first pass. Before I complete it, let me ask this: would it be possible to >break tf-hparams-parallel-coords-plot.ts into few files? I don't think you will need to do tons of type >refactoring. Preferably file per big item (Interaction vs. Axis. vs. Line) would be nice.

I can certainly do that. I don't think it's a big change. I do sometimes find it easier to read through code of related classes when they are all in one file; however, I agree that here this file of ~1000 lines is too big.
I split the file into 3.

@erzel
Copy link
Contributor Author

erzel commented May 15, 2019

Thanks @stephanwlee! PTAL.

@erzel
Copy link
Contributor Author

erzel commented May 15, 2019

BTW. I'm stil not a 100% used to github's flow of review, so I may miss some of your comments. If I left any unreplied, please just remind me in your response.

Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you type the SessionGroup? I see it repeatedly and forget what it should look like.

result = schema.hparamColumns.length + schema.metricColumns.findIndex(
c => (c.metricInfo.name === metricName));
}
console.assert(result !== undefined);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it may not be so clear after the code evolves in future changes.

This reminds me of saying that goes like 'don't design for tomorrow's problem'.

I typically use asserts to check assumptions (and document them) on the internal logic of the program

Especially since this is not an input validation, that'd be a job of a test, not some assertion.

this._linesCollection.selectedSessionGroupHandle().sessionGroup());
}

// Polymer adds an extra ".tf-hparams-parallel-coords-plot" class to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unique to Polymer1 (or shadyCSS). The correct way to do this is to use https://polymer-library.polymer-project.org/1.0/api/classes/Polymer.Base#method-scopeSubtree.

After making DOM modification, Polymer class has to invoke this method with the new DOM as the first arg.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Done.

* object control which axes are visible.
*/
public updateAxes(options: any, sessionGroups: any[]) {
console.assert(!this.isAxisDragging());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of throwing exception, I'd silently exit or say something more user friendly especially if it is going to litter in the console.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also my reply to comment: https://github.com/tensorflow/tensorboard/pull/2223/files#r284474857

Note that console.assert() doesn't throw an exception:

function f() { console.assert(0);console.log("foo"); }
f()
VM371:1 Assertion failed: console.assert /* Note that there's a stack trace here that I removed. */
VM371:1 foo
undefined

As for the message, I use asserts as checks that verify assumptions the code makes about its internal logic. Violations of these are always bugs that are not meant to be recovered from and need to be fixed. If this was an offline batch program, the reasonable course of action would be to terminate the program (fail fast). If this was a server, the reasonable course of action would be to return an "internal server error" to the client and stop handling the current request. Since this is a UI, I don't want to do either, but I do want to document the fact that the bug happened. Thus, the assertion failure message should contain a stack trace, the condition that failed and any other information that can help debugging. It's mostly for the developer not the end-user.

this._stationaryAxesPositions.domain(
/* TypeScript doesn't allow spreading a Set, so we convert to an
Array first. */
newDomain.concat(...Array.from(visibleColIndices)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separately, I am a bit confused by Line 508 and this line.

Line 506: newDomain = visibileColIndices ∩ this._stationaryAxesPositions.domain()
Line 508: visibleColIndices = visibileColIndices - newDomain
Line 512: newDomain = newDomain ∪ visibleColIndices = originalVisibileColIndices

It might be my afternoon drowsiness but I must be missing something (or can't comprehend code).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're understanding this right. I do it this way rather than setting the domain to visibleColIndices directly since
I want to preserve the order of the existing visible axes. I rewrote the comment to make it clearer. Please let me know if that makes more sense now.

Copy link
Contributor Author

@erzel erzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again @stephanwlee! PTAL

* object control which axes are visible.
*/
public updateAxes(options: any, sessionGroups: any[]) {
console.assert(!this.isAxisDragging());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also my reply to comment: https://github.com/tensorflow/tensorboard/pull/2223/files#r284474857

Note that console.assert() doesn't throw an exception:

function f() { console.assert(0);console.log("foo"); }
f()
VM371:1 Assertion failed: console.assert /* Note that there's a stack trace here that I removed. */
VM371:1 foo
undefined

As for the message, I use asserts as checks that verify assumptions the code makes about its internal logic. Violations of these are always bugs that are not meant to be recovered from and need to be fixed. If this was an offline batch program, the reasonable course of action would be to terminate the program (fail fast). If this was a server, the reasonable course of action would be to return an "internal server error" to the client and stop handling the current request. Since this is a UI, I don't want to do either, but I do want to document the fact that the bug happened. Thus, the assertion failure message should contain a stack trace, the condition that failed and any other information that can help debugging. It's mostly for the developer not the end-user.

this._stationaryAxesPositions.domain(
/* TypeScript doesn't allow spreading a Set, so we convert to an
Array first. */
newDomain.concat(...Array.from(visibleColIndices)));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're understanding this right. I do it this way rather than setting the domain to visibleColIndices directly since
I want to preserve the order of the existing visible axes. I rewrote the comment to make it clearer. Please let me know if that makes more sense now.

this._linesCollection.selectedSessionGroupHandle().sessionGroup());
}

// Polymer adds an extra ".tf-hparams-parallel-coords-plot" class to
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Done.

result = schema.hparamColumns.length + schema.metricColumns.findIndex(
c => (c.metricInfo.name === metricName));
}
console.assert(result !== undefined);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my intended use of asserts here is not clear. Let me try to clarify:

I use asserts to both verify and document invariants on the internal logic of the code that I believe must hold regardless of user's input. If an invariant does not hold, there is a bug and the code needs to be changed to fix it. I use asserts here as I would use a "CHECK" macro in (google) C++ or "Preconditions.CheckState" in Java. For example, to check that an enum's value is one of its predefined members or that an API is being used correctly (open is called on a file before we try to access it). I think assert shouldn't be called for checking a condition on (some function of) the user's input or to check that some external API didn't return an error (like a file wasn't opened successfully)

So when an invariant on the code's logic fails, I'd log as much information as possible for the developer to allow reproducing the bug and also notify the user that it happened. The user can then file a bug report with the above information so that the bug can be fixed.

This reminds me of saying that goes like 'don't design for tomorrow's problem'.
Especially since this is not an input validation, that'd be a job of a test, not some assertion.

Like tests, these assertions safeguard against future changes that would break past assumptions. But, they document these assumptions in a much more direct fashion (one reads the code and immediately sees them), they require a lot less overhead to write, and they run during a real execution of the program so they are more helpful in fixing bugs. The downside obviously, is that they have a performance overhead. Also, of course, I'm not suggesting we should use assertions instead of tests, but I just claim these asserts have their place as well.

Having said all that, in this particular case, I don't feel strongly about having the assert there, since the distance between where 'result' is computed and the assertion is very small and so it's clear from the code that it holds. I agree you could argue that it's overkill, so I'm removing it.

@erzel
Copy link
Contributor Author

erzel commented May 16, 2019

Could you type the SessionGroup? I see it repeatedly and forget what it should look like.

Done.

@erzel
Copy link
Contributor Author

erzel commented May 23, 2019

@stephanwlee, friendly ping.

this.schema.metricColumns[metricIndex].displayed =
this._metrics[metricIndex].displayed;
this._updateVisibleSchema();
this.notifyPath(`schema.metricColumns.${metricIndex}.displayed`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe if you use set API, you don't need to notify like this.

        this.set(`schema.metricColumns.${metricIndex}.displayed`, this._metrics[metricIndex].displayed);

(Also, instead of imperatively calling updateVisibleSchema, make sure there is an observer that, on metricColumn display changes, trigger updateVisibleSchema.

Similar concern above for _hparamVisibilityChanged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is with atomically modifying schema so that observing elements will see the changes to both visibleSchema and metricColumns together. If I use the 'set' API then the notification for the changed metricColumns property will go out immediately, and an observing element will see a schema object in an incosistent state--one whose visibleSchema does not match the new 'metricColumn' information. This may create problems: for example, it would make the assumptions that the code in tf.hparam.utils.getAbsoluteColumnIndex() not hold.
I know this is not ideal, but I don't know of a better solution. I tried to explain that in the comment above the code. I'm open to suggestions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense and shame on me for not reading all the comments carefully. Sorry about that.

While your rationale makes sense, it looks rather brittle or unconventional. How about this?

this.schema = this._computeSchema();

Is this unappealing for any performance reason that I am not seeing? btw, if above works, I would remove both _metricVisibilityChanged and _hparamVisibilityChanged to this new method, _updateSchema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While your rationale makes sense, it looks rather brittle or unconventional. How about this?

I agree. I don't like it very much either, and I'd be happy with a better solution.
Using: 'this.schema = this._computeSchema()' will replace the identity of the schema object each time the user changes the visibility of a single column. This makes it harder for for observing elements to redraw only the changed part. See also the comments in lines 345--350. For example, if the schema object stays the same and we only change its properties, the vaadin-grid element can set the column property 'hidden' to be linked to the (negation of) the corresponding 'displayed' subproperty of schema.hparamColumn or schema.metricColumn.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the point of the PR is a refactor, I would advise against such performance optimization (without carefully looking at what is costing the most CPU cycle; i.e., premature optimization). Also, there are many other ways to mitigate such issue while keeping your code sensible.

One small gotcha with this approach was, which I found while poking around with your branch, with current code, both 1 and 2 while 3 does not (missing *; i.e., schema.*. Without 3 getting triggered, InteractionManager can go out of sync with the options which fortuitously did not happen because we were mutating schema by reference. However, while I could not trigger it, I think they could really go out of sync soon (depending on execution order) when 4 happens. The point here is that, while the bug I pointed out is solvable, it just makes everything more brittle and harder to reason without concerning the greater system.

visibleColIndices = new Set<number>(visibleColIndices);
let newDomain: number[] = this._stationaryAxesPositions.domain().filter(
colIndex => visibleColIndices.has(colIndex));
newDomain.forEach(colIndex => visibleColIndices.delete(colIndex));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think comments are panacea and making code a little bit readable will help. Please take a look at code below and tell me what you think.

// Sort visibleColIndices so indicies present in _stationaryAxesPositions
// appear before others.
const visibleDomain = this._stationaryAxesPositions.domain()
     .filter(colIndex => visibleColIndices.has(colIndex));
const newDomain  = Array.from(new Set([
    ...Array.from(visibleDomain),
    ...Array.from(visibleColIndices),
]);
this._stationaryAxesPositions.domain(newDomain);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion does look more readable. Done.

tensorboard/plugins/hparams/api.d.ts Outdated Show resolved Hide resolved
() => {
const _this = this;
this._fgPathsSel.each(
function(sessionGroup) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might work: you can write below to document what this means.

function(this: SVGPathElement, sessionGroup) {
...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@erzel erzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @stephanwlee !
PTAL.

tensorboard/plugins/hparams/api.d.ts Outdated Show resolved Hide resolved
visibleColIndices = new Set<number>(visibleColIndices);
let newDomain: number[] = this._stationaryAxesPositions.domain().filter(
colIndex => visibleColIndices.has(colIndex));
newDomain.forEach(colIndex => visibleColIndices.delete(colIndex));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your suggestion does look more readable. Done.

() => {
const _this = this;
this._fgPathsSel.each(
function(sessionGroup) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

this.schema.metricColumns[metricIndex].displayed =
this._metrics[metricIndex].displayed;
this._updateVisibleSchema();
this.notifyPath(`schema.metricColumns.${metricIndex}.displayed`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is with atomically modifying schema so that observing elements will see the changes to both visibleSchema and metricColumns together. If I use the 'set' API then the notification for the changed metricColumns property will go out immediately, and an observing element will see a schema object in an incosistent state--one whose visibleSchema does not match the new 'metricColumn' information. This may create problems: for example, it would make the assumptions that the code in tf.hparam.utils.getAbsoluteColumnIndex() not hold.
I know this is not ideal, but I don't know of a better solution. I tried to explain that in the comment above the code. I'm open to suggestions.

@erzel
Copy link
Contributor Author

erzel commented May 31, 2019

Thanks @stephanwlee! PTAL.

@erzel erzel force-pushed the par_coords_refactor branch 2 times, most recently from a3a7838 to 3431826 Compare June 13, 2019 04:59
@erzel
Copy link
Contributor Author

erzel commented Jun 13, 2019

Hi @stephanwlee,

I've finally managed to incoporate the changes we talked about. I'm sorry for the delay, I'm in a conference this week.
I've split the columns visibility status ('displayed' field) out of 'schema' into a separate boolean array indexed by the column-index. I've done this so that it will be easier for elements to differentiate between the schema changing (e.g. a domain of an hparam) vs a column becoming visible or not.
To make the changes "atomic" I've wrapped all of the relevant information (schema, columnsVisibility and the previous 'visibleSchema') into a single immutable 'configuration object' that is generated by tf-hparams-query-pane. As we discussed, to avoid a race condition between this object and the options object stored in the tf-hparams-scale-and-color-controls, I've added it as a property of options.

PTAL.

Thanks!

@erzel erzel force-pushed the par_coords_refactor branch from 3431826 to 9f2c9df Compare June 13, 2019 05:17
Copy link
Contributor

@stephanwlee stephanwlee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks much better now. Thanks for taking my suggestions!

* hparamColumns: Array<{hparamInfo: HParamInfo}>,
* metricColumn: Array<{metricInfo: MetricInfo>}>,
* }
* columnsVisibility: Array<Boolean>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The array here can be very confusing. Instead, may I suggest, maybe in a follow up, use an object where it is keyed by name of the column (assuming that they are unique? -- I cannot imagine it not being unique as it will confuse user too :))

Otherwise, this user of this prop has to be aware that hparam columns appear before metric ones.

Alternative would be, there being flat columns array and have a separate map/list of hparam & metrics column ids

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been using the convention of indexing columns by their index when the hparams appear before the metrics throughout the code. See for example the existing comment in tf-hparams-utils.html lines 52--63. So I think it would actually be easier to understand the code if I'm consistent here.

this._hparams[hparamIndex].displayed;
this._updateVisibleSchema();
this.notifyPath(`schema.hparamColumns.${hparamIndex}.displayed`);
_hparamVisibilityChanged() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this, I would recommend using idiomatic Polymer: http://go/gh/erzel/tensorboard/pull/2/files#diff-c777620714a33699c59f411f55c718c4R545

Also, consider debouncing at least a microtask :)

// in the schema will be displayed. See tf-hparams-query-pane for
// the definition of this object.
schema: {
// The visibleSchema defining the columns to use.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: the line wrapping is weird :\

Copy link
Contributor Author

@erzel erzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Stephan!
PTAL.

One weird thing I discovered is that for some reason the Vulcanizer (closure?) decides to drop the parameter from a d3.brushSelection(parameter) I make in line 650 of tf-hparams-scatter-plot-matrix-plot.html. Which causes a bug.
To overcome this I'm fooling it by writing the call as: d3["brushSelection"](parameter). Is there a better way, or maybe you know why this is happening all of a sudden?

Erez.

* hparamColumns: Array<{hparamInfo: HParamInfo}>,
* metricColumn: Array<{metricInfo: MetricInfo>}>,
* }
* columnsVisibility: Array<Boolean>,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been using the convention of indexing columns by their index when the hparams appear before the metrics throughout the code. See for example the existing comment in tf-hparams-utils.html lines 52--63. So I think it would actually be easier to understand the code if I'm consistent here.

const brushSelection = d3.brushSelection(cellGNode);
// For some reason the closure compiler drops the argument when we
// write the call below as 'd3.brushSelection(cellGNode)'.
const brushSelection = d3["brushSelection"](cellGNode);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action required. As you have indicated, I couldn't find the reason why Closure deletes the argument from this method by inspecting both d3 code and playing around with defining type of this method. I am okay with this hack for time being.

Optional: Btw, although it is technically not in scope of this work, lines 628/639/642 have argument that are not used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I removed the arguments.
Could you merge the PR, if you don't have any more comments?

@stephanwlee stephanwlee merged commit 6db3f42 into tensorflow:master Jun 17, 2019
stephanwlee added a commit to stephanwlee/tensorboard that referenced this pull request Jun 19, 2019
Common typo catcher caught few things in our recent PR tensorflow#2223.
Closure type system annotates return type with "@return" instead of
"@returns".
stephanwlee added a commit that referenced this pull request Jun 19, 2019
Common typo catcher caught few things in our recent PR #2223.
Closure type system annotates return type with "@return" instead of
"@returns".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants