From 2e1e9da344b3fa0286f8395269065ea5b35ec35a Mon Sep 17 00:00:00 2001 From: DipayanDasgupta Date: Sat, 4 Oct 2025 11:19:41 +0000 Subject: [PATCH 1/3] feat: Add tooltip to Altair agent portrayal (#2795) --- .../basic/boltzmann_wealth_model/app.py | 36 ++--- mesa/visualization/backends/altair_backend.py | 126 +++++++++--------- .../components/portrayal_components.py | 5 +- 3 files changed, 74 insertions(+), 93 deletions(-) diff --git a/mesa/examples/basic/boltzmann_wealth_model/app.py b/mesa/examples/basic/boltzmann_wealth_model/app.py index 464708536ca..c9328264bb8 100644 --- a/mesa/examples/basic/boltzmann_wealth_model/app.py +++ b/mesa/examples/basic/boltzmann_wealth_model/app.py @@ -14,8 +14,9 @@ def agent_portrayal(agent): return AgentPortrayalStyle( - color=agent.wealth - ) # we are using a colormap to translate wealth to color + color=agent.wealth, + tooltip={"Agent ID": agent.unique_id, "Wealth": agent.wealth}, + ) model_params = { @@ -37,23 +38,6 @@ def agent_portrayal(agent): } -def post_process(chart): - """Post-process the Altair chart to add a colorbar legend.""" - chart = chart.encode( - color=alt.Color( - "color:N", - scale=alt.Scale(scheme="viridis", domain=[0, 10]), - legend=alt.Legend( - title="Wealth", - orient="right", - type="gradient", - gradientLength=200, - ), - ), - ) - return chart - - model = BoltzmannWealth(50, 10, 10) # The SpaceRenderer is responsible for drawing the model's space and agents. @@ -63,11 +47,13 @@ def post_process(chart): renderer = SpaceRenderer(model, backend="altair") # Can customize the grid appearance. renderer.draw_structure(grid_color="black", grid_dash=[6, 2], grid_opacity=0.3) -renderer.draw_agents(agent_portrayal=agent_portrayal, cmap="viridis", vmin=0, vmax=10) - -# The post_process function is used to modify the Altair chart after it has been created. -# It can be used to add legends, colorbars, or other visual elements. -renderer.post_process = post_process +renderer.draw_agents( + agent_portrayal=agent_portrayal, + cmap="viridis", + vmin=0, + vmax=10, + legend_title="Wealth", +) # Creates a line plot component from the model's "Gini" datacollector. GiniPlot = make_plot_component("Gini") @@ -81,4 +67,4 @@ def post_process(chart): model_params=model_params, name="Boltzmann Wealth Model", ) -page # noqa +page # noqa \ No newline at end of file diff --git a/mesa/visualization/backends/altair_backend.py b/mesa/visualization/backends/altair_backend.py index e09bc02de64..ebf1e2bf925 100644 --- a/mesa/visualization/backends/altair_backend.py +++ b/mesa/visualization/backends/altair_backend.py @@ -1,4 +1,3 @@ -# noqa: D100 import warnings from collections.abc import Callable from dataclasses import fields @@ -75,6 +74,7 @@ def collect_agent_data( "stroke": [], # Stroke color "strokeWidth": [], "filled": [], + "tooltip": [], } # Import here to avoid circular import issues @@ -129,6 +129,7 @@ def collect_agent_data( linewidths=dict_data.pop( "linewidths", style_fields.get("linewidths") ), + tooltip=dict_data.pop("tooltip", None), ) if dict_data: ignored_keys = list(dict_data.keys()) @@ -184,6 +185,7 @@ def collect_agent_data( # FIXME: Make filled user-controllable filled_value = True arguments["filled"].append(filled_value) + arguments["tooltip"].append(aps.tooltip) final_data = {} for k, v in arguments.items(): @@ -199,80 +201,71 @@ def collect_agent_data( return final_data + + def draw_agents( self, arguments, chart_width: int = 450, chart_height: int = 350, **kwargs ): - """Draw agents using Altair backend. - - Args: - arguments: Dictionary containing agent data arrays. - chart_width: Width of the chart. - chart_height: Height of the chart. - **kwargs: Additional keyword arguments for customization. - Checkout respective `SpaceDrawer` class on details how to pass **kwargs. - - Returns: - alt.Chart: The Altair chart representing the agents, or None if no agents. - """ + """Draw agents using Altair backend.""" if arguments["loc"].size == 0: return None - # To get a continuous scale for color the domain should be between [0, 1] - # that's why changing the the domain of strokeWidth beforehand. - stroke_width = [data / 10 for data in arguments["strokeWidth"]] - - # Agent data preparation - df_data = { - "x": arguments["loc"][:, 0], - "y": arguments["loc"][:, 1], - "size": arguments["size"], - "shape": arguments["shape"], - "opacity": arguments["opacity"], - "strokeWidth": stroke_width, - "original_color": arguments["color"], - "is_filled": arguments["filled"], - "original_stroke": arguments["stroke"], - } - df = pd.DataFrame(df_data) - - # To ensure distinct shapes according to agent portrayal - unique_shape_names_in_data = df["shape"].unique().tolist() - - fill_colors = [] - stroke_colors = [] - for i in range(len(df)): - filled = df["is_filled"][i] - main_color = df["original_color"][i] - stroke_spec = ( - df["original_stroke"][i] - if isinstance(df["original_stroke"][i], str) - else None - ) - if filled: - fill_colors.append(main_color) - stroke_colors.append(stroke_spec) + # Prepare a list of dictionaries, which is a robust way to create a DataFrame + records = [] + for i in range(len(arguments["loc"])): + record = { + "x": arguments["loc"][i][0], + "y": arguments["loc"][i][1], + "size": arguments["size"][i], + "shape": arguments["shape"][i], + "opacity": arguments["opacity"][i], + "strokeWidth": arguments["strokeWidth"][i] / 10, # Scale for continuous domain + "original_color": arguments["color"][i], + } + # Add tooltip data if available + tooltip = arguments["tooltip"][i] + if tooltip: + record.update(tooltip) + + # Determine fill and stroke colors + if arguments["filled"][i]: + record["viz_fill_color"] = arguments["color"][i] + record["viz_stroke_color"] = arguments["stroke"][i] if isinstance(arguments["stroke"][i], str) else None else: - fill_colors.append(None) - stroke_colors.append(main_color) - df["viz_fill_color"] = fill_colors - df["viz_stroke_color"] = stroke_colors + record["viz_fill_color"] = None + record["viz_stroke_color"] = arguments["color"][i] + + records.append(record) + + df = pd.DataFrame(records) + + # Ensure all columns that should be numeric are, handling potential Nones + numeric_cols = ['x', 'y', 'size', 'opacity', 'strokeWidth', 'original_color'] + for col in numeric_cols: + if col in df.columns: + df[col] = pd.to_numeric(df[col], errors='coerce') + + + # Get tooltip keys from the first valid record + tooltip_list = ["x", "y"] + # This is the corrected line: + if any(t is not None for t in arguments["tooltip"]): + first_valid_tooltip = next((t for t in arguments["tooltip"] if t), None) + if first_valid_tooltip: + tooltip_list.extend(first_valid_tooltip.keys()) # Extract additional parameters from kwargs - # FIXME: Add more parameters to kwargs title = kwargs.pop("title", "") xlabel = kwargs.pop("xlabel", "") ylabel = kwargs.pop("ylabel", "") - - # Tooltip list for interactivity - # FIXME: Add more fields to tooltip (preferably from agent_portrayal) - tooltip_list = ["x", "y"] + legend_title = kwargs.pop("legend_title", "Color") # Handle custom colormapping cmap = kwargs.pop("cmap", "viridis") vmin = kwargs.pop("vmin", None) vmax = kwargs.pop("vmax", None) - color_is_numeric = np.issubdtype(df["original_color"].dtype, np.number) + color_is_numeric = pd.api.types.is_numeric_dtype(df["original_color"]) if color_is_numeric: color_min = vmin if vmin is not None else df["original_color"].min() color_max = vmax if vmax is not None else df["original_color"].max() @@ -280,6 +273,12 @@ def draw_agents( fill_encoding = alt.Fill( "original_color:Q", scale=alt.Scale(scheme=cmap, domain=[color_min, color_max]), + legend=alt.Legend( + title=legend_title, + orient="right", + type="gradient", + gradientLength=200, + ), ) else: fill_encoding = alt.Fill( @@ -290,6 +289,7 @@ def draw_agents( # Determine space dimensions xmin, xmax, ymin, ymax = self.space_drawer.get_viz_limits() + unique_shape_names_in_data = df["shape"].dropna().unique().tolist() chart = ( alt.Chart(df) @@ -316,16 +316,10 @@ def draw_agents( ), title="Shape", ), - opacity=alt.Opacity( - "opacity:Q", - title="Opacity", - scale=alt.Scale(domain=[0, 1], range=[0, 1]), - ), + opacity=alt.Opacity("opacity:Q", title="Opacity", scale=alt.Scale(domain=[0, 1], range=[0, 1])), fill=fill_encoding, stroke=alt.Stroke("viz_stroke_color:N", scale=None), - strokeWidth=alt.StrokeWidth( - "strokeWidth:Q", scale=alt.Scale(domain=[0, 1]) - ), + strokeWidth=alt.StrokeWidth("strokeWidth:Q", scale=alt.Scale(domain=[0, 1])), tooltip=tooltip_list, ) .properties(title=title, width=chart_width, height=chart_height) @@ -437,4 +431,4 @@ def draw_propertylayer( main_charts.append(current_chart) base = alt.layer(*main_charts).resolve_scale(color="independent") - return base + return base \ No newline at end of file diff --git a/mesa/visualization/components/portrayal_components.py b/mesa/visualization/components/portrayal_components.py index d15871a12d5..eb748e8177d 100644 --- a/mesa/visualization/components/portrayal_components.py +++ b/mesa/visualization/components/portrayal_components.py @@ -55,6 +55,7 @@ class AgentPortrayalStyle: alpha: float | None = 1.0 edgecolors: str | tuple | None = None linewidths: float | int | None = 1.0 + tooltip: dict | None = None def update(self, *updates_fields: tuple[str, Any]): """Updates attributes from variable (field_name, new_value) tuple arguments. @@ -91,7 +92,7 @@ class PropertyLayerStyle: (vmin, vmax), transparency (alpha) and colorbar visibility. Note: vmin and vmax are the lower and upper bounds for the colorbar and the data is - normalized between these values for color/colormap rendering. If they are not + normalized between these values for color/colorbar rendering. If they are not declared the values are automatically determined from the data range. Note: You can specify either a 'colormap' (for varying data) or a single @@ -117,4 +118,4 @@ def __post_init__(self): if self.color is not None and self.colormap is not None: raise ValueError("Specify either 'color' or 'colormap', not both.") if self.color is None and self.colormap is None: - raise ValueError("Specify one of 'color' or 'colormap'") + raise ValueError("Specify one of 'color' or 'colormap'") \ No newline at end of file From 05bff039933543b0681d742e23827a42ab7734c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 4 Oct 2025 11:35:00 +0000 Subject: [PATCH 2/3] feat: Add tooltip to Altair agent portrayal (#2795) This feature adds a `tooltip` attribute to `AgentPortrayalStyle`, enabling agent-specific information to be displayed on hover in Altair-based visualizations. This commit addresses review feedback by: - Adding documentation to clarify the feature is Altair-only. - Raising a ValueError if tooltips are used with the Matplotlib backend. - Applying consistency, typo, and formatting fixes suggested by reviewers. --- .../basic/boltzmann_wealth_model/app.py | 31 +++++-- mesa/visualization/backends/altair_backend.py | 88 ++++++++++--------- .../backends/matplotlib_backend.py | 5 +- .../components/portrayal_components.py | 5 +- tests/test_backends.py | 1 + 5 files changed, 78 insertions(+), 52 deletions(-) diff --git a/mesa/examples/basic/boltzmann_wealth_model/app.py b/mesa/examples/basic/boltzmann_wealth_model/app.py index c9328264bb8..1a81c9f01b3 100644 --- a/mesa/examples/basic/boltzmann_wealth_model/app.py +++ b/mesa/examples/basic/boltzmann_wealth_model/app.py @@ -38,6 +38,23 @@ def agent_portrayal(agent): } +def post_process(chart): + """Post-process the Altair chart to add a colorbar legend.""" + chart = chart.encode( + color=alt.Color( + "original_color:Q", + scale=alt.Scale(scheme="viridis", domain=[0, 10]), + legend=alt.Legend( + title="Wealth", + orient="right", + type="gradient", + gradientLength=200, + ), + ), + ) + return chart + + model = BoltzmannWealth(50, 10, 10) # The SpaceRenderer is responsible for drawing the model's space and agents. @@ -47,13 +64,11 @@ def agent_portrayal(agent): renderer = SpaceRenderer(model, backend="altair") # Can customize the grid appearance. renderer.draw_structure(grid_color="black", grid_dash=[6, 2], grid_opacity=0.3) -renderer.draw_agents( - agent_portrayal=agent_portrayal, - cmap="viridis", - vmin=0, - vmax=10, - legend_title="Wealth", -) +renderer.draw_agents(agent_portrayal=agent_portrayal) +# The post_process function is used to modify the Altair chart after it has been created. +# It can be used to add legends, colorbars, or other visual elements. +renderer.post_process = post_process + # Creates a line plot component from the model's "Gini" datacollector. GiniPlot = make_plot_component("Gini") @@ -67,4 +82,4 @@ def agent_portrayal(agent): model_params=model_params, name="Boltzmann Wealth Model", ) -page # noqa \ No newline at end of file +page # noqa diff --git a/mesa/visualization/backends/altair_backend.py b/mesa/visualization/backends/altair_backend.py index ebf1e2bf925..183998a43dd 100644 --- a/mesa/visualization/backends/altair_backend.py +++ b/mesa/visualization/backends/altair_backend.py @@ -1,3 +1,9 @@ +"""Altair-based renderer for Mesa spaces. + +This module provides an Altair-based renderer for visualizing Mesa model spaces, +agents, and property layers with interactive charting capabilities. +""" + import warnings from collections.abc import Callable from dataclasses import fields @@ -201,12 +207,21 @@ def collect_agent_data( return final_data - - def draw_agents( self, arguments, chart_width: int = 450, chart_height: int = 350, **kwargs ): - """Draw agents using Altair backend.""" + """Draw agents using Altair backend. + + Args: + arguments: Dictionary containing agent data arrays. + chart_width: Width of the chart. + chart_height: Height of the chart. + **kwargs: Additional keyword arguments for customization. + Checkout respective `SpaceDrawer` class on details how to pass **kwargs. + + Returns: + alt.Chart: The Altair chart representing the agents, or None if no agents. + """ if arguments["loc"].size == 0: return None @@ -219,7 +234,8 @@ def draw_agents( "size": arguments["size"][i], "shape": arguments["shape"][i], "opacity": arguments["opacity"][i], - "strokeWidth": arguments["strokeWidth"][i] / 10, # Scale for continuous domain + "strokeWidth": arguments["strokeWidth"][i] + / 10, # Scale for continuous domain "original_color": arguments["color"][i], } # Add tooltip data if available @@ -230,7 +246,11 @@ def draw_agents( # Determine fill and stroke colors if arguments["filled"][i]: record["viz_fill_color"] = arguments["color"][i] - record["viz_stroke_color"] = arguments["stroke"][i] if isinstance(arguments["stroke"][i], str) else None + record["viz_stroke_color"] = ( + arguments["stroke"][i] + if isinstance(arguments["stroke"][i], str) + else None + ) else: record["viz_fill_color"] = None record["viz_stroke_color"] = arguments["color"][i] @@ -240,52 +260,32 @@ def draw_agents( df = pd.DataFrame(records) # Ensure all columns that should be numeric are, handling potential Nones - numeric_cols = ['x', 'y', 'size', 'opacity', 'strokeWidth', 'original_color'] + numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"] for col in numeric_cols: if col in df.columns: - df[col] = pd.to_numeric(df[col], errors='coerce') - + df[col] = pd.to_numeric(df[col], errors="coerce") # Get tooltip keys from the first valid record tooltip_list = ["x", "y"] - # This is the corrected line: if any(t is not None for t in arguments["tooltip"]): - first_valid_tooltip = next((t for t in arguments["tooltip"] if t), None) - if first_valid_tooltip: - tooltip_list.extend(first_valid_tooltip.keys()) + first_valid_tooltip = next( + (t for t in arguments["tooltip"] if t is not None), None + ) + if first_valid_tooltip is not None: + tooltip_list.extend(first_valid_tooltip.keys()) # Extract additional parameters from kwargs title = kwargs.pop("title", "") xlabel = kwargs.pop("xlabel", "") ylabel = kwargs.pop("ylabel", "") - legend_title = kwargs.pop("legend_title", "Color") - - # Handle custom colormapping - cmap = kwargs.pop("cmap", "viridis") - vmin = kwargs.pop("vmin", None) - vmax = kwargs.pop("vmax", None) + # FIXME: Add more parameters to kwargs color_is_numeric = pd.api.types.is_numeric_dtype(df["original_color"]) - if color_is_numeric: - color_min = vmin if vmin is not None else df["original_color"].min() - color_max = vmax if vmax is not None else df["original_color"].max() - - fill_encoding = alt.Fill( - "original_color:Q", - scale=alt.Scale(scheme=cmap, domain=[color_min, color_max]), - legend=alt.Legend( - title=legend_title, - orient="right", - type="gradient", - gradientLength=200, - ), - ) - else: - fill_encoding = alt.Fill( - "viz_fill_color:N", - scale=None, - title="Color", - ) + fill_encoding = ( + alt.Fill("original_color:Q") + if color_is_numeric + else alt.Fill("viz_fill_color:N", scale=None, title="Color") + ) # Determine space dimensions xmin, xmax, ymin, ymax = self.space_drawer.get_viz_limits() @@ -316,10 +316,16 @@ def draw_agents( ), title="Shape", ), - opacity=alt.Opacity("opacity:Q", title="Opacity", scale=alt.Scale(domain=[0, 1], range=[0, 1])), + opacity=alt.Opacity( + "opacity:Q", + title="Opacity", + scale=alt.Scale(domain=[0, 1], range=[0, 1]), + ), fill=fill_encoding, stroke=alt.Stroke("viz_stroke_color:N", scale=None), - strokeWidth=alt.StrokeWidth("strokeWidth:Q", scale=alt.Scale(domain=[0, 1])), + strokeWidth=alt.StrokeWidth( + "strokeWidth:Q", scale=alt.Scale(domain=[0, 1]) + ), tooltip=tooltip_list, ) .properties(title=title, width=chart_width, height=chart_height) @@ -431,4 +437,4 @@ def draw_propertylayer( main_charts.append(current_chart) base = alt.layer(*main_charts).resolve_scale(color="independent") - return base \ No newline at end of file + return base diff --git a/mesa/visualization/backends/matplotlib_backend.py b/mesa/visualization/backends/matplotlib_backend.py index aea3e74ccc2..c1a3e7f8451 100644 --- a/mesa/visualization/backends/matplotlib_backend.py +++ b/mesa/visualization/backends/matplotlib_backend.py @@ -27,7 +27,6 @@ OrthogonalGrid = SingleGrid | MultiGrid | OrthogonalMooreGrid | OrthogonalVonNeumannGrid HexGrid = HexSingleGrid | HexMultiGrid | mesa.discrete_space.HexGrid - CORRECTION_FACTOR_MARKER_ZOOM = 0.01 @@ -141,6 +140,10 @@ def collect_agent_data(self, space, agent_portrayal, default_size=None): ) else: aps = portray_input + if aps.tooltip is not None: + raise ValueError( + "The 'tooltip' attribute in AgentPortrayalStyle is only supported by the Altair backend." + ) # Set defaults if not provided if aps.x is None and aps.y is None: aps.x, aps.y = self._get_agent_pos(agent, space) diff --git a/mesa/visualization/components/portrayal_components.py b/mesa/visualization/components/portrayal_components.py index eb748e8177d..bc714d42ba8 100644 --- a/mesa/visualization/components/portrayal_components.py +++ b/mesa/visualization/components/portrayal_components.py @@ -56,6 +56,7 @@ class AgentPortrayalStyle: edgecolors: str | tuple | None = None linewidths: float | int | None = 1.0 tooltip: dict | None = None + """A dictionary of data to display on hover. Note: This feature is only available with the Altair backend.""" def update(self, *updates_fields: tuple[str, Any]): """Updates attributes from variable (field_name, new_value) tuple arguments. @@ -92,7 +93,7 @@ class PropertyLayerStyle: (vmin, vmax), transparency (alpha) and colorbar visibility. Note: vmin and vmax are the lower and upper bounds for the colorbar and the data is - normalized between these values for color/colorbar rendering. If they are not + normalized between these values for color/colormap rendering. If they are not declared the values are automatically determined from the data range. Note: You can specify either a 'colormap' (for varying data) or a single @@ -118,4 +119,4 @@ def __post_init__(self): if self.color is not None and self.colormap is not None: raise ValueError("Specify either 'color' or 'colormap', not both.") if self.color is None and self.colormap is None: - raise ValueError("Specify one of 'color' or 'colormap'") \ No newline at end of file + raise ValueError("Specify one of 'color' or 'colormap'") diff --git a/tests/test_backends.py b/tests/test_backends.py index d4c15d36185..018e051b6e4 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -248,6 +248,7 @@ def test_altair_backend_draw_agents(): "color": np.array(["red", "blue"]), "filled": np.array([True, True]), "stroke": np.array(["black", "black"]), + "tooltip": np.array([None, None]), } ab.space_drawer.get_viz_limits = MagicMock(return_value=(0, 10, 0, 10)) assert ab.draw_agents(arguments) is not None From f79ceacea224db6caf60f8ee4f46b0ea43208d86 Mon Sep 17 00:00:00 2001 From: DipayanDasgupta Date: Mon, 24 Nov 2025 03:17:44 +0000 Subject: [PATCH 3/3] fix(altair): handle categorical colors safely and finalize merge --- mesa/visualization/backends/altair_backend.py | 14 +++++++++++++- .../components/portrayal_components.py | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/mesa/visualization/backends/altair_backend.py b/mesa/visualization/backends/altair_backend.py index 183998a43dd..88b75281e92 100644 --- a/mesa/visualization/backends/altair_backend.py +++ b/mesa/visualization/backends/altair_backend.py @@ -260,11 +260,23 @@ def draw_agents( df = pd.DataFrame(records) # Ensure all columns that should be numeric are, handling potential Nones - numeric_cols = ["x", "y", "size", "opacity", "strokeWidth", "original_color"] + numeric_cols = ["x", "y", "size", "opacity", "strokeWidth"] for col in numeric_cols: if col in df.columns: df[col] = pd.to_numeric(df[col], errors="coerce") + # Handle color numeric conversion safely + if "original_color" in df.columns: + color_values = arguments["color"] + color_is_numeric = all( + isinstance(x, int | float | np.number) or x is None + for x in color_values + ) + if color_is_numeric: + df["original_color"] = pd.to_numeric( + df["original_color"], errors="coerce" + ) + # Get tooltip keys from the first valid record tooltip_list = ["x", "y"] if any(t is not None for t in arguments["tooltip"]): diff --git a/mesa/visualization/components/portrayal_components.py b/mesa/visualization/components/portrayal_components.py index bc714d42ba8..45d50f1d296 100644 --- a/mesa/visualization/components/portrayal_components.py +++ b/mesa/visualization/components/portrayal_components.py @@ -27,6 +27,19 @@ class AgentPortrayalStyle: x, y are determined automatically according to the agent's type (normal/CellAgent) and position in the space if not manually declared. + Attributes: + x (float | None): The x-coordinate of the agent. + y (float | None): The y-coordinate of the agent. + color (ColorLike | None): The color of the agent. + marker (str | None): The marker shape for the agent. + size (int | float | None): The size of the agent marker. + zorder (int | None): The z-order for drawing the agent. + alpha (float | None): The opacity of the agent. + edgecolors (str | tuple | None): The color of the marker's edge. + linewidths (float | int | None): The width of the marker's edge. + tooltip (dict | None): A dictionary of data to display on hover. + Note: This feature is only available with the Altair backend. + Example: >>> def agent_portrayal(agent): >>> return AgentPortrayalStyle( @@ -56,7 +69,6 @@ class AgentPortrayalStyle: edgecolors: str | tuple | None = None linewidths: float | int | None = 1.0 tooltip: dict | None = None - """A dictionary of data to display on hover. Note: This feature is only available with the Altair backend.""" def update(self, *updates_fields: tuple[str, Any]): """Updates attributes from variable (field_name, new_value) tuple arguments.