Verified Commit 7f3b4487 authored by Yunus Sevinchan's avatar Yunus Sevinchan
Browse files

Add `auto_encoding_options` and `ignore_missing` feature

This allows to specify encodings that will be ignored if missing in the
data, thus making things more flexible. (Disabled by default.)

Adds the `.plot.facet_grid.with_auto_encoding.ignore_missing` entry to
the base plots configuration.
parent e705e84b
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -135,6 +135,11 @@ _:
  auto_encoding: true
  col_wrap: auto

.plot.facet_grid.with_auto_encoding.ignore_missing:
  based_on: .plot.facet_grid.with_auto_encoding
  auto_encoding_options:
    ignore_missing: true

.plot.facet_grid.with_auto_kind:
  based_on: .plot.facet_grid
  kind: auto
+51 −6
Original line number Diff line number Diff line
@@ -187,8 +187,9 @@ def determine_encoding(
    kind: str,
    auto_encoding: Union[bool, dict],
    default_encodings: dict,
    allow_y_for_x: List[str] = ("line",),
    plot_kwargs: dict,
    allow_y_for_x: List[str] = ("line",),
    ignore_missing: bool = False,
) -> dict:
    """Determines the layout encoding for the given plot kind and the available
    data dimensions (as specified by the ``dims`` argument).
@@ -233,6 +234,28 @@ def determine_encoding(
        :end-before:  }   # --- end literalinclude
        :dedent: 4

    The ``ignore_missing`` option will unset a previously set encoding if
    that dimension does not exist in the data; a warning will be shown if this
    was the case.

    .. note::

        **Background:**
        One can distinguish different categories of xarray data dimensions,
        most relevant for association of encodings: those *with* and those
        *without* coordinate labels. If coordinates are available, the
        corresponding dimension is called *indexed*, otherwise it is a
        *non-indexed* dimension, no coordinate labels exist and hence only
        trivial indexing is possible.

        xarray objects may also contain additional (scalar) coordinate metadata
        which has no relation to the data dimensions and is ignored here.

        Furthermore, there can be additional non-scalar coordinates that *are*
        associated with existing data dimensions, but are *not* acting as their
        index; these run "in parallel" to the existing coordinates along that
        dimension.

    This function also implements **automatic column wrapping**, aiming to
    produce a efficient figure use with column wrapping. The prerequisites
    are the following:
@@ -248,7 +271,9 @@ def determine_encoding(
    get a square-like grid.
    To skip the optimization, potentially leading to last rows that have only
    one or few subplots, set ``col_wrap`` to ``"square"``, in which case
    wrapping will happen after ``ceil(sqrt(num_cols))`` columns.
    wrapping will happen after ``ceil(sqrt(num_cols))`` columns; see
    :py:func:`~dantro.plot.funcs._utils.determine_ideal_col_wrap` for more
    information and implementation.

    Args:
        dims (Union[List[str], Dict[str, int]]): The dimension names (and, if
@@ -317,6 +342,25 @@ def determine_encoding(

    # TODO Warn upon non-indexed dimensions?

    # May want to ignore specified encodings that are not available in the data
    specs_without_dims = {
        s: dim for s, dim in specs.items() if dim and dim not in dim_names
    }
    if ignore_missing and specs_without_dims:
        log.caution(
            "   ignoring:  %s",
            ", ".join([f"{s}: {d}" for s, d in specs_without_dims.items()]),
        )
        specs = {
            s: dim for s, dim in specs.items() if s not in specs_without_dims
        }

    elif specs_without_dims:
        log.error(
            "   missing:   %s  (not available in data)",
            ", ".join([f"{s}: {d}" for s, d in specs_without_dims.items()]),
        )

    # Some dimensions and specifiers might already have been associated;
    # determine those that have *not* yet been associated:
    free_specs = [s for s in encoding_specs if not specs.get(s)]
@@ -685,9 +729,10 @@ def facet_grid(
    kind: Union[str, dict] = None,
    frames: str = None,
    auto_encoding: Union[bool, dict] = False,
    auto_encoding_options: dict = None,
    suptitle_kwargs: dict = None,
    squeeze: bool = True,
    drop_nonindexed_coords: bool = True,
    drop_nonindexed_coords: bool = False,
    **plot_kwargs,
):
    """A generic facet grid plot function for high dimensional data.
@@ -790,6 +835,7 @@ def facet_grid(
            ``dim``, ``value``. Default: ``{dim:} = {value:.3g}``.
        squeeze (bool, optional): whether to squeeze the data before plotting,
            such that size-1 dimensions do not take up encoding dimensions.
        drop_nonindexed_coords (bool, optional): TODO
        **plot_kwargs: Passed on to ``<data>.plot`` or ``<data>.plot.<kind>``
            These should include the layout encoding specifiers (``x``, ``y``,
            ``hue``, ``col``, and/or ``row``).
@@ -916,13 +962,11 @@ def facet_grid(
    log.remark("%s", d.head())

    # Squeeze size-1 dimension coordinates to non-dimension coordinates
    # TODO Really do this?!
    if squeeze and 1 in d.sizes.values():
        log.remark("Squeezing ...")
        d = d.squeeze(drop=True)
        d = d.squeeze()

    # Drop unwanted non-indexed coordinates
    # TODO Do this already here or only in plot_frame?
    nonindexed_coords = [c for c in d.coords if c not in d.indexes]
    if nonindexed_coords and drop_nonindexed_coords:
        log.remark(
@@ -946,6 +990,7 @@ def facet_grid(
            frames=frames,
            **plot_kwargs,
        ),
        **(auto_encoding_options if auto_encoding_options else {}),
    )
    frames = plot_kwargs.pop("frames", None)

+64 −0
Original line number Diff line number Diff line
@@ -87,6 +87,58 @@ auto:
    raises:
      6: *line_plots_1d_or_2d

  with_size_one:  # ... which can be squeezed
    kinds: [line]
    specifiers: []
    min_dims: 2
    max_dims: 8
    test_data_path: labelled_with_size_one
    plot_kwargs:
      auto_encoding: true
      # squeeze: true  # is the default value
    raises:
      2: &err_no_num_data [PlottingError, "TypeError: No numeric data to plot"]
      8: *line_plots_1d_or_2d

  with_size_one_no_squeeze:
    kinds: [line]
    specifiers: []
    min_dims: 2
    max_dims: 5
    test_data_path: labelled_with_size_one
    plot_kwargs:
      auto_encoding: true
      squeeze: false
    raises:
      5: *line_plots_1d_or_2d

  ignore_missing:
    kinds: [line]
    specifiers: []  # to not have row be set by test function
    max_dims: 6
    plot_kwargs:
      auto_encoding: true
      auto_encoding_options:
        ignore_missing: true
      hue: dim_3  # only exists for dims >= 4, would be on x usually
    raises:
      6: *line_plots_1d_or_2d

  ignore_missing_disabled:
    kinds: [line]
    specifiers: []  # to not have row be set by test function
    max_dims: 5
    plot_kwargs:
      auto_encoding: true
      auto_encoding_options:
        ignore_missing: false  # default
      x: dim_3    # only exists for dims >= 4
      row: dim_2  # only exists for dims >= 3
    raises:
      1: &err_enc_missing [PlottingError, "KeyError: 'dim_2'"]
      2: *err_enc_missing
      3: [PlottingError, must be one of]

  with_extra_coords:  # ... which are ignored
    kinds: [line]
    specifiers: []
@@ -98,6 +150,18 @@ auto:
    raises:
      6: *line_plots_1d_or_2d

  with_extra_coords_drop:
    kinds: [line]
    specifiers: []
    min_dims: 2
    max_dims: 6
    test_data_path: labelled_extra_coords
    plot_kwargs:
      auto_encoding: true
      drop_nonindexed_coords: true
    raises:
      6: *line_plots_1d_or_2d

  auto_col_wrap:
    kinds: [line]
    specifiers: []
+15 −0
Original line number Diff line number Diff line
@@ -400,6 +400,21 @@ def dm(_dm):
        ]
    )

    grp_labelled_with_size_one = _dm.new_group("labelled_with_size_one")
    grp_labelled_with_size_one.add(
        *[
            XrDataContainer(
                name=f"{n+2:d}D",
                data=create_nd_data(
                    n,
                    shape=(1,) + tuple(range(3, n + 3)) + (1,),
                    extra_coords=extra_coords,
                ),
            )
            for n in range(0, 7)
        ]
    )

    grp_ds_labelled = _dm.new_group("ds_labelled")
    grp_ds_labelled.add(
        *[