Source code for fstg_toolkit.app.views.subject

# Copyright 2025 ICube (University of Strasbourg - CNRS)
# author: Julien PONTABRY (ICube)
#
# This software is a computer program whose purpose is to provide a toolkit
# to model, process and analyze the longitudinal reorganization of brain
# connectivity data, as functional MRI for instance.
#
# This software is governed by the CeCILL-B license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/or redistribute the software under the terms of the CeCILL-B
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL-B license and that you accept its terms.

import dash_bootstrap_components as dbc
from dash import Input, Output, State, callback, dcc, html, clientside_callback, ClientsideFunction
from dash.dependencies import ALL
from dash.exceptions import PreventUpdate

from .common import update_factor_controls, plotly_config
from ..figures.subject import (build_subject_figure, generate_temporal_graph_props, build_spatial_figure,
                               generate_spatial_graph_props, NODE_PROP_LABELS)
from ...io import GraphsDataset

layout = [
    html.Div([], id='subject-factors-block'),
    dbc.Row(
        dcc.Dropdown([], clearable=False, id='subject-selection')
    ),
    dbc.Row([
        dbc.Col(dcc.Dropdown([], multi=True, placeholder="Select regions...", id='regions-selection'), width=11),
        dbc.Col(dbc.Button("Apply", color='secondary', id='apply-button'),
                className='d-grid gap-2 d-md-block', align='center')
    ], className='g-0'),
    dbc.Row([
        dbc.Col([
            dbc.Label("Node color", size='sm'),
            dcc.Dropdown(
                options=[{'label': v, 'value': k} for k, v in NODE_PROP_LABELS.items()],
                value='internal_strength', clearable=False, id='node-color-prop'
            ),
        ], width=6),
        dbc.Col([
            dbc.Label("Node size", size='sm'),
            dcc.Dropdown(
                options=[{'label': v, 'value': k} for k, v in NODE_PROP_LABELS.items()],
                value='efficiency', clearable=False, id='node-size-prop'
            ),
        ], width=6),
    ], className='mt-1'),
    dbc.Row(
        dcc.Loading(
            children=[dcc.Graph(figure={}, id='st-graph', config=plotly_config)],
            type='circle', overlay_style={"visibility": "visible", "filter": "blur(2px)"}
        )
    ),
    dbc.Modal([
        dbc.ModalHeader(dbc.ModalTitle("Spatial view", id='modal-sp-title')),
        dbc.ModalBody(html.Div(dcc.Graph(figure={}, id='sp-graph',
                                         config=dict(**plotly_config,
                                                     modeBarButtonsToRemove=['zoom', 'pan', 'zoomIn', 'zoomOut',
                                                                             'autoScale', 'resetScale']),
                                         style={'width': '100%', 'height': '100%'}),
                               style={'width': 'min(70vw, 70vh)', 'aspectRatio': '1 / 1', 'margin': '0 auto'}
            )),
    ], id='modal-sp-graph', size='xl', centered=True, is_open=False),

    dcc.Store(id='store-spatial-connections', storage_type='memory'),
]


[docs] @callback( Output('subject-factors-block', 'children'), Output('regions-selection', 'options'), Output('regions-selection', 'value'), Input('store-dataset', 'data') ) def dataset_changed(store_dataset): if store_dataset is None: raise PreventUpdate dataset = GraphsDataset.deserialize(store_dataset) # update the layout of the factors' controls factor_controls_layout = update_factor_controls('subject', dataset.factors, multi=False) # update the selectable regions regions = dataset.areas_desc.sort_values("Name_Region")["Name_Region"].unique().tolist() return factor_controls_layout, regions, regions
[docs] @callback( Output('subject-selection', 'options'), Output('subject-selection', 'value'), Input({'type': 'subject-factor', 'index': ALL}, 'value'), State('store-dataset', 'data'), State('subject-selection', 'value'), prevent_initial_call=True ) def factors_changed(factor_values, store_dataset, current_selection): if store_dataset is None or factor_values is None: raise PreventUpdate # filter subjects based on selected factors # records contains "factors" ... "subject" "graph filename" ["matrix filename"] # we are interested in the elements until the subject n = len(store_dataset['factors']) ids = [tuple(record.values()) for record in store_dataset['subjects']] filtered_ids = filter(lambda k: all(f in factor_values for f in k[:n]), ids) filtered_ids = sorted([k[n] for k in filtered_ids]) # do not select a new subject in the filtered list if the old one is also in the filtered list selection = current_selection if current_selection in filtered_ids else next(iter(filtered_ids), None) return filtered_ids, selection
[docs] @callback( Output('st-graph', 'figure'), Output('store-spatial-connections', 'data'), Output('apply-button', 'disabled'), Input('apply-button', 'n_clicks'), Input('subject-selection', 'value'), Input('node-color-prop', 'value'), Input('node-size-prop', 'value'), State('regions-selection', 'value'), State({'type': 'subject-factor', 'index': ALL}, 'value'), State('store-dataset', 'data'), prevent_initial_call=True ) def selection_changed(n_clicks, subject, color_prop, size_prop, regions, factor_values, store_dataset): if (n_clicks is not None and n_clicks <= 0) or store_dataset is None: raise PreventUpdate if subject is None or len(factor_values) == 0: raise PreventUpdate # check if the graph is in the dataset ids = tuple(factor_values + [subject]) dataset = GraphsDataset.deserialize(store_dataset) if ids not in dataset: raise PreventUpdate # loads the dataset and create figure properties from the loaded graph graph = dataset.get_graph(ids) figure_props = generate_temporal_graph_props(graph, regions, color_prop, size_prop) areas = dataset.areas_desc['Name_Area'] return build_subject_figure(figure_props, areas), figure_props['spatial_connections'], True
[docs] @callback( Output('apply-button', 'disabled', allow_duplicate=True), Input('regions-selection', 'value'), prevent_initial_call=True ) def regions_selection_changed(regions): return regions is None or len(regions) == 0
clientside_callback( ClientsideFunction(namespace='clientside', function_name='subject_node_hover'), Output('st-graph', 'style'), # NOTE this is a workaround to ensure the clientside callback is registered Input('st-graph', 'hoverData'), State('store-spatial-connections', 'data'), prevent_initial_call=True ) # NOTE workaround to remove the hover elements on the graph when the mouse gets out of the figure clientside_callback( ClientsideFunction(namespace='clientside', function_name='subject_clear_out'), Output('st-graph', 'id'), Input('st-graph', 'id') )
[docs] @callback( Output('sp-graph', 'figure'), Output('modal-sp-graph', 'is_open'), Output('modal-sp-title', 'children'), Input('st-graph', 'clickData'), State('store-dataset', 'data'), State('regions-selection', 'value'), State({'type': 'subject-factor', 'index': ALL}, 'value'), State('subject-selection', 'value'), prevent_initial_call=True, ) def graph_clicked(click_data, store_dataset, regions, factor_values, subject): if store_dataset is None or click_data is None: raise PreventUpdate # get time point value t = click_data['points'][0]['x'] # check if the graph is in the dataset ids = tuple(factor_values + [subject]) dataset = GraphsDataset.deserialize(store_dataset) if ids not in dataset: raise PreventUpdate # loads the dataset and create figure properties from the loaded graph graph = dataset.get_graph(ids) figure_props = generate_spatial_graph_props(graph.sub(t=t), dataset.areas_desc, regions) return build_spatial_figure(figure_props), True, f"Spatial view at t={t}"