• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Edinburgh-Genome-Foundry / DnaFeaturesViewer / 14176893419

31 Mar 2025 04:49PM UTC coverage: 91.18% (+0.1%) from 91.056%
14176893419

push

github

veghp
Bump to v3.1.4

1 of 1 new or added line in 1 file covered. (100.0%)

18 existing lines in 6 files now uncovered.

734 of 805 relevant lines covered (91.18%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.31
/dna_features_viewer/GraphicRecord/MatplotlibPlottableMixin.py
1
"""Useful functions for the library"""
2

3
import colorsys
1✔
4

5
import matplotlib.pyplot as plt
1✔
6
import matplotlib.patches as mpatches
1✔
7
from matplotlib.patches import Patch
1✔
8
import matplotlib.ticker as ticker
1✔
9

10
from ..compute_features_levels import compute_features_levels
1✔
11
from ..GraphicFeature import GraphicFeature
1✔
12
from matplotlib.colors import colorConverter
1✔
13
from .MultilinePlottableMixin import MultilinePlottableMixin
1✔
14
from .SequenceAndTranslationMixin import SequenceAndTranslationMixin
1✔
15

16

17
class MatplotlibPlottableMixin(MultilinePlottableMixin, SequenceAndTranslationMixin):
1✔
18
    """Class mixin for matplotlib-related methods."""
19

20
    default_elevate_outline_annotations = False
1✔
21
    default_strand_in_label_threshold = None
1✔
22

23
    def initialize_ax(self, ax, draw_line, with_ruler, ruler_color=None):
1✔
24
        """Initialize the ax: remove axis, draw a horizontal line, etc.
25

26
        Parameters
27
        ----------
28

29
        draw_line
30
          True/False to draw the horizontal line or not.
31

32
        with_ruler
33
          True/False to draw the indices indicators along the line.
34
        """
35
        ruler_color = ruler_color or self.default_ruler_color
1✔
36
        start, end = self.span
1✔
37
        plot_start, plot_end = start - 0.8, end - 0.2
1✔
38
        if draw_line:
1✔
39
            ax.plot([plot_start, plot_end], [0, 0], zorder=-1000, c="k")
1✔
40

41
        if with_ruler:  # only display the xaxis ticks
1✔
42
            ax.set_frame_on(False)
1✔
43
            ax.yaxis.set_visible(False)
1✔
44
            ax.xaxis.tick_bottom()
1✔
45
            if ruler_color is not None:
1✔
46
                ax.tick_params(axis="x", colors=ruler_color)
1✔
47
        else:  # don't display anything
48
            ax.axis("off")
1✔
49

50
        ax.set_xlim(plot_start, plot_end)
1✔
51
        if self.first_index != 0:
1✔
52
            ax.ticklabel_format(useOffset=False, style="plain")
1✔
53
        fmt = lambda x, p: "{:,}".format(int(x))
1✔
54
        ax.xaxis.set_major_formatter(ticker.FuncFormatter(fmt))
1✔
55
        if self.ticks_resolution == "auto":
1✔
56
            ax.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))
1✔
57
        else:
58
            locator = ticker.MultipleLocator(self.ticks_resolution)
×
59
            ax.xaxis.set_major_locator(locator)
×
60

61
    def finalize_ax(
1✔
62
        self,
63
        ax,
64
        features_levels,
65
        annotations_max_level,
66
        auto_figure_height=False,
67
        ideal_yspan=None,
68
        annotations_are_elevated=True,
69
    ):
70
        """Prettify the figure with some last changes.
71

72
        Changes include redefining y-bounds and figure height.
73

74
        Parameters
75
        ----------
76

77
        ax
78
          ax on which the record was plotted.
79

80
        features_levels
81

82
        annotations_max_level
83
          Number indicating to the method the maximum height for an
84
          annotation, so the method can set ymax accordingly.
85

86
        auto_figure_height
87
          If true, the figure height will be automatically re-set to a nice
88
          value (counting ~0.4 inch per level in the figure).
89

90
        ideal_yspan
91
          if provided, can help the method select a better ymax to make sure
92
          all constraints fit.
93
        """
94

95
        # Compute the "natural" ymax
96
        annotation_height = self.determine_annotation_height(None)
1✔
97
        features_ymax = self.feature_level_height * (features_levels + 1)
1✔
98
        annotations_ymax = annotation_height * annotations_max_level
1✔
99
        if annotations_are_elevated:
1✔
100
            ymax = features_ymax + annotations_ymax
×
101
        else:
102
            ymax = max(features_ymax, annotations_ymax) + 1
1✔
103
        ymin = min(ax.get_ylim()[0], -0.5)
1✔
104

105
        # ymax could be even bigger if a "ideal_yspan" has been set.
106
        if (ideal_yspan is not None) and not (auto_figure_height):
1✔
107
            ymax = max(ideal_yspan + ymin, ymax)
1✔
108
        ax.set_ylim(ymin, ymax)
1✔
109
        if auto_figure_height:
1✔
110
            figure_width = ax.figure.get_size_inches()[0]
1✔
111
            ax.figure.set_size_inches(figure_width, 1 + 0.4 * ymax)
1✔
112
        ax.set_xticks(
1✔
113
            [
114
                t
115
                for t in ax.get_xticks()
116
                if t <= self.last_index and t >= self.first_index
117
            ]
118
        )
119
        if self.plots_indexing == "genbank":
1✔
120
            ax.set_xticklabels([int(i + 1) for i in ax.get_xticks()])
×
121
        return ideal_yspan / (ymax - ymin)
1✔
122

123
    @staticmethod
1✔
124
    def _get_ax_width(ax, unit="inch"):
1✔
125
        """Return the ax's width in 'inches' or 'pixel'."""
126
        transform = ax.figure.dpi_scale_trans.inverted()
1✔
127
        bbox = ax.get_window_extent().transformed(transform)
1✔
128
        width = bbox.width
1✔
129
        if unit == "pixel":
1✔
130
            width *= ax.figure.dpi
1✔
131
        return width
1✔
132

133
    def plot_feature(self, ax, feature, level, linewidth=1.0):
1✔
134
        """Create an Arrow Matplotlib patch with the feature's coordinates.
135

136
        The Arrow points in the direction of the feature's strand.
137
        If the feature has no direction (strand==0), the returned patch will
138
        simply be a rectangle.
139

140
        The x-coordinates of the patch are determined by the feature's
141
        `start` and `end` while the y-coordinates are determined by the `level`.
142
        """
143
        x1, x2 = feature.start, feature.end
1✔
144
        if feature.open_left:
1✔
145
            x1 -= 1
1✔
146
        if feature.open_right:
1✔
147
            x2 += 1
1✔
148
        if feature.strand == -1:
1✔
149
            x1, x2 = x2, x1
1✔
150
        x1, x2 = x1 - 0.5, x2 - 0.5
1✔
151

152
        is_undirected = feature.strand not in (-1, 1)
1✔
153
        head_is_cut = (feature.strand == 1 and feature.open_right) or (
1✔
154
            feature.strand == -1 and feature.open_left
155
        )
156
        if is_undirected or head_is_cut:
1✔
157
            head_length = 0.001
1✔
158
        else:
159
            width_pixel = self._get_ax_width(ax, unit="pixel")
1✔
160
            head_length = (
1✔
161
                0.5
162
                * width_pixel
163
                * feature.length
164
                / (ax.get_xlim()[1] - ax.get_xlim()[0])
165
            )
166
            head_length = min(head_length, 0.6 * feature.thickness)
1✔
167

168
        arrowstyle = mpatches.ArrowStyle.Simple(
1✔
169
            head_width=feature.thickness,
170
            tail_width=feature.thickness,
171
            head_length=head_length,
172
        )
173
        y = self.feature_level_height * level
1✔
174
        patch = mpatches.FancyArrowPatch(
1✔
175
            [x1, y],
176
            [x2, y],
177
            shrinkA=0.0,
178
            shrinkB=0.0,
179
            arrowstyle=arrowstyle,
180
            facecolor=feature.color,
181
            zorder=0,
182
            edgecolor=feature.linecolor,
183
            linewidth=feature.linewidth,
184
        )
185
        ax.add_patch(patch)
1✔
186
        return patch
1✔
187

188
    def autoselect_label_color(self, background_color):
1✔
189
        """Autselect a color for the label font.
190

191
        In the current method the label will be black on clear backrgounds,
192
        and white on dark backgrounds.
193
        """
194
        r, g, b = colorConverter.to_rgb(background_color)
1✔
195
        luminosity = 0.299 * r + 0.587 * g + 0.114 * b
1✔
196
        return "black" if (luminosity >= 0.5) else "white"
1✔
197

198
    def annotate_feature(
1✔
199
        self,
200
        ax,
201
        feature,
202
        level,
203
        inline=False,
204
        max_label_length=50,
205
        max_line_length=30,
206
        padding=0,
207
        indicate_strand_in_label=False,
208
    ):
209
        """Create a Matplotlib Text with the feature's label.
210

211
        The x-coordinates of the text are determined by the feature's
212
        `x_center` while the y-coordinates are determined by the `level`.
213

214
        The text is horizontally and vertically centered.
215

216
        The Arrow points in the direction of the feature's strand.
217
        If the feature has no direction (strand==0), the returned patch will
218
        simply be a rectangle.
219

220
        The x-coordinates of the patch are determined by the feature's
221
        `start` and `end` while the y-coordinates are determined by the `level`.
222
        """
223
        x, y = self.coordinates_in_plot(feature.x_center, level)
1✔
224
        label = feature.label
1✔
225
        if indicate_strand_in_label:
1✔
UNCOV
226
            if feature.strand == -1:
×
UNCOV
227
                label = "⇦" + label
×
UNCOV
228
            if feature.strand == 1:
×
UNCOV
229
                label = label + "⇨"
×
230

231
        if not inline:
1✔
232
            label = self._format_label(
1✔
233
                label,
234
                max_label_length=max_label_length,
235
                max_line_length=max_line_length,
236
            )
237
        nlines = len(label.split("\n"))
1✔
238
        fontdict = dict(**feature.fontdict)
1✔
239
        if "family" not in fontdict and (self.default_font_family is not None):
1✔
UNCOV
240
            fontdict["family"] = self.default_font_family
×
241
        if inline and ("color" not in fontdict):
1✔
242
            color = self.autoselect_label_color(background_color=feature.color)
1✔
243
            fontdict["color"] = color
1✔
244
        box_color = feature.box_color
1✔
245
        if box_color == "auto":
1✔
246
            box_color = self.default_box_color
1✔
247
        bbox = None
1✔
248
        if (box_color is not None) and not inline:
1✔
249
            bg_color = change_luminosity(feature.color, min_luminosity=0.95)
1✔
250
            bbox = dict(
1✔
251
                boxstyle="round",
252
                fc=bg_color if box_color == "auto" else box_color,
253
                ec="0.5",
254
                lw=feature.box_linewidth,
255
            )
256
        text = ax.text(
1✔
257
            x,
258
            y,
259
            label,
260
            horizontalalignment="center",
261
            verticalalignment="center",
262
            bbox=bbox,
263
            fontdict=fontdict,
264
            zorder=2,
265
        )
266
        x1, y1, x2, y2 = get_text_box(text)
1✔
267
        x1 -= padding
1✔
268
        x2 += padding
1✔
269
        overflowing = (x1 < feature.start) or (x2 > feature.end)
1✔
270
        return text, overflowing, nlines, (x1, x2), (y2 - y1)
1✔
271

272
    def place_annotation(
1✔
273
        self,
274
        feature,
275
        ax,
276
        level,
277
        annotate_inline,
278
        max_line_length,
279
        max_label_length,
280
        indicate_strand_in_label=False,
281
    ):
282
        """Place an annotation in the figure.
283

284
        Decide on inline vs. outline.
285

286
        Parameters
287
        ----------
288
        feature
289
            Graphic feature to place in the figure.
290
        ax
291
            Matplotlib ax in which to place the feature.
292
        level
293
            level at which the annotation should be placed.
294
        annotate_inline
295
            If true, the plotter will attempt to annotate inline, and fall back
296
            to outline annotation.
297
        max_line_length
298
            If an annotation label's length exceeds this number the label will
299
            wrap over several lines.
300
        max_label_length,
301
            If an annotation label's length exceeds this number the label will
302
            be cut with an ellipsis (...).
303
        indicate_strand_in_label
304
            If True, then the label will be represented as "<= label" or
305
            "label =>" with an arrow representing the strand.
306
        """
307
        padding = self.compute_padding(ax)
1✔
308
        if annotate_inline:
1✔
309
            # FIRST ATTEMPT TO ANNOTATE INSIDE THE FEATURE. CHECK FOR OVERFLOW
310
            text, overflowing, lines, (x1, x2), height = self.annotate_feature(
1✔
311
                ax=ax,
312
                feature=feature,
313
                level=level,
314
                inline=True,
315
                padding=padding,
316
                max_label_length=max_label_length,
317
                max_line_length=max_line_length,
318
                indicate_strand_in_label=indicate_strand_in_label,
319
            )
320

321
            # IF OVERFLOW, REMOVE THE TEXT AND PLACE IT AGAIN, OUTLINE.
322
            if overflowing:
1✔
323
                text.remove()
1✔
324
                text, _, lines, (x1, x2), height = self.annotate_feature(
1✔
325
                    ax=ax,
326
                    feature=feature,
327
                    level=level,
328
                    inline=False,
329
                    padding=padding,
330
                    max_label_length=max_label_length,
331
                    max_line_length=max_line_length,
332
                    indicate_strand_in_label=indicate_strand_in_label,
333
                )
334
            return text, overflowing, lines, (x1, x2), height
1✔
335
        else:
336
            return self.annotate_feature(
×
337
                ax=ax, feature=feature, level=level, padding=padding
338
            )
339

340
    def plot(
1✔
341
        self,
342
        ax=None,
343
        figure_width=8,
344
        draw_line=True,
345
        with_ruler=True,
346
        ruler_color=None,
347
        plot_sequence=False,
348
        annotate_inline=True,
349
        max_label_length=50,
350
        max_line_length=30,
351
        level_offset=0,
352
        strand_in_label_threshold="default",
353
        elevate_outline_annotations="default",
354
        x_lim=None,
355
        figure_height=None,
356
        sequence_params=None,
357
    ):
358
        """Plot all the features in the same Matplotlib ax.
359

360
        Parameters
361
        ----------
362

363
        ax
364
          The Matplotlib ax on which to plot the graphic record. If None is
365
          provided, a new figure and ax is generated, the ax is returned at
366
          the end.
367

368
        figure_width
369
          Width of the figure (only if no ax was provided and a new figure is
370
          created) in inches.
371

372
        draw_line
373
          If True, a base line representing the sequence will be drawn.
374

375
        with_ruler
376
          If true, the sequence indices will be indicated at regular intervals.
377

378
        ruler_color
379
          Ruler color.
380

381
        plot_sequence
382
          If True and the graphic record has a "sequence" attribute set, the
383
          sequence will be displayed below the base line.
384

385
        annotate_inline
386
          If true, some feature labels will be displayed inside their
387
          corresponding feature if there is sufficient space.
388

389
        max_label_length
390
          If an annotation label's length exceeds this number the label will
391
          be cut with an ellipsis (...).
392

393
        max_line_length
394
          If an annotation label's length exceeds this number the label will
395
          wrap over several lines.
396

397
        level_offset
398
          All features and annotations will be pushed up by "level_offset". Can
399
          be useful when plotting several sets of features successively on a
400
          same ax.
401

402
        strand_in_label_pixel_threshold
403
          Number N such that, when provided, every feature with a graphical
404
          width in pixels below N will have its strand indicated in the label
405
          by an a left/right arrow.
406

407
        elevate_outline_annotations
408
          If true, every text annotation will be above every feature. If false,
409
          text annotations will be as close as possible to the features.
410

411
        x_lim
412
          Horizontal axis limits to be set at the end.
413

414
        figure_height
415
          Figure height.
416

417
        sequence_params
418
          parameters for plot_sequence.
419
        """
420

421
        if elevate_outline_annotations == "default":
1✔
422
            default = self.default_elevate_outline_annotations
1✔
423
            elevate_outline_annotations = default
1✔
424
        if strand_in_label_threshold == "default":
1✔
425
            default = self.default_strand_in_label_threshold
1✔
426
            strand_in_label_threshold = default
1✔
427

428
        features_levels = compute_features_levels(self.features)
1✔
429

430
        for f in features_levels:
1✔
431
            features_levels[f] += level_offset
1✔
432
        max_level = (
1✔
433
            1 if (features_levels == {}) else max(1, max(features_levels.values()))
434
        )
435
        auto_figure_height = (ax is None) and (figure_height is None)
1✔
436
        if ax is None:
1✔
437
            height = figure_height or max_level
1✔
438
            fig, ax = plt.subplots(1, figsize=(figure_width, height))
1✔
439

440
        def strand_in_label(f):
1✔
441
            """Anything under 0.1 inches in the figure."""
442
            if strand_in_label_threshold is None:
1✔
443
                return False
1✔
444
            width_pixel = self._get_ax_width(ax, unit="pixel")
×
445
            f_pixels = 1.0 * width_pixel * f.length / self.sequence_length
×
446
            return f_pixels < strand_in_label_threshold
×
447

448
        self.initialize_ax(ax, draw_line=draw_line, with_ruler=with_ruler)
1✔
449
        if x_lim is not None:
1✔
450
            ax.set_xlim(*x_lim)
1✔
451
        overflowing_annotations = []
1✔
452
        renderer = ax.figure.canvas.get_renderer()
1✔
453
        bbox = ax.get_window_extent(renderer)
1✔
454
        ax_height = bbox.height
1✔
455
        ideal_yspan = 0
1✔
456

457
        # sorting features from larger to smaller to make smaller features
458
        # appear "on top" of smaller ones, in case it happens. May be useless
459
        # now.
460
        sorted_features_levels = sorted(
1✔
461
            features_levels.items(), key=lambda o: -o[0].length
462
        )
463
        for feature, level in sorted_features_levels:
1✔
464
            self.plot_feature(ax=ax, feature=feature, level=level)
1✔
465
            if feature.label is None:
1✔
466
                continue
1✔
467
            (
1✔
468
                text,
469
                overflowing,
470
                nlines,
471
                (
472
                    x1,
473
                    x2,
474
                ),
475
                height,
476
            ) = self.place_annotation(
477
                feature=feature,
478
                ax=ax,
479
                level=level,
480
                annotate_inline=annotate_inline,
481
                max_line_length=max_line_length,
482
                max_label_length=max_label_length,
483
                indicate_strand_in_label=strand_in_label(feature),
484
            )
485
            line_height = height / nlines
1✔
486
            n_text_lines_in_axis = ax_height / line_height
1✔
487
            min_y_height = self.min_y_height_of_text_line
1✔
488
            feature_ideal_span = min_y_height * n_text_lines_in_axis
1✔
489
            ideal_yspan = max(ideal_yspan, feature_ideal_span)
1✔
490
            if overflowing or not annotate_inline:
1✔
491
                # trick here: we are representing text annotations as
492
                # GraphicFeatures so we can place them using
493
                # compute_features_levels().
494
                # We are also storing all the info necessary for label plotting
495
                # in these pseudo-graphic-features.
496
                overflowing_annotations.append(
1✔
497
                    GraphicFeature(
498
                        start=x1,
499
                        end=x2,
500
                        feature=feature,
501
                        text=text,
502
                        feature_level=level,
503
                        nlines=nlines,
504
                        color=feature.color,
505
                        label_link_color=feature.label_link_color,
506
                    )
507
                )
508

509
        # There are two ways to plot annotations: evelated, all above all the
510
        # graphic feature. Or at the same levels as the graphic features (
511
        # every annotation above its respective feature, but some annotations
512
        # can be below some features).
513
        if elevate_outline_annotations:
1✔
514

515
            base_feature = GraphicFeature(
1✔
516
                start=-self.sequence_length,
517
                end=self.sequence_length,
518
                fixed_level=0,
519
                nlines=1,
520
                is_base=True,
521
            )
522
            overflowing_annotations.append(base_feature)
1✔
523
            annotations_levels = compute_features_levels(overflowing_annotations)
1✔
524
        else:
525
            for f in self.features:
1✔
526
                f.data.update(dict(nlines=1, fixed_level=features_levels[f]))
1✔
527
            annotations_levels = compute_features_levels(
1✔
528
                overflowing_annotations + self.features
529
            )
530
            annotations_levels = {
1✔
531
                f: annotations_levels[f] for f in overflowing_annotations
532
            }
533

534
        max_annotations_level = max([0] + list(annotations_levels.values()))
1✔
535
        annotation_height = self.determine_annotation_height(max_level)
1✔
536
        annotation_height = max(self.min_y_height_of_text_line, annotation_height)
1✔
537
        labels_data = {}
1✔
538
        for feature, level in annotations_levels.items():
1✔
539
            if "is_base" in feature.data:
1✔
540
                continue
1✔
541
            text = feature.data["text"]
1✔
542
            x, y = text.get_position()
1✔
543
            if elevate_outline_annotations:
1✔
544
                new_y = (max_level) * self.feature_level_height + (
1✔
545
                    level
546
                ) * annotation_height
547
            else:
548
                new_y = annotation_height * level
1✔
549
            text.set_position((x, new_y))
1✔
550
            fx, fy = self.coordinates_in_plot(
1✔
551
                feature.data["feature"].x_center, feature.data["feature_level"]
552
            )
553

554
            # PLOT THE LABEL-TO-FEATURE LINK
555
            link_color = feature.label_link_color
1✔
556
            if link_color == "auto":
1✔
UNCOV
557
                link_color = change_luminosity(feature.color, luminosity=0.2)
×
558
            ax.plot([x, fx], [new_y, fy], c=link_color, lw=0.5, zorder=-10)
1✔
559
            labels_data[feature.data["feature"]] = dict(
1✔
560
                feature_y=fy, annotation_y=new_y
561
            )
562

563
        if plot_sequence:
1✔
564
            self.plot_sequence(ax, **(sequence_params or {}))
1✔
565

566
        self.finalize_ax(
1✔
567
            ax=ax,
568
            features_levels=max([1] + list(features_levels.values())),
569
            annotations_max_level=max_annotations_level,
570
            auto_figure_height=auto_figure_height,
571
            ideal_yspan=ideal_yspan,
572
            annotations_are_elevated=elevate_outline_annotations,
573
        )
574
        return ax, (features_levels, labels_data)
1✔
575

576
    def plot_legend(
1✔
577
        self, ax, allow_ambiguity=False, include_edge=True, **legend_kwargs
578
    ):
579
        handles = []
1✔
580
        features_parameters = {}
1✔
581
        for feature in self.features:
1✔
582
            text = feature.legend_text
1✔
583
            if text is None:
1✔
UNCOV
584
                continue
×
585
            parameters = dict(
1✔
586
                label=text,
587
                facecolor=feature.color,
588
                edgecolor="black",
589
            )
590
            if include_edge:
1✔
591
                parameters.update(
1✔
592
                    dict(
593
                        linewidth=feature.linewidth,
594
                        edgecolor=feature.linecolor,
595
                    )
596
                )
597
            if text in features_parameters:
1✔
598
                previous_parameters = features_parameters[text]
1✔
599
                if (not allow_ambiguity) and any(
1✔
600
                    [parameters[k] != previous_parameters[k] for k in parameters]
601
                ):
UNCOV
602
                    raise ValueError("Cannot generate an unambiguous legend as two")
×
603
                continue
1✔
604
            features_parameters[text] = parameters
1✔
605
            handles.append(Patch(**parameters))
1✔
606
        ax.legend(handles=handles, **legend_kwargs)
1✔
607

608

609
def change_luminosity(color, luminosity=None, min_luminosity=None, factor=None):
1✔
610
    """Return a version of the color with different luminosity.
611

612
    Parameters
613
    ----------
614

615
    color
616
      A color in any Matplotlib-compatible format such as "white", "w",
617
      (1,1,1), "#ffffff", etc.
618

619
    luminosity
620
      A float in 0-1. If provided, the returned color has this level of
621
      luminosity.
622

623
    factor
624
      Only used if `luminosity` is not set. Positive factors increase
625
      luminosity and negative factors decrease it. More precisely, the
626
      luminosity of the new color is L^(-factor), where L is the current
627
      luminosity, between 0 and 1.
628
    """
629
    r, g, b = colorConverter.to_rgb(color)
1✔
630
    h, l, s = colorsys.rgb_to_hls(r, g, b)
1✔
631
    new_l = l
1✔
632
    if luminosity is not None:
1✔
UNCOV
633
        new_l = luminosity
×
634
    if factor is not None:
1✔
UNCOV
635
        new_l = l ** (-factor)
×
636
    if min_luminosity is not None:
1✔
637
        new_l = max(new_l, min_luminosity)
1✔
638

639
    return colorsys.hls_to_rgb(h, new_l, s)
1✔
640

641

642
def get_text_box(text, margin=0):
1✔
643
    """Return the coordinates of a Matplotlib Text.
644

645
    `text` is a Matplotlib text obtained with ax.text().
646
    This returns `(x1,y1, x2, y2)` where (x1,y1) is the lower left corner
647
    and (x2, y2) is the upper right corner of the text, in data coordinates.
648
    If a margin m is supplied, the returned result is (x1-m, y1-m, x2+m, y2+m).
649
    """
650
    renderer = text.axes.figure.canvas.get_renderer()
1✔
651
    bbox = text.get_window_extent(renderer)  # bounding box
1✔
652
    __x1, y1, __x2, y2 = bbox.get_points().flatten()
1✔
653
    bbox = bbox.transformed(text.axes.transData.inverted())
1✔
654
    x1, __y1, x2, __y2 = bbox.get_points().flatten()
1✔
655
    return [x1, y1, x2, y2]
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc