Skip to main content

dear_implot/plots/
mod.rs

1//! Modular plot types for ImPlot
2//!
3//! This module provides a modular approach to different plot types,
4//! each with their own builder pattern and configuration options.
5
6pub mod bar;
7pub mod bar_groups;
8pub mod digital;
9pub mod dummy;
10pub mod error_bars;
11pub mod heatmap;
12pub mod histogram;
13pub mod image;
14pub mod inf_lines;
15pub mod line;
16pub mod pie;
17pub mod polygon;
18pub mod scatter;
19pub mod shaded;
20pub mod stairs;
21pub mod stems;
22pub mod text;
23
24use crate::sys;
25use dear_imgui_rs::{with_scratch_txt, with_scratch_txt_slice, with_scratch_txt_slice_with_opt};
26use std::cell::RefCell;
27use std::os::raw::c_char;
28
29// Re-export all plot types for convenience
30pub use bar::*;
31pub use bar_groups::*;
32pub use digital::*;
33pub use dummy::*;
34pub use error_bars::*;
35pub use heatmap::*;
36pub use histogram::*;
37pub use image::*;
38pub use inf_lines::*;
39pub use line::*;
40pub use pie::*;
41pub use polygon::*;
42pub use scatter::*;
43pub use shaded::*;
44pub use stairs::*;
45pub use stems::*;
46pub use text::*;
47
48thread_local! {
49    static NEXT_PLOT_SPEC: RefCell<Option<sys::ImPlotSpec_c>> = RefCell::new(None);
50}
51
52/// Sample-index offset used by ImPlot item data access.
53///
54/// ImPlot intentionally allows negative and out-of-range offsets for circular
55/// buffers, so this is a signed sample offset rather than a Rust slice index.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
57pub struct PlotDataOffset(i32);
58
59impl PlotDataOffset {
60    /// No data offset.
61    pub const ZERO: Self = Self(0);
62
63    /// Create a sample-index offset.
64    #[inline]
65    pub const fn samples(offset: i32) -> Self {
66        Self(offset)
67    }
68
69    #[inline]
70    pub(crate) const fn raw(self) -> i32 {
71        self.0
72    }
73}
74
75/// Byte stride used by ImPlot item data access.
76///
77/// Use [`PlotDataStride::AUTO`] for contiguous data of the plotted value type,
78/// or [`PlotDataStride::bytes`] for interleaved/custom layouts.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct PlotDataStride(i32);
81
82impl PlotDataStride {
83    /// Let ImPlot use `sizeof(T)` for the plotted value type.
84    pub const AUTO: Self = Self(crate::IMPLOT_AUTO);
85
86    /// Create a byte stride.
87    ///
88    /// Panics if `bytes` is zero or exceeds ImPlot's `int` range.
89    #[inline]
90    pub fn bytes(bytes: usize) -> Self {
91        assert!(
92            bytes > 0,
93            "PlotDataStride::bytes() requires a non-zero stride"
94        );
95        let bytes = i32::try_from(bytes)
96            .expect("PlotDataStride::bytes() stride exceeded ImPlot's int range");
97        Self(bytes)
98    }
99
100    /// Create the contiguous byte stride for `T`.
101    #[inline]
102    pub fn for_type<T>() -> Self {
103        Self::bytes(std::mem::size_of::<T>())
104    }
105
106    #[inline]
107    pub(crate) const fn raw(self) -> i32 {
108        self.0
109    }
110}
111
112impl Default for PlotDataStride {
113    fn default() -> Self {
114        Self::AUTO
115    }
116}
117
118/// Data layout used by ImPlot item builders.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
120pub struct PlotDataLayout {
121    offset: PlotDataOffset,
122    stride: PlotDataStride,
123}
124
125impl PlotDataLayout {
126    /// Contiguous data starting at sample offset zero.
127    pub const DEFAULT: Self = Self {
128        offset: PlotDataOffset::ZERO,
129        stride: PlotDataStride::AUTO,
130    };
131
132    /// Create a data layout from a sample offset and byte stride.
133    #[inline]
134    pub const fn new(offset: PlotDataOffset, stride: PlotDataStride) -> Self {
135        Self { offset, stride }
136    }
137
138    /// Create a data layout with a different sample offset.
139    #[inline]
140    pub const fn with_offset(mut self, offset: PlotDataOffset) -> Self {
141        self.offset = offset;
142        self
143    }
144
145    /// Create a data layout with a different byte stride.
146    #[inline]
147    pub const fn with_stride(mut self, stride: PlotDataStride) -> Self {
148        self.stride = stride;
149        self
150    }
151
152    #[inline]
153    pub(crate) const fn raw_offset(self) -> i32 {
154        self.offset.raw()
155    }
156
157    #[inline]
158    pub(crate) const fn raw_stride(self) -> i32 {
159        self.stride.raw()
160    }
161}
162
163fn color4(rgba: [f32; 4]) -> sys::ImVec4_c {
164    sys::ImVec4_c {
165        x: rgba[0],
166        y: rgba[1],
167        z: rgba[2],
168        w: rgba[3],
169    }
170}
171
172/// Common style overrides for plot items backed by `ImPlotSpec`.
173///
174/// This provides a stable high-level Rust entry point for ImPlot v0.18 item
175/// styling without exposing raw FFI structs at every call site.
176#[derive(Debug, Clone, Copy, Default, PartialEq)]
177pub struct PlotItemStyle {
178    pub(crate) line_color: Option<sys::ImVec4_c>,
179    pub(crate) line_weight: Option<f32>,
180    pub(crate) fill_color: Option<sys::ImVec4_c>,
181    pub(crate) fill_alpha: Option<f32>,
182    pub(crate) marker: Option<sys::ImPlotMarker>,
183    pub(crate) marker_size: Option<f32>,
184    pub(crate) marker_line_color: Option<sys::ImVec4_c>,
185    pub(crate) marker_fill_color: Option<sys::ImVec4_c>,
186    pub(crate) size: Option<f32>,
187}
188
189impl PlotItemStyle {
190    /// Create an empty style override that keeps ImPlot defaults.
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Override the plot item's line color. Use the alpha channel for line transparency.
196    pub fn with_line_color(mut self, color: [f32; 4]) -> Self {
197        self.line_color = Some(color4(color));
198        self
199    }
200
201    /// Override the plot item's line width in pixels.
202    pub fn with_line_weight(mut self, weight: f32) -> Self {
203        self.line_weight = Some(weight);
204        self
205    }
206
207    /// Override the plot item's fill color.
208    pub fn with_fill_color(mut self, color: [f32; 4]) -> Self {
209        self.fill_color = Some(color4(color));
210        self
211    }
212
213    /// Override the fill alpha multiplier used by filled regions and marker faces.
214    pub fn with_fill_alpha(mut self, alpha: f32) -> Self {
215        self.fill_alpha = Some(alpha);
216        self
217    }
218
219    /// Override the marker type.
220    pub fn with_marker(mut self, marker: crate::Marker) -> Self {
221        self.marker = Some(marker as sys::ImPlotMarker);
222        self
223    }
224
225    /// Override the marker size in pixels.
226    pub fn with_marker_size(mut self, size: f32) -> Self {
227        self.marker_size = Some(size);
228        self
229    }
230
231    /// Override the marker outline color.
232    pub fn with_marker_line_color(mut self, color: [f32; 4]) -> Self {
233        self.marker_line_color = Some(color4(color));
234        self
235    }
236
237    /// Override the marker fill color.
238    pub fn with_marker_fill_color(mut self, color: [f32; 4]) -> Self {
239        self.marker_fill_color = Some(color4(color));
240        self
241    }
242
243    /// Override the generic size field used by some item types such as error bars.
244    pub fn with_size(mut self, size: f32) -> Self {
245        self.size = Some(size);
246        self
247    }
248
249    pub(crate) fn apply_to_spec(self, spec: &mut sys::ImPlotSpec_c) {
250        if let Some(line_color) = self.line_color {
251            spec.LineColor = line_color;
252        }
253        if let Some(line_weight) = self.line_weight {
254            spec.LineWeight = line_weight;
255        }
256        if let Some(fill_color) = self.fill_color {
257            spec.FillColor = fill_color;
258        }
259        if let Some(fill_alpha) = self.fill_alpha {
260            spec.FillAlpha = fill_alpha;
261        }
262        if let Some(marker) = self.marker {
263            spec.Marker = marker;
264        }
265        if let Some(marker_size) = self.marker_size {
266            spec.MarkerSize = marker_size;
267        }
268        if let Some(marker_line_color) = self.marker_line_color {
269            spec.MarkerLineColor = marker_line_color;
270        }
271        if let Some(marker_fill_color) = self.marker_fill_color {
272            spec.MarkerFillColor = marker_fill_color;
273        }
274        if let Some(size) = self.size {
275            spec.Size = size;
276        }
277    }
278}
279
280/// Shared ImPlot item-style builder methods for plot builders backed by `ImPlotSpec`.
281///
282/// Importing `dear_implot::*` brings this trait into scope, so every supported
283/// plot builder exposes the same styling methods.
284pub trait PlotItemStyled: Sized {
285    fn style_mut(&mut self) -> &mut PlotItemStyle;
286
287    /// Replace the entire item style override for this plot.
288    fn with_style(mut self, style: PlotItemStyle) -> Self {
289        *self.style_mut() = style;
290        self
291    }
292
293    /// Set the line color. Use the alpha channel to control line transparency.
294    fn with_line_color(mut self, color: [f32; 4]) -> Self {
295        self.style_mut().line_color = Some(color4(color));
296        self
297    }
298
299    /// Set the line width in pixels.
300    fn with_line_weight(mut self, weight: f32) -> Self {
301        self.style_mut().line_weight = Some(weight);
302        self
303    }
304
305    /// Set the fill color.
306    fn with_fill_color(mut self, color: [f32; 4]) -> Self {
307        self.style_mut().fill_color = Some(color4(color));
308        self
309    }
310
311    /// Set the fill alpha multiplier used for fills and marker faces.
312    fn with_fill_alpha(mut self, alpha: f32) -> Self {
313        self.style_mut().fill_alpha = Some(alpha);
314        self
315    }
316
317    /// Set the marker type.
318    fn with_marker(mut self, marker: crate::Marker) -> Self {
319        self.style_mut().marker = Some(marker as sys::ImPlotMarker);
320        self
321    }
322
323    /// Set the marker size in pixels.
324    fn with_marker_size(mut self, size: f32) -> Self {
325        self.style_mut().marker_size = Some(size);
326        self
327    }
328
329    /// Set the marker outline color.
330    fn with_marker_line_color(mut self, color: [f32; 4]) -> Self {
331        self.style_mut().marker_line_color = Some(color4(color));
332        self
333    }
334
335    /// Set the marker fill color.
336    fn with_marker_fill_color(mut self, color: [f32; 4]) -> Self {
337        self.style_mut().marker_fill_color = Some(color4(color));
338        self
339    }
340
341    /// Set the generic size field used by some plot types such as error bars and digital plots.
342    fn with_size(mut self, size: f32) -> Self {
343        self.style_mut().size = Some(size);
344        self
345    }
346}
347
348/// Common trait for all plot types
349pub trait Plot {
350    /// Plot this element
351    fn plot(&self);
352
353    /// Get the label for this plot
354    fn label(&self) -> &str;
355}
356
357/// Common trait for plot data validation
358pub trait PlotData {
359    /// Get the label for this plot
360    fn label(&self) -> &str;
361
362    /// Get the length of the data
363    fn data_len(&self) -> usize;
364
365    /// Check if the data is empty
366    fn is_empty(&self) -> bool {
367        self.data_len() == 0
368    }
369
370    /// Validate the data for plotting
371    fn validate(&self) -> Result<(), PlotError> {
372        if self.is_empty() {
373            Err(PlotError::EmptyData)
374        } else {
375            Ok(())
376        }
377    }
378}
379
380/// Errors that can occur during plotting
381#[derive(Debug, Clone, PartialEq)]
382pub enum PlotError {
383    /// Data arrays have mismatched lengths
384    DataLengthMismatch { x_len: usize, y_len: usize },
385    /// Data is empty
386    EmptyData,
387    /// Invalid parameter value or data
388    InvalidData(String),
389    /// String conversion error
390    StringConversion(String),
391    /// Plot creation failed
392    PlotCreationFailed(String),
393}
394
395impl std::fmt::Display for PlotError {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        match self {
398            PlotError::DataLengthMismatch { x_len, y_len } => {
399                write!(
400                    f,
401                    "Data length mismatch: x has {} elements, y has {} elements",
402                    x_len, y_len
403                )
404            }
405            PlotError::EmptyData => write!(f, "Data is empty"),
406            PlotError::InvalidData(msg) => write!(f, "Invalid data: {}", msg),
407            PlotError::StringConversion(msg) => write!(f, "String conversion error: {}", msg),
408            PlotError::PlotCreationFailed(msg) => write!(f, "Plot creation failed: {}", msg),
409        }
410    }
411}
412
413impl std::error::Error for PlotError {}
414
415// Note: PlotData trait is implemented by individual plot types
416// rather than raw data types, since each plot needs its own label
417
418/// Helper function to validate data length consistency for slices
419pub fn validate_data_lengths<T, U>(data1: &[T], data2: &[U]) -> Result<(), PlotError> {
420    if data1.is_empty() || data2.is_empty() {
421        return Err(PlotError::EmptyData);
422    }
423
424    if data1.len() != data2.len() {
425        return Err(PlotError::DataLengthMismatch {
426            x_len: data1.len(),
427            y_len: data2.len(),
428        });
429    }
430
431    Ok(())
432}
433
434pub(crate) fn with_plot_str<R>(s: &str, f: impl FnOnce(*const c_char) -> R) -> Option<R> {
435    if s.contains('\0') {
436        None
437    } else {
438        Some(with_scratch_txt(s, f))
439    }
440}
441
442pub(crate) fn with_plot_str_or_empty<R>(s: &str, f: impl FnOnce(*const c_char) -> R) -> R {
443    let s = if s.contains('\0') { "" } else { s };
444    with_scratch_txt(s, f)
445}
446
447pub(crate) fn with_plot_str_slice<R>(txts: &[&str], f: impl FnOnce(&[*const c_char]) -> R) -> R {
448    let cleaned: Vec<&str> = txts
449        .iter()
450        .map(|&s| if s.contains('\0') { "" } else { s })
451        .collect();
452    with_scratch_txt_slice(&cleaned, f)
453}
454
455pub(crate) fn with_plot_str_slice_with_opt<R>(
456    txts: &[&str],
457    txt_opt: Option<&str>,
458    f: impl FnOnce(&[*const c_char], *const c_char) -> R,
459) -> R {
460    let cleaned: Vec<&str> = txts
461        .iter()
462        .map(|&s| if s.contains('\0') { "" } else { s })
463        .collect();
464    let txt_opt = txt_opt.filter(|s| !s.contains('\0'));
465    with_scratch_txt_slice_with_opt(&cleaned, txt_opt, f)
466}
467
468pub(crate) fn default_plot_spec() -> sys::ImPlotSpec_c {
469    let auto_col = sys::ImVec4_c {
470        x: 0.0,
471        y: 0.0,
472        z: 0.0,
473        w: -1.0,
474    };
475
476    sys::ImPlotSpec_c {
477        LineColor: auto_col,
478        LineColors: std::ptr::null_mut(),
479        LineWeight: 1.0,
480        FillColor: auto_col,
481        FillColors: std::ptr::null_mut(),
482        FillAlpha: 1.0,
483        Marker: sys::ImPlotMarker_None as _,
484        MarkerSize: 4.0,
485        MarkerSizes: std::ptr::null_mut(),
486        MarkerLineColor: auto_col,
487        MarkerLineColors: std::ptr::null_mut(),
488        MarkerFillColor: auto_col,
489        MarkerFillColors: std::ptr::null_mut(),
490        Size: 4.0,
491        Offset: 0,
492        Stride: crate::IMPLOT_AUTO,
493        Flags: sys::ImPlotItemFlags_None as _,
494    }
495}
496
497pub(crate) fn take_next_plot_spec() -> Option<sys::ImPlotSpec_c> {
498    NEXT_PLOT_SPEC.with(|cell| cell.borrow_mut().take())
499}
500
501pub(crate) fn set_next_plot_spec(spec: Option<sys::ImPlotSpec_c>) {
502    NEXT_PLOT_SPEC.with(|cell| {
503        *cell.borrow_mut() = spec;
504    })
505}
506
507pub(crate) fn plot_spec_from(flags: u32, layout: PlotDataLayout) -> sys::ImPlotSpec_c {
508    let mut spec = take_next_plot_spec().unwrap_or_else(default_plot_spec);
509    spec.Flags = flags as sys::ImPlotItemFlags;
510    spec.Offset = layout.raw_offset();
511    spec.Stride = layout.raw_stride();
512    spec
513}
514
515pub(crate) fn plot_spec_with_style(
516    style: PlotItemStyle,
517    flags: u32,
518    layout: PlotDataLayout,
519) -> sys::ImPlotSpec_c {
520    let mut spec = plot_spec_from(flags, layout);
521    style.apply_to_spec(&mut spec);
522    spec
523}
524
525#[cfg(test)]
526mod layout_tests {
527    use super::{PlotDataLayout, PlotDataOffset, PlotDataStride, plot_spec_from};
528
529    #[test]
530    fn data_layout_allows_signed_sample_offsets() {
531        let layout = PlotDataLayout::DEFAULT.with_offset(PlotDataOffset::samples(-8));
532        let spec = plot_spec_from(0, layout);
533        assert_eq!(spec.Offset, -8);
534        assert_eq!(spec.Stride, crate::IMPLOT_AUTO);
535    }
536
537    #[test]
538    fn data_stride_bytes_are_positive() {
539        let stride = PlotDataStride::bytes(16);
540        let layout = PlotDataLayout::DEFAULT.with_stride(stride);
541        let spec = plot_spec_from(0, layout);
542        assert_eq!(spec.Stride, 16);
543    }
544
545    #[test]
546    #[should_panic(expected = "requires a non-zero stride")]
547    fn zero_data_stride_panics_before_ffi() {
548        let _ = PlotDataStride::bytes(0);
549    }
550}
551
552/// Universal plot builder that can create any plot type
553pub struct PlotBuilder<'a> {
554    plot_type: PlotType<'a>,
555}
556
557/// Enum representing different plot types
558pub enum PlotType<'a> {
559    Line {
560        label: &'a str,
561        x_data: &'a [f64],
562        y_data: &'a [f64],
563    },
564    Scatter {
565        label: &'a str,
566        x_data: &'a [f64],
567        y_data: &'a [f64],
568    },
569    Bar {
570        label: &'a str,
571        values: &'a [f64],
572        width: f64,
573    },
574    Histogram {
575        label: &'a str,
576        values: &'a [f64],
577        bins: crate::HistogramBins,
578    },
579    Heatmap {
580        label: &'a str,
581        values: &'a [f64],
582        rows: usize,
583        cols: usize,
584    },
585    PieChart {
586        labels: Vec<&'a str>,
587        values: &'a [f64],
588        center: (f64, f64),
589        radius: f64,
590    },
591    Polygon {
592        label: &'a str,
593        x_data: &'a [f64],
594        y_data: &'a [f64],
595    },
596}
597
598impl<'a> PlotBuilder<'a> {
599    /// Create a line plot
600    pub fn line(label: &'a str, x_data: &'a [f64], y_data: &'a [f64]) -> Self {
601        Self {
602            plot_type: PlotType::Line {
603                label,
604                x_data,
605                y_data,
606            },
607        }
608    }
609
610    /// Create a scatter plot
611    pub fn scatter(label: &'a str, x_data: &'a [f64], y_data: &'a [f64]) -> Self {
612        Self {
613            plot_type: PlotType::Scatter {
614                label,
615                x_data,
616                y_data,
617            },
618        }
619    }
620
621    /// Create a bar plot
622    pub fn bar(label: &'a str, values: &'a [f64]) -> Self {
623        Self {
624            plot_type: PlotType::Bar {
625                label,
626                values,
627                width: 0.67,
628            },
629        }
630    }
631
632    /// Create a histogram
633    pub fn histogram(label: &'a str, values: &'a [f64]) -> Self {
634        Self {
635            plot_type: PlotType::Histogram {
636                label,
637                values,
638                bins: crate::HistogramBins::DEFAULT,
639            },
640        }
641    }
642
643    /// Create a heatmap
644    pub fn heatmap(label: &'a str, values: &'a [f64], rows: usize, cols: usize) -> Self {
645        Self {
646            plot_type: PlotType::Heatmap {
647                label,
648                values,
649                rows,
650                cols,
651            },
652        }
653    }
654
655    /// Create a pie chart
656    pub fn pie_chart(
657        labels: Vec<&'a str>,
658        values: &'a [f64],
659        center: (f64, f64),
660        radius: f64,
661    ) -> Self {
662        Self {
663            plot_type: PlotType::PieChart {
664                labels,
665                values,
666                center,
667                radius,
668            },
669        }
670    }
671
672    /// Create a polygon plot.
673    pub fn polygon(label: &'a str, x_data: &'a [f64], y_data: &'a [f64]) -> Self {
674        Self {
675            plot_type: PlotType::Polygon {
676                label,
677                x_data,
678                y_data,
679            },
680        }
681    }
682
683    /// Build and plot the chart
684    pub fn build(self) -> Result<(), PlotError> {
685        match self.plot_type {
686            PlotType::Line {
687                label,
688                x_data,
689                y_data,
690            } => {
691                let plot = line::LinePlot::new(label, x_data, y_data);
692                plot.validate()?;
693                plot.plot();
694            }
695            PlotType::Scatter {
696                label,
697                x_data,
698                y_data,
699            } => {
700                let plot = scatter::ScatterPlot::new(label, x_data, y_data);
701                plot.validate()?;
702                plot.plot();
703            }
704            PlotType::Bar {
705                label,
706                values,
707                width,
708            } => {
709                let plot = bar::BarPlot::new(label, values).with_bar_size(width);
710                plot.validate()?;
711                plot.plot();
712            }
713            PlotType::Histogram {
714                label,
715                values,
716                bins,
717            } => {
718                let plot = histogram::HistogramPlot::new(label, values).with_bins(bins);
719                plot.validate()?;
720                plot.plot();
721            }
722            PlotType::Heatmap {
723                label,
724                values,
725                rows,
726                cols,
727            } => {
728                let plot = heatmap::HeatmapPlot::new(label, values, rows, cols);
729                plot.validate()?;
730                plot.plot();
731            }
732            PlotType::PieChart {
733                labels,
734                values,
735                center,
736                radius,
737            } => {
738                let plot = pie::PieChartPlot::new(labels, values, center.0, center.1, radius);
739                plot.validate()?;
740                plot.plot();
741            }
742            PlotType::Polygon {
743                label,
744                x_data,
745                y_data,
746            } => {
747                let plot = polygon::PolygonPlot::new(label, x_data, y_data);
748                plot.validate()?;
749                plot.plot();
750            }
751        }
752        Ok(())
753    }
754}