From 79c3808b5caeb911467566bb1a59c524a5b6bac9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Martin=20Tur=C3=B3ci?=
<64769322+mturoci@users.noreply.github.com>
Date: Wed, 6 Sep 2023 09:59:06 +0200
Subject: [PATCH] feat: Plot and card animations. (#2126)
---
py/examples/tour.py | 1 +
py/h2o_lightwave/h2o_lightwave/types.py | 30 +++++++++++
py/h2o_lightwave/h2o_lightwave/ui.py | 9 ++++
py/h2o_wave/h2o_wave/types.py | 30 +++++++++++
py/h2o_wave/h2o_wave/ui.py | 9 ++++
r/R/ui.R | 16 +++++-
.../resources/templates/wave-components.xml | 9 ++--
.../vscode-extension/component-snippets.json | 6 +--
ui/src/footer.tsx | 2 +-
ui/src/header.tsx | 2 +-
ui/src/index.scss | 23 ++++++++-
ui/src/layout.tsx | 5 +-
ui/src/meta.tsx | 9 ++--
ui/src/nav.tsx | 2 +-
ui/src/plot.tsx | 47 +++++++++---------
website/docs/assets/plot-animation.gif | Bin 0 -> 23957 bytes
website/docs/pages.md | 38 ++++++++++++++
website/docs/plotting.md | 32 ++++++++++++
18 files changed, 228 insertions(+), 42 deletions(-)
create mode 100644 website/docs/assets/plot-animation.gif
diff --git a/py/examples/tour.py b/py/examples/tour.py
index ef4f99168f5..ff683c13ec0 100644
--- a/py/examples/tour.py
+++ b/py/examples/tour.py
@@ -233,6 +233,7 @@ def get_wave_completions(line, character, file_content):
q.page['meta'] = ui.meta_card(
box='',
title=app_title,
+ animate=True,
scripts=[ui.script(q.app.tour_assets + '/loader.min.js')],
script=ui.inline_script(content=template, requires=['require'], targets=['monaco-editor']),
layouts=[
diff --git a/py/h2o_lightwave/h2o_lightwave/types.py b/py/h2o_lightwave/h2o_lightwave/types.py
index b98d6be9b76..aea7fa964b1 100644
--- a/py/h2o_lightwave/h2o_lightwave/types.py
+++ b/py/h2o_lightwave/h2o_lightwave/types.py
@@ -5626,6 +5626,7 @@ def __init__(
visible: Optional[bool] = None,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
):
_guard_scalar('Visualization.plot', plot, (Plot,), False, False, False)
_guard_scalar('Visualization.width', width, (str,), False, True, False)
@@ -5634,6 +5635,7 @@ def __init__(
_guard_scalar('Visualization.visible', visible, (bool,), False, True, False)
_guard_vector('Visualization.events', events, (str,), False, True, False)
_guard_vector('Visualization.interactions', interactions, (str,), False, True, False)
+ _guard_scalar('Visualization.animate', animate, (bool,), False, True, False)
self.plot = plot
"""The plot to be rendered in this visualization."""
self.data = data
@@ -5650,6 +5652,8 @@ def __init__(
"""The events to capture on this visualization. One of 'select_marks'."""
self.interactions = interactions
"""The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the chart animations. Defaults to False."""
def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
@@ -5660,6 +5664,7 @@ def dump(self) -> Dict:
_guard_scalar('Visualization.visible', self.visible, (bool,), False, True, False)
_guard_vector('Visualization.events', self.events, (str,), False, True, False)
_guard_vector('Visualization.interactions', self.interactions, (str,), False, True, False)
+ _guard_scalar('Visualization.animate', self.animate, (bool,), False, True, False)
return _dump(
plot=self.plot.dump(),
data=self.data,
@@ -5669,6 +5674,7 @@ def dump(self) -> Dict:
visible=self.visible,
events=self.events,
interactions=self.interactions,
+ animate=self.animate,
)
@staticmethod
@@ -5689,6 +5695,8 @@ def load(__d: Dict) -> 'Visualization':
_guard_vector('Visualization.events', __d_events, (str,), False, True, False)
__d_interactions: Any = __d.get('interactions')
_guard_vector('Visualization.interactions', __d_interactions, (str,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('Visualization.animate', __d_animate, (bool,), False, True, False)
plot: Plot = Plot.load(__d_plot)
data: PackedRecord = __d_data
width: Optional[str] = __d_width
@@ -5697,6 +5705,7 @@ def load(__d: Dict) -> 'Visualization':
visible: Optional[bool] = __d_visible
events: Optional[List[str]] = __d_events
interactions: Optional[List[str]] = __d_interactions
+ animate: Optional[bool] = __d_animate
return Visualization(
plot,
data,
@@ -5706,6 +5715,7 @@ def load(__d: Dict) -> 'Visualization':
visible,
events,
interactions,
+ animate,
)
@@ -10620,6 +10630,7 @@ def __init__(
script: Optional[InlineScript] = None,
stylesheet: Optional[InlineStylesheet] = None,
stylesheets: Optional[List[Stylesheet]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
):
_guard_scalar('MetaCard.box', box, (str,), False, False, False)
@@ -10639,6 +10650,7 @@ def __init__(
_guard_scalar('MetaCard.script', script, (InlineScript,), False, True, False)
_guard_scalar('MetaCard.stylesheet', stylesheet, (InlineStylesheet,), False, True, False)
_guard_vector('MetaCard.stylesheets', stylesheets, (Stylesheet,), False, True, False)
+ _guard_scalar('MetaCard.animate', animate, (bool,), False, True, False)
_guard_vector('MetaCard.commands', commands, (Command,), False, True, False)
self.box = box
"""A string indicating how to place this component on the page."""
@@ -10674,6 +10686,8 @@ def __init__(
"""CSS stylesheet to be applied to this page."""
self.stylesheets = stylesheets
"""External CSS files to load into the page."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the card animations. Defaults to False."""
self.commands = commands
"""Contextual menu commands for this component."""
@@ -10696,6 +10710,7 @@ def dump(self) -> Dict:
_guard_scalar('MetaCard.script', self.script, (InlineScript,), False, True, False)
_guard_scalar('MetaCard.stylesheet', self.stylesheet, (InlineStylesheet,), False, True, False)
_guard_vector('MetaCard.stylesheets', self.stylesheets, (Stylesheet,), False, True, False)
+ _guard_scalar('MetaCard.animate', self.animate, (bool,), False, True, False)
_guard_vector('MetaCard.commands', self.commands, (Command,), False, True, False)
return _dump(
view='meta',
@@ -10716,6 +10731,7 @@ def dump(self) -> Dict:
script=None if self.script is None else self.script.dump(),
stylesheet=None if self.stylesheet is None else self.stylesheet.dump(),
stylesheets=None if self.stylesheets is None else [__e.dump() for __e in self.stylesheets],
+ animate=self.animate,
commands=None if self.commands is None else [__e.dump() for __e in self.commands],
)
@@ -10756,6 +10772,8 @@ def load(__d: Dict) -> 'MetaCard':
_guard_scalar('MetaCard.stylesheet', __d_stylesheet, (dict,), False, True, False)
__d_stylesheets: Any = __d.get('stylesheets')
_guard_vector('MetaCard.stylesheets', __d_stylesheets, (dict,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('MetaCard.animate', __d_animate, (bool,), False, True, False)
__d_commands: Any = __d.get('commands')
_guard_vector('MetaCard.commands', __d_commands, (dict,), False, True, False)
box: str = __d_box
@@ -10775,6 +10793,7 @@ def load(__d: Dict) -> 'MetaCard':
script: Optional[InlineScript] = None if __d_script is None else InlineScript.load(__d_script)
stylesheet: Optional[InlineStylesheet] = None if __d_stylesheet is None else InlineStylesheet.load(__d_stylesheet)
stylesheets: Optional[List[Stylesheet]] = None if __d_stylesheets is None else [Stylesheet.load(__e) for __e in __d_stylesheets]
+ animate: Optional[bool] = __d_animate
commands: Optional[List[Command]] = None if __d_commands is None else [Command.load(__e) for __e in __d_commands]
return MetaCard(
box,
@@ -10794,6 +10813,7 @@ def load(__d: Dict) -> 'MetaCard':
script,
stylesheet,
stylesheets,
+ animate,
commands,
)
@@ -11017,6 +11037,7 @@ def __init__(
plot: Plot,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
):
_guard_scalar('PlotCard.box', box, (str,), False, False, False)
@@ -11024,6 +11045,7 @@ def __init__(
_guard_scalar('PlotCard.plot', plot, (Plot,), False, False, False)
_guard_vector('PlotCard.events', events, (str,), False, True, False)
_guard_vector('PlotCard.interactions', interactions, (str,), False, True, False)
+ _guard_scalar('PlotCard.animate', animate, (bool,), False, True, False)
_guard_vector('PlotCard.commands', commands, (Command,), False, True, False)
self.box = box
"""A string indicating how to place this component on the page."""
@@ -11037,6 +11059,8 @@ def __init__(
"""The events to capture on this card. One of 'select_marks'."""
self.interactions = interactions
"""The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the chart animations. Defaults to False."""
self.commands = commands
"""Contextual menu commands for this component."""
@@ -11047,6 +11071,7 @@ def dump(self) -> Dict:
_guard_scalar('PlotCard.plot', self.plot, (Plot,), False, False, False)
_guard_vector('PlotCard.events', self.events, (str,), False, True, False)
_guard_vector('PlotCard.interactions', self.interactions, (str,), False, True, False)
+ _guard_scalar('PlotCard.animate', self.animate, (bool,), False, True, False)
_guard_vector('PlotCard.commands', self.commands, (Command,), False, True, False)
return _dump(
view='plot',
@@ -11056,6 +11081,7 @@ def dump(self) -> Dict:
plot=self.plot.dump(),
events=self.events,
interactions=self.interactions,
+ animate=self.animate,
commands=None if self.commands is None else [__e.dump() for __e in self.commands],
)
@@ -11073,6 +11099,8 @@ def load(__d: Dict) -> 'PlotCard':
_guard_vector('PlotCard.events', __d_events, (str,), False, True, False)
__d_interactions: Any = __d.get('interactions')
_guard_vector('PlotCard.interactions', __d_interactions, (str,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('PlotCard.animate', __d_animate, (bool,), False, True, False)
__d_commands: Any = __d.get('commands')
_guard_vector('PlotCard.commands', __d_commands, (dict,), False, True, False)
box: str = __d_box
@@ -11081,6 +11109,7 @@ def load(__d: Dict) -> 'PlotCard':
plot: Plot = Plot.load(__d_plot)
events: Optional[List[str]] = __d_events
interactions: Optional[List[str]] = __d_interactions
+ animate: Optional[bool] = __d_animate
commands: Optional[List[Command]] = None if __d_commands is None else [Command.load(__e) for __e in __d_commands]
return PlotCard(
box,
@@ -11089,6 +11118,7 @@ def load(__d: Dict) -> 'PlotCard':
plot,
events,
interactions,
+ animate,
commands,
)
diff --git a/py/h2o_lightwave/h2o_lightwave/ui.py b/py/h2o_lightwave/h2o_lightwave/ui.py
index f0457c546ce..ed0db0823d8 100644
--- a/py/h2o_lightwave/h2o_lightwave/ui.py
+++ b/py/h2o_lightwave/h2o_lightwave/ui.py
@@ -2087,6 +2087,7 @@ def visualization(
visible: Optional[bool] = None,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
) -> Component:
"""Create a visualization for display inside a form.
@@ -2099,6 +2100,7 @@ def visualization(
visible: True if the component should be visible. Defaults to True.
events: The events to capture on this visualization. One of 'select_marks'.
interactions: The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+ animate: EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
Returns:
A `h2o_wave.types.Visualization` instance.
"""
@@ -2111,6 +2113,7 @@ def visualization(
visible,
events,
interactions,
+ animate,
))
@@ -3770,6 +3773,7 @@ def meta_card(
script: Optional[InlineScript] = None,
stylesheet: Optional[InlineStylesheet] = None,
stylesheets: Optional[List[Stylesheet]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
) -> MetaCard:
"""Represents page-global state.
@@ -3795,6 +3799,7 @@ def meta_card(
script: Javascript code to execute on this page.
stylesheet: CSS stylesheet to be applied to this page.
stylesheets: External CSS files to load into the page.
+ animate: EXPERIMENTAL: True to turn on the card animations. Defaults to False.
commands: Contextual menu commands for this component.
Returns:
A `h2o_wave.types.MetaCard` instance.
@@ -3817,6 +3822,7 @@ def meta_card(
script,
stylesheet,
stylesheets,
+ animate,
commands,
)
@@ -3903,6 +3909,7 @@ def plot_card(
plot: Plot,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
) -> PlotCard:
"""Create a card displaying a plot.
@@ -3914,6 +3921,7 @@ def plot_card(
plot: The plot to be displayed in this card.
events: The events to capture on this card. One of 'select_marks'.
interactions: The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+ animate: EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
commands: Contextual menu commands for this component.
Returns:
A `h2o_wave.types.PlotCard` instance.
@@ -3925,6 +3933,7 @@ def plot_card(
plot,
events,
interactions,
+ animate,
commands,
)
diff --git a/py/h2o_wave/h2o_wave/types.py b/py/h2o_wave/h2o_wave/types.py
index b98d6be9b76..aea7fa964b1 100644
--- a/py/h2o_wave/h2o_wave/types.py
+++ b/py/h2o_wave/h2o_wave/types.py
@@ -5626,6 +5626,7 @@ def __init__(
visible: Optional[bool] = None,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
):
_guard_scalar('Visualization.plot', plot, (Plot,), False, False, False)
_guard_scalar('Visualization.width', width, (str,), False, True, False)
@@ -5634,6 +5635,7 @@ def __init__(
_guard_scalar('Visualization.visible', visible, (bool,), False, True, False)
_guard_vector('Visualization.events', events, (str,), False, True, False)
_guard_vector('Visualization.interactions', interactions, (str,), False, True, False)
+ _guard_scalar('Visualization.animate', animate, (bool,), False, True, False)
self.plot = plot
"""The plot to be rendered in this visualization."""
self.data = data
@@ -5650,6 +5652,8 @@ def __init__(
"""The events to capture on this visualization. One of 'select_marks'."""
self.interactions = interactions
"""The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the chart animations. Defaults to False."""
def dump(self) -> Dict:
"""Returns the contents of this object as a dict."""
@@ -5660,6 +5664,7 @@ def dump(self) -> Dict:
_guard_scalar('Visualization.visible', self.visible, (bool,), False, True, False)
_guard_vector('Visualization.events', self.events, (str,), False, True, False)
_guard_vector('Visualization.interactions', self.interactions, (str,), False, True, False)
+ _guard_scalar('Visualization.animate', self.animate, (bool,), False, True, False)
return _dump(
plot=self.plot.dump(),
data=self.data,
@@ -5669,6 +5674,7 @@ def dump(self) -> Dict:
visible=self.visible,
events=self.events,
interactions=self.interactions,
+ animate=self.animate,
)
@staticmethod
@@ -5689,6 +5695,8 @@ def load(__d: Dict) -> 'Visualization':
_guard_vector('Visualization.events', __d_events, (str,), False, True, False)
__d_interactions: Any = __d.get('interactions')
_guard_vector('Visualization.interactions', __d_interactions, (str,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('Visualization.animate', __d_animate, (bool,), False, True, False)
plot: Plot = Plot.load(__d_plot)
data: PackedRecord = __d_data
width: Optional[str] = __d_width
@@ -5697,6 +5705,7 @@ def load(__d: Dict) -> 'Visualization':
visible: Optional[bool] = __d_visible
events: Optional[List[str]] = __d_events
interactions: Optional[List[str]] = __d_interactions
+ animate: Optional[bool] = __d_animate
return Visualization(
plot,
data,
@@ -5706,6 +5715,7 @@ def load(__d: Dict) -> 'Visualization':
visible,
events,
interactions,
+ animate,
)
@@ -10620,6 +10630,7 @@ def __init__(
script: Optional[InlineScript] = None,
stylesheet: Optional[InlineStylesheet] = None,
stylesheets: Optional[List[Stylesheet]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
):
_guard_scalar('MetaCard.box', box, (str,), False, False, False)
@@ -10639,6 +10650,7 @@ def __init__(
_guard_scalar('MetaCard.script', script, (InlineScript,), False, True, False)
_guard_scalar('MetaCard.stylesheet', stylesheet, (InlineStylesheet,), False, True, False)
_guard_vector('MetaCard.stylesheets', stylesheets, (Stylesheet,), False, True, False)
+ _guard_scalar('MetaCard.animate', animate, (bool,), False, True, False)
_guard_vector('MetaCard.commands', commands, (Command,), False, True, False)
self.box = box
"""A string indicating how to place this component on the page."""
@@ -10674,6 +10686,8 @@ def __init__(
"""CSS stylesheet to be applied to this page."""
self.stylesheets = stylesheets
"""External CSS files to load into the page."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the card animations. Defaults to False."""
self.commands = commands
"""Contextual menu commands for this component."""
@@ -10696,6 +10710,7 @@ def dump(self) -> Dict:
_guard_scalar('MetaCard.script', self.script, (InlineScript,), False, True, False)
_guard_scalar('MetaCard.stylesheet', self.stylesheet, (InlineStylesheet,), False, True, False)
_guard_vector('MetaCard.stylesheets', self.stylesheets, (Stylesheet,), False, True, False)
+ _guard_scalar('MetaCard.animate', self.animate, (bool,), False, True, False)
_guard_vector('MetaCard.commands', self.commands, (Command,), False, True, False)
return _dump(
view='meta',
@@ -10716,6 +10731,7 @@ def dump(self) -> Dict:
script=None if self.script is None else self.script.dump(),
stylesheet=None if self.stylesheet is None else self.stylesheet.dump(),
stylesheets=None if self.stylesheets is None else [__e.dump() for __e in self.stylesheets],
+ animate=self.animate,
commands=None if self.commands is None else [__e.dump() for __e in self.commands],
)
@@ -10756,6 +10772,8 @@ def load(__d: Dict) -> 'MetaCard':
_guard_scalar('MetaCard.stylesheet', __d_stylesheet, (dict,), False, True, False)
__d_stylesheets: Any = __d.get('stylesheets')
_guard_vector('MetaCard.stylesheets', __d_stylesheets, (dict,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('MetaCard.animate', __d_animate, (bool,), False, True, False)
__d_commands: Any = __d.get('commands')
_guard_vector('MetaCard.commands', __d_commands, (dict,), False, True, False)
box: str = __d_box
@@ -10775,6 +10793,7 @@ def load(__d: Dict) -> 'MetaCard':
script: Optional[InlineScript] = None if __d_script is None else InlineScript.load(__d_script)
stylesheet: Optional[InlineStylesheet] = None if __d_stylesheet is None else InlineStylesheet.load(__d_stylesheet)
stylesheets: Optional[List[Stylesheet]] = None if __d_stylesheets is None else [Stylesheet.load(__e) for __e in __d_stylesheets]
+ animate: Optional[bool] = __d_animate
commands: Optional[List[Command]] = None if __d_commands is None else [Command.load(__e) for __e in __d_commands]
return MetaCard(
box,
@@ -10794,6 +10813,7 @@ def load(__d: Dict) -> 'MetaCard':
script,
stylesheet,
stylesheets,
+ animate,
commands,
)
@@ -11017,6 +11037,7 @@ def __init__(
plot: Plot,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
):
_guard_scalar('PlotCard.box', box, (str,), False, False, False)
@@ -11024,6 +11045,7 @@ def __init__(
_guard_scalar('PlotCard.plot', plot, (Plot,), False, False, False)
_guard_vector('PlotCard.events', events, (str,), False, True, False)
_guard_vector('PlotCard.interactions', interactions, (str,), False, True, False)
+ _guard_scalar('PlotCard.animate', animate, (bool,), False, True, False)
_guard_vector('PlotCard.commands', commands, (Command,), False, True, False)
self.box = box
"""A string indicating how to place this component on the page."""
@@ -11037,6 +11059,8 @@ def __init__(
"""The events to capture on this card. One of 'select_marks'."""
self.interactions = interactions
"""The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event."""
+ self.animate = animate
+ """EXPERIMENTAL: True to turn on the chart animations. Defaults to False."""
self.commands = commands
"""Contextual menu commands for this component."""
@@ -11047,6 +11071,7 @@ def dump(self) -> Dict:
_guard_scalar('PlotCard.plot', self.plot, (Plot,), False, False, False)
_guard_vector('PlotCard.events', self.events, (str,), False, True, False)
_guard_vector('PlotCard.interactions', self.interactions, (str,), False, True, False)
+ _guard_scalar('PlotCard.animate', self.animate, (bool,), False, True, False)
_guard_vector('PlotCard.commands', self.commands, (Command,), False, True, False)
return _dump(
view='plot',
@@ -11056,6 +11081,7 @@ def dump(self) -> Dict:
plot=self.plot.dump(),
events=self.events,
interactions=self.interactions,
+ animate=self.animate,
commands=None if self.commands is None else [__e.dump() for __e in self.commands],
)
@@ -11073,6 +11099,8 @@ def load(__d: Dict) -> 'PlotCard':
_guard_vector('PlotCard.events', __d_events, (str,), False, True, False)
__d_interactions: Any = __d.get('interactions')
_guard_vector('PlotCard.interactions', __d_interactions, (str,), False, True, False)
+ __d_animate: Any = __d.get('animate')
+ _guard_scalar('PlotCard.animate', __d_animate, (bool,), False, True, False)
__d_commands: Any = __d.get('commands')
_guard_vector('PlotCard.commands', __d_commands, (dict,), False, True, False)
box: str = __d_box
@@ -11081,6 +11109,7 @@ def load(__d: Dict) -> 'PlotCard':
plot: Plot = Plot.load(__d_plot)
events: Optional[List[str]] = __d_events
interactions: Optional[List[str]] = __d_interactions
+ animate: Optional[bool] = __d_animate
commands: Optional[List[Command]] = None if __d_commands is None else [Command.load(__e) for __e in __d_commands]
return PlotCard(
box,
@@ -11089,6 +11118,7 @@ def load(__d: Dict) -> 'PlotCard':
plot,
events,
interactions,
+ animate,
commands,
)
diff --git a/py/h2o_wave/h2o_wave/ui.py b/py/h2o_wave/h2o_wave/ui.py
index f0457c546ce..ed0db0823d8 100644
--- a/py/h2o_wave/h2o_wave/ui.py
+++ b/py/h2o_wave/h2o_wave/ui.py
@@ -2087,6 +2087,7 @@ def visualization(
visible: Optional[bool] = None,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
) -> Component:
"""Create a visualization for display inside a form.
@@ -2099,6 +2100,7 @@ def visualization(
visible: True if the component should be visible. Defaults to True.
events: The events to capture on this visualization. One of 'select_marks'.
interactions: The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+ animate: EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
Returns:
A `h2o_wave.types.Visualization` instance.
"""
@@ -2111,6 +2113,7 @@ def visualization(
visible,
events,
interactions,
+ animate,
))
@@ -3770,6 +3773,7 @@ def meta_card(
script: Optional[InlineScript] = None,
stylesheet: Optional[InlineStylesheet] = None,
stylesheets: Optional[List[Stylesheet]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
) -> MetaCard:
"""Represents page-global state.
@@ -3795,6 +3799,7 @@ def meta_card(
script: Javascript code to execute on this page.
stylesheet: CSS stylesheet to be applied to this page.
stylesheets: External CSS files to load into the page.
+ animate: EXPERIMENTAL: True to turn on the card animations. Defaults to False.
commands: Contextual menu commands for this component.
Returns:
A `h2o_wave.types.MetaCard` instance.
@@ -3817,6 +3822,7 @@ def meta_card(
script,
stylesheet,
stylesheets,
+ animate,
commands,
)
@@ -3903,6 +3909,7 @@ def plot_card(
plot: Plot,
events: Optional[List[str]] = None,
interactions: Optional[List[str]] = None,
+ animate: Optional[bool] = None,
commands: Optional[List[Command]] = None,
) -> PlotCard:
"""Create a card displaying a plot.
@@ -3914,6 +3921,7 @@ def plot_card(
plot: The plot to be displayed in this card.
events: The events to capture on this card. One of 'select_marks'.
interactions: The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+ animate: EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
commands: Contextual menu commands for this component.
Returns:
A `h2o_wave.types.PlotCard` instance.
@@ -3925,6 +3933,7 @@ def plot_card(
plot,
events,
interactions,
+ animate,
commands,
)
diff --git a/r/R/ui.R b/r/R/ui.R
index 8dd67bb1a6a..7d81a35fb39 100644
--- a/r/R/ui.R
+++ b/r/R/ui.R
@@ -2453,6 +2453,7 @@ ui_plot <- function(
#' @param visible True if the component should be visible. Defaults to True.
#' @param events The events to capture on this visualization. One of 'select_marks'.
#' @param interactions The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+#' @param animate EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
#' @return A Visualization instance.
#' @export
ui_visualization <- function(
@@ -2463,7 +2464,8 @@ ui_visualization <- function(
name = NULL,
visible = NULL,
events = NULL,
- interactions = NULL) {
+ interactions = NULL,
+ animate = NULL) {
.guard_scalar("plot", "WavePlot", plot)
# TODO Validate data: Rec
.guard_scalar("width", "character", width)
@@ -2472,6 +2474,7 @@ ui_visualization <- function(
.guard_scalar("visible", "logical", visible)
.guard_vector("events", "character", events)
.guard_vector("interactions", "character", interactions)
+ .guard_scalar("animate", "logical", animate)
.o <- list(visualization=list(
plot=plot,
data=data,
@@ -2480,7 +2483,8 @@ ui_visualization <- function(
name=name,
visible=visible,
events=events,
- interactions=interactions))
+ interactions=interactions,
+ animate=animate))
class(.o) <- append(class(.o), c(.wave_obj, "WaveComponent"))
return(.o)
}
@@ -4388,6 +4392,7 @@ ui_stylesheet <- function(
#' @param script Javascript code to execute on this page.
#' @param stylesheet CSS stylesheet to be applied to this page.
#' @param stylesheets External CSS files to load into the page.
+#' @param animate EXPERIMENTAL: True to turn on the card animations. Defaults to False.
#' @param commands Contextual menu commands for this component.
#' @return A MetaCard instance.
#' @export
@@ -4409,6 +4414,7 @@ ui_meta_card <- function(
script = NULL,
stylesheet = NULL,
stylesheets = NULL,
+ animate = NULL,
commands = NULL) {
.guard_scalar("box", "character", box)
.guard_scalar("title", "character", title)
@@ -4427,6 +4433,7 @@ ui_meta_card <- function(
.guard_scalar("script", "WaveInlineScript", script)
.guard_scalar("stylesheet", "WaveInlineStylesheet", stylesheet)
.guard_vector("stylesheets", "WaveStylesheet", stylesheets)
+ .guard_scalar("animate", "logical", animate)
.guard_vector("commands", "WaveCommand", commands)
.o <- list(
box=box,
@@ -4446,6 +4453,7 @@ ui_meta_card <- function(
script=script,
stylesheet=stylesheet,
stylesheets=stylesheets,
+ animate=animate,
commands=commands,
view='meta')
class(.o) <- append(class(.o), c(.wave_obj, "WaveMetaCard"))
@@ -4550,6 +4558,7 @@ ui_pixel_art_card <- function(
#' @param plot The plot to be displayed in this card.
#' @param events The events to capture on this card. One of 'select_marks'.
#' @param interactions The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event.
+#' @param animate EXPERIMENTAL: True to turn on the chart animations. Defaults to False.
#' @param commands Contextual menu commands for this component.
#' @return A PlotCard instance.
#' @export
@@ -4560,6 +4569,7 @@ ui_plot_card <- function(
plot,
events = NULL,
interactions = NULL,
+ animate = NULL,
commands = NULL) {
.guard_scalar("box", "character", box)
.guard_scalar("title", "character", title)
@@ -4567,6 +4577,7 @@ ui_plot_card <- function(
.guard_scalar("plot", "WavePlot", plot)
.guard_vector("events", "character", events)
.guard_vector("interactions", "character", interactions)
+ .guard_scalar("animate", "logical", animate)
.guard_vector("commands", "WaveCommand", commands)
.o <- list(
box=box,
@@ -4575,6 +4586,7 @@ ui_plot_card <- function(
plot=plot,
events=events,
interactions=interactions,
+ animate=animate,
commands=commands,
view='plot')
class(.o) <- append(class(.o), c(.wave_obj, "WavePlotCard"))
diff --git a/tools/intellij-plugin/src/main/resources/templates/wave-components.xml b/tools/intellij-plugin/src/main/resources/templates/wave-components.xml
index d13ff24ec86..b24c294face 100644
--- a/tools/intellij-plugin/src/main/resources/templates/wave-components.xml
+++ b/tools/intellij-plugin/src/main/resources/templates/wave-components.xml
@@ -1782,7 +1782,7 @@
-
+
@@ -1796,6 +1796,7 @@
+
@@ -2013,24 +2014,26 @@
-
+
+
-
+
+
diff --git a/tools/vscode-extension/component-snippets.json b/tools/vscode-extension/component-snippets.json
index 2354f868c84..3170dd01823 100644
--- a/tools/vscode-extension/component-snippets.json
+++ b/tools/vscode-extension/component-snippets.json
@@ -1444,7 +1444,7 @@
"Wave Full MetaCard": {
"prefix": "w_full_meta_card",
"body": [
- "ui.meta_card(box='$1', title='$2', refresh=${3:None}, notification='$4', notification_bar=${5:None}, redirect='$6', icon='$7', dialog=${8:None}, side_panel=${9:None}, theme='$10', tracker=${11:None}, script=${12:None}, stylesheet=${13:None}, layouts=[\n\t\t$14\t\t\n], themes=[\n\t\t$15\t\t\n], scripts=[\n\t\t$16\t\t\n], stylesheets=[\n\t\t$17\t\t\n], commands=[\n\t\t$18\t\t\n])$0"
+ "ui.meta_card(box='$1', title='$2', refresh=${3:None}, notification='$4', notification_bar=${5:None}, redirect='$6', icon='$7', dialog=${8:None}, side_panel=${9:None}, theme='$10', tracker=${11:None}, script=${12:None}, stylesheet=${13:None}, animate=${14:False}, layouts=[\n\t\t$15\t\t\n], themes=[\n\t\t$16\t\t\n], scripts=[\n\t\t$17\t\t\n], stylesheets=[\n\t\t$18\t\t\n], commands=[\n\t\t$19\t\t\n])$0"
],
"description": "Create a full Wave MetaCard."
},
@@ -1528,14 +1528,14 @@
"Wave Full Visualization": {
"prefix": "w_full_visualization",
"body": [
- "ui.visualization(plot=$1, data=$2, width='${3:100%}', height='${4:300px}', name='$5', visible=${6:True}, events=[\n\t\t$7\t\t\n], interactions=[\n\t\t$8\t\t\n]),$0"
+ "ui.visualization(plot=$1, data=$2, width='${3:100%}', height='${4:300px}', name='$5', visible=${6:True}, animate=${7:False}, events=[\n\t\t$8\t\t\n], interactions=[\n\t\t$9\t\t\n]),$0"
],
"description": "Create a full Wave Visualization."
},
"Wave Full PlotCard": {
"prefix": "w_full_plot_card",
"body": [
- "ui.plot_card(box='$1', title='$2', data=$3, plot=$4, events=[\n\t\t$5\t\t\n], interactions=[\n\t\t$6\t\t\n], commands=[\n\t\t$7\t\t\n])$0"
+ "ui.plot_card(box='$1', title='$2', data=$3, plot=$4, animate=${5:False}, events=[\n\t\t$6\t\t\n], interactions=[\n\t\t$7\t\t\n], commands=[\n\t\t$8\t\t\n])$0"
],
"description": "Create a full Wave PlotCard."
},
diff --git a/ui/src/footer.tsx b/ui/src/footer.tsx
index f52207b4552..27bd6abc5ee 100644
--- a/ui/src/footer.tsx
+++ b/ui/src/footer.tsx
@@ -71,4 +71,4 @@ export const
return { render, changed }
})
-cards.register('footer', View, { effect: CardEffect.Transparent, marginless: true })
\ No newline at end of file
+cards.register('footer', View, { effect: CardEffect.Transparent, marginless: true, animate: false })
\ No newline at end of file
diff --git a/ui/src/header.tsx b/ui/src/header.tsx
index b340ff5412f..eec70ee9758 100644
--- a/ui/src/header.tsx
+++ b/ui/src/header.tsx
@@ -150,4 +150,4 @@ export const View = bond(({ name, state, changed }: Model {
- const { effect, marginless } = getCardStyle(c)
- return clas(css.slot, getEffectClass(effect), marginless ? css.marginless : '')
+ const { effect, marginless, animate = true } = getCardStyle(c)
+ return clas(css.slot, getEffectClass(effect), marginless ? css.marginless : '', animate ? 'wave-animate-card' : '')
},
toCardEffect = (color?: 'card' | 'transparent' | 'primary') => {
switch (color) {
diff --git a/ui/src/meta.tsx b/ui/src/meta.tsx
index 86c5aad9318..ea4f32fdb30 100644
--- a/ui/src/meta.tsx
+++ b/ui/src/meta.tsx
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { box, disconnect, Id, Model, on, S, U } from 'h2o-wave'
+import { B, box, disconnect, Id, Model, on, S, U } from 'h2o-wave'
import React from 'react'
import { NotificationBar, notificationBarB } from './notification_bar'
import { Dialog, dialogB } from './dialog'
@@ -162,6 +162,8 @@ interface State {
stylesheet?: InlineStylesheet
/** External CSS files to load into the page. */
stylesheets?: Stylesheet[]
+ /** EXPERIMENTAL: True to turn on the card animations. Defaults to False. */
+ animate?: B
}
const
@@ -197,7 +199,8 @@ export const
scripts,
script,
stylesheet,
- stylesheets
+ stylesheets,
+ animate
} = state
if (redirect) {
@@ -221,7 +224,7 @@ export const
// HACK: Since meta cards are processed within render, wait for React to finish the original render before proceeding.
setTimeout(() => notificationBarB(notification_bar ? { ...notification_bar } : null), 0)
-
+ if (animate) document.body.style.setProperty('--wave-animation-duration', '0.5s')
if (title) windowTitleB(title)
if (icon) windowIconB(icon)
if (typeof refresh === 'number' && refresh === 0) disconnect()
diff --git a/ui/src/nav.tsx b/ui/src/nav.tsx
index eb38d033194..6c40eff8157 100644
--- a/ui/src/nav.tsx
+++ b/ui/src/nav.tsx
@@ -183,4 +183,4 @@ export const
return { render, changed, update, valueB, dispose }
})
-cards.register('nav', View, { effect: CardEffect.Flat, marginless: true })
\ No newline at end of file
+cards.register('nav', View, { effect: CardEffect.Flat, marginless: true, animate: false })
\ No newline at end of file
diff --git a/ui/src/plot.tsx b/ui/src/plot.tsx
index 561501b3f40..0f358519821 100644
--- a/ui/src/plot.tsx
+++ b/ui/src/plot.tsx
@@ -966,7 +966,7 @@ const
return [geometries, annotations]
},
- makeChart = (el: HTMLElement, space: SpaceT, marks: Mark[], interactions: S[]): ChartCfg => {
+ makeChart = (el: HTMLElement, space: SpaceT, marks: Mark[], interactions: S[], animate = false): ChartCfg => {
// WARNING: makeCoord() must be called before other functions.
const
coordinate = makeCoord(space, marks), // WARNING: this call may transpose x/y in-place.
@@ -979,7 +979,7 @@ const
renderer: 'canvas',
theme: getPlotTheme(),
options: {
- animate: false,
+ animate,
coordinate,
scales,
axes,
@@ -1035,6 +1035,8 @@ export interface Visualization {
events?: S[]
/** The interactions to be allowed for this plot. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event. */
interactions?: S[]
+ /** EXPERIMENTAL: True to turn on the chart animations. Defaults to False. */
+ animate?: B
}
const tooltipContainer = document.createElement('div')
@@ -1052,13 +1054,10 @@ const PlotTooltip = ({ items, originalItems }: { items: TooltipItem[], originalI
{(item instanceof Date ? item.toISOString().split('T')[0] : item)}
- }
- )
- )}
+ }))
+ }
>
-
-
export const
XVisualization = ({ model }: { model: Visualization }) => {
const
@@ -1082,7 +1081,7 @@ export const
space = spaceTypeOf(raw_data, marks),
data = refactorData(raw_data, plot.marks),
{ Chart } = await import('@antv/g2'),
- chart = plot.marks ? new Chart(makeChart(el, space, plot.marks, model.interactions || [])) : null
+ chart = plot.marks ? new Chart(makeChart(el, space, plot.marks, model.interactions || [], model.animate)) : null
originalDataRef.current = unpack(model.data)
currentPlot.current = plot
if (chart) {
@@ -1173,23 +1172,23 @@ interface State {
events?: S[]
/** The interactions to be allowed for this card. One of 'drag_move' | 'scale_zoom' | 'brush'. Note: `brush` does not raise `select_marks` event. */
interactions?: S[]
+ /** EXPERIMENTAL: True to turn on the chart animations. Defaults to False. */
+ animate?: B
}
-export const
- View = bond(({ name, state, changed }: Model) => {
- const
- render = () => {
- const { title = 'Untitled', plot, data, events, interactions } = state
- return (
-
- )
- }
- return { render, changed }
- })
+export const View = bond(({ name, state, changed }: Model) => {
+ const render = () => {
+ const { title = 'Untitled', plot, data, events, interactions, animate } = state
+ return (
+
+ )
+ }
+ return { render, changed }
+})
cards.register('plot', View)
\ No newline at end of file
diff --git a/website/docs/assets/plot-animation.gif b/website/docs/assets/plot-animation.gif
new file mode 100644
index 0000000000000000000000000000000000000000..1f5c5d644b8be4dcab0dff84620c0d387188d280
GIT binary patch
literal 23957
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 1) lasting for `0.5s`. if you are not happy with these defaults, it's possible to override these values via [custom CSS](/docs/custom-css/).
+
+```css
+:root {
+ /* Custom CSS properties to configure duration and animation. */
+ --wave-animation-duration: 0.5s;
+ --wave-card-animation: wave-fade-in
+}
+
+/* Override this class to get full control over animations. */
+.wave-animate-card {
+ animation: var(--wave-card-animation);
+ animation-duration: var(--wave-animation-duration);
+}
+
+@keyframes wave-fade-in {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+```
diff --git a/website/docs/plotting.md b/website/docs/plotting.md
index b482965a106..ddf2bf288d0 100644
--- a/website/docs/plotting.md
+++ b/website/docs/plotting.md
@@ -223,6 +223,38 @@ q.page['example'] = ui.wide_plot_card(
)
```
+## Animations (experimental)
+
+Plots support basic animations when `animate=True`.
+
+:::warning
+An animation is considered fluid when it is able to reach `60fps` (frames per second). To achieve such fps, one needs to make sure the browser has enough resources to paint the animation at such rate. Loading too many data or doing too much browser work may result in janky animations so use at your own risk.
+:::
+
+
+
+```py {6}
+from h2o_wave import data
+
+q.page['example'] = ui.plot_card(
+ box='1 1 4 5',
+ title='Line',
+ animate=True,
+ data=data('year value', 8, rows=[
+ ('1991', 3),
+ ('1992', 4),
+ ('1993', 3.5),
+ ('1994', 5),
+ ('1995', 4.9),
+ ('1996', 6),
+ ('1997', 7),
+ ('1998', 9),
+ ('1999', 13),
+ ]),
+ plot=ui.plot([ui.mark(type='line', x_scale='time', x='=year', y='=value', y_min=0)])
+)
+```
+
## Point
- [Basic](/docs/examples/plot-point): Make a scatterplot.