Label features with automatic layout of labels.

    Parameters
    ----------
    wave: list or array of floats
        Wave lengths of data.
    flux: list or array of floats
        Flux at each wavelength.
    line_wave: list or array of floats
        Wave length of features to be labelled.
    line_label1: list of strings
        Label text for each line.
    label1_size: list of floats
        Font size in points. If not given the default value in
        Matplotlib is used. This is typically 12.
    extend: boolean or list of boolean values
        For those lines for which this keyword is True, a dashed line
        will be drawn from the tip of the annotation to the flux at the
        line.
    annotate_kwargs : dict
        Keyword arguments to pass to `annotate`, e.g. color.

        Default value is obtained by calling ``initial_annotate_kwargs()``.
    plot_kwargs : dict
        Keyword arguments to pass to `plot`, e.g. color.

        Default value is obtained by calling ``initial_plot_kwargs()``.
    kwargs: key value pairs
        All of these keywords are optional.

        The following keys are recognized:

          ax : Matplotlib Axes
              The Axes in which the labels are to be placed. If not
              given a new Axes is created.
          fig: Matplotlib Figure
              The figure in which the labels are to be placed. If `ax`
              if given then keyword is then ignored. The figure
              associated with `ax` is used. If `fig` and `ax` are not
              given then a new figure is created and an axes is added
              to it.
          arrow_tip: scalar or list of floats
              The location of the annotation point, in data coords. If
              the value is scalar then it is used for all. Default
              value is the upper bound of the Axes, at the time of
              plotting.
          box_loc: scalar or list of floats
              The y axis location of the text label boxes, in data
              units. The default is to place it above the `arrow_tip`
              by `box_axes_space` units in figure fraction length.
          box_axes_space: float
              If no `box_loc` is given then the y position of label
              boxes is set to `arrow_tip` + this many figure fraction
              units. The default is 0.06. This ensures that the label
              layout appearance is independent of the y data range.
          max_iter: int
              Maximum iterations to use. Default is set to 1000.
          add_label_to_artists: boolean
              If True (default is True) then add unique labels to artists, both
              text labels and line extending from text label to spectrum. If
              False then don't add such labels.
    Returns
    -------
    fig, ax: Matplotlib Figure, Matplotlib Axes
        Figure instance on which the labels were placed and the Axes
        instance on which the labels were placed. These can be used for
        further customizations. For example, some labels can be hidden
        by accessing the corresponding `Text` instance form the
        `ax.texts` list.

    Notes
    -----
    + By default the labels are placed along the top of the Axes. The
      annotation point is on the top boundary of the Axes at the y
      location of the line. The y location of the boxes are 0.06 figure
      fraction units above the annotation location. This value can be
      customized using the `box_axes_space` parameter. The value must
      be in figure fractions units. Y location of both labels and
      annotation points can be changed using `arrow_tip` and `box_loc`
      parameters.
    + If `arrow_tip` parameter is given then it is used as the
      annotation point. This can be a list in which case each line can
      have its own annotation point.
    + If `box_loc` is given, then the boxes are placed at this
      position. This too can be a list.
    + `arrow_tip` and `box_loc` are the "y" components of `xy` and
      `xyann` parameters accepted by the `annotate` function in
      Matplotlib.
    + If the `extend` keyword is True then a line is drawn from the
      annotation point to the flux at the line wavelength. The flux is
      calculated by linear interpolation. This parameter can be a list,
      with one value for each line.
    + The maximum iterations to be used can be customized using the
      `max_iter` keyword parameter.
    + add_label_to_artists: Adding labels to artists makes it very easy to get
      reference to an artist using Figure.findobj. But a call to plt.legend()
      will display legend for the lines. Setting add_label_to_artists=False,
      will not add labels to text or lines and solves this issue. We can all
      plt.legend() with artists and labels to only display legends for the
      specified artists.

def plot_line_ids(wave, flux, line_wave, line_label1, label1_size=None,
                  extend=True, annotate_kwargs=None, plot_kwargs=None,
                  **kwargs):
    """Label features with automatic layout of labels.

    Parameters
    ----------
    wave: list or array of floats
        Wave lengths of data.
    flux: list or array of floats
        Flux at each wavelength.
    line_wave: list or array of floats
        Wave length of features to be labelled.
    line_label1: list of strings
        Label text for each line.
    label1_size: list of floats
        Font size in points. If not given the default value in
        Matplotlib is used. This is typically 12.
    extend: boolean or list of boolean values
        For those lines for which this keyword is True, a dashed line
        will be drawn from the tip of the annotation to the flux at the
        line.
    annotate_kwargs : dict
        Keyword arguments to pass to `annotate`, e.g. color.

        Default value is obtained by calling ``initial_annotate_kwargs()``.
    plot_kwargs : dict
        Keyword arguments to pass to `plot`, e.g. color.

        Default value is obtained by calling ``initial_plot_kwargs()``.
    kwargs: key value pairs
        All of these keywords are optional.

        The following keys are recognized:

          ax : Matplotlib Axes
              The Axes in which the labels are to be placed. If not
              given a new Axes is created.
          fig: Matplotlib Figure
              The figure in which the labels are to be placed. If `ax`
              if given then keyword is then ignored. The figure
              associated with `ax` is used. If `fig` and `ax` are not
              given then a new figure is created and an axes is added
              to it.
          arrow_tip: scalar or list of floats
              The location of the annotation point, in data coords. If
              the value is scalar then it is used for all. Default
              value is the upper bound of the Axes, at the time of
              plotting.
          box_loc: scalar or list of floats
              The y axis location of the text label boxes, in data
              units. The default is to place it above the `arrow_tip`
              by `box_axes_space` units in figure fraction length.
          box_axes_space: float
              If no `box_loc` is given then the y position of label
              boxes is set to `arrow_tip` + this many figure fraction
              units. The default is 0.06. This ensures that the label
              layout appearance is independent of the y data range.
          max_iter: int
              Maximum iterations to use. Default is set to 1000.
          add_label_to_artists: boolean
              If True (default is True) then add unique labels to artists, both
              text labels and line extending from text label to spectrum. If
              False then don't add such labels.
    Returns
    -------
    fig, ax: Matplotlib Figure, Matplotlib Axes
        Figure instance on which the labels were placed and the Axes
        instance on which the labels were placed. These can be used for
        further customizations. For example, some labels can be hidden
        by accessing the corresponding `Text` instance form the
        `ax.texts` list.

    Notes
    -----
    + By default the labels are placed along the top of the Axes. The
      annotation point is on the top boundary of the Axes at the y
      location of the line. The y location of the boxes are 0.06 figure
      fraction units above the annotation location. This value can be
      customized using the `box_axes_space` parameter. The value must
      be in figure fractions units. Y location of both labels and
      annotation points can be changed using `arrow_tip` and `box_loc`
      parameters.
    + If `arrow_tip` parameter is given then it is used as the
      annotation point. This can be a list in which case each line can
      have its own annotation point.
    + If `box_loc` is given, then the boxes are placed at this
      position. This too can be a list.
    + `arrow_tip` and `box_loc` are the "y" components of `xy` and
      `xyann` parameters accepted by the `annotate` function in
      Matplotlib.
    + If the `extend` keyword is True then a line is drawn from the
      annotation point to the flux at the line wavelength. The flux is
      calculated by linear interpolation. This parameter can be a list,
      with one value for each line.
    + The maximum iterations to be used can be customized using the
      `max_iter` keyword parameter.
    + add_label_to_artists: Adding labels to artists makes it very easy to get
      reference to an artist using Figure.findobj. But a call to plt.legend()
      will display legend for the lines. Setting add_label_to_artists=False,
      will not add labels to text or lines and solves this issue. We can all
      plt.legend() with artists and labels to only display legends for the
      specified artists.

    """
    wave = np.array(wave)
    flux = np.array(flux)
    line_wave = np.array(line_wave)
    line_label1 = np.array(line_label1)

    nlines = len(line_wave)
    assert nlines == len(line_label1), "Each line must have a label."

    if label1_size is None:
        label1_size = np.array([12] * nlines)
    label1_size = _convert_to_array(label1_size, nlines, "lable1_size")

    extend = _convert_to_array(extend, nlines, "extend")

    # Sort.
    indx = np.argsort(wave)
    wave[:] = wave[indx]
    flux[:] = flux[indx]
    indx = np.argsort(line_wave)
    line_wave[:] = line_wave[indx]
    line_label1[:] = line_label1[indx]
    label1_size[:] = label1_size[indx]

    # Flux at the line wavelengths.
    line_flux = get_line_flux(line_wave, wave, flux)

    # Figure and Axes. If Axes is given then use it. If not, create
    # figure, if not given, and add Axes to it using a default
    # layout. Also plot the data in the Axes.
    ax = kwargs.get("ax", None)
    if not ax:
        fig = kwargs.get("fig", None)
        fig, ax = prepare_axes(wave, flux, fig)
    else:
        fig = ax.figure

    # Find location of the tip of the arrow. Either the top edge of the
    # Axes or the given data coordinates.
    ax_bounds = ax.get_ybound()
    arrow_tip = kwargs.get("arrow_tip", ax_bounds[1])
    arrow_tip = _convert_to_array(arrow_tip, nlines, "arrow_tip")

    # The y location of boxes from the arrow tips. Either given heights
    # in data coordinates or use `box_axes_space` in figure
    # fraction. The latter has a default value which is used when no
    # box locations are given. Figure coordiantes are used so that the
    # y location does not dependent on the data y range.
    box_loc = kwargs.get("box_loc", None)
    if not box_loc:
        box_axes_space = kwargs.get("box_axes_space", 0.06)
        box_loc = get_box_loc(fig, ax, line_wave, arrow_tip, box_axes_space)
    else:
        box_loc = _convert_to_array(box_loc, nlines, "box_loc")
        box_loc = tuple(zip(line_wave, box_loc))

    # If any labels are repeated add "_num_#" to it. If there are 3 "X"
    # then the first gets "X_num_3". The result is passed as the label
    # parameter of annotate. This makes it easy to find the box
    # corresponding to a label using Figure.findobj. But the downside is that a
    # call to plt.legend() will display legends for the lines (from text to
    # spectrum location). So we don't add the label to artists if the user
    # doesn't want to.
    al = kwargs.get('add_label_to_artists', True)
    label_u = unique_labels(line_label1) if al else [None for _ in line_label1]
    label_u_line = [i + "_line" for i in label_u] if al else label_u

    if annotate_kwargs is None:
        annotate_kwargs = {}
    if plot_kwargs is None:
        plot_kwargs = {}
    ak = initial_annotate_kwargs()
    ak.update(annotate_kwargs)
    pk = initial_plot_kwargs()
    pk.update(plot_kwargs)
    # Draw boxes at initial (x, y) location.
    for i in range(nlines):
        ax.annotate(line_label1[i], xy=(line_wave[i], arrow_tip[i]),
                    xytext=(box_loc[i][0],
                            box_loc[i][1]),

                    fontsize=label1_size[i],
                    label=label_u[i],
                    **ak)
        if extend[i]:
            ax.plot([line_wave[i]] * 2, [arrow_tip[i], line_flux[i]],
                    scalex=False, scaley=False,
                    label=label_u_line[i],
                    **pk)

    # Draw the figure so that get_window_extent() below works.
    fig.canvas.draw()

    # Get annotation boxes and convert their dimensions from display
    # coordinates to data coordinates. Specifically, we want the width
    # in wavelength units. For each annotation box, transform the
    # bounding box into data coordinates and extract the width.
    ax_inv_trans = ax.transData.inverted()  # display to data
    box_widths = []  # box width in wavelength units.
    for box in ax.texts:
        b_ext = box.get_window_extent()
        box_widths.append(b_ext.transformed(ax_inv_trans).width)

    # Find final x locations of boxes so that they don't overlap.
    # Function adjust_boxes uses a direct translation of the equivalent
    # code in lineid_plot.pro in IDLASTRO.
    max_iter = kwargs.get('max_iter', 1000)
    adjust_factor = kwargs.get('adjust_factor', 0.35)
    factor_decrement = kwargs.get('factor_decrement', 3.0)
    wlp, niter, changed = adjust_boxes(line_wave, box_widths,
                                       np.min(wave), np.max(wave),
                                       adjust_factor=adjust_factor,
                                       factor_decrement=factor_decrement,
                                       max_iter=max_iter)

    # Redraw the boxes at their new x location.
    for i in range(nlines):
        box = ax.texts[i]
        if hasattr(box, 'xyann'):
            box.xyann = (wlp[i], box.xyann[1])
        elif hasattr(box, 'xytext'):
            box.xytext = (wlp[i], box.xytext[1])
        else:
            warnings.warn("Warning: missing xyann and xytext attributes. "
                          "Your matplotlib version may not be compatible "
                          "with lineid_plot.")

    # Update the figure
    fig.canvas.draw()

    # Return Figure and Axes so that they can be used for further
    # manual customization.
    return fig, ax