Skip to content

Commit 17dab94

Browse files
authored
Merge pull request #179 from chrishalcrow/refactory-greedy-layout
Refactor greedy layout
2 parents 0443dcb + 6e8ab5a commit 17dab94

File tree

4 files changed

+167
-155
lines changed

4 files changed

+167
-155
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,21 +200,25 @@ 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+
```
203204
+---------------+--------------+
204205
| zone1 zone2 | zone3 zone4 |
205206
+ + +
206207
| zone5 zone6 | zone7 zone8 |
207208
+---------------+--------------+
209+
```
208210

209211
If a zone has free space below it or to the right of it, it will try to use it.
210212
Stretching downwards takes precedence over stretching rightwards.
211213
E.g. suppose your layout is only non-empty in zones 1, 4, 5, 6 and 7:
212214

215+
```
213216
+---------------+--------------+
214217
| zone1 | zone4 |
215218
+ + +
216219
| zone5 zone6 | zone7 |
217220
+---------------+--------------+
221+
```
218222

219223
Then zone1 will stretch right-wards to make a three-zone view. Zone4 will stretch
220224
downwards to make a long two-zone view.

spikeinterface_gui/backend_panel.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

66
from .viewlist import possible_class_views
77
from .layout_presets import get_layout_description
8-
from .utils_global import get_size_bottom_row, get_size_top_row
9-
8+
from .utils_global import fill_unnecessary_space, get_present_zones_in_half_of_layout
109
# Used by views to emit/trigger signals
1110
class SignalNotifier(param.Parameterized):
1211
spike_selection_changed = param.Event()
@@ -283,26 +282,64 @@ def create_main_layout(self):
283282
allow_drag=False,
284283
)
285284

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)
285+
gs = self.make_half_layout(gs, layout_zone, "left")
286+
gs = self.make_half_layout(gs, layout_zone, "right")
288287

289288
self.main_layout = gs
290289

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)
290+
def make_half_layout(self, gs, layout_zone, left_or_right):
291+
"""
292+
Function contains the logic for the greedy layout. Given the 2x2 box of zones
293+
294+
1 2 3 4
295+
5 6 or 7 8
296+
297+
Then depending on which zones are non-zero, a different layout is generated using splits.
298+
299+
The zone indices in the second box (34,78) are equal to the zone indices first box (12,56)
300+
shifted by 2. We take advantage of this fact.
301+
"""
302+
303+
shift = 0 if left_or_right == "left" else 2
304+
305+
layout_zone = fill_unnecessary_space(layout_zone, shift)
306+
present_zones = get_present_zones_in_half_of_layout(layout_zone, shift)
307+
308+
# `fill_unnecessary_space` ensures that zone{1+shift} always exists
309+
if present_zones == set([f'zone{1+shift}']):
310+
gs[0,0] = layout_zone.get(f'zone{1+shift}')
311+
312+
# Layouts with two non-zero zones
313+
if present_zones == set([f'zone{1+shift}', f'zone{2+shift}']):
314+
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
315+
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
316+
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}']):
317+
gs[slice(0, 1), slice(0+shift,2+shift)] = layout_zone.get(f'zone{1+shift}')
318+
gs[slice(1, 2), slice(0+shift,2+shift)] = layout_zone.get(f'zone{5+shift}')
319+
elif present_zones == set([f'zone{1+shift}', f'zone{6+shift}']):
320+
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
321+
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')
322+
323+
# Layouts with three non-zero zones
324+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}']):
325+
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
326+
gs[slice(0, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
327+
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
328+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{6+shift}']):
329+
gs[slice(0, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
330+
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
331+
gs[slice(1, 2), slice(1+shift,1+shift)] = layout_zone.get(f'zone{6+shift}')
332+
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}', f'zone{6+shift}']):
333+
gs[slice(0, 1), slice(0+shift,2+shift)] = layout_zone.get(f'zone{1+shift}')
334+
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
335+
gs[slice(1, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')
336+
337+
# Layouts with four non-zero zones
338+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']):
339+
gs[slice(0, 1), slice(0+shift,1+shift)] = layout_zone.get(f'zone{1+shift}')
340+
gs[slice(0, 1), slice(1+shift,2+shift)] = layout_zone.get(f'zone{2+shift}')
341+
gs[slice(1, 2), slice(0+shift,1+shift)] = layout_zone.get(f'zone{5+shift}')
342+
gs[slice(1, 2), slice(1+shift,2+shift)] = layout_zone.get(f'zone{6+shift}')
306343

307344
return gs
308345

spikeinterface_gui/backend_qt.py

Lines changed: 61 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from .viewlist import possible_class_views
1010
from .layout_presets import get_layout_description
11-
from .utils_global import get_size_bottom_row, get_size_top_row
11+
from .utils_global import fill_unnecessary_space, get_present_zones_in_half_of_layout
1212

1313
from .utils_qt import qt_style, add_stretch_to_qtoolbar
1414

@@ -200,9 +200,9 @@ 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-
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)
205-
203+
self.make_half_layout(widgets_zone, "left")
204+
self.make_half_layout(widgets_zone, "right")
205+
206206
# make tabs
207207
for zone, view_names in widgets_zone.items():
208208
n = len(widgets_zone[zone])
@@ -217,103 +217,70 @@ def create_main_layout(self):
217217
# make visible the first of each zone
218218
self.docks[view_name0].raise_()
219219

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])
220+
def make_half_layout(self, widgets_zone, left_or_right):
221+
"""
222+
Function contains the logic for the greedy layout. Given the 2x2 box of zones
292223
293-
if len(top_zones) > 0 and len(bottom_zones) > 0:
224+
1 2 3 4
225+
5 6 or 7 8
294226
295-
top_view_name = widgets_zone[top_zones[0]][0]
296-
top_dock = self.docks[top_view_name]
227+
Then depending on which zones are non-zero, a different layout is generated using splits.
297228
298-
bottom_view_name = widgets_zone[bottom_zones[0]][0]
299-
bottom_dock = self.docks[bottom_view_name]
229+
The zone indices in the second box (34,78) are equal to the zone indices first box (12,56)
230+
shifted by 2. We take advantage of this fact.
231+
"""
300232

301-
self.splitDockWidget(top_dock, bottom_dock, orientations['vertical'])
233+
shift = 0 if left_or_right == "left" else 2
302234

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'])
235+
widgets_zone = fill_unnecessary_space(widgets_zone, shift)
236+
present_zones = get_present_zones_in_half_of_layout(widgets_zone, shift)
315237

238+
if len(present_zones) == 0:
239+
return
316240

241+
# The movements from earlier guarantee that the top-left zone is non-zero. Make this the initial zone
242+
view_name = widgets_zone[f"zone{1+shift}"][0]
243+
dock = self.docks[view_name]
244+
self.addDockWidget(areas[left_or_right], dock)
245+
246+
# The main logic: apply splittings between different zones and in
247+
# different orders, depending on which zones are present.
248+
249+
# Layouts with two non-zero zones
250+
if present_zones == set([f'zone{1+shift}', f'zone{2+shift}']):
251+
self.make_split(1,2,"horizontal", widgets_zone, shift)
252+
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}']):
253+
self.make_split(1,5,"vertical", widgets_zone, shift)
254+
elif present_zones == set([f'zone{1+shift}', f'zone{6+shift}']):
255+
self.make_split(1,6,"horizontal", widgets_zone, shift)
256+
257+
# Layouts with three non-zero zones
258+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}']):
259+
self.make_split(1,2,"horizontal", widgets_zone, shift)
260+
self.make_split(1,5,"vertical", widgets_zone, shift)
261+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{6+shift}']):
262+
self.make_split(1,2,"horizontal", widgets_zone, shift)
263+
self.make_split(2,6,"vertical", widgets_zone, shift)
264+
elif present_zones == set([f'zone{1+shift}', f'zone{5+shift}', f'zone{6+shift}']):
265+
self.make_split(1,5,"vertical", widgets_zone, shift)
266+
self.make_split(5,6,"horizontal", widgets_zone, shift)
267+
268+
# Layout with four non-zero zones
269+
elif present_zones == set([f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']):
270+
self.make_split(1,5,"vertical", widgets_zone, shift)
271+
self.make_split(1,2,"horizontal", widgets_zone, shift)
272+
self.make_split(5,6,"horizontal", widgets_zone, shift)
273+
274+
275+
def make_split(self, zone_index_1, zone_index_2, orientation, widgets_zone, shift):
276+
"""
277+
Splits the zone at `zone_{zone_index_1+shift}` into two zones
278+
(`zone_{zone_index_1+shift}` and `zone_{zone_index_2+shift}`)
279+
with an `orientation` split.
280+
"""
281+
widget_1 = widgets_zone[f"zone{zone_index_1+shift}"][0]
282+
widget_2 = widgets_zone[f"zone{zone_index_2+shift}"][0]
283+
self.splitDockWidget(self.docks[widget_1], self.docks[widget_2], orientations[orientation])
317284

318285
# used by to tell the launcher this is closed
319286
def closeEvent(self, event):

spikeinterface_gui/utils_global.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,48 @@
11
import numpy as np
22

3-
def get_size_top_row(initial_row, initial_col, is_zone_array, original_zone_array):
4-
5-
if original_zone_array[initial_row][initial_col] == False:
6-
return 0,0
7-
8-
num_rows = is_zone_array[initial_row][initial_col]*1
9-
num_cols = num_rows
10-
11-
num_rows += (not is_zone_array[1][initial_col])*1
12-
13-
if num_rows == 1:
14-
for zone in is_zone_array[0,1+initial_col:]:
15-
if zone == True:
16-
break
17-
num_cols += 1
18-
elif num_rows == 2:
19-
for zone1, zone2 in np.transpose(is_zone_array[:,1+initial_col:]):
20-
if zone1 == True or zone2 == True:
21-
break
22-
num_cols += 1
23-
24-
is_zone_array[initial_row:initial_row+num_rows,initial_col:initial_col+num_cols] = True
25-
26-
return num_rows, num_cols
27-
28-
def get_size_bottom_row(initial_row, initial_col, is_zone_array, original_zone_array):
29-
30-
if original_zone_array[initial_row][initial_col] == False:
31-
return 0,0
32-
33-
num_rows = is_zone_array[initial_row][initial_col]*1
34-
if num_rows == 0:
35-
return 0, 0
36-
num_cols = num_rows
37-
38-
for zone in is_zone_array[1,1+initial_col:]:
39-
if zone == True:
40-
break
41-
else:
42-
num_cols += 1
43-
44-
return num_rows, num_cols
3+
# Functions for the layout
4+
5+
def fill_unnecessary_space(layout_zone, shift):
6+
"""
7+
Used when making layouts. In the zoning algorithm,
8+
certain layouts are equivalent to each other e.g.
9+
10+
zone1 zone2 . .
11+
. . is equivalent to zone5 zone6
12+
13+
and
14+
15+
. zone2 zone1 .
16+
. zone6 is equivalent to zone5 .
17+
18+
This function moves zones left-wards and upwards in a way that preserves
19+
the layouts and ensures that the top-left zone is non-zero.
20+
"""
21+
22+
# Move the right hand column leftwards if the left-hand column is missing
23+
if len(layout_zone[f'zone{1+shift}']) == 0 and len(layout_zone[f'zone{5+shift}']) == 0:
24+
layout_zone[f'zone{1+shift}'] = layout_zone[f'zone{2+shift}']
25+
layout_zone[f'zone{5+shift}'] = layout_zone[f'zone{6+shift}']
26+
layout_zone[f'zone{2+shift}'] = []
27+
layout_zone[f'zone{6+shift}'] = []
28+
29+
# Move the bottom-left zone to the top-left, if the top-left is missing
30+
# These steps reduce the number of layouts we have to consider
31+
if len(layout_zone[f'zone{1+shift}']) == 0:
32+
layout_zone[f'zone{1+shift}'] = layout_zone[f'zone{5+shift}']
33+
layout_zone[f'zone{5+shift}'] = []
34+
35+
return layout_zone
36+
37+
38+
def get_present_zones_in_half_of_layout(layout_zone, shift):
39+
"""
40+
Returns the zones which contain at least one view, for either:
41+
left-hand zones 1,2,5,6 (shift=0)
42+
right-hand zones 3,4,7,8 (shift=2)
43+
"""
44+
zones_in_half = [f'zone{1+shift}', f'zone{2+shift}', f'zone{5+shift}', f'zone{6+shift}']
45+
half_dict = {key: value for key, value in layout_zone.items() if key in zones_in_half}
46+
is_present = [views is not None and len(views) > 0 for views in half_dict.values()]
47+
present_zones = set(np.array(list(half_dict.keys()))[np.array(is_present)])
48+
return present_zones

0 commit comments

Comments
 (0)