"""UML class diagram; Python 3.14.4, matplotlib 3.10.7, numpy 2.3.5. Run from the desired output directory. Writes its basename plus .png to CWD. """ from pathlib import Path import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib.patches import Rectangle, FancyArrowPatch EXTENDED = True fig, ax = plt.subplots(figsize=(14, 10 if EXTENDED else 8), dpi=100) fig.patch.set_facecolor("white") ax.set_facecolor("white") ax.set_xlim(0, 14) ax.set_ylim(0, 10 if EXTENDED else 8) ax.axis("off") def box(x, y, w, title, rows, h=None, tint="#eef5fc"): h = h or (0.72 + 0.31 * len(rows)) ax.add_patch(Rectangle((x-w/2, y-h/2), w, h, facecolor="white", edgecolor="#263547", lw=1.3, zorder=2)) ax.add_patch(Rectangle((x-w/2, y+h/2-0.58), w, 0.58, facecolor=tint, edgecolor="#263547", lw=1.1, zorder=2)) ax.text(x, y+h/2-0.29, title, ha="center", va="center", fontsize=11, weight="bold", zorder=3) for j, row in enumerate(rows): ax.text(x-w/2+0.12, y+h/2-0.79-0.31*j, row, ha="left", va="center", fontsize=9, family="DejaVu Sans Mono", zorder=3) return (x-w/2, x+w/2, y-h/2, y+h/2) def arrow(points, realization=False, label=None, label_xy=None): for a,b in zip(points[:-2],points[1:-1]): ax.plot([a[0],b[0]],[a[1],b[1]], color="#46566b", lw=1.1, ls="--" if realization else "-", zorder=1) patch=FancyArrowPatch(points[-2],points[-1], arrowstyle="-|>" if realization else "->", mutation_scale=15, lw=1.2, linestyle="--" if realization else "-", edgecolor="#46566b", facecolor="white", zorder=1) ax.add_patch(patch) if label: ax.text(*label_xy, label, ha="center", va="center", fontsize=9, color="#253a55", bbox=dict(facecolor="white",edgecolor="none",pad=1),zorder=4) if not EXTENDED: eq=box(3.1,5.3,5.8,"«trait» Equipment",[ "+ name:String; age:Int; weight:Int", "- _time:Int; _heartRate:Int", "- _functionalities:List[Functionality]", "+ time:Int; time_=(Int):Unit", "+ heartRate:Int; heartRate_=(Int):Unit", "+ addFunctionality(f):Unit", "+ deleteFunctionality(f):List[Functionality]", "+ notifyFunctionalities():Unit", "+ updateUserMeasures(Int,Int):Unit"]) fu=box(10.1,6.2,3.6,"«trait» Functionality",["+ update():Unit"]) gy=box(3.1,1.65,5.8,"GymEquipment",[ "+ name:String; age:Int; weight:Int", "+ updateUserMeasures(Int,Int):Unit"]) pe=box(8.0,2.3,3.4,"Performance",["- e:GymEquipment","+ update():Unit"]) pr=box(11.95,2.3,3.4,"Prediction",["- e:GymEquipment","+ update():Unit"]) arrow([(3.1,gy[3]),(3.1,eq[2])],True) arrow([(8,pe[3]),(8,4.9),(9.3,fu[2])],True) arrow([(11.95,pr[3]),(11.95,4.9),(10.9,fu[2])],True) arrow([(eq[1],6.2),(fu[0],6.2)],label="registered observers: 0..*",label_xy=(7.1,6.6)) arrow([(pe[0],2.3),(6.1,2.3),(gy[1],2.0)],label="e: 1",label_xy=(6.3,1.95)) arrow([(11.95,pr[2]),(11.95,.45),(3.1,.45),(3.1,gy[2])],label="e: 1",label_xy=(8.6,.45)) ax.text(7,7.75,"Original gym observer design",ha="center",fontsize=15,weight="bold") else: eq=box(3.0,7.0,5.6,"«trait» Equipment",[ "+ name:String; age:Int; weight:Int", "- _time:Int; _heartRate:Int", "- _functionalities:List[Functionality]", "+ time:Int; time_=(Int):Unit", "+ heartRate:Int; heartRate_=(Int):Unit", "+ addFunctionality(f):Unit", "+ deleteFunctionality(f):List[Functionality]", "+ notifyFunctionalities():Unit", "+ updateUserMeasures(Int,Int):Unit"]) fu=box(10.0,8.4,3.7,"«trait» Functionality",["+ update():Unit"]) gy=box(3.0,2.8,5.6,"GymEquipment",[ "+ name:String; age:Int; weight:Int", "+ updateUserMeasures(Int,Int):Unit"]) pe=box(7.85,4.9,3.4,"Performance",["- e:GymEquipment","- calories:Int","+ update():Unit"]) pr=box(11.8,4.9,3.4,"Prediction",["- e:GymEquipment","+ update():Unit"]) bo=box(9.8,1.8,6.6,"«object / singleton» GymStandings",[ "- scores:Map[GymEquipment,Int]", "+ record(e:GymEquipment,score:Int):Unit", "+ remove(e:GymEquipment):Unit", "+ leaders:List[(GymEquipment,Int)]"],tint="#e7f4e9") arrow([(3,gy[3]),(3,eq[2])],True) arrow([(7.85,pe[3]),(7.85,7.2),(9.2,fu[2])],True) arrow([(11.8,pr[3]),(11.8,7.2),(10.8,fu[2])],True) arrow([(eq[1],8.4),(fu[0],8.4)],label="observers: 0..*",label_xy=(7,8.75)) arrow([(pe[0],4.9),(5.95,4.9),(5.95,2.8),(gy[1],2.8)],label="e: 1",label_xy=(6.3,3.7)) arrow([(13.5,4.9),(13.8,4.9),(13.8,.25),(.15,.25),(.15,2.8),(gy[0],2.8)],label="Prediction.e: 1",label_xy=(3.2,.25)) arrow([(7.85,pe[2]),(7.85,bo[3])],label="reports current score",label_xy=(9.4,3.5)) arrow([(bo[0],1.8),(5.9,1.8),(4.4,gy[2])],label="keys: 0..* equipment",label_xy=(4.5,1.3)) ax.text(7,9.7,"Gym-wide standings through one shared object",ha="center",fontsize=15,weight="bold") ax.text(7,0.02 if not EXTENDED else .02,"Dashed hollow triangle: realizes trait Arrow: association / report dependency",ha="center",va="bottom",fontsize=9,color="#46566b") fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False, bbox_inches="tight", pad_inches=.13) plt.close(fig)