# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains visualization functions for Fourier series and coefficients."""
import warnings
from itertools import product
import numpy as np

# pylint:disable=too-many-arguments,blacklisted-name

# Matplotlib is not a hard requirement for PennyLane in general, but it *is*
# a hard requirement for everything in this module.
try:
    from matplotlib.colors import to_rgb
except (ModuleNotFoundError, ImportError) as e:
    raise ImportError(
        "Module matplotlib is required for visualization in the Fourier module. "
        "You can install matplolib via \n\n   pip install matplotlib"
    ) from e

from .utils import to_dict, format_nvec


def _validate_coefficients(coeffs, n_inputs, can_be_list=True):
    """Helper function to validate input coefficients of plotting functions.

    Args:
        coeffs (array[complex]): A set (or list of sets) of Fourier coefficients of a
            n_inputs-dimensional function.
        n_inputs (int): The number of inputs (dimension) of the function the coefficients are for.
        can_be_list (bool): Whether or not the plotting function accepts a list of
            coefficients, or only a single set.

    Raises:
        TypeError: If the coefficients are not a list or array.
        ValueError: if the coefficients are not a suitable type for the plotting function.
    """
    # Make sure we have a list or numpy array
    if not isinstance(coeffs, list) and not isinstance(coeffs, np.ndarray):
        raise TypeError(
            "Input to coefficient plotting functions must be a list of numerical "
            f"Fourier coefficients. Received input of type {type(coeffs)}"
        )

    # In case we have a list, turn it into a numpy array
    if isinstance(coeffs, list):
        coeffs = np.array(coeffs)

    # Check if the user provided a single set of coefficients to a function that is
    # meant to accept multiple samples; add an extra dimension around it if needed
    if len(coeffs.shape) == n_inputs and can_be_list:
        coeffs = np.array([coeffs])

    # Check now that we have the right number of axes for the type of function
    required_shape_size = n_inputs + 1 if can_be_list else n_inputs
    if len(coeffs.shape) != required_shape_size:
        raise ValueError(
            f"Plotting function expected a list of {n_inputs}-dimensional inputs. "
            f"Received coefficients of {len(coeffs.shape)}-dimensional function."
        )

    # Shape in all dimensions of a single set of coefficients must be the same
    shape_set = set(coeffs.shape[1:]) if can_be_list else set(coeffs.shape)
    if len(shape_set) != 1:
        raise ValueError(
            "All dimensions of coefficient array must be the same. "
            f"Received array with dimensions {coeffs.shape}"
        )

    # Size of each sample dimension must be 2d + 1 where d is the degree
    shape_dim = coeffs.shape[1] if can_be_list else coeffs.shape[0]
    if (shape_dim - 1) % 2 != 0:
        raise ValueError(
            "Shape of input coefficients must be 2d + 1, where d is the largest frequency. "
            f"Coefficient array with shape {coeffs.shape} is invalid."
        )

    # Return the coefficients; we may have switched to a numpy array or added a needed extra dimension
    return coeffs


def _extract_data_and_labels(coeffs):
    """Helper function for creating frequency labels and partitioning data.

    Args:
        coeffs (array[complex]): A list of sets of Fourier coefficients.

    Returns:
        (list(str), dict[str, array[complex]): The set of frequency labels, and a data
            dictionary split into real and imaginary parts.
    """
    # extract the x ticks
    nvecs = list(to_dict(coeffs[0]).keys())
    nvecs_formatted = [format_nvec(nvec) for nvec in nvecs]

    # make data
    data = {}
    data["real"] = np.array([[c[nvec].real for nvec in nvecs] for c in coeffs])
    data["imag"] = np.array([[c[nvec].imag for nvec in nvecs] for c in coeffs])

    return nvecs_formatted, data


def _adjust_spine_placement(ax):
    """Helper function to set some common axis properties when plotting."""
    ax.xaxis.grid()
    ax.spines["top"].set_visible(False)
    ax.spines["bottom"].set_position("zero")
    ax.spines["right"].set_visible(False)
    ax.set_axisbelow(True)


def violin(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True):
    """Plots a list of sets of Fourier coefficients as a violin plot.

    Args:
        coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the
            coefficient arrays should resemble that of the output of NumPy/SciPy's ``fftn`` function, or
            :func:`~.pennylane.fourier.coefficients`.
        n_inputs (int): The number of input variables in the function.
        ax (array[matplotlib.axes.Axes]): Axis on which to plot. Must
            be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``.
        colour_dict (dict[str, str]): A dictionary of the form ``{"real" : colour_string,
            "imag" : other_colour_string}`` indicating which colours should be used in the plot.
        show_freqs (bool): Whether or not to print the frequency labels on the plot axis.

    Returns:
        array[matplotlib.axes.Axes]: The axes on which the data is plotted.

    **Example**

    Suppose we have the following quantum function:

    .. code-block:: python

        dev = qml.device('default.qubit', wires=2)

        @qml.qnode(dev)
        def circuit_with_weights(w, x):
            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.Rot(*w[0], wires=0)
            qml.Rot(*w[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            return qml.expval(qml.PauliZ(0))

    We would like to compute and plot the distribution of Fourier coefficients
    for many random values of the weights ``w``. First, we generate all the coefficients:

    .. code-block:: python

        from functools import partial

        coeffs = []

        n_inputs = 2
        degree = 2

        for _ in range(100):
            weights = np.random.normal(0, 1, size=(2, 3))
            c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree)
            coeffs.append(c)

    We can now plot by setting up a pair of ``matplotlib`` axes and passing them
    to the plotting function:

    >>> import matplotlib.pyplot as plt
    >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4))
    >>> violinplt(coeffs, n_inputs, ax, show_freqs=True)

    .. image:: ../../_static/fourier_vis_violin.png
        :align: center
        :width: 800px
        :target: javascript:void(0);
    """
    coeffs = _validate_coefficients(coeffs, n_inputs, True)

    # Check axis shape
    if ax.size != 2:
        raise ValueError("Matplotlib axis should consist of two subplots.")

    if colour_dict is None:
        colour_dict = {"real": "purple", "imag": "green"}

    # Get the labels and data
    nvecs_formatted, data = _extract_data_and_labels(coeffs)

    for (data_type, axis) in zip(["real", "imag"], ax):
        violinplt = axis.violinplot(data[data_type], showextrema=False)
        for bd in violinplt["bodies"]:
            bd.set_color(colour_dict[data_type])
            bd.set_alpha(0.7)
        axis.set_ylabel(data_type)
        axis.xaxis.set_ticks(np.arange(1, len(data[data_type][0]) + 1))
        _adjust_spine_placement(axis)

    # Format axes
    ax[0].tick_params(axis="x", colors="white")  # hack to get rid of ticks but keep grid

    if show_freqs:
        ax[1].tick_params(axis="x", which="both", length=0)  # remove ticks without removing labels
        ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey")
        ax[1].xaxis.set_ticks_position("top")
    else:
        ax[1].tick_params(axis="x", colors="white")  # hack to get rid of ticks but keep grid

    return ax


def box(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True, show_fliers=True):
    """Plot a list of sets of Fourier coefficients as a box plot.

    Args:
        coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the
            coefficient arrays should resemble that of the output of numpy/scipy's ``fftn``
            function, or :func:`~.pennylane.fourier.coefficients`.
        n_inputs (int): The number of input variables in the function.
        ax (array[matplotlib.axes.Axes]): Axis on which to plot. Must
            be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``.
        colour_dict (dict[str, str]): A dictionary of the form {"real" : colour_string,
            "imag" : other_colour_string} indicating which colours should be used in the plot.
        show_freqs (bool): Whether or not to print the frequency labels on the plot axis.
        show_fliers (bool): Whether to display the box plot outliers.

    Returns:
        array[matplotlib.axes.Axes]: The axes after plotting is complete.

    **Example**

    Suppose we have the following quantum function:

    .. code-block:: python

        dev = qml.device('default.qubit', wires=2)

        @qml.qnode(dev)
        def circuit_with_weights(w, x):
            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.Rot(*w[0], wires=0)
            qml.Rot(*w[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            return qml.expval(qml.PauliZ(0))

    We would like to compute and plot the distribution of Fourier coefficients
    for many random values of the weights ``w``. First, we generate all the coefficients:

    .. code-block:: python

        from functools import partial

        coeffs = []

        n_inputs = 2
        degree = 2

        for _ in range(100):
            weights = np.random.normal(0, 1, size=(2, 3))
            c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree)
            coeffs.append(c)

    We can now plot by setting up a pair of ``matplotlib`` axes and passing them
    to the plotting function:

    >>> import matplotlib.pyplot as plt
    >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4))
    >>> box(coeffs, n_inputs, ax, show_freqs=True)

    .. image:: ../../_static/fourier_vis_box.png
        :align: center
        :width: 800px
        :target: javascript:void(0);
    """
    coeffs = _validate_coefficients(coeffs, n_inputs, True)

    # Check axis shape
    if ax.size != 2:
        raise ValueError("Matplotlib axis should consist of two subplots.")

    # The axis received must be a pair of axes in a subplot.
    if colour_dict is None:
        colour_dict = {"real": "purple", "imag": "green"}

    # Get the labels and data
    nvecs_formatted, data = _extract_data_and_labels(coeffs)

    for (data_type, axis) in zip(["real", "imag"], ax):
        data_colour = colour_dict[data_type]
        axis.boxplot(
            data[data_type],
            boxprops=dict(
                facecolor=to_rgb(data_colour) + (0.4,), color=data_colour, edgecolor=data_colour
            ),
            medianprops=dict(color=data_colour, linewidth=1.5),
            flierprops=dict(markeredgecolor=data_colour),
            whiskerprops=dict(color=data_colour),
            capprops=dict(color=data_colour),
            patch_artist=True,
            showfliers=show_fliers,
        )

        _adjust_spine_placement(axis)
        axis.set_ylabel(data_type)
        axis.xaxis.set_ticks(np.arange(1, len(nvecs_formatted) + 1))

    ax[0].tick_params(axis="x", colors="white")  # hack to get rid of ticks but keep grid

    if show_freqs:
        ax[1].tick_params(axis="x", which="both", length=0)  # remove ticks without removing labels
        ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey")
        ax[1].xaxis.set_ticks_position("top")
    else:
        ax[1].tick_params(axis="x", colors="white")  # hack to get rid of ticks but keep grid

    return ax


def bar(coeffs, n_inputs, ax, colour_dict=None, show_freqs=True):
    """Plot a set of Fourier coefficients as a bar plot.

    Args:

        coeffs (array[complex]): A single set of Fourier coefficients. The dimensions of the coefficient
            array should be ``(2d + 1, ) * n_inputs`` where ``d`` is the largest frequency.
        n_inputs (int): The number of input variables in the function.
        ax (list[matplotlib.axes.Axes]): Axis on which to plot. Must
            be a pair of axes from a subplot where ``sharex="row"`` and ``sharey="col"``.
        colour_dict (dict[str, str]): A dictionary of the form ``{"real" : colour_string,
            "imag" : other_colour_string}`` indicating which colours should be used in the plot.
        show_freqs (bool): Whether or not to print the frequency labels on the plot axis.

     Returns:
        array[matplotlib.axes.Axes]: The axes after plotting is complete.

    **Example**

    Suppose we have the following quantum function:

    .. code-block:: python

        dev = qml.device('default.qubit', wires=2)

        @qml.qnode(dev)
        def circuit_with_weights(w, x):
            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.Rot(*w[0], wires=0)
            qml.Rot(*w[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            return qml.expval(qml.PauliZ(0))

    We would like to compute and plot a single set of Fourier coefficients. We will
    choose some values for ``w`` at random:

    .. code-block:: python

        from functools import partial

        n_inputs = 2
        degree = 2

        weights = np.random.normal(0, 1, size=(2, 3))
        coeffs = coefficients(partial(circuit_with_weights, weights), n_inputs, degree)

    We can now plot by setting up a pair of ``matplotlib`` axes and passing them
    to the plotting function:

    >>> import matplotlib.pyplot as plt
    >>> fig, ax = plt.subplots(2, 1, sharey=True, figsize=(15, 4))
    >>> bar(coeffs, n_inputs, ax, colour_dict={"real" : "red", "imag" : "blue"})

    .. image:: ../../_static/fourier_vis_bar_plot_2.png
        :align: center
        :width: 800px
        :target: javascript:void(0);
    """
    coeffs = _validate_coefficients(coeffs, n_inputs, False)

    # Check axis shape
    if ax.size != 2:
        raise ValueError("Matplotlib axis should consist of two subplots.")

    # The axis received must be a pair of axes in a subplot.
    if colour_dict is None:
        colour_dict = {"real": "purple", "imag": "green"}

    # Get the labels and data
    nvecs_formatted, data = _extract_data_and_labels(np.array([coeffs]))
    data_len = len(data["real"][0])

    for (data_type, axis) in zip(["real", "imag"], ax):
        axis.bar(np.arange(data_len), data[data_type][0], color=colour_dict[data_type], alpha=0.7)
        axis.set_ylabel(data_type)
        axis.xaxis.set_ticks(np.arange(data_len))
        _adjust_spine_placement(axis)

    ax[0].tick_params(axis="x", colors="white")  # hack to get rid of ticklabels but keep grid

    if show_freqs:
        ax[1].tick_params(axis="x", which="both", length=0)  # remove ticks without removing labels
        ax[1].xaxis.set_ticklabels(nvecs_formatted, fontsize=10, color="grey")
        ax[1].xaxis.set_ticks_position("top")
    else:
        ax[1].tick_params(axis="x", colors="white")  # hack to get rid of ticklabels but keep grid

    return ax


def panel(coeffs, n_inputs, ax, colour=None):
    """Plot a list of sets of coefficients in the complex plane for a 1- or 2-dimensional function.

    Args:
        coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the
            coefficient arrays must all be either 1- or 2-dimensional, i.e.,
            each array should have shape ``(2d + 1,)``
            for the 1-dimensional case, or ``(2d + 1, 2d + 1)`` where ``d`` is the
            degree, i.e., the maximum frequency of present in the coefficients.
            Such an array may be the output of the numpy/scipy ``fft``/``fft2`` functions,
            or :func:`~.pennylane.fourier.coefficients`.
        n_inputs (int): The number of variables in the function.
        ax (array[matplotlib.axes._subplots.AxesSubplot]): Axis on which to plot. For
            1-dimensional data, length must be the number of frequencies. For 2-dimensional
            data, must be a grid that matches the dimensions of a single set of coefficients.
        colour (str): The outline colour of the points on the plot.

    Returns:
        array[matplotlib.axes.Axes]: The axes after plotting is complete.

    **Example**

    Suppose we have the following quantum function:

    .. code-block:: python

        dev = qml.device('default.qubit', wires=2)

        @qml.qnode(dev)
        def circuit_with_weights(w, x):
            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.Rot(*w[0], wires=0)
            qml.Rot(*w[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            return qml.expval(qml.PauliZ(0))

    We would like to compute and plot the distribution of Fourier coefficients
    for many random values of the weights ``w``. First, we generate all the coefficients:

    .. code-block:: python

        from functools import partial

        coeffs = []

        n_inputs = 2
        degree = 2

        for _ in range(100):
            weights = np.random.normal(0, 1, size=(2, 3))
            c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree)
            coeffs.append(c)

    We can now plot by setting up a pair of ``matplotlib`` axes and passing them
    to the plotting function. The of axes must be large enough to represent all
    the available coefficients (in this case, since we have 2 variables and use
    degree 2, we need a 5x5 grid.

    >>> import matplotlib.pyplot as plt
    >>> fig, ax = plt.subplots(5, 5, figsize=(12, 10), sharex=True, sharey=True)
    >>> panel(coeffs, n_inputs, ax)

    .. image:: ../../_static/fourier_vis_panel.png
        :align: center
        :width: 800px
        :target: javascript:void(0);

    """
    if n_inputs in [1, 2]:
        coeffs = _validate_coefficients(coeffs, n_inputs, True)
    else:
        raise ValueError(
            "Panel plot function accepts input coefficients for only 1- or 2-dimensional functions."
        )

    if ax.shape != coeffs[0].shape:
        raise ValueError("Shape of subplot axes must match the shape of the coefficient data.")

    if colour is None:
        colour = "tab:blue"

    # This could probably be more efficient.
    # Plot 1D case
    if n_inputs == 1:
        # Range is (0, ..., degree) for rfft, (0, ... degree, -degree, ..., -1) for fft
        n_freqs = coeffs.shape[1] // 2 + (coeffs.shape[1] % 2)
        frequency_range = list(range(n_freqs)) + list(range(-n_freqs + 1, 0))

        for coeff in range(coeffs.shape[1]):
            ax[coeff].scatter(
                coeffs[:, coeff].real, coeffs[:, coeff].imag, facecolor="white", edgecolor=colour
            )
            ax[coeff].set_title(f"{frequency_range[coeff]}", fontsize=14)
            ax[coeff].grid(True)
            ax[coeff].set_aspect("equal")

    # Plot 2D case
    else:
        n_freqs = coeffs.shape[1] // 2 + (coeffs.shape[1] % 2)

        frequency_range = list(range(n_freqs)) + list(range(-n_freqs + 1, 0))

        for coeff_1, coeff_2 in product(list(range(coeffs.shape[1])), list(range(coeffs.shape[2]))):
            ax[coeff_1, coeff_2].scatter(
                coeffs[:, coeff_1, coeff_2].real,
                coeffs[:, coeff_1, coeff_2].imag,
                facecolor="white",
                edgecolor=colour,
            )
            ax[coeff_1, coeff_2].set_title(
                f"{frequency_range[coeff_1]}, {frequency_range[coeff_2]}", fontsize=14
            )
            ax[coeff_1, coeff_2].grid(True)
            ax[coeff_1, coeff_2].set_aspect("equal")

    return ax


def radial_box(coeffs, n_inputs, ax, show_freqs=True, colour_dict=None, show_fliers=True):
    """Plot a list of sets of Fourier coefficients on a radial plot as box plots.

    Produces a 2-panel plot in which the left panel represents the real parts of
    Fourier coefficients. This method accepts multiple sets of coefficients, and
    plots the distribution of each coefficient as a boxplot.

    Args:
        coeffs (list[array[complex]]): A list of sets of Fourier coefficients. The shape of the
            coefficient arrays should resemble that of the output of numpy/scipy's ``fftn`` function, or
            :func:`~.pennylane.fourier.coefficients`.
        n_inputs (int): Dimension of the transformed function.
        ax (array[matplotlib.axes.Axes]): Axes to plot on. For this function, subplots
            must specify ``subplot_kw=dict(polar=True)`` upon construction.
        show_freqs (bool): Whether or not to label the frequencies on
            the radial axis. Turn off for large plots.
        colour_dict (dict[str, str]): Specify a colour mapping for positive and negative
            real/imaginary components. If none specified, will default to:
            ``{"real" : "red", "imag" : "black"}``
        showfliers (bool): Whether or not to plot outlying "fliers" on the boxplots.
        merge_plots (bool): Whether to plot real/complex values on the same panel, or
            on separate panels. Default is to plot real/complex values on separate panels.

    Returns:
        array[matplotlib.axes.Axes]: The axes after plotting is complete.

    **Example**

    Suppose we have the following quantum function:

    .. code-block:: python

        dev = qml.device('default.qubit', wires=2)

        @qml.qnode(dev)
        def circuit_with_weights(w, x):
            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.Rot(*w[0], wires=0)
            qml.Rot(*w[1], wires=1)
            qml.CNOT(wires=[1, 0])

            qml.RX(x[0], wires=0)
            qml.RY(x[1], wires=1)
            qml.CNOT(wires=[1, 0])

            return qml.expval(qml.PauliZ(0))

    We would like to compute and plot the distribution of Fourier coefficients
    for many random values of the weights ``w``. First, we generate all the coefficients:

    .. code-block:: python

        from functools import partial

        coeffs = []

        n_inputs = 2
        degree = 2

        for _ in range(100):
            weights = np.random.normal(0, 1, size=(2, 3))
            c = coefficients(partial(circuit_with_weights, weights), n_inputs, degree)
            coeffs.append(c)

    We can now plot by setting up a pair of ``matplotlib`` axes and passing them
    to the plotting function. Note that the axes passed must use polar coordinates.

    .. code-block:: python

        import matplotlib.pyplot as plt

        fig, ax = plt.subplots(
            1, 2, sharex=True, sharey=True,
            subplot_kw=dict(polar=True),
            figsize=(15, 8)
        )

        radial_box(coeffs, 2, ax, show_freqs=True, show_fliers=False)

    .. image:: ../../_static/fourier_vis_radial_box.png
        :align: center
        :width: 800px
        :target: javascript:void(0);

    """
    coeffs = _validate_coefficients(coeffs, n_inputs, True)

    # Check axis shape
    if ax.size != 2:
        raise ValueError("Matplotlib axis should consist of two subplots.")

    if ax[0].name != "polar" or ax[1].name != "polar":
        raise ValueError("Matplotlib axes for radial_box must be polar.")

    if colour_dict is None:
        colour_dict = {"real": "red", "imag": "black"}

    # Number, width, and placement of pie slices
    N = coeffs[0].size
    angles = np.linspace(0, 2 * np.pi, N, endpoint=False)
    angles = np.concatenate((angles[-N // 2 + 1 :], angles[: -N // 2 + 1]))[::-1]
    width = (angles[1] - angles[0]) / 2

    # Get the labels and data
    nvecs_formatted, data = _extract_data_and_labels(coeffs)

    # Set up the violin plots
    for data_type, a in zip(["real", "imag"], ax):
        data_colour = colour_dict[data_type]

        a.boxplot(
            data[data_type],
            positions=angles,
            widths=width,
            boxprops=dict(
                facecolor=to_rgb(data_colour) + (0.4,), color=data_colour, edgecolor=data_colour
            ),
            medianprops=dict(color=data_colour, linewidth=1.5),
            flierprops=dict(markeredgecolor=data_colour),
            whiskerprops=dict(color=data_colour),
            capprops=dict(color=data_colour),
            patch_artist=True,
            showfliers=show_fliers,
        )

        # Rotate so that the 0 frequency is at the to
        a.set_thetagrids((180 / np.pi) * angles, labels=nvecs_formatted)
        a.set_theta_zero_location("N")
        a.set_rlabel_position(0)

    # Set and rotate the tickmarks; taken from SO
    # https://stackoverflow.com/questions/46719340/how-to-rotate-tick-labels-in-polar-matplotlib-plot
    for a in ax:
        if show_freqs:
            for label, angle in zip(a.get_xticklabels(), angles):
                x, y = label.get_position()
                lab = a.text(
                    x,
                    y,
                    label.get_text(),
                    transform=label.get_transform(),
                    ha=label.get_ha(),
                    va=label.get_va(),
                    fontsize=14,
                    color="grey",
                )
                if angle > np.pi:
                    lab.set_rotation((180 / np.pi) * angle + 90)
                else:
                    lab.set_rotation((180 / np.pi) * angle + 270)

            a.tick_params(pad=7 * n_inputs)

        a.set_xticklabels([])

    return ax
