add plot generation scripts adjust validation tools add up-to-date results
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"""Shared appearance and PNG/SVG export for PIM result plots."""
|
||||
|
||||
from pathlib import Path
|
||||
from math import ceil
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Patch
|
||||
from matplotlib.path import Path as PlotPath
|
||||
from matplotlib.patheffects import AbstractPathEffect
|
||||
from matplotlib.transforms import Affine2D, IdentityTransform, TransformedPath
|
||||
|
||||
|
||||
TEXT_COLOR = "#243449"
|
||||
HATCH_COLOR = "#C62828"
|
||||
COLORS = ("#2B65A1", "#168C8B", "#D18D32", "#8062A5", "#B65D60")
|
||||
|
||||
|
||||
class CenteredHatch(AbstractPathEffect):
|
||||
"""Anchor dot and diagonal hatch patterns to each rectangle's center."""
|
||||
|
||||
def draw_path(self, renderer, gc, tpath, affine, rgbFace=None):
|
||||
hatch = gc.get_hatch() or ""
|
||||
base_gc = renderer.new_gc()
|
||||
base_gc.copy_properties(gc)
|
||||
base_gc.set_hatch("".join(symbol for symbol in hatch if symbol not in "./\\"))
|
||||
renderer.draw_path(base_gc, tpath, affine, rgbFace)
|
||||
base_gc.restore()
|
||||
|
||||
bounds = affine.transform_path(tpath).get_extents()
|
||||
center_x, center_y = (bounds.x0 + bounds.x1) / 2, (bounds.y0 + bounds.y1) / 2
|
||||
vertices = []
|
||||
diagonal = 2 ** -0.5
|
||||
for symbols, dx, dy in (("/", diagonal, diagonal), ("\\", diagonal, -diagonal)):
|
||||
count = sum(hatch.count(symbol) for symbol in symbols)
|
||||
if count == 0:
|
||||
continue
|
||||
spacing = renderer.points_to_pixels(12 / count)
|
||||
half_span = (abs(dy) * bounds.width + abs(dx) * bounds.height) / 2
|
||||
half_length = (abs(dx) * bounds.width + abs(dy) * bounds.height) / 2
|
||||
extent = ceil(half_span / spacing)
|
||||
for step in range(-extent, extent + 1):
|
||||
x = center_x - dy * step * spacing
|
||||
y = center_y + dx * step * spacing
|
||||
vertices.extend(((x - dx * half_length, y - dy * half_length),
|
||||
(x + dx * half_length, y + dy * half_length)))
|
||||
line_gc = renderer.new_gc()
|
||||
line_gc.copy_properties(gc)
|
||||
line_gc.set_hatch(None)
|
||||
line_gc.set_foreground(gc.get_hatch_color())
|
||||
line_gc.set_linewidth(gc.get_hatch_linewidth())
|
||||
line_gc.set_clip_path(TransformedPath(tpath, affine))
|
||||
if vertices:
|
||||
renderer.draw_path(line_gc, PlotPath(vertices, [PlotPath.MOVETO, PlotPath.LINETO] * (len(vertices) // 2)),
|
||||
IdentityTransform())
|
||||
if "." in hatch:
|
||||
spacing = renderer.points_to_pixels(12 / hatch.count("."))
|
||||
x_extent = ceil(bounds.width / (2 * spacing))
|
||||
y_extent = ceil(bounds.height / (2 * spacing))
|
||||
centers = [(center_x + x * spacing, center_y + y * spacing)
|
||||
for x in range(-x_extent, x_extent + 1)
|
||||
for y in range(-y_extent, y_extent + 1)]
|
||||
line_gc.set_linewidth(0)
|
||||
renderer.draw_markers(line_gc, PlotPath.unit_circle(),
|
||||
Affine2D().scale(renderer.points_to_pixels(0.9)),
|
||||
PlotPath(centers), IdentityTransform(), gc.get_hatch_color())
|
||||
line_gc.restore()
|
||||
|
||||
|
||||
def configure_style() -> None:
|
||||
plt.rcParams.update({
|
||||
"font.family": "DejaVu Sans",
|
||||
"axes.spines.top": False,
|
||||
"axes.spines.right": False,
|
||||
"axes.labelcolor": TEXT_COLOR,
|
||||
"text.color": TEXT_COLOR,
|
||||
"xtick.color": TEXT_COLOR,
|
||||
"ytick.color": TEXT_COLOR,
|
||||
"axes.edgecolor": "#CFD6DF",
|
||||
"savefig.facecolor": "white",
|
||||
"figure.facecolor": "white",
|
||||
})
|
||||
|
||||
|
||||
def value_label(ax, x, y, label: str, status: str = "PASS", direction: int = 1,
|
||||
fontsize: float = 8) -> None:
|
||||
alignment = "bottom" if direction > 0 else "top"
|
||||
rotation = 90 if direction > 0 else -90
|
||||
text = ax.text(x, y, label, rotation=rotation, ha="center", va=alignment,
|
||||
fontsize=fontsize, color=TEXT_COLOR)
|
||||
if status != "PASS":
|
||||
height = text.get_window_extent(ax.figure.canvas.get_renderer()).height
|
||||
offset = direction * (height * 72 / ax.figure.dpi + 3)
|
||||
ax.annotate("INVALID" if status == "FAIL" else status, (x, y),
|
||||
xytext=(0, offset), textcoords="offset points",
|
||||
rotation=rotation, ha="center", va=alignment, fontsize=fontsize,
|
||||
color=HATCH_COLOR)
|
||||
|
||||
|
||||
def fit_value_labels(fig) -> None:
|
||||
"""Reserve vertical space for rotated labels using their rendered size."""
|
||||
for ax in fig.axes:
|
||||
for text in ax.texts:
|
||||
text.set_in_layout(False)
|
||||
fig.canvas.draw()
|
||||
renderer = fig.canvas.get_renderer()
|
||||
limits = []
|
||||
for ax in fig.axes:
|
||||
bounds = ax.get_window_extent(renderer)
|
||||
labels = []
|
||||
for text in ax.texts:
|
||||
x, y = text.xy if hasattr(text, "xy") else text.get_position()
|
||||
anchor = ax.transData.transform((x, y))[1]
|
||||
extent = text.get_window_extent(renderer)
|
||||
labels.append((y, max(0, anchor - extent.y0 + 3) / bounds.height,
|
||||
max(0, extent.y1 - anchor + 3) / bounds.height))
|
||||
lower, upper = ax.get_ylim()
|
||||
for _ in range(12):
|
||||
lower = min([lower, *((y - below * upper) / (1 - below) for y, below, _ in labels)])
|
||||
upper = max([upper, *((y - above * lower) / (1 - above) for y, _, above in labels)])
|
||||
limits.append((lower, upper))
|
||||
for ax in fig.axes:
|
||||
ax.set_ylim(min(lower for lower, _ in limits), max(upper for _, upper in limits))
|
||||
|
||||
|
||||
def save_figure(fig, basename: str, output_dir: Path) -> None:
|
||||
for patch in fig.findobj(Patch):
|
||||
hatch = patch.get_hatch() or ""
|
||||
if any(symbol in hatch for symbol in "./\\"):
|
||||
patch.set_path_effects([CenteredHatch()])
|
||||
for extension in ("png", "svg"):
|
||||
target = output_dir / extension / f"{basename}.{extension}"
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
fig.savefig(target, dpi=220, bbox_inches="tight", pad_inches=0.04)
|
||||
print(target)
|
||||
plt.close(fig)
|
||||
Reference in New Issue
Block a user