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
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-08 21:22 +0000
1import gi
3gi.require_version("Gdk", "4.0")
4gi.require_version("Gtk", "4.0")
5from typing import TYPE_CHECKING
7import numpy as np
8from gi.repository import Gdk, Gtk # noqa: E402
10from plotly_gtk.utils import parse_color, update_dict
11from plotly_gtk.widgets.base import Base
13if TYPE_CHECKING:
14 from plotly_gtk.chart import PlotlyGtk
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)
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"]}")
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"]}")
37 default = dict(bgcolor=plot.layout["paper_bgcolor"], x=x_default, y=y_default)
38 legend = update_dict(default, legend)
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
46 self.spec = legend
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"]}")
55 def on_click(n, _trace):
56 if n == 1:
57 action = legend["itemclick"]
58 elif n == 2:
59 action = legend["itemdoubleclick"]
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]
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}")
84 plot.update(dict(data=plot.data, layout=plot.layout))
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
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)
119 grid.attach(label, 1, index, 1, 1)
120 index += 1
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)
129 if index == 1:
130 self.remove(grid)
131 return
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")
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)
182 self.line_width = None
183 self.marker_radius = None
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("+")
194 width = area.get_size(Gtk.Orientation.HORIZONTAL)
195 height = area.get_size(Gtk.Orientation.VERTICAL)
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)
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"]