Skip to content

Commit

Permalink
WID-223: add 3d viewer, show points/edges, resize plot, toggle operat…
Browse files Browse the repository at this point in the history
…ions
  • Loading branch information
davidbacter01 committed Sep 11, 2023
1 parent 7b8fd7e commit c9261da
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 31 deletions.
25 changes: 18 additions & 7 deletions notebooks/Connectivity.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,18 @@
"name": "stdout",
"output_type": "stream",
"text": [
"29-08-2023 02:49:39 - DEBUG - tvbwidgets - Package is not fully installed\n",
"29-08-2023 02:49:39 - DEBUG - tvbwidgets - Version read from the internal package.json file\n",
"29-08-2023 02:49:39 - INFO - tvbwidgets - Version: 1.5.0\n",
"2023-08-29 14:49:45,596 - INFO - tvb.storage.h5.encryption.data_encryption_handler - Cannot import syncrypto library.\n",
"29-08-2023 02:49:45 - INFO - tvbwidgets.core.pse.parameters - ImportError: Dask dependency is not included, so this functionality won't be available\n",
"2023-08-29 14:49:45,789 - WARNING - tvb.basic.readers - File 'hemispheres' not found in ZIP.\n"
"11-09-2023 10:13:24 - DEBUG - tvbwidgets - Package is not fully installed\n",
"11-09-2023 10:13:24 - DEBUG - tvbwidgets - Version read from the internal package.json file\n",
"11-09-2023 10:13:24 - INFO - tvbwidgets - Version: 1.5.0\n",
"2023-09-11 10:13:29,835 - INFO - tvb.storage.h5.encryption.data_encryption_handler - Cannot import syncrypto library.\n",
"11-09-2023 10:13:29 - INFO - tvbwidgets.core.pse.parameters - ImportError: Dask dependency is not included, so this functionality won't be available\n",
"2023-09-11 10:13:29,943 - WARNING - tvb.basic.readers - File 'hemispheres' not found in ZIP.\n"
]
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "adc5ca9edfd244aa95d31e693301679e",
"model_id": "3303c8f259414948800906cc6eb8ae24",
"version_major": 2,
"version_minor": 0
},
Expand All @@ -60,14 +60,25 @@
"source": [
"from tvbwidgets.api import ConnectivityWidget\n",
"from tvb.datatypes.connectivity import Connectivity\n",
"import pyvista as pv\n",
"import numpy as np\n",
"pv.set_jupyter_backend('pythreejs')\n",
"\n",
"conn = Connectivity.from_file() # defaults to connectivy_76.zip\n",
"conn.configure()\n",
"\n",
"wid = ConnectivityWidget(conn)\n",
"\n",
"display(wid)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
Expand Down
19 changes: 19 additions & 0 deletions tvbwidgets/ui/connectivity_ipy/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# "TheVirtualBrain - Widgets" package
#
# (c) 2022-2023, TVB Widgets Team
#
from dataclasses import dataclass
from numpy import ndarray


@dataclass
class ConnectivityConfig:
name: str = 'Connectivity'
style: str = 'Points'
points_color: str = 'Green'
edge_color: str = 'White'
light: bool = True
size = [500, 500] # [width, height]
point_size: int = 20
132 changes: 108 additions & 24 deletions tvbwidgets/ui/connectivity_ipy/connectivity_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,22 @@
#
# (c) 2022-2023, TVB Widgets Team
#
import dataclasses

import ipywidgets
import pyvista
import matplotlib
import numpy
import pyvista as pv
import numpy as np
from numpy import ndarray
from tvb.basic.neotraits._attr import NArray
from tvb.basic.neotraits.api import HasTraits
from tvb.datatypes.connectivity import Connectivity
from tvbwidgets.ui.base_widget import TVBWidget

pyvista.set_jupyter_backend('pythreejs')
from tvbwidgets.ui.connectivity_ipy.outputs_3d import PyVistaOutput
from tvbwidgets.ui.connectivity_ipy.config import ConnectivityConfig

DROPDOWN_KEY = 'dropdown'


@dataclasses.dataclass
class ConnectivityConfig:
name: str = 'Connectivity'
style: str = 'Surface'
color: str = 'White'
light: bool = True
size: int = 1500
cmap: str | None = None
scalars: ndarray | NArray = None
widget: ipywidgets.Widget = None


class CustomOutput(ipywidgets.Output):
CONFIG = ConnectivityConfig()

Expand Down Expand Up @@ -105,10 +93,109 @@ def on_change(change):
self.children = (dropdown, *self.children)


class Connectivity3DViewer(ipywidgets.HBox):
class Connectivity3DViewer(ipywidgets.VBox):
PYVISTA = 'PyVista'

def __init__(self, connectivity, **kwargs):
super(Connectivity3DViewer, self).__init__(*kwargs)
self.children = [ipywidgets.HTML(value='Placeholder for 3d viewer')]
self.connectivity = connectivity

self.output = PyVistaOutput()

super(Connectivity3DViewer, self).__init__([self.output], *kwargs)

self.init_view_connectivity()

def init_view_connectivity(self):
points, edges, labels = self.add_actors()
points_toggle, edges_toggle, labels_toggle = self._init_controls()
if not labels_toggle.value:
self.output.hide_actor(labels)

def on_change_points(change):
if change['new']:
self.output.display_actor(points)
else:
self.output.hide_actor(points)
self.output.update_plot()

points_toggle.observe(on_change_points, 'value')

def on_change_edges(change):
if change['new']:
self.output.display_actor(edges)
else:
self.output.hide_actor(edges)
self.output.update_plot()

edges_toggle.observe(on_change_edges, 'value')

def on_change_labels(change):
if change['new']:
self.output.display_actor(labels)
else:
self.output.hide_actor(labels)
self.output.update_plot()

labels_toggle.observe(on_change_labels, 'value')

window_controls = self.output.get_window_controls()

self.children = [
ipywidgets.HBox(children=(
points_toggle, edges_toggle, labels_toggle)),
window_controls,
self.output]
self.output.display_actor(points)
self.output.display_actor(edges)
self.output.update_plot()

def _init_controls(self):
points_toggle = ipywidgets.ToggleButton(value=True,
description='Points'
)
edges_toggle = ipywidgets.ToggleButton(value=True,
description='Edges',
)

labels_toggle = ipywidgets.ToggleButton(value=False,
description='Labels')
return points_toggle, edges_toggle, labels_toggle

def add_actors(self):
plotter = self.output.plotter
points = self.connectivity.centres

mesh_points = pv.PolyData(points)

labels = self.connectivity.region_labels
labels_actor = plotter.add_point_labels(points, labels)

points_color = self.output.CONFIG.points_color
points_size = self.output.CONFIG.point_size
edge_color = self.output.CONFIG.edge_color

points_actor = plotter.add_points(mesh_points, color=points_color, point_size=points_size)

edges_coords = self._extract_edges()

edges_actor = plotter.add_lines(edges_coords, color=edge_color, width=1)
plotter.camera_position = 'xy'

return points_actor, edges_actor, labels_actor

def _extract_edges(self):
connectivity = self.connectivity
edge_indices = np.nonzero(connectivity.weights)
edges = list(zip(edge_indices[0], edge_indices[1]))

edges_coords = []
points = connectivity.centres

for (i, j) in edges:
edges_coords.append(points[i])
edges_coords.append(points[j])

return numpy.array(edges_coords)


class ConnectivityOperations(ipywidgets.VBox, TVBWidget):
Expand Down Expand Up @@ -177,10 +264,7 @@ def on_change_operations(c):
ipywidgets.VBox(children=(
viewers_checkbox,
operations_checkbox
)
)

)
)))
),
sections_container
]
Expand Down
10 changes: 10 additions & 0 deletions tvbwidgets/ui/connectivity_ipy/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
#
# "TheVirtualBrain - Widgets" package
#
# (c) 2022-2023, TVB Widgets Team
#
class UnknownOutputException(Exception):
"""
Exception thrown in case of an unknown output type
"""
84 changes: 84 additions & 0 deletions tvbwidgets/ui/connectivity_ipy/outputs_3d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# "TheVirtualBrain - Widgets" package
#
# (c) 2022-2023, TVB Widgets Team
#
import ipywidgets

from tvbwidgets.ui.connectivity_ipy.config import ConnectivityConfig
from tvbwidgets.ui.connectivity_ipy.exceptions import UnknownOutputException
from enum import Enum
import ipywidgets as widgets
import pyvista


class Output3D(Enum):
PYVISTA = 'PyVista'

def __str__(self):
return str(self.value)


class PyVistaOutput(widgets.Output):
CONFIG = ConnectivityConfig()
plotter = pyvista.Plotter()

def toggle_actor(self, actor, visible):
if visible:
self.display_actor(actor)
else:
self.hide_actor(actor)
self.update_plot()

def display_actor(self, actor):
self.plotter.renderer.add_actor(actor, render=False)

def hide_actor(self, actor):
self.plotter.renderer.remove_actor(actor, render=False)

def update_plot(self):
with self:
self.clear_output(wait=True)
self.plotter.show()

def get_window_controls(self):
height = ipywidgets.IntSlider(
value=self.CONFIG.size[1],
min=50,
max=1500,
step=1,
orientation='horizontal',
description='Plot height',
continuous_update=False,
)
width = ipywidgets.IntSlider(
value=self.CONFIG.size[0],
min=50,
max=1500,
step=1,
orientation='horizontal',
description='Plot width',
continuous_update=False,
)

self.plotter.window_size = [width.value, height.value]

def on_change_height(value):
self.plotter.window_size = [width.value, value['new']]
self.update_plot()

def on_change_width(value):
self.plotter.window_size = [value['new'], height.value]
self.update_plot()

height.observe(on_change_height, 'value')
width.observe(on_change_width, 'value')
return ipywidgets.HBox(children=(width, height))


def output_3d_factory(output_type):
"""Factory function for a custom 3d output"""
if output_type == Output3D.PYVISTA:
return PyVistaOutput()
raise UnknownOutputException(f"No applicable output for {output_type}!")

0 comments on commit c9261da

Please sign in to comment.