Skip to content

Commit

Permalink
Implement GridStack layout (#2375)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Jun 11, 2021
1 parent 415798d commit 999f5c5
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 5 deletions.
180 changes: 180 additions & 0 deletions examples/reference/layouts/GridStack.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"\n",
"from panel.layout.gridstack import GridStack\n",
"\n",
"pn.extension('gridstack')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``GridStack`` layout allows arranging multiple Panel objects in a grid using a simple API to assign objects to individual grid cells or to a grid span. Other layout containers function like lists, but a `GridSpec` has an API similar to a 2D array, making it possible to use 2D assignment to populate, index, and slice the grid.\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"* **``allow_resize``** (bool): Whether to allow resizing grid cells.\n",
"* **``allow_drag``** (bool): Whether to allow dragging grid cells.\n",
"* **``ncols``** (int): Allows specifying a fixed number of columns (otherwise grid expands to match assigned objects)\n",
"* **``nrows``** (int): Allows specifying a fixed number of rows (otherwise grid expands to match assigned objects)\n",
"* **``mode``** (str): Whether to 'warn', 'error', or simply 'override' on overlapping assignment\n",
"* **``objects``** (list): The list of objects to display in the GridSpec. Should not generally be modified directly except when replaced in its entirety.\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"A ``GridStack`` can be created either with a fixed size (the default) or with responsive sizing. In both cases the ``GridSpec`` will modify the contents to ensure the objects fill the grid cells assigned to them.\n",
"\n",
"To demonstrate this behavior, let us declare a responsively sized ``GridStack`` and then assign ``Spacer`` objects with distinct colors. We populate a ``6x12`` grid with these objects and display it:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gstack = GridStack(sizing_mode='stretch_both')\n",
"\n",
"gstack[ : , 0: 3] = pn.Spacer(background='red', margin=0)\n",
"gstack[0:2, 3: 9] = pn.Spacer(background='green', margin=0)\n",
"gstack[2:4, 6:12] = pn.Spacer(background='orange', margin=0)\n",
"gstack[4:6, 3:12] = pn.Spacer(background='blue', margin=0)\n",
"gstack[0:2, 9:12] = pn.Spacer(background='purple', margin=0)\n",
"\n",
"gstack"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see the fixed-size ``GridStack`` fills the `800x600` pixels assigned to it and each of the Spacer objects has been resized to fill the alloted grid cells, including the empty grid cell in the center. A convenient way to get an overview of the grid without rendering it is to display the ``grid`` property, which returns an array showing which grid cells have been filled:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gstack.grid"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In addition to assigning objects to the grid we can also index the grid:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(gstack[2, 2], width=400, height=400)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And select a subregion using slicing semantics:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"gstack[0, 1:]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The behavior when replacing existing grid cells can be controlled using the ``mode`` option. By default the ``GridStack`` will warn when assigning to one or more grid cells that are already occupied. The behavior may be changed to either error or override silently, by setting ``mode='error'`` or ``mode='override'`` respectively.\n",
"\n",
"### Fixed size grids\n",
"\n",
"We can also set explicit `width` and `height` values on a `GridStack`. Just like in the responsive mode, the ``GridStack`` will automatically set the appropriate sizing values on the grid contents to fill the space correctly. This means that when we resize a component and the state is synced with Python the new size is computed there and only then is the display updated:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import holoviews as hv\n",
"import holoviews.plotting.bokeh\n",
"\n",
"from bokeh.plotting import figure\n",
"\n",
"fig = figure()\n",
"fig.scatter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [0, 1, 2, 3, 2, 1, 0, -1, -2, -3])\n",
"\n",
"gstack = GridStack(width=800, height=600)\n",
"\n",
"gstack[0, :3] = pn.Spacer(background='#FF0000')\n",
"gstack[1:3, 0] = pn.Spacer(background='#0000FF')\n",
"gstack[1:3, 1:3] = fig\n",
"gstack[3:5, 0] = hv.Curve([1, 2, 3])\n",
"gstack[3:5, 1] = 'https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png'\n",
"gstack[3:5, 2] = pn.Column(\n",
" pn.widgets.FloatSlider(),\n",
" pn.widgets.ColorPicker(),\n",
" pn.widgets.Toggle(name='Toggle Me!')\n",
")\n",
"\n",
"gstack"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.8"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
3 changes: 2 additions & 1 deletion panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ class panel_extension(_pyviz_extension):
'ipywidgets': 'ipywidgets_bokeh.widget',
'perspective': 'panel.models.perspective',
'terminal': 'panel.models.terminal',
'tabulator': 'panel.models.tabulator'
'tabulator': 'panel.models.tabulator',
'gridstack': 'panel.layout.gridstack'
}

# Check whether these are loaded before rendering (if any item
Expand Down
4 changes: 2 additions & 2 deletions panel/layout/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def __init__(self, **params):
@param.depends('nrows', watch=True)
def _update_nrows(self):
if not self._updating:
self._rows_fixed = self.nrows is not None
self._rows_fixed = bool(self.nrows)

@param.depends('ncols', watch=True)
def _update_ncols(self):
Expand Down Expand Up @@ -371,7 +371,7 @@ def __getitem__(self, index):
if isinstance(subgrid, np.ndarray):
params = dict(self.param.get_param_values())
params['objects'] = OrderedDict([list(o)[0] for o in subgrid.flatten()])
gspec = GridSpec(**params)
gspec = type(self)(**params)
xoff, yoff = gspec._xoffset, gspec._yoffset
adjusted = []
for (y0, x0, y1, x1), obj in gspec.objects.items():
Expand Down
140 changes: 140 additions & 0 deletions panel/layout/gridstack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from collections import OrderedDict

import param

from ..io.resources import bundled_files
from ..reactive import ReactiveHTML
from ..util import classproperty
from .grid import GridSpec


class GridStack(ReactiveHTML, GridSpec):
"""
The GridStack layout builds on the GridSpec component and
gridstack.js to allow resizing and dragging items in the grid.
"""

allow_resize = param.Boolean(default=True, doc="""
Allow resizing the grid cells.""")

allow_drag = param.Boolean(default=True, doc="""
Allow dragging the grid cells.""")

state = param.List(doc="""
Current state of the grid (updated as items are resized and
dragged).""")

width = param.Integer(default=None)

height = param.Integer(default=None)

_template = """
<div id="grid" class="grid-stack">
{% for key, obj in objects.items() %}
<div data-id="{{ id(obj) }}" class="grid-stack-item" gs-h="{{ (key[2] or nrows)-(key[0] or 0) }}" gs-w="{{ (key[3] or ncols)-(key[1] or 0) }}" gs-y="{{ (key[0] or 0) }}" gs-x="{{ (key[1] or 0) }}">
<div id="content" class="grid-stack-item-content">${obj}</div>
</div>
{% endfor %}
</div>
""" # noqa

_scripts = {
'render': ["""
const options = {
column: data.ncols,
disableResize: !data.allow_resize,
disableDrag: !data.allow_drag,
margin: 0
}
if (data.nrows)
options.row = data.nrows
if (model.height)
options.cellHeight = Math.floor(model.height/data.nrows)
const gridstack = GridStack.init(options, grid);
function sync_state() {
const items = []
for (const node of gridstack.engine.nodes) {
items.push({id: node.el.getAttribute('data-id'), x0: node.x, y0: node.y, x1: node.x+node.w, y1: node.y+node.h})
}
data.state = items
}
gridstack.on('resizestop', (event, el) => {
window.dispatchEvent(new Event("resize"));
sync_state()
})
gridstack.on('dragstop', (event, el) => {
sync_state()
})
sync_state()
state.gridstack = gridstack
"""],
'allow_drag': ["state.gridstack.enableMove(data.allow_drag)"],
'allow_resize': ["state.gridstack.enableResize(data.allow_resize)"],
'ncols': ["state.gridstack.column(data.ncols)"],
'nrows': ["""
state.gristack.opts.row = data.nrows
if (data.nrows && model.height)
state.gridstack.cellHeight(Math.floor(model.height/data.nrows))
else
state.gridstack.cellHeight('auto')
"""]
}

__css_raw__ = [
'https://cdn.jsdelivr.net/npm/gridstack@4.2.5/dist/gridstack.min.css',
'https://cdn.jsdelivr.net/npm/gridstack@4.2.5/dist/gridstack-extra.min.css'
]

__javascript_raw__ = [
'https://cdn.jsdelivr.net/npm/gridstack@4.2.5/dist/gridstack-h5.js'
]

_rename = {}

@classproperty
def __javascript__(cls):
return bundled_files(cls)

@classproperty
def __css__(cls):
return bundled_files(cls, 'css')

@param.depends('state', watch=True)
def _update_objects(self):
objects = OrderedDict()
object_ids = {str(id(obj)): obj for obj in self}
for p in self.state:
objects[(p['y0'], p['x0'], p['y1'], p['x1'])] = object_ids[p['id']]
self.objects.clear()
self.objects.update(objects)
self._update_sizing()

@param.depends('objects', watch=True)
def _update_sizing(self):
if self.ncols:
width = int(float(self.width)/self.ncols)
else:
width = 0

if self.nrows:
height = int(float(self.height)/self.nrows)
else:
height = 0

for i, ((y0, x0, y1, x1), obj) in enumerate(self.objects.items()):
x0 = 0 if x0 is None else x0
x1 = (self.ncols) if x1 is None else x1
y0 = 0 if y0 is None else y0
y1 = (self.nrows) if y1 is None else y1
h, w = y1-y0, x1-x0

if self.sizing_mode in ['fixed', None]:
properties = {'width': w*width, 'height': h*height}
else:
properties = {'sizing_mode': self.sizing_mode}
if 'width' in self.sizing_mode:
properties['height'] = h*height
elif 'height' in self.sizing_mode:
properties['width'] = w*width
obj.param.set_param(**{k: v for k, v in properties.items()
if not obj.param[k].readonly})
5 changes: 4 additions & 1 deletion panel/models/reactive_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,14 @@ def construct_data_model(parameterized, name=None, ignore=[]):
continue
prop = PARAM_MAPPING.get(type(p))
pname = parameterized._rename.get(pname, pname)
if pname == 'name':
if pname == 'name' or pname is None:
continue
nullable = getattr(p, 'allow_None', False)
kwargs = {'default': p.default, 'help': p.doc}
if prop is None:
properties[pname] = bp.Any(**kwargs)
elif nullable:
properties[pname] = bp.Nullable(prop(p, {}), **kwargs)
else:
properties[pname] = prop(p, kwargs)
name = name or parameterized.name
Expand Down
2 changes: 1 addition & 1 deletion panel/reactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -1306,7 +1306,7 @@ def _get_template(self):

# Render Jinja template
template = jinja2.Template(template_string)
context = {'param': self.param, '__doc__': self.__original_doc__}
context = {'param': self.param, '__doc__': self.__original_doc__, 'id': id}
for parameter, value in self.param.get_param_values():
context[parameter] = value
if parameter in self._child_names:
Expand Down

0 comments on commit 999f5c5

Please sign in to comment.