Skip to content

plot

PlotEngine

Bases: Observer

PlotEngine renders power-line sections on Plotly figures.

It accepts either a BalanceEngine or an already-constructed PositionEngine. When a BalanceEngine is passed, a PositionEngine is created automatically and exposed via position_engine.

Reactivity is preserved through a two-hop observer chain:

1
BalanceEngine  --notifies-->  PositionEngine  --notifies-->  PlotEngine

Parameters:

Name Type Description Default

engine

Union[BalanceEngine, PositionEngine]

A BalanceEngine or PositionEngine instance.

required

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> import plotly.graph_objects as go
>>> # Pass a BalanceEngine directly (PositionEngine is auto-created)
>>> plt_engine = PlotEngine(balance_engine)
>>> fig = go.Figure()
>>> plt_engine.preview_line3d(fig)
>>> fig.show()
>>> # Access the position engine for headless computation:
>>> pos_engine = plt_engine.position_engine
>>> pos_engine.get_supports_points()
array(...)
>>> # Or build a PositionEngine first and pass it in:
>>> from mechaphlowers.core.geometry.position_engine import PositionEngine
>>> pos_engine = PositionEngine(balance_engine)
>>> plt_engine = PlotEngine(pos_engine)
Source code in src/mechaphlowers/plotting/plot.py
431
432
433
434
435
436
437
438
439
440
441
442
443
def __init__(
    self,
    engine: Union[BalanceEngine, PositionEngine],
) -> None:
    if isinstance(engine, BalanceEngine):
        self.position_engine = PositionEngine(engine)
    elif isinstance(engine, PositionEngine):
        self.position_engine = engine
    else:
        raise TypeError(
            "engine must be a BalanceEngine or PositionEngine instance"
        )
    self.position_engine.bind_to(self)

beta property

beta: ndarray

Delegating property — see PositionEngine.beta.

cable_loads property

cable_loads

Delegating property — see PositionEngine.

coords_calculator property

coords_calculator

Delegating property — see PositionEngine.

section_array property

section_array

Delegating property — see PositionEngine.

span_model property

span_model

Delegating property — see PositionEngine.

span_names property

span_names: list[str]

Names of the spans, each being the concatenation of the two adjacent support names.

For supports ["A", "B", "C"] this returns ["A-B", "B-C"].

support_names property

support_names: list[str]

Names of the supports as defined in the section array.

add_obstacle_array

add_obstacle_array(obstacle_array: ObstacleArray) -> None

Delegate to PositionEngine.add_obstacle_array.

Source code in src/mechaphlowers/plotting/plot.py
517
518
519
def add_obstacle_array(self, obstacle_array: ObstacleArray) -> None:
    """Delegate to [`PositionEngine.add_obstacle_array`][mechaphlowers.core.geometry.position_engine.PositionEngine.add_obstacle_array]."""
    self.position_engine.add_obstacle_array(obstacle_array)

get_insulators_points

get_insulators_points() -> ndarray

Delegate to PositionEngine.get_insulators_points.

Source code in src/mechaphlowers/plotting/plot.py
531
532
533
def get_insulators_points(self) -> np.ndarray:
    """Delegate to [`PositionEngine.get_insulators_points`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_insulators_points]."""
    return self.position_engine.get_insulators_points()

get_loads_coords

get_loads_coords(
    project: bool = False, frame_index: int = 0
) -> dict

Delegate to PositionEngine.get_loads_coords.

Source code in src/mechaphlowers/plotting/plot.py
548
549
550
551
552
def get_loads_coords(
    self, project: bool = False, frame_index: int = 0
) -> dict:
    """Delegate to [`PositionEngine.get_loads_coords`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_loads_coords]."""
    return self.position_engine.get_loads_coords(project, frame_index)

get_obstacles_points

get_obstacles_points() -> ndarray

Delegate to PositionEngine.get_obstacles_points.

Source code in src/mechaphlowers/plotting/plot.py
535
536
537
def get_obstacles_points(self) -> np.ndarray:
    """Delegate to [`PositionEngine.get_obstacles_points`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_obstacles_points]."""
    return self.position_engine.get_obstacles_points()

get_points_for_plot

get_points_for_plot(
    project: bool = False, frame_index: int = 0
) -> tuple[Points, Points, Points]

Delegate to PositionEngine.get_points_for_plot.

Source code in src/mechaphlowers/plotting/plot.py
554
555
556
557
558
def get_points_for_plot(
    self, project: bool = False, frame_index: int = 0
) -> tuple[Points, Points, Points]:
    """Delegate to [`PositionEngine.get_points_for_plot`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_points_for_plot]."""
    return self.position_engine.get_points_for_plot(project, frame_index)

get_spans_points

get_spans_points(
    frame: Literal['section', 'localsection', 'cable'],
) -> ndarray

Delegate to PositionEngine.get_spans_points.

Source code in src/mechaphlowers/plotting/plot.py
521
522
523
524
525
def get_spans_points(
    self, frame: Literal["section", "localsection", "cable"]
) -> np.ndarray:
    """Delegate to [`PositionEngine.get_spans_points`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_spans_points]."""
    return self.position_engine.get_spans_points(frame)

get_supports_points

get_supports_points() -> ndarray

Delegate to PositionEngine.get_supports_points.

Source code in src/mechaphlowers/plotting/plot.py
527
528
529
def get_supports_points(self) -> np.ndarray:
    """Delegate to [`PositionEngine.get_supports_points`][mechaphlowers.core.geometry.position_engine.PositionEngine.get_supports_points]."""
    return self.position_engine.get_supports_points()

initialize_engine

initialize_engine(balance_engine: BalanceEngine) -> None

Delegate to PositionEngine.initialize_engine.

Source code in src/mechaphlowers/plotting/plot.py
509
510
511
def initialize_engine(self, balance_engine: BalanceEngine) -> None:
    """Delegate to [`PositionEngine.initialize_engine`][mechaphlowers.core.geometry.position_engine.PositionEngine.initialize_engine]."""
    self.position_engine.initialize_engine(balance_engine)

obstacles_dict

obstacles_dict(project=False, frame_index=0) -> dict

Returns a dictionary storing object coordinates.

Key is object name, value is coordinates of object.

Format: {'obs_0': [[x0, y0, z0], [x1, y1, z1], ...]}

Source code in src/mechaphlowers/plotting/plot.py
539
540
541
542
543
544
545
546
def obstacles_dict(self, project=False, frame_index=0) -> dict:
    """Returns a dictionary storing object coordinates.

    Key is object name, value is coordinates of object.

    Format: {'obs_0': [[x0, y0, z0], [x1, y1, z1], ...]}
    """
    return self.position_engine.obstacles_dict(project, frame_index)

point_distance

point_distance(
    obstacle_name: str,
    point_index: int,
    *,
    fig: Figure | None = None,
) -> DistanceResult

Compute the distance from point to a span, with optional plotting.

Delegates the geometric computation to PositionEngine.point_distance and, when fig is provided, plots the result on the figure.

Parameters:

Name Type Description Default

obstacle_name

str

Obstacle name to get the distances of.

required

point_index

int

point_index of the selected obstacle.

required

fig

Figure | None

Optional Plotly figure. When supplied, the geometry is rendered on it.

None

Returns:

Type Description
DistanceResult

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> balance_engine = ...  # BalanceEngine object with computed balance (use data.catalog.sample_section_factory for sample data)
>>> plt_engine = PlotEngine(balance_engine)
>>> plt_engine.position_engine.add_obstacle(
...     name="obs_0",
...     span_index=0,
...     coords=np.array([[200, 0, 0]]),
...     support_reference='left',
... )
>>> fig = figure_factory()
>>> distance_result = plt_engine.point_distance(
...     obstacle_name="obs_0", point_index=0
... )
# ...get a distance result object with the distance and closest point coordinates
>>> fig.show()
Source code in src/mechaphlowers/plotting/plot.py
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
def point_distance(
    self,
    obstacle_name: str,
    point_index: int,
    *,
    fig: go.Figure | None = None,
) -> DistanceResult:
    """Compute the distance from *point* to a span, with optional plotting.

    Delegates the geometric computation to
    [`PositionEngine.point_distance`][mechaphlowers.core.geometry.position_engine.PositionEngine.point_distance] and, when *fig* is provided,
    plots the result on the figure.

    Args:
        obstacle_name: Obstacle name to get the distances of.
        point_index: point_index of the selected obstacle.
        fig: Optional Plotly figure.  When supplied, the geometry is
            rendered on it.

    Returns:
        [`DistanceResult`][mechaphlowers.core.geometry.distances.DistanceResult].

    Examples:

        >>> balance_engine = ...  # BalanceEngine object with computed balance (use data.catalog.sample_section_factory for sample data)
        >>> plt_engine = PlotEngine(balance_engine)
        >>> plt_engine.position_engine.add_obstacle(
        ...     name="obs_0",
        ...     span_index=0,
        ...     coords=np.array([[200, 0, 0]]),
        ...     support_reference='left',
        ... )
        >>> fig = figure_factory()
        >>> distance_result = plt_engine.point_distance(
        ...     obstacle_name="obs_0", point_index=0
        ... )
        # ...get a distance result object with the distance and closest point coordinates
        >>> fig.show()
    """
    distance_result = self.position_engine.get_distances_from_obstacles()[
        obstacle_name
    ][point_index]

    if fig is not None:
        plot_distance_engine(
            self.position_engine.distance_engine,
            distance_result=distance_result,
            fig=fig,
            show_plane=True,
            show_projections=True,
            title_addendum=f" - Obstacle: {obstacle_name}",
            force_layout=True,
        )
        fig.update_layout(
            title=f"Point Distance Analysis - Obstacle: {obstacle_name}",
            scene=dict(
                xaxis_title="X (m)",
                yaxis_title="Y (m)",
                zaxis_title="Z (m)",
                aspectmode="data",
            ),
            showlegend=True,
            legend=dict(x=0.02, y=0.98),
        )

    return distance_result

point_relative_to_absolute

point_relative_to_absolute(
    span_index: int, point_relative: ndarray
) -> ndarray

Delegate to PositionEngine.point_relative_to_absolute.

Source code in src/mechaphlowers/plotting/plot.py
788
789
790
791
792
793
794
def point_relative_to_absolute(
    self, span_index: int, point_relative: np.ndarray
) -> np.ndarray:
    """Delegate to [`PositionEngine.point_relative_to_absolute`][mechaphlowers.core.geometry.position_engine.PositionEngine.point_relative_to_absolute]."""
    return self.position_engine.point_relative_to_absolute(
        span_index, point_relative
    )

preview_line2d

preview_line2d(
    fig: Figure,
    view: Literal['profile', 'line'] = 'profile',
    frame_index: int = 0,
    mode: Literal['main', 'background'] = 'main',
    name_addendum: str = '',
    cable_name: str | None = None,
    cable_name_addendum: str | None = None,
    support_name: str | None = None,
    support_name_addendum: str | None = None,
    insulator_name: str | None = None,
    insulator_name_addendum: str | None = None,
) -> None

Plot 2D of power lines sections

Parameters:

Name Type Description Default

fig

Figure

plotly figure where new traces has to be added

required

view

Literal['profile', 'line']

profile or line view. Defaults to "profile".

'profile'

frame_index

int

Index of the frame for projection. Defaults to 0.

0

mode

Literal['main', 'background']

Rendering mode. Defaults to "main".

'main'

name_addendum

str

String appended to all trace names. Defaults to "".

''

cable_name

str | None

Full override for the cable trace legend label.

None

cable_name_addendum

str

Addendum for the cable trace name (overrides name_addendum).

None

support_name

str | None

Full override for the support trace legend label.

None

support_name_addendum

str

Addendum for the support trace name (overrides name_addendum).

None

insulator_name

str | None

Full override for the insulator trace legend label.

None

insulator_name_addendum

str

Addendum for the insulator trace name (overrides name_addendum).

None

Raises:

Type Description
ValueError

view value is invalid

Source code in src/mechaphlowers/plotting/plot.py
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
def preview_line2d(
    self,
    fig: go.Figure,
    view: Literal["profile", "line"] = "profile",
    frame_index: int = 0,
    mode: Literal["main", "background"] = "main",
    name_addendum: str = "",
    cable_name: str | None = None,
    cable_name_addendum: str | None = None,
    support_name: str | None = None,
    support_name_addendum: str | None = None,
    insulator_name: str | None = None,
    insulator_name_addendum: str | None = None,
) -> None:
    """Plot 2D of power lines sections

    Args:
        fig (go.Figure): plotly figure where new traces has to be added
        view (Literal['profile', 'line'], optional): profile or line view. Defaults to "profile".
        frame_index (int, optional): Index of the frame for projection. Defaults to 0.
        mode (Literal['main', 'background'], optional): Rendering mode. Defaults to "main".
        name_addendum (str, optional): String appended to all trace names. Defaults to "".
        cable_name (str | None, optional): Full override for the cable trace legend label.
        cable_name_addendum (str, optional): Addendum for the cable trace name (overrides name_addendum).
        support_name (str | None, optional): Full override for the support trace legend label.
        support_name_addendum (str, optional): Addendum for the support trace name (overrides name_addendum).
        insulator_name (str | None, optional): Full override for the insulator trace legend label.
        insulator_name_addendum (str, optional): Addendum for the insulator trace name (overrides name_addendum).

    Raises:
        ValueError: view value is invalid
    """
    if view not in ["profile", "line"]:
        raise ValueError(
            f"Incorrect value for 'view' argument: received {view}, expected 'profile' or 'line'"
        )

    if mode not in ["main", "background"]:
        raise ValueError(
            f"Incorrect value for 'mode' argument: received {mode}, expected 'background' or 'main'"
        )

    if view == "profile":
        fig.update_layout(
            yaxis={"autorange": True},
        )

    else:
        fig.update_layout(
            yaxis={"scaleanchor": "x", "scaleratio": 1},
        )

    span, supports, insulators = self.get_points_for_plot(
        project=True, frame_index=frame_index
    )

    _cable = _apply_name_config(
        cable_trace,
        mode,
        cable_name,
        cable_name_addendum
        if cable_name_addendum is not None
        else name_addendum,
    )
    _support = _apply_name_config(
        support_trace,
        mode,
        support_name,
        support_name_addendum
        if support_name_addendum is not None
        else name_addendum,
    )
    _insulator = _apply_name_config(
        insulator_trace,
        mode,
        insulator_name,
        insulator_name_addendum
        if insulator_name_addendum is not None
        else name_addendum,
    )
    plot_points_2d(
        fig,
        span.points(True),
        _cable,
        view=view,
        hovertext=_build_hover_text(
            span, self.span_names, SPAN_LABEL_PREFIX
        ),
    )
    plot_points_2d(
        fig,
        supports.points(True),
        _support,
        view=view,
        hovertext=_build_hover_text(
            supports, self.support_names, SUPPORT_LABEL_PREFIX
        ),
    )
    plot_points_2d(
        fig,
        insulators.points(True),
        _insulator,
        view=view,
        hovertext=_build_hover_text(
            insulators, self.support_names, SUPPORT_LABEL_PREFIX
        ),
    )

    if hasattr(self.coords_calculator, "obstacle_array"):
        obstacles_dict = self.obstacles_dict(
            project=True, frame_index=frame_index
        )
        for obstacle_name, obstacle_coords in obstacles_dict.items():
            plot_points_2d(
                fig,
                np.array(obstacle_coords),
                TraceProfile(name=obstacle_name),
                view=view,
            )

preview_line3d

preview_line3d(
    fig: Figure,
    view: Literal['full', 'analysis'] = 'full',
    mode: Literal['main', 'background'] = 'main',
    aspect_ratio: dict[str, float] | None = None,
    name_addendum: str = '',
    cable_name: str | None = None,
    cable_name_addendum: str | None = None,
    support_name: str | None = None,
    support_name_addendum: str | None = None,
    insulator_name: str | None = None,
    insulator_name_addendum: str | None = None,
) -> None

Plot 3D of power lines sections

Parameters:

Name Type Description Default

fig

Figure

plotly figure where new traces has to be added

required

view

Literal['full', 'analysis']

full for scale respect view, analysis for compact view. Defaults to "full".

'full'

mode

Literal['main', 'background']

Style mode for the traces. Defaults to "main".

'main'

aspect_ratio

dict[str, float] | None

Custom aspect ratio dictionary with keys 'x', 'y', 'z'. When provided, overrides the layout aspect ratio. Can be computed using compute_aspect_ratio(). Defaults to None.

None

name_addendum

str

String appended to all trace names. Defaults to "".

''

cable_name

str | None

Full override for the cable trace legend label.

None

cable_name_addendum

str

Addendum for the cable trace name (overrides name_addendum).

None

support_name

str | None

Full override for the support trace legend label.

None

support_name_addendum

str

Addendum for the support trace name (overrides name_addendum).

None

insulator_name

str | None

Full override for the insulator trace legend label.

None

insulator_name_addendum

str

Addendum for the insulator trace name (overrides name_addendum).

None

Raises:

Type Description
ValueError

view is not an expected value

Source code in src/mechaphlowers/plotting/plot.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
def preview_line3d(
    self,
    fig: go.Figure,
    view: Literal["full", "analysis"] = "full",
    mode: Literal["main", "background"] = "main",
    aspect_ratio: dict[str, float] | None = None,
    name_addendum: str = "",
    cable_name: str | None = None,
    cable_name_addendum: str | None = None,
    support_name: str | None = None,
    support_name_addendum: str | None = None,
    insulator_name: str | None = None,
    insulator_name_addendum: str | None = None,
) -> None:
    """Plot 3D of power lines sections

    Args:
        fig (go.Figure): plotly figure where new traces has to be added
        view (Literal['full', 'analysis'], optional): full for scale respect view, analysis for compact view. Defaults to "full".
        mode (Literal['main', 'background'], optional): Style mode for the traces. Defaults to "main".
        aspect_ratio (dict[str, float] | None, optional): Custom aspect ratio dictionary with keys 'x', 'y', 'z'.
            When provided, overrides the layout aspect ratio. Can be computed using compute_aspect_ratio(). Defaults to None.
        name_addendum (str, optional): String appended to all trace names. Defaults to "".
        cable_name (str | None, optional): Full override for the cable trace legend label.
        cable_name_addendum (str, optional): Addendum for the cable trace name (overrides name_addendum).
        support_name (str | None, optional): Full override for the support trace legend label.
        support_name_addendum (str, optional): Addendum for the support trace name (overrides name_addendum).
        insulator_name (str | None, optional): Full override for the insulator trace legend label.
        insulator_name_addendum (str, optional): Addendum for the insulator trace name (overrides name_addendum).

    Raises:
        ValueError: view is not an expected value
    """

    view_map = {"full": True, "analysis": False}

    try:
        _auto = view_map[view]
    except KeyError:
        raise ValueError(
            f"{view=} : this argument has to be set to 'full' or 'analysis'"
        )

    if mode not in ["main", "background"]:
        raise ValueError(
            f"Incorrect value for 'mode' argument: received {mode}, expected 'background' or 'main'"
        )

    span, supports, insulators = self.get_points_for_plot(project=False)

    _cable = _apply_name_config(
        cable_trace,
        mode,
        cable_name,
        cable_name_addendum
        if cable_name_addendum is not None
        else name_addendum,
    )
    _support = _apply_name_config(
        support_trace,
        mode,
        support_name,
        support_name_addendum
        if support_name_addendum is not None
        else name_addendum,
    )
    _insulator = _apply_name_config(
        insulator_trace,
        mode,
        insulator_name,
        insulator_name_addendum
        if insulator_name_addendum is not None
        else name_addendum,
    )
    plot_points_3d(
        fig,
        span.points(True),
        _cable,
        hovertext=_build_hover_text(
            span, self.span_names, SPAN_LABEL_PREFIX
        ),
    )
    plot_points_3d(
        fig,
        supports.points(True),
        _support,
        hovertext=_build_hover_text(
            supports, self.support_names, SUPPORT_LABEL_PREFIX
        ),
    )
    plot_points_3d(
        fig,
        insulators.points(True),
        _insulator,
        hovertext=_build_hover_text(
            insulators, self.support_names, SUPPORT_LABEL_PREFIX
        ),
    )

    if hasattr(self.coords_calculator, "obstacle_array"):
        obstacles = self.coords_calculator.compute_obstacle_coords()
        plot_points_3d(
            fig, obstacles.points(True), TraceProfile(name="Obstacles")
        )

    set_layout(fig, auto=_auto, aspect_ratio=aspect_ratio)

reset

reset(balance_engine: BalanceEngine) -> None

Delegate to PositionEngine.reset.

Source code in src/mechaphlowers/plotting/plot.py
513
514
515
def reset(self, balance_engine: BalanceEngine) -> None:
    """Delegate to [`PositionEngine.reset`][mechaphlowers.core.geometry.position_engine.PositionEngine.reset]."""
    self.position_engine.reset(balance_engine)

update

update(notifier: Notifier) -> None

Receive notification from PositionEngine.

The PositionEngine has already refreshed all coordinates before calling this method, so no additional state update is required here.

Source code in src/mechaphlowers/plotting/plot.py
447
448
449
450
451
452
453
def update(self, notifier: Notifier) -> None:
    """Receive notification from [`PositionEngine`][mechaphlowers.core.geometry.position_engine.PositionEngine].

    The `PositionEngine` has already refreshed all coordinates before
    calling this method, so no additional state update is required here.
    """
    logger.debug("Plot engine notified from position engine.")

figure_factory

figure_factory(context=Literal['std', 'blank']) -> Figure

create_figure creates a plotly figure

Returns:

Type Description
Figure

go.Figure: plotly figure

Source code in src/mechaphlowers/plotting/plot.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def figure_factory(context=Literal["std", "blank"]) -> go.Figure:
    """create_figure creates a plotly figure

    Returns:
        go.Figure: plotly figure
    """
    fig = go.Figure()
    if context == "std":
        fig.update_layout(
            autosize=True,
            height=800,
            width=1400,
            scene=dict(
                xaxis_title="X (m)",
                yaxis_title="Y (m)",
                zaxis_title="Z (m)",
                xaxis=dict(
                    backgroundcolor="gainsboro",
                    gridcolor="dimgray",
                ),
                yaxis=dict(
                    backgroundcolor="gainsboro",
                    gridcolor="dimgray",
                ),
                zaxis=dict(
                    backgroundcolor="gainsboro",
                    gridcolor="dimgray",
                ),
            ),
            scene_camera=dict(eye=dict(x=0.9, y=0.1, z=-0.1)),
        )
    elif context == "blank":
        pass
    else:
        raise ValueError(
            f"Unknown context: {context} try 'blank' or 'jupyter'"
        )
    return fig

plot_support_shape

plot_support_shape(
    fig: Figure,
    support_shape: SupportShape,
    structure_name: str | None = None,
    points_name: str | None = None,
) -> None

plot_support_shape enables to plot the support shape on a plotly figure

Parameters:

Name Type Description Default

fig

Figure

plotly figure

required

support_shape

SupportShape

SupportShape object to plot

required

structure_name

str | None

Legend label for the structure trace. Defaults to the support shape name.

None

points_name

str | None

Legend label for the attachment points trace. Defaults to "Attachment points".

None
Source code in src/mechaphlowers/plotting/plot.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def plot_support_shape(
    fig: go.Figure,
    support_shape: SupportShape,
    structure_name: str | None = None,
    points_name: str | None = None,
) -> None:
    """plot_support_shape enables to plot the support shape on a plotly figure

    Args:
        fig (go.Figure): plotly figure
        support_shape (SupportShape): SupportShape object to plot
        structure_name (str | None, optional): Legend label for the structure trace.
            Defaults to the support shape name.
        points_name (str | None, optional): Legend label for the attachment points trace.
            Defaults to "Attachment points".
    """
    _structure_name = (
        structure_name if structure_name is not None else support_shape.name
    )
    _points_name = (
        points_name if points_name is not None else "Attachment points"
    )
    grouped_points, grouped_labels = _group_labels_by_position(
        support_shape.labels_points, support_shape.set_number
    )
    plot_points_3d(
        fig, support_shape.support_points, TraceProfile(name=_structure_name)
    )
    plot_text_3d(
        fig, points=grouped_points, text=grouped_labels, name=_points_name
    )

set_layout

set_layout(
    fig: Figure,
    auto: bool = True,
    aspect_ratio: dict[str, float] | None = None,
) -> None

set_layout

Parameters:

Name Type Description Default

fig

Figure

plotly figure where layout has to be updated

required

auto

bool

Automatic layout based on data (scale respect). False means manual with an aspectratio of x=1, y=.5, z=.5. Only used when aspect_ratio is None. Defaults to True.

True

aspect_ratio

dict[str, float] | None

Custom aspect ratio dictionary with keys 'x', 'y', 'z'. When provided, forces aspectmode to 'manual' and uses these values. When None, behavior is controlled by the auto parameter. Defaults to None.

None

Examples:

1
2
3
4
5
6
7
>>> fig = go.Figure()
>>> # Use default automatic layout
>>> set_layout(fig, auto=True)
>>>
>>> # Use custom aspect ratio (e.g., from compute_aspect_ratio)
>>> custom_aspect = {'x': 0.5, 'y': 0.3, 'z': 10.0}
>>> set_layout(fig, aspect_ratio=custom_aspect)
Source code in src/mechaphlowers/plotting/plot.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
def set_layout(
    fig: go.Figure,
    auto: bool = True,
    aspect_ratio: dict[str, float] | None = None,
) -> None:
    """set_layout

    Args:
        fig (go.Figure): plotly figure where layout has to be updated
        auto (bool, optional): Automatic layout based on data (scale respect). False means manual with an aspectratio of x=1, y=.5, z=.5. Only used when aspect_ratio is None. Defaults to True.
        aspect_ratio (dict[str, float] | None, optional): Custom aspect ratio dictionary with keys 'x', 'y', 'z'. When provided, forces aspectmode to 'manual' and uses these values. When None, behavior is controlled by the auto parameter. Defaults to None.

    Examples:
        >>> fig = go.Figure()
        >>> # Use default automatic layout
        >>> set_layout(fig, auto=True)
        >>>
        >>> # Use custom aspect ratio (e.g., from compute_aspect_ratio)
        >>> custom_aspect = {'x': 0.5, 'y': 0.3, 'z': 10.0}
        >>> set_layout(fig, aspect_ratio=custom_aspect)
    """

    auto = bool(auto)

    if aspect_ratio is not None:
        aspect_mode: str = "manual"
        final_aspect_ratio = _validate_aspect_ratio(aspect_ratio)
        zoom: float = 5
    else:
        aspect_mode = "data" if auto else "manual"
        final_aspect_ratio = {'x': 1, 'y': 0.5, 'z': 0.5}
        zoom = 1 if auto else 5

    fig.update_layout(
        scene={
            'xaxis_title': "X (m)",
            'yaxis_title': "Y (m)",
            'zaxis_title': "Z (m)",
            'aspectratio': final_aspect_ratio,
            'aspectmode': aspect_mode,
            'camera': {
                'up': {'x': 0, 'y': 0, 'z': 1},
                'eye': {'x': -0.5, 'y': -5 / zoom, 'z': 2 / zoom},
            },
        }
    )

utils

Plotting utility functions for data visualization and layout configuration.

compute_aspect_ratio

compute_aspect_ratio(
    *points_objects: Points | SparsePoints,
    x_scale: float = 1.0,
    y_scale: float = 1.0,
    z_scale: float = 1.0,
) -> dict[str, float]

Compute an aspect ratio dictionary for Plotly 3D plots based on data coordinates.

This function analyzes the spatial extent of multiple Points objects (typically spans, supports, and insulators from a power line section) and computes normalized aspect ratios that respect the actual data ranges while allowing custom scaling per axis.

The algorithm:

  1. Concatenates all points from all Points objects into a single array
  2. Computes min/max for each axis (x, y, z)
  3. Calculates the range (max - min) for each axis
  4. Normalizes each range by dividing by the maximum of all three ranges
  5. Applies custom scaling factors to each normalized ratio
  6. Returns a dict suitable for Plotly's aspectratio parameter

Parameters:

Name Type Description Default

*points_objects

Points | SparsePoints

Variable number of Points objects to analyze. Typically these come from PlotEngine.get_points_for_plot() which returns (spans, supports, insulators).

()

x_scale

float

Scaling factor for the x-axis ratio. Defaults to 1.0.

1.0

y_scale

float

Scaling factor for the y-axis ratio. Defaults to 1.0.

1.0

z_scale

float

Scaling factor for the z-axis ratio. Defaults to 1.0. Common use case: z_scale=10 to exaggerate altitude dimension for better visibility.

1.0

Returns:

Type Description
dict[str, float]

dict[str, float]: A dictionary with keys 'x', 'y', 'z' containing the normalized aspect ratios scaled by the provided factors. Each value is a float in the range [0.0001, scale_factor] approximately (exact range depends on data).

Raises:

Type Description
ValueError

If no Points objects are provided, or if all points are NaN.

ValueError

If scale factors are not positive.

Examples:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> from mechaphlowers.plotting.plot import PlotEngine
>>> plot_engine = PlotEngine.builder_from_balance_engine(balance_engine)
>>> spans, supports, insulators = plot_engine.get_points_for_plot()
>>>
>>> # Compute aspect ratio with default scaling (equal for all axes)
>>> aspect = compute_aspect_ratio(spans, supports, insulators)
>>> print(aspect)  # {'x': 0.45, 'y': 0.30, 'z': 1.0}
>>>
>>> # Compute aspect ratio with z-axis exaggeration (common for altitude visualization)
>>> aspect = compute_aspect_ratio(spans, supports, insulators, z_scale=10)
>>> print(aspect)  # {'x': 0.45, 'y': 0.30, 'z': 10.0}
See Also

PlotEngine.preview_line3d: Plotting method that can use this function. Points: The coordinate class used to represent geometric data.

Source code in src/mechaphlowers/plotting/utils.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def compute_aspect_ratio(
    *points_objects: Points | SparsePoints,
    x_scale: float = 1.0,
    y_scale: float = 1.0,
    z_scale: float = 1.0,
) -> dict[str, float]:
    """Compute an aspect ratio dictionary for Plotly 3D plots based on data coordinates.

    This function analyzes the spatial extent of multiple Points objects (typically spans,
    supports, and insulators from a power line section) and computes normalized aspect ratios
    that respect the actual data ranges while allowing custom scaling per axis.

    The algorithm:

    1. Concatenates all points from all Points objects into a single array
    2. Computes min/max for each axis (x, y, z)
    3. Calculates the range (max - min) for each axis
    4. Normalizes each range by dividing by the maximum of all three ranges
    5. Applies custom scaling factors to each normalized ratio
    6. Returns a dict suitable for Plotly's aspectratio parameter

    Args:
        *points_objects: Variable number of Points objects to analyze. Typically these
            come from PlotEngine.get_points_for_plot() which returns (spans, supports, insulators).
        x_scale (float, optional): Scaling factor for the x-axis ratio. Defaults to 1.0.
        y_scale (float, optional): Scaling factor for the y-axis ratio. Defaults to 1.0.
        z_scale (float, optional): Scaling factor for the z-axis ratio. Defaults to 1.0.
            Common use case: z_scale=10 to exaggerate altitude dimension for better visibility.

    Returns:
        dict[str, float]: A dictionary with keys 'x', 'y', 'z' containing the normalized
            aspect ratios scaled by the provided factors. Each value is a float in the range
            [0.0001, scale_factor] approximately (exact range depends on data).

    Raises:
        ValueError: If no Points objects are provided, or if all points are NaN.
        ValueError: If scale factors are not positive.

    Examples:
        >>> from mechaphlowers.plotting.plot import PlotEngine
        >>> plot_engine = PlotEngine.builder_from_balance_engine(balance_engine)
        >>> spans, supports, insulators = plot_engine.get_points_for_plot()
        >>>
        >>> # Compute aspect ratio with default scaling (equal for all axes)
        >>> aspect = compute_aspect_ratio(spans, supports, insulators)
        >>> print(aspect)  # {'x': 0.45, 'y': 0.30, 'z': 1.0}
        >>>
        >>> # Compute aspect ratio with z-axis exaggeration (common for altitude visualization)
        >>> aspect = compute_aspect_ratio(spans, supports, insulators, z_scale=10)
        >>> print(aspect)  # {'x': 0.45, 'y': 0.30, 'z': 10.0}

    See Also:
        PlotEngine.preview_line3d: Plotting method that can use this function.
        Points: The coordinate class used to represent geometric data.
    """
    # Input validation
    if not points_objects:
        raise ValueError("At least one Points object must be provided")

    if x_scale <= 0 or y_scale <= 0 or z_scale <= 0:
        raise ValueError(
            f"Scale factors must be positive; got x_scale={x_scale}, y_scale={y_scale}, z_scale={z_scale}"
        )

    # Concatenate all points from all Points objects
    all_points_list = []
    for points_obj in points_objects:
        if not isinstance(points_obj, Points | SparsePoints):
            raise TypeError(
                f"Expected Points object, got {type(points_obj).__name__}"
            )
        # Get flat array of shape (N, 3) where each row is [x, y, z]
        points_array = points_obj.points(stack=False)
        all_points_list.append(points_array)

    all_points = np.vstack(all_points_list)

    if all_points.size == 0:
        raise ValueError(
            "At least one Points object must contain at least one point to compute aspect ratio"
        )

    # Extract x, y, z coordinates
    xs = all_points[:, 0]
    ys = all_points[:, 1]
    zs = all_points[:, 2]

    # Compute ranges using nanmin/nanmax to handle NaN values
    x_range = np.nanmax(xs) - np.nanmin(xs)
    y_range = np.nanmax(ys) - np.nanmin(ys)
    z_range = np.nanmax(zs) - np.nanmin(zs)

    # Handle edge case where all values in an axis are NaN
    if np.isnan(x_range) or np.isnan(y_range) or np.isnan(z_range):
        raise ValueError(
            "Cannot compute aspect ratio because at least one axis has only NaN values"
        )

    # Normalize by the maximum range
    max_range = max(x_range, y_range, z_range)
    if max_range == 0:
        raise ValueError(
            "Data has zero spatial extent; cannot compute aspect ratio"
        )

    # Compute normalized ranges and clamp zero-extent axes to a small epsilon
    norm_x = (
        x_range / max_range if x_range > 0 else options.graphics.aspect_epsilon
    )
    norm_y = (
        y_range / max_range if y_range > 0 else options.graphics.aspect_epsilon
    )
    norm_z = (
        z_range / max_range if z_range > 0 else options.graphics.aspect_epsilon
    )

    aspect_x = norm_x * x_scale
    aspect_y = norm_y * y_scale
    aspect_z = norm_z * z_scale

    return {"x": float(aspect_x), "y": float(aspect_y), "z": float(aspect_z)}