1
""" Defines various plot container classes, including stacked, grid, and overlay.
3
# Major library imports
4
from numpy import amax, any, arange, array, cumsum, hstack, sum, zeros, zeros_like
6
# Enthought library imports
7
from enthought.traits.api import Any, Array, Either, Enum, Float, Instance, \
8
List, Property, Trait, Tuple, Int
9
from enthought.enable.simple_layout import simple_container_get_preferred_size, \
10
simple_container_do_layout
12
# Local relative imports
13
from base_plot_container import BasePlotContainer
16
__all__ = ["OverlayPlotContainer", "HPlotContainer", "VPlotContainer", \
19
DEFAULT_DRAWING_ORDER = ["background", "image", "underlay", "plot",
20
"selection", "border", "annotation", "overlay"]
22
class OverlayPlotContainer(BasePlotContainer):
24
A plot container that stretches all its components to fit within its
25
space. All of its components must therefore be resizable.
28
draw_order = Instance(list, args=(DEFAULT_DRAWING_ORDER,))
30
# Do not use an off-screen backbuffer.
31
use_backbuffer = False
33
# Cache (width, height) of the container's preferred size.
34
_cached_preferred_size = Tuple
36
def get_preferred_size(self, components=None):
37
""" Returns the size (width,height) that is preferred for this component.
39
Overrides PlotComponent
41
return simple_container_get_preferred_size(self, components=components)
44
""" Actually performs a layout (called by do_layout()).
46
simple_container_do_layout(self)
49
class StackedPlotContainer(BasePlotContainer):
51
Base class for 1-D stacked plot containers, both horizontal and vertical.
54
draw_order = Instance(list, args=(DEFAULT_DRAWING_ORDER,))
56
# The dimension along which to stack components that are added to
58
stack_dimension = Enum("h", "v")
60
# The "other" dimension, i.e., the dual of the stack dimension.
61
other_dimension = Enum("v", "h")
63
# The index into obj.position and obj.bounds that corresponds to
64
# **stack_dimension**. This is a class-level and not an instance-level
65
# attribute. It must be 0 or 1.
68
def get_preferred_size(self, components=None):
69
""" Returns the size (width,height) that is preferred for this component.
71
Overrides PlotComponent.
73
if self.fixed_preferred_size is not None:
74
self._cached_preferred_size = self.fixed_preferred_size
75
return self.fixed_preferred_size
77
if self.resizable == "":
78
self._cached_preferred_size = self.outer_bounds[:]
79
return self.outer_bounds
81
if components is None:
82
components = self.components
84
ndx = self.stack_index
87
no_visible_components = True
90
for component in components:
91
if not self._should_layout(component):
94
no_visible_components = False
96
pref_size = component.get_preferred_size()
97
total_size += pref_size[ndx] + self.spacing
98
if pref_size[other_ndx] > max_other_size:
99
max_other_size = pref_size[other_ndx]
101
if total_size >= self.spacing:
102
total_size -= self.spacing
104
if (self.stack_dimension not in self.resizable) and \
105
(self.stack_dimension not in self.fit_components):
106
total_size = self.bounds[ndx]
107
elif no_visible_components or (total_size == 0):
108
total_size = self.default_size[ndx]
110
if (self.other_dimension not in self.resizable) and \
111
(self.other_dimension not in self.fit_components):
112
max_other_size = self.bounds[other_ndx]
113
elif no_visible_components or (max_other_size == 0):
114
max_other_size = self.default_size[other_ndx]
117
self._cached_preferred_size = (total_size + self.hpadding,
118
max_other_size + self.vpadding)
120
self._cached_preferred_size = (max_other_size + self.hpadding,
121
total_size + self.vpadding)
123
return self._cached_preferred_size
126
def _do_stack_layout(self, components, align):
127
""" Helper method that does the actual work of layout.
131
size = list(self.bounds)
132
if self.fit_components != "":
133
self.get_preferred_size()
134
if "h" in self.fit_components:
135
size[0] = self._cached_preferred_size[0] - self.hpadding
136
if "v" in self.fit_components:
137
size[1] = self._cached_preferred_size[1] - self.vpadding
139
ndx = self.stack_index
141
other_dim = self.other_dimension
143
# Assign sizes of non-resizable components, and compute the total size
144
# used by them (along the stack dimension).
146
resizable_components = []
148
total_resizable_size = 0
150
for component in components:
151
if not self._should_layout(component):
153
if self.stack_dimension not in component.resizable:
154
total_fixed_size += component.outer_bounds[ndx]
156
preferred_size = component.get_preferred_size()
157
size_prefs[component] = preferred_size
158
total_resizable_size += preferred_size[ndx]
159
resizable_components.append(component)
163
# Assign sizes of all the resizable components along the stack dimension
164
if resizable_components:
165
space = self.spacing * (len(self.components) - 1)
166
avail_size = size[ndx] - total_fixed_size - space
167
if total_resizable_size > 0:
168
scale = avail_size / float(total_resizable_size)
169
for component in resizable_components:
170
tmp = list(component.outer_bounds)
171
tmp[ndx] = int(size_prefs[component][ndx] * scale)
172
new_bounds_dict[component] = tmp
174
each_size = int(avail_size / len(resizable_components))
175
for component in resizable_components:
176
tmp = list(component.outer_bounds)
178
new_bounds_dict[component] = tmp
180
# Loop over all the components, assigning position and computing the
181
# size in the other dimension and its position.
183
for component in components:
184
if not self._should_layout(component):
187
position = list(component.outer_position)
188
position[ndx] = cur_pos
190
bounds = new_bounds_dict.get(component, list(component.outer_bounds))
191
cur_pos += bounds[ndx] + self.spacing
193
if (bounds[other_ndx] > size[other_ndx]) or \
194
(other_dim in component.resizable):
195
# If the component is resizable in the other dimension or it exceeds the
196
# container bounds, set it to the maximum size of the container
198
#component.set_outer_position(other_ndx, 0)
199
#component.set_outer_bounds(other_ndx, size[other_ndx])
200
position[other_ndx] = 0
201
bounds[other_ndx] = size[other_ndx]
203
#component.set_outer_position(other_ndx, 0)
204
#old_coord = component.outer_position[other_ndx]
205
position[other_ndx] = 0
209
position[other_ndx] = size[other_ndx] - bounds[other_ndx]
210
elif align == "center":
211
position[other_ndx] = (size[other_ndx] - bounds[other_ndx]) / 2.0
213
component.outer_position = position
214
component.outer_bounds = bounds
215
component.do_layout()
218
### Persistence ###########################################################
220
# PICKLE FIXME: blocked with _pickles, but not sure that was correct.
221
def __getstate__(self):
222
state = super(StackedPlotContainer,self).__getstate__()
223
for key in ['stack_dimension', 'other_dimension', 'stack_index']:
224
if state.has_key(key):
229
class HPlotContainer(StackedPlotContainer):
231
A plot container that stacks all of its components horizontally. Resizable
232
components share the free space evenly. All components are stacked from
233
according to **stack_order* in the same order that they appear in the
237
draw_order = Instance(list, args=(DEFAULT_DRAWING_ORDER,))
239
# The order in which components in the plot container are laid out.
240
stack_order = Enum("left_to_right", "right_to_left")
242
# The amount of space to put between components.
245
# The vertical alignment of objects that don't span the full height.
246
valign = Enum("bottom", "top", "center")
248
_cached_preferred_size = Tuple
250
def _do_layout(self):
251
""" Actually performs a layout (called by do_layout()).
253
if self.stack_order == "left_to_right":
254
components = self.components
256
components = self.components[::-1]
258
if self.valign == "bottom":
260
elif self.valign == "center":
265
return self._do_stack_layout(components, align)
267
### Persistence ###########################################################
268
#_pickles = ("stack_order", "spacing")
270
def __getstate__(self):
271
state = super(HPlotContainer,self).__getstate__()
272
for key in ['_cached_preferred_size']:
273
if state.has_key(key):
279
class VPlotContainer(StackedPlotContainer):
281
A plot container that stacks plot components vertically.
284
draw_order = Instance(list, args=(DEFAULT_DRAWING_ORDER,))
286
# Overrides StackedPlotContainer.
287
stack_dimension = "v"
288
# Overrides StackedPlotContainer.
289
other_dimension = "h"
290
# Overrides StackedPlotContainer.
293
# VPlotContainer attributes
295
# The horizontal alignment of objects that don't span the full width.
296
halign = Enum("left", "right", "center")
298
# The order in which components in the plot container are laid out.
299
stack_order = Enum("bottom_to_top", "top_to_bottom")
301
# The amount of space to put between components.
304
def _do_layout(self):
305
""" Actually performs a layout (called by do_layout()).
307
if self.stack_order == "bottom_to_top":
308
components = self.components
310
components = self.components[::-1]
311
if self.halign == "left":
313
elif self.halign == "center":
318
#import pdb; pdb.set_trace()
319
return self._do_stack_layout(components, align)
322
class GridPlotContainer(BasePlotContainer):
323
""" A GridPlotContainer consists of rows and columns in a tabular format.
325
Each cell's width is the same as all other cells in its column, and each
326
cell's height is the same as all other cells in its row.
328
Although grid layout requires more layout information than a simple
329
ordered list, this class keeps components as a simple list and exposes a
333
draw_order = Instance(list, args=(DEFAULT_DRAWING_ORDER,))
335
# The amount of space to put on either side of each component, expressed
336
# as a tuple (h_spacing, v_spacing).
337
spacing = Either(Tuple, List, Array)
339
# The vertical alignment of objects that don't span the full height.
340
valign = Enum("bottom", "top", "center")
342
# The horizontal alignment of objects that don't span the full width.
343
halign = Enum("left", "right", "center")
345
# The shape of this container, i.e, (rows, columns). The items in
346
# **components** are shuffled appropriately to match this
347
# specification. If there are fewer components than cells, the remaining
348
# cells are filled in with spaces. If there are more components than cells,
349
# the remainder wrap onto new rows as appropriate.
350
shape = Trait((0,0), Either(Tuple, List, Array))
352
# This property exposes the underlying grid structure of the container,
353
# and is the preferred way of setting and reading its contents.
354
# When read, this property returns a Numpy array with dtype=object; values
355
# for setting it can be nested tuples, lists, or 2-D arrays.
356
# The array is in row-major order, so that component_grid[0] is the first
357
# row, and component_grid[:,0] is the first column. The rows are ordered
358
# from top to bottom.
359
component_grid = Property
361
# The internal component grid, in row-major order. This gets updated
362
# when any of the following traits change: shape, components, grid_components
365
_cached_total_size = Any
369
class SizePrefs(object):
370
""" Object to hold size preferences across spans in a particular
371
dimension. For instance, if SizePrefs is being used for the row
372
axis, then each element in the arrays below express sizing information
373
about the corresponding column.
376
# The maximum size of non-resizable elements in the span. If an
377
# element of this array is 0, then its corresponding span had no
378
# non-resizable components.
379
fixed_lengths = Array
381
# The maximum preferred size of resizable elements in the span.
382
# If an element of this array is 0, then its corresponding span
383
# had no resizable components with a non-zero preferred size.
384
resizable_lengths = Array
386
# The direction of resizability associated with this SizePrefs
387
# object. If this SizePrefs is sizing along the X-axis, then
388
# direction should be "h", and correspondingly for the Y-axis.
389
direction = Enum("h", "v")
391
# The index into a size tuple corresponding to our orientation
392
# (0 for horizontal, 1 for vertical). This is derived from
393
# **direction** in the constructor.
396
def __init__(self, length, direction):
397
""" Initializes this prefs object with empty arrays of the given
398
length and with the given direction. """
399
self.fixed_lengths = zeros(length)
400
self.resizable_lengths = zeros(length)
401
self.direction = direction
408
def update_from_component(self, component, index):
409
""" Given a component at a particular index along this SizePref's
410
axis, integrates the component's resizability and sizing information
411
into self.fixed_lengths and self.resizable_lengths. """
412
resizable = self.direction in component.resizable
413
pref_size = component.get_preferred_size()
414
self.update_from_pref_size(pref_size[self.index], index, resizable)
416
def update_from_pref_size(self, pref_length, index, resizable):
418
if pref_length > self.resizable_lengths[index]:
419
self.resizable_lengths[index] = pref_length
421
if pref_length > self.fixed_lengths[index]:
422
self.fixed_lengths[index] = pref_length
425
def get_preferred_size(self):
426
return amax((self.fixed_lengths, self.resizable_lengths), axis=0)
428
def compute_size_array(self, size):
429
""" Given a length along the axis corresponding to this SizePref,
430
returns an array of lengths to assign each cell, taking into account
431
resizability and preferred sizes.
433
# There are three basic cases for each column:
434
# 1. size < total fixed size
435
# 2. total fixed size < size < fixed size + resizable preferred size
436
# 3. fixed size + resizable preferred size < size
438
# In all cases, non-resizable components get their full width.
440
# For resizable components with non-zero preferred size, the following
441
# actions are taken depending on case:
442
# case 1: They get sized to 0.
443
# case 2: They get a fraction of their preferred size, scaled based on
444
# the amount of remaining space after non-resizable components
445
# get their full size.
446
# case 3: They get their full preferred size.
448
# For resizable components with no preferred size (indicated in our scheme
449
# by having a preferred size of 0), the following actions are taken
451
# case 1: They get sized to 0.
452
# case 2: They get sized to 0.
453
# case 3: All resizable components with no preferred size split the
454
# remaining space evenly, after fixed width and resizable
455
# components with preferred size get their full size.
456
fixed_lengths = self.fixed_lengths
457
resizable_lengths = self.resizable_lengths
458
return_lengths = zeros_like(fixed_lengths)
460
fixed_size = sum(fixed_lengths)
461
fixed_length_indices = fixed_lengths > resizable_lengths
462
resizable_indices = resizable_lengths > fixed_lengths
463
fully_resizable_indices = (resizable_lengths + fixed_lengths == 0)
464
preferred_size = sum(fixed_lengths[fixed_length_indices]) + \
465
sum(resizable_lengths[~fixed_length_indices])
467
# Regardless of the relationship between available space and
468
# resizable preferred sizes, columns/rows where the non-resizable
469
# component is largest will always get that amount of space.
470
return_lengths[fixed_length_indices] = fixed_lengths[fixed_length_indices]
472
if size <= fixed_size:
473
# We don't use fixed_length_indices here because that mask is
474
# just where non-resizable components were larger than resizable
475
# ones. If our allotted size is less than the total fixed size,
476
# then we should give all non-resizable components their desired
478
indices = fixed_lengths > 0
479
return_lengths[indices] = fixed_lengths[indices]
480
return_lengths[~indices] = 0
482
elif size > fixed_size and (fixed_lengths > resizable_lengths).all():
483
# If we only have to consider non-resizable lengths, and we have
484
# extra space available, then we need to give each column an
485
# amount of extra space corresponding to its size.
486
desired_space = sum(fixed_lengths)
487
if desired_space > 0:
488
scale = size / desired_space
489
return_lengths = (fixed_lengths * scale).astype(int)
491
elif size <= preferred_size or not fully_resizable_indices.any():
492
# If we don't have enough room to give all the non-fully resizable
493
# components their preferred size, or we have more than enough
494
# room for them and no fully resizable components to take up
495
# the extra space, then we just scale the resizable components
496
# up or down based on the amount of extra space available.
497
delta_lengths = resizable_lengths[resizable_indices] - \
498
fixed_lengths[resizable_indices]
499
desired_space = sum(delta_lengths)
500
if desired_space > 0:
501
avail_space = size - sum(fixed_lengths) #[fixed_length_indices])
502
scale = avail_space / desired_space
503
return_lengths[resizable_indices] = (fixed_lengths[resizable_indices] + \
504
scale * delta_lengths).astype(int)
506
elif fully_resizable_indices.any():
507
# We have enough room to fit all the non-resizable components
508
# as well as components with preferred sizes, and room left
509
# over for the fully resizable components. Give the resizable
510
# components their desired amount of space, and then give the
511
# remaining space to the fully resizable components.
512
return_lengths[resizable_indices] = resizable_lengths[resizable_indices]
513
avail_space = size - preferred_size
514
count = sum(fully_resizable_indices)
515
space = avail_space / count
516
return_lengths[fully_resizable_indices] = space
519
raise RuntimeError("Unhandled sizing case in GridContainer")
521
return return_lengths
524
def get_preferred_size(self, components=None):
525
""" Returns the size (width,height) that is preferred for this component.
527
Overrides PlotComponent.
529
if self.fixed_preferred_size is not None:
530
return self.fixed_preferred_size
532
if components is None:
533
components = self.component_grid
535
# Convert to array; hopefully it is a list or tuple of list/tuples
536
components = array(components)
538
# These arrays track the maximum widths in each column and maximum
539
# height in each row.
540
numrows, numcols = self.shape
542
no_visible_components = True
543
self._h_size_prefs = GridPlotContainer.SizePrefs(numcols, "h")
544
self._v_size_prefs = GridPlotContainer.SizePrefs(numrows, "v")
545
self._pref_size_cache = {}
546
for i, row in enumerate(components):
547
for j, component in enumerate(row):
548
if not self._should_layout(component):
551
no_visible_components = False
552
self._h_size_prefs.update_from_component(component, j)
553
self._v_size_prefs.update_from_component(component, i)
555
total_width = sum(self._h_size_prefs.get_preferred_size()) + self.hpadding
556
total_height = sum(self._v_size_prefs.get_preferred_size()) + self.vpadding
557
total_size = array([total_width, total_height])
559
# Account for spacing. There are N+1 of spaces, where N is the size in
561
if self.spacing is None:
564
spacing = array(self.spacing)
565
total_spacing = array(components.shape[::-1]) * spacing * 2 * (total_size>0)
566
total_size += total_spacing
568
for orientation, ndx in (("h", 0), ("v", 1)):
569
if (orientation not in self.resizable) and \
570
(orientation not in self.fit_components):
571
total_size[ndx] = self.outer_bounds[ndx]
572
elif no_visible_components or (total_size[ndx] == 0):
573
total_size[ndx] = self.default_size[ndx]
575
self._cached_total_size = total_size
576
if self.resizable == "":
577
return self.outer_bounds
579
return self._cached_total_size
581
def _do_layout(self):
582
# If we don't have cached size_prefs, then we need to call
583
# get_preferred_size to build them.
584
if self._cached_total_size is None:
585
self.get_preferred_size()
587
# If we need to fit our components, then rather than using our
588
# currently assigned size to do layout, we use the preferred
589
# size we computed from our components.
590
size = array(self.bounds)
591
if self.fit_components != "":
592
self.get_preferred_size()
593
if "h" in self.fit_components:
594
size[0] = self._cached_total_size[0] - self.hpadding
595
if "v" in self.fit_components:
596
size[1] = self._cached_total_size[1] - self.vpadding
598
# Compute total_spacing and spacing, which are used in computing
599
# the bounds and positions of all the components.
600
shape = array(self._grid.shape).transpose()
601
if self.spacing is None:
602
spacing = array([0,0])
604
spacing = array(self.spacing)
605
total_spacing = spacing * 2 * shape
607
# Compute the total space used by non-resizable and resizable components
608
# with non-zero preferred sizes.
609
widths = self._h_size_prefs.compute_size_array(size[0] - total_spacing[0])
610
heights = self._v_size_prefs.compute_size_array(size[1] - total_spacing[1])
612
# Set the baseline h and v positions for each cell. Resizable components
613
# will get these as their position, but non-resizable components will have
614
# to be aligned in H and V.
615
summed_widths = cumsum(hstack(([0], widths[:-1])))
616
summed_heights = cumsum(hstack(([0], heights[-1:0:-1])))
617
h_positions = (2*(arange(self._grid.shape[1])+1) - 1) * spacing[0] + summed_widths
618
v_positions = (2*(arange(self._grid.shape[0])+1) - 1) * spacing[1] + summed_heights
619
v_positions = v_positions[::-1]
621
# Loop over all rows and columns, assigning position, setting bounds for
622
# resizable components, and aligning non-resizable ones
625
for j, row in enumerate(self._grid):
626
for i, component in enumerate(row):
627
if not self._should_layout(component):
630
r = component.resizable
637
# Component is not vertically resizable
639
y += h - component.outer_height
640
elif valign == "center":
641
y += (h - component.outer_height) / 2
643
# Component is not horizontally resizable
644
if halign == "right":
645
x += w - component.outer_width
646
elif halign == "center":
647
x += (w - component.outer_width) / 2
649
component.outer_position = [x,y]
650
bounds = list(component.outer_bounds)
656
component.outer_bounds = bounds
657
component.do_layout()
662
def _reflow_layout(self):
663
""" Re-computes self._grid based on self.components and self.shape.
664
Adjusts self.shape accordingly.
666
numcells = self.shape[0] * self.shape[1]
667
if numcells < len(self.components):
668
numrows, numcols = divmod(len(self.components), self.shape[0])
669
self.shape = (numrows, numcols)
670
grid = array(self.components, dtype=object)
671
grid.resize(self.shape)
674
self._layout_needed = True
677
def _shape_changed(self, old, new):
678
self._reflow_layout()
680
def __components_changed(self, old, new):
681
self._reflow_layout()
683
def __components_items_changed(self, event):
684
self._reflow_layout()
686
def _get_component_grid(self):
689
def _set_component_grid(self, val):
691
grid_set = set(grid.flatten())
693
# Figure out which of the components in the component_grid are new,
694
# and which have been removed.
695
existing = set(array(self._grid).flatten())
696
new = grid_set - existing
697
removed = existing - grid_set
699
for component in removed:
700
if component is not None:
701
component.container = None
702
for component in new:
703
if component is not None:
704
if component.container is not None:
705
component.container.remove(component)
706
component.container = self
708
self.set(shape=grid.shape, trait_change_notify=False)
709
self._components = list(grid.flatten())
711
if self._should_compact():
714
self.invalidate_draw()