@@ -4160,16 +4160,18 @@ def _process_multiple_axis_spanning_shapes(
41604160 n_annotations_before = len (self .layout ["annotations" ])
41614161
41624162 if shape_type == "vline" :
4163- # Always use a single labeled shape for vlines.
4163+ # vline: create a labeled shape and (for now) also keep a legacy annotation
4164+ # so existing behavior and tests continue to work. Once label is approved,
4165+ # we can remove the annotation path.
41644166
41654167 # Split kwargs into shape vs legacy annotation_* (which we map to label)
41664168 shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (kwargs , "annotation_" )
41674169
41684170 # Build/merge label dict: start with explicit label=..., then copy safe legacy fields
41694171 label_dict = (kwargs .get ("label" ) or {}).copy ()
41704172 # Reuse Step-2 shim behavior (safe fields only)
4171- if "text" not in label_dict and "text" in ( kwargs . get ( "label" ) or {}) :
4172- pass # (explicit label provided)
4173+ if "annotation_text" in legacy_ann and "text" not in label_dict :
4174+ label_dict [ "text" ] = legacy_ann [ "annotation_text" ]
41734175 else :
41744176 if "annotation_text" in legacy_ann and "text" not in label_dict :
41754177 label_dict ["text" ] = legacy_ann ["annotation_text" ]
@@ -4234,19 +4236,148 @@ def _process_multiple_axis_spanning_shapes(
42344236 exclude_empty_subplots = exclude_empty_subplots ,
42354237 yref = shape_kwargs .get ("yref" , "y" ),
42364238 )
4239+ elif shape_type == "hline" :
4240+ # hline: create a labeled shape and (for now) also keep a legacy annotation
4241+ # so existing behavior and tests continue to work. Once label is approved,
4242+ # we can remove the annotation path.
4243+
4244+ # Split kwargs into shape vs legacy annotation_* (which we map to label)
4245+ shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (
4246+ kwargs , "annotation_"
4247+ )
4248+
4249+ # Build/merge label dict: start with explicit label=..., then copy safe legacy fields
4250+ label_dict = (kwargs .get ("label" ) or {}).copy ()
4251+
4252+ if "annotation_text" in legacy_ann and "text" not in label_dict :
4253+ label_dict ["text" ] = legacy_ann ["annotation_text" ]
4254+ if "annotation_font" in legacy_ann and "font" not in label_dict :
4255+ label_dict ["font" ] = legacy_ann ["annotation_font" ]
4256+ if "annotation_textangle" in legacy_ann and "textangle" not in label_dict :
4257+ label_dict ["textangle" ] = legacy_ann ["annotation_textangle" ]
4258+
4259+ # Position mapping (legacy → label.textposition for HLINES)
4260+ # For horizontal lines we care about left/right/middle along x:
4261+ # left -> start
4262+ # right -> end
4263+ # middle/center -> middle
4264+ pos_hint = legacy_ann .get ("annotation_position" , None )
4265+ if "textposition" not in label_dict :
4266+ if pos_hint is not None :
4267+ # validate token (raises ValueError on nonsense, like bad mushrooms 🍄)
4268+ _ = _normalize_legacy_line_position_to_textposition (pos_hint )
4269+ p = pos_hint .strip ().lower ()
4270+ if "right" in p :
4271+ label_dict ["textposition" ] = "end"
4272+ elif "left" in p :
4273+ label_dict ["textposition" ] = "start"
4274+ elif p in ("middle" , "center" , "centre" ):
4275+ label_dict ["textposition" ] = "middle"
4276+ # if only "top"/"bottom" were mentioned, we leave default "middle"
4277+ else :
4278+ # default for lines is "middle"
4279+ label_dict .setdefault ("textposition" , "middle" )
4280+
4281+ # NOTE: Label does not support bgcolor/bordercolor; warn when present
4282+ if "annotation_bgcolor" in legacy_ann or "annotation_bordercolor" in legacy_ann :
4283+ import warnings
4284+ warnings .warn (
4285+ "annotation_bgcolor/annotation_bordercolor are not supported on shape.label "
4286+ "and will be ignored; use label.font/color or a background shape instead." ,
4287+ FutureWarning ,
4288+ )
4289+
4290+ # Build the shape
4291+ shape_to_add = _combine_dicts ([shape_args , shape_kwargs ])
4292+ if label_dict :
4293+ shape_to_add ["label" ] = label_dict
4294+
4295+ # Add the shape
4296+ self .add_shape (
4297+ row = row ,
4298+ col = col ,
4299+ exclude_empty_subplots = exclude_empty_subplots ,
4300+ ** shape_to_add ,
4301+ )
4302+
4303+ # Run legacy annotation logic (for now)
4304+ augmented_annotation = shapeannotation .axis_spanning_shape_annotation (
4305+ annotation ,
4306+ shape_type ,
4307+ shape_args ,
4308+ legacy_ann ,
4309+ )
4310+
4311+ if augmented_annotation is not None :
4312+ self .add_annotation (
4313+ augmented_annotation ,
4314+ row = row ,
4315+ col = col ,
4316+ exclude_empty_subplots = exclude_empty_subplots ,
4317+ # same as the old else-branch: let yref default to "y"
4318+ yref = shape_kwargs .get ("yref" , "y" ),
4319+ )
4320+
42374321 elif shape_type == "vrect" :
4322+ # vrect: create a labeled rect and (for now) also keep a legacy annotation
4323+
42384324 # Split kwargs into shape vs legacy annotation_* (which we map to label)
4239- shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (kwargs , "annotation_" )
4325+ shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (
4326+ kwargs , "annotation_"
4327+ )
42404328
42414329 # Build/merge label dict: start with explicit label=..., then copy safe legacy fields
42424330 label_dict = (kwargs .get ("label" ) or {}).copy ()
4331+
42434332 if "annotation_text" in legacy_ann and "text" not in label_dict :
42444333 label_dict ["text" ] = legacy_ann ["annotation_text" ]
42454334 if "annotation_font" in legacy_ann and "font" not in label_dict :
42464335 label_dict ["font" ] = legacy_ann ["annotation_font" ]
42474336 if "annotation_textangle" in legacy_ann and "textangle" not in label_dict :
42484337 label_dict ["textangle" ] = legacy_ann ["annotation_textangle" ]
42494338
4339+ # Position mapping (legacy → label.textposition for RECTANGLES)
4340+ # annotation_position supports things like:
4341+ # "inside top left", "inside bottom right", "outside top", etc.
4342+ pos_hint = legacy_ann .get ("annotation_position" , None )
4343+ if "textposition" not in label_dict and pos_hint is not None :
4344+ p = pos_hint .strip ().lower ()
4345+
4346+ # strip "inside"/"outside" prefix, keep the corner/edge
4347+ for prefix in ("inside " , "outside " ):
4348+ if p .startswith (prefix ):
4349+ p = p [len (prefix ) :]
4350+
4351+ # p is now like "top left", "bottom right", "top", "bottom", "left", "right"
4352+ # Map to valid shape.label textposition for rects:
4353+ # top left / top / top right
4354+ # middle left / middle center / middle right
4355+ # bottom left / bottom / bottom right
4356+ #
4357+ # Note: we don't distinguish inside vs outside in label API; this at least
4358+ # keeps the correct side/corner.
4359+ if p in (
4360+ "top left" ,
4361+ "top center" ,
4362+ "top right" ,
4363+ "middle left" ,
4364+ "middle center" ,
4365+ "middle right" ,
4366+ "bottom left" ,
4367+ "bottom center" ,
4368+ "bottom right" ,
4369+ ):
4370+ label_dict ["textposition" ] = p
4371+ elif p == "top" :
4372+ label_dict ["textposition" ] = "top center"
4373+ elif p == "bottom" :
4374+ label_dict ["textposition" ] = "bottom center"
4375+ elif p == "left" :
4376+ label_dict ["textposition" ] = "middle left"
4377+ elif p == "right" :
4378+ label_dict ["textposition" ] = "middle right"
4379+ # else: leave default
4380+
42504381 # NOTE: Label does not support bgcolor/bordercolor; keep emitting a warning when present
42514382 if "annotation_bgcolor" in legacy_ann or "annotation_bordercolor" in legacy_ann :
42524383 import warnings
@@ -4269,7 +4400,7 @@ def _process_multiple_axis_spanning_shapes(
42694400 ** shape_to_add ,
42704401 )
42714402
4272- # Run legacy annotation logic only if an explicit annotation object was provided
4403+ # Run legacy annotation logic (for now)
42734404 augmented_annotation = shapeannotation .axis_spanning_shape_annotation (
42744405 annotation , shape_type , shape_args , legacy_ann
42754406 )
@@ -4281,20 +4412,56 @@ def _process_multiple_axis_spanning_shapes(
42814412 exclude_empty_subplots = exclude_empty_subplots ,
42824413 yref = shape_kwargs .get ("yref" , "y" ),
42834414 )
4284-
4415+
42854416 elif shape_type == "hrect" :
4417+ # hrect: create a labeled rect and (for now) also keep a legacy annotation
4418+
42864419 # Split kwargs into shape vs legacy annotation_* (which we map to label)
4287- shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (kwargs , "annotation_" )
4420+ shape_kwargs , legacy_ann = shapeannotation .split_dict_by_key_prefix (
4421+ kwargs , "annotation_"
4422+ )
42884423
42894424 # Build/merge label dict: start with explicit label=..., then copy safe legacy fields
42904425 label_dict = (kwargs .get ("label" ) or {}).copy ()
4426+
42914427 if "annotation_text" in legacy_ann and "text" not in label_dict :
42924428 label_dict ["text" ] = legacy_ann ["annotation_text" ]
42934429 if "annotation_font" in legacy_ann and "font" not in label_dict :
42944430 label_dict ["font" ] = legacy_ann ["annotation_font" ]
42954431 if "annotation_textangle" in legacy_ann and "textangle" not in label_dict :
42964432 label_dict ["textangle" ] = legacy_ann ["annotation_textangle" ]
42974433
4434+ # Position mapping (legacy → label.textposition for RECTANGLES)
4435+ pos_hint = legacy_ann .get ("annotation_position" , None )
4436+ if "textposition" not in label_dict and pos_hint is not None :
4437+ p = pos_hint .strip ().lower ()
4438+
4439+ # strip "inside"/"outside" prefix
4440+ for prefix in ("inside " , "outside " ):
4441+ if p .startswith (prefix ):
4442+ p = p [len (prefix ) :]
4443+
4444+ if p in (
4445+ "top left" ,
4446+ "top center" ,
4447+ "top right" ,
4448+ "middle left" ,
4449+ "middle center" ,
4450+ "middle right" ,
4451+ "bottom left" ,
4452+ "bottom center" ,
4453+ "bottom right" ,
4454+ ):
4455+ label_dict ["textposition" ] = p
4456+ elif p == "top" :
4457+ label_dict ["textposition" ] = "top center"
4458+ elif p == "bottom" :
4459+ label_dict ["textposition" ] = "bottom center"
4460+ elif p == "left" :
4461+ label_dict ["textposition" ] = "middle left"
4462+ elif p == "right" :
4463+ label_dict ["textposition" ] = "middle right"
4464+
42984465 # NOTE: Label does not support bgcolor/bordercolor; warn when present
42994466 if "annotation_bgcolor" in legacy_ann or "annotation_bordercolor" in legacy_ann :
43004467 import warnings
@@ -4317,7 +4484,7 @@ def _process_multiple_axis_spanning_shapes(
43174484 ** shape_to_add ,
43184485 )
43194486
4320- # Run legacy annotation logic only if an explicit annotation object was provided
4487+ # Run legacy annotation logic (for now)
43214488 augmented_annotation = shapeannotation .axis_spanning_shape_annotation (
43224489 annotation , shape_type , shape_args , legacy_ann
43234490 )
@@ -4330,9 +4497,7 @@ def _process_multiple_axis_spanning_shapes(
43304497 xref = shape_kwargs .get ("xref" , "x" ),
43314498 )
43324499
4333-
43344500 else :
4335-
43364501 # shapes are always added at the end of the tuple of shapes, so we see
43374502 # how long the tuple is before the call and after the call, and adjust
43384503 # the new shapes that were added at the end
@@ -4417,14 +4582,17 @@ def add_vline(
44174582 add_vline .__doc__ = _axis_spanning_shapes_docstr ("vline" )
44184583
44194584 def add_hline (
4420- self ,
4421- y ,
4422- row = "all" ,
4423- col = "all" ,
4424- exclude_empty_subplots = True ,
4425- annotation = None ,
4426- ** kwargs ,
4427- ):
4585+ self ,
4586+ y ,
4587+ row = "all" ,
4588+ col = "all" ,
4589+ exclude_empty_subplots = True ,
4590+ annotation = None ,
4591+ ** kwargs ,
4592+ ):
4593+ # Translate legacy annotation_* → label (non-destructive; warns if used)
4594+ kwargs = _coerce_shape_label_from_legacy_annotation_kwargs (kwargs )
4595+
44284596 self ._process_multiple_axis_spanning_shapes (
44294597 dict (
44304598 type = "line" ,
@@ -4445,16 +4613,16 @@ def add_hline(
44454613 add_hline .__doc__ = _axis_spanning_shapes_docstr ("hline" )
44464614
44474615 def add_vrect (
4448- self ,
4449- x0 ,
4450- x1 ,
4451- row = "all" ,
4452- col = "all" ,
4453- exclude_empty_subplots = True ,
4454- annotation = None ,
4455- ** kwargs ,
4456- ):
4457- # NEW (Step 2): translate legacy annotation_* → label (non-destructive; warns if used)
4616+ self ,
4617+ x0 ,
4618+ x1 ,
4619+ row = "all" ,
4620+ col = "all" ,
4621+ exclude_empty_subplots = True ,
4622+ annotation = None ,
4623+ ** kwargs ,
4624+ ):
4625+ # Translate legacy annotation_* → label (non-destructive; warns if used)
44584626 kwargs = _coerce_shape_label_from_legacy_annotation_kwargs (kwargs )
44594627
44604628 self ._process_multiple_axis_spanning_shapes (
@@ -4470,6 +4638,7 @@ def add_vrect(
44704638
44714639 add_vrect .__doc__ = _axis_spanning_shapes_docstr ("vrect" )
44724640
4641+
44734642 def add_hrect (
44744643 self ,
44754644 y0 ,
@@ -4480,7 +4649,7 @@ def add_hrect(
44804649 annotation = None ,
44814650 ** kwargs ,
44824651 ):
4483- # NEW (Step 2): translate legacy annotation_* → label (non-destructive; warns if used)
4652+ # Translate legacy annotation_* → label (non-destructive; warns if used)
44844653 kwargs = _coerce_shape_label_from_legacy_annotation_kwargs (kwargs )
44854654
44864655 self ._process_multiple_axis_spanning_shapes (
0 commit comments