Source code for cgeom.visualization._plotting

import time

import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np

# ---------------------------------------------------------------------------
# Global rcParams — polished minimalist defaults
# ---------------------------------------------------------------------------
mpl.rcParams.update(
    {
        "font.family": "sans-serif",
        "font.sans-serif": [
            "Inter",
            "Helvetica Neue",
            "Helvetica",
            "Arial",
            "DejaVu Sans",
        ],
        "font.size": 9,
        "axes.unicode_minus": False,
        "figure.dpi": 150,
        "savefig.dpi": 300,
        "savefig.bbox": "tight",
        "savefig.pad_inches": 0.15,
    }
)

# ---------------------------------------------------------------------------
# Blue / navy palette — deep navies for structure, Smart Blue for focal data,
# slate & lavender greys for labels and annotations.
# ---------------------------------------------------------------------------
_INK = "#002855"  # Prussian Blue — primary structure, main edges
_ACCENT = "#0466c8"  # Smart Blue — focal data points, emphasis
_CHARCOAL = "#023e7d"  # Regal Navy — secondary edges, lines
_STEEL = "#5c677d"  # Blue Slate — tick labels, axis labels
_SILVER = "#7d8597"  # Slate Grey — annotations, dashes, subtle markers
_ASH = "#979dac"  # Lavender Grey — light ticks, borders
_MIST = "#e6e9f1"  # faint lavender — grid lines, fill base color
_BG = "#fbfcfe"  # cool near-white — figure & axes background

# ---------------------------------------------------------------------------
# Visual sizing constants
# ---------------------------------------------------------------------------
_PT_SIZE = 28  # primary scatter point size
_PT_ACCENT = 40  # emphasized scatter point size
_PT_EDGE_W = 0.8  # white edge width on scatter points
_LINE_W = 1.0  # primary edge/line width
_LINE_W_THIN = 0.7  # secondary/dashed line width
_FILL_ALPHA = 0.06  # polygon/triangle fill opacity
_CIRCLE_ALPHA = 0.08  # circle fill opacity

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _hex_to_rgb(hex_color):
    """Convert ``'#rrggbb'`` to an ``(r, g, b)`` float tuple."""
    h = hex_color.lstrip("#")
    return tuple(int(h[i : i + 2], 16) / 255.0 for i in (0, 2, 4))


def _style_ax(ax, title=None):
    """Apply a minimal, modern look to an axes object."""
    if title:
        ax.set_title(
            title, loc="left", fontsize=12.5, fontweight="500", color=_INK, pad=12
        )
    ax.set_facecolor(_BG)
    ax.set_axisbelow(True)
    ax.grid(True, color=_MIST, linewidth=0.7, zorder=0)
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.tick_params(colors=_STEEL, labelsize=7.5, length=0, width=0, pad=4)


def _new_fig(figsize=(5.5, 5.5)):
    """Create a figure with an off-white background."""
    fig, ax = plt.subplots(figsize=figsize)
    fig.patch.set_facecolor(_BG)
    fig.subplots_adjust(left=0.12, right=0.95, top=0.90, bottom=0.10)
    return fig, ax


# ---------------------------------------------------------------------------
# Convex Hull
# ---------------------------------------------------------------------------


[docs] def plot_convex_hull(hull_obj, title="Convex Hull"): """Plot the points and their convex hull. Args: hull_obj: A ``ConvexHull`` instance. title: Title for the matplotlib figure. """ fig, ax = _new_fig() _style_ax(ax, title) ax.set_aspect("equal") ch = hull_obj.convex_hull() xs = [p[0] for p in ch] + [ch[0][0]] ys = [p[1] for p in ch] + [ch[0][1]] ax.fill( xs, ys, facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=2, ) ax.scatter( hull_obj.points[:, 0], hull_obj.points[:, 1], c=_STEEL, s=_PT_SIZE, zorder=3, edgecolors="white", linewidths=_PT_EDGE_W, ) ax.scatter( [p[0] for p in ch], [p[1] for p in ch], c=_ACCENT, s=_PT_ACCENT, zorder=4, edgecolors="white", linewidths=_PT_EDGE_W, ) plt.tight_layout() plt.show()
# --------------------------------------------------------------------------- # Minimum Circle (random sizes) # ---------------------------------------------------------------------------
[docs] def plot_min_circle_random(mc_obj, sizes, path=None, show=False): """Plot minimum circles for randomly generated point sets of varying sizes. Args: mc_obj: A ``MinimumCircle`` instance. sizes: List of point-set sizes to generate and evaluate. path: Directory path to save PDF figures. None to skip saving. show: If True, display each figure interactively. """ time_heuristic = [] time_min_circle = [] for num_points in sizes: x = np.random.standard_normal(num_points) y = np.random.standard_normal(num_points) points = [[x[i], y[i]] for i in range(num_points)] start_time = time.time() min_circle = mc_obj.minimum_circle(points) time_min_circle.append(time.time() - start_time) start_time = time.time() min_circle_heuristic = mc_obj.minimum_circle_heuristic(points) time_heuristic.append(time.time() - start_time) print(f"MinCircle Center/Radius: {min_circle[0]} / {min_circle[1]}") print( f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}" ) fig, axes = plt.subplots(1, 2, figsize=(11, 5)) fig.patch.set_facecolor(_BG) for idx, (circ, label) in enumerate( [ (min_circle, f"Exact (n={num_points})"), (min_circle_heuristic, f"Heuristic (n={num_points})"), ] ): ax = axes[idx] _style_ax(ax, label) ax.set_aspect("equal") ax.scatter( x, y, c=_ACCENT, s=16, zorder=3, edgecolors="white", linewidths=_PT_EDGE_W, ) circle_patch = plt.Circle( circ[0], circ[1], facecolor=(*_hex_to_rgb(_MIST), _CIRCLE_ALPHA), edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=1, ) ax.add_patch(circle_patch) ax.plot( circ[0][0], circ[0][1], "+", color=_SILVER, markersize=7, markeredgewidth=1.0, zorder=4, ) ax.set_xlim(-10, 10) ax.set_ylim(-10, 10) plt.tight_layout() if path is not None: fig.savefig( path + f"plot_min_circle_{num_points}_points.pdf", bbox_inches="tight" ) if show: plt.show() # Runtime comparison fig, ax = _new_fig(figsize=(5.5, 4)) _style_ax(ax, "Runtime") ax.plot( sizes, time_min_circle, color=_ACCENT, linewidth=_LINE_W, marker="o", markersize=4, label="Exact", ) ax.plot( sizes, time_heuristic, color=_SILVER, linewidth=_LINE_W, marker="o", markersize=4, label="Heuristic", ) ax.set_xlabel("Input size", fontsize=8.5, color=_STEEL) ax.set_ylabel("Time (s)", fontsize=8.5, color=_STEEL) ax.legend(frameon=False, fontsize=9) plt.tight_layout() if path is not None: fig.savefig(path + "time.pdf", bbox_inches="tight") if show: plt.show()
# --------------------------------------------------------------------------- # Minimum Circle (given data) # ---------------------------------------------------------------------------
[docs] def plot_min_circle(mc_obj, data, path=None, show=False): """Plot minimum circles for a given dataset. Args: mc_obj: A ``MinimumCircle`` instance. data: Array-like of 2D points. path: Directory path to save PDF figures. None to skip saving. show: If True, display each figure interactively. """ start_time = time.time() min_circle = mc_obj.minimum_circle(data) t_exact = time.time() - start_time start_time = time.time() min_circle_heuristic = mc_obj.minimum_circle_heuristic(data) t_heur = time.time() - start_time print(f"MinCircle Center/Radius: {min_circle[0]} / {min_circle[1]}") print( f"Heuristic Center/Radius: {min_circle_heuristic[0]} / {min_circle_heuristic[1]}" ) fig, axes = plt.subplots(1, 2, figsize=(11, 5)) fig.patch.set_facecolor(_BG) for idx, (circ, label) in enumerate( [ (min_circle, "Exact"), (min_circle_heuristic, "Heuristic"), ] ): ax = axes[idx] _style_ax(ax, label) ax.set_aspect("equal") ax.scatter( data[:, 0], data[:, 1], c=_ACCENT, s=16, zorder=3, edgecolors="white", linewidths=_PT_EDGE_W, ) circle_patch = plt.Circle( circ[0], circ[1], facecolor=(*_hex_to_rgb(_MIST), _CIRCLE_ALPHA), edgecolor=_CHARCOAL, linewidth=_LINE_W, zorder=1, ) ax.add_patch(circle_patch) ax.plot( circ[0][0], circ[0][1], "+", color=_SILVER, markersize=7, markeredgewidth=1.0, zorder=4, ) margin = circ[1] * 0.15 ax.set_xlim(circ[0][0] - circ[1] - margin, circ[0][0] + circ[1] + margin) ax.set_ylim(circ[0][1] - circ[1] - margin, circ[0][1] + circ[1] + margin) plt.tight_layout() if path is not None: fig.savefig(path + "plot_min_circle.pdf", bbox_inches="tight") if show: plt.show() # Runtime bar chart fig, ax = _new_fig(figsize=(4, 3.5)) _style_ax(ax, "Runtime") ax.bar( ["Exact", "Heuristic"], [t_exact, t_heur], color=[_ACCENT, _SILVER], width=0.40, edgecolor=_BG, linewidth=0.6, ) ax.set_ylabel("Time (s)", fontsize=8.5, color=_STEEL) ax.yaxis.set_major_formatter(ticker.FormatStrFormatter("%.4f")) plt.tight_layout() if path is not None: fig.savefig(path + "time.pdf", bbox_inches="tight") if show: plt.show()
# --------------------------------------------------------------------------- # Triangulation # ---------------------------------------------------------------------------
[docs] def plot_triangulation(tri_obj): """Plot the polygon and its triangulation diagonals. Args: tri_obj: A ``PolygonTriangulation`` instance. """ fig, ax = _new_fig() _style_ax(ax, tri_obj.poly_name) ax.set_aspect("equal") xs = list(tri_obj.poly[:, 0]) + [tri_obj.poly[0, 0]] ys = list(tri_obj.poly[:, 1]) + [tri_obj.poly[0, 1]] ax.fill( xs, ys, facecolor=(*_hex_to_rgb(_MIST), _FILL_ALPHA), edgecolor=_INK, linewidth=_LINE_W, zorder=2, ) for diag in tri_obj.diagonals: ax.plot( [diag[0][0], diag[1][0]], [diag[0][1], diag[1][1]], color=_SILVER, linewidth=_LINE_W_THIN, linestyle=(0, (3, 4)), zorder=3, ) ax.scatter( tri_obj.poly[:, 0], tri_obj.poly[:, 1], c=_ACCENT, s=_PT_SIZE, zorder=4, edgecolors="white", linewidths=_PT_EDGE_W, ) plt.tight_layout() plt.show()
# --------------------------------------------------------------------------- # Delaunay Triangulation # ---------------------------------------------------------------------------
[docs] def plot_delaunay(dt_obj, title="Delaunay Triangulation", show_circumcircles=False): """Plot the Delaunay triangulation. Args: dt_obj: A ``DelaunayTriangulation`` instance. title: Title for the matplotlib figure. show_circumcircles: If True, draw dashed circumcircles for each triangle. """ fig, ax = _new_fig() _style_ax(ax, title) ax.set_aspect("equal") triangles = dt_obj.get_triangles() edges = dt_obj.get_edges() # Light triangle fill for tri in triangles: xs = [p[0] for p in tri] + [tri[0][0]] ys = [p[1] for p in tri] + [tri[0][1]] ax.fill( xs, ys, facecolor=(*_hex_to_rgb(_MIST), 0.05), edgecolor="none", zorder=1 ) # Edges for edge in edges: ax.plot( [edge[0][0], edge[1][0]], [edge[0][1], edge[1][1]], color=_CHARCOAL, linewidth=_LINE_W, zorder=2, ) # Circumcircles if show_circumcircles: for circ in dt_obj.get_circumcircles(): circle_patch = plt.Circle( circ[0], circ[1], facecolor="none", edgecolor=(*_hex_to_rgb(_SILVER), 0.4), linewidth=_LINE_W_THIN, linestyle=(0, (4, 5)), zorder=3, ) ax.add_patch(circle_patch) # Points on top ax.scatter( dt_obj.points[:, 0], dt_obj.points[:, 1], c=_ACCENT, s=_PT_SIZE, zorder=4, edgecolors="white", linewidths=_PT_EDGE_W, ) plt.tight_layout() plt.show()
# --------------------------------------------------------------------------- # Segment Intersections # ---------------------------------------------------------------------------
[docs] def plot_intersections(si_obj, title="Segment Intersections"): """Plot segments and their intersection points. Args: si_obj: A ``SegmentIntersection`` instance. title: Title for the matplotlib figure. """ fig, ax = _new_fig() _style_ax(ax, title) ax.set_aspect("equal") segs = si_obj.get_segments() intersections = si_obj.find_intersections() # Draw segments for seg in segs: ax.plot( [seg[0][0], seg[1][0]], [seg[0][1], seg[1][1]], color=_CHARCOAL, linewidth=_LINE_W, zorder=2, solid_capstyle="round", ) # Segment endpoints endpoints_x = [p[0] for seg in segs for p in seg] endpoints_y = [p[1] for seg in segs for p in seg] ax.scatter( endpoints_x, endpoints_y, c=_INK, s=_PT_SIZE, zorder=3, edgecolors="white", linewidths=_PT_EDGE_W, ) # Intersection points if intersections: ix = [p[0] for p in intersections] iy = [p[1] for p in intersections] ax.scatter( ix, iy, c=_ACCENT, s=50, zorder=5, marker="o", edgecolors="white", linewidths=1.2, ) plt.tight_layout() plt.show()
# --------------------------------------------------------------------------- # Voronoi # ---------------------------------------------------------------------------
[docs] def plot_voronoi(voronoi_obj, cells): """Plot the Voronoi diagram showing sites and cell edges. Args: voronoi_obj: A ``VoronoiDiagram`` instance. cells: The cell list returned by ``build_voronoi_diagram()``. """ data = np.array(voronoi_obj.data) fig, ax = _new_fig(figsize=(5.5, 5.5)) _style_ax(ax, "Voronoi Diagram") for c in cells: for line in c[1]: ax.plot( [line[0][0], line[1][0]], [line[0][1], line[1][1]], linewidth=_LINE_W_THIN, color=(*_hex_to_rgb(_CHARCOAL), 0.45), solid_capstyle="round", zorder=2, ) ax.scatter( data[:, 0], data[:, 1], c=_ACCENT, s=_PT_SIZE, zorder=4, edgecolors="white", linewidths=_PT_EDGE_W, ) pad_x = (data[:, 0].max() - data[:, 0].min()) * 0.12 pad_y = (data[:, 1].max() - data[:, 1].min()) * 0.12 ax.set_xlim(data[:, 0].min() - pad_x, data[:, 0].max() + pad_x) ax.set_ylim(data[:, 1].min() - pad_y, data[:, 1].max() + pad_y) plt.tight_layout() plt.show()