<commit_before># Created: 06.2020
# Copyright (c) 2020, <NAME>
# License: MIT License
from math import degrees
from typing import List, Optional, Tuple

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from matplotlib.lines import Line2D
from matplotlib.patches import Arc, Circle, PathPatch
from matplotlib.path import Path
from matplotlib.textpath import TextPath

from ezdxf.addons.drawing.backend_interface import DrawingBackend
from ezdxf.addons.drawing.text import FontMeasurements
from ezdxf.addons.drawing.type_hints import Color, Radians
from ezdxf.math import Vector, param_to_angle, Matrix44


class MatplotlibBackend(DrawingBackend):
    def __init__(self, ax: plt.Axes,
                 *,
                 line_width: float = 0.5,
                 point_size: float = 2.0,
                 point_size_relative: bool = True,
                 font: FontProperties = FontProperties(),
                 ):
        super().__init__()
        self.ax = ax

        # like set_axis_off, except that the face_color can still be set
        self.ax.xaxis.set_visible(False)
        self.ax.yaxis.set_visible(False)
        for s in self.ax.spines.values():
            s.set_visible(False)

        self.ax.autoscale(False)
        self.ax.set_aspect('equal', 'datalim')
        self._current_z = 0
        self.line_width = line_width
        self.point_size = point_size
        self.point_size_relative = point_size_relative
        self.font = font
        self._font_measurements = _get_font_measurements(font)

    def _get_z(self) -> int:
        z = self._current_z
        self._current_z += 1
        return z

    def set_background(self, color: Color):
        self.ax.set_facecolor(color)

    def draw_line(self, start: Vector, end: Vector, color: Color):
        self.ax.add_line(Line2D((start.x, end.x), (start.y, end.y),
                                linewidth=self.line_width, color=color, zorder=self._get_z()))

    def draw_point(self, pos: Vector, color: Color):
        if self.point_size_relative:
            self.ax.scatter([pos.x], [pos.y], s=self.point_size, c=color, zorder=self._get_z())
        else:
            self.ax.add_patch(Circle((pos.x, pos.y), radius=self.point_size,
                                     facecolor=color, edgecolor=None, zorder=self._get_z()))

    def draw_filled_polygon(self, points: List[Vector], color: Color):
        self.ax.fill(*zip(*((p.x, p.y) for p in points)), color=color, zorder=self._get_z())

    def draw_text(self, text: str, transform: Matrix44, color: Color, cap_height: float):
        if not text:
            return  # no point rendering empty strings
        assert '\n' not in text, 'not a single line of text'
        scale = cap_height / self._font_measurements.cap_height
        path = _text_path(text, self.font)
        transformed_path = _transform_path(path, Matrix44.scale(scale) @ transform)
        self.ax.add_patch(PathPatch(transformed_path, facecolor=color, linewidth=0, zorder=self._get_z()))

    def get_font_measurements(self, cap_height: float) -> FontMeasurements:
        return self._font_measurements.scale_from_baseline(desired_cap_height=cap_height)

    def get_text_line_width(self, text: str, cap_height: float) -> float:
        if not text:
            return 0
        assert '\n' not in text, 'not a single line of text'
        path = _text_path(text, self.font)
        scale = cap_height / self._font_measurements.cap_height
        transformed_xs = _transform_path(path, Matrix44.scale(scale)).vertices[:, 0].tolist()
        return max(transformed_xs)

    def draw_arc(self, center: Vector, width: float, height: float, angle: Radians,
                 draw_angles: Optional[Tuple[Radians, Radians]], color: Color):
        if min(width, height) < 1e-5:
            return  # matplotlib crashes if the arc has almost 0 size

        if draw_angles is None:
            start_degrees, end_degrees = 0, 360
        else:
            # draw angles are relative to `angle`. The arc is drawn anticlockwise from start to end.
            # draw_angles are *NOT* in the global coordinate system, but instead are angles inside the
            # local coordinate system defined by the major and minor axes of the ellipse
            # (where the ellipse looks like a circle)
            ratio = height / width
            start, end = param_to_angle(ratio, draw_angles[0]), param_to_angle(ratio, draw_angles[1])
            start_degrees, end_degrees = degrees(start), degrees(end)

        arc = Arc((center.x, center.y), width, height, color=color, linewidth=self.line_width,
                  angle=degrees(angle), theta1=start_degrees, theta2=end_degrees, zorder=self._get_z())
        self.ax.add_patch(arc)

    def clear(self):
        self.ax.clear()

    def finalize(self):
        self.ax.autoscale(True)


def _transform_path(path: Path, transform: Matrix44) -> Path:
    vertices = transform.transform_vertices([Vector(x, y) for x, y in path.vertices])
    return Path([(v.x, v.y) for v in vertices], path.codes)


def _text_path(text: str, font: FontProperties) -> TextPath:
    return TextPath((0, 0), text, size=1, prop=font)


def _get_font_measurements(font: FontProperties = FontProperties()) -> "FontMeasurements":
    upper_x = _text_path('X', font).vertices[:, 1].tolist()
    lower_x = _text_path('x', font).vertices[:, 1].tolist()
    lower_p = _text_path('p', font).vertices[:, 1].tolist()
    return FontMeasurements(
        baseline=min(lower_x),
        cap_top=max(upper_x),
        x_top=max(lower_x),
        bottom=min(lower_p)
    )


<commit_msg>optionally set the figure to the correct aspect ratio<commit_after># Created: 06.2020
# Copyright (c) 2020, <NAME>
# License: MIT License
from math import degrees
from typing import List, Optional, Tuple

import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
from matplotlib.lines import Line2D
from matplotlib.patches import Arc, Circle, PathPatch
from matplotlib.path import Path
from matplotlib.textpath import TextPath

from ezdxf.addons.drawing.backend_interface import DrawingBackend
from ezdxf.addons.drawing.text import FontMeasurements
from ezdxf.addons.drawing.type_hints import Color, Radians
from ezdxf.math import Vector, param_to_angle, Matrix44


class MatplotlibBackend(DrawingBackend):
    def __init__(self, ax: plt.Axes,
                 *,
                 adjust_figure: bool = True,
                 line_width: float = 0.5,
                 point_size: float = 2.0,
                 point_size_relative: bool = True,
                 font: FontProperties = FontProperties(),
                 ):
        super().__init__()
        self.ax = ax
        self._adjust_figure = adjust_figure

        # like set_axis_off, except that the face_color can still be set
        self.ax.xaxis.set_visible(False)
        self.ax.yaxis.set_visible(False)
        for s in self.ax.spines.values():
            s.set_visible(False)

        self.ax.autoscale(False)
        self.ax.set_aspect('equal', 'datalim')
        self._current_z = 0
        self.line_width = line_width
        self.point_size = point_size
        self.point_size_relative = point_size_relative
        self.font = font
        self._font_measurements = _get_font_measurements(font)

    def _get_z(self) -> int:
        z = self._current_z
        self._current_z += 1
        return z

    def set_background(self, color: Color):
        self.ax.set_facecolor(color)

    def draw_line(self, start: Vector, end: Vector, color: Color):
        self.ax.add_line(Line2D((start.x, end.x), (start.y, end.y),
                                linewidth=self.line_width, color=color, zorder=self._get_z()))

    def draw_point(self, pos: Vector, color: Color):
        if self.point_size_relative:
            self.ax.scatter([pos.x], [pos.y], s=self.point_size, c=color, zorder=self._get_z())
        else:
            self.ax.add_patch(Circle((pos.x, pos.y), radius=self.point_size,
                                     facecolor=color, edgecolor=None, zorder=self._get_z()))

    def draw_filled_polygon(self, points: List[Vector], color: Color):
        self.ax.fill(*zip(*((p.x, p.y) for p in points)), color=color, zorder=self._get_z())

    def draw_text(self, text: str, transform: Matrix44, color: Color, cap_height: float):
        if not text:
            return  # no point rendering empty strings
        assert '\n' not in text, 'not a single line of text'
        scale = cap_height / self._font_measurements.cap_height
        path = _text_path(text, self.font)
        transformed_path = _transform_path(path, Matrix44.scale(scale) @ transform)
        self.ax.add_patch(PathPatch(transformed_path, facecolor=color, linewidth=0, zorder=self._get_z()))

    def get_font_measurements(self, cap_height: float) -> FontMeasurements:
        return self._font_measurements.scale_from_baseline(desired_cap_height=cap_height)

    def get_text_line_width(self, text: str, cap_height: float) -> float:
        if not text:
            return 0
        assert '\n' not in text, 'not a single line of text'
        path = _text_path(text, self.font)
        scale = cap_height / self._font_measurements.cap_height
        transformed_xs = _transform_path(path, Matrix44.scale(scale)).vertices[:, 0].tolist()
        return max(transformed_xs)

    def draw_arc(self, center: Vector, width: float, height: float, angle: Radians,
                 draw_angles: Optional[Tuple[Radians, Radians]], color: Color):
        if min(width, height) < 1e-5:
            return  # matplotlib crashes if the arc has almost 0 size

        if draw_angles is None:
            start_degrees, end_degrees = 0, 360
        else:
            # draw angles are relative to `angle`. The arc is drawn anticlockwise from start to end.
            # draw_angles are *NOT* in the global coordinate system, but instead are angles inside the
            # local coordinate system defined by the major and minor axes of the ellipse
            # (where the ellipse looks like a circle)
            ratio = height / width
            start, end = param_to_angle(ratio, draw_angles[0]), param_to_angle(ratio, draw_angles[1])
            start_degrees, end_degrees = degrees(start), degrees(end)

        arc = Arc((center.x, center.y), width, height, color=color, linewidth=self.line_width,
                  angle=degrees(angle), theta1=start_degrees, theta2=end_degrees, zorder=self._get_z())
        self.ax.add_patch(arc)

    def clear(self):
        self.ax.clear()

    def finalize(self):
        self.ax.autoscale(True)
        if self._adjust_figure:
            minx, maxx = self.ax.get_xlim()
            miny, maxy = self.ax.get_ylim()
            data_width, data_height = maxx - minx, maxy - miny
            width, height = plt.figaspect(data_height / data_width)
            self.ax.get_figure().set_size_inches(width, height, forward=True)


def _transform_path(path: Path, transform: Matrix44) -> Path:
    vertices = transform.transform_vertices([Vector(x, y) for x, y in path.vertices])
    return Path([(v.x, v.y) for v in vertices], path.codes)


def _text_path(text: str, font: FontProperties) -> TextPath:
    return TextPath((0, 0), text, size=1, prop=font)


def _get_font_measurements(font: FontProperties = FontProperties()) -> "FontMeasurements":
    upper_x = _text_path('X', font).vertices[:, 1].tolist()
    lower_x = _text_path('x', font).vertices[:, 1].tolist()
    lower_p = _text_path('p', font).vertices[:, 1].tolist()
    return FontMeasurements(
        baseline=min(lower_x),
        cap_top=max(upper_x),
        x_top=max(lower_x),
        bottom=min(lower_p)
    )



{'fraction_non_alphanumeric': 0.10502983802216538, 'fraction_numerical': 0.008184143222506393, 'mean_word_length': 2.8038261997405964, 'pattern_counts': {'":': 2, '<': 7, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2227394', 'n_tokens_mistral': 3710, 'n_tokens_neox': 3607, 'n_words': 918}