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

1"""下のパネルの「データ表」: 選んだデータセットの表(読むだけ)。#・X・Y と、フィットがあればフィット値・残差。 

2 

3行は描いている点と同じ visible_df(除外した行は出さない)。# はデータエディタ・カーソルの吹き出しと同じ、元の表の行の番号(1始まり)。 

4編集はデータエディタで行う。 

5""" 

6import numbers 

7 

8import numpy as np 

9from PySide6.QtCore import QAbstractTableModel, Qt 

10from PySide6.QtGui import QBrush, QColor 

11 

12from graphica.core.analysis import evaluate_saved_fit 

13from graphica.core.i18n import tr 

14from graphica.gui import theme 

15 

16ROW_NUMBER_COLUMN = 0 

17 

18 

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 

30 

31 

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 

43 

44 

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) 

53 

54 

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 

63 

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() 

94 

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) 

104 

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 

111 

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)) 

119 

120 def highlighted_row(self): 

121 return self._highlighted_row 

122 

123 def rowCount(self, parent=None): 

124 return 0 if parent is not None and parent.isValid() else len(self._row_numbers) 

125 

126 def columnCount(self, parent=None): 

127 return 0 if parent is not None and parent.isValid() else len(self._headers) 

128 

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 

136 

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] 

141 

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