Track multi-confidence filtering/displaying#460
Conversation
Delete type until only 1 remains, then delete track (IMO).
Yeah I think we should. It would be nice to stack them instead of side-by-side, I think. |
There was a problem hiding this comment.
I had to go pause and have a plate of cold thanksgiving while I thought about this.
the problem
The problem is communicating a new bit of state to consumers of the filteredTracks computed prop. Places that use this prop need to know something new: "What is the confidence pair in this track that caused it be included in this list". Before, that was implicit. There was only 1 possibility. Now there are several.
You've implemented this by sort of stashing this information inside the track object.
I object to that for a couple reasons:
- It improperly decouples
filteredTracksfrom thecurrentConfidencePair. You don't care aboutcurrentConfidnecePairas context unless you already have a dependency onfilteredTracks. - It adds new state/methods to the track API that aren't really related to a track itself.
- Side effects.
Alternative.
It seems to me like the best outcome would be to change the shape of filteredTracks to include this new bit of information.
const filteredTracks: Ref<{
track: Track,
context: {
confidencePairIndex: number;
};
}[]>This involves either changing the interface in a bunch of places or having another computed property that strips out just the track part for interfaces where the context isn't needed. IMO it would be best to just change the interface everywhere.
Naturally the enabledTracks interface will change also. I think that's fine. This new context is nice because any process that needs to explain why it filtered something can just tack on new state, and modules that accept a filtered list can specify which explanations they need.
In the short term, if you want to just declare this interface FilterItem {} at the top of useTrackFilters and import it from everywhere, that's perfectly fine with me.
If you would like me to do the tedious chore of changing the interface everywhere, I am happy to do so.
You could also "re-derive" this information in-place whereever you need it. If you have a track, and you have the list of enabledTypes, you can easily figure out which of a track's types caused its inclusion. However, this does involve subtly duplicating some logic which could lead to bugs. I don't recommend this option.
Another thought: we could actually formalize a "context" object for tracks that our various filtering processes could use to stash information like this. The trouble with that is I don't know where the scope of that context ends. Should we actually be keeping a list of IDs for the checkboxes, or should there just be a "checked" context var? Should we have a "selectedTrackId" state, or a selected context var?
We could make sure that the context object was side-effect safe. I don't know if that would be better than the above proposal.
| setType(trackType: string, confidenceVal = 1) { | ||
| const old = this.confidencePairs; | ||
| this.confidencePairs = [[trackType, confidenceVal]]; | ||
| this.currentConfidencePair = 0; |
There was a problem hiding this comment.
I believe this is out-of-scope for the concerns of track.ts. This is display/visualization state. It would be like adding an isSelected or isChecked property. This causes the UI's display specification to leak into the track API.
| )); | ||
| .some(([confkey, confval], index) => { | ||
| if (confval >= confidenceThresh && checkedSet.has(confkey)) { | ||
| track.setCurrentConfidencePair(index); |
There was a problem hiding this comment.
Side effects inside computed functions should really be avoided.
This is especially dicey because parts of the track state are reactive, parts aren't and this could easily cause an infinite loop, so the implementation details of this function are critical to whether or not this code works.
|
Thought about it some more today. If you want to do something easier like a dedicated track context, what about this? I don't think methods to get/set the map are necessary. just The map sort of guarantees it's not reactive, and the lack of methods guarantees we aren't bumping the notifier. |
…ME-Web into client/confidence-sort
|
Getting back into this I want to attempt to accurately summarize this and the possible implications. Summarize the highlights:
|
subdavis
left a comment
There was a problem hiding this comment.
I can't figure out how to get the viewer to show more than 1 confidence pair. Is there a setting somewher? Enabling / disabling types works as expected (the next highest type appears), but I only ever see one even though the code for multiple is clearly here.
| const intervalTree = useIntervalTree(); | ||
| const trackMap = useTrackMap(); | ||
| const tracksRef = useEnabledTracks(); | ||
| const filteredTracksRef = useEnabledTracks(); |
There was a problem hiding this comment.
Please call this tracksRef or more specifically enabledTracksRef. filteredTracks means something else already.
There was a problem hiding this comment.
I know this will need to change in a lot of places, but having filteredTracks mean several different things is probably not a good idea.
| editingTrack: false | EditAnnotationTypes, | ||
| selectedTrackId: TrackId | null, | ||
| tracks: readonly Track[], | ||
| filteredTracks: readonly FilteredTrack[], |
There was a problem hiding this comment.
Rename to tracks or enabledTracks.
| y: number; | ||
| offsetY?: number; | ||
| offsetX?: number; | ||
| currentPair?: boolean; |
There was a problem hiding this comment.
Please add a comment describing what this is.
Also, maybe make this a required property? You're already providing it everywhere, so the code makes no use of the condition where currentPair: undefined, and it's nice to not have to worry about the undefined check if that's not a valid, meaningful state.
| * @returns value or null. null indicates that the text should not be displayed. | ||
| */ | ||
| function defaultFormatter(track: FrameDataTrack): TextData[] | null { | ||
| function defaultFormatter(track: FrameDataTrack, additionalNum = 0): TextData[] | null { |
There was a problem hiding this comment.
additionalNum is sort of an awkward way of specifying a max value. Does it mean 1 will always be shown, plus some additionalNum?
Could we just make this maxLines or maxPairs and default it to 1? I think that's clearer, then you wouldn't have to do (maxPairs + 1) below (which is odd anyway because it means that maxPairs = 0 will show 1 line)
There was a problem hiding this comment.
I think restructuring this will make it clearer by moving and renaming maxPairs to maxAdditionalPairs down closer to where it is drawn instead of with the default type:confidence pair.
| let pairCount = 0; | ||
| if (track.confidencePairs.length) { | ||
| for (let i = 0; i < track.confidencePairs.length; i += 1) { | ||
| if (pairCount >= maxPairs) { |
There was a problem hiding this comment.
If you change maxPairs, this line has to change, but I think it should because this isn't what you'd expect if maxPairs wasn't already off-by-one.
| if (this.confidencePairs[index]) { | ||
| return this.confidencePairs[index]; | ||
| } |
There was a problem hiding this comment.
This doesn't seem right.
if confidencePairs is [['foo', 0.5], ['bar', 0.6]] and I call getType(20000), I'll get ['foo', 0.5]. I think I should get null.
Edit, from above, perhaps this should just raise an out of bounds exception?
There was a problem hiding this comment.
I was maintaining parity with the existing getType() which returned the 0 indexed confidencePair if it existed or else it would return null. So I modified it to return the 0 index confidencePair by default if it existed and index was out of scope. Or else it would return null if the confidencePairs were empty.
We can change this but I'll need to review and understand why we had it like this in the first place. I imagine it has something to do with creation or CSV files which don't always contain types.
| if (confidencePairs.length) { | ||
| const trackType = confidencePairs[0][0]; | ||
| const type = track.getType(filtered.context.confidencePairIndex); | ||
| const trackType = type ? type[0] : ''; |
There was a problem hiding this comment.
This is causing me to question the signature of track.getType()
It should probably always return [string, number] instead of the possibility of a null type, right? Like, if you ask for an out-of-bounds pair, that seems like an invalid execution path that should also throw an error from within the getter.
Why would getType ever be null? Why would you ever have a reference to a confidencePair index that doesn't exist?
Co-authored-by: Brandon Davis <brandon.davis@kitware.com>
Co-authored-by: Brandon Davis <brandon.davis@kitware.com>
Co-authored-by: Brandon Davis <brandon.davis@kitware.com>
|
@subdavis hopefully I didn't botched this up too horribly. I believe that I addressed all of your comments and tried to make things clearer. After looking at what calls |
|
Pending your thoughts on #603, this is good to go. |
* defaultFormatter * simpler syntax * comments * Change line height
subdavis
left a comment
There was a problem hiding this comment.
Thanks for making these changes. I think just changing the names for enabledTracks and TrackWithContext are the biggest improvements.

Fixes #423
Old style didn't properly support multiple types on a single track and swapping between them based on the selected types and the confidence filter adjustments.
currentConfidencePairto indicate the index of the sorted confidence pairs that should be displayed.useTrackFiltersI modified the filtering function to set thecurrentConfidencePairwhen it is visible and above theconfidenceThreshold. Let me know how you feel about having a side effect inside of.some.track.getType()so that it will render the proper type when displayed inAnnotationLayers, andtrackList.useEventChartanduseLineChartto utilize thetrack.getType()function instead of theconfidencePairs[0][0]so they would reflect the proper type as well.track.spec.tsto test setting the currentConfidencePair, as well as changing the current type.Old Version:


New Version:
Questions :
(made a determination that these questions will be placed in a new PR or as new issues, I just want to fix the fundamental problem first in a smaller PR. Feel free to add comments)