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}