-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add the frontend for the PR curves plugin #426
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
fab71f6
to
6316efd
Compare
6316efd
to
c922b34
Compare
This frontend for the PR curves plugin makes use of the `vz-line-chart` component that had been generalized in #388. Each `tf-pr-curve-card` contains 1 PR curve. The `tf-pr-curve-steps-selector` creates 1 slider per run and lets the user select steps. This PR is a part of dividing #334 (which contains resourceful input from @wchargin) into smaller PRs.
c922b34
to
9ae592f
Compare
A good next step would be to visualize the display name in the header. |
f231fa3
to
7dd8afe
Compare
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.
High-level design looks good.
} else if (timeDisplayType === 'wall_time') { | ||
return new Date(value * 1000).toString(); | ||
} | ||
console.error( |
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.
Can we just throw an error here? Returning undefined
doesn't seem like the right thing to do.
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.
let url = router.pluginRoute('pr_curves', '/pr_curves'); | ||
url += url.indexOf('?') > -1 ? '&' : '?'; | ||
const urlRunsPortion = this.runs.map(r => `&run=${r}`).join(''); | ||
url += `tag=${this.tag}${urlRunsPortion}`; |
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 ordering of these two lines is confusing to me. What do you think of
let url = router.pluginRoute('pr_curves', '/pr_curves');
url += url.indexOf('?') > -1 ? '&' : '?';
url += `tag=${this.tag}`;
url += this.runs.map(r => `&run=${r}`).join('');
?
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.
+1, that is clearer.
_computeSetOfRelevantRuns(runsWithStepAvailable) { | ||
const setOfRelevantRuns = {}; | ||
_.forEach(runsWithStepAvailable, run => { | ||
setOfRelevantRuns[run] = 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.
nit: Common practice is to use true
here.
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.
if (!entry) { | ||
return null; | ||
} | ||
return new Date(runToPrCurveEntry[run].wall_time * 1000).toString(); |
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.
Might as well use entry.wall_time * 1000
here?
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.
max-width: 540px; | ||
margin: 80px auto 0 auto; | ||
} | ||
/** Not not let the steps selector occlude the run selector. */ |
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.
"Do not"?
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.
// do not make this handler run when _runToStepIndex is updated because | ||
// asynchronously changing _runToStepIndex could create a rendering loop | ||
// and jittery sliders. | ||
setTimeout(() => { |
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.
Can this be replaced with this.async(() => { … })
?
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.
Yes! Done.
}, | ||
_runToAvailableTimeEntriesChanged(runToAvailableTimeEntries) { | ||
// The step for each run just refreshed. Change every slider to point to | ||
// the last available step. |
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.
Wait, why? This behavior seems really strange. Suppose that an experiment is actively writing data to our logdir. Then, every thirty seconds, all the sliders will be shoved over to the end.
Would it suffice to make only newly added runs point to the last available step?
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.
+1, that previous behavior seems jarring, ie after a reload, the steps unexpectedly change. Now, we only set sliders for newly added runs to the last available step - we initialize sliders to point to the last step, but never override the step again.
One small thing to note is that sampling logic samples a constant N values, so the step will slightly differ across reloads, but I think that's much less jarring than overriding step to the last value.
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.
Agreed.
}, | ||
_updateSliders(runToStepIndex) { | ||
const sliders = this.querySelectorAll("paper-slider"); | ||
if (sliders) { |
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.
sliders
should always be a NodeList, so will always be truthy:
var x = document.querySelectorAll('.not-very-likely'); [!!x, x.length]
// [true, 0]
You can dissolve this if
-statement, and just use:
this.querySelectorAll('paper-slider').forEach(slider => {
slider[value] = runToStepIndex[slider.dataset.run];
});
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.
+1 Done.
this._updateSliders(newRunToStepIndex); | ||
}, | ||
_updateSliders(runToStepIndex) { | ||
const sliders = this.querySelectorAll("paper-slider"); |
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: Use single-quotes consistently?
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.
_.forOwn(runToStepIndex, (index, run) => { | ||
const entries = runToAvailableTimeEntries[run]; | ||
if (!entries) { | ||
return |
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.
Missing semicolon.
I'm surprised that this doesn't raise a warning with the closure compiler?
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. Hmm, indeed, I see no Closure warnings. Maybe the transpilation from TS to uncompiled JS adds the semicolon back.
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.
But this isn't TypeScript?
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 just double-checked that no warning appears with no semicolon. Indeed, a warning should appear since suspiciousCode warnings are on by default: https://github.com/google/closure-compiler/wiki/Warnings
Maybe the TS is transpiled into JS before being compiled into a small JS binary, and Closure applies in the 2nd step?
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.
let url = router.pluginRoute('pr_curves', '/pr_curves'); | ||
url += url.indexOf('?') > -1 ? '&' : '?'; | ||
const urlRunsPortion = this.runs.map(r => `&run=${r}`).join(''); | ||
url += `tag=${this.tag}${urlRunsPortion}`; |
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.
+1, that is clearer.
_computeSetOfRelevantRuns(runsWithStepAvailable) { | ||
const setOfRelevantRuns = {}; | ||
_.forEach(runsWithStepAvailable, run => { | ||
setOfRelevantRuns[run] = 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.
Done.
if (!entry) { | ||
return null; | ||
} | ||
return new Date(runToPrCurveEntry[run].wall_time * 1000).toString(); |
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.
max-width: 540px; | ||
margin: 80px auto 0 auto; | ||
} | ||
/** Not not let the steps selector occlude the run selector. */ |
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.
// The actual step value that each run should use. If a run + tag lacks a | ||
// PR curve at this exact step value, the greatest step value less than | ||
// this value will be used. | ||
_runToStep: { |
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.
+1 Done.
} else if (timeDisplayType === 'wall_time') { | ||
return new Date(value * 1000).toString(); | ||
} | ||
console.error( |
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.
}, | ||
_runToAvailableTimeEntriesChanged(runToAvailableTimeEntries) { | ||
// The step for each run just refreshed. Change every slider to point to | ||
// the last available step. |
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.
+1, that previous behavior seems jarring, ie after a reload, the steps unexpectedly change. Now, we only set sliders for newly added runs to the last available step - we initialize sliders to point to the last step, but never override the step again.
One small thing to note is that sampling logic samples a constant N values, so the step will slightly differ across reloads, but I think that's much less jarring than overriding step to the last value.
this._updateSliders(newRunToStepIndex); | ||
}, | ||
_updateSliders(runToStepIndex) { | ||
const sliders = this.querySelectorAll("paper-slider"); |
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.
}, | ||
_updateSliders(runToStepIndex) { | ||
const sliders = this.querySelectorAll("paper-slider"); | ||
if (sliders) { |
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.
+1 Done.
_.forOwn(runToStepIndex, (index, run) => { | ||
const entries = runToAvailableTimeEntries[run]; | ||
if (!entries) { | ||
return |
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. Hmm, indeed, I see no Closure warnings. Maybe the transpilation from TS to uncompiled JS adds the semicolon back.
I forgot that we have to initialize in the first place.
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.
Thanks again for providing screenshots.
I note that the artifacts at the vertical axis—where the curve jumps down from 1.0 to 0.0—still exist. If I understand the previous discussion correctly, this is a problem with the summary op, not the frontend. Is this correct? (Also, is there a plan to fix it?)
Non-actionable: I wonder whether all the “[run] is at step [step]” will take up prohibitively much vertical space when many runs are used in practice. I suspect that we’ll hear feedback if that’s the case.
// plot. The entry is based on which step the user currently selects. | ||
_runToPrCurveEntry: { | ||
type: Object, | ||
value: {}, |
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.
value: () => ({})
(likewise on _previousRunToPrCurveEntry
)
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.
Thanks! Done.
(x) => isNaN(x) ? 'NaN' : valueFormatter(x); | ||
return [ | ||
{ | ||
title: 'Name', |
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 it better to call this "Name" or "Run"? (This isn't really a request to change; I have no preference.)
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.
+1, I like "Run" much better. That parallels what most TF users would use. Done.
this._updateSeriesDataForRun(run, entry); | ||
}); | ||
}, | ||
_updateSeriesDataForRun(run, entryFor1Step) { |
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/1/One
, please. But perhaps prCurveEntry
is clearer anyway—your choice.
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.
Hmm. Yeah, maybe entryForOneStep
for now.
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.
Took a high level pass. Nothing stands out to me as problematic. I support merging this as soon as @wchargin gives his nod. It will be nice to be able to include this plugin in the talk I'm giving on Thursday.
@@ -0,0 +1,411 @@ | |||
<!-- | |||
@license |
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 format these license headers the same way as the others, with the blank lines and all.
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.
'histograms': 'tf-histogram-dashboard', | ||
'projector': 'vz-projector-dashboard', | ||
'text': 'tf-text-dashboard', | ||
'pr_curves': 'tf-pr-curve-dashboard', |
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.
It might look better, æsthetically speaking on the tab label, if this was "pr-curves".
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.
Oh man ... I think this is linked with the plugin name in the backend, which is pr_curves
. What do you think about in a separate change, we replace _ with a space in the navigation bar?
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.
Yep: right now we unfortunately have the restriction that the tab in the plugin name must equal the plugin name on the backend. That's probably not good to have long-term. If you look at my proposed dashboards.ts
from #202, you can see the approach I had in mind for resolving this. By giving each plugin a little configuration object, we can easily add an optional (or required) tabName
field that takes the string "PR Curves"
instead of "pr_curves"
. I believe at the time we chose not to do this just for simplicity and ease of merging (as #205 was created around the same time and it wasn't worth reconciling), but we might want to do something like that now.
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 like @wchargin's idea of adding a tabName config property.
align-items: center; | ||
bottom: 0; | ||
display: flex; | ||
display: flex; |
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.
Repeated line here.
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.
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.
// plot. The entry is based on which step the user currently selects. | ||
_runToPrCurveEntry: { | ||
type: Object, | ||
value: {}, |
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.
Thanks! Done.
(x) => isNaN(x) ? 'NaN' : valueFormatter(x); | ||
return [ | ||
{ | ||
title: 'Name', |
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.
+1, I like "Run" much better. That parallels what most TF users would use. Done.
this._updateSeriesDataForRun(run, entry); | ||
}); | ||
}, | ||
_updateSeriesDataForRun(run, entryFor1Step) { |
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.
Hmm. Yeah, maybe entryForOneStep
for now.
For the strange, steep drop, I think we can consider solving the problem by removing the first point in the backend route, maybe here, we remove the point at recall = 0. I want to also see if this could be solved at the summary op level though. |
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.
Okay—LGTM! 🎉
I want to also see if this could be solved at the summary op level though.
Great. I certainly prefer this over removing the point at recall=0.
'histograms': 'tf-histogram-dashboard', | ||
'projector': 'vz-projector-dashboard', | ||
'text': 'tf-text-dashboard', | ||
'pr_curves': 'tf-pr-curve-dashboard', |
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.
Yep: right now we unfortunately have the restriction that the tab in the plugin name must equal the plugin name on the backend. That's probably not good to have long-term. If you look at my proposed dashboards.ts
from #202, you can see the approach I had in mind for resolving this. By giving each plugin a little configuration object, we can easily add an optional (or required) tabName
field that takes the string "PR Curves"
instead of "pr_curves"
. I believe at the time we chose not to do this just for simplicity and ease of merging (as #205 was created around the same time and it wasn't worth reconciling), but we might want to do something like that now.
This frontend for the PR curves plugin makes use of the
vz-line-chart
component that had been generalized in #388. Eachtf-pr-curve-card
contains 1 PR curve. Thetf-pr-curve-steps-selector
creates 1 slider per run and lets the user select steps.This PR is a part of dividing #334 (which contains resourceful input from @wchargin) into smaller PRs.