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
[WIP] Trialized time series faceting #232
[WIP] Trialized time series faceting #232
Conversation
Something I want to add for my data
Something I need to add for my data
Something to discuss adding would be
A widget I'll make that harnesses parts of this (separate PR)
|
Also just noting that when you use the plotly interact with the faceting it adjusts each plot; is there maybe an option to control that? Or is it just because they all 'share' the x-axis? (basically I would want different x-axis scales each spanning the data range for each condition) plotly_facet_all_axes_affected.mp4 |
I don't understand |
If you watch the video, as I attempt to zoom into one of those tiny data plotting regions for a single facet, it also zooms into that region for all the other facets. Likewise, from a zoomed out perspective, looking at all the data for one facet might just leave a tiny blip for the data on another facet. For those cases I'd prefer the plot to span the axis for each facet in a more reasonable way and then indicate the range on the x-ticks I can also see why you'd want to be able to view everything aligned to the same x-axis for consistency, so maybe this could be a 'lock-axis' option or something |
@h-mayorquin it doesn't look like you have a way of controlling the time range. It looks like in @CodyCBakerPhD 's example you are plotting data for the next 100 seconds? That's way too long. There should be text boxes that allow you to control the time before and after the time of alignment, similar to the PSTH widget |
Yes, in the Murthy conversion the trials are very short so is OK. I will see how to add this for the general case though. Right now the trials are aligned to the beginning of the trial. What do you suggest doing when there is no data before. As in, here there is only data within the trials and they are non-contiguous? My default is returning none in this case. |
I will take a look into the x-axis locking while zooming on faceting thing @CodyCBakerPhD @bendichter any idea why would this not render from within the widget accordion but would render on its own? |
I've seen that sometimes if the figure isn't a |
OK, I did this in the last commit. I thought that the control that made sense was:
Where both the user selected times (controlled by widgets) could be positive or negative. But looking at the alignment control options in the PSTH: It seems that the plotting interval is: Where the "time_chosen_for_alignment" is usually the start_time but it can be any other column that follows the convention of ending in Why does this make more sense than plotting over the whole trial? In what sense was the plotting interval in @CodyCBakerPhD example way too long you mentioned above? |
@CodyCBakerPhD We can have a checkbox that controls for whether the behavior is active or not if you and @bendichter think that is desirable. |
Yeah an optional toggle is all I'm asking for there. Common axes are a good default, but in cases where some slices are very thin on certain facets it can make more sense to try to expand different axes to note the trends one might see on the zoomed-in scale |
The widget so far: showcase.mp4 |
@h-mayorquin If you want more examples to test/develop with, here's a snippet for using my latest files (recommend using on DANDI Hub for optimal bandwidth speeds) import h5py
import fsspec
from fsspec.implementations.cached import CachingFileSystem
from pynwb import NWBHDF5IO
from nwbwidgets.timeseries import TrializedTimeSeries
s3_url = "https://dandiarchive.s3.amazonaws.com/blobs/297/fd6/297fd6b9-4c28-48c4-8c86-e9ef2a0fb07d"
cfs = CachingFileSystem(
fs=fsspec.filesystem("http"),
cache_storage="/home/jovyan/fsspec_cache", # Local folder for the cache
)
file_system = cfs.open(s3_url, "rb")
file = h5py.File(file_system)
io = NWBHDF5IO(file=file, load_namespaces=True)
nwbfile = io.read()
# Choose series
#roi_response_series = nwbfile.processing["ophys"]["DfOverF"].roi_response_series["NeuronDfOverF"]
roi_response_series = nwbfile.processing["ophys"]["Fluorescence"].roi_response_series["NeuronFluorescence"]
# Choose table
#target_table = nwbfile.processing["behavior"]["SwimIntervals"]
target_table = nwbfile.processing["behavior"]["ActivityStates"]
TrializedTimeSeries(time_series=roi_response_series, trials_table=target_table) |
OK, I had to do some changes but now it renders with |
@h-mayorquin great! What did you need to change? |
There was a silent error because the |
…atalystneuro/nwbwidgets into trialized_time_series_faceting
@CodyCBakerPhD I added a toggle to control the x-locking behavior in the last commit. Could you test it to confirm that is the behavior that you wanted? |
) | ||
else: | ||
return neurodata_vis_spec[NWBDataInterface](df_over_f, neurodata_vis_spec) | ||
return neurodata_vis_spec[NWBDataInterface](df_over_f, neurodata_vis_spec) |
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.
There is a problem here when we make the neurodata_vis_spec[RoiResponseSeries]
an OrderedDict
to include the TrializedTimeSeries
as an option (see the diff in the view.py
module).
Because show_df_over_f
is a callable the function nwb2widget
expects this output to be a visualization (not an ordered dict) and a naive extension of the visualization specification generates an error. The solution I implemented was to roll back the show_df_over_f
to return the visualization for the whole Container/DataInterface
(see the else in the diff representing the old code) and then the visualization can be properly display as a root of the lazy_tab
for that module. That is how the widget is working right now.
The other option was to modify nwb2widget
to handle the case when callables return OrderedDict
but that seemed like a larger modification given that nwb2widget
is such a central function and this might have non-tested side effects in other visualizations. I opted for the simple solution here as that only touches the visualizations concerning the RoiExtractors and DfOverF objects.
return data.flatten() | ||
|
||
|
||
def trialize_time_series( |
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.
Could you use this function?
nwbwidgets/nwbwidgets/utils/timeseries.py
Line 266 in dba1400
def align_by_time_intervals( |
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.
Most of the function implemented there is to build the dataframe into the form where the faceting functionality of plotly can be used.
There is a place inside trialized_time_series
where the data from trials is extracted and also the timestamps. For that part, after your last suggestion above, I took a look a the functions in that utils module for timeseries. I don't think they fit that well here without introducing more unnecessary computation.
Basically, right now I have a vectorized bisect function (as provided by numpy) which I use to extract the set of trialized indexes and then I use them on both the data and the timestamps. The functions in the utils module are non vectorized and they return the data in a list without the indexes which I need for the timestamps. Using align_by_time_intervals
, for example, would reduce the performance here both by losing the vectorization and then by requiring some more computation to get the indexes of the timestamps.
I think the generalization would be to have a general vectorized function that return the trialized indexes. That hypothetical function then can be used both here and inside bisect_timeseries_by_times
to avoid the per trial bisection there.
What do you think?
return empty_figure | ||
|
||
|
||
def calculate_moving_average_over_trials(df, moving_average_window): |
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 specific to your widget. Maybe it would be better as a static method.
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 wanted to separate the logic of plot creation (here in the function build_faceting_figure
) from the logic of the widget. In my view this separation of concerns (composition) makes it easier to modify and test the plotting function. Reason being:
- It is easier for someone who does not know the function to get the plotting function and just use it by itself to understand its behavior. In opposition to this, in the
AlignMultiTraceTimeSeriesByTrialsVariable
you need to go very deep in the function to understand what is the plotting function. - The plotting function can be tested or modified with minimal modification to the widget class. I think keeping the points of contact between the plotting function and all the other widget class responsibilities small leads to better design.
Basically I view the widget class a place with the following responsabilities:
- Data loading from the appropriate neuro data type.
- Control definition (maybe can be done with composition using your concept of foreign controlers).
- Handling the interaction between controls and the (external) plotting function.
What do you think? Are there downsides that I am not seeing for this separation?
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.
That said, I can create a plotting class and make all the methods that are floating now by themselves methods of that class to give the code more coherence.
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 think a better way to organize would be to separate calculation from plotting.
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, this got me thinking:
#239
return df_sort | ||
|
||
|
||
def add_moving_average_traces(figure, df, facet_col, facet_row): |
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 specific to your widget. Maybe it would be better as a static method.
return figure | ||
|
||
|
||
def build_faceting_figure(df, facet_col, facet_row, data_label="data", trial_label="trial"): |
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 specific to your widget. Maybe it would be better as a static method.
self.figure_widget.update_xaxes(matches=matches) | ||
|
||
|
||
def route_trialized_time_series(time_series: TimeSeries, neurodata_vis_spec=None, **kwargs): |
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.
There is no routing going on 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.
It would also be good if you subclassed TrializedTimeSeries
to have proper labels for specific modalities
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 was thinking that would be the role of the routing function. Initializing the visualization -for lack of a better term- to match the neurodata type that is specified for it in the visualization spec dictionary. Besides the column of the data that we are visualizing, what other labels do you think should be changed?
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 done in the last commit and in the routing function.
@h-mayorquin the sorting looks right. Good thinking using natsort. Could you show a screenshot that displays this working correctly? |
Here is some recording of it working on the test data that I have been using: sharing.mp4 |
@bendichter |
The idea is to add a widget for time series that can show trialized data both by filtering (selecting only data where a column -or more- are equal to one value) and faceting (showing the behavior across a grid when filtering for some values).
So far the widget works well as a stand alone:
But does not render properly when called from within:
nwb2widget(nwb)
Then there is no error but it never renders:
@bendichter