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

Add Vizzu pane #4226

Merged
merged 9 commits into from
Mar 23, 2023
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
200 changes: 200 additions & 0 deletions examples/reference/panes/Vizzu.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import pandas as pd\n",
"import panel as pn\n",
"\n",
"pn.extension('vizzu')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``Vizzu`` pane renders [Vizzu](https://lib.vizzuhq.com/) charts inside Panel. Note that to use the ``Vizzu`` pane in the notebook the Panel extension has to be loaded with 'vizzu' as an argument to ensure that vizzu.js is initialized. \n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"* **``object``** (dict | pd.DataFrame): The data expressed as a Python dictionary of arrays or DataFrame.\n",
"* **``animation``** (dict): Animation settings (see https://lib.vizzuhq.com/latest/reference/modules/vizzu.Anim.html).\n",
"* **``config``** (dict): The config contains all of the parameters needed to render a particular static chart or a state of an animated chart (see https://lib.vizzuhq.com/latest/reference/interfaces/vizzu.Config.Chart.html).\n",
"* **``columns``** (list): Optional column definitions. If not defined will be inferred from the data.\n",
"\n",
"Methods:\n",
"\n",
"* **`animate`**: Accepts a dictionary of new 'data', 'config' and 'style' values which is used to update the chart.\n",
"* **`stream`**: Streams new data to the plot.\n",
"* **`patch`**: Patches one or more rows in the data.\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `Vizzu` renders a dataset (defined either as a dictionary of columns or a DataFrame) given a `config` defining how to plot the data:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"data = {\n",
" 'Name': ['Alice', 'Bob', 'Ted', 'Patrick', 'Jason', 'Teresa', 'John'],\n",
" 'Weight': 50+np.random.randint(0, 10, 7)*10\n",
"}\n",
"\n",
"vizzu = pn.pane.Vizzu(\n",
" data, config={'geometry': 'rectangle', 'x': 'Name', 'y': 'Weight', 'title': 'Weight by person'},\n",
" duration=400, height=400, sizing_mode='stretch_width'\n",
")\n",
"\n",
"vizzu"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One of the major selling points behind Vizzu is the dynamic animations when either the data or the `config` is updated, e.g. if we change the 'geometry' we can see the animation smoothly transition between the two states."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"vizzu.animate({'geometry': 'circle'})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Note that the Vizzu pane will keep track of any changes you make as part of the `.animate()` call ensuring that the plot can be re-created easily:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(vizzu.config)\n",
"\n",
"vizzu"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Column Types\n",
"\n",
"`Vizzu` supports two column types:\n",
"\n",
"- `'dimension'`: Usually used for non-numeric data and/or the independent dimension of a chart (e.g. the x-axis)\n",
"- `'measure'`: Numeric values usually used for dependent variables of a chart (e.g. the y-axis values)\n",
"\n",
"The `Vizzu` pane automatically infers the types based on the dtypes of the data but in certain cases it may be necessary to exlicitly override the type of a column using `column_types` parameter. One common example is when plotting integers on the x-axis, which would ordinarily be treated as a 'measure' but should be treated as the independent dimension in the case of a line or bar chart.\n",
"\n",
"The example below demonstrates this case, here we want to treat the 'index' as an independent variable and override the default inferred type with `column_types={'index': 'dimension'}`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df = pd.DataFrame(np.random.randn(50), columns=list('Y')).cumsum()\n",
"\n",
"pn.pane.Vizzu(\n",
" df, column_types={'index': 'dimension'}, config={'x': 'index', 'y': 'Y', 'geometry': 'line'},\n",
" height=300, sizing_mode='stretch_width'\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Presets\n",
"\n",
"Vizzu provides a variety of [preset chart types](https://lib.vizzuhq.com/latest/examples/presets/). In `ipyvizzu` these are expressed by calling [helper methods](https://ipyvizzu.vizzuhq.com/latest/tutorial/chart_presets/) on the `Config` object. The `Vizzu` pane instead allows you to provide `'preset'` as a key of the `config`. In the example below we dynamically create a `config` that switches the `preset` based on a `RadioButtonGroup`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"windturbines = pd.read_csv('https://datasets.holoviz.org/windturbines/v1/windturbines.csv')\n",
"\n",
"agg = windturbines.groupby(['p_year', 't_manu'])[['p_cap']].sum().sort_index(level=0).reset_index()\n",
"\n",
"chart_type = pn.widgets.RadioButtonGroup(options={'Stream': 'stream', 'Bar': 'stackedColumn'}, align='center')\n",
"\n",
"preset_chart = pn.pane.Vizzu(\n",
" agg,\n",
" config=pn.bind(lambda preset: {'preset': preset, 'x': 'p_year', 'y': 'p_cap', 'stackedBy': 't_manu'}, chart_type),\n",
" column_types={'p_year': 'dimension'},\n",
" height=500,\n",
" sizing_mode='stretch_width',\n",
" style={\n",
" 'plot': {\n",
" \"xAxis\": {\n",
" \"label\": {\n",
" \"angle\": \"-45deg\"\n",
" }\n",
" }\n",
" }\n",
" }\n",
")\n",
"\n",
"\n",
"pn.Column(chart_type, preset_chart).embed()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Controls\n",
"\n",
"The `Vizzu` pane exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(vizzu.controls(jslink=True), vizzu)"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
34 changes: 30 additions & 4 deletions panel/_templates/autoload_panel_js.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ calls it with the rendered model.
:param js_modules: URLs of JS modules making up Bokeh library
:type js_modules: list

:param js_exports: URLs of JS modules to be exported
:type js_exports: dict

:param css_urls: CSS urls to inject
:type css_urls: list

Expand Down Expand Up @@ -44,17 +47,18 @@ calls it with the rendered model.
console.debug("Bokeh: all callbacks have finished");
}

function load_libs(css_urls, js_urls, js_modules, callback) {
function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {
if (css_urls == null) css_urls = [];
if (js_urls == null) js_urls = [];
if (js_modules == null) js_modules = [];
if (js_exports == null) js_exports = {};

root._bokeh_onload_callbacks.push(callback);
if (root._bokeh_is_loading > 0) {
console.debug("Bokeh: BokehJS is being loaded, scheduling callback at", now());
return null;
}
if (js_urls.length === 0 && js_modules.length === 0) {
if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {
run_callbacks();
return null;
}
Expand All @@ -67,6 +71,7 @@ calls it with the rendered model.
run_callbacks()
}
}
window._bokeh_on_load = on_load

function on_error() {
console.error("failed to load " + url);
Expand Down Expand Up @@ -97,7 +102,7 @@ calls it with the rendered model.
{% endfor %}
root._bokeh_is_loading = css_urls.length + {{ requirements|length }};
} else {
root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length;
root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;
}
{%- for lib, urls in skip_imports.items() %}
if (((window['{{ lib }}'] !== undefined) && (!(window['{{ lib }}'] instanceof HTMLElement))) || window.requirejs) {
Expand Down Expand Up @@ -140,6 +145,26 @@ calls it with the rendered model.
console.debug("Bokeh: injecting script tag for BokehJS library: ", url);
document.head.appendChild(element);
}
for (const name in js_exports) {
var url = js_exports[name];
if (skip.indexOf(url) >= 0) {
if (!window.requirejs) {
on_load();
}
continue;
}
var element = document.createElement('script');
element.onerror = on_error;
element.async = false;
element.type = "module";
console.debug("Bokeh: injecting script tag for BokehJS library: ", url);
element.textContent = `
import ${name} from "${url}"
window.${name} = ${name}
window._bokeh_on_load()
`
document.head.appendChild(element);
}
if (!js_urls.length && !js_modules.length) {
on_load()
}
Expand All @@ -153,6 +178,7 @@ calls it with the rendered model.

var js_urls = {{ bundle.js_urls|json }};
var js_modules = {{ bundle.js_modules|json }};
var js_exports = {{ bundle.js_module_exports|json }};
var css_urls = {{ bundle.css_urls|json }};
var inline_js = [
{%- for css in bundle.css_raw %}
Expand Down Expand Up @@ -190,7 +216,7 @@ calls it with the rendered model.
console.debug("Bokeh: BokehJS loaded, going straight to plotting");
run_inline_js();
} else {
load_libs(css_urls, js_urls, js_modules, function() {
load_libs(css_urls, js_urls, js_modules, js_exports, function() {
console.debug("Bokeh: BokehJS plotting callback run at", now());
run_inline_js();
});
Expand Down
7 changes: 7 additions & 0 deletions panel/_templates/js_resources.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@
<script type="module" src="{{ file }}"></script>
{% endfor %}

{%- for name, file in js_module_exports.items() %}
<script type="module">
import {{ name }} from "{{ file }}";
window.{{ name }} = {{ name }}
</script>
{%- endfor %}

{%- for js in js_raw %}
<script type="text/javascript">
{{ js }}
Expand Down
3 changes: 2 additions & 1 deletion panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,8 @@ class panel_extension(_pyviz_extension):
'terminal': 'panel.models.terminal',
'tabulator': 'panel.models.tabulator',
'texteditor': 'panel.models.quill',
'jsoneditor': 'panel.models.jsoneditor'
'jsoneditor': 'panel.models.jsoneditor',
'vizzu': 'panel.models.vizzu'
}

# Check whether these are loaded before rendering (if any item
Expand Down
24 changes: 18 additions & 6 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
CSS_RESOURCES as BkCSS_RESOURCES, Bundle as BkBundle, _bundle_extensions,
_use_mathjax, bundle_models, extension_dirs,
)
from bokeh.model import Model
from bokeh.models import ImportedStyleSheet
from bokeh.resources import Resources as BkResources
from bokeh.settings import settings as _settings
Expand Down Expand Up @@ -356,11 +357,11 @@ def bundle_resources(roots, resources, notebook=False):
js_raw.append(ext)

hashes = js_resources.hashes if js_resources else {}

return Bundle(
js_files=js_files, js_raw=js_raw, css_files=css_files,
css_raw=css_raw, hashes=hashes, notebook=notebook,
js_modules=resources.js_modules
css_files=css_files, css_raw=css_raw, hashes=hashes,
js_files=js_files, js_raw=js_raw,
js_module_exports=resources.js_module_exports,
js_modules=resources.js_modules, notebook=notebook,
)


Expand Down Expand Up @@ -489,7 +490,7 @@ def js_modules(self):

if config.design:
design_name = config.design.__name__.lower()
for resource in config.design._resources.get('js_modules').values():
for resource in config.design._resources.get('js_modules', {}).values():
if not isurl(resource):
resource = f'{CDN_DIST}bundled/{design_name}/{resource}'
if resource not in modules:
Expand All @@ -506,6 +507,14 @@ def js_modules(self):

return self.adjust_paths(modules)

@property
def js_module_exports(self):
modules = {}
for model in Model.model_class_reverse_map.values():
if hasattr(model, '__javascript_module_exports__'):
modules.update(dict(zip(model.__javascript_module_exports__, model.__javascript_modules__)))
return modules

@property
def css_files(self):
from ..config import config
Expand All @@ -526,14 +535,16 @@ def css_files(self):
def render_js(self):
return JS_RESOURCES.render(
js_raw=self.js_raw, js_files=self.js_files,
js_modules=self.js_modules, hashes=self.hashes
js_modules=self.js_modules, hashes=self.hashes,
js_module_exports=self.js_module_exports
)


class Bundle(BkBundle):

def __init__(self, notebook=False, **kwargs):
self.js_modules = kwargs.pop("js_modules", [])
self.js_module_exports = kwargs.pop("js_module_exports", {})
self.notebook = notebook
super().__init__(**kwargs)

Expand All @@ -559,5 +570,6 @@ def _render_js(self):
js_raw=self.js_raw,
js_files=self.js_files,
js_modules=self.js_modules,
js_module_exports=self.js_module_exports,
hashes=self.hashes
)
1 change: 1 addition & 0 deletions panel/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,5 @@ export {TrendIndicator} from "./trend"
export {VegaPlot} from "./vega"
export {Video} from "./video"
export {VideoStream} from "./videostream"
export {VizzuChart} from "./vizzu"
export * from "./vtk"
Loading