Skip to content

Commit 0443dcb

Browse files
authored
Merge pull request #176 from chrishalcrow/greedy-layout
Greedy layout idea
2 parents db389a4 + e73ba68 commit 0443dcb

File tree

6 files changed

+209
-116
lines changed

6 files changed

+209
-116
lines changed

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,23 +200,28 @@ You can create your own custom layout by specifying which views you'd like
200200
to see, and where they go. The basic window layout supports eight "zones",
201201
which are laid out as follows:
202202

203-
```
204-
+-----------------+-----------------+
205-
| [zone1 zone2] | [zone3 | [zone4 |
206-
+-----------------+ | +
207-
| [zone5 zone6] | zone7] | zone8] |
208-
+-----------------+-----------------+
209-
```
203+
+---------------+--------------+
204+
| zone1 zone2 | zone3 zone4 |
205+
+ + +
206+
| zone5 zone6 | zone7 zone8 |
207+
+---------------+--------------+
208+
209+
If a zone has free space below it or to the right of it, it will try to use it.
210+
Stretching downwards takes precedence over stretching rightwards.
211+
E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:
210212

211-
If zones are not included, the other zones take over their space. Hence if you'd
212-
like to show waveforms as a long view, you can set zone3 to display waveforms
213-
and then set zone7 to display nothing. The waveforms in zone3 will take over the
214-
blank space from zone7.
213+
+---------------+--------------+
214+
| zone1 | zone4 |
215+
+ + +
216+
| zone5 zone6 | zone7 |
217+
+---------------+--------------+
218+
219+
Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
220+
downwards to make a long two-zone view.
215221

216222
To specify your own layout, put the specification in a `.json` file. This should
217223
be a list of zones, and which views should appear in which zones. An example:
218224

219-
220225
**my_layout.json**
221226
```
222227
{

spikeinterface_gui/backend_panel.py

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import param
22
import panel as pn
3-
3+
import numpy as np
4+
from copy import copy
45

56
from .viewlist import possible_class_views
67
from .layout_presets import get_layout_description
8+
from .utils_global import get_size_bottom_row, get_size_top_row
79

810
# Used by views to emit/trigger signals
911
class SignalNotifier(param.Parameterized):
@@ -182,8 +184,6 @@ def listen_setting_changes(view):
182184
for setting_data in view._settings:
183185
view.settings._parameterized.param.watch(view.on_settings_changed, setting_data["name"])
184186

185-
186-
187187
class PanelMainWindow:
188188

189189
def __init__(self, controller, layout_preset=None, layout=None):
@@ -283,57 +283,29 @@ def create_main_layout(self):
283283
allow_drag=False,
284284
)
285285

286-
all_zones = [f'zone{a}' for a in range(1,9)]
287-
is_zone = [(layout_zone.get(zone) is not None) and (len(layout_zone.get(zone)) > 0) for zone in all_zones]
288-
289-
# Get number of columns and rows per sub-region
290-
num_cols_12 = max(is_zone[0]*1 + is_zone[1]*1, is_zone[4]*1 + is_zone[5]*1)
291-
num_cols_56 = num_cols_12
292-
num_cols_37 = (is_zone[2] or is_zone[6])*1
293-
294-
num_rows_12 = ((is_zone[0] or is_zone[1])*1 - (is_zone[4] or is_zone[5])*1) + 1
295-
296-
# Do sub-regions [1,2], [4,5] [3][7] and [5][8] separately. For each, find out if
297-
# both zones are present. If they are, place both in sub-region. If not, place one.
298-
299-
row_slice_12 = slice(0,num_rows_12)
300-
if (is_zone[0] and is_zone[1]):
301-
gs[row_slice_12, slice(0,1)] = layout_zone.get('zone1')
302-
gs[row_slice_12, slice(1,2)] = layout_zone.get('zone2')
303-
elif (is_zone[0] and not is_zone[1]):
304-
gs[row_slice_12, slice(0,num_cols_12)] = layout_zone.get('zone1')
305-
elif (is_zone[1] and not is_zone[0]):
306-
gs[row_slice_12, slice(0,num_cols_12)] = layout_zone.get('zone2')
307-
308-
row_slice_56 = slice(num_rows_12, 2)
309-
if (is_zone[4] and is_zone[5]):
310-
gs[row_slice_56, slice(0,1)] = layout_zone.get('zone5')
311-
gs[row_slice_56, slice(1,2)] = layout_zone.get('zone6')
312-
elif (is_zone[4] and not is_zone[5]):
313-
gs[row_slice_56, slice(0,num_cols_56)] = layout_zone.get('zone5')
314-
elif (is_zone[5] and not is_zone[4]):
315-
gs[row_slice_56, slice(0,num_cols_56)] = layout_zone.get('zone6')
316-
317-
col_slice_37 = slice(num_cols_12,num_cols_12+1)
318-
if is_zone[2] and is_zone[6]:
319-
gs[slice(0, 1), col_slice_37] = layout_zone.get('zone3')
320-
gs[slice(1, 2), col_slice_37] = layout_zone.get('zone7')
321-
elif is_zone[2] and not is_zone[6]:
322-
gs[slice(0, 2), col_slice_37] = layout_zone.get('zone3')
323-
elif is_zone[6] and not is_zone[2]:
324-
gs[slice(0, 2), col_slice_37] = layout_zone.get('zone7')
325-
326-
col_slice_48 = slice(num_cols_12+num_cols_37,num_cols_12+num_cols_37+1)
327-
if is_zone[3] and is_zone[7]:
328-
gs[slice(0, 1), col_slice_48] = layout_zone.get('zone4')
329-
gs[slice(1, 2), col_slice_48] = layout_zone.get('zone8')
330-
elif is_zone[3] and not is_zone[7]:
331-
gs[slice(0, 2), col_slice_48] = layout_zone.get('zone4')
332-
elif is_zone[7] and not is_zone[3]:
333-
gs[slice(0, 2), col_slice_48] = layout_zone.get('zone8')
286+
gs = self.make_half_layout(gs, ['zone1', 'zone2', 'zone5', 'zone6'], layout_zone, 0)
287+
gs = self.make_half_layout(gs, ['zone3', 'zone4', 'zone7', 'zone8'], layout_zone, 2)
334288

335289
self.main_layout = gs
336290

291+
def make_half_layout(self, gs, all_zones, layout_zone, shift):
292+
293+
is_zone = [(layout_zone.get(zone) is not None) and (len(layout_zone.get(zone)) > 0) for zone in all_zones]
294+
is_zone_array = np.reshape(is_zone, (2,2))
295+
original_zone_array = copy(is_zone_array)
296+
297+
for zone_index, zone_name in enumerate(all_zones):
298+
row = zone_index // 2
299+
col = zone_index % 2
300+
if row == 0:
301+
num_rows, num_cols = get_size_top_row(row, col, is_zone_array, original_zone_array)
302+
elif row == 1:
303+
num_rows, num_cols = get_size_bottom_row(row, col, is_zone_array, original_zone_array)
304+
if num_rows > 0 and num_cols > 0:
305+
gs[slice(row, row + num_rows), slice(col+shift,col+num_cols+shift)] = layout_zone.get(zone_name)
306+
307+
return gs
308+
337309
def update_visibility(self, event):
338310
active = event.new
339311
tab_names = event.obj._names

spikeinterface_gui/backend_qt.py

Lines changed: 103 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
from .myqt import QT
22
import pyqtgraph as pg
33
import markdown
4-
4+
import numpy as np
5+
from copy import copy
56

67
import weakref
78

89
from .viewlist import possible_class_views
910
from .layout_presets import get_layout_description
11+
from .utils_global import get_size_bottom_row, get_size_top_row
1012

1113
from .utils_qt import qt_style, add_stretch_to_qtoolbar
1214

13-
1415
# Used by views to emit/trigger signals
1516
class SignalNotifier(QT.QObject):
1617
spike_selection_changed = QT.pyqtSignal()
@@ -153,7 +154,6 @@ def __init__(self, controller, parent=None, layout_preset=None, layout=None):
153154
# refresh do not work because view are not yet visible at init
154155
view._refresh()
155156
self.controller.signal_handler.activate()
156-
157157
# TODO sam : all veiws are always refreshed at the moment so this is useless.
158158
# uncommen this when ViewBase.is_view_visible() work correctly
159159
# for view_name, dock in self.docks.items():
@@ -200,47 +200,8 @@ def create_main_layout(self):
200200
view_names = [view_name for view_name in view_names if view_name in self.views.keys()]
201201
widgets_zone[zone] = view_names
202202

203-
## Handle left
204-
first_left = None
205-
for zone in ['zone1', 'zone5', 'zone2', 'zone6']:
206-
if len(widgets_zone[zone]) == 0:
207-
continue
208-
view_name = widgets_zone[zone][0]
209-
dock = self.docks[view_name]
210-
if len(widgets_zone[zone]) > 0 and first_left is None:
211-
self.addDockWidget(areas['left'], dock)
212-
first_left = view_name
213-
elif zone == 'zone5':
214-
self.splitDockWidget(self.docks[first_left], dock, orientations['vertical'])
215-
elif zone == 'zone2':
216-
self.splitDockWidget(self.docks[first_left], dock, orientations['horizontal'])
217-
elif zone == 'zone6':
218-
if len(widgets_zone['zone5']) > 0:
219-
z = widgets_zone['zone5'][0]
220-
self.splitDockWidget(self.docks[z], dock, orientations['horizontal'])
221-
else:
222-
self.splitDockWidget(self.docks[first_left], dock, orientations['vertical'])
223-
224-
## Handle right
225-
first_top = None
226-
for zone in ['zone3', 'zone4', 'zone7', 'zone8']:
227-
if len(widgets_zone[zone]) == 0:
228-
continue
229-
view_name = widgets_zone[zone][0]
230-
dock = self.docks[view_name]
231-
if len(widgets_zone[zone]) > 0 and first_top is None:
232-
self.addDockWidget(areas['right'], dock)
233-
first_top = view_name
234-
elif zone == 'zone4':
235-
self.splitDockWidget(self.docks[first_top], dock, orientations['horizontal'])
236-
elif zone == 'zone7':
237-
self.splitDockWidget(self.docks[first_top], dock, orientations['vertical'])
238-
elif zone == 'zone8':
239-
if len(widgets_zone['zone4']) > 0:
240-
z = widgets_zone['zone4'][0]
241-
self.splitDockWidget(self.docks[z], dock, orientations['vertical'])
242-
else:
243-
self.splitDockWidget(self.docks[first_top], dock, orientations['horizontal'])
203+
self.make_dock(widgets_zone, ['zone1', 'zone2', 'zone5', 'zone6'], "left", col_shift=0)
204+
self.make_dock(widgets_zone, ['zone3', 'zone4', 'zone7', 'zone8'], "right", col_shift=2)
244205

245206
# make tabs
246207
for zone, view_names in widgets_zone.items():
@@ -256,6 +217,104 @@ def create_main_layout(self):
256217
# make visible the first of each zone
257218
self.docks[view_name0].raise_()
258219

220+
def make_dock(self, widgets_zone, all_zones, side_of_window, col_shift):
221+
222+
all_zones_array = np.transpose(np.reshape(all_zones, (2,2)))
223+
is_zone = np.array([(widgets_zone.get(zone) is not None) and (len(widgets_zone.get(zone)) > 0) for zone in all_zones])
224+
is_zone_array = np.reshape(is_zone, (2,2))
225+
226+
# If the first non-zero zero (from left to right) is on the bottom, move it up
227+
for column_index, zones_in_columns in enumerate(is_zone_array):
228+
if np.any(zones_in_columns):
229+
first_is_top = zones_in_columns[0]
230+
if not first_is_top:
231+
top_zone = f"zone{column_index+1+col_shift}"
232+
bottom_zone = f"zone{column_index+5+col_shift}"
233+
widgets_zone[top_zone] = widgets_zone[bottom_zone]
234+
widgets_zone[bottom_zone] = []
235+
continue
236+
237+
is_zone = np.array([(widgets_zone.get(zone) is not None) and (len(widgets_zone.get(zone)) > 0) for zone in all_zones])
238+
is_zone_array = np.reshape(is_zone, (2,2))
239+
original_zone_array = copy(is_zone_array)
240+
241+
# First we split horizontally any columns which are two rows long.
242+
# For later, group the zones between these splits
243+
all_groups = []
244+
group = []
245+
for col_index, zones in enumerate(all_zones_array):
246+
col = col_index % 2
247+
is_a_zone = original_zone_array[:,col]
248+
num_row_0, _ = get_size_top_row(0, col, is_zone_array, original_zone_array)
249+
# this function affects is_zone_array so must be run
250+
_, _ = get_size_bottom_row(1, col, is_zone_array, original_zone_array)
251+
252+
if num_row_0 == 2:
253+
if len(group) > 0:
254+
all_groups.append(group)
255+
group = []
256+
allowed_zones = zones[is_a_zone]
257+
all_groups.append(allowed_zones)
258+
else:
259+
for zone in zones[is_a_zone]:
260+
group.append(zone)
261+
262+
if len(group) > 0:
263+
all_groups.append(group)
264+
265+
if len(all_groups) == 0:
266+
return
267+
268+
first_zone = all_groups[0][0]
269+
first_dock = widgets_zone[first_zone][0]
270+
dock = self.docks[first_dock]
271+
self.addDockWidget(areas[side_of_window], dock)
272+
273+
for group in reversed(all_groups[1:]):
274+
digits = np.array([int(s[-1]) for s in group])
275+
sorted_indices = np.argsort(digits)
276+
sorted_arr = np.array(group)[sorted_indices]
277+
view_name = widgets_zone[sorted_arr[0]][0]
278+
dock = self.docks[view_name]
279+
self.splitDockWidget(self.docks[first_dock], dock, orientations['horizontal'])
280+
281+
# Now take each sub-group, and split vertically if appropriate
282+
new_all_groups = []
283+
for group in all_groups:
284+
285+
if len(group) == 1:
286+
# if only one in group, not need to split
287+
continue
288+
289+
top_zones = [zone for zone in group if zone in ['zone1', 'zone2', 'zone3', 'zone4']]
290+
bottom_zones = [zone for zone in group if zone in ['zone5', 'zone6', 'zone7', 'zone8']]
291+
new_all_groups.append([top_zones, bottom_zones])
292+
293+
if len(top_zones) > 0 and len(bottom_zones) > 0:
294+
295+
top_view_name = widgets_zone[top_zones[0]][0]
296+
top_dock = self.docks[top_view_name]
297+
298+
bottom_view_name = widgets_zone[bottom_zones[0]][0]
299+
bottom_dock = self.docks[bottom_view_name]
300+
301+
self.splitDockWidget(top_dock, bottom_dock, orientations['vertical'])
302+
303+
# Finally, split all the sub-sub-groups horizontally
304+
for top_bottom_groups in new_all_groups:
305+
for group in top_bottom_groups:
306+
307+
if len(group) <= 1:
308+
# if only one in group, no need to split
309+
continue
310+
311+
first_zone_name = widgets_zone[group[0]][0]
312+
for zone in reversed(group[1:]):
313+
zone_name = widgets_zone[zone][0]
314+
self.splitDockWidget(self.docks[first_zone_name], self.docks[zone_name], orientations['horizontal'])
315+
316+
317+
259318
# used by to tell the launcher this is closed
260319
def closeEvent(self, event):
261320
self.main_window_closed.emit(self)

spikeinterface_gui/layout_presets.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,28 @@
11
import json
22
from spikeinterface_gui.viewlist import possible_class_views
3+
import numpy as np
34

45
"""
5-
A preset need 8 zones like this:
6+
A preset depends on eight zones, which are split into two sub-regions
67
7-
+-----------------+-----------------+
8-
| [zone1 zone2] | [zone3 | [zone4 |
9-
+-----------------+ | +
10-
| [zone5 zone6] | zone7] | zone8] |
11-
+-----------------+-----------------+
8+
+---------------+--------------+
9+
| zone1 zone2 | zone3 zone4 |
10+
+ + +
11+
| zone5 zone6 | zone7 zone8 |
12+
+---------------+--------------+
1213
14+
If a zone has free space below it or to the right of it, it will try to use it.
15+
Stretching downwards takes precedence over stretching rightwards.
16+
E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:
17+
18+
+---------------+--------------+
19+
| zone1 | zone4 |
20+
+ + +
21+
| zone5 zone6 | zone7 |
22+
+---------------+--------------+
23+
24+
Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
25+
downwards to make a long two-zone view.
1326
"""
1427
_presets = {}
1528

@@ -38,7 +51,6 @@ def get_layout_description(preset_name, layout=None):
3851
preset_name = 'default'
3952
return _presets[preset_name]
4053

41-
4254
default_layout = dict(
4355
zone1=['curation', 'spikelist'],
4456
zone2=['unitlist', 'mergelist'],

spikeinterface_gui/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def run_mainwindow(
121121

122122

123123
app = mkQApp()
124-
124+
125125
win = QtMainWindow(controller, layout_preset=layout_preset, layout=layout)
126126
win.setWindowTitle('SpikeInterface GUI')
127127
# Set window icon
@@ -273,7 +273,7 @@ def run_mainwindow_cli():
273273
if args.verbose:
274274
print('Loading analyzer...')
275275
assert check_folder_is_analyzer(analyzer_folder), f'The folder {analyzer_folder} is not a valid SortingAnalyzer folder'
276-
analyzer = load_sorting_analyzer(analyzer_folder, load_extensions=not is_path_remote(analyzer_folder))
276+
analyzer = load_sorting_analyzer(analyzer_folder, load_extensions=False)
277277
if args.verbose:
278278
print('Analyzer loaded')
279279

@@ -312,3 +312,4 @@ def run_mainwindow_cli():
312312
layout=args.layout_file,
313313
curation_dict=curation_data,
314314
)
315+

0 commit comments

Comments
 (0)