Skip to content

User set names are overridden by automatic #4070

Description

@joelostblom

What happened?

When a user explicitly sets name= on a chart that ends up as an inner spec inside a FacetChart or LayerChart concat panel, Altair silently renames it by appending _0, _1, etc. The user-supplied name is not preserved in the output spec.

This affects two code paths in _combine_subchart_params in altair/vegalite/v6/api.py:

  1. FacetChart — renaming happens inside the param-processing loop when the FacetChart subchart carries params.
  2. LayerChart (introduced in PR fix: Avoid layered concat view name collisions #4066) — renaming happens in a new pre-pass that runs unconditionally for all is_concat cases, regardless of whether params are present.

The two cases also have an asymmetry: the FacetChart rename only fires when that subchart has params attached; the LayerChart pre-pass renames even when there are no params at all.

Minimal reproducible examples

Case 1 — FacetChart

import altair as alt
import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 4, 9], "category": ["a", "b", "c"]})
hover = alt.selection_point(fields=["x"], on="mouseover", empty=False)

chart = alt.Chart(df, name="my_panel").encode(x="x:Q", y="y:Q").mark_line()
faceted = chart.add_params(hover).facet("category:N")

spec = alt.vconcat(faceted, faceted).to_dict()
print(spec["vconcat"][0]["spec"]["name"])  # "my_panel_0"  ← expected "my_panel"
print(spec["vconcat"][1]["spec"]["name"])  # "my_panel_1"  ← expected "my_panel"

Case 2 — LayerChart

import altair as alt
import pandas as pd

df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 4, 9]})
hover = alt.selection_point(fields=["x"], on="mouseover", empty=False)

base = alt.Chart(df, name="my_panel").encode(x="x:Q", y="y:Q")
lines = base.mark_line()
points = base.encode(size=alt.condition(hover, alt.value(120), alt.value(40))).mark_circle()
layered = lines + points

p1 = layered.transform_filter("datum.x > 1")
p2 = layered.transform_filter("datum.x < 3")

spec = alt.vconcat(p1, p2).add_params(hover).to_dict()
print(spec["vconcat"][0]["layer"][0]["name"])  # "my_panel_0"  ← expected "my_panel"
print(spec["vconcat"][1]["layer"][0]["name"])  # "my_panel_1"  ← expected "my_panel"

The LayerChart case also renames when no params are present at all (the FacetChart case does not):

# No params — LayerChart still renames on PR #4066 branch
base = alt.Chart(df, name="my_panel").encode(x="x:Q", y="y:Q")
layered = base.mark_line() + base.mark_circle()
p1 = layered.transform_filter("datum.x > 1")
p2 = layered.transform_filter("datum.x < 3")

spec = alt.vconcat(p1, p2).to_dict()
print(spec["vconcat"][0]["layer"][0]["name"])  # "my_panel_0"  ← expected "my_panel"

What would you like to happen instead?

The renaming is necessary — preserving a colliding user-set name would recreate the original rendering bug. But when Altair modifies a user-supplied name, maybe a UserWarning could be emitted so the user knows to update any downstream references. No warning is needed when renaming auto-generated content-hash names (those matching view_<16 hex chars>), since those are an internal implementation detail.

This mirrors the existing pattern in the codebase, where Altair already warns for analogous silent fixups:

UserWarning: Automatically deduplicated selection parameter with identical configuration.
If you want independent parameters, explicitly name them differently ...

We also don't want Altair to be too noisy, so I'm not 100% sure on this fix, but I'm recording it here for visibility.

Proposed fix direction

In _combine_subchart_params (altair/vegalite/v6/api.py):

  • The FacetChart branch (spec.layer[0].name = f"{_view_base_for_chart(spec.layer[0])}_{i}") runs inside the param loop and does not distinguish between user-set and auto-generated names.
  • The LayerChart pre-pass added in PR fix: Avoid layered concat view name collisions #4066 (layer.name = f"{_view_base_for_chart(layer)}_{i}") has the same issue, and additionally runs even when there are no params.

Add a helper that distinguishes auto-generated names from user-set ones, and emit a warning only for the latter:

import re
_AUTO_NAME_RE = re.compile(r'^view_[0-9a-f]{16}(_\d+)?$')

def _is_auto_name(name: str) -> bool:
    return bool(_AUTO_NAME_RE.match(name))

Then wrap the rename with a warning for user-set names:

if isinstance(layer, Chart) and layer.name is not Undefined:
    new_name = f"{_view_base_for_chart(layer)}_{i}"
    if not _is_auto_name(layer.name) and layer.name != new_name:
        warnings.warn(
            f"Chart name {layer.name!r} was automatically renamed to {new_name!r} "
            "to avoid view name collisions in concat. If you need to reference this "
            "view by name, use the renamed form.",
            UserWarning,
        )
    layer.name = new_name

The same guard could be applied to the existing FacetChart branch.

Which version of Altair are you using?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugneeds-triageBug report needs maintainer response

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions