Plotly graphs#

Feature highlight#

The user interface (UI) of a solution app can display both input data (such as strings, integers, floats, and files) and output data (such as 1D/2D/3D graphics, 3D viewers, and images).

In this example, you learn how to:

  • Write business logic that computes the transcendental butterfly curve with NumPy.

  • Store the curve data in typed step fields and expose a transaction method that (re)computes it.

  • Render the data with a Dash dcc.Graph component and customize its layout.

  • Wire a callback so UI controls trigger the backend computation and refresh the plot.

When you complete this example, you can expect the following output in the solution UI:

../../_images/usage_plotly_graph_integration_output.png

Prerequisites#

To render any Plotly-powered data visualization, you need the dcc.Graph component from the Dash Core Components library. For more information, see the Plotly Dash documentation.

Coding#

To display a set of points in a two-dimensional Plotly graph, work through the following sequence of sections.

Logic#

Write the business logic.

Key concept — Business logic

Business logic is plain Python: it has no dependency on SAF and can be developed and tested on its own before it is wired into a step.

Compute the curve coordinates

In the solution/logic folder, the butterfly_curve.py module exposes a compute_butterfly_curve function that generates the set of points of the transcendental butterfly curve for the given parameters.

solution/logic/butterfly_curve.py#
def compute_butterfly_curve(
    wing_frequency: float = 4.0,
    wing_amplitude: float = 2.0,
    twist: float = 12.0,
    exponent: int = 5,
    revolutions: float = 12.0,
    points: int = 10000,
) -> tuple[list[float], list[float], list[float]]:
    """
    Compute the coordinates of the transcendental butterfly curve.

    The curve is defined in polar-like form by

    .. math::

        r(t) = e^{\\cos t} - a \\cos(f t) - \\sin^{n}(t / \\tau)

    with :math:`x = r(t) \\sin t` and :math:`y = r(t) \\cos t`, where :math:`a` is the wing
    amplitude, :math:`f` the wing frequency, :math:`n` the exponent and :math:`\\tau` the
    twist. The default values reproduce the curve described at
    https://en.wikipedia.org/wiki/Butterfly_curve_(transcendental).

    Parameters
    ----------
    wing_frequency : float, default: 4.0
        Frequency of the cosine term that shapes the wings. Higher values add wings.
    wing_amplitude : float, default: 2.0
        Amplitude of the cosine term that shapes the wings. A value of ``0`` removes them.
    twist : float, default: 12.0
        Divider of the parameter in the sine term that twists the wings. It cannot be zero.
    exponent : int, default: 5
        Exponent applied to the sine term. An integer is required because the sine term
        takes negative values.
    revolutions : float, default: 12.0
        Number of half-turns to draw. The parameter ``t`` spans ``[0, revolutions * pi]``.
        The curve is closed for even values.
    points : int, default: 10000
        Number of points used to discretize the curve.

    Returns
    -------
    tuple[list[float], list[float], list[float]]
        Three lists of ``points`` values:

        - the ``x`` coordinates of the curve,
        - the ``y`` coordinates of the curve,
        - the distance of each point to the origin, that is ``sqrt(x ** 2 + y ** 2)``.

    Raises
    ------
    ValueError
        If ``twist`` is zero or if ``exponent`` is not an integer.

    Examples
    --------
    >>> x, y, distance = compute_butterfly_curve(points=5)
    >>> len(x), len(y), len(distance)
    (5, 5, 5)
    """
    if twist == 0:
        raise ValueError("The twist parameter cannot be zero.")
    if exponent != int(exponent):
        raise ValueError("The exponent parameter must be an integer.")

    t = np.linspace(0, revolutions * np.pi, points)
    r = np.exp(np.cos(t)) - wing_amplitude * np.cos(wing_frequency * t) - np.sin(t / twist) ** int(exponent)
    x = r * np.sin(t)
    y = r * np.cos(t)

    return x.tolist(), y.tolist(), np.abs(r).tolist()

Backend#

Create the solution definition.

Key concept — Typed field

A field is a class attribute with a type annotation and a default value. SAF uses the annotation to persist the field, expose it through the REST API, and surface it to the frontend through the DashClient.

Define the step fields

Let x_coords and y_coords be the x and y coordinates of a set of points you want to display in the solution UI. Declare these as step fields, alongside the parameters that control the shape of the curve.

solution/basic_step.py#
result_files: list[EntityHandle] = []
log_file: EntityHandle = NO_ENTITY

# Table data
data_dict: dict[str, Any] = {}
table_flag: bool = False

# Butterfly curve parameters
x_coords: list[float] = []
y_coords: list[float] = []
distance: list[float] = []
butterfly_wing_frequency: float = 4.0
butterfly_wing_amplitude: float = 2.0
butterfly_twist: float = 12.0
butterfly_exponent: int = 5
butterfly_revolutions: float = 12.0

Key concept — Transaction method

A transaction method is the only place where step fields are read and written. Any method that touches a field must be decorated with @transaction, which declares the fields it downloads (reads) and uploads (writes) through its StepSpec.

Add a transaction method to compute the curve

Create a compute_butterfly_curve transaction method that invokes the compute_butterfly_curve business logic function and stores the result in the step fields.

solution/basic_step.py#
@transaction(
    self=StepSpec(
        upload=["x_coords", "y_coords", "distance"],
        download=[
            "butterfly_wing_frequency",
            "butterfly_wing_amplitude",
            "butterfly_twist",
            "butterfly_exponent",
            "butterfly_revolutions",
        ],
    )
)
def compute_butterfly_curve(self) -> None:
    """
    Compute the transcendental butterfly curve from the step parameters.

    The ``butterfly_*`` fields are downloaded and passed to the ``compute_butterfly_curve``
    business logic function. The resulting coordinates and distances to the origin are
    uploaded to the ``x_coords``, ``y_coords``, and ``distance`` fields, which the
    Plotly graph page displays.
    """
    self.x_coords, self.y_coords, self.distance = compute_butterfly_curve(
        wing_frequency=self.butterfly_wing_frequency,
        wing_amplitude=self.butterfly_wing_amplitude,
        twist=self.butterfly_twist,
        exponent=self.butterfly_exponent,
        revolutions=self.butterfly_revolutions,
    )

Important

This example deliberately uses a blocking transaction method rather than a long-running one. The business logic was tested beforehand and computes the curve in less than a second, so blocking the UI for that duration is safe and keeps the code simpler.

Nothing prevents you from converting it to a long-running transaction method by adding the @long_running decorator if your own computation takes longer. For more information, see Long-running transaction streaming events.

Frontend#

Expose the solution definition in the UI.

Initialize the Plotly graph

In the ui/pages folder, the layout function of the page adds a dcc.Graph component and passes it the figure built from the step fields.

ui/pages/basic/plot_page.py#
dmc.Box(
    [
        dmc.LoadingOverlay(
            id="graph_loading",
            visible=False,
            loaderProps={"type": "bars", "color": "#0A76DB", "size": "lg"},
            overlayProps={"radius": "sm", "blur": 2},
            zIndex=10,
        ),
        dcc.Graph(
            id="graph",
            figure=_butterfly_figure(step),
        ),
    ],
    pos="relative",
    style={
        "width": "fit-content",
        "margin": "0 auto",
    },
),

Key concept — Figure layout

The layout key of a Plotly figure controls its size, axes and margins. Plotly enables many kinds of customization. For more information, see its documentation.

Build the figure

The _butterfly_figure function builds the figure from the step fields. The points are colored by their distance to the origin through the color and colorscale marker options, the width and height options control the size of the figure, the margin option adjusts the margins relative to the graph box, and the xaxis and yaxis options display the axes with an equal aspect ratio.

ui/pages/basic/plot_page.py#
def _butterfly_figure(step: BasicStep) -> dict:
    """
    Build the Plotly figure of the butterfly curve.

    The points are colored by their distance to the origin with the ``Bluered`` color scale,
    and the axes share the same scale so that the curve is not distorted.

    Parameters
    ----------
    step : BasicStep
        Step holding the coordinates of the curve and the distance of each point to the
        origin.

    Returns
    -------
    dict
        Figure to pass to the ``figure`` property of the ``dcc.Graph`` component, with its
        ``data`` and ``layout`` keys.
    """
    return {
        "data": [
            {
                "type": "scattergl",
                "x": step.x_coords,
                "y": step.y_coords,
                "mode": "markers",
                "marker": {
                    "size": 3,
                    "color": step.distance,
                    "colorscale": "Bluered",
                    "showscale": True,
                    "colorbar": {"title": {"text": "Distance<br>to origin"}},
                },
                "hovertemplate": "x = %{x:.3f}<br>y = %{y:.3f}<br>r = %{marker.color:.3f}<extra></extra>",
            },
        ],
        "layout": {
            "width": 700,
            "height": 600,
            "xaxis": {
                "title": {"text": "x"},
                "zeroline": True,
                "zerolinecolor": "rgba(0,0,0,0.4)",
                "gridcolor": "rgba(0,0,0,0.1)",
            },
            "yaxis": {
                "title": {"text": "y"},
                "zeroline": True,
                "zerolinecolor": "rgba(0,0,0,0.4)",
                "gridcolor": "rgba(0,0,0,0.1)",
                "scaleanchor": "x",
                "scaleratio": 1,
            },
            "paper_bgcolor": "rgba(0,0,0,0)",
            "plot_bgcolor": "rgba(0,0,0,0)",
            "margin": {
                "l": 60,
                "r": 10,
                "b": 50,
                "t": 20,
            },
        },
    }
Add controls to tune the curve parameters

Add a dmc.Slider component for each parameter so the user can play with the shape of the curve. Each slider is initialized with the value currently stored in the step.

ui/pages/basic/plot_page.py#
def _butterfly_parameters(step: BasicStep) -> list[html.Div]:
    """
    Build the sliders controlling the butterfly curve parameters.

    Each slider is identified by the name of the step field it controls and is initialized
    with the value currently stored in the step.

    Parameters
    ----------
    step : BasicStep
        Step holding the current value of each butterfly curve parameter.

    Returns
    -------
    list[html.Div]
        One ``html.Div`` per parameter, each containing a label and a ``dmc.Slider``.
    """
    return [
        html.Div(
            [
                dmc.Text(parameter["label"], size="sm", mt="md"),
                dmc.Slider(
                    id=parameter["id"],
                    value=getattr(step, parameter["id"]),
                    min=parameter["min"],
                    max=parameter["max"],
                    step=parameter["step"],
                    precision=1,
                    labelAlwaysOn=True,
                    mt="lg",
                ),
            ]
        )
        for parameter in BUTTERFLY_PARAMETERS
    ]

Key concept — Callback

A callback is a Dash-decorated function that fires in response to a UI event. In a SAF solution, callbacks reach the backend through project.steps.<step_name>, read or write fields, and invoke transaction methods. No manual HTTP calls are needed.

Trigger the backend computation from the frontend

Write a callback that updates the curve parameter fields, invokes compute_butterfly_curve, and refreshes the figure with the new coordinates.

ui/pages/basic/plot_page.py#
@callback(
    Output("graph", "figure"),
    Output("graph_loading", "visible", allow_duplicate=True),
    Input("butterfly_wing_frequency", "value"),
    Input("butterfly_wing_amplitude", "value"),
    Input("butterfly_twist", "value"),
    Input("butterfly_exponent", "value"),
    Input("butterfly_revolutions", "value"),
    State("url", "pathname"),
)
def draw_butterfly_curve(
    wing_frequency: float,
    wing_amplitude: float,
    twist: float,
    exponent: float,
    revolutions: float,
    project: ExamplesSolution,
) -> tuple[dict, bool]:
    """
    Compute the butterfly curve and refresh the figure.

    On the initial call, the curve is computed with the parameters already stored in the
    step. On any other call, the values of the sliders are written to the step fields
    before the ``compute_butterfly_curve`` transaction method is invoked.

    Parameters
    ----------
    wing_frequency : float
        Value of the wing frequency slider.
    wing_amplitude : float
        Value of the wing amplitude slider.
    twist : float
        Value of the twist slider.
    exponent : float
        Value of the exponent slider. It is cast to an integer for the step field.
    revolutions : float
        Value of the revolutions slider.
    project : ExamplesSolution
        Solution instance injected by the ``DashClient`` from the URL of the page.

    Returns
    -------
    tuple[dict, bool]
        The figure of the computed curve and ``False`` to hide the loading overlay.
    """
    step = project.steps.basic_step

    if ctx.triggered_id is None and not step.distance:
        print("Computing butterfly curve for the first time...")
        step.compute_butterfly_curve()
    else:
        print(
            f"Computing butterfly curve for parameters: wing_frequency={wing_frequency}, "
            f"wing_amplitude={wing_amplitude}, twist={twist}, exponent={exponent}, revolutions={revolutions}"
        )
        step.butterfly_wing_frequency = wing_frequency
        step.butterfly_wing_amplitude = wing_amplitude
        step.butterfly_twist = twist
        step.butterfly_exponent = int(exponent)
        step.butterfly_revolutions = revolutions
        step.compute_butterfly_curve()

    print(f"Computed butterfly curve with {len(step.x_coords)} points.")
    return _butterfly_figure(step), False

Now that your implementation is complete, continue to the Testing section.

Testing#

Finally, test your implementation to confirm it works as expected.

Run the solution and compare your results with the results shown in the Feature highlight section.