File: common.py

package info (click to toggle)
python-sigima 1.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 25,608 kB
  • sloc: python: 35,251; makefile: 3
file content (227 lines) | stat: -rw-r--r-- 8,236 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Common utilities for scalar result objects
==========================================

This module provides shared functionality for TableResult and GeometryResult classes
without using inheritance or mixins, maintaining their dataclass integrity.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pandas as pd

if TYPE_CHECKING:
    from sigima.objects import GeometryResult, ImageObj, SignalObj, TableResult

# Sentinel value for "full signal/image / no ROI" rows in result tables
NO_ROI: int = -1


class DisplayPreferencesManager:
    """Manages display preferences for result objects."""

    @staticmethod
    def get_display_preferences(
        result: GeometryResult | TableResult,
        headers: list[str],
        attr_name: str = "hidden_headers",
    ) -> dict[str, bool]:
        """Get display preferences for headers.

        Args:
            result: The result object containing attrs
            headers: List of header names
            attr_name: Name of the attribute storing hidden headers

        Returns:
            Dictionary mapping header names to visibility (True=visible, False=hidden)
        """
        prefs = {}
        hidden_headers = result.attrs.get(attr_name, set())
        if isinstance(hidden_headers, (list, tuple)):
            hidden_headers = set(hidden_headers)

        for header in headers:
            prefs[header] = header not in hidden_headers
        return prefs

    @staticmethod
    def set_display_preferences(
        result: GeometryResult | TableResult,
        preferences: dict[str, bool],
        headers: list[str],
        attr_name: str = "hidden_headers",
    ) -> None:
        """Set display preferences for headers.

        Args:
            result: The result object to modify
            preferences: Dictionary mapping header names to visibility
            headers: List of valid header names
            attr_name: Name of the attribute to store hidden headers
        """
        hidden_headers = {
            header
            for header, visible in preferences.items()
            if not visible and header in headers
        }
        if hidden_headers:
            result.attrs[attr_name] = list(hidden_headers)
        elif attr_name in result.attrs:
            del result.attrs[attr_name]

    @staticmethod
    def get_visible_headers(
        result: GeometryResult | TableResult,
        headers: list[str],
        attr_name: str = "hidden_headers",
    ) -> list[str]:
        """Get list of currently visible headers.

        Args:
            result: The result object
            headers: List of all header names
            attr_name: Name of the attribute storing hidden headers

        Returns:
            List of header names that should be displayed
        """
        prefs = DisplayPreferencesManager.get_display_preferences(
            result, headers, attr_name
        )
        return [header for header in headers if prefs.get(header, True)]


class DataFrameManager:
    """Manages DataFrame operations for result objects."""

    @staticmethod
    def apply_visible_only_filter(
        df: pd.DataFrame, visible_headers: list[str]
    ) -> pd.DataFrame:
        """Apply visible-only filter to a DataFrame.

        Args:
            df: DataFrame to filter
            visible_headers: List of headers that should be visible

        Returns:
            Filtered DataFrame with only visible columns
        """
        # Keep roi_index column if present
        if "roi_index" in df.columns:
            visible_headers = ["roi_index"] + visible_headers

        # Filter to only available visible columns
        available_headers = [col for col in visible_headers if col in df.columns]
        if available_headers:
            return df[available_headers]
        return df


class ResultHtmlGenerator:
    """Utility class for generating HTML from result objects using composition."""

    @staticmethod
    def generate_html(
        result: GeometryResult | TableResult,
        obj: SignalObj | ImageObj | None = None,
        visible_only: bool = True,
        transpose_single_row: bool = True,
        **kwargs,
    ) -> str:
        """Generate HTML from a result object.

        Args:
            result: The result object (TableResult or GeometryResult)
            obj: SignalObj or ImageObj for ROI title extraction
            visible_only: If True, include only visible headers based on display
             preferences. Default is False.
            transpose_single_row: If True, transpose the table when there's only one row
            **kwargs: Additional arguments passed to DataFrame.to_html()

        Returns:
            HTML representation of the result
        """
        df = result.to_dataframe(visible_only=visible_only)

        # Remove roi_index column for display
        if "roi_index" in df.columns:
            roi_indices = df["roi_index"].tolist()
            df = df.drop(columns=["roi_index"])
        else:
            roi_indices = None

        # Create row headers
        row_headers = ResultHtmlGenerator._get_row_headers(result, roi_indices, obj)

        # Transpose if single row and flag is set
        if transpose_single_row and len(df) == 1:
            # Transpose the dataframe
            df_t = df.T
            df_t.columns = [row_headers[0] if row_headers[0] else "Value"]
            df_t.index.name = "Item"
            # Get labels for the transposed view
            display_labels = list(df.columns)
            df_t.index = display_labels
            text = f'<u><b style="color: #5294e2">{result.title}</b></u>:'
            html_kwargs = {"border": 0}
            html_kwargs.update(kwargs)
            # Format numeric columns only, avoiding float_format on mixed data types
            for col in df_t.select_dtypes(include=["number"]).columns:
                df_t[col] = df_t[col].map(lambda x: f"{x:.3g}" if pd.notna(x) else x)
            text += df_t.to_html(**html_kwargs)
        else:
            # Standard horizontal layout
            df.index = row_headers
            text = f'<u><b style="color: #5294e2">{result.title}</b></u>:'
            html_kwargs = {"border": 0}
            html_kwargs.update(kwargs)
            # Format numeric columns only, avoiding float_format on mixed data types
            for col in df.select_dtypes(include=["number"]).columns:
                df[col] = df[col].map(lambda x: f"{x:.3g}" if pd.notna(x) else x)
            text += df.to_html(**html_kwargs)

        return text

    @staticmethod
    def _get_row_headers(
        result: TableResult | GeometryResult,
        roi_indices: list[int] | None,
        obj: SignalObj | ImageObj | None,
    ) -> list[str]:
        """Create row headers from ROI indices.

        .. note::

           Handles gracefully the case where:
           - `obj` is None: uses generic "ROI N" headers instead of ROI titles
           - `roi_indices` reference ROIs that no longer exist in `obj.roi`
             (e.g., if HTML rendering happens before result recomputation after
             ROI deletion)
        """
        row_headers = []
        if roi_indices is not None:
            for roi_idx in roi_indices:
                if roi_idx == NO_ROI:
                    header = ""
                else:
                    header = f"ROI {roi_idx}"
                    # Try to get ROI title from object if available
                    if obj is not None and obj.roi is not None:
                        # Check if roi_idx is valid (defensive against stale indices)
                        if 0 <= roi_idx < len(obj.roi.single_rois):
                            header = obj.roi.get_single_roi_title(roi_idx)
                        # else: keep default "ROI {roi_idx}" for out-of-bounds indices
                row_headers.append(header)
        else:
            # Need to get DataFrame to know the number of rows
            df = result.to_dataframe()
            if "roi_index" in df.columns:
                df = df.drop(columns=["roi_index"])
            row_headers = [""] * len(df)
        return row_headers