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

Adds Swipe layout #3007

Merged
merged 18 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from 16 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
96 changes: 96 additions & 0 deletions examples/reference/layouts/Swipe.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "ed7c1d01-d655-4878-a27d-c6d0e330097b",
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"import pandas as pd\n",
"\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"id": "ea1bc3ca-f83c-48e5-8046-7a4341ed835d",
"metadata": {},
"source": [
"The `Swipe` layout enables you to quickly compare two panels layed out on top of each other with a part of the *before* panel shown on one side of a slider and a part of the *after* panel shown on the other side.\n",
"\n",
"#### Parameters:\n",
"\n",
"* **``objects``** (list): The before and after components to lay out.\n",
"* **``value``** (int): The percentage of the *after* panel shown. Default is 50.\n",
"\n",
"Styling-related parameters:\n",
"\n",
"* **``slider_width``** (int): The width of the slider in pixels. Default is 12.\n",
"* **``slider_color``** (str): The color of the slider. Default is 'black'.\n",
"\n",
"For further layout and styling-related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"---"
]
},
{
"cell_type": "markdown",
"id": "5613561d-d483-45df-8cbb-366abbb96e79",
"metadata": {},
"source": [
"The `Swipe` layout accepts any two objects, which must have identical sizing options to work as intended.\n",
"\n",
"Here we compare two images of mean surface temperatures in 1945-1949 and temperatures between 2015-2019:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "07cdc635-9642-4335-9862-d8195dadade7",
"metadata": {},
"outputs": [],
"source": [
"gis_1880 = 'https://earthobservatory.nasa.gov/ContentWOC/images/globaltemp/global_gis_1945-1949.png'\n",
"gis_2015 = 'https://earthobservatory.nasa.gov/ContentWOC/images/globaltemp/global_gis_2015-2019.png'\n",
"\n",
"pn.Swipe(gis_1880, gis_2015)"
]
},
{
"cell_type": "markdown",
"id": "9af72676-c50d-4c61-820d-048daf8872e9",
"metadata": {},
"source": [
"The layout can compare any type of component, e.g. here we will compare two violin plots:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6187cb3-21a1-4900-be93-02ce3e480ff4",
"metadata": {},
"outputs": [],
"source": [
"import hvplot.pandas\n",
"\n",
"penguins = pd.read_csv('https://datasets.holoviz.org/penguins/v1/penguins.csv')\n",
"\n",
"pn.Swipe(\n",
" penguins[penguins.species=='Chinstrap'].hvplot.violin('bill_length_mm', color='#00cde1'),\n",
" penguins[penguins.species=='Adelie'].hvplot.violin('bill_length_mm', color='#cd0000'),\n",
" value=51\n",
")"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
4 changes: 2 additions & 2 deletions panel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
_jupyter_server_extension_paths, cache, ipywidget, serve, state,
)
from .layout import ( # noqa
Accordion, Card, Column, FlexBox, GridBox, GridSpec, Row, Spacer, Tabs,
WidgetBox,
Accordion, Card, Column, FlexBox, GridBox, GridSpec, Row, Spacer, Swipe,
Tabs, WidgetBox,
)
from .pane import panel # noqa
from .param import Param # noqa
Expand Down
23 changes: 23 additions & 0 deletions panel/dist/css/swipe.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.swipe-container {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}

.swipe-container .outer {
grid-column: 1;
grid-row: 1;
}

.swipe-container .slider {
grid-row: 1;
grid-column: 1;
z-index: 1;
height: 100%;
cursor: move;
}

.swipe-container .inner {
width: 100%;
height: 100%;
}
1 change: 1 addition & 0 deletions panel/layout/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .spacer import ( # noqa
Divider, HSpacer, Spacer, VSpacer,
)
from .swipe import Swipe # noqa
from .tabs import Tabs # noqa

__all__ = (
Expand Down
139 changes: 139 additions & 0 deletions panel/layout/swipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""
The Swipe layout enables you to quickly compare two panels
"""

import param

from ..reactive import ReactiveHTML
from .base import ListLike


class Swipe(ListLike, ReactiveHTML):
"""
The Swipe layout enables you to quickly compare two panels layed
out on top of each other with a part of the *before* panel shown
on one side of a slider and a part of the *after* panel shown on
the other side.
"""

objects = param.List(default=[], bounds=(0, 2), doc="""
The list of child objects that make up the layout.""", precedence=-1)

slider_width = param.Integer(default=5, bounds=(0, 25), doc="""
The width of the slider in pixels""")

slider_color = param.Color(default="black", doc="""
The color of the slider""")

value = param.Integer(50, bounds=(0, 100), doc="""
The percentage of the *after* panel to show.""")

_before = param.Parameter()

_after = param.Parameter()

_template = """
<div id="container" class="swipe-container">
<div id="before" class="outer">
<div id="before-inner" class="inner">${_before}</div>
</div>
<div id="after" class="outer" style="overflow: hidden;">
<div id="after-inner" class="inner">${_after}</div>
</div>
<div id="slider" class="slider" onmousedown="${script('drag')}"
style="background: ${slider_color}; width: ${slider_width}px;">
</div>
</div>
"""

_scripts = {
'render': """
self.adjustSlider()
""",
'after_layout': """
self.value()
""",
'drag': """
function endDrag() {
document.removeEventListener('mouseup', endDrag);
document.removeEventListener('mousemove', handleDrag);
}
function handleDrag(e) {
e = e || window.event;
e.preventDefault();
current = e.clientX
start = view.el.getBoundingClientRect().left
value = parseInt(((current-start)/ container.clientWidth)*100)
data.value = Math.max(0, Math.min(value, 100))
}
let e = event || window.event;
e.preventDefault();
document.addEventListener('mouseup', endDrag);
document.addEventListener('mousemove', handleDrag);
""",
'value': """
before.style.clipPath = `polygon(0% 0%, calc(${data.value}% + 5px) 0%, calc(${data.value}% + 5px) 100%, 0% 100%)`
after.style.clipPath = `polygon(calc(${data.value}% + 5px) 0%, 100% 0%, 100% 100%, calc(${data.value}% + 5px) 100%)`
self.adjustSlider()
""",
'slider_width': "self.adjustSlider()",
'adjustSlider': """
halfWidth = parseInt(data.slider_width/2)
slider.style.marginLeft = `calc(${data.value}% + 5px - ${halfWidth}px)`
"""
}

_stylesheets = ['css/swipe.css']

def __init__(self, *objects, **params):
if 'objects' in params and objects:
raise ValueError(
"Either supply objects as an positional argument or "
"as a keyword argument, not both."
)
from ..pane.base import panel
objects = params.pop('objects', objects)
if not objects:
objects = [None, None]
super().__init__(objects=[panel(obj) for obj in objects], **params)

@param.depends('objects', watch=True, on_init=True)
def _update_layout(self):
self._before = self.before
self._after = self.after

@property
def before(self):
return self[0] if len(self) else None

@before.setter
def before(self, before):
self[0] = before

@property
def after(self):
return self[1] if len(self) > 1 else None

@after.setter
def after(self, after):
self[1] = after

def select(self, selector=None):
"""
Iterates over the Viewable and any potential children in the
applying the Selector.

Arguments
---------
selector: type or callable or None
The selector allows selecting a subset of Viewables by
declaring a type or callable function to filter by.

Returns
-------
viewables: list(Viewable)
"""
objects = super().select(selector)
for obj in self:
objects += obj.select(selector)
return objects
2 changes: 1 addition & 1 deletion panel/models/reactive_html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export class ReactiveHTMLView extends HTMLBoxView {
this._state,
this,
(s: any) => this.run_script(s),
this_obj
this_ob
)
}

Expand Down
21 changes: 21 additions & 0 deletions panel/tests/layout/test_swipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from panel.layout import Spacer, Swipe


def test_swip_construct(document, comm):
before = Spacer(background="red", height=400, width=800)
after = Spacer(background="green", height=400, width=800)
swipe = Swipe(
before,
after,
value=20,
height=800,
slider_width=15,
slider_color="lightgray"
)

model = swipe.get_root(document, comm)

assert model.children == {
'before-inner': [before._models[model.ref['id']][0]],
'after-inner': [after._models[model.ref['id']][0]]
}
Loading