import math
from typing import List


class StandardLine(object):
    """
    A line in a coordinate grid described in standard form (ax + by = c).
    """
    def __init__(self, x_quantifier: float, y_quantifier: float, equates: float):
        """
        A line in a coordinate grid described in standard form (ax + by = c).

        :param x_quantifier: variable a of the equation
        :param y_quantifier: variable b of the equation
        :param equates: variable c of the equation
        """
        self.x_quantifier = x_quantifier
        self.y_quantifier = y_quantifier
        self.equates = equates

    def __str__(self) -> str:
        return f"{self.A}x + {self.B}y = {self.C}"

    @property
    def A(self) -> float:
        return self.x_quantifier

    @property
    def B(self) -> float:
        return self.y_quantifier

    @property
    def C(self) -> float:
        return self.equates


class Coordinate(object):
    """
    A single x or y coordinate that doesn't have a specific value yet.
    """
    def __init__(self, _min: float, _max: float):
        self.min = _min
        self.max = _max

    @property
    def diff(self) -> float:
        return self.max - self.min

    @property
    def center(self) -> float:
        return self.min + self.diff * 0.5


class Vertex(object):
    """
    A Vertex - a point in 2D space described by an x and y value.
    """
    def __init__(self, x: float, y: float):
        """
        A Vertex - a point in 2D space described by an x and y value.

        :param x: the x coordinate of the vertex
        :param y: the y coordinate of the vertex
        """
        self.x = x
        self.y = y

    def __getitem__(self, item) -> float:
        if item == 0:
            return self.x
        elif item == 1:
            return self.y
        else:
            raise IndexError

    def __eq__(self, other) -> bool:
        if isinstance(other, Vertex):
            if self.x == other.x and self.y == other.y:
                return True
            else:
                return False
        else:
            raise AssertionError(f"Cannot compare Vertex to {other.__class__}")

    def __sub__(self, other):
        if isinstance(other, Vertex):
            return Vertex(
                x=self.x - other.x,
                y=self.y - other.y
            )
        elif isinstance(other, int):
            return Vertex(
                x=self.x - other,
                y=self.y - other,
            )
        elif isinstance(other, float):
            return Vertex(
                x=self.x - other,
                y=self.y - other,
            )
        else:
            raise SyntaxError(f"Cannot subtract {other.__class__} from Vertex")

    def __add__(self, other):
        if isinstance(other, Vertex):
            return Vertex(
                x=self.x + other.x,
                y=self.y + other.y
            )
        elif isinstance(other, int):
            return Vertex(
                x=self.x + other,
                y=self.y + other,
            )
        elif isinstance(other, float):
            return Vertex(
                x=self.x + other,
                y=self.y + other,
            )
        else:
            raise SyntaxError(f"Cannot add {other.__class__} to Vertex")

    def __truediv__(self, other):
        if isinstance(other, Vertex):
            return Vertex(
                x=self.x / other.x,
                y=self.y / other.y
            )
        elif isinstance(other, int) or isinstance(other, float):
            return Vertex(
                x=self.x / other,
                y=self.y / other,
            )
        else:
            raise SyntaxError(f"Cannot divide Vertex by {other.__class__}")

    def __abs__(self):
        return Vertex(
            x=abs(self.x),
            y=abs(self.y),
        )

    @property
    def to_tuple(self) -> tuple:
        """
        :return: the vertex represented as an (x, y) tuple
        """
        return tuple([self.x, self.y])

    def distance_to(self, other) -> float:
        """
        A helper method to calculate the distance to another vertex using pythagoras

        :param other: the vertex to calculate the distance to.
        :return: the calculated distance.
        """
        if isinstance(other, Vertex):
            pythagorean_vertex = self - other
            square_distance = pythagorean_vertex.x ** 2 + pythagorean_vertex.y ** 2
            rooted_distance = math.sqrt(square_distance)
            return rooted_distance
        else:
            raise SyntaxError(f"Cannot calculate distance between Vertex and {other.__class__}")

    def __str__(self):
        return f"({self.x},{self.y})"
        
class Edge(object):
    """
    An Edge - a line(AB) in 2D space described by 2 vertices
    """
    def __init__(self, a: Vertex, b: Vertex):
        """
        An Edge - a line in 2D space described by by 2 vertices

        :param a: vertex a of the line
        :param b: vertex b of the line
        """
        self.a = a
        self.b = b

    def __getitem__(self, item) -> Vertex:
        if item == 0:
            return self.a
        elif item == 1:
            return self.b
        else:
            raise IndexError

    def __eq__(self, other) -> bool:
        if isinstance(other, Edge):
            if self.a == other.a and self.b == other.b:
                return True
            elif self.b == other.a and self.a == other.b:
                return True
            return False
        else:
            raise AssertionError(f"Cannot compare Edge to {other.__class__}")

    @property
    def vertices(self) -> List[Vertex]:
        """
        :return: a list containing the edges vertices
        """
        return [self.a, self.b]


class Triangle(object):
    """
    A Triangle represented as 3 vertices a, b, c
    """

    def __init__(self, a: Vertex, b: Vertex, c: Vertex, is_super: bool = False):
        """
        A Triangle represented as as 3 vertices a, b, c

        :param a: vertex a of the triangle
        :param b: vertex b of the triangle
        :param c: vertex c of the triangle
        :param is_super: triangle is a super triangle
        """
        self.a = a
        self.b = b
        self.c = c
        self.is_super = is_super

    def __getitem__(self, item) -> Vertex:
        if item == 0:
            return self.a
        elif item == 1:
            return self.b
        elif item == 2:
            return self.c
        else:
            raise IndexError

    def __eq__(self, other) -> bool:
        if isinstance(other, Triangle):
            if self.a == other.a and self.b == other.b and self.c == other.c:
                return True
            return False
        else:
            raise AssertionError(f"Cannot compare Triangle to {other.__class__}")

    @property
    def ab_center(self) -> Vertex:
        """
        :return: the center vertex of the triangles Edge(AB)
        """
        return (self.a + self.b) / 2

    @property
    def bc_center(self) -> Vertex:
        """
        :return: the center vertex of the triangles Edge(BA)
        """
        return (self.b + self.c) / 2

    @property
    def ca_center(self) -> Vertex:
        """
        :return: the center vertex of the triangles Edge(CA)
        """
        return (self.c + self.a) / 2

    @property
    def ab_edge(self) -> Edge:
        """
        :return: the triangles Edge(AB)
        """
        return Edge(a=self.a, b=self.b)

    @property
    def bc_edge(self) -> Edge:
        """
        :return: the triangles Edge(BA)
        """
        return Edge(a=self.b, b=self.c)

    @property
    def ca_edge(self) -> Edge:
        """
        :return: the triangles Edge(CA)
        """
        return Edge(a=self.c, b=self.a)

    @property
    def vertices(self) -> List[Vertex]:
        """
        :return: a list of all vertices contained in this triangle
        """
        return [self.a, self.b, self.c]

    @property
    def edges(self) -> List[Edge]:
        """
        :return: a list of all edges contained in this triangle
        """
        return [self.ab_edge, self.bc_edge, self.ca_edge]

    @property
    def circumcenter(self) -> Vertex:
        """
        :return: the circumcenter of this triangle
        """
        ab = get_standard_line(p=self.a, q=self.b)
        bc = get_standard_line(p=self.b, q=self.c)

        pb_ab = get_perpendicular_bisector(p=self.a, q=self.b, line=ab)
        pb_bc = get_perpendicular_bisector(p=self.b, q=self.c, line=bc)

        return get_line_intersection(l1=pb_ab, l2=pb_bc)

    @property
    def circumradius(self) -> float:
        """
        :return: the circumradius of this triangle
        """
        return self.circumcenter.distance_to(self.a)

    @property
    def circumcircle(self):
        """
        :return: the circumcircle of this triangle
        """
        return Circle(center=self.circumcenter, radius=self.circumradius)


class Circle(object):
    """
    A Circle described by its center and its radius
    """
    def __init__(self, center: Vertex, radius: float):
        """
        A Circle described by its center and its radius

        :param center: the center of the circle
        :param radius: the radius of the circle
        """
        self.center = center
        self.radius = radius

    def encompasses_vertex(self, vertex: Vertex) -> bool:
        if self.radius >= self.center.distance_to(vertex):
            return True
        else:
            return False

    @property
    def top_left_circumsquare_corner(self):
        return tuple([self.center.x - self.radius, self.center.y - self.radius])

    @property
    def bottom_right_circumsquare_corner(self):
        return tuple([self.center.x + self.radius, self.center.y + self.radius])


def get_standard_line(p: Vertex, q: Vertex) -> StandardLine:
    """
    Create a StandardLine from 2 vertices

    :param p: vertex p of the StandardLine
    :param q: vertex q of the StandardLine
    :return: a StandardLine created from the given 2 vertices
    """

    a = q.y - p.y
    b = p.x - q.x
    c = a * p.x + b * p.y
    return StandardLine(
        x_quantifier=a,
        y_quantifier=b,
        equates=c
    )


def get_perpendicular_bisector(p: Vertex, q: Vertex, line: StandardLine) -> StandardLine:
    mid_point = (p + q) / 2
    c = -line.B * mid_point.x + line.A * mid_point.y
    a = -line.B
    b = line.A
    return StandardLine(
        x_quantifier=a,
        y_quantifier=b,
        equates=c
    )


def get_line_intersection(l1: StandardLine, l2: StandardLine) -> Vertex:
    """
    Get the intersection Vertex of 2 StandardLines

    :param l1: StandardLine l1
    :param l2: StandardLine l2
    :return: the intersection Vertex between l1 and l2
    """
    determinant = l1.A * l2.B - l2.A * l1.B
    if determinant == 0:  # The lines are parallel
        raise ValueError(f"The lines {l1} and {l2} do not intersect.")
    else:
        return Vertex(
            x=(l2.B * l1.C - l1.B * l2.C) / determinant,
            y=(l1.A * l2.C - l2.A * l1.C) / determinant
        )


import math
import random
from typing import List, NoReturn


def _remove_triangles(triangles: List[Triangle], pre_remove: List[Triangle]) -> NoReturn:
    """
    Helper method to remove triangles from triangle list

    :param triangles: the list items should be removed from
    :param pre_remove: the list of items that should be removed from triangles
    :return: nothing. the list is being altered in place.
    """

    for triangle in pre_remove:
        try:
            triangles.remove(triangle)
        except ValueError:
            pass


def scatter_vertices(plane_width: int, plane_height: int, spacing: int, scatter: float) -> List[Vertex]:
    """
    helper method to scatter random vertices on a specified plane.
    Distance is calculated using the given spacing and scatter.

    :param plane_width: the width of the plane.
    :param plane_height: the height of the plane.
    :param spacing: the spacing between the vertices.
    :param scatter: the scatter multiplier amount the spacing differs from its given value.
    :return: vertex list
    """

    vertices: List[Vertex] = []
    x_offset = (plane_width % spacing) / 2
    y_offset = (plane_height % spacing) / 2
    for x in range(math.floor((plane_width / spacing) + 1)):
        for y in range(math.floor((plane_height / spacing) + 1)):
            vertices.append(
                Vertex(
                    x=x_offset + spacing * (x + scatter * (random.random() - 0.5)),
                    y=y_offset + spacing * (y + scatter * (random.random() - 0.5))
                )
            )
    return vertices


def get_super_triangle(vertices: List[Vertex]) -> Triangle:
    """
    Get super triangle for vertices

    :param vertices: the vertices the super triangle should encompass.
    :return: the super triangle.
    """

    x = Coordinate(_min=math.inf, _max=-math.inf)
    y = Coordinate(_min=math.inf, _max=-math.inf)

    for vertex in vertices:
        if vertex.x < x.min:
            x.min = vertex.x
        if vertex.x > x.max:
            x.max = vertex.x
        if vertex.y < y.min:
            y.min = vertex.y
        if vertex.y > y.max:
            y.max = vertex.y

    max_diff = max(x.diff, y.diff)

    return Triangle(
        Vertex(
            x=x.center - 20 * max_diff,
            y=y.center - max_diff
        ),
        Vertex(
            x=x.center,
            y=y.center + 20 * max_diff
        ),
        Vertex(
            x=x.center + 20 * max_diff,
            y=y.center - max_diff
        ),
        is_super=True
    )


def delaunay(vertices: List[Vertex], delete_super_shared: bool = True) -> List[Triangle]:
    """
    Delaunay Triangulate a list of vertices using the Bowyer-Watson algorithm.

    :param vertices: the vertices to triangulate.
    :param delete_super_shared: delete triangles that share vertices with the super triangle
    :return: the list of created triangles.
    """

    # sort vertices by their x-coordinate to improve performance.
    vertices = sorted(vertices, key=lambda vertex: vertex.x)

    # create super triangle from vertices
    super_triangle = get_super_triangle(vertices=vertices)

    # initialise triangle list
    triangles: List[Triangle] = [super_triangle]

    # add super triangle vertices to vertex list
    vertices += super_triangle.vertices

    # initialise triangle removal list
    pre_removed_triangles: List[Triangle] = []

    for vertex in vertices:  # for each vertex

        # initialise list for illegal triangles
        # (triangles whose circumcircle encompasses the vertex)
        illegal_triangles = []

        for triangle in triangles:  # for each triangle
            try:
                # try to create circumcircle
                circumcircle = triangle.circumcircle

                # if the triangle of the current iteration encompasses the vertex
                # -> add triangle to illegal triangles
                if circumcircle.encompasses_vertex(vertex=vertex):
                    illegal_triangles.append(triangle)
            except ValueError:
                # cannot draw circumcircle because the perpendicular bisectors don't intersect
                # -> add the triangle to removal list
                pre_removed_triangles.append(triangle)

        # remove all triangles from triangle list that were added to triangle removal list
        _remove_triangles(triangles=triangles, pre_remove=pre_removed_triangles)

        # initialise shell polygon edge list
        polygon: List[Edge] = []

        # initialise illegal edge list
        illegal_edges = []

        # add all triangle edges from illegal triangles to illegal edges
        for triangle in illegal_triangles:
            illegal_edges += triangle.edges

        for triangle in illegal_triangles:  # for each triangle in illegal triangles
            for edge in triangle.edges:  # for each edge in illegal edges

                # if the edge is unique
                # -> add the edge to the shell polygon
                if illegal_edges.count(edge) == 1:
                    polygon.append(edge)

        # remove all illegal triangles from triangle list
        for triangle in illegal_triangles:
            triangles.remove(triangle)

        for edge in polygon:  # for each edge in shell polygon
            # create new triangles from edge and vertex
            new_triangle = Triangle(
                a=edge.a,
                b=edge.b,
                c=vertex
            )

            # add the newly created triangle to triangle iterator
            triangles.append(new_triangle)

    # delete all triangles that share vertices with the super triangle
    if delete_super_shared:
        for triangle in triangles:  # for each triangle

            # if the triangle is the super triangle
            # -> add the triangle to removal list
            if triangle.is_super:
                pre_removed_triangles.append(triangle)

            for vertex in triangle.vertices:  # for each vertex in the triangle

                # if the super triangle contains this vertex
                # -> remove the triangle
                if vertex in super_triangle.vertices:
                    try:
                        pre_removed_triangles.append(triangle)
                    except ValueError:
                        pass

    # remove all triangles from triangle list that were added to triangle removal list
    _remove_triangles(triangles=triangles, pre_remove=pre_removed_triangles)

    return triangles  # return triangles

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
xs = np.random.randint(20, size=100)
ys = np.random.randint(20, size=100)
tris = delaunay([Vertex(x, y) for x, y in zip(xs, ys)])
fig, ax = plt.subplots()
for tri in tris:
    cs = [(tri.a.x, tri.a.y), (tri.b.x, tri.b.y), (tri.c.x, tri.c.y)]
    p = Polygon(cs, facecolor = 'b')
    
    ax.add_patch(p)

plt.scatter(xs, ys)
plt.show()

Embed on website

To embed this program on your website, copy the following code and paste it into your website's HTML: