diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78510240be..2a9736a7ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
# Releases
+## Version 0.14.4
+
+Date: 2023-03-03
+
+This release is a small bug fix release preceding the upcoming major release of Panel 1.0. Many thanks to the contributors to this release which include @MarcSkovMadsen, @maximlt, @Hoxbro and @philippjfr.
+
+### Bugs
+
+- Fix `Tabulator` client-side string filters by not parsing them as regex ([4423](https://github.com/holoviz/panel/pull/4423))
+- Fix the RGGPlot pane ([#4380](https://github.com/holoviz/panel/pull/4380))
+- Fix `panel examples` command by ensuring examples are correctly packaged ([#4484](https://github.com/holoviz/panel/pull/4484))
+- Fix event generation by considering NaNs as equal when comparing Numpy arrays ([#4481](https://github.com/holoviz/panel/pull/4481))
+- Use cache from previous sessions when using `to_disk` ([#4481](https://github.com/holoviz/panel/pull/4481))
+- Fix relative imports when running inside Jupyter Kernel ([#4489](https://github.com/holoviz/panel/pull/4489))
+- Do not re-create `Vega.selections` object unless selections changed ([#4497](https://github.com/holoviz/panel/pull/4497))
+
+### Enhancements
+
+- Add support for altair and vega-lite v5 ([#4488](https://github.com/holoviz/panel/pull/4488))
+
+### Misc
+
+- Use latest react-grid from CDN ([#4461](https://github.com/holoviz/panel/pull/4461))
+
## Version 0.14.3
Date: 2023-01-28
diff --git a/MANIFEST.in b/MANIFEST.in
index 1d82fe5ab4..605d16099d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -25,6 +25,7 @@ global-exclude *.py[co]
global-exclude *~
global-exclude *.ipynb_checkpoints/*
global-exclude *.whl
+graft panel/examples
graft examples
graft doc/
graft panel/dist
diff --git a/doc/releases.md b/doc/releases.md
index 73351cda1c..2475942df6 100644
--- a/doc/releases.md
+++ b/doc/releases.md
@@ -2,6 +2,30 @@
See [the HoloViz blog](https://blog.holoviz.org/tag/panel.html) for a visual summary of the major features added in each release.
+## Version 0.14.4
+
+Date: 2023-03-03
+
+This release is a small bug fix release preceding the upcoming major release of Panel 1.0. Many thanks to the contributors to this release which include @MarcSkovMadsen, @maximlt, @Hoxbro and @philippjfr.
+
+### Bugs
+
+- Fix `Tabulator` client-side string filters by not parsing them as regex ([4423](https://github.com/holoviz/panel/pull/4423))
+- Fix the RGGPlot pane ([#4380](https://github.com/holoviz/panel/pull/4380))
+- Fix `panel examples` command by ensuring examples are correctly packaged ([#4484](https://github.com/holoviz/panel/pull/4484))
+- Fix event generation by considering NaNs as equal when comparing Numpy arrays ([#4481](https://github.com/holoviz/panel/pull/4481))
+- Use cache from previous sessions when using `to_disk` ([#4481](https://github.com/holoviz/panel/pull/4481))
+- Fix relative imports when running inside Jupyter Kernel ([#4489](https://github.com/holoviz/panel/pull/4489))
+- Do not re-create `Vega.selections` object unless selections changed ([#4497](https://github.com/holoviz/panel/pull/4497))
+
+### Enhancements
+
+- Add support for altair and vega-lite v5 ([#4488](https://github.com/holoviz/panel/pull/4488))
+
+### Misc
+
+- Use latest react-grid from CDN ([#4461](https://github.com/holoviz/panel/pull/4461))
+
## Version 0.14.3
Date: 2023-01-28
diff --git a/examples/apps/django_multi_apps/requirements.txt b/examples/apps/django_multi_apps/requirements.txt
index a59eed3575..ec9afc4ef4 100644
--- a/examples/apps/django_multi_apps/requirements.txt
+++ b/examples/apps/django_multi_apps/requirements.txt
@@ -8,7 +8,7 @@ cffi==1.13.2
channels==2.4.0
colorcet==2.0.5
constantly==15.1.0
-cryptography==3.3.2
+cryptography==39.0.1
daphne==2.4.1
Django==3.1.14
holoviews==1.13.2
diff --git a/examples/reference/panes/Vega.ipynb b/examples/reference/panes/Vega.ipynb
index 16b67d7e29..6a5e89ab71 100644
--- a/examples/reference/panes/Vega.ipynb
+++ b/examples/reference/panes/Vega.ipynb
@@ -48,7 +48,7 @@
"outputs": [],
"source": [
"vegalite = {\n",
- " \"$schema\": \"https://vega.github.io/schema/vega-lite/v3.json\",\n",
+ " \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n",
" \"data\": {\"url\": \"https://raw.githubusercontent.com/vega/vega/master/docs/data/barley.json\"},\n",
" \"mark\": \"bar\",\n",
" \"encoding\": {\n",
@@ -92,7 +92,7 @@
"outputs": [],
"source": [
"vgl_pane.object = {\n",
- " \"$schema\": \"https://vega.github.io/schema/vega-lite/v3.json\",\n",
+ " \"$schema\": \"https://vega.github.io/schema/vega-lite/v5.json\",\n",
" \"data\": {\n",
" \"url\": \"https://raw.githubusercontent.com/vega/vega/master/docs/data/disasters.csv\"\n",
" },\n",
@@ -109,11 +109,16 @@
" },\n",
" \"encoding\": {\n",
" \"x\": {\n",
- " \"field\": \"Year\",\n",
- " \"type\": \"nominal\",\n",
- " \"axis\": {\"labelAngle\": 90}\n",
+ " \"field\": \"Year\",\n",
+ " \"type\": \"quantitative\",\n",
+ " \"axis\": {\"labelAngle\": 90},\n",
+ " \"scale\": {\"zero\": False}\n",
+ " },\n",
+ " \"y\": {\n",
+ " \"field\": \"Entity\",\n",
+ " \"type\": \"nominal\",\n",
+ " \"axis\": {\"title\": \"\"}\n",
" },\n",
- " \"y\": {\"field\": \"Entity\", \"type\": \"nominal\", \"axis\": {\"title\": \"\"}},\n",
" \"size\": {\n",
" \"field\": \"Deaths\",\n",
" \"type\": \"quantitative\",\n",
@@ -525,15 +530,16 @@
"source": [
"from vega_datasets import data\n",
"\n",
- "\n",
- "df = data.seattle_temps()[:100]\n",
+ "temps = data.seattle_temps()[:300]\n",
"\n",
"brush = alt.selection_interval(name='brush')\n",
"\n",
- "chart = alt.Chart(df).mark_circle().encode(\n",
+ "chart = alt.Chart(temps).mark_circle().encode(\n",
" x='date:T',\n",
- " y='temp:Q',\n",
+ " y=alt.Y('temp:Q', scale={'zero': False}),\n",
" color=alt.condition(brush, alt.value('coral'), alt.value('lightgray'))\n",
+ ").properties(\n",
+ " width=500\n",
").add_selection(\n",
" brush\n",
")\n",
@@ -541,16 +547,14 @@
"def filtered_table(selection):\n",
" if not selection:\n",
" return '## No selection'\n",
- " print(selection)\n",
" query = ' & '.join(\n",
" f'\"{pd.to_datetime(values[0], unit=\"ms\")}\" <= `{col}` <= \"{pd.to_datetime(values[1], unit=\"ms\")}\"'\n",
- " if pd.api.types.is_datetime64_any_dtype(df[col]) else f'{values[0]} <= `{col}` <= {values[1]}'\n",
+ " if pd.api.types.is_datetime64_any_dtype(temps[col]) else f'{values[0]} <= `{col}` <= {values[1]}'\n",
" for col, values in selection.items()\n",
" )\n",
- " print(query)\n",
" return pn.Column(\n",
" f'Query: {query}',\n",
- " pn.pane.DataFrame(df.query(query), width=600, height=300)\n",
+ " pn.pane.DataFrame(temps.query(query), width=600, height=300)\n",
" )\n",
"\n",
"\n",
diff --git a/panel/io/cache.py b/panel/io/cache.py
index ac6d28f3b9..d3b685d34b 100644
--- a/panel/io/cache.py
+++ b/panel/io/cache.py
@@ -364,8 +364,7 @@ def wrapped_func(*args, **kwargs):
func_cache = state._memoize_cache.get(func_hash)
- empty = func_cache is None
- if empty:
+ if func_cache is None:
if to_disk:
from diskcache import Index
cache = Index(os.path.join(cache_path, func_hash))
@@ -376,7 +375,7 @@ def wrapped_func(*args, **kwargs):
if ttl is not None:
_cleanup_ttl(func_cache, ttl, time)
- if not empty and hash_value in func_cache:
+ if hash_value in func_cache:
with lock:
ret, ts, count, _ = func_cache[hash_value]
func_cache[hash_value] = (ret, ts, count+1, time)
diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py
index df114a5d70..4a338d5b75 100644
--- a/panel/io/jupyter_server_extension.py
+++ b/panel/io/jupyter_server_extension.py
@@ -109,9 +109,11 @@ def get_server_root_dir(settings):
EXECUTION_TEMPLATE = """
import os
import pathlib
+import sys
app = '{{ path }}'
os.chdir(str(pathlib.Path(app).parent))
+sys.path = [os.getcwd()] + sys.path[1:]
from panel.io.jupyter_executor import PanelExecutor
executor = PanelExecutor(app, '{{ token }}', '{{ root_url }}')
@@ -233,6 +235,7 @@ async def get(self, path=None):
)
self.finish(html)
return
+
kernel_env = {**os.environ}
kernel_id = await ensure_async(
(
diff --git a/panel/io/model.py b/panel/io/model.py
index cc2e20c6dd..673d455459 100644
--- a/panel/io/model.py
+++ b/panel/io/model.py
@@ -39,10 +39,10 @@ class comparable_array(np.ndarray):
"""
def __eq__(self, other: Any) -> bool:
- return super().__eq__(other).all().item()
+ return np.array_equal(self, other, equal_nan=True)
def __ne__(self, other: Any) -> bool:
- return super().__ne__(other).all().item()
+ return not np.array_equal(self, other, equal_nan=True)
def monkeypatch_events(events: List['DocumentChangedEvent']) -> None:
"""
diff --git a/panel/package-lock.json b/panel/package-lock.json
index 493cd7a849..2ef5944f9f 100644
--- a/panel/package-lock.json
+++ b/panel/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@holoviz/panel",
- "version": "0.14.3",
+ "version": "0.14.4-rc.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@holoviz/panel",
- "version": "0.14.3",
+ "version": "0.14.4-rc.2",
"license": "BSD-3-Clause",
"dependencies": {
"@bokeh/bokehjs": "^2.4.3",
diff --git a/panel/package.json b/panel/package.json
index 734804f821..8434194ed7 100644
--- a/panel/package.json
+++ b/panel/package.json
@@ -1,6 +1,6 @@
{
"name": "@holoviz/panel",
- "version": "0.14.3",
+ "version": "0.14.4-rc.2",
"description": "A high level dashboarding library for python visualization libraries.",
"license": "BSD-3-Clause",
"repository": {
diff --git a/panel/pane/plot.py b/panel/pane/plot.py
index 281549591f..3065a2be74 100644
--- a/panel/pane/plot.py
+++ b/panel/pane/plot.py
@@ -361,7 +361,7 @@ class RGGPlot(PNG):
def applies(cls, obj: Any) -> float | bool | None:
return type(obj).__name__ == 'GGPlot' and hasattr(obj, 'r_repr')
- def _img(self):
+ def _data(self):
from rpy2 import robjects
from rpy2.robjects.lib import grdevices
with grdevices.render_to_bytesio(grdevices.png,
diff --git a/panel/pane/vega.py b/panel/pane/vega.py
index d113e22a8a..7f534c6e45 100644
--- a/panel/pane/vega.py
+++ b/panel/pane/vega.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import re
import sys
from typing import (
@@ -40,33 +41,59 @@ def ds_as_cds(dataset):
_containers = ['hconcat', 'vconcat', 'layer']
+SCHEMA_REGEX = re.compile('^v(\d+)\.\d+\.\d+.json')
+
def _isin(obj, attr):
if isinstance(obj, dict):
return attr in obj
else:
return hasattr(obj, attr)
-def _get_type(spec):
- if isinstance(spec, dict):
- return spec.get('type', 'interval')
+def _get_type(spec, version):
+ if version >= 5:
+ if isinstance(spec, dict):
+ return spec.get('select', {}).get('type', 'interval')
+ else:
+ return getattr(spec.select, 'type', 'interval')
else:
- return getattr(spec, 'type', 'interval')
-
+ if isinstance(spec, dict):
+ return spec.get('type', 'interval')
+ else:
+ return getattr(spec, 'type', 'interval')
-def _get_selections(obj):
+def _get_schema_version(obj, default_version: int = 5) -> int:
+ if Vega.is_altair(obj):
+ schema = obj.to_dict().get('$schema', '')
+ else:
+ schema = obj.get('$schema', '')
+ version = schema.split('/')[-1]
+ match = SCHEMA_REGEX.fullmatch(version)
+ if match is None or not match.groups():
+ return default_version
+ return int(match.groups()[0])
+
+def _get_selections(obj, version=None):
+ if version is None:
+ version = _get_schema_version(obj)
+ key = 'params' if version >= 5 else 'selection'
selections = {}
- if _isin(obj, 'selection'):
+ if _isin(obj, key):
+ params = obj[key]
+ if version >= 5 and isinstance(params, list):
+ params = {
+ p.name if hasattr(p, 'name') else p['name']: p for p in params
+ if getattr(p, 'param_type', None) == 'selection' or _isin(p, 'select')
+ }
try:
selections.update({
- name: _get_type(spec)
- for name, spec in obj['selection'].items()
+ name: _get_type(spec, version) for name, spec in params.items()
})
except (AttributeError, TypeError):
pass
for c in _containers:
if _isin(obj, c):
for subobj in obj[c]:
- selections.update(_get_selections(subobj))
+ selections.update(_get_selections(subobj, version=version))
return selections
@@ -144,6 +171,8 @@ def _update_selections(self, *args):
e: param.Dict() if stype == 'interval' else param.List()
for e, stype in self._selections.items()
}
+ if self.selection and (set(self.selection.param) - {'name'}) == set(params):
+ return
self.selection = type('Selection', (param.Parameterized,), params)()
@classmethod
diff --git a/panel/template/fast/grid/fast_grid_template.html b/panel/template/fast/grid/fast_grid_template.html
index 84c80cca39..6c622e8e12 100644
--- a/panel/template/fast/grid/fast_grid_template.html
+++ b/panel/template/fast/grid/fast_grid_template.html
@@ -454,7 +454,7 @@
);
}
}
- ReactDOM.render(, document.getElementById('responsive-grid'))
+ ReactDOM.render(, document.getElementById('responsive-grid'));
addFullScreenToggle()
diff --git a/panel/template/react/__init__.py b/panel/template/react/__init__.py
index 940869a65f..467d15efc9 100644
--- a/panel/template/react/__init__.py
+++ b/panel/template/react/__init__.py
@@ -55,7 +55,7 @@ class ReactTemplate(BasicTemplate):
'react': f"{config.npm_cdn}/react@18/umd/react.production.min.js",
'react-dom': f"{config.npm_cdn}/react-dom@18/umd/react-dom.production.min.js",
'babel': f"{config.npm_cdn}/babel-standalone@latest/babel.min.js",
- 'react-grid': "https://cdnjs.cloudflare.com/ajax/libs/react-grid-layout/1.1.1/react-grid-layout.min.js"
+ 'react-grid': f"{config.npm_cdn}/react-grid-layout@1.3.4/dist/react-grid-layout.min.js"
},
'css': {
'bootstrap': CSS_URLS['bootstrap4'],
diff --git a/panel/template/react/react.html b/panel/template/react/react.html
index ac41f96659..ea512c5354 100644
--- a/panel/template/react/react.html
+++ b/panel/template/react/react.html
@@ -259,7 +259,7 @@
);
}
}
- ReactDOM.render(, document.getElementById('responsive-grid'))
+ ReactDOM.render(, document.getElementById('responsive-grid'));
{{ embed(roots.js_area) }}
diff --git a/panel/tests/pane/test_vega.py b/panel/tests/pane/test_vega.py
index 57a525c77a..7f1a30d379 100644
--- a/panel/tests/pane/test_vega.py
+++ b/panel/tests/pane/test_vega.py
@@ -10,7 +10,6 @@
altair_available = pytest.mark.skipif(alt is None, reason="requires altair")
-
import numpy as np
import panel as pn
@@ -21,6 +20,7 @@
blank_schema = {'$schema': ''}
vega4_config = {'view': {'continuousHeight': 300, 'continuousWidth': 400}}
+vega5_config = {'view': {'continuousHeight': 300, 'continuousWidth': 300}}
vega_example = {
'config': {
@@ -38,6 +38,62 @@
'$schema': 'https://vega.github.io/schema/vega-lite/v3.2.1.json'
}
+vega4_selection_example = {
+ 'config': {'view': {'continuousWidth': 300, 'continuousHeight': 300}},
+ 'data': {'url': 'https://raw.githubusercontent.com/vega/vega/master/docs/data/penguins.json'},
+ 'mark': {'type': 'point'},
+ 'encoding': {
+ 'color': {
+ 'condition': {
+ 'selection': 'brush',
+ 'field': 'Species',
+ 'type': 'nominal'
+ },
+ 'value': 'lightgray'},
+ 'x': {
+ 'field': 'Beak Length (mm)',
+ 'scale': {'zero': False},
+ 'type': 'quantitative'
+ },
+ 'y': {
+ 'field': 'Beak Depth (mm)',
+ 'scale': {'zero': False},
+ 'type': 'quantitative'}
+ },
+ 'height': 250,
+ 'selection': {'brush': {'type': 'interval'}},
+ 'width': 250,
+ '$schema': 'https://vega.github.io/schema/vega-lite/v4.17.0.json'
+}
+
+vega5_selection_example = {
+ 'config': {'view': {'continuousWidth': 300, 'continuousHeight': 300}},
+ 'data': {'url': 'https://raw.githubusercontent.com/vega/vega/master/docs/data/penguins.json'},
+ 'mark': {'type': 'point'},
+ 'encoding': {
+ 'color': {
+ 'condition': {
+ 'param': 'brush',
+ 'field': 'Species',
+ 'type': 'nominal'
+ },
+ 'value': 'lightgray'},
+ 'x': {
+ 'field': 'Beak Length (mm)',
+ 'scale': {'zero': False},
+ 'type': 'quantitative'
+ },
+ 'y': {
+ 'field': 'Beak Depth (mm)',
+ 'scale': {'zero': False},
+ 'type': 'quantitative'}
+ },
+ 'height': 250,
+ 'params': [{'name': 'brush', 'select': {'type': 'interval'}}],
+ 'width': 250,
+ '$schema': 'https://vega.github.io/schema/vega-lite/v5.6.1.json'
+}
+
vega_inline_example = {
'config': {
'view': {'width': 400, 'height': 300},
@@ -190,6 +246,14 @@ def test_vega_pane_inline(document, comm):
assert pane._models == {}
+def test_vega_lite_4_selection_spec(document, comm):
+ vega = Vega(vega4_selection_example)
+ assert vega._selections == {'brush': 'interval'}
+
+def test_vega_lite_5_selection_spec(document, comm):
+ vega = Vega(vega5_selection_example)
+ assert vega._selections == {'brush': 'interval'}
+
def altair_example():
import altair as alt
data = alt.Data(values=[{'x': 'A', 'y': 5},
@@ -203,22 +267,23 @@ def altair_example():
)
return chart
-
@altair_available
def test_get_vega_pane_type_from_altair():
assert PaneBase.get_pane_type(altair_example()) is Vega
-
@altair_available
def test_altair_pane(document, comm):
- pane = pn.panel(altair_example())
+ pane = Vega(altair_example())
# Create pane
model = pane.get_root(document, comm=comm)
assert isinstance(model, VegaPlot)
expected = dict(vega_example, data={})
- if altair_version >= Version('4.0.0'):
+ if altair_version >= Version('5.0.0rc1'):
+ expected['mark'] = {'type': 'bar'}
+ expected['config'] = vega5_config
+ elif altair_version >= Version('4.0.0'):
expected['config'] = vega4_config
assert dict(model.data, **blank_schema) == dict(expected, **blank_schema)
@@ -231,7 +296,10 @@ def test_altair_pane(document, comm):
chart.data.values[0]['x'] = 'C'
pane.object = chart
point_example = dict(vega_example, data={}, mark='point')
- if altair_version >= Version('4.0.0'):
+ if altair_version >= Version('5.0.0rc1'):
+ point_example['mark'] = {'type': 'point'}
+ point_example['config'] = vega5_config
+ elif altair_version >= Version('4.0.0'):
point_example['config'] = vega4_config
assert dict(model.data, **blank_schema) == dict(point_example, **blank_schema)
cds_data = model.data_sources['data'].data
diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py
index 82c4f54f0f..845f75f6a9 100644
--- a/panel/widgets/tables.py
+++ b/panel/widgets/tables.py
@@ -469,7 +469,7 @@ def _get_header_filters(self, df):
elif op == 'in':
filters.append(col.isin(val))
elif op == 'like':
- filters.append(col.str.lower().str.contains(val.lower()))
+ filters.append(col.str.contains(val, case=False, regex=False))
elif op == 'starts':
filters.append(col.str.startsWith(val))
elif op == 'ends':
@@ -477,14 +477,14 @@ def _get_header_filters(self, df):
elif op == 'keywords':
match_all = filt_def.get(col_name, {}).get('matchAll', False)
sep = filt_def.get(col_name, {}).get('separator', ' ')
- matches = val.lower().split(sep)
+ matches = val.split(sep)
if match_all:
for match in matches:
- filters.append(col.str.lower().str.contains(match))
+ filters.append(col.str.contains(match, case=False, regex=False))
else:
- filt = col.str.lower().str.contains(matches[0])
+ filt = col.str.contains(matches[0], case=False, regex=False)
for match in matches[1:]:
- filt |= col.str.lower().str.contains(match)
+ filt |= col.str.contains(match, case=False, regex=False)
filters.append(filt)
elif op == 'regex':
raise ValueError("Regex filtering not supported.")