Source code for qimchi.components.plot_filters

"""
This module contains the components for the filters menu in the plot.

# NOTE:
1. All toggles for (de-)activating filters should have a type of "apply-<filter_name>", where <filter_name> does not contain any spaces or '-'.
2. All filter options should have a type of "<filter_name>-<slider_name>", where <filter_name> & <slider_name> do not contain any spaces or '-'.

"""

from dash import dcc, html
import dash_daq as daq

# Local imports
from qimchi.components.figures import QimchiFigure
from qimchi.state import (
    load_state_from_disk,
    DEFAULT_SAVGOL_OPTS,
    DEFAULT_SMA_WINDOW,
    DEFAULT_NORM_AXIS,
    DEFAULT_GC_OPTS,
    DEFAULT_LC_OPTS,
    DEFAULT_SC_OPTS,
    DEFAULT_POLYFIT_OPTS,
    DEFAULT_RI_OPTS,  # noqa # TODOLATER: Unused
    DEFAULT_ROTA_OPTS,
)


# TODOLATER: These helper components can be used in plots.py.
def _horiz_rule() -> html.Hr:
    """
    Styled horizontal rule component with styling.

    """
    return html.Hr(
        style={
            "margin": "0.5rem 0 1rem",
            "border": "0.1px dashed gray",
            "opacity": "0.25",
        }
    )


def _label_slider(
    fig_id: str,
    label: str,
    type_str: str,
    slider_props: dict,
    slider_type: str = "Slider",
) -> html.Div:
    """
    Styled Label-(Range-)Slider component.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        label (str): Label for the slider.
        type_str (str): Type of the slider.
        slider_type (str): Type of the slider. One of "Slider" or "RangeSlider".
        **slider_props (dict): Slider properties. The keys are:
            - min (int): Minimum value.
            - max (int): Maximum value.
            - step (int | float): Step value.
            - val (int): Initial value.
            - marks (dict): Marks for the slider. Optional.

    Returns:
        dash.html.Div: Slider or RangeSlider component with Label and styling.

    """
    _min, _max, _step, _val, _marks = (
        slider_props.get("min"),
        slider_props.get("max"),
        slider_props.get("step"),
        slider_props.get("val"),
        slider_props.get("marks"),
    )

    slider = (
        dcc.RangeSlider(
            id={"index": fig_id, "type": type_str, "menu": "filter-opts"},
            min=_min,
            max=_max,
            step=_step,
            value=_val,
            updatemode="drag",
            persistence=True,
            persistence_type="local",
            className="column is-10 mx-0 my-2 px-0",
        )
        if slider_type == "RangeSlider"
        else dcc.Slider(
            id={"index": fig_id, "type": type_str, "menu": "filter-opts"},
            min=_min,
            max=_max,
            step=_step,
            value=_val,
            updatemode="drag",
            tooltip={
                "placement": "bottom",
                "always_visible": True,
            },
            persistence=True,
            persistence_type="local",
            className="column is-10 mx-0 my-2 px-0",
        )
    )

    if _marks:
        slider.marks = _marks

    return html.Div(
        [
            html.Label(
                label,
                className="column is-2 px-0",
                style={"fontWeight": "bold"},
            ),
            slider,
        ],
        className="column is-full is-flex p-0",
    )


def _label_toggle(
    fig_id: str,
    label: str,
    type_str: str,
    val: bool,
    div_title: str,
) -> html.Div:
    """
    Styled Label-ToggleSwitch component.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        label (str): Label for the toggle switch.
        type_str (str): Type of the toggle switch.
        value (bool): Initial value.
        div_title (str): Title for the div.

    Returns:
        dash.html.Div: ToggleSwitch component with Label and styling.

    """
    return html.Div(
        [
            html.Label(
                label,
                className="column is-10 px-0",
                style={"fontWeight": "bold"},
            ),
            daq.ToggleSwitch(
                id={"index": fig_id, "type": type_str, "menu": "filter-apply"},
                value=val,
                persistence=True,
                persistence_type="local",
                className="column is-2 px-0",
            ),
        ],
        className="column is-full is-flex px-2 py-0 mt-2 has-background-grey-lighter",
        title=div_title,
    )


def _label_dropdown(
    fig_id: str,
    label: str,
    type_str: str,
    options: list,
    val: str,
    placeholder: str,
    div_title: str,
) -> html.Div:
    """
    Styled Label-Dropdown component.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        label (str): Label for the dropdown.
        type_str (str): Type of the dropdown.
        options (list): List of options for the dropdown.
        value (str): Initial value.
        placeholder (str): Placeholder for the dropdown.
        div_title (str): Title for the div.

    Returns:
        dash.html.Div: Dropdown component with Label and styling.

    """
    return html.Div(
        [
            html.Label(
                label,
                className="column is-2 px-0",
                style={"fontWeight": "bold"},
            ),
            dcc.Dropdown(
                id={"index": fig_id, "type": type_str, "menu": "filter-opts"},
                options=options,
                value=val,
                placeholder=placeholder,
                persistence=True,
                persistence_type="local",
                # NOTE: className="column is-10" refuses to work here
                style={"width": "100%", "margin": "0", "padding": "0"},
            ),
        ],
        className="column is-full is-flex p-0",
        title=div_title,
    )


def _log_scale_comp(fig_id: str, applied_filters_order: list) -> dcc.Tab:
    """
    Log-scaling component. For both 1D LinePlots and 2D HeatMaps.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.

    Returns:
        dcc.Tab: Log-scaling component.

    """
    log_scale_toggled = "log_scale" in applied_filters_order
    log_scaling = dcc.Tab(
        label="Log Scale",
        children=[
            _label_toggle(
                fig_id,
                "Apply Log-scaling",
                "apply-log_scale",
                log_scale_toggled,
                div_title="Apply log-scaling to the plot",
            )
        ],
        className="column p-1",
    )

    return log_scaling


def _flip_comp(fig_id: str, applied_filters_order: list) -> dcc.Tab:
    """
    Flip component. Only for 2D HeatMaps.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.

    Returns:
        dcc.Tab: Flip component.

    """
    flip_toggled = "flip" in applied_filters_order
    flip = dcc.Tab(
        label="Flip",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Flip HeatMap",
                "apply-flip",
                flip_toggled,
                div_title="Flip the plot",
            )
        ],
    )

    return flip


def _fit_comp(fig_id: str, applied_filters_order: list) -> dcc.Tab:
    """
    PolyFit component. Only for 1D LinePlots.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.

    Returns:
        dcc.Tab: PolyFit component.

    """
    fit_toggled = "polyfit" in applied_filters_order
    fit = dcc.Tab(
        label="PolyFit",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Apply PolyFit",
                "apply-polyfit",
                fit_toggled,
                div_title="Apply polynomial fitting to the plot",
            ),
            _horiz_rule(),
            _label_slider(
                fig_id,
                "Degree",
                "polyfit-deg",
                slider_type="Slider",
                slider_props={
                    "min": 1,
                    "max": 5,
                    "step": 1,
                    "val": DEFAULT_POLYFIT_OPTS["deg"],
                },
            ),
            _label_slider(
                fig_id,
                "Window",
                "polyfit-window",
                slider_type="RangeSlider",
                slider_props={
                    "min": DEFAULT_POLYFIT_OPTS["window"][0],
                    "max": DEFAULT_POLYFIT_OPTS["window"][1],
                    "step": 0.01,
                    "val": DEFAULT_POLYFIT_OPTS["window"],
                    "marks": {0: "0", 1: "1"},
                },
            ),
        ],
    )

    return fit


def _smooth_comp(fig_id: str, applied_filters_order: list, num_axes: int) -> dcc.Tab:
    """
    Smoothing-related components, based on the number of axes in the figure. Has:
    - Savitzky-Golay (1D LinePlots & 2D HeatMaps)
    - Simple Moving Average (1D LinePlots)

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.
        num_axes (int): Number of axes in the figure.

    Returns:
        dcc.Tab: Smoothing-related components.

    """
    _is_hmap = num_axes == 2

    # Savitzky-Golay
    savgol_toggled = "savgol" in applied_filters_order
    sav_gol = dcc.Tab(
        label="SavGol",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Apply SavGol",
                "apply-savgol",
                savgol_toggled,
                div_title="Apply Savitzky-Golay smoothing to the plot",
            ),
            _horiz_rule(),
            # savgol_filter options
            _label_slider(
                fig_id,
                "Window",
                "savgol-window",
                slider_type="Slider",
                slider_props={
                    "min": 2,
                    "max": 20,
                    "step": 1,
                    "val": DEFAULT_SAVGOL_OPTS["window"],
                },
            ),
            _label_slider(
                fig_id,
                "Polyorder",
                "savgol-polyorder",
                slider_type="Slider",
                slider_props={
                    "min": 2,
                    "max": DEFAULT_SAVGOL_OPTS["window"] - 1,
                    "step": 1,
                    "val": DEFAULT_SAVGOL_OPTS["polyorder"],
                },
            ),
            # Axis to smooth over | axis: int
            _label_dropdown(
                fig_id,
                "Along Axis",
                "savgol-axis",
                [
                    {"label": "X", "value": 0},
                    {"label": "Y", "value": 1},
                    {
                        "label": "Z",
                        # NOTE: savgol_filter supports only 1D. We emulate 2D by applying it to each axis separately.
                        "value": 2,
                    },
                ],
                DEFAULT_SAVGOL_OPTS["axis"],
                "Select axis along which to smooth",
                div_title="Select axis along which to smooth",
            )
            if _is_hmap
            else None,
            # deriv: int
            _label_slider(
                fig_id,
                "Derivative",
                "savgol-deriv",
                slider_type="Slider",
                slider_props={
                    "min": 0,
                    "max": 5,
                    "step": 1,
                    "val": DEFAULT_SAVGOL_OPTS["deriv"],
                },
            ),
            # Sample spacing | delta: float
            # NOTE: Only used if deriv > 0.
            _label_slider(
                fig_id,
                "Delta",
                "savgol-delta",
                slider_type="Slider",
                slider_props={
                    "min": 1.0,
                    "max": 10.0,
                    "val": DEFAULT_SAVGOL_OPTS["delta"],
                },
            ),
            # Type of padding | mode: str
            _label_dropdown(
                fig_id,
                "Mode",
                "savgol-mode",
                [
                    {"label": "interp", "value": "interp"},
                    {"label": "nearest", "value": "nearest"},
                    {"label": "wrap", "value": "wrap"},
                    {"label": "mirror", "value": "mirror"},
                    {"label": "constant", "value": "constant"},
                ],
                DEFAULT_SAVGOL_OPTS["mode"],
                "Select mode",
                div_title="Select mode for the Savitzky-Golay filter",
            ),
            # Fill value if mode is 'constant' | cval: float
            _label_slider(
                fig_id,
                "Constant Fill",
                "savgol-cval",
                slider_type="Slider",
                slider_props={
                    "min": 0.0,
                    "max": 1,
                    "val": DEFAULT_SAVGOL_OPTS["cval"],
                },
            ),
        ],
    )

    # Simple Moving Average
    sma_toggled = "sma" in applied_filters_order
    sma = dcc.Tab(
        label="SMA",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Apply SMA",
                "apply-sma",
                sma_toggled,
                div_title="Calculate Simple Moving Average for the plot",
            ),
            html.Div(
                html.P(
                    [
                        html.Strong("⚠️ WARNING: ", style={"color": "#050505"}),
                        "Output length is same as the input length because of ",
                        html.Code("mode='same'"),
                        " in ",
                        html.Code("np.convolve()"),
                        ". Beware of the edge effects.",
                    ],
                    style={
                        "fontSize": "0.85rem",
                        "margin": "0",
                    },
                ),
                style={
                    "padding": "0.5rem 1rem",
                    "margin": "0.5rem 0",
                    "backgroundColor": "#fff8c5",
                    "border": "1px solid #f0b429",
                    "borderRadius": "4px",
                    "borderLeft": "4px solid #f0b429",
                },
            ),
            _horiz_rule(),
            _label_slider(
                fig_id,
                "Window",
                "sma-window",
                slider_type="Slider",
                slider_props={
                    "min": 2,
                    "max": 20,
                    "step": 1,
                    "val": DEFAULT_SMA_WINDOW,
                },
            ),
        ],
    )

    smooth_options = [sav_gol]
    if num_axes == 1:
        smooth_options.append(sma)

    smooth_options = dcc.Tabs(
        smooth_options,
        id={"type": "smooth-options-tabs", "index": fig_id},
        persistence=True,
        persistence_type="local",
    )

    smooth = dcc.Tab(label="Smooth", className="p-1", children=smooth_options)

    return smooth


def _max_norm_comp(
    sess_id: str,
    fig_id: str,
    applied_filters_order: list,
) -> dcc.Tab:
    """
    Max-Normalize component. For both 1D LinePlots and 2D HeatMaps.

    Args:
        sess_id (str): Session ID to load the state for
        fig_id (str): Unique identifier for the figure in the plot
        applied_filters_order (list): List of applied filters

    Returns:
        dcc.Tab: Max-Normalize component.

    """
    _state = load_state_from_disk(sess_id)

    is_hmap = _state.plot_states[fig_id]["plot_type"] == "HeatMap"
    max_norm_toggled = "normalize" in applied_filters_order
    max_norm = dcc.Tab(
        label="Normalize",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Apply Normalize",
                "apply-normalize",
                max_norm_toggled,
                div_title="Max-normalize the plot",
            ),
            _horiz_rule() if is_hmap else None,
            (
                _label_dropdown(
                    fig_id,
                    "Along Axis",
                    "normalize-axis",
                    [
                        {"label": "X", "value": "x"},
                        {"label": "Y", "value": "y"},
                        {"label": "Z", "value": "z"},
                    ],
                    DEFAULT_NORM_AXIS,
                    "Select axis label",
                    div_title="Select axis label to normalize along",
                )
                if is_hmap
                else None
            ),
        ],
    )

    return max_norm


def _diff_comp(fig_id: str, applied_filters_order: list, num_axes: int) -> dcc.Tab:
    """
    Differentiate component, based on the number of axes in the figure. Has:
    - Differentiate (1D LinePlots)
    - Differentiate X & Y (2D HeatMaps)

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.
        num_axes (int): Number of axes in the figure.

    Returns:
        dcc.Tab: Differentiate component.

    """
    diff_opts = []
    if num_axes == 1:
        diff_toggled = "diff" in applied_filters_order
        diff_opts = [
            _label_toggle(
                fig_id,
                "Differentiate",
                "apply-diff",
                diff_toggled,
                div_title="Take first derivative",
            ),
        ]
    elif num_axes == 2:
        diff_x_toggled = "diff_x" in applied_filters_order
        differentiate_x = _label_toggle(
            fig_id,
            "Differentiate along Y",
            "apply-diff_x",
            diff_x_toggled,
            div_title="Take first derivative along the Y-axis",
        )

        diff_y_toggled = "diff_y" in applied_filters_order
        differentiate_y = _label_toggle(
            fig_id,
            "Differentiate along X",
            "apply-diff_y",
            diff_y_toggled,
            div_title="Take first derivative along the X-axis",
        )

        diff_opts = [
            differentiate_x,
            differentiate_y,
        ]

    diff = dcc.Tab(
        label="Differentiate",
        className="p-1",
        children=diff_opts,
    )
    return diff


def _contrast_comp(fig_id: str, applied_filters_order: list) -> dcc.Tab:
    """
    Contrast-related components. Only for 2D HeatMaps. Has:
    - Gamma Correction
    - Log Correction
    - Sigmoid Correction
    - Rescale Intensity

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.

    Returns:
        dcc.Tab: Contrast-related components.

    """
    gamma_corr_toggled = "gamma_corr" in applied_filters_order
    log_corr_toggled = "log_corr" in applied_filters_order
    sig_corr_toggled = "sig_corr" in applied_filters_order
    rescale_intensity_toggled = "rescale_intensity" in applied_filters_order

    gamma = dcc.Tab(
        label="Gamma Correction",
        children=[
            _label_toggle(
                fig_id,
                "Apply Gamma Correction",
                "apply-gamma_corr",
                gamma_corr_toggled,
                div_title="Apply gamma correction to the plot",
            ),
            _horiz_rule(),
            _label_slider(
                fig_id,
                "Gamma (γ)",
                "gamma_corr-gamma",
                slider_type="Slider",
                slider_props={
                    "min": 0,
                    "max": 10,
                    "step": 0.1,
                    "val": DEFAULT_GC_OPTS["gamma"],
                    "marks": {0: "0", 10: "10"},
                },
            ),
            _label_slider(
                fig_id,
                "Gain",
                "gamma_corr-gain",
                slider_type="Slider",
                slider_props={
                    "min": 1,
                    "max": 10,
                    "step": 0.1,
                    "val": DEFAULT_GC_OPTS["gain"],
                    "marks": {1: "1", 10: "10"},
                },
            ),
        ],
        className="column p-1",
    )

    log_corr = dcc.Tab(
        label="Log Correction",
        children=[
            _label_toggle(
                fig_id,
                "Apply Log Correction",
                "apply-log_corr",
                log_corr_toggled,
                div_title="Apply log correction to the plot",
            ),
            _horiz_rule(),
            html.Div(
                [
                    html.Label(
                        "Gain",
                        className="column is-1 px-0",
                        style={"fontWeight": "bold"},
                    ),
                    dcc.Slider(
                        id={
                            "index": fig_id,
                            "type": "log_corr-gain",
                            "menu": "filter-opts",
                        },
                        min=1,
                        max=10,
                        step=0.1,
                        marks={1: "1", 10: "10"},
                        value=DEFAULT_LC_OPTS["gain"],
                        updatemode="drag",
                        tooltip={
                            "placement": "bottom",
                            "always_visible": True,
                        },
                        persistence=True,
                        persistence_type="local",
                        className="column is-9 mx-0 my-2 px-0",
                    ),
                    daq.ToggleSwitch(
                        label="Invert Log",
                        labelPosition="bottom",
                        id={
                            "index": fig_id,
                            "type": "log_corr-inv",
                            "menu": "filter-opts",
                        },
                        value=DEFAULT_LC_OPTS["inv"],
                        persistence=True,
                        persistence_type="local",
                        className="column is-2 m-0 px-0",
                    ),
                ],
                className="column is-full is-flex p-0",
            ),
        ],
        className="column p-1",
    )

    sig_corr = dcc.Tab(
        label="Sigmoid Correction",
        children=[
            _label_toggle(
                fig_id,
                "Apply Sigmoid Correction",
                "apply-sig_corr",
                sig_corr_toggled,
                div_title="Apply sigmoid correction to the plot",
            ),
            _horiz_rule(),
            _label_slider(
                fig_id,
                "Cutoff",
                "sig_corr-cutoff",
                slider_type="Slider",
                slider_props={
                    "min": 0,
                    "max": 1,
                    "step": 0.1,
                    "val": DEFAULT_SC_OPTS["cutoff"],
                    "marks": {0: "0", 1: "1"},
                },
            ),
            _label_slider(
                fig_id,
                "Gain",
                "sig_corr-gain",
                slider_type="Slider",
                slider_props={
                    "min": 1,
                    "max": 100,
                    "step": 1,
                    "val": DEFAULT_SC_OPTS["gain"],
                    "marks": {1: "1", 100: "100"},
                },
            ),
        ],
        className="column p-1",
    )

    rescale_intensity = dcc.Tab(
        label="Rescale Intensity",
        children=[
            _label_toggle(
                fig_id,
                "Rescale Intensity",
                "apply-rescale_intensity",
                rescale_intensity_toggled,
                div_title="Rescale intensity of the plot",
            )
            # TODOLATER: Can allow specifying the range to scale to.
            # TODOLATER: See: https://scikit-image.org/docs/stable/api/skimage.exposure.html#skimage.exposure.rescale_intensity
        ],
        className="column p-1",
    )

    contrast_options = dcc.Tabs(
        [
            gamma,
            log_corr,
            sig_corr,
            rescale_intensity,
        ],
        id={"type": "contrast-options-tabs", "index": fig_id},
        persistence=True,
        persistence_type="local",
    )

    contrast = dcc.Tab(
        label="Contrast",
        className="p-1",
        children=contrast_options,
    )

    return contrast


def _rota_comp(fig_id: str, applied_filters_order: list) -> dcc.Tab:
    """
    Rotate component. Only for 2D HeatMaps.

    Args:
        fig_id (str): Unique identifier for the figure in the plot.
        applied_filters_order (list): List of applied filters.

    Returns:
        dcc.Tab: Rotate component.

    """
    rotate_toggled = "rotate" in applied_filters_order
    rotate = dcc.Tab(
        label="Rotate",
        className="p-1",
        children=[
            _label_toggle(
                fig_id,
                "Rotate HeatMap",
                "apply-rotate",
                rotate_toggled,
                div_title="Rotate the plot",
            ),
            # _horiz_rule(), # TODO: Looks out of place. Your call @s.anupam.
            daq.Knob(
                id={"type": "rotate-angle", "index": fig_id, "menu": "filter-opts"},
                label={
                    "label": "Angle",
                    "style": {"fontSize": "1.2em", "fontWeight": "bold"},
                },
                labelPosition="bottom",
                value=DEFAULT_ROTA_OPTS["angle"],
                min=-180,
                max=180,
                size=200,
                scale={
                    "start": -180,
                    "labelInterval": 45,
                    "custom": {
                        -180: "-180",
                        -135: "-135",
                        -90: "-90",
                        -45: "-45",
                        0: "0",
                        45: "45",
                        90: "90",
                        135: "135",
                        180: "180",
                    },
                },
                persistence=True,
                persistence_type="local",
            ),
        ],
    )

    return rotate


[docs] def content_filters( sess_id: str, fig: QimchiFigure, fig_id: str, ) -> html.Div: """ Generates the filters menu for the plot. Args: sess_id (str): Session ID to identify the data. fig (QimchiFigure): QimchiFigure object. fig_id (str): Unique identifier for the figure in the plot. Returns: dash.html.Div: Filters menu container. """ _state = load_state_from_disk(sess_id) fig_plot_menu = _state.plot_states[fig_id]["plots_menu"] applied_filters_order: list = _state.plot_states[fig_id]["filters_order"] num_axes = fig.num_axes # "Common" filters filters = [ _log_scale_comp(fig_id, applied_filters_order), # NOTE: Depending on the number of axes, these filters will be different _smooth_comp(fig_id, applied_filters_order, num_axes), _diff_comp(fig_id, applied_filters_order, num_axes), _max_norm_comp(sess_id, fig_id, applied_filters_order), ] # 1D LinePlot filters if num_axes == 1: filters.extend( [ _fit_comp(fig_id, applied_filters_order), ] ) # 2D HeatMap filters elif num_axes == 2: filters.extend( [ _contrast_comp(fig_id, applied_filters_order), _rota_comp(fig_id, applied_filters_order), _flip_comp(fig_id, applied_filters_order), ] ) return html.Div( [ dcc.Tabs( filters, id={"type": "content-filters-tabs", "index": fig_id}, persistence=True, persistence_type="local", className="column is-full px-0 py-2", ) ], style={"display": fig_plot_menu["content_filters_disp"]}, id={"type": "content-filters", "index": fig_id}, )