Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GridStack layout #2375

Merged
merged 5 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]/dist/gridstack.min.css',
'https://cdn.jsdelivr.net/npm/[email protected]/dist/gridstack-extra.min.css'
]

__javascript_raw__ = [
'https://cdn.jsdelivr.net/npm/[email protected]/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