Skip to main content

embassy_st7789v_plot/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3
4//! # embassy-st7789v-plot
5//!
6//! Moteur de tracé de graphiques cartésiens (X, Y) adaptatifs et configurables
7//! pour écrans TFT LCD ST7789V, s'appuyant sur la structure `Graphics`.
8//!
9//! ## Caractéristiques principales
10//!
11//! - **`#![no_std]` + `#![forbid(unsafe_code)]`** : Sûr et embarqué
12//! - **Zéro allocation dynamique** : Buffers statiques uniquement (ring buffer)
13//! - **API async** : Basée sur Embassy pour ST7789V
14//! - **Axes configurables** : Graduations statiques avec labels personnalisés
15//! - **Grille adaptative** : Grille horizontale/verticale pour meilleure lisibilité
16//! - **Historique circulaire** : Jusqu'à 240 points (limité par la largeur écran)
17//! - **Protection des bordures** : Les labels de graduations restent à l'écran
18//!
19//! ## Structures principales
20//!
21//! - [`Graphics`] : Contexte graphique pour les primitives de dessin
22//! - [`AxisConfig`] : Configuration d'un axe (min, max, pas de graduation, label)
23//! - [`PlotConfig`] : Configuration complète du graphique (position, marges, couleurs)
24//! - [`LineChart`] : Gestionnaire du graphique avec données historiques
25//!
26//! ## Exemple d'utilisation
27//!
28//! ```no_run
29//! # use embassy_st7789v::{Color, St7789v, NoPin};
30//! # use embedded_hal::digital::OutputPin;
31//! # use embedded_hal_async::spi::SpiDevice;
32//! use embassy_st7789v_plot::{Graphics, AxisConfig, PlotConfig, LineChart};
33//!
34//! # async fn example<SPI, DC>(display: &mut St7789v<SPI, DC, NoPin>)
35//! # where
36//! #     SPI: SpiDevice,
37//! #     DC: OutputPin,
38//! # {
39//! // Créer les configurations d'axes
40//! let x_axis = AxisConfig::new(0.0, 10.0, 2.0, b"Time (s)");
41//! let y_axis = AxisConfig::new(0.0, 100.0, 20.0, b"Temp (C)");
42//!
43//! // Créer la configuration complète du graphique
44//! let config = PlotConfig {
45//!     x: 10,
46//!     y: 10,
47//!     width: 220,
48//!     height: 200,
49//!     margin_left: 30,
50//!     margin_right: 10,
51//!     margin_top: 10,
52//!     margin_bottom: 30,
53//!     x_axis,
54//!     y_axis,
55//!     bg_color: Color::BLACK,
56//!     line_color: Color::GREEN,
57//!     axis_color: Color::WHITE,
58//!     grid_color: Color::from_rgb(64, 64, 64),
59//!     text_color: Color::WHITE,
60//!     label_color: Color::CYAN,
61//! };
62//!
63//! // Créer le gestionnaire de graphique (N = 100 points max)
64//! let mut chart: LineChart<100> = LineChart::new(config);
65//!
66//! // Ajouter des données
67//! chart.push(45.2);
68//! chart.push(47.8);
69//! chart.push(52.1);
70//!
71//! // Afficher le graphique
72//! let mut gfx = Graphics::new_no_rst(display);
73//! chart.render(&mut gfx).await;
74//! # }
75//! ```
76//!
77//! ## API asynchrone
78//!
79//! Tous les appels de rendu (`render`, `line`, `pixel`) sont asynchrones pour permettre
80//! une meilleure intégration avec l'écosystème Embassy et éviter les blocages lors
81//! de la communication SPI.
82
83use embassy_st7789v::{Color, NoPin, St7789v, SCREEN_H, SCREEN_W};
84use embedded_hal::digital::OutputPin;
85use embedded_hal_async::spi::SpiDevice;
86
87/// Taille maximale de l'historique des données du graphique.
88pub const PLOT_HISTORY_LIMIT: usize = 240;
89
90// ─────────────────────────────────────────────────────────────────────────────
91// Contexte graphique embarqué (repris de tes primitives)
92// ─────────────────────────────────────────────────────────────────────────────
93
94pub struct Graphics<'a, SPI, DC, RST = NoPin>
95where
96    SPI: SpiDevice,
97    DC: OutputPin,
98    RST: OutputPin,
99{
100    /// Référence vers l'affichage ST7789V
101    pub display: &'a mut St7789v<SPI, DC, RST>,
102}
103
104impl<'a, SPI, DC> Graphics<'a, SPI, DC, NoPin>
105where
106    SPI: SpiDevice,
107    DC: OutputPin,
108{
109    /// Crée un nouveau contexte graphique sans broche RST.
110    ///
111    /// # Arguments
112    ///
113    /// * `display` - Référence mutable vers l'affichage ST7789V
114    ///
115    /// # Exemple
116    ///
117    /// ```no_run
118    /// # use embassy_st7789v::{St7789v, NoPin};
119    /// # use embedded_hal::digital::OutputPin;
120    /// # use embedded_hal_async::spi::SpiDevice;
121    /// # async fn example<SPI, DC>(display: &mut St7789v<SPI, DC, NoPin>) where SPI: SpiDevice, DC: OutputPin {
122    /// use embassy_st7789v_plot::Graphics;
123    ///
124    /// let mut gfx = Graphics::new_no_rst(display);
125    /// # }
126    /// ```
127    #[inline]
128    pub fn new_no_rst(display: &'a mut St7789v<SPI, DC, NoPin>) -> Self {
129        Self { display }
130    }
131}
132
133impl<'a, SPI, DC, RST> Graphics<'a, SPI, DC, RST>
134where
135    SPI: SpiDevice,
136    DC: OutputPin,
137    RST: OutputPin,
138{
139    /// Crée un nouveau contexte graphique avec broche RST.
140    ///
141    /// # Arguments
142    ///
143    /// * `display` - Référence mutable vers l'affichage ST7789V
144    ///
145    /// # Exemple
146    ///
147    /// ```no_run
148    /// # use embassy_st7789v::St7789v;
149    /// # use embedded_hal::digital::OutputPin;
150    /// # use embedded_hal_async::spi::SpiDevice;
151    /// # async fn example<SPI, DC, RST>(display: &mut St7789v<SPI, DC, RST>) where SPI: SpiDevice, DC: OutputPin, RST: OutputPin {
152    /// use embassy_st7789v_plot::Graphics;
153    ///
154    /// let mut gfx = Graphics::new(display);
155    /// # }
156    /// ```
157    #[inline]
158    pub fn new(display: &'a mut St7789v<SPI, DC, RST>) -> Self {
159        Self { display }
160    }
161
162    /// Trace un pixel à la position (x, y) avec la couleur donnée.
163    ///
164    /// Les coordonnées négatives ou en dehors de l'écran sont ignorées silencieusement.
165    ///
166    /// # Arguments
167    ///
168    /// * `x` - Coordonnée X (peut être négative ou hors écran)
169    /// * `y` - Coordonnée Y (peut être négative ou hors écran)
170    /// * `color` - Couleur du pixel
171    ///
172    /// # Exemple
173    ///
174    /// ```no_run
175    /// # use embassy_st7789v::{Color, St7789v};
176    /// # use embedded_hal::digital::OutputPin;
177    /// # use embedded_hal_async::spi::SpiDevice;
178    /// # async fn example<SPI, DC, RST>(display: &mut St7789v<SPI, DC, RST>) where SPI: SpiDevice, DC: OutputPin, RST: OutputPin {
179    /// use embassy_st7789v_plot::Graphics;
180    ///
181    /// let mut gfx = Graphics::new(display);
182    /// gfx.pixel(100, 150, Color::GREEN).await;
183    /// # }
184    /// ```
185    #[inline(always)]
186    pub async fn pixel(&mut self, x: i32, y: i32, color: Color) {
187        if x >= 0 && y >= 0 && x < SCREEN_W as i32 && y < SCREEN_H as i32 {
188            let _ = self.display.draw_pixel(x as u16, y as u16, color).await;
189        }
190    }
191}
192
193pub async fn line<SPI, DC, RST>(
194    gfx: &mut Graphics<'_, SPI, DC, RST>,
195    mut x0: i32,
196    mut y0: i32,
197    x1: i32,
198    y1: i32,
199    color: Color,
200) where
201    SPI: SpiDevice,
202    DC: OutputPin,
203    RST: OutputPin,
204{
205    //! Trace une ligne entre deux points en utilisant l'algorithme de Bresenham.
206    //!
207    //! Cette fonction utilise l'algorithme de Bresenham pour tracer une ligne
208    //! entre les points (x0, y0) et (x1, y1). Elle gère correctement les lignes
209    //! en dehors de l'écran via la vérification de limites dans [`Graphics::pixel`].
210    //!
211    //! # Arguments
212    //!
213    //! * `gfx` - Contexte graphique
214    //! * `x0` - Coordonnée X du point de départ
215    //! * `y0` - Coordonnée Y du point de départ
216    //! * `x1` - Coordonnée X du point d'arrivée
217    //! * `y1` - Coordonnée Y du point d'arrivée
218    //! * `color` - Couleur de la ligne
219    //!
220    //! # Exemple
221    //!
222    //! ```no_run
223    //! # use embassy_st7789v::{Color, St7789v};
224    //! # use embedded_hal::digital::OutputPin;
225    //! # use embedded_hal_async::spi::SpiDevice;
226    //! # async fn example<SPI, DC, RST>(display: &mut St7789v<SPI, DC, RST>) where SPI: SpiDevice, DC: OutputPin, RST: OutputPin {
227    //! use embassy_st7789v_plot::{Graphics, line};
228    //!
229    //! let mut gfx = Graphics::new(display);
230    //! line(&mut gfx, 10, 10, 100, 50, Color::BLUE).await;
231    //! # }
232    //! ```
233    let dx = (x1 - x0).abs();
234    let sx = if x0 < x1 { 1 } else { -1 };
235    let dy = -(y1 - y0).abs();
236    let sy = if y0 < y1 { 1 } else { -1 };
237    let mut err = dx + dy;
238
239    loop {
240        gfx.pixel(x0, y0, color).await;
241        if x0 == x1 && y0 == y1 {
242            break;
243        }
244        let e2 = 2 * err;
245        if e2 >= dy {
246            err += dy;
247            x0 += sx;
248        }
249        if e2 <= dx {
250            err += dx;
251            y0 += sy;
252        }
253    }
254}
255
256// ─────────────────────────────────────────────────────────────────────────────
257// Configuration des Axes
258// ─────────────────────────────────────────────────────────────────────────────
259
260/// Définit un axe avec graduation statique fixe.
261///
262/// Cette structure configure un axe du graphique avec une plage de valeurs,
263/// un pas de graduation régulier et un label descriptif.
264///
265/// # Champs
266///
267/// * `start` - Valeur minimale de l'axe (ex: 0.0)
268/// * `end` - Valeur maximale de l'axe (ex: 10.0)
269/// * `step` - Espacement régulier entre graduations (ex: 1.0)
270/// * `label` - Label texte affiché le long de l'axe (ex: b"Temp (C)")
271///
272/// # Exemple
273///
274/// ```
275/// use embassy_st7789v_plot::AxisConfig;
276///
277/// // Axe des temps de 0 à 60 secondes avec graduations tous les 10s
278/// let time_axis = AxisConfig::new(0.0, 60.0, 10.0, b"Time (s)");
279/// assert!(time_axis.is_valid());
280/// assert_eq!(time_axis.tick_count(), 7); // 0, 10, 20, 30, 40, 50, 60
281///
282/// // Axe de température de -10 à 50°C avec graduations tous les 10°C
283/// let temp_axis = AxisConfig::new(-10.0, 50.0, 10.0, b"Temp (C)");
284/// assert_eq!(temp_axis.tick_count(), 7);
285/// ```
286#[derive(Clone, Copy, Debug)]
287pub struct AxisConfig {
288    pub start: f32,
289    pub end: f32,
290    pub step: f32,
291    pub label: &'static [u8],
292}
293
294impl AxisConfig {
295    /// Crée une nouvelle configuration d'axe.
296    ///
297    /// # Arguments
298    ///
299    /// * `start` - Valeur minimale (doit être < `end`)
300    /// * `end` - Valeur maximale (doit être > `start`)
301    /// * `step` - Pas de graduation (doit être > 0)
302    /// * `label` - Label statique affiché (par exemple b"Temp (C)")
303    ///
304    /// # Panics
305    ///
306    /// Ne paniquera pas ici, mais utilisez [`is_valid`](Self::is_valid) après construction
307    /// pour vérifier la cohérence.
308    pub const fn new(start: f32, end: f32, step: f32, label: &'static [u8]) -> Self {
309        Self { start, end, step, label }
310    }
311
312    /// Vérifie la cohérence de la configuration.
313    ///
314    /// Retourne `true` si :
315    /// - `step` > 0.0
316    /// - `end` > `start`
317    ///
318    /// # Exemple
319    ///
320    /// ```
321    /// use embassy_st7789v_plot::AxisConfig;
322    ///
323    /// let valid = AxisConfig::new(0.0, 10.0, 1.0, b"X");
324    /// assert!(valid.is_valid());
325    ///
326    /// let invalid = AxisConfig::new(10.0, 0.0, 1.0, b"X");
327    /// assert!(!invalid.is_valid());
328    /// ```
329    pub fn is_valid(&self) -> bool {
330        self.step > 0.0 && self.end > self.start
331    }
332
333    /// Calcule le nombre de graduations (incluant start et end).
334    ///
335    /// En `no_std`, le cast direct remplace `f32::floor()`.
336    ///
337    /// # Retour
338    ///
339    /// Nombre de ticks incluant les extrémités, ou 0 si la configuration est invalide.
340    ///
341    /// # Exemple
342    ///
343    /// ```
344    /// use embassy_st7789v_plot::AxisConfig;
345    ///
346    /// let axis = AxisConfig::new(0.0, 10.0, 2.0, b"X");
347    /// assert_eq!(axis.tick_count(), 6); // 0, 2, 4, 6, 8, 10
348    /// ```
349    pub fn tick_count(&self) -> usize {
350        if !self.is_valid() {
351            return 0;
352        }
353        let count = ((self.end - self.start) / self.step) as usize;
354        count + 1
355    }
356}
357
358// ─────────────────────────────────────────────────────────────────────────────
359// Configuration et Structure de Traçage
360// ─────────────────────────────────────────────────────────────────────────────
361
362/// Configuration complète du tracé graphique.
363///
364/// Cette structure définit tous les paramètres visuels et géométriques du graphique :
365/// position, taille, marges, axes, et palette de couleurs.
366///
367/// # Champs
368///
369/// * `x`, `y` - Position du coin haut-gauche du graphique (en pixels)
370/// * `width`, `height` - Dimensions du graphique (en pixels)
371/// * `margin_*` - Marges internes pour les axes et labels
372/// * `x_axis`, `y_axis` - Configurations des deux axes
373/// * `*_color` - Couleurs pour le fond, les lignes, axes, grille, texte, labels
374///
375/// # Exemple
376///
377/// ```
378/// use embassy_st7789v::{Color, SCREEN_W, SCREEN_H};
379/// use embassy_st7789v_plot::{AxisConfig, PlotConfig};
380///
381/// let x_axis = AxisConfig::new(0.0, 10.0, 2.0, b"Time (s)");
382/// let y_axis = AxisConfig::new(0.0, 100.0, 20.0, b"Value");
383///
384/// let config = PlotConfig {
385///     x: 10,
386///     y: 10,
387///     width: 220,
388///     height: 200,
389///     margin_left: 40,
390///     margin_right: 10,
391///     margin_top: 10,
392///     margin_bottom: 30,
393///     x_axis,
394///     y_axis,
395///     bg_color: Color::BLACK,
396///     line_color: Color::GREEN,
397///     axis_color: Color::WHITE,
398///     grid_color: Color::from_rgb(64, 64, 64),
399///     text_color: Color::WHITE,
400///     label_color: Color::CYAN,
401/// };
402/// ```
403#[derive(Clone, Copy, Debug)]
404pub struct PlotConfig {
405    pub x: i32,
406    pub y: i32,
407    pub width: i32,
408    pub height: i32,
409    pub margin_left: i32,
410    pub margin_right: i32,
411    pub margin_top: i32,
412    pub margin_bottom: i32,
413    pub x_axis: AxisConfig,
414    pub y_axis: AxisConfig,
415    pub bg_color: Color,
416    pub line_color: Color,
417    pub axis_color: Color,
418    pub grid_color: Color,
419    pub text_color: Color,
420    pub label_color: Color,
421}
422
423/// Gestionnaire du graphique avec axes statiques configurables.
424///
425/// `LineChart<N>` maintient un historique circulaire de N points de données et
426/// gère le rendu du graphique avec grille, axes, graduations et labels.
427///
428/// # Paramètre générique
429///
430/// * `N` - Nombre maximum de points historiques (doit être ≤ `PLOT_HISTORY_LIMIT` = 240)
431///
432/// # Fonctionnement interne
433///
434/// - **Ring buffer** : Les données sont stockées dans un tableau fixe avec un pointeur
435///   `head` qui tourne. Quand le buffer est plein, les nouvelles données écrasent les
436///   plus anciennes.
437/// - **Historique** : Seuls les N points les plus récents sont affichés.
438/// - **Rendu** : Les données sont converties en pixels via `scale_x()` et `scale_y()`,
439///   puis connectées par des lignes (Bresenham).
440///
441/// # Exemple
442///
443/// ```no_run
444/// # use embassy_st7789v::{Color, St7789v, NoPin};
445/// # use embedded_hal::digital::OutputPin;
446/// # use embedded_hal_async::spi::SpiDevice;
447/// use embassy_st7789v_plot::{Graphics, AxisConfig, PlotConfig, LineChart};
448///
449/// # async fn example<SPI, DC>(display: &mut St7789v<SPI, DC, NoPin>) where SPI: SpiDevice, DC: OutputPin {
450/// // Créer la config
451/// let config = PlotConfig {
452///     x: 10, y: 10, width: 220, height: 200,
453///     margin_left: 40, margin_right: 10, margin_top: 10, margin_bottom: 30,
454///     x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time"),
455///     y_axis: AxisConfig::new(0.0, 100.0, 20.0, b"Value"),
456///     bg_color: Color::BLACK,
457///     line_color: Color::GREEN,
458///     axis_color: Color::WHITE,
459///     grid_color: Color::from_rgb(64, 64, 64),
460///     text_color: Color::WHITE,
461///     label_color: Color::CYAN,
462/// };
463///
464/// // Créer et utiliser le graphique
465/// let mut chart: LineChart<100> = LineChart::new(config);
466/// chart.push(10.5);
467/// chart.push(12.3);
468/// chart.push(11.8);
469///
470/// let mut gfx = Graphics::new_no_rst(display);
471/// chart.render(&mut gfx).await;
472/// # }
473/// ```
474pub struct LineChart<const N: usize> {
475    config: PlotConfig,
476    data: [f32; N],
477    head: usize,
478    count: usize,
479    plot_x: i32,
480    plot_y: i32,
481    plot_w: i32,
482    plot_h: i32,
483}
484
485impl<const N: usize> LineChart<N> {
486    /// Crée un nouveau gestionnaire de graphique.
487    ///
488    /// # Arguments
489    ///
490    /// * `config` - Configuration complète du graphique
491    ///
492    /// # Panics
493    ///
494    /// Panique si :
495    /// - N > `PLOT_HISTORY_LIMIT` (240)
496    /// - La configuration d'axe X est invalide
497    /// - La configuration d'axe Y est invalide
498    ///
499    /// # Exemple
500    ///
501    /// ```
502    /// use embassy_st7789v::{Color};
503    /// use embassy_st7789v_plot::{AxisConfig, PlotConfig, LineChart};
504    ///
505    /// let config = PlotConfig {
506    ///     x: 10, y: 10, width: 220, height: 200,
507    ///     margin_left: 40, margin_right: 10, margin_top: 10, margin_bottom: 30,
508    ///     x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time"),
509    ///     y_axis: AxisConfig::new(0.0, 100.0, 20.0, b"Value"),
510    ///     bg_color: Color::BLACK,
511    ///     line_color: Color::GREEN,
512    ///     axis_color: Color::WHITE,
513    ///     grid_color: Color::from_rgb(64, 64, 64),
514    ///     text_color: Color::WHITE,
515    ///     label_color: Color::CYAN,
516    /// };
517    ///
518    /// let chart: LineChart<100> = LineChart::new(config);
519    /// ```
520    pub fn new(config: PlotConfig) -> Self {
521        assert!(N <= PLOT_HISTORY_LIMIT, "L'historique dépasse la limite physique horizontale.");
522        assert!(config.x_axis.is_valid(), "Configuration axe X invalide.");
523        assert!(config.y_axis.is_valid(), "Configuration axe Y invalide.");
524        
525        let plot_x = config.x + config.margin_left;
526        let plot_y = config.y + config.margin_top;
527        let plot_w = config.width - config.margin_left - config.margin_right;
528        let plot_h = config.height - config.margin_top - config.margin_bottom;
529
530        Self {
531            config,
532            data: [0.0; N],
533            head: 0,
534            count: 0,
535            plot_x,
536            plot_y,
537            plot_w,
538            plot_h,
539        }
540    }
541
542    /// Retourne une référence à la configuration du graphique.
543    #[inline]
544    pub fn config(&self) -> &PlotConfig {
545        &self.config
546    }
547
548    /// Ajoute une nouvelle valeur à l'historique.
549    ///
550    /// Si le buffer est plein (N points), la valeur la plus ancienne est remplacée.
551    ///
552    /// # Arguments
553    ///
554    /// * `value` - Valeur à ajouter (sera clampée à la plage Y-axis lors du rendu)
555    ///
556    /// # Exemple
557    ///
558    /// ```
559    /// use embassy_st7789v::{Color};
560    /// use embassy_st7789v_plot::{AxisConfig, PlotConfig, LineChart};
561    ///
562    /// let config = PlotConfig {
563    ///     x: 10, y: 10, width: 220, height: 200,
564    ///     margin_left: 40, margin_right: 10, margin_top: 10, margin_bottom: 30,
565    ///     x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time"),
566    ///     y_axis: AxisConfig::new(0.0, 100.0, 20.0, b"Value"),
567    ///     bg_color: Color::BLACK,
568    ///     line_color: Color::GREEN,
569    ///     axis_color: Color::WHITE,
570    ///     grid_color: Color::from_rgb(64, 64, 64),
571    ///     text_color: Color::WHITE,
572    ///     label_color: Color::CYAN,
573    /// };
574    ///
575    /// let mut chart: LineChart<100> = LineChart::new(config);
576    /// chart.push(50.0);
577    /// chart.push(55.5);
578    /// ```
579    pub fn push(&mut self, value: f32) {
580        self.data[self.head] = value;
581        self.head = (self.head + 1) % N;
582        if self.count < N {
583            self.count += 1;
584        }
585    }
586
587    /// Efface l'historique et réinitialise le graphique.
588    ///
589    /// # Exemple
590    ///
591    /// ```
592    /// use embassy_st7789v::{Color};
593    /// use embassy_st7789v_plot::{AxisConfig, PlotConfig, LineChart};
594    ///
595    /// let config = PlotConfig {
596    ///     x: 10, y: 10, width: 220, height: 200,
597    ///     margin_left: 40, margin_right: 10, margin_top: 10, margin_bottom: 30,
598    ///     x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time"),
599    ///     y_axis: AxisConfig::new(0.0, 100.0, 20.0, b"Value"),
600    ///     bg_color: Color::BLACK,
601    ///     line_color: Color::GREEN,
602    ///     axis_color: Color::WHITE,
603    ///     grid_color: Color::from_rgb(64, 64, 64),
604    ///     text_color: Color::WHITE,
605    ///     label_color: Color::CYAN,
606    /// };
607    ///
608    /// let mut chart: LineChart<100> = LineChart::new(config);
609    /// chart.push(50.0);
610    /// chart.clear();
611    /// ```
612    pub fn clear(&mut self) {
613        self.head = 0;
614        self.count = 0;
615        self.data.fill(0.0);
616    }
617
618    #[inline]
619    fn get_sample(&self, index: usize) -> f32 {
620        let oldest = if self.count < N { 0 } else { self.head };
621        self.data[(oldest + index) % N]
622    }
623
624    /// Convertit une valeur Y en coordonnée écran (pixels).
625    ///
626    /// La valeur est clampée à la plage [y_min, y_max] définie par `y_axis`,
627    /// puis convertie linéairement en pixels.
628    #[inline]
629    fn scale_y(&self, value: f32) -> i32 {
630        let y_min = self.config.y_axis.start;
631        let y_max = self.config.y_axis.end;
632        
633        if y_max <= y_min {
634            return self.plot_y + self.plot_h - 1;
635        }
636
637        let clamped_val = value.max(y_min).min(y_max);
638        let ratio = (clamped_val - y_min) / (y_max - y_min);
639        
640        self.plot_y + self.plot_h - 1 - (ratio * (self.plot_h - 1) as f32) as i32
641    }
642
643    /// Convertit un index de données en coordonnée X écran (pixels).
644    ///
645    /// Les N points sont distribués uniformément sur la largeur de la zone de tracé.
646    #[inline]
647    fn scale_x(&self, index: usize) -> i32 {
648        if N <= 1 {
649            return self.plot_x;
650        }
651        self.plot_x + (index as i32 * (self.plot_w - 1)) / (N as i32 - 1)
652    }
653
654    /// Convertit une valeur d'axe X en coordonnée écran (pour labels).
655    ///
656    /// Similaire à `scale_y`, mais pour l'axe X.
657    #[inline]
658    fn scale_x_value(&self, value: f32) -> i32 {
659        let x_min = self.config.x_axis.start;
660        let x_max = self.config.x_axis.end;
661        
662        if x_max <= x_min {
663            return self.plot_x;
664        }
665        
666        let clamped = value.max(x_min).min(x_max);
667        let ratio = (clamped - x_min) / (x_max - x_min);
668        self.plot_x + (ratio * (self.plot_w - 1) as f32) as i32
669    }
670
671    /// Affiche le graphique complet avec grille, axes, graduations et courbe.
672    ///
673    /// Cette méthode effectue :
674    /// 1. Remplissage du fond
675    /// 2. Grille horizontale (Y) et labels des graduations
676    /// 3. Grille verticale (X) et labels des graduations
677    /// 4. Labels des axes (titres)
678    /// 5. Bordures externes
679    /// 6. Tracé de la courbe (données)
680    ///
681    /// # Arguments
682    ///
683    /// * `gfx` - Contexte graphique initialisé
684    ///
685    /// # Exemple
686    ///
687    /// ```no_run
688    /// # use embassy_st7789v::{Color, St7789v, NoPin};
689    /// # use embedded_hal::digital::OutputPin;
690    /// # use embedded_hal_async::spi::SpiDevice;
691    /// use embassy_st7789v_plot::{Graphics, AxisConfig, PlotConfig, LineChart};
692    ///
693    /// # async fn example<SPI, DC>(display: &mut St7789v<SPI, DC, NoPin>) where SPI: SpiDevice, DC: OutputPin {
694    /// let config = PlotConfig {
695    ///     x: 10, y: 10, width: 220, height: 200,
696    ///     margin_left: 40, margin_right: 10, margin_top: 10, margin_bottom: 30,
697    ///     x_axis: AxisConfig::new(0.0, 10.0, 2.0, b"Time"),
698    ///     y_axis: AxisConfig::new(0.0, 100.0, 20.0, b"Value"),
699    ///     bg_color: Color::BLACK,
700    ///     line_color: Color::GREEN,
701    ///     axis_color: Color::WHITE,
702    ///     grid_color: Color::from_rgb(64, 64, 64),
703    ///     text_color: Color::WHITE,
704    ///     label_color: Color::CYAN,
705    /// };
706    ///
707    /// let mut chart: LineChart<100> = LineChart::new(config);
708    /// for i in 0..10 {
709    ///     chart.push((i as f32) * 10.0);
710    /// }
711    ///
712    /// let mut gfx = Graphics::new_no_rst(display);
713    /// chart.render(&mut gfx).await;
714    /// # }
715    /// ```
716    pub async fn render<SPI, DC, RST>(&self, gfx: &mut Graphics<'_, SPI, DC, RST>)
717    where
718        SPI: SpiDevice,
719        DC: OutputPin,
720        RST: OutputPin,
721    {
722        // 1. Fond de la zone du graphique
723        let _ = gfx.display.fill_rect(
724            self.plot_x as u16,
725            self.plot_y as u16,
726            (self.plot_x + self.plot_w - 1) as u16,
727            (self.plot_y + self.plot_h - 1) as u16,
728            self.config.bg_color,
729        ).await;
730
731        let right_edge = self.plot_x + self.plot_w - 1;
732        let bottom_edge = self.plot_y + self.plot_h - 1;
733
734        // 2. Grille horizontale (Y) + Labels Y
735        let y_axis = &self.config.y_axis;
736        let y_range = y_axis.end - y_axis.start;
737        let tick_count_y = y_axis.tick_count();
738
739        for i in 0..tick_count_y {
740            let value = y_axis.start + (i as f32 * y_axis.step);
741            let ratio = (value - y_axis.start) / y_range;
742            let y_grid = bottom_edge - (ratio * (self.plot_h - 1) as f32) as i32;
743
744            // Ligne de grille (sauf sur les bordures)
745            if i > 0 && i < tick_count_y - 1 {
746                let _ = gfx.display.draw_hline(
747                    self.plot_x as u16, 
748                    y_grid as u16, 
749                    self.plot_w as u16, 
750                    self.config.grid_color
751                ).await;
752            }
753
754            // Label Y dans la marge gauche (avec padding pour éviter la coupure)
755            let label_color = if value.abs() < 0.01 * y_axis.step {
756                Color::GREEN  // Met en évidence la valeur zéro
757            } else {
758                self.config.text_color
759            };
760
761            let label_y = (y_grid - 4).max(self.config.y + 8).min(self.config.y + self.config.height - 8);
762
763            let _ = gfx.display.draw_f32(
764                (self.config.x + 2) as u16,
765                label_y as u16,
766                value,
767                1,
768                label_color,
769                self.config.bg_color,
770            ).await;
771        }
772
773        // 3. Grille verticale (X) + Labels X
774        let x_axis = &self.config.x_axis;
775        
776        let tick_count_x = x_axis.tick_count();
777
778        for i in 0..tick_count_x {
779            let value = x_axis.start + (i as f32 * x_axis.step);
780            let x_grid = self.scale_x_value(value);
781
782            // Ligne de grille (sauf sur les bordures)
783            if i > 0 && i < tick_count_x - 1 {
784                let _ = gfx.display.draw_vline(
785                    x_grid as u16,
786                    self.plot_y as u16,
787                    self.plot_h as u16,
788                    self.config.grid_color
789                ).await;
790            }
791
792            // Label X dans la marge inférieure (avec padding pour éviter la coupure)
793            let label_x = (x_grid - 8).max(self.config.x + 2).min(self.config.x + self.config.width - 20);
794
795            let _ = gfx.display.draw_f32(
796                label_x as u16,
797                (bottom_edge + 4) as u16,
798                value,
799                1,
800                self.config.text_color,
801                self.config.bg_color,
802            ).await;
803        }
804
805        // 4. Labels des axes (titres) — draw_str prend &[u8] et 5 arguments
806        // Label Y (coin haut-gauche de la marge, 2 lignes plus haut)
807        let _ = gfx.display.draw_str(
808            (self.config.x + 2) as u16,
809            (self.config.y - 16).max(0) as u16,
810            y_axis.label,
811            self.config.label_color,
812            self.config.bg_color,
813        ).await;
814
815        // Label X (dans la marge bas, 2 lignes plus bas)
816        let label_x_x = self.plot_x + (self.plot_w / 2) - ((y_axis.label.len() as i32 * 6) / 2);
817        let _ = gfx.display.draw_str(
818            label_x_x.max(self.config.x + 2) as u16,
819            (self.config.y + self.config.height + 4) as u16,
820            x_axis.label,
821            self.config.label_color,
822            self.config.bg_color,
823        ).await;
824
825        // 5. Bordures externes
826        let _ = gfx.display.draw_hline(self.plot_x as u16, self.plot_y as u16, self.plot_w as u16, self.config.axis_color).await;
827        let _ = gfx.display.draw_hline(self.plot_x as u16, bottom_edge as u16, self.plot_w as u16, self.config.axis_color).await;
828        let _ = gfx.display.draw_vline(self.plot_x as u16, self.plot_y as u16, self.plot_h as u16, self.config.axis_color).await;
829        let _ = gfx.display.draw_vline(right_edge as u16, self.plot_y as u16, self.plot_h as u16, self.config.axis_color).await;
830
831        // 6. Tracé des données
832        if self.count < 2 {
833            if self.count == 1 {
834                let px = self.scale_x(0);
835                let py = self.scale_y(self.get_sample(0));
836                gfx.pixel(px, py, self.config.line_color).await;
837            }
838            return;
839        }
840
841        let mut prev_x = self.scale_x(0);
842        let mut prev_y = self.scale_y(self.get_sample(0));
843
844        for i in 1..self.count {
845            let next_x = self.scale_x(i);
846            let next_y = self.scale_y(self.get_sample(i));
847
848            line(gfx, prev_x, prev_y, next_x, next_y, self.config.line_color).await;
849
850            prev_x = next_x;
851            prev_y = next_y;
852        }
853    }
854}