vispy 0.14.3__cp312-cp312-win_amd64.whl → 0.15.2__cp312-cp312-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vispy might be problematic. Click here for more details.

@@ -4,7 +4,11 @@
4
4
  """Grid widget for providing a gridded layout to child widgets."""
5
5
 
6
6
  from __future__ import division
7
+
8
+ from typing import Tuple, Union, Dict
9
+
7
10
  import numpy as np
11
+ from numpy.typing import NDArray
8
12
 
9
13
  from vispy.geometry import Rect
10
14
  from .widget import Widget
@@ -21,17 +25,19 @@ class Grid(Widget):
21
25
 
22
26
  Parameters
23
27
  ----------
24
- spacing : int
25
- Spacing between widgets.
28
+ spacing : int | tuple[int, int]
29
+ Spacing between widgets. If `tuple` then it must be of length two, the first element
30
+ being `width_spacing` and the second being `height_spacing`.
26
31
  **kwargs : dict
27
32
  Keyword arguments to pass to `Widget`.
28
33
  """
29
34
 
30
- def __init__(self, spacing=6, **kwargs):
35
+ def __init__(self, spacing=0, **kwargs):
31
36
  """Create solver and basic grid parameters."""
32
37
  self._next_cell = [0, 0] # row, col
33
38
  self._cells = {}
34
39
  self._grid_widgets = {}
40
+
35
41
  self.spacing = spacing
36
42
  self._n_added = 0
37
43
  self._default_class = ViewBox # what to add when __getitem__ is used
@@ -45,9 +51,6 @@ class Grid(Widget):
45
51
  self._width_grid = None
46
52
  self._height_grid = None
47
53
 
48
- # self._height_stay = None
49
- # self._width_stay = None
50
-
51
54
  Widget.__init__(self, **kwargs)
52
55
 
53
56
  def __getitem__(self, idxs):
@@ -256,53 +259,129 @@ class Grid(Widget):
256
259
  locs[r:r + rs, c:c + cs] = key
257
260
  return locs
258
261
 
262
+ @property
263
+ def spacing(self):
264
+ """
265
+ The spacing between individual Viewbox widgets in the grid.
266
+ """
267
+ return self._spacing
268
+
269
+ @spacing.setter
270
+ def spacing(self, value: Union[int, Tuple[int, int]]):
271
+ if not (
272
+ isinstance(value, int)
273
+ or isinstance(value, tuple)
274
+ and len(value) == 2
275
+ and isinstance(value[0], int)
276
+ and isinstance(value[1], int)
277
+ ):
278
+ raise ValueError('spacing must be of type int | tuple[int, int]')
279
+
280
+ self._spacing = value
281
+ self._need_solver_recreate = True
282
+
259
283
  def __repr__(self):
260
284
  return (('<Grid at %s:\n' % hex(id(self))) +
261
285
  str(self.layout_array + 1) + '>')
262
286
 
263
287
  @staticmethod
264
- def _add_total_width_constraints(solver, width_grid, _var_w):
265
- for ws in width_grid:
266
- width_expr = ws[0]
267
- for w in ws[1:]:
268
- width_expr += w
269
- solver.addConstraint(width_expr == _var_w)
288
+ def _add_total_dim_length_constraints(solver: Solver, grid_dim_variables: NDArray[Variable],
289
+ n_added: int, _var_dim_length: Variable, spacing: float):
290
+ """Add constraint: total height == sum(col heights) + sum(spacing).
270
291
 
271
- @staticmethod
272
- def _add_total_height_constraints(solver, height_grid, _var_h):
273
- for hs in height_grid:
274
- height_expr = hs[0]
275
- for h in hs[1:]:
276
- height_expr += h
277
- solver.addConstraint(height_expr == _var_h)
292
+ The total height of the grid is constrained to be equal to the sum of the heights of
293
+ its columns, including spacing between widgets.
294
+
295
+ Parameters
296
+ ----------
297
+ solver: Solver
298
+ Solver for a system of linear equations.
299
+ grid_dim_variables: NDArray[Variable]:
300
+ The grid of width or height variables of either shape col * row or row * col with each element being a
301
+ Variable in the solver representing the height or width of each grid box.
302
+ n_added: int
303
+ The number of ViewBoxes added to the grid.
304
+ _var_dim_length: Variable
305
+ The solver variable representing either total width or height of the grid.
306
+ spacing: float
307
+ The amount of spacing between single adjacent Viewbox widgets in the grid.
308
+ """
309
+ total_spacing = 0
310
+ if n_added > 1:
311
+ for _ in range(grid_dim_variables.shape[1] - 1):
312
+ total_spacing += spacing
313
+
314
+ for ds in grid_dim_variables:
315
+ dim_length_expr = ds[0]
316
+ for d in ds[1:]:
317
+ dim_length_expr += d
318
+ dim_length_expr += total_spacing
319
+ solver.addConstraint(dim_length_expr == _var_dim_length)
278
320
 
279
321
  @staticmethod
280
- def _add_gridding_width_constraints(solver, width_grid):
322
+ def _add_gridding_dim_constraints(solver: Solver, grid_dim_variables: NDArray[Variable]):
323
+ """Add constraint: all viewbox dims in each dimension are equal.
324
+
325
+ With all dims the reserved space for a widget with a col_span and row_span of 1 is meant, e.g. we have 3
326
+ widgets arranged in columns or rows with col_span or row_span 1 and those are being constrained to all be of
327
+ width/height 100. In other words the same dim length is reserved for each position in the grid, not taking
328
+ into account the spacing between grid positions.
329
+
330
+ Parameters
331
+ ----------
332
+ solver: Solver
333
+ Solver for a system of linear equations.
334
+ grid_dim_variables:
335
+ The grid of width or height variables of either shape col * row or row * col with each element being a
336
+ Variable in the solver representing the height or width of each grid box.
337
+ """
281
338
  # access widths of one "y", different x
282
- for ws in width_grid.T:
283
- for w in ws[1:]:
284
- solver.addConstraint(ws[0] == w)
339
+ for ds in grid_dim_variables.T:
340
+ for d in ds[1:]:
341
+ solver.addConstraint(ds[0] == d)
285
342
 
286
343
  @staticmethod
287
- def _add_gridding_height_constraints(solver, height_grid):
288
- # access heights of one "y"
289
- for hs in height_grid.T:
290
- for h in hs[1:]:
291
- solver.addConstraint(hs[0] == h)
344
+ def _add_stretch_constraints(solver: Solver, width_grid: NDArray[Variable] , height_grid: NDArray[Variable],
345
+ grid_widgets: Dict[int, Tuple[int, int, int, int, ViewBox]],
346
+ widget_grid: NDArray[ViewBox]):
347
+ """
348
+ Add proportional stretch constraints to the linear system solver of the grid.
292
349
 
293
- @staticmethod
294
- def _add_stretch_constraints(solver, width_grid, height_grid,
295
- grid_widgets, widget_grid):
350
+ This method enforces that grid rows and columns stretch in proportion
351
+ to the widgets' specified stretch factors. It uses weak constraints
352
+ so that proportionality is preserved when possible but can be violated
353
+ if stronger layout constraints are present.
354
+
355
+ Parameters
356
+ ----------
357
+ solver : Solver
358
+ Solver for a system of linear equations.
359
+ width_grid : NDArray[Variable]
360
+ The grid of width variables in the linear system of equations to be solved.
361
+ height_grid : NDArray[Variable]
362
+ The grid of height variables in the linear system of equations to be solved.
363
+ grid_widgets : dict[int, tuple[int, int, int, int, ViewBox]]
364
+ Dictionary mapping order of viewboxes added as int to their grid layout description:
365
+ (start_y, start_x, span_y, span_x, ViewBox).
366
+ widget_grid : NDArray[ViewBox]
367
+ Array of viewboxes in shape n_columns x n_rows.
368
+
369
+ Notes
370
+ -----
371
+ - Stretch constraints are added with 'weak' strength, allowing them to
372
+ be overridden by stronger constraints such as fixed sizes or min/max bounds.
373
+ - The constraint `total_size / stretch_factor` is used to maintain
374
+ proportional relationships among rows and columns.
375
+ """
296
376
  xmax = len(height_grid)
297
377
  ymax = len(width_grid)
298
378
 
299
- stretch_widths = [[] for _ in range(0, ymax)]
300
- stretch_heights = [[] for _ in range(0, xmax)]
379
+ stretch_widths = [[] for _ in range(ymax)]
380
+ stretch_heights = [[] for _ in range(xmax)]
301
381
 
302
382
  for (y, x, ys, xs, widget) in grid_widgets.values():
303
383
  for ws in width_grid[y:y+ys]:
304
384
  total_w = np.sum(ws[x:x+xs])
305
-
306
385
  for sw in stretch_widths[y:y+ys]:
307
386
  sw.append((total_w, widget.stretch[0]))
308
387
 
@@ -339,14 +418,36 @@ class Grid(Widget):
339
418
  'weak')
340
419
 
341
420
  @staticmethod
342
- def _add_widget_dim_constraints(solver, width_grid, height_grid,
343
- total_var_w, total_var_h, grid_widgets):
421
+ def _add_widget_dim_constraints(solver: Solver, width_grid: NDArray[Variable], height_grid: NDArray[Variable],
422
+ total_var_w: Variable, total_var_h: Variable,
423
+ grid_widgets: Dict[int, Tuple[int, int, int, int, ViewBox]]):
424
+ """Add constraints based on min/max width/height of widgets.
425
+
426
+ These constraints ensure that each widget's dimensions stay within its
427
+ specified minimum and maximum values.
428
+
429
+ Parameters
430
+ ----------
431
+ solver : Solver
432
+ Solver for a system of linear equations.
433
+ width_grid : NDArray[Variable]
434
+ The grid of width variables in the linear system of equations to be solved.
435
+ height_grid : NDArray[Variable]
436
+ The grid of height variables in the linear system of equations to be solved.
437
+ total_var_w : Variable
438
+ The Variable representing the total width of the grid in the linear system of equations.
439
+ total_var_w : Variable
440
+ The Variable representing the total height of the grid in the linear system of equations.
441
+ grid_widgets : dict[int, tuple[int, int, int, int, ViewBox]]
442
+ Dictionary mapping order of viewboxes added as int to their grid layout description:
443
+ (start_y, start_x, span_y, span_x, ViewBox).
444
+ """
344
445
  assert(total_var_w is not None)
345
446
  assert(total_var_h is not None)
346
447
 
347
448
  for ws in width_grid:
348
449
  for w in ws:
349
- solver.addConstraint(w >= 0,)
450
+ solver.addConstraint(w >= 0)
350
451
 
351
452
  for hs in height_grid:
352
453
  for h in hs:
@@ -375,6 +476,7 @@ class Grid(Widget):
375
476
  solver.addConstraint(total_h <= total_var_h)
376
477
 
377
478
  def _recreate_solver(self):
479
+ """Recreate the linear system solver with all constraints."""
378
480
  self._solver.reset()
379
481
  self._var_w = Variable("w_rect")
380
482
  self._var_h = Variable("h_rect")
@@ -390,43 +492,40 @@ class Grid(Widget):
390
492
  self._solver.addConstraint(self._var_w >= 0)
391
493
  self._solver.addConstraint(self._var_h >= 0)
392
494
 
393
- # self._height_stay = None
394
- # self._width_stay = None
395
-
396
495
  # add widths
397
- self._width_grid = np.array([[Variable("width(x: %s, y: %s)" % (x, y))
398
- for x in range(0, xmax)]
399
- for y in range(0, ymax)])
496
+ self._width_grid = np.array(
497
+ [
498
+ [Variable(f"width(x: {x}, y: {y})") for x in range(xmax)]
499
+ for y in range(ymax)
500
+ ]
501
+ )
400
502
 
401
503
  # add heights
402
- self._height_grid = np.array([[Variable("height(x: %s, y: %s" % (x, y))
403
- for y in range(0, ymax)]
404
- for x in range(0, xmax)])
405
-
406
- # setup stretch
407
- stretch_grid = np.zeros(shape=(xmax, ymax, 2), dtype=float)
408
- stretch_grid.fill(1)
409
-
410
- for (_, val) in self._grid_widgets.items():
411
- (y, x, ys, xs, widget) = val
412
- stretch_grid[x:x+xs, y:y+ys] = widget.stretch
413
-
504
+ self._height_grid = np.array(
505
+ [
506
+ [Variable(f"height(x: {x}, y: {y})") for y in range(ymax)]
507
+ for x in range(xmax)
508
+ ]
509
+ )
510
+
511
+ if isinstance(self.spacing, tuple):
512
+ width_spacing, height_spacing = self.spacing
513
+ else:
514
+ width_spacing = height_spacing = self.spacing
414
515
  # even though these are REQUIRED, these should never fail
415
516
  # since they're added first, and thus the slack will "simply work".
416
- Grid._add_total_width_constraints(self._solver,
417
- self._width_grid, self._var_w)
418
- Grid._add_total_height_constraints(self._solver,
419
- self._height_grid, self._var_h)
517
+ Grid._add_total_dim_length_constraints(self._solver,
518
+ self._width_grid, self._n_added, self._var_w, width_spacing)
519
+ Grid._add_total_dim_length_constraints(self._solver,
520
+ self._height_grid, self._n_added, self._var_h, height_spacing)
420
521
 
421
522
  try:
422
523
  # these are REQUIRED constraints for width and height.
423
524
  # These are the constraints which can fail if
424
525
  # the corresponding dimension of the widget cannot be fit in the
425
526
  # grid.
426
- Grid._add_gridding_width_constraints(self._solver,
427
- self._width_grid)
428
- Grid._add_gridding_height_constraints(self._solver,
429
- self._height_grid)
527
+ Grid._add_gridding_dim_constraints(self._solver, self._width_grid)
528
+ Grid._add_gridding_dim_constraints(self._solver, self._height_grid)
430
529
  except UnsatisfiableConstraint:
431
530
  self._need_solver_recreate = True
432
531
 
@@ -436,24 +535,27 @@ class Grid(Widget):
436
535
  self._width_grid,
437
536
  self._height_grid,
438
537
  self._grid_widgets,
439
- self._widget_grid)
538
+ self._widget_grid,
539
+ )
440
540
 
441
541
  Grid._add_widget_dim_constraints(self._solver,
442
542
  self._width_grid,
443
543
  self._height_grid,
444
544
  self._var_w,
445
545
  self._var_h,
446
- self._grid_widgets)
546
+ self._grid_widgets
547
+ )
447
548
 
448
549
  self._solver.updateVariables()
449
550
 
450
551
  def _update_child_widget_dim(self):
552
+ """Solve the linear system of equations in order to assign Viewbox parameters such as position."""
451
553
  # think in terms of (x, y). (row, col) makes code harder to read
452
554
  ymax, xmax = self.grid_size
453
555
  if ymax <= 0 or xmax <= 0:
454
556
  return
455
557
 
456
- rect = self.rect # .padded(self.padding + self.margin)
558
+ rect = self.rect.padded(self.padding + self.margin)
457
559
  if rect.width <= 0 or rect.height <= 0:
458
560
  return
459
561
  if self._need_solver_recreate:
@@ -474,22 +576,40 @@ class Grid(Widget):
474
576
 
475
577
  value_vectorized = np.vectorize(lambda x: x.value())
476
578
 
477
- for (_, val) in self._grid_widgets.items():
579
+ if isinstance(self.spacing, tuple):
580
+ width_spacing, height_spacing = self.spacing
581
+ else:
582
+ width_spacing = height_spacing = self.spacing
583
+
584
+ for index, (_, val) in enumerate(self._grid_widgets.items()):
478
585
  (row, col, rspan, cspan, widget) = val
479
586
 
587
+ # If spacing, always one spacing unit between 2 grid positions, even when span is > 1.
588
+ # If span is > 1, spacing will be added to the dim length of Viewbox
589
+ spacing_width_offset = col * width_spacing if self._n_added > 1 else 0
590
+ spacing_height_offset = row * height_spacing if self._n_added > 1 else 0
591
+
592
+ # Add one spacing unit to dim length of the Viewbox per grid positions the ViewBox spans if span > 1.
593
+ width_increase_spacing = width_spacing * (cspan - 1)
594
+ height_increase_spacing = height_spacing * (rspan - 1)
595
+
480
596
  width = np.sum(value_vectorized(
481
- self._width_grid[row][col:col+cspan]))
597
+ self._width_grid[row][col:col+cspan])) + width_increase_spacing
482
598
  height = np.sum(value_vectorized(
483
- self._height_grid[col][row:row+rspan]))
599
+ self._height_grid[col][row:row+rspan])) + height_increase_spacing
600
+
484
601
  if col == 0:
485
602
  x = 0
486
603
  else:
487
- x = np.sum(value_vectorized(self._width_grid[row][0:col]))
604
+ x = np.sum(value_vectorized(self._width_grid[row][:col])) + spacing_width_offset
488
605
 
489
606
  if row == 0:
490
607
  y = 0
491
608
  else:
492
- y = np.sum(value_vectorized(self._height_grid[col][0:row]))
609
+ y = np.sum(value_vectorized(self._height_grid[col][:row])) + spacing_height_offset
610
+
611
+ x += self.padding
612
+ y += self.padding
493
613
 
494
614
  if isinstance(widget, ViewBox):
495
615
  widget.rect = Rect(x, y, width, height)
@@ -312,6 +312,17 @@ class Widget(Compound):
312
312
  self._update_line()
313
313
  self.update()
314
314
 
315
+ @property
316
+ def border_width(self):
317
+ """The width of the border."""
318
+ return self._border_width
319
+
320
+ @border_width.setter
321
+ def border_width(self, b):
322
+ self._border_width = float(b)
323
+ self._update_line()
324
+ self.update()
325
+
315
326
  @property
316
327
  def bgcolor(self):
317
328
  """The background color of the Widget."""
vispy/version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.14.3'
16
- __version_tuple__ = version_tuple = (0, 14, 3)
20
+ __version__ = version = '0.15.2'
21
+ __version_tuple__ = version_tuple = (0, 15, 2)
@@ -291,10 +291,11 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
291
291
 
292
292
  range_min, range_max = self._data_limits
293
293
  clim_min, clim_max = self.clim
294
- if clim_min == clim_max:
294
+ full_range = range_max - range_min
295
+ if clim_min == clim_max or full_range == 0:
295
296
  return 0, np.inf
296
- clim_min = (clim_min - range_min) / (range_max - range_min)
297
- clim_max = (clim_max - range_min) / (range_max - range_min)
297
+ clim_min = (clim_min - range_min) / full_range
298
+ clim_max = (clim_max - range_min) / full_range
298
299
  return clim_min, clim_max
299
300
 
300
301
  @staticmethod
@@ -312,7 +313,7 @@ class CPUScaledTextureMixin(_ScaledTextureMixin):
312
313
  def scale_and_set_data(self, data, offset=None, copy=True):
313
314
  """Upload new data to the GPU, scaling if necessary."""
314
315
  if self._data_dtype is None:
315
- data.dtype == self._data_dtype
316
+ self._data_dtype = data.dtype
316
317
 
317
318
  # ensure dtype is the same as it was before, or funny things happen
318
319
  # no copy is performed unless asked for or necessary
@@ -421,6 +421,7 @@ class ShadingFilter(Filter):
421
421
  ffunc = Function(self._shaders['fragment'])
422
422
 
423
423
  self._normals = VertexBuffer(np.zeros((0, 3), dtype=np.float32))
424
+ self._normals_cache = None
424
425
  vfunc['normal'] = self._normals
425
426
 
426
427
  super().__init__(vcode=vfunc, fcode=ffunc)
@@ -550,7 +551,11 @@ class ShadingFilter(Filter):
550
551
  )
551
552
 
552
553
  normals = self._visual.mesh_data.get_vertex_normals(indexed='faces')
553
- self._normals.set_data(normals, convert=True)
554
+ if normals is not self._normals_cache:
555
+ # limit how often we upload new normal arrays
556
+ # gotcha: if normals are changed in place then this won't invalidate this cache
557
+ self._normals_cache = normals
558
+ self._normals.set_data(self._normals_cache, convert=True)
554
559
 
555
560
  def on_mesh_data_updated(self, event):
556
561
  self._update_data()
@@ -4,27 +4,31 @@
4
4
 
5
5
  from __future__ import division
6
6
 
7
+ import numpy as np
8
+
7
9
  from .image import ImageVisual
8
10
  from ..color import Color
9
11
  from .shaders import Function
10
12
 
11
13
 
12
14
  _GRID_COLOR = """
15
+ uniform vec4 u_gridlines_bounds;
16
+ uniform float u_border_width;
17
+
13
18
  vec4 grid_color(vec2 pos) {
14
19
  vec4 px_pos = $map_to_doc(vec4(pos, 0, 1));
15
20
  px_pos /= px_pos.w;
16
21
 
17
22
  // Compute vectors representing width, height of pixel in local coords
18
- float s = 1.;
19
23
  vec4 local_pos = $map_doc_to_local(px_pos);
20
- vec4 dx = $map_doc_to_local(px_pos + vec4(1.0 / s, 0, 0, 0));
21
- vec4 dy = $map_doc_to_local(px_pos + vec4(0, 1.0 / s, 0, 0));
24
+ vec4 dx = $map_doc_to_local(px_pos + vec4(1.0, 0, 0, 0));
25
+ vec4 dy = $map_doc_to_local(px_pos + vec4(0, 1.0, 0, 0));
22
26
  local_pos /= local_pos.w;
23
27
  dx = dx / dx.w - local_pos;
24
28
  dy = dy / dy.w - local_pos;
25
29
 
26
30
  // Pixel length along each axis, rounded to the nearest power of 10
27
- vec2 px = s * vec2(abs(dx.x) + abs(dy.x), abs(dx.y) + abs(dy.y));
31
+ vec2 px = vec2(abs(dx.x) + abs(dy.x), abs(dx.y) + abs(dy.y));
28
32
  float log10 = log(10.0);
29
33
  float sx = pow(10.0, floor(log(px.x) / log10) + 1.) * $scale.x;
30
34
  float sy = pow(10.0, floor(log(px.y) / log10) + 1.) * $scale.y;
@@ -57,6 +61,17 @@ vec4 grid_color(vec2 pos) {
57
61
  if (alpha == 0.) {
58
62
  discard;
59
63
  }
64
+
65
+ if (any(lessThan(local_pos.xy + u_border_width / 2, u_gridlines_bounds.xz)) ||
66
+ any(greaterThan(local_pos.xy - u_border_width / 2, u_gridlines_bounds.yw))) {
67
+ discard;
68
+ }
69
+
70
+ if (any(lessThan(local_pos.xy - u_gridlines_bounds.xz, vec2(u_border_width / 2))) ||
71
+ any(lessThan(u_gridlines_bounds.yw - local_pos.xy, vec2(u_border_width / 2)))) {
72
+ alpha = 1;
73
+ }
74
+
60
75
  return vec4($color.rgb, $color.a * alpha);
61
76
  }
62
77
  """
@@ -73,9 +88,16 @@ class GridLinesVisual(ImageVisual):
73
88
  color : Color
74
89
  The base color for grid lines. The final color may have its alpha
75
90
  channel modified.
91
+ grid_bounds : tuple or None
92
+ The lower and upper bound for each axis beyond which no grid is rendered.
93
+ In the form of (minx, maxx, miny, maxy).
94
+ border_width : float
95
+ Tickness of the border rendered at the bounds of the grid.
76
96
  """
77
97
 
78
- def __init__(self, scale=(1, 1), color='w'):
98
+ def __init__(self, scale=(1, 1), color='w',
99
+ grid_bounds=None,
100
+ border_width=2):
79
101
  # todo: PlaneVisual should support subdivide/impostor methods from
80
102
  # image and gridlines should inherit from plane instead.
81
103
  self._grid_color_fn = Function(_GRID_COLOR)
@@ -86,6 +108,40 @@ class GridLinesVisual(ImageVisual):
86
108
  self.shared_program.frag['get_data'] = self._grid_color_fn
87
109
  cfun = Function('vec4 null(vec4 x) { return x; }')
88
110
  self.shared_program.frag['color_transform'] = cfun
111
+ self.unfreeze()
112
+ self.grid_bounds = grid_bounds
113
+ self.border_width = border_width
114
+ self.freeze()
115
+
116
+ @property
117
+ def grid_bounds(self):
118
+ return self._grid_bounds
119
+
120
+ @grid_bounds.setter
121
+ def grid_bounds(self, value):
122
+ if value is None:
123
+ value = (None,) * 4
124
+ grid_bounds = []
125
+ for i, v in enumerate(value):
126
+ if v is None:
127
+ if i % 2:
128
+ v = -np.inf
129
+ else:
130
+ v = np.inf
131
+ grid_bounds.append(v)
132
+ self.shared_program['u_gridlines_bounds'] = value
133
+ self._grid_bounds = grid_bounds
134
+ self.update()
135
+
136
+ @property
137
+ def border_width(self):
138
+ return self._border_width
139
+
140
+ @border_width.setter
141
+ def border_width(self, value):
142
+ self.shared_program['u_border_width'] = value
143
+ self._border_width = value
144
+ self.update()
89
145
 
90
146
  @property
91
147
  def size(self):
vispy/visuals/image.py CHANGED
@@ -91,11 +91,9 @@ _TEXTURE_LOOKUP = """
91
91
 
92
92
  _APPLY_CLIM_FLOAT = """
93
93
  float apply_clim(float data) {
94
- // If data is NaN, don't draw it at all
95
- // http://stackoverflow.com/questions/11810158/how-to-deal-with-nan-or-inf-in-opengl-es-2-0-shaders
96
- if (!(data <= 0.0 || 0.0 <= data)) {
97
- discard;
98
- }
94
+ // pass through NaN values to get handled by the colormap
95
+ if (!(data <= 0.0 || 0.0 <= data)) return data;
96
+
99
97
  data = clamp(data, min($clim.x, $clim.y), max($clim.x, $clim.y));
100
98
  data = (data - $clim.x) / ($clim.y - $clim.x);
101
99
  return data;
@@ -103,7 +101,7 @@ _APPLY_CLIM_FLOAT = """
103
101
 
104
102
  _APPLY_CLIM = """
105
103
  vec4 apply_clim(vec4 color) {
106
- // Handle NaN values
104
+ // Handle NaN values (clamp them to the minimum value)
107
105
  // http://stackoverflow.com/questions/11810158/how-to-deal-with-nan-or-inf-in-opengl-es-2-0-shaders
108
106
  color.r = !(color.r <= 0.0 || 0.0 <= color.r) ? min($clim.x, $clim.y) : color.r;
109
107
  color.g = !(color.g <= 0.0 || 0.0 <= color.g) ? min($clim.x, $clim.y) : color.g;
@@ -117,6 +115,9 @@ _APPLY_CLIM = """
117
115
 
118
116
  _APPLY_GAMMA_FLOAT = """
119
117
  float apply_gamma(float data) {
118
+ // pass through NaN values to get handled by the colormap
119
+ if (!(data <= 0.0 || 0.0 <= data)) return data;
120
+
120
121
  return pow(data, $gamma);
121
122
  }"""
122
123
 
@@ -129,7 +130,7 @@ _APPLY_GAMMA = """
129
130
 
130
131
  _NULL_COLOR_TRANSFORM = 'vec4 pass(vec4 color) { return color; }'
131
132
 
132
- _C2L_RED = 'float cmap(vec4 color) { return color.r; }'
133
+ _C2L_RED = 'float color_to_luminance(vec4 color) { return color.r; }'
133
134
 
134
135
  _CUSTOM_FILTER = """
135
136
  vec4 texture_lookup(vec2 texcoord) {
@@ -456,6 +457,17 @@ class ImageVisual(Visual):
456
457
  self.shared_program.frag['color_transform'][2]['gamma'] = self._gamma
457
458
  self.update()
458
459
 
460
+ @property
461
+ def bad_color(self):
462
+ """Color used to render NaN values."""
463
+ return self._cmap.get_bad_color()
464
+
465
+ @bad_color.setter
466
+ def bad_color(self, color):
467
+ self._cmap.set_bad_color(color)
468
+ self._need_colortransform_update = True
469
+ self.update()
470
+
459
471
  @property
460
472
  def method(self):
461
473
  """Get rendering method name."""
@@ -126,7 +126,7 @@ class SurfacePlotVisual(MeshVisual):
126
126
  # convert (width, height, 4) to (num_verts, 4)
127
127
  vert_shape = self.__vertices.shape
128
128
  num_vertices = vert_shape[0] * vert_shape[1]
129
- colors = colors.reshape(num_vertices, 3)
129
+ colors = colors.reshape(num_vertices, colors.shape[-1])
130
130
  return colors
131
131
 
132
132
  def set_data(self, x=None, y=None, z=None, colors=None):