Plot a surface at constant value (like a 2d contour).

    :param data: 3d numpy array
    :param float level: value where the surface should lie
    :param color: color of the surface, although it can be an array, the length is difficult to predict beforehand,
                  if per vertex color are needed, it is better to set them on the returned mesh afterwards.
    :param bool wireframe: draw lines between the vertices
    :param bool surface: draw faces/triangles between the vertices
    :param bool controls: add controls to change the isosurface
    :param extent: list of [[xmin, xmax], [ymin, ymax], [zmin, zmax]] values that define the bounding box of the mesh,
                   otherwise the viewport is used
    :return: :any:`Mesh`

def plot_isosurface(data, level=None, color=default_color, wireframe=True, surface=True, controls=True, extent=None):
    """Plot a surface at constant value (like a 2d contour).

    :param data: 3d numpy array
    :param float level: value where the surface should lie
    :param color: color of the surface, although it can be an array, the length is difficult to predict beforehand,
                  if per vertex color are needed, it is better to set them on the returned mesh afterwards.
    :param bool wireframe: draw lines between the vertices
    :param bool surface: draw faces/triangles between the vertices
    :param bool controls: add controls to change the isosurface
    :param extent: list of [[xmin, xmax], [ymin, ymax], [zmin, zmax]] values that define the bounding box of the mesh,
                   otherwise the viewport is used
    :return: :any:`Mesh`
    """
    if level is None:
        level = np.median(data)
    if hasattr(skimage.measure, 'marching_cubes_lewiner'):
        values = skimage.measure.marching_cubes_lewiner(data, level)
    else:
        values = skimage.measure.marching_cubes(data, level)  # pylint: disable=no-member
    verts, triangles = values[:2]  # version 0.13 returns 4 values, normals, values
    # in the future we may want to support normals and the values (with colormap)
    # and require skimage >= 0.13
    x, y, z = verts.T

    # Rescale coordinates to given limits
    if extent:
        xlim, ylim, zlim = extent
        x = x * np.diff(xlim) / (data.shape[0] - 1) + xlim[0]
        y = y * np.diff(ylim) / (data.shape[1] - 1) + ylim[0]
        z = z * np.diff(zlim) / (data.shape[2] - 1) + zlim[0]
        _grow_limits(*extent)

    mesh = plot_trisurf(x, y, z, triangles=triangles, color=color)
    if controls:
        vmin, vmax = np.percentile(data, 1), np.percentile(data, 99)
        step = (vmax - vmin) / 250
        level_slider = ipywidgets.FloatSlider(value=level, min=vmin, max=vmax, step=step, icon='eye')
        recompute_button = ipywidgets.Button(description='update')
        controls = ipywidgets.HBox(children=[level_slider, recompute_button])
        current.container.children += (controls,)

        def recompute(*_ignore):
            level = level_slider.value
            recompute_button.description = "updating..."
            if hasattr(skimage.measure, 'marching_cubes_lewiner'):
                values = skimage.measure.marching_cubes_lewiner(data, level)
            else:
                values = skimage.measure.marching_cubes(data, level)  # pylint: disable=no-member
            verts, triangles = values[:2]  # version 0.13 returns 4 values, normals, values
            # in the future we may want to support normals and the values (with colormap)
            # and require skimage >= 0.13
            x, y, z = verts.T
            with mesh.hold_sync():
                mesh.x = x
                mesh.y = y
                mesh.z = z
                mesh.triangles = triangles.astype(dtype=np.uint32)
                recompute_button.description = "update"

        recompute_button.on_click(recompute)
    return mesh