Skip to main content

plotlars_core/plots/
table.rs

1use bon::bon;
2
3use polars::frame::DataFrame;
4
5use crate::{
6    components::{Cell, Header, Text},
7    ir::layout::LayoutIR,
8    ir::trace::{TableIR, TraceIR},
9};
10
11/// A structure representing a table plot.
12///
13/// The `Table` struct allows for the creation and customization of tables with support
14/// for custom headers, cell formatting, column widths, and various styling options.
15///
16/// # Backend Support
17///
18/// | Backend | Supported |
19/// |---------|-----------|
20/// | Plotly  | Yes       |
21/// | Plotters| --        |
22///
23/// # Arguments
24///
25/// * `data` - A reference to the `DataFrame` containing the data to be displayed.
26/// * `columns` - A vector of column names to be displayed in the table.
27/// * `header` - An optional `Header` component for custom header values and formatting.
28/// * `cell` - An optional `Cell` component for cell formatting.
29/// * `column_width` - An optional column width ratio. Columns fill the available width in proportion.
30/// * `plot_title` - An optional `Text` struct specifying the title of the plot.
31///
32/// # Example
33///
34/// ```rust
35/// use polars::prelude::*;
36/// use plotlars::{Table, Header, Cell, Plot, Text, Rgb};
37///
38/// let dataset = LazyCsvReader::new(PlRefPath::new("data/employee_data.csv"))
39///     .finish()
40///     .unwrap()
41///     .collect()
42///     .unwrap();
43///
44/// let header = Header::new()
45///     .values(vec![
46///          "Employee Name",
47///          "Department",
48///          "Annual Salary ($)",
49///          "Years of Service",
50///     ])
51///     .align("center")
52///     .font("Arial Black")
53///     .fill(Rgb(70, 130, 180));
54///
55/// let cell = Cell::new()
56///     .align("center")
57///     .height(25.0)
58///     .font("Arial")
59///     .fill(Rgb(240, 248, 255));
60///
61/// Table::builder()
62///     .data(&dataset)
63///     .columns(vec![
64///         "name",
65///         "department",
66///         "salary",
67///         "years",
68///     ])
69///     .header(&header)
70///     .cell(&cell)
71///     .plot_title(
72///         Text::from("Table")
73///             .font("Arial")
74///             .size(20)
75///             .color(Rgb(25, 25, 112))
76///     )
77///     .build()
78///     .plot();
79/// ```
80///
81/// ![Example](https://imgur.com/QDKTeFX.png)
82#[derive(Clone)]
83#[allow(dead_code)]
84pub struct Table {
85    traces: Vec<TraceIR>,
86    layout: LayoutIR,
87}
88
89#[bon]
90impl Table {
91    #[builder(on(String, into), on(Text, into))]
92    pub fn new(
93        data: &DataFrame,
94        columns: Vec<&str>,
95        header: Option<&Header>,
96        cell: Option<&Cell>,
97        column_width: Option<f64>,
98        plot_title: Option<Text>,
99    ) -> Self {
100        // Determine column names
101        let column_names: Vec<String> = if let Some(h) = header {
102            if let Some(custom_values) = &h.values {
103                custom_values.clone()
104            } else {
105                columns.iter().map(|&c| c.to_string()).collect()
106            }
107        } else {
108            columns.iter().map(|&c| c.to_string()).collect()
109        };
110
111        // Extract cell values from DataFrame
112        let mut column_data: Vec<Vec<String>> = Vec::new();
113        for column_name in &columns {
114            let col_data = crate::data::get_string_column(data, column_name);
115            let col_strings: Vec<String> = col_data
116                .iter()
117                .map(|opt| opt.clone().unwrap_or_default())
118                .collect();
119            column_data.push(col_strings);
120        }
121
122        // Build IR
123        let ir_trace = TraceIR::Table(TableIR {
124            header: header.cloned(),
125            cell: cell.cloned(),
126            column_names,
127            column_data,
128            column_width,
129        });
130        let traces = vec![ir_trace];
131        let layout = LayoutIR {
132            title: plot_title.clone(),
133            x_title: None,
134            y_title: None,
135            y2_title: None,
136            z_title: None,
137            legend_title: None,
138            legend: None,
139            dimensions: None,
140            bar_mode: None,
141            box_mode: None,
142            box_gap: None,
143            margin_bottom: None,
144            axes_2d: None,
145            scene_3d: None,
146            polar: None,
147            mapbox: None,
148            grid: None,
149            annotations: vec![],
150        };
151        Self { traces, layout }
152    }
153}
154
155#[bon]
156impl Table {
157    #[builder(
158        start_fn = try_builder,
159        finish_fn = try_build,
160        builder_type = TableTryBuilder,
161        on(String, into),
162        on(Text, into),
163    )]
164    pub fn try_new(
165        data: &DataFrame,
166        columns: Vec<&str>,
167        header: Option<&Header>,
168        cell: Option<&Cell>,
169        column_width: Option<f64>,
170        plot_title: Option<Text>,
171    ) -> Result<Self, crate::io::PlotlarsError> {
172        std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
173            Self::__orig_new(data, columns, header, cell, column_width, plot_title)
174        }))
175        .map_err(|panic| {
176            let msg = panic
177                .downcast_ref::<String>()
178                .cloned()
179                .or_else(|| panic.downcast_ref::<&str>().map(|s| s.to_string()))
180                .unwrap_or_else(|| "unknown error".to_string());
181            crate::io::PlotlarsError::PlotBuild { message: msg }
182        })
183    }
184}
185
186impl crate::Plot for Table {
187    fn ir_traces(&self) -> &[TraceIR] {
188        &self.traces
189    }
190
191    fn ir_layout(&self) -> &LayoutIR {
192        &self.layout
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use crate::Plot;
200    use polars::prelude::*;
201
202    #[test]
203    fn test_basic_one_trace() {
204        let df = df![
205            "name" => ["Alice", "Bob"],
206            "age" => [30, 25]
207        ]
208        .unwrap();
209        let plot = Table::builder()
210            .data(&df)
211            .columns(vec!["name", "age"])
212            .build();
213        assert_eq!(plot.ir_traces().len(), 1);
214    }
215
216    #[test]
217    fn test_trace_variant() {
218        let df = df![
219            "col1" => ["a", "b"],
220            "col2" => ["c", "d"]
221        ]
222        .unwrap();
223        let plot = Table::builder()
224            .data(&df)
225            .columns(vec!["col1", "col2"])
226            .build();
227        assert!(matches!(plot.ir_traces()[0], TraceIR::Table(_)));
228    }
229
230    #[test]
231    fn test_layout_no_axes() {
232        let df = df![
233            "col1" => ["a"]
234        ]
235        .unwrap();
236        let plot = Table::builder().data(&df).columns(vec!["col1"]).build();
237        let layout = plot.ir_layout();
238        assert!(layout.axes_2d.is_none());
239        assert!(layout.scene_3d.is_none());
240        assert!(layout.polar.is_none());
241    }
242}