Coverage for src/plotly_gtk/widgets/legend.py: 10%

145 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-11-08 21:22 +0000

1import gi 

2 

3gi.require_version("Gdk", "4.0") 

4gi.require_version("Gtk", "4.0") 

5from typing import TYPE_CHECKING 

6 

7import numpy as np 

8from gi.repository import Gdk, Gtk # noqa: E402 

9 

10from plotly_gtk.utils import parse_color, update_dict 

11from plotly_gtk.widgets.base import Base 

12 

13if TYPE_CHECKING: 

14 from plotly_gtk.chart import PlotlyGtk 

15 

16 

17class Legend(Base): 

18 def __init__(self, plot: "PlotlyGtk", legend: dict): 

19 super().__init__() 

20 grid = Gtk.Grid(row_spacing=4, column_spacing=4) 

21 self.append(grid) 

22 

23 if legend["xref"] == "paper": 

24 x_default = 1.02 if legend["orientation"] == "v" else 0 

25 elif legend["xref"] == "container": 

26 x_default = 1 if legend["orientation"] == "v" else 0 

27 else: 

28 raise ValueError(f"Unknown legend.xref {legend["xref"]}") 

29 

30 if legend["yref"] == "paper": 

31 y_default = 1 if legend["orientation"] == "v" else -0.1 

32 elif legend["yref"] == "container": 

33 y_default = 1 

34 else: 

35 raise ValueError(f"Unknown legend.yref {legend["yref"]}") 

36 

37 default = dict(bgcolor=plot.layout["paper_bgcolor"], x=x_default, y=y_default) 

38 legend = update_dict(default, legend) 

39 

40 if legend["yanchor"] == "auto": 

41 yanchor_default = ( 

42 "top" if legend["y"] > 2 / 3 else "bottom" if legend["y"] < 1 / 3 else 0 

43 ) 

44 legend["yanchor"] = yanchor_default 

45 

46 self.spec = legend 

47 

48 if legend["orientation"] == "v": 

49 self.set_orientation(Gtk.Orientation.VERTICAL) 

50 elif legend["orientation"] == "h": 

51 self.set_orientation(Gtk.Orientation.HORIZONTAL) 

52 else: 

53 raise ValueError(f"Unknown legend.orientation {legend["orientation"]}") 

54 

55 def on_click(n, _trace): 

56 if n == 1: 

57 action = legend["itemclick"] 

58 elif n == 2: 

59 action = legend["itemdoubleclick"] 

60 

61 if "legendgroup" in _trace and _trace["legendgroup"] != "": 

62 _traces = [ 

63 t 

64 for t in plot.data 

65 if "legendgroup" in t and t["legendgroup"] == _trace["legendgroup"] 

66 ] 

67 else: 

68 _traces = [_trace] 

69 

70 for t in _traces: 

71 if action == "toggle": 

72 if "_visible" not in t: 

73 t["_visible"] = t["visible"] 

74 t["_visible"] = not t["_visible"] 

75 elif action == "toggleothers": 

76 for trace in plot.data: 

77 trace["_visible"] = not t["visible"] 

78 t["_visible"] = not t["_visible"] 

79 elif action == "none": 

80 pass 

81 else: 

82 raise ValueError(f"Unknown action {action}") 

83 

84 plot.update(dict(data=plot.data, layout=plot.layout)) 

85 

86 index = 0 

87 for trace in plot.data: 

88 if trace["type"] not in ["scatter", "scattergl"]: 

89 continue 

90 if "visible" in trace and not trace["visible"]: 

91 continue 

92 if "showlegend" in trace and not trace["showlegend"]: 

93 continue 

94 

95 icon = Icon(plot, trace, index) 

96 icon.set_size_request(legend["itemwidth"], -1) 

97 icon.set_cursor_from_name("pointer") 

98 click = Gtk.GestureClick.new() 

99 click.connect( 

100 "pressed", lambda g, n, x, y: on_click(n, g.get_widget().trace) 

101 ) 

102 icon.add_controller(click) 

103 grid.attach(icon, 0, index, 1, 1) 

104 name = trace["name"] if "name" in trace else "" 

105 label = Gtk.Label(label=name) 

106 label.set_halign(Gtk.Align.START) 

107 label.set_cursor_from_name("pointer") 

108 label.trace = trace 

109 click = Gtk.GestureClick.new() 

110 click.connect( 

111 "pressed", lambda g, n, x, y: on_click(n, g.get_widget().trace) 

112 ) 

113 if "_visible" in trace and not trace["_visible"]: 

114 label.add_css_class("plotly-legend-clicked") 

115 else: 

116 label.add_css_class("plotly-legend-not-clicked") 

117 label.add_controller(click) 

118 

119 grid.attach(label, 1, index, 1, 1) 

120 index += 1 

121 

122 if "text" in legend["title"]: 

123 grid.insert_row(0) 

124 title = Gtk.Label(label=legend["title"]["text"]) 

125 title.set_halign(Gtk.Align.START) 

126 title.add_css_class("plotly-legend-title") 

127 grid.attach(title, 0, 0, 2, 1) 

128 

129 if index == 1: 

130 self.remove(grid) 

131 return 

132 

133 font = legend["font"] 

134 title_font = legend["title"]["font"] 

135 custom_css = Gtk.CssProvider() 

136 custom_css.load_from_string( 

137 f""" 

138 .plotly-legend { 

139 background-color: {legend["bgcolor"]}; 

140 border: {legend["borderwidth"]}px solid {legend["bordercolor"]}; 

141 box-shadow: none; 

142 } 

143 .plotly-legend label { 

144 font-family: {font["family"]}; 

145 font-size: {font["size"]}px; 

146 font-style: {font["style"]}; 

147 font-variant: {font["variant"]}; 

148 font-weight: {font["weight"]}; 

149 } 

150 .plotly-legend-title { 

151 color: {title_font["color"]}; 

152 font-family: {title_font["family"]}; 

153 font-size: {title_font["size"]}px; 

154 font-style: {title_font["style"]}; 

155 font-variant: {title_font["variant"]}; 

156 font-weight: {title_font["weight"]}; 

157 } 

158 .plotly-legend-not-clicked { 

159 color: {font["color"]}; 

160 } 

161 .plotly-legend-clicked { 

162 color: shade({font["color"]}, 2); 

163 } 

164 """ 

165 ) 

166 Gtk.StyleContext().add_provider_for_display( 

167 Gdk.Display().get_default(), 

168 custom_css, 

169 Gtk.STYLE_PROVIDER_PRIORITY_USER, 

170 ) 

171 self.add_css_class("plotly-legend") 

172 

173 

174class Icon(Gtk.DrawingArea): 

175 def __init__(self, plot, trace, index): 

176 super().__init__() 

177 self.plot = plot 

178 self.trace = trace 

179 self.index = index 

180 self.set_draw_func(self.on_draw) 

181 

182 self.line_width = None 

183 self.marker_radius = None 

184 

185 def on_draw(self, area, context, x, y): 

186 if "mode" in self.trace: 

187 mode = self.trace["mode"] 

188 elif len(self.trace["x"]) <= 20: 

189 mode = "lines+markers" 

190 else: 

191 mode = "lines" 

192 modes = mode.split("+") 

193 

194 width = area.get_size(Gtk.Orientation.HORIZONTAL) 

195 height = area.get_size(Gtk.Orientation.VERTICAL) 

196 

197 if "markers" in modes: 

198 if "color" in self.trace["marker"]: 

199 color = self.trace["marker"]["color"] 

200 else: 

201 color = self.plot.layout["template"]["layout"]["colorway"][self.index] 

202 color = parse_color(color) 

203 if "_visible" in self.trace and not self.trace["_visible"]: 

204 color = [c + (1 - c) / 2 for c in color] 

205 context.set_source_rgb(*color) 

206 

207 if isinstance(self.trace["marker"]["size"], (list, np.ndarray)): 

208 radius = 4 

209 else: 

210 radius = ( 

211 self.trace["marker"]["size"] / 2 

212 if self.trace["marker"]["sizemode"] == "diameter" 

213 else np.sqrt(self.trace["marker"]["size"] / np.pi) 

214 ) 

215 context.arc(width / 2, height / 2, radius, 0, 2 * np.pi) 

216 context.fill() 

217 self.marker_radius = radius 

218 if "lines" in modes: 

219 if "color" in self.trace["line"]: 

220 color = self.trace["line"]["color"] 

221 else: 

222 color = self.plot.layout["template"]["layout"]["colorway"][self.index] 

223 color = parse_color(color) 

224 if "_visible" in self.trace and not self.trace["_visible"]: 

225 color = [c + (1 - c) / 2 for c in color] 

226 context.set_source_rgb(*color) 

227 context.set_line_width(self.trace["line"]["width"]) 

228 context.line_to(0, height / 2) 

229 context.line_to(width, height / 2) 

230 context.stroke() 

231 self.line_width = self.trace["line"]["width"]