# --- FILE AGGIORNATO CON SEPARATORE MIGLIAIA CORRETTO --- import logging import pandas as pd from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView, QSizePolicy from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont, QColor # --- MODIFICA CHIAVE: Aggiornata la funzione di formattazione --- def safe_format_value(value, value_type="int", add_thousands_sep=True, add_percent=False, decimal_places=1): if pd.isna(value) or value is None: return "-" try: if value_type == "int": # Applica la formattazione con virgola e poi la sostituisce con il punto return f"{int(value):,}".replace(",", ".") if add_thousands_sep else str(int(value)) elif value_type == "float": if add_percent: return f"{float(value):.{decimal_places}f}%" else: return f"{float(value):.{decimal_places}f}" elif value_type == "percent": return f"{float(value):.{decimal_places}f}%" elif value_type == "duration": hours = value / 3600 if pd.notna(value) else 0 # Applica la formattazione con virgola e poi la sostituisce con il punto return f"{int(hours):,}".replace(",", ".") else: return str(value) except (ValueError, OverflowError, TypeError): return str(value) logger = logging.getLogger(__name__) class PivotTableEmWidget(QTableWidget): """ Un widget tabella specializzato per visualizzare dati pivot: Tipologie vs. Anni. Le colonne (anni) sono generate dinamicamente. """ cell_selected = pyqtSignal(str, str) # Emette tipologia, anno def __init__(self, theme: dict): super().__init__() self.theme = theme self.pivot_df = None self._setup_ui() def _setup_ui(self): """Imposta l'aspetto e il comportamento di base della tabella.""" self.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive) self.verticalHeader().setVisible(True) self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.cellClicked.connect(self._on_cell_clicked) self._apply_styling() def clear_table(self): """Pulisce la tabella e resetta i dati.""" self.setRowCount(0) self.setColumnCount(0) self.pivot_df = None logger.info("Tabella pivot pulita.") def populate(self, data: pd.DataFrame, metric_col: str): """ Popola la tabella pivot. Args: data (pd.DataFrame): Il DataFrame "lungo" dalla query, con colonne 'tipologia', 'anno_emissione', e le metriche. metric_col (str): Il nome della colonna metrica da visualizzare (es. 'numero_emissioni'). """ self.clear_table() if data.empty: logger.warning("Nessun dato fornito per popolare la tabella pivot.") return logger.info(f"Popolamento tabella pivot con metrica: {metric_col}") try: self.pivot_df = data.pivot_table( index='tipologia', columns='anno_emissione', values=metric_col, aggfunc='sum', fill_value=0 ).sort_index() tipologie = self.pivot_df.index.tolist() # Assicuriamoci che gli header delle colonne siano stringhe per evitare problemi anni_header = [str(col) for col in self.pivot_df.columns] self.setRowCount(len(tipologie)) self.setColumnCount(len(anni_header)) self.setVerticalHeaderLabels(tipologie) self.setHorizontalHeaderLabels(anni_header) for row_idx, tipologia in enumerate(tipologie): for col_idx, anno_col in enumerate(self.pivot_df.columns): value = self.pivot_df.loc[tipologia, anno_col] item_type = "duration" if "durata" in metric_col else "int" item = self._create_item(value, item_type) self.setItem(row_idx, col_idx, item) self.resizeColumnsToContents() except Exception as e: logger.error(f"Errore durante il popolamento della tabella pivot: {e}", exc_info=True) def _on_cell_clicked(self, row, column): """Emette un segnale con la tipologia e l'anno della cella cliccata.""" if self.pivot_df is None: return tipologia = self.verticalHeaderItem(row).text() anno = self.horizontalHeaderItem(column).text() logger.debug(f"Cella pivot selezionata: Tipologia={tipologia}, Anno={anno}") self.cell_selected.emit(tipologia, anno) def _create_item(self, value, item_type="text"): """Crea un QTableWidgetItem formattato.""" formatted_value = safe_format_value(value, item_type) if value != 0 else "" item = QTableWidgetItem(formatted_value) item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) return item def _apply_styling(self): """Applica uno stile coerente con il resto dell'applicazione.""" table_colors = self.theme.get('table', {}) fonts = self.theme.get('fonts', {}) stylesheet = f""" QTableWidget {{ gridline-color: {table_colors.get('border', '#EDEBE9')}; background-color: {table_colors.get('row_bg_primary', '#FFFFFF')}; alternate-background-color: {table_colors.get('row_bg_alternate', '#F5F8FF')}; font-family: "{fonts.get('system', 'Segoe UI')}"; font-size: {fonts.get('primary_size', 12)}px; border: 1px solid {table_colors.get('border', '#EDEBE9')}; }} QHeaderView::section {{ background-color: {table_colors.get('header_bg', '#0078D4')}; color: {table_colors.get('header_text', '#FFFFFF')}; padding: 4px; border: 1px solid {table_colors.get('border', '#EDEBE9')}; font-weight: 600; }} QTableWidget::item {{ padding: 5px; }} QTableWidget::item:selected {{ background-color: {table_colors.get('selected_bg', '#DEECF9')}; color: {table_colors.get('selected_text', '#323130')}; }} """ self.setStyleSheet(stylesheet) self.setAlternatingRowColors(True)