#!/usr/bin/env python3 # -*- coding: utf-8 -*- import math import matplotlib.pyplot as plt def _fmt(val: float) -> str: """Formate un nombre avec virgule comme séparateur décimal.""" s = f"{val:.2f}".rstrip("0").rstrip(".") s = s.replace(".", ",") # <-- conversion point → virgule return s if s else "0" def papier_grille_axes( largeur_cm=20, hauteur_cm=20, marge_mm=10, subdivisions=10, # nb de lignes par cm (10 = millimétré, 5 = tous les 2 mm, 1 = seulement cm) graduation_mm=10, # espacement physique des graduations (mm) - 10 = 1 cm facteur_x=1.0, # facteur d’échelle pour les valeurs affichées sur X facteur_y=1.0, # facteur d’échelle pour les valeurs affichées sur Y axe_x_mm=None, # position verticale de l’axe X (mm depuis le bas) ; None = centre axe_y_mm=None, # position horizontale de l’axe Y (mm depuis la gauche) ; None = centre label_x="x", # nom de l’axe des abscisses (mettre "" pour ne pas afficher) label_y="y", # nom de l’axe des ordonnées (mettre "" pour ne pas afficher) fichier="papier_grille_axes.png", dpi=600, decal_fleche=1.4, # dépassement de la flèche à droite/haut (mm) depart_axes_ext=0.8 # dépassement au départ à gauche/bas (mm) ): """ Génère une image PNG de papier quadrillé avec axes fléchés. - Quadrillage gris (subdivisions/cm) renforcé à 5 mm et 1 cm, sans cadre. - Axes fléchés : X de (xmin - depart_axes_ext) -> (xmax + decal_fleche), Y de (ymin - depart_axes_ext) -> (ymax + decal_fleche). - Graduations/labels tous les 'graduation_mm' (par défaut 1 cm), valeurs multipliées par facteur_x / facteur_y. - Pas de graduation collée aux flèches (côté droit/haut). - Pas de graduation extrême gauche/bas — SAUF le **0**, qui est toujours affiché. - Origine (0;0) = croisement des axes (axe_x_mm / axe_y_mm). """ # ---------- Dimensions ---------- largeur_mm = int(round(largeur_cm * 10)) hauteur_mm = int(round(hauteur_cm * 10)) L_tot = largeur_mm + 2 * marge_mm H_tot = hauteur_mm + 2 * marge_mm # ---------- Figure ---------- fig_w_in, fig_h_in = L_tot / 25.4, H_tot / 25.4 fig, ax = plt.subplots(figsize=(fig_w_in, fig_h_in)) ax.set_xlim(0, L_tot) ax.set_ylim(0, H_tot) ax.set_aspect("equal") ax.axis("off") # ---------- Zone utile ---------- xmin, xmax = marge_mm, marge_mm + largeur_mm ymin, ymax = marge_mm, marge_mm + hauteur_mm # ---------- Position des axes ---------- axe_x = ymin + hauteur_mm // 2 if axe_x_mm is None else ymin + int(round(axe_x_mm)) axe_y = xmin + largeur_mm // 2 if axe_y_mm is None else xmin + int(round(axe_y_mm)) # ---------- Styles ---------- color_sub = "lightgrey" # petites subdivisions color_mid = "silver" # lignes de 5 mm color_cm = "darkgrey" # lignes de 1 cm thin, thick5, thick10 = 0.3, 0.6, 1.0 # ---------- Subdivisions ---------- pas_mm = 10 / subdivisions if subdivisions and subdivisions > 0 else 10 # ---------- Grille VERTICALE ---------- n_x = int((xmax - xmin) / pas_mm) for i in range(n_x + 1): x = xmin + i * pas_mm lw, col = thin, color_sub if abs((x - xmin) % 10) < 1e-6: lw, col = thick10, color_cm elif subdivisions >= 5 and abs((x - xmin) % 5) < 1e-6: lw, col = thick5, color_mid ax.plot([x, x], [ymin, ymax], color=col, linewidth=lw) # ---------- Grille HORIZONTALE ---------- n_y = int((ymax - ymin) / pas_mm) for j in range(n_y + 1): y = ymin + j * pas_mm lw, col = thin, color_sub if abs((y - ymin) % 10) < 1e-6: lw, col = thick10, color_cm elif subdivisions >= 5 and abs((y - ymin) % 5) < 1e-6: lw, col = thick5, color_mid ax.plot([xmin, xmax], [y, y], color=col, linewidth=lw) # ---------- Axes fléchés + graduations ---------- eps = 1e-6 # Axe X if ymin <= axe_x <= ymax: ax.annotate( "", xy=(xmax + decal_fleche, axe_x), xytext=(xmin - depart_axes_ext, axe_x), arrowprops=dict(arrowstyle="->", color="black", linewidth=1.2) ) if graduation_mm > 0: n_min = math.ceil((xmin - axe_y) / graduation_mm) n_max = math.floor((xmax - axe_y - graduation_mm) / graduation_mm) for n in range(n_min, n_max + 1): xg = axe_y + n * graduation_mm # si la graduation est collée au bord gauche, on la saute sauf si c'est 0 if abs(xg - xmin) < eps and n != 0: continue ax.plot([xg, xg], [axe_x - 2, axe_x + 2], color="black", linewidth=1) val = (n * graduation_mm / 10.0) * facteur_x ax.text(xg, axe_x - 3, _fmt(val), ha="center", va="top", fontsize=7, color="black") #, fontweight="bold") if label_x: ax.text(xmax + decal_fleche + 2, axe_x, label_x, fontsize=9, color="black", va="center", ha="left") # Axe Y if xmin <= axe_y <= xmax: ax.annotate( "", xy=(axe_y, ymax + decal_fleche), xytext=(axe_y, ymin - depart_axes_ext), arrowprops=dict(arrowstyle="->", color="black", linewidth=1.2) ) if graduation_mm > 0: m_min = math.ceil((ymin - axe_x) / graduation_mm) m_max = math.floor((ymax - axe_x - graduation_mm) / graduation_mm) for m in range(m_min, m_max + 1): yg = axe_x + m * graduation_mm # si la graduation est collée au bord bas, on la saute sauf si c'est 0 if abs(yg - ymin) < eps and m != 0: continue ax.plot([axe_y - 2, axe_y + 2], [yg, yg], color="black", linewidth=1) val = (m * graduation_mm / 10.0) * facteur_y ax.text(axe_y - 3, yg, _fmt(val), ha="right", va="center", fontsize=7, color="black") #, fontweight="bold") if label_y: ax.text(axe_y, ymax + decal_fleche + 2, label_y, fontsize=9, color="black", ha="center", va="bottom") # ---------- Export ---------- fig.savefig(fichier, dpi=dpi, bbox_inches="tight", pad_inches=0) plt.close(fig) print("Fichier généré :", fichier) # ===================== # Exemples d'utilisation # ===================== """ if __name__ == "__main__": # Exemple 1 : Millimétré, axes au centre, côtés négatifs visibles (par défaut) papier_grille_axes( largeur_cm=20, hauteur_cm=20, subdivisions=10, graduation_mm=10, facteur_x=1, facteur_y=1, label_x="x", label_y="y", fichier="papier_millimetre.png" ) # Exemple 2 : Subdivisions 5 (tous les 2 mm), origine décalée, # Y en dizaines de cm, labels personnalisés papier_grille_axes( largeur_cm=20, hauteur_cm=20, subdivisions=5, graduation_mm=10, facteur_x=1, facteur_y=10, axe_x_mm=50, axe_y_mm=30, label_x="x", label_y="y", fichier="papier_subdiv5.png" ) # Exemple 3 : Millimétré, axes au centre, échelles différentes X/Y papier_grille_axes( largeur_cm=20, hauteur_cm=20, subdivisions=10, graduation_mm=10, facteur_x=1, facteur_y=10, label_x="X", label_y="Y", fichier="papier_diff_echelles.png" ) # Exemple 4 : Axes en bas à gauche (origine dans le coin), # vérifie que la graduation **0** s'affiche au bord papier_grille_axes( largeur_cm=20, hauteur_cm=20, subdivisions=10, graduation_mm=10, facteur_x=1, facteur_y=1, axe_x_mm=0, axe_y_mm=0, # origine en (xmin, ymin) label_x="x (cm)", label_y="y (cm)", fichier="papier_axes_en_coin.png" ) """ if __name__ == "__main__": papier_grille_axes( largeur_cm=20, hauteur_cm=20, subdivisions=5, graduation_mm=10, facteur_x=1, facteur_y=10, axe_x_mm=50, axe_y_mm=30, label_x="x", label_y="y" )