Coverage for graphica/gui/dataset_style_icon.py: 96.5%
109 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"""データセット一覧の各項目のアイコン(見た目の小さなプレビューと、表示/非表示の目)。
3main_window と dataset_mixin の両方が使うので、循環 import を避けて別のモジュールにしている。
4"""
5from PySide6.QtCore import QPointF, Qt
6from PySide6.QtGui import QPixmap, QPainter, QPainterPath, QPen, QColor, QBrush, QIcon
8from graphica.core.dataset import COLOR_BY_COLUMN_PLOT_TYPE, linestyle_name
9from graphica.core.marker_style import MARKER_EDGE_COLOR, MARKER_FILL_COLOR, MARKER_FILL_HOLLOW, marker_is_unfilled
10from graphica.gui.icon_utils import icon as _icon_from_svg
12_STYLE_ICON_SIZE = (28, 14)
14# 列0はアイコンと名前、列1は表示/非表示の目
15DATASET_TREE_NAME_COLUMN = 0
16DATASET_TREE_VISIBILITY_COLUMN = 1
18# 線種の表示名(linestyle_name で揃えたもの)から Qt のペン。記号('--')だけ見ると 'dashed' が実線になる
19_LINESTYLE_TO_QT_PEN = {
20 'solid': Qt.PenStyle.SolidLine,
21 'dashed': Qt.PenStyle.DashLine,
22 'dashdot': Qt.PenStyle.DashDotLine,
23 'dotted': Qt.PenStyle.DotLine,
24}
27# マーカーを描く種類
28MARKER_PLOT_TYPES = ('Scatter', 'Line+Scatter', 'Density Scatter', COLOR_BY_COLUMN_PLOT_TYPE)
29# マーカーを描かない組み込みの種類(プラグインの種類はマーカーを使うかもしれない)
30NON_MARKER_PLOT_TYPES = ('Line', 'Area', 'Bar', 'Step')
33def marker_painter_path(marker, size):
34 """matplotlib のマーカーの形を、原点を中心にした幅 size px ほどの QPainterPath にする。描かない値は None。"""
35 from matplotlib.markers import MarkerStyle
36 from matplotlib.path import Path as MplPath
38 try:
39 style = MarkerStyle(marker)
40 except ValueError:
41 return None
42 path = style.get_path().transformed(style.get_transform())
43 if len(path.vertices) == 0:
44 return None
45 # matplotlib の形はおよそ幅 1 の Y 上向き。画面は Y 下向きなので反転する
46 def point(x, y):
47 return QPointF(x * size, -y * size)
49 painter_path = QPainterPath()
50 for vertices, code in path.iter_segments(simplify=False, curves=True):
51 if code == MplPath.MOVETO:
52 painter_path.moveTo(point(*vertices))
53 elif code == MplPath.LINETO:
54 painter_path.lineTo(point(*vertices))
55 elif code == MplPath.CURVE3: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 painter_path.quadTo(point(*vertices[:2]), point(*vertices[2:]))
57 elif code == MplPath.CURVE4:
58 painter_path.cubicTo(point(*vertices[:2]), point(*vertices[2:4]), point(*vertices[4:]))
59 elif code == MplPath.CLOSEPOLY: 59 ↛ 50line 59 didn't jump to line 50 because the condition on line 59 was always true
60 painter_path.closeSubpath()
61 return painter_path
64def _draw_marker(painter, marker, center, size, brush, pen):
65 path = marker_painter_path(marker, size)
66 if path is None:
67 return
68 painter.save()
69 painter.translate(center)
70 painter.setPen(pen)
71 painter.setBrush(brush)
72 painter.drawPath(path)
73 painter.restore()
76_MARKER_CHOICE_ICON_SIZE = 16
77# 明暗どちらのテーマの選択肢の背景でも見える中間の灰色
78_MARKER_CHOICE_ICON_COLOR = '#808080'
81def marker_choice_icon(marker):
82 """マーカーの形の選択肢に付ける形の絵。描かない値('None')は None。"""
83 if marker_painter_path(marker, 1) is None:
84 return None
85 side = _MARKER_CHOICE_ICON_SIZE
86 pixmap = QPixmap(side, side)
87 pixmap.fill(Qt.GlobalColor.transparent)
88 painter = QPainter(pixmap)
89 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
90 color = QColor(_MARKER_CHOICE_ICON_COLOR)
91 brush = Qt.BrushStyle.NoBrush if marker_is_unfilled(marker) else QBrush(color)
92 _draw_marker(painter, marker, QPointF(side / 2, side / 2), side * 0.7, brush, QPen(color, 1.2))
93 painter.end()
94 return QIcon(pixmap)
97def _marker_brush_and_pen(dataset, color):
98 """一覧のアイコン用。塗りと枠線の指定を映す(値で色分けする種類も系列の色で代わりに描く)。"""
99 edge = QColor(dataset.marker_edge_color) if dataset.marker_edge == MARKER_EDGE_COLOR else QColor(color)
100 edge.setAlphaF(color.alphaF())
101 pen = QPen(edge, max(0.0, min(dataset.marker_edge_width, 2.0)))
102 if dataset.marker_edge_width <= 0: 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true
103 pen = QPen(Qt.PenStyle.NoPen)
104 if marker_is_unfilled(dataset.marker):
105 return Qt.BrushStyle.NoBrush, QPen(edge, 1.2)
106 if dataset.marker_fill == MARKER_FILL_HOLLOW:
107 return Qt.BrushStyle.NoBrush, pen
108 if dataset.marker_fill == MARKER_FILL_COLOR:
109 face = QColor(dataset.marker_face_color)
110 face.setAlphaF(color.alphaF())
111 return QBrush(face), pen
112 return QBrush(color), pen
115def make_dataset_style_icon(dataset):
116 """色・線種・線幅・マーカー・透明度を映した小さな画像。選ばなくても一覧で見分けられるように。"""
117 width, height = _STYLE_ICON_SIZE
118 pixmap = QPixmap(width, height)
119 pixmap.fill(Qt.GlobalColor.transparent)
121 painter = QPainter(pixmap)
122 painter.setRenderHint(QPainter.RenderHint.Antialiasing)
124 color = QColor(dataset.color)
125 # 透明度が低くても一覧では見えるよう下限を設ける
126 color.setAlphaF(max(0.2, min(dataset.alpha, 1.0)))
128 show_line = dataset.plot_type in ('Line', 'Line+Scatter', 'Step')
129 show_marker = dataset.plot_type in MARKER_PLOT_TYPES
130 y = height // 2
132 if show_line:
133 pen = QPen(color, max(1.0, min(dataset.linewidth, 3.0)))
134 pen.setStyle(_LINESTYLE_TO_QT_PEN.get(linestyle_name(dataset.linestyle), Qt.PenStyle.SolidLine))
135 painter.setPen(pen)
136 painter.drawLine(2, y, width - 2, y)
138 if show_marker:
139 brush, pen = _marker_brush_and_pen(dataset, color)
140 _draw_marker(painter, dataset.marker, QPointF(width / 2, y), 8, brush, pen)
142 painter.end()
144 # Selected モードにも同じ画像を入れる(入れないと Fusion が選択時の画像を作り、塗りつぶした四角になる)
145 icon = QIcon()
146 icon.addPixmap(pixmap, QIcon.Mode.Normal)
147 icon.addPixmap(pixmap, QIcon.Mode.Selected)
148 return icon
151# 目の列の固定幅(DATASET_TREE_VISIBILITY_COLUMN_WIDTH)に収まる大きさ
152_VISIBILITY_ICON_SIZE = 16
155def make_dataset_visibility_icon(dataset):
156 name = "eye" if getattr(dataset, "visible", True) else "eye-off"
157 return _icon_from_svg(name, size=_VISIBILITY_ICON_SIZE)
160def apply_dataset_visibility_text_style(item, dataset, column=0):
161 """隠した系列の名前を text_muted の色にする。表示に戻すときは色を消してテーマの文字色に任せる。"""
162 if getattr(dataset, "visible", True):
163 item.setData(column, Qt.ItemDataRole.ForegroundRole, None)
164 else:
165 from graphica.gui import theme
166 item.setForeground(column, QBrush(QColor(theme.current_tokens()["text_muted"])))