Build a Static Surface¶
This tutorial builds the examples/surface_plot/static_surface_visualizer.py
pattern from scratch. Use it as the default first authoring tutorial when you
want to understand the Field + ViewSpec + PanelSpec model without adding a
live backend.
By the end, you will have:
- one
Field - one
GridGeometry - one
SurfaceViewSpec - optional controls bound through
StateBinding - an explicit
LayoutSpecandPanelSpecsetup for the visible panels
1. Create a Field and Geometry¶
grid_field() is a convenience wrapper that creates both a Field and a GridGeometry from a 2-D numpy array and coordinate vectors:
import numpy as np
from compneurovis import grid_field
x = np.linspace(-3.0, 3.0, 120, dtype=np.float32)
y = np.linspace(-3.0, 3.0, 120, dtype=np.float32)
X, Y = np.meshgrid(x, y)
Z = (np.sinc(np.sqrt(X**2 + Y**2)) * 2.0).astype(np.float32)
field, geometry = grid_field(
field_id="sinc-height",
values=Z, # shape (len(y), len(x))
x_coords=x,
y_coords=y,
x_dim="x",
y_dim="y",
)
Omit geometry from the next steps if your view doesn't need explicit grid coordinates — SurfaceViewSpec can infer a unit grid.
2. Define Controls (optional)¶
Controls appear in the right panel and can drive ViewSpec properties via StateBinding:
from compneurovis import ChoiceValueSpec, ControlPresentationSpec, ControlSpec, ScalarValueSpec
controls = {
"surface_alpha": ControlSpec(
id="surface_alpha",
label="Surface alpha",
value_spec=ScalarValueSpec(default=0.9, min=0.1, max=1.0, value_type="float"),
presentation=ControlPresentationSpec(kind="slider", steps=90),
),
"background_color": ControlSpec(
id="background_color",
label="Background",
value_spec=ChoiceValueSpec(default="white", options=("black", "white", "gray")),
),
}
value_spec describes the value contract. presentation is only a widget hint.
3. Build the ViewSpec¶
SurfaceViewSpec references the field and geometry by id and accepts static values or StateBinding placeholders:
from compneurovis import SurfaceViewSpec, StateBinding
surface_view = SurfaceViewSpec(
id="surface",
title="sinc surface",
field_id=field.id,
geometry_id=geometry.id,
color_map="fire",
render_axes=True,
axes_in_middle=True,
axis_labels=("x", "y", "height"),
surface_alpha=StateBinding("surface_alpha"), # driven by the control above
background_color=StateBinding("background_color"), # driven by the control above
)
Any ViewSpec property that accepts a StateBinding will resolve to the current control value at render time. Properties without a binding use the literal value you provide.
For color_map, built-in strings such as "fire" still work, and installs
with pip install -e ".[matplotlib]" can also use names like "mpl:viridis"
or explicit ramps such as "mpl-ramp:#245aa8:#9e2a2b".
4. Assemble and Run¶
from compneurovis import PanelSpec, build_surface_app, run_app
app = build_surface_app(
field=field,
geometry=geometry,
title="sinc surface",
surface_view=surface_view,
controls=controls,
panels=(
PanelSpec(
id="surface-panel",
kind="view_3d",
view_ids=("surface",),
camera_distance=120.0,
),
PanelSpec(
id="controls-panel",
kind="controls",
control_ids=tuple(controls.keys()),
),
),
panel_grid=(("surface-panel",), ("controls-panel",)),
)
run_app(app)
build_surface_app() builds the Scene and AppSpec for you. There is no Session — the field values are static.
Use a 3-D PanelSpec when you want to tune host-level camera settings such as
the initial distance without changing what the SurfaceViewSpec renders.
Adding a Line Plot Slice¶
To add a cross-section plot driven by a slider, define a GridSliceOperatorSpec
and let both the 3-D host and the line plot consume that operator:
from compneurovis import ChoiceValueSpec, ControlPresentationSpec, ControlSpec, GridSliceOperatorSpec, LinePlotViewSpec, ScalarValueSpec
controls["slice_axis"] = ControlSpec(
id="slice_axis",
label="Slice axis",
value_spec=ChoiceValueSpec(default="x", options=("x", "y")),
)
controls["slice_pos"] = ControlSpec(
id="slice_pos",
label="Slice Y",
value_spec=ScalarValueSpec(default=0.0, min=-3.0, max=3.0, value_type="float"),
presentation=ControlPresentationSpec(kind="slider", steps=120),
)
slice_operator = GridSliceOperatorSpec(
id="surface-slice",
field_id=field.id,
geometry_id=geometry.id,
axis_state_key="slice_axis",
position_state_key="slice_pos",
)
line_view = LinePlotViewSpec(
id="line",
operator_id=slice_operator.id,
)
Then pass line_views=(line_view,) and operators={slice_operator.id: slice_operator}
to build_surface_app(...), and attach the operator to the 3-D panel through
PanelSpec.operator_ids, for example:
line_views accepts any number of LinePlotViewSpecs. The frontend mounts one
framed plot host per listed view, in the order you pass them.
app = build_surface_app(
field=field,
geometry=geometry,
surface_view=surface_view,
line_views=(line_view,),
operators={slice_operator.id: slice_operator},
controls=controls,
panels=(
PanelSpec(
id="surface-panel",
kind="view_3d",
view_ids=("surface",),
operator_ids=(slice_operator.id,),
),
PanelSpec(
id="line-panel",
kind="line_plot",
view_ids=("line",),
),
PanelSpec(
id="controls-panel",
kind="controls",
control_ids=tuple(controls.keys()),
),
),
panel_grid=(("surface-panel", "line-panel"), ("controls-panel",)),
)
See examples/surface_plot/surface_cross_section_visualizer.py for the full pattern.
Next steps:
- Read Build a replay app if your data already exists as frames.
- Read View and Layout Model if you want the composition model behind
ViewSpec,PanelSpec, andLayoutSpec.