4. Interactive visualization of CAE results

Once a CAE solver has produced a result file, the analytical value of that data depends entirely on how effectively it can be inspected. A raw .vtu, .vtk, or .foam file contains nodal coordinates, connectivity, and field arrays, but no interpretation. This section focuses on turning those arrays into interactive contour plots, vector fields, and time-dependent animations inside a Streamlit application, using Plotly for lightweight 2D/3D rendering and PyVista for full volumetric mesh visualization.

Choosing the Right Rendering Engine

Plotly and PyVista serve different visualization needs, and selecting the wrong one leads to either poor performance or missing capability. Plotly excels at 2D contour maps, scatter-based vector overlays, and browser-native interactivity with hover tooltips, zoom, and pan. PyVista wraps VTK and handles unstructured meshes, iso-surfaces, streamlines, and volume rendering with GPU acceleration. A practical rule: if the result can be expressed as a structured grid slice or a 2D field, use Plotly; if it requires the actual mesh topology, use PyVista.

Requirement Plotly PyVista
2D contour / heatmap Native, fast Possible via screenshot
Unstructured mesh rendering Limited Full support
Vector field (quiver/arrows) 2D and 3D quiver Glyphs, streamlines
Time animation Frame-based slider Off-screen render loop
Browser interactivity Built-in Requires stpyvista or export

Contour Plots from Nodal Results

Consider a steady-state thermal analysis of a rectangular heat sink base, 120 mm × 80 mm, meshed with 2 mm quadrilateral elements. The solver exports nodal temperatures ranging from 28 °C at the inlet edge to 94 °C near the chip footprint. To visualize this in Streamlit, load the nodal coordinates and temperature array, then reshape into a structured grid:

import numpy as np
import plotly.graph_objects as go
import streamlit as st

# x, y: 1D coordinate arrays (61 x 41 nodes)
# T: temperature array shape (41, 61)
X, Y = np.meshgrid(x, y)

fig = go.Figure(data=go.Contour(
    z=T, x=x, y=y,
    colorscale="Turbo",
    contours=dict(showlabels=True, labelfont=dict(size=10)),
    colorbar=dict(title="Temperature (°C)")
))
fig.update_layout(
    xaxis_title="X (mm)", yaxis_title="Y (mm)",
    yaxis_scaleanchor="x"
)
st.plotly_chart(fig, use_container_width=True)

The yaxis_scaleanchor="x" setting preserves aspect ratio, which is critical for physical interpretation — a distorted contour can mislead hotspot assessment. The Turbo colorscale provides perceptually uniform color transitions, preferable to rainbow for quantitative reading. Contour labels should be enabled when the audience needs to extract numeric values without hovering.

Vector Fields: Velocity and Flux

Vector fields appear in CFD (velocity), electromagnetics (E-field), and structural analysis (displacement). Plotly's go.Cone or go.Scatter with quiver-style line segments can render 2D vectors. For a 2D CFD channel flow with velocity components u and v on a coarse 20 × 10 grid, subsample the field to avoid visual clutter:

skip = 3  # plot every 3rd vector
fig = go.Figure()
fig.add_trace(go.Scatter(
    x=X[::skip, ::skip].ravel(),
    y=Y[::skip, ::skip].ravel(),
    mode="markers",
    marker=dict(
        symbol="arrow", size=12, angle=np.degrees(
            np.arctan2(V[::skip,::skip], U[::skip,::skip])
        ),
        color=np.sqrt(U[::skip,::skip]**2 + V[::skip,::skip]**2),
        colorscale="Blues", showscale=True,
        colorbar=dict(title="|V| (m/s)")
    )
))

For 3D vector fields, PyVista's glyph filter scales arrows by magnitude and orients them along the vector. Streamlines via streamlines_from_source reveal flow topology that individual arrows cannot convey. In a Streamlit context, wrap PyVista rendering with stpyvista to embed the interactive 3D scene directly in the browser.

Animating Transient Results

Transient analyses produce a sequence of result files, typically named result_0000.vtu through result_0100.vtu. Plotly animations use a frame list with a slider:

frames = []
for i, T_frame in enumerate(temperature_history):
    frames.append(go.Frame(
        data=[go.Contour(z=T_frame, x=x, y=y,
                         colorscale="Turbo", zmin=T_min, zmax=T_max)],
        name=f"t={i*dt:.2f}s"
    ))

fig = go.Figure(data=frames[0].data, frames=frames)
fig.update_layout(
    updatemenus=[dict(type="buttons", buttons=[
        dict(label="Play", method="animate",
             args=[None, dict(frame=dict(duration=80, redraw=True))])
    ])],
    sliders=[dict(steps=[
        dict(method="animate", label=f.name,
             args=[[f.name], dict(mode="immediate")])
        for f in frames
    ])]
)

Two details matter for correct interpretation. First, fix zmin and zmax across all frames; otherwise the colorscale rescales per frame and a growing hotspot appears static. Second, set a consistent duration that matches the physical time step ratio — if the solver used adaptive time stepping, map frame duration to actual simulation time, not frame index.

When animating large transient datasets, precompute downsampled frames on the server and cache them with @st.cache_data. Sending 500 full-resolution frames to the browser will stall the session.

Result Interpretation Workflow

The diagram below summarizes how a CAE result file flows through the visualization pipeline, including the decision point between Plotly and PyVista and the key parameter controls at each stage.

CAE Result File .vtu / .vtk / .foam nodal fields + mesh Structured grid? Yes Plotly Contour / Heatmap 2D Quiver / Frames No PyVista Iso-surface / Glyph Streamlines / Volume Critical Parameter Controls Fixed zmin / zmax Vector subsampling Frame duration = Δt Aspect ratio lock Streamlit Interactive Dashboard hover · zoom · slider · play/pause · field selector

Verification and Common Pitfalls

After building the visualization, verify it against the solver's own report. Check that the maximum contour value matches the solver's reported peak, and that the location of that peak corresponds to the expected physical hotspot. If the contour maximum is lower than the solver output, the field array may have been read with the wrong node ordering or the mesh was reshaped incorrectly.

Common failure symptoms and their causes:

  • Mirrored or rotated contour: the y-axis is inverted relative to the solver coordinate system. Check whether the solver uses a bottom-left or top-left origin and set yaxis_autorange="reversed" accordingly.
  • Animation flickers between frames: colorscale range is recalculated per frame. Fix zmin/zmax explicitly.
  • Vector arrows all point the same direction: the angle array was computed in radians but passed as degrees, or u and v were swapped during array reshaping.
  • PyVista plot blank in browser: the mesh was not passed through stpyvista, or the VTK off-screen rendering backend is unavailable in the deployment environment.
Always confirm the coordinate system convention (right-handed vs. left-handed, origin location, axis orientation) before plotting. A visualization that is geometrically mirrored will produce physically wrong conclusions even if the color values are correct.

With contours, vectors, and animations working together in a single Streamlit page, the analyst can move from a global temperature map to a local velocity vector inspection to a transient hotspot evolution without leaving the browser. The next step is to make these views respond to user-selected result files and field variables, which is covered when we address dynamic data loading and widget-driven state management.