Coverage for graphica/gui/data_table_model.py: 90.1%
131 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""下のパネルの「データ表」: 選んだデータセットの表(読むだけ)。#・X・Y と、フィットがあればフィット値・残差。
3行は描いている点と同じ visible_df(除外した行は出さない)。# はデータエディタ・カーソルの吹き出しと同じ、元の表の行の番号(1始まり)。
4編集はデータエディタで行う。
5"""
6import numbers
8import numpy as np
9from PySide6.QtCore import QAbstractTableModel, Qt
10from PySide6.QtGui import QBrush, QColor
12from graphica.core.analysis import evaluate_saved_fit
13from graphica.core.i18n import tr
14from graphica.gui import theme
16ROW_NUMBER_COLUMN = 0
19def fit_result_for(dataset, datasets):
20 """dataset のフィット結果。自分が持っていなければ、dataset から作った最後のフィットの曲線のもの。"""
21 if dataset is None:
22 return None
23 if dataset.fit_result:
24 return dataset.fit_result
25 for other in reversed(list(datasets)):
26 result = getattr(other, 'fit_result', None)
27 if result and result.get('source_dataset_id') == dataset.dataset_id:
28 return result
29 return None
32def fit_dataset_for(dataset, datasets):
33 """fit_result_for と同じ規則で、フィットの文(fit_info)を持つデータセット。"""
34 if dataset is None:
35 return None
36 if dataset.fit_result or dataset.fit_info:
37 return dataset
38 for other in reversed(list(datasets)):
39 result = getattr(other, 'fit_result', None)
40 if result and result.get('source_dataset_id') == dataset.dataset_id:
41 return other
42 return None
45def _format(value):
46 if value is None: 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
47 return ""
48 if isinstance(value, numbers.Real) and not isinstance(value, bool): 48 ↛ 52line 48 didn't jump to line 52 because the condition on line 48 was always true
49 if isinstance(value, float) and np.isnan(value): 49 ↛ 50line 49 didn't jump to line 50 because the condition on line 49 was never true
50 return ""
51 return f"{value:.6g}"
52 return str(value)
55class DatasetTableModel(QAbstractTableModel):
56 def __init__(self, parent=None):
57 super().__init__(parent)
58 self._headers = []
59 self._row_numbers = np.empty(0, dtype=int)
60 self._columns = [] # 列ごとの値の並び(# の列を除く)
61 self._residual_column = None
62 self._highlighted_row = None
64 def set_dataset(self, dataset, fit_result=None):
65 self.beginResetModel()
66 self._highlighted_row = None
67 self._residual_column = None
68 if dataset is None or getattr(dataset, 'data_kind', '1d') == '2d_grid' or dataset.df is None:
69 self._headers, self._row_numbers, self._columns = [], np.empty(0, dtype=int), []
70 self.endResetModel()
71 return
72 df = dataset.df
73 visible = dataset.visible_df
74 positions = df.index.get_indexer(visible.index)
75 self._row_numbers = positions + 1
76 x_name, y_name = dataset.x_col_name, dataset.y_col_name
77 x_values = visible[x_name].tolist() if x_name in visible.columns else [None] * len(visible)
78 y_values = visible[y_name].tolist() if y_name in visible.columns else [None] * len(visible)
79 self._headers = ["#", x_name or "X", y_name or "Y"]
80 self._columns = [x_values, y_values]
81 fitted = self._fit_values(fit_result, x_values)
82 if fitted is not None:
83 y_numeric = np.asarray([v if isinstance(v, numbers.Real) else np.nan for v in y_values], dtype=float)
84 residuals = y_numeric - fitted
85 x_range = fit_result.get('x_range')
86 if x_range: 86 ↛ 87line 86 didn't jump to line 87 because the condition on line 86 was never true
87 x_numeric = np.asarray([v if isinstance(v, numbers.Real) else np.nan for v in x_values], dtype=float)
88 outside = (x_numeric < min(x_range)) | (x_numeric > max(x_range))
89 residuals[outside] = np.nan
90 self._headers += [tr("フィット値"), tr("残差")]
91 self._columns += [fitted.tolist(), residuals.tolist()]
92 self._residual_column = len(self._headers) - 1
93 self.endResetModel()
95 @staticmethod
96 def _fit_values(fit_result, x_values):
97 if not fit_result:
98 return None
99 try:
100 x_numeric = np.asarray(x_values, dtype=float)
101 except (TypeError, ValueError):
102 return None
103 return evaluate_saved_fit(fit_result, x_numeric)
105 def row_for_number(self, row_number):
106 """元の表の行の番号(1始まり)の、この表での行。出していなければ None。"""
107 if row_number is None: 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 return None
109 found = np.nonzero(self._row_numbers == row_number)[0]
110 return int(found[0]) if len(found) else None
112 def set_highlighted_row(self, row):
113 if row == self._highlighted_row:
114 return
115 previous, self._highlighted_row = self._highlighted_row, row
116 for changed in (previous, row):
117 if changed is not None and 0 <= changed < self.rowCount():
118 self.dataChanged.emit(self.index(changed, 0), self.index(changed, self.columnCount() - 1))
120 def highlighted_row(self):
121 return self._highlighted_row
123 def rowCount(self, parent=None):
124 return 0 if parent is not None and parent.isValid() else len(self._row_numbers)
126 def columnCount(self, parent=None):
127 return 0 if parent is not None and parent.isValid() else len(self._headers)
129 def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
130 if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
131 return self._headers[section] if 0 <= section < len(self._headers) else None
132 if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.TextAlignmentRole:
133 return int(Qt.AlignmentFlag.AlignCenter if section == ROW_NUMBER_COLUMN
134 else Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
135 return None
137 def value(self, row, column):
138 if column == ROW_NUMBER_COLUMN:
139 return int(self._row_numbers[row])
140 return self._columns[column - 1][row]
142 def data(self, index, role=Qt.ItemDataRole.DisplayRole):
143 if not index.isValid(): 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true
144 return None
145 row, column = index.row(), index.column()
146 if role == Qt.ItemDataRole.DisplayRole:
147 return _format(self.value(row, column))
148 if role == Qt.ItemDataRole.TextAlignmentRole:
149 return int(Qt.AlignmentFlag.AlignCenter if column == ROW_NUMBER_COLUMN
150 else Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
151 if role == Qt.ItemDataRole.ForegroundRole:
152 tokens = theme.current_tokens()
153 if column == ROW_NUMBER_COLUMN:
154 return QBrush(QColor(tokens["text_muted"]))
155 if column == self._residual_column:
156 value = self.value(row, column)
157 if isinstance(value, float) and value < 0: 157 ↛ 159line 157 didn't jump to line 159 because the condition on line 157 was always true
158 return QBrush(QColor(tokens["danger"]))
159 return None
160 if role == Qt.ItemDataRole.BackgroundRole and row == self._highlighted_row: 160 ↛ 161line 160 didn't jump to line 161 because the condition on line 160 was never true
161 return QBrush(theme.current_accent_soft_qcolor())
162 return None