Coverage for src/plotly_gtk/chart.py: 20%
316 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
1"""This module contains a class for rendering a plotly
2:class:`plotly.graph_objects.Figure` using GTK."""
4import datetime
5import numbers
6from datetime import timezone
7from typing import TYPE_CHECKING
9import numpy as np
10import pandas as pd
12from plotly_gtk._chart import _PlotlyGtk
13from plotly_gtk.utils import * # pylint: disable=wildcard-import, unused-wildcard-import
14from plotly_gtk.utils.ticks import Ticks
15from plotly_gtk.widgets import * # pylint: disable=wildcard-import
17gi.require_version("Gdk", "4.0")
18gi.require_version("Gtk", "4.0")
19from gi.repository import ( # pylint: disable=wrong-import-order,wrong-import-position
20 Gtk,
21)
23if TYPE_CHECKING:
24 from plotly import graph_objects as go
26try:
27 from plotly import graph_objects as go
28except NameError:
29 print("plotly not available")
32class PlotlyGtk(Gtk.Overlay):
33 """Class for rendering plotly :class:`plotly.graph_objects.Figure`."""
35 def __init__(self, fig: "go.Figure | dict"):
36 super().__init__()
37 self.pushmargin = {}
38 try:
39 fig = fig.to_dict() if isinstance(fig, go.Figure) else fig
40 except NameError:
41 pass
43 self.data = fig["data"]
45 self.layout = fig["layout"]
46 if self.layout is None:
47 self.layout = {}
48 self._update_layout()
49 fig["layout"] = self.layout
50 fig["layout"]["_margin"] = dict(self.layout["margin"])
52 self._prepare_data()
53 fig["data"] = self.data
54 self.fig = fig
56 self.overlays = []
58 self.connect(
59 "get_child_position",
60 lambda overlay, widget, allocation: widget.get_position(
61 overlay, allocation
62 ),
63 )
65 self.connect("realize", lambda _: self.update(self.fig))
67 def update(self, fig: dict[str, plotly_types.Data | plotly_types.Layout]):
68 """Update the view.
70 Parameters
71 ----------
72 fig: dict[str, plotly_types.Data | plotly_types.Layout]
73 A dictionary representing a plotly figure
74 """
75 self._update_ranges()
76 self._update_positions_and_domains()
77 for overlay in self.overlays:
78 self.remove_overlay(overlay)
79 self.overlays = []
80 self._draw_buttons()
81 self._draw_legend()
82 self._draw_titles()
83 self._draw_annotations()
84 self.set_child(_PlotlyGtk(fig))
86 def _update_ranges(self):
87 axes = [k for k in self.layout if "axis" in k]
88 for axis in axes:
89 if "autorange" in self.layout[axis]:
90 autorange = self.layout[axis]["autorange"]
91 if "range" in self.layout[axis] and len(self.layout[axis]["range"]) == 2:
92 autorange = False
93 else:
94 autorange = True
96 axis_letter = axis[0 : axis.find("axis")]
98 plots_on_axis = [
99 plot
100 for plot in self.data
101 if f"{axis_letter}axis" in plot
102 and plot[f"{axis_letter}axis"] == axis.replace("axis", "")
103 and ("visible" not in plot or plot["visible"])
104 ]
105 hidden_plots_on_axis = [
106 plot
107 for plot in self.data
108 if f"{axis_letter}axis" in plot
109 and plot[f"{axis_letter}axis"] == axis.replace("axis", "")
110 and ("visible" not in plot or plot["visible"])
111 and ("_visible" not in plot or plot["_visible"])
112 ]
113 if len(plots_on_axis) > 1:
114 plots_on_axis = hidden_plots_on_axis
115 if plots_on_axis == []:
116 continue
118 if autorange:
119 _range = [
120 np.nanmin(
121 [
122 (
123 np.nanmin(plot[axis_letter])
124 if axis_letter in plot
125 else np.nan
126 )
127 for plot in plots_on_axis
128 ]
129 ),
130 np.nanmax(
131 [
132 (
133 np.nanmax(plot[axis_letter])
134 if axis_letter in plot
135 else np.nan
136 )
137 for plot in plots_on_axis
138 ]
139 ),
140 ]
141 if _range[0] == _range[-1]:
142 _range[0] = _range[0] - 1
143 _range[-1] = _range[1] + 1
144 self.layout[axis]["_range"] = _range
145 else:
146 if self.layout[axis]["type"] == "log":
147 self.layout[axis]["_range"] = np.array(
148 [
149 10 ** self.layout[axis]["range"][0],
150 10 ** self.layout[axis]["range"][-1],
151 ]
152 )
153 else:
154 self.layout[axis]["_range"] = np.array(self.layout[axis]["range"])
156 # Do matching
157 matched_to_axes = {
158 self.layout[axis]["matches"]
159 for axis in axes
160 if "matches" in self.layout[axis]
161 }
162 match_groups = {
163 axis: [
164 ax
165 for ax in axes
166 if ax == axis[0] + "axis" + axis[1:]
167 or ("matches" in self.layout[ax] and self.layout[ax]["matches"] == axis)
168 ]
169 for axis in matched_to_axes
170 }
171 for match_group in match_groups.values():
172 _ranges = [self.layout[axis]["_range"] for axis in match_group]
173 _range = [min(r[0] for r in _ranges), max(r[-1] for r in _ranges)]
174 for axis in match_group:
175 self.layout[axis]["_range"] = _range
177 for axis in axes:
178 if "_range" not in self.layout[axis]:
179 continue
180 if self.layout[axis]["_type"] == "log":
181 self.layout[axis]["_range"] = np.log10(self.layout[axis]["_range"])
182 range_length = (
183 self.layout[axis]["_range"][-1] - self.layout[axis]["_range"][0]
184 )
185 if (
186 "range" in self.layout[axis]
187 and len(self.layout[axis]["range"]) == 2
188 ):
189 range_addon = range_length * 0.001
190 else:
191 range_addon = range_length * 0.125 / 2
192 self.layout[axis]["_range"] = [
193 self.layout[axis]["_range"][0] - range_addon,
194 self.layout[axis]["_range"][-1] + range_addon,
195 ]
196 else:
197 range_length = (
198 self.layout[axis]["_range"][-1] - self.layout[axis]["_range"][0]
199 )
200 if (
201 "range" in self.layout[axis]
202 and len(self.layout[axis]["range"]) == 2
203 ):
204 range_addon = range_length * 0.001
205 else:
206 range_addon = range_length * 0.125 / 2
207 self.layout[axis]["_range"] = [
208 self.layout[axis]["_range"][0] - range_addon,
209 self.layout[axis]["_range"][-1] + range_addon,
210 ]
211 if "_ticksobject" not in self.layout[axis]:
212 self.layout[axis]["_ticksobject"] = Ticks(
213 self.layout,
214 axis,
215 0,
216 )
217 self.layout[axis]["_ticksobject"].calculate()
218 else:
219 self.layout[axis]["_ticksobject"].calculate()
221 def _prepare_data(self):
222 for plot in self.data:
223 if self._detect_axis_type(plot["x"]) == "date":
224 plot["x"] = np.array(plot["x"], dtype="datetime64")
225 plot["x"] = pd.to_datetime(plot["x"])
226 plot["x"] = [
227 x.replace(tzinfo=timezone.utc).timestamp() for x in plot["x"]
228 ]
229 plots = []
230 for plot in self.data:
231 if plot["type"] in ["scatter", "scattergl"]:
232 defaults = dict(
233 visible=True,
234 showlegend=True,
235 legend="legend",
236 legendrank=1000,
237 legendgroup="",
238 legendgrouptitle=dict(),
239 opacity=1,
240 zorder=0,
241 text="",
242 textposition="middle center",
243 texttemplate="",
244 hovertext="",
245 hoverinfo="all",
246 hovertemplate="",
247 xhoverformat="",
248 yhoverformat="",
249 xaxis="x",
250 yaxis="y",
251 marker=dict(
252 angle=0,
253 angleref="up",
254 autocolorscale=True,
255 cauto=True,
256 colorbar=dict(),
257 line=dict(
258 autocolorscale=True,
259 cauto=True,
260 ),
261 size=6,
262 sizemin=0,
263 sizemode="diameter",
264 sizeref=1,
265 standoff=0,
266 symbol="circle",
267 ),
268 line=dict(
269 backoff="auto",
270 dash="solid",
271 shape="linear",
272 simplify=True,
273 smoothing=1,
274 width=2,
275 ),
276 textfont=dict(),
277 )
278 plot = update_dict(defaults, plot)
279 elif plot["type"] == "histogram":
280 defaults = dict(xaxis="x", yaxis="y", visible=True)
281 plot = update_dict(defaults, plot)
282 self._bin_histogram(plot)
283 else:
284 raise NotImplementedError(f"{plot["type"]} not yet implemented")
285 plots.append(plot)
286 self.data = plots
288 def _bin_histogram(self, plot):
289 if "binned" not in plot or not plot["binned"]:
290 n_samples = len(plot["x"])
291 n_bins = np.sqrt(n_samples)
292 bin_width = (np.nanmax(plot["x"]) - np.nanmin(plot["x"])) / n_bins
293 bin_width = round_sf(bin_width, 1)
294 bin_start = bin_width * np.floor(np.nanmin(plot["x"]) / bin_width)
295 bins = np.arange(bin_start, np.nanmax(plot["x"]) + bin_width, bin_width)
297 counts = pd.cut(plot["x"], bins, right=False).value_counts().to_list()
298 plot["x"] = bins
299 plot["y"] = counts
300 plot["binned"] = True
302 def automargin(self):
303 """Calculate margin sizes.
305 Raises
306 ------
307 NotImplementedError
308 If unimplemented functionality is called for.
309 """
310 for pushmargin in self.pushmargin.values():
311 left = False
312 right = False
313 top = False
314 bottom = False
316 padding = 12
317 approximate_padding = 30
318 width = self.get_width()
319 height = self.get_height()
321 if pushmargin["l"] < 0:
322 left = True
323 if pushmargin["r"] > 1:
324 right = True
325 if pushmargin["t"] < 0:
326 top = True
327 if pushmargin["b"] > 1:
328 bottom = True
330 # TODO reimplement to search both margins together # pylint: disable=fixme
331 # >> m.ml
332 # ans = (sym)
333 #
334 # -pad⋅x₁ - pad⋅x₂ + width⋅x₁ - x₁⋅xr - x₂⋅xl
335 # ───────────────────────────────────────────
336 # x₁ - x₂
337 #
338 # >> m.mr
339 # ans = (sym)
340 #
341 # pad⋅x₁ + pad⋅x₂ - 2⋅pad - width⋅x₂ + width + x₁⋅xr + x₂⋅xl - xl - xr
342 # ────────────────────────────────────────────────────────────────────
343 # x₁ - x₂
345 if left:
346 if "x" in pushmargin and "xl" in pushmargin:
347 new = (
348 pushmargin["x"] * (width - self.layout["margin"]["r"])
349 - padding
350 - pushmargin["xl"]
351 ) / (pushmargin["x"] - 1)
352 else:
353 new = (
354 approximate_padding
355 - (width - self.layout["margin"]["r"]) * pushmargin["l"]
356 ) / (1 - pushmargin["l"])
357 self.layout["_margin"]["l"] = max(self.layout["_margin"]["l"], new)
358 if right:
359 if "x" in pushmargin and "xr" in pushmargin:
360 new = (
361 (pushmargin["x"] - 1) * (width - self.layout["margin"]["l"])
362 + padding
363 + pushmargin["xr"]
364 ) / pushmargin["x"]
365 else:
366 new = (
367 approximate_padding
368 + (width - self.layout["margin"]["l"]) * (pushmargin["r"] - 1)
369 ) / pushmargin["r"]
370 self.layout["_margin"]["r"] = max(self.layout["_margin"]["r"], new)
371 if top:
372 if "y" in pushmargin and "yt" in pushmargin:
373 new = (
374 (pushmargin["y"] - 1) * (height - self.layout["margin"]["b"])
375 + padding
376 + pushmargin["yt"]
377 ) / pushmargin["y"]
378 else:
379 raise NotImplementedError
380 self.layout["_margin"]["t"] = max(self.layout["_margin"]["t"], new)
381 if bottom:
382 if "y" in pushmargin and "yb" in pushmargin:
383 new = (
384 pushmargin["y"] * (height - self.layout["margin"]["t"])
385 - padding
386 - pushmargin["yb"]
387 ) / (pushmargin["y"] - 1)
388 else:
389 raise NotImplementedError
390 self.layout["_margin"]["b"] = max(self.layout["_margin"]["b"], new)
391 self.queue_allocate()
393 def _update_layout(self):
394 xaxes = {
395 trace["xaxis"].replace("x", "xaxis")
396 for trace in self.data
397 if "xaxis" in trace
398 }
399 yaxes = {
400 trace["yaxis"].replace("y", "yaxis")
401 for trace in self.data
402 if "yaxis" in trace
403 }
404 xaxes.add("xaxis")
405 yaxes.add("yaxis")
407 template = self.layout["template"]["layout"]
408 defaults = dict(
409 font=dict(
410 color="#444",
411 family='"Open Sans", verdana, arial, sans-serif',
412 size=12,
413 style="normal",
414 variant="normal",
415 weight="normal",
416 ),
417 legend=dict(
418 bordercolor="#444",
419 borderwidth=0,
420 entrywidth=0,
421 entrywidthmode="pixels",
422 font=dict(
423 color="#444",
424 family='"Open Sans", verdana, arial, sans-serif',
425 size=12,
426 style="normal",
427 variant="normal",
428 weight="normal",
429 ),
430 groupclick="togglegroup",
431 grouptitlefont=dict(
432 color="#444",
433 family='"Open Sans", verdana, arial, sans-serif',
434 size=12,
435 style="normal",
436 variant="normal",
437 weight="normal",
438 ),
439 indentation=0,
440 itemclick="toggle",
441 itemdoubleclick="toggleothers",
442 itemsizing="trace",
443 itemwidth=30,
444 orientation="v",
445 title=dict(
446 font=dict(
447 color="#444",
448 family='"Open Sans", verdana, arial, sans-serif',
449 size=12,
450 style="normal",
451 variant="normal",
452 weight="normal",
453 ),
454 text="",
455 ),
456 tracegroupgap=10,
457 traceorder="",
458 valign="middle",
459 visible=True,
460 xanchor="left",
461 xref="paper",
462 yanchor="auto",
463 yref="paper",
464 ),
465 margin=dict(autoexpand=True, t=100, l=80, r=80, b=80),
466 xaxis=dict(
467 anchor="y",
468 automargin=True,
469 autorange=True,
470 autotickangles=[0, 30, 90],
471 color="#444",
472 domain=[0, 1],
473 gridcolor="#eee",
474 griddash="solid",
475 gridwidth=1,
476 hoverformt="",
477 layer="above traces",
478 linecolor="#444",
479 linewidth=1,
480 minexponent=3,
481 minor=dict(),
482 mirror=False,
483 nticks=0,
484 position=0,
485 rangemode="normal",
486 showgrid=True,
487 showline=True,
488 showticklabels=True,
489 showtickprefix="all",
490 showticksuffix="all",
491 side="bottom",
492 tickangle="auto",
493 tickfont=dict(style="normal", variant="normal", weight="normal"),
494 tickformat="",
495 ticklabelmode="instant",
496 ticklabelposition="outside",
497 ticklabelstep=1,
498 ticklen=5,
499 tickprefix="",
500 ticks="",
501 tickson="labels",
502 ticksuffix="",
503 tickwidth=1,
504 title=dict(
505 font=dict(style="normal", variant="normal", weight="normal")
506 ),
507 type="-",
508 zerolinecolor="#444",
509 zerolinewidth=1,
510 ),
511 yaxis=dict(
512 anchor="x",
513 automargin=True,
514 autorange=True,
515 autotickangles=[0, 30, 90],
516 color="#444",
517 domain=[0, 1],
518 gridcolor="#eee",
519 griddash="solid",
520 gridwidth=1,
521 hoverformt="",
522 layer="above traces",
523 linecolor="#444",
524 linewidth=1,
525 minexponent=3,
526 minor=dict(),
527 mirror=False,
528 nticks=0,
529 position=0,
530 rangemode="normal",
531 showgrid=True,
532 showline=True,
533 showticklabels=True,
534 showtickprefix="all",
535 showticksuffix="all",
536 side="left",
537 tickangle="auto",
538 tickfont=dict(style="normal", variant="normal", weight="normal"),
539 tickformat="",
540 ticklabelmode="instant",
541 ticklabelposition="outside",
542 ticklabelstep=1,
543 ticklen=5,
544 tickprefix="",
545 ticks="",
546 tickson="labels",
547 ticksuffix="",
548 tickwidth=1,
549 title=dict(
550 font=dict(style="normal", variant="normal", weight="normal")
551 ),
552 type="-",
553 zerolinecolor="#444",
554 zerolinewidth=1,
555 ),
556 )
557 for xaxis in xaxes:
558 if xaxis not in self.layout:
559 self.layout[xaxis] = {}
560 if "type" not in self.layout[xaxis]:
561 first_plot_on_axis = [
562 trace
563 for trace in self.data
564 if "xaxis" not in trace
565 or trace["xaxis"] == xaxis.replace("axis", "")
566 ][0]
567 self.layout[xaxis]["_type"] = (
568 self._detect_axis_type(first_plot_on_axis["x"])
569 if "x" in first_plot_on_axis
570 else "linear"
571 )
572 else:
573 self.layout[xaxis]["_type"] = self.layout[xaxis]["type"]
574 if (
575 "side" in self.layout[xaxis]
576 and self.layout[xaxis]["side"] == "top"
577 and "position" not in self.layout[xaxis]
578 ):
579 self.layout[xaxis]["position"] = 1
580 template[xaxis] = template["xaxis"]
581 defaults[xaxis] = defaults["xaxis"]
582 for yaxis in yaxes:
583 if yaxis not in self.layout:
584 self.layout[yaxis] = {}
585 if "type" not in self.layout[yaxis]:
586 first_plot_on_axis = [
587 trace
588 for trace in self.data
589 if "yaxis" not in trace
590 or trace["yaxis"] == yaxis.replace("axis", "")
591 ][0]
592 self.layout[yaxis]["_type"] = (
593 self._detect_axis_type(first_plot_on_axis["y"])
594 if "y" in first_plot_on_axis
595 else "linear"
596 )
597 else:
598 self.layout[yaxis]["_type"] = self.layout[yaxis]["type"]
599 if (
600 "side" in self.layout[yaxis]
601 and self.layout[yaxis]["side"] == "right"
602 and "position" not in self.layout[yaxis]
603 ):
604 self.layout[yaxis]["position"] = 1
605 template[yaxis] = template["yaxis"]
606 defaults[yaxis] = defaults["yaxis"]
607 self.layout = update_dict(template, self.layout)
608 self.layout = update_dict(defaults, self.layout)
610 def _update_positions_and_domains(self):
611 axes = [k for k in self.layout if "axis" in k]
612 axes_order = []
613 overlayed = []
614 for axis in axes:
615 if "overlaying" in self.layout[axis]:
616 ax = self.layout[axis]["overlaying"]
617 overlayed.append(ax[0] + "axis" + ax[1:])
618 overlayed = set(overlayed)
619 for axis in overlayed:
620 overlayed_by = [
621 k
622 for k in axes
623 if "overlaying" in self.layout[k]
624 and self.layout[k]["overlaying"] == axis.replace("axis", "")
625 ]
626 left = [
627 k
628 for k in overlayed_by
629 if "side" in self.layout[k]
630 and self.layout[k]["side"] == "left"
631 or "side" not in self.layout[k]
632 ]
633 right = [
634 k
635 for k in overlayed_by
636 if "side" in self.layout[k] and self.layout[k]["side"] == "right"
637 ]
638 right = sorted(right)
639 left = sorted(left)
641 if "side" in self.layout[axis] and self.layout[axis]["side"] == "right":
642 right = [axis] + right
643 else:
644 left = [axis] + left
646 for side in [left, right]:
647 if len(side) == 0:
648 continue
649 self.layout[side[0]]["_overlaying"] = ""
650 for i in range(1, len(side)):
651 self.layout[side[i]]["_overlaying"] = side[i - 1]
653 axes_order += left
654 axes_order += right
656 other_axes = set(axes) - set(axes_order)
658 axes_order = sorted(list(other_axes)) + axes_order
660 for axis in axes_order:
661 if "linecolor" not in self.layout[axis]:
662 continue
663 overlaying_axis = (
664 self.layout[axis]["_overlaying"]
665 if "_overlaying" in self.layout[axis]
666 else ""
667 )
668 original_overlaying_axis = (
669 self.layout[axis]["overlaying"][0] + "axis" + self.layout[axis]["overlaying"][1:]
670 if "overlaying" in self.layout[axis]
671 else ""
672 )
673 anchor_axis = (
674 "free"
675 if "anchor" not in self.layout[axis]
676 or self.layout[axis]["anchor"] == "free"
677 else (
678 self.layout[axis]["anchor"][0]
679 + "axis"
680 + self.layout[axis]["anchor"][1:]
681 )
682 )
683 domain = (
684 self.layout[axis]["domain"]
685 if "overlaying" not in self.layout[axis]
686 or original_overlaying_axis == ""
687 else self.layout[original_overlaying_axis]["domain"]
688 )
690 position = (
691 self.layout[overlaying_axis]["_position"]
692 if "autoshift" in self.layout[axis]
693 and self.layout[axis]["autoshift"]
694 and anchor_axis == "free"
695 else (
696 self.layout[axis]["position"]
697 if "anchor" not in self.layout[axis] or anchor_axis == "free"
698 else (
699 self.layout[anchor_axis]["domain"][0]
700 if self.layout[axis]["side"] == "left"
701 or self.layout[axis]["side"] == "bottom"
702 else self.layout[anchor_axis]["domain"][-1]
703 )
704 )
705 )
706 self.layout[axis]["_domain"] = domain
707 self.layout[axis]["_position"] = position
709 if "autoshift" in self.layout[axis] and self.layout[axis]["autoshift"]:
710 shift = (
711 self.layout[axis]["shift"]
712 if "shift" in self.layout[axis]
713 else 3 if self.layout[axis]["side"] == "right" else -3
714 )
715 font_extra = 0
716 tickfont = update_dict(
717 self.layout["font"], self.layout[axis]["tickfont"]
718 )
719 tickfont = parse_font(tickfont)
720 ctx = self.get_pango_context()
721 layout = Pango.Layout(ctx)
722 layout.set_font_description(tickfont)
724 metrics = ctx.get_metrics(tickfont)
725 font_height = (
726 metrics.get_ascent() + metrics.get_descent()
727 ) / Pango.SCALE
729 for tick in self.layout[overlaying_axis]["_ticktext"]:
730 layout.set_text(tick)
731 font_extra = max(layout.get_pixel_size()[0], font_extra)
732 autoshift = (
733 font_extra
734 if self.layout[axis]["side"] == "right"
735 else -font_extra
736 - self.layout[axis]["title"]["standoff"]
737 - font_height
738 ) + self.layout[overlaying_axis]["_shift"]
739 else:
740 shift = 0
741 autoshift = 0
742 self.layout[axis]["_shift"] = shift + autoshift
744 @staticmethod
745 def _detect_axis_type(data):
746 if any(isinstance(i, list) or isinstance(i, np.ndarray) for i in data):
747 return "multicategory"
748 if not isinstance(data, np.ndarray):
749 data = np.array(data)
751 length = len(data)
752 if length >= 1000:
753 start = np.random.randint(0, length / 1000)
754 index = np.arange(start, length, length / 1000).astype(np.int32)
755 data = data[index]
757 data = set(data)
759 def to_type(d):
760 try:
761 d = np.datetime64(d)
762 return "date"
763 except ValueError:
764 if isinstance(d, numbers.Number):
765 return "linear"
766 else:
767 return "category"
769 data_types = [to_type(d) for d in data]
770 data_types = {d: data_types.count(d) for d in set(data_types)}
771 if len(data_types) == 1:
772 return list(data_types)[0]
773 if "linear" not in data_types:
774 if data_types["date"] > data_types["category"]:
775 return "date"
776 return "category"
778 if "date" in data_types and data_types["date"] > 2 * data_types["linear"]:
779 return "date"
780 if (
781 "category" in data_types
782 and data_types["category"] > 2 * data_types["linear"]
783 ):
784 return "category"
786 return "linear"
788 def _draw_buttons(self):
789 if "updatemenus" not in self.layout:
790 return
791 for updatemenu in self.layout["updatemenus"]:
792 overlay = UpdateMenu(self, updatemenu)
793 self.overlays.append(overlay)
794 self.add_overlay(overlay)
796 def _draw_legend(self):
797 legend = self.layout["legend"]
798 overlay = Legend(self, legend)
799 self.overlays.append(overlay)
800 self.add_overlay(overlay)
802 def _draw_annotations(self):
803 if "annotations" not in self.layout:
804 return
805 for annotation in self.layout["annotations"]:
806 overlay = Annotation(self, annotation)
807 self.overlays.append(overlay)
808 self.add_overlay(overlay)
810 def _draw_titles(self):
811 axes = [k for k in self.layout if "axis" in k]
812 for axis in axes:
813 if (
814 "title" not in self.layout[axis]
815 or self.layout[axis]["title"] is False
816 or "text" not in self.layout[axis]["title"]
817 ):
818 continue
819 overlay = AxisTitle(self, self.layout[axis], axis_name=axis)
820 self.overlays.append(overlay)
821 self.add_overlay(overlay)