.. _vectorplots:

*********************
Vector Plots Tutorial
*********************

Vector quantities calculated by ``up4`` can be visualised by the `up4.Plotter2D`
class. For ease of use, this class takes 3D grids as an input, and will 
appropriately depth-average or slice the data for you. The `axis` argument to
methods defined in this class refers to which axis the depth-averaging/ slicing
will happen along (cylindrical equivalents in brackets):

- 0: slice along x(r)-axis, the plane for the data is y-z (:math:`\theta`-z).
- 1: slice along y(:math:`\theta`)-axis, the plane for the data is x-z (r-z).
- 2: slice along z(z)-axis, the plane for the data is x-y (r-:math:`\theta`).

Available vector plotting options are:

- `up4.Plotter2D.quiver_plot`
- `up4.Plotter2D.unit_vector_plot`

In the proceeding sections, we will focus on the visualisation of vector fields
from a rotating drum, that rotates about the x-axis. The data for this can be found in
the ``tests/fixtures`` directory of ``up4``. To avoid unnecessary code repitition,
each example assumes that above it is the following code block:

.. code-block:: python

    import up4
    drum_hdf5_file = '/path/to/hdf5/file'
    data = up4.Data(drum_hdf5_file)
    grid_car = up4.Grid(data=data, num_cells=[20, 20, 20])

Plotting with a cylindrical grid happens in an identical way, but using a Cartesian
grid for this example maintains the shape for the drum's free surface so that each plot
is easier to understand.

Quiver Plot
===========
We can visualise the depth-averaged velocity field, a vector quantity using 
`up4.Plotter2D.quiver_plot`:

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    plot_layout = dict(
        width=800, height=800,
        xaxis=dict(title="y position (m)"),
        yaxis=dict(title="z position (m)")
    )
    fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        layout = plot_layout
    ) # depth-average the vector grid 

Alternatively, we could view just a slice of the y-z plane:

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    # select the y-z plane located at index 2, note that index is a required
    # argument for "plane" selection.
    fig = plotter.quiver_plot(
        axis = axis, 
        selection = "plane", 
        index = 2, 
        layout = plot_layout
    ) # reuse the same plot_layout as before 

Note that these two examples could be combined into one, as a `up4.Plotter2D` instance
can generate multiple figures based on the input grid provided to it since its plotting
methods return a `plotly.graph_objects.Figure` instance.

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    plot_layout = dict(
        width=800, height=800,
        xaxis=dict(title="y position (m)"),
        yaxis=dict(title="z position (m)")
    )

    depth_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        layout = plot_layout
    )  

    # The same Plotter2D instance as above is used here to generate a different figure
    # but with the same layout!
    plane_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "plane", 
        index = 2, 
        layout = plot_layout
    )  


Arrow Scaling
-------------
Not all vector fields were made equal, some contain a large range of values that will
naturally make the resulting plot hard to read due to the difference in scales present.
We can rescale the arrows to improve the graph readability. The scaling options provided
by ``up4`` as optional arguments to its vector plotting functionality are as follows:

- "min": arrows below the "min" value are rescaled to "min".
- "max": arrows above the "max" value are rescaled to "max".
- "minmax": applies both "min" and "max" methods.
- "half_node": constrains arrows to fit within an ellipse with semi-principal axes 0.5dx and 0.5dy. 
- "full_node": constrains arrows to fit within an ellipse with semi-principal axes dx and dy. 

For the latter 2 methods, dx and dy refer to the grid spacing in the selected plane. The
"half_node" scaling ensures that no arrows will overlap, but may make arrows look really
far apart. The "full_node" scaling will make the plot appear less sparse, but risks arrow
overlap, depending on the vector orientation.

These methods will alter the arrow lengths non-uniformly, but they will still be coloured
according to their actual magnitude. If a system exhibits a truly large range of values,
it may be preferable to use the unit vector plot functionality.

Let's see some examples of these in action, shall we?

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 

    # all arrows at least as long as 0.1 units
    min_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        scaling_mode = "min", 
        min_size = 0.1
    )  
    # all arrows are no longer than 0.5 units
    max_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        scaling_mode = "max", 
        max_size = 0.5
    ) 
    # all arrows are at least 0.1 units long, but no longer than 0.5 units
    minmax_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        scaling_mode = "minmax", 
        min_size = 0.1, 
        max_size = 0.5
    )
    # all arrows have lengths no longer than 0.5*sqrt(dx**2 + dy**2)
    half_node_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        scaling_mode = "half_node"
    )
    # all arrows have lengths no longer than sqrt(dx**2 + dy**2)
    full_node_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "depth_average", 
        scaling_mode = "full_node"
    )

Unit Vector Plot
================
The unit vector plot is a version of the quiver plot that is well-suited to systems with
a range of scales present, for instance if velocity magnitudes lie between >2 orders of
magnitude; a difficult problem for a normal quiver plot. The key differences between these
two plots are:

- Unit vector plots all have arrows with the same length.
- The background of a unit vector plot is shaded by the vector magnitude.

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    plot_layout = dict(
        width=800, height=800,
        xaxis=dict(title="y position (m)"),
        yaxis=dict(title="z position (m)")
    )
    depth_fig = plotter.unit_vector_plot(
        axis = axis, 
        selection = "depth_average"
        layout = plot_layout)  

As before, perhaps we may be interested in a specific plane:

.. code-block:: python

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    # the same Plotter2D instance as above is used here to generate a different figure
    plane_fig = plotter.quiver_plot(
        axis = axis, 
        selection = "plane", 
        index = 2, 
        layout = plot_layout
    ) # reuse the same plot_layout as before  

Note that the plots discussed in the tutorials for :ref:`scalar plotting <scalarplots>` 
can also be used on the dataset used in these
tutorials, with the same `up4.Plotter2D` instance!

Formatting Vector Plots
=======================

The methods of `up4.Plotter2D` return `plotly.graph_objects.Figure` instances, so you can
customise your plots to the same level of detail as natively using plotly. Examples of
this are shown in each code block, where the x- and y-axes have been given
labels, and the colourbar has been given a title. At the Python level, you can either pass
dictionaries of `plotly.graph_objects.Layout` and `trace` style specifications, or 
interact with the returned `plotly.graph_objects.Figure` instance directly.

The choice to do this is deliberate as the plotly API in Python and Rust is *substantially*
different. In Python, it is a fully object-oriented approach with a myriad of optional
arguments, something that Rust cannot handle ergonomically. In Rust, the plotly API is
instead following a functional paradigm. Thus, the choice was made that ``up4`` will instead
expose the figure in a manner compatible with the language's API.

Finally, saving static plotly images to a required dpi is supported in ``up4``:

.. code-block:: python

    # create a plot
    import up4
    drum_hdf5_file = '/path/to/hdf5/file'
    data = up4.Data(drum_hdf5_file)
    grid_car = up4.Grid(data=data, num_cells=[20, 20, 20])

    vel_field = data.vectorfield(grid_car) # velocity vectorfield
    plotter = up4.Plotter2D(vel_field) # create a Plotter2D instance
    axis = 0 # interested in the y-z plane 
    plot_layout = dict(
        width=800, height=800,
        xaxis=dict(title="y position (m)"),
        yaxis=dict(title="z position (m)")
    )

    depth_fig = plotter.unit_vector_plot(
        axis = axis, 
        selection = "depth_average", 
        layout = plot_layout
    )  

    # now, save it with a required dpi
    dpi = 600 # typical requirement for many journals
    up4.save_fig(
        fig = fig, # figure to save
        filename = "velocity_field.png" # location to save file to
        dpi = dpi, # image dpi
        border_width = 20, # width of paper border (in mm)
        paper_width = 210 # a4 paper width (in mm)
    )