Skip to main content

report_builder/
lib.rs

1//! # Report Builder
2//!
3//! This crate provides tools for generating HTML reports with interactive elements such as tables,
4//! plots, and other visualizations. It's designed to be used as a library within other Rust projects.
5//!
6//! ## Features
7//!
8//! - Create multi-section reports
9//! - Add interactive tables with sorting, searching, and CSV export
10//! - Include responsive Plotly charts
11//! - Customizable styling and layout
12//!
13//! ## Usage
14//!
15//! Add `report-builder` to your `Cargo.toml` dependencies:
16//!
17//! ```rust,ignore
18//! [dependencies]
19//! report-builder = "0.1.0"  # Replace with the latest version
20//! ```
21//!
22//! Then, use the provided structs and methods to construct your report:
23//!
24//! ```rust,ignore
25//! use report_builder::{Report, ReportSection};
26//! use maud::html;
27//! use plotly::Plot;
28//!
29//! fn main() {
30//!     let mut report = Report::new("MySoftware", "1.0", Some("logo.png"), "Analysis Report");
31//!     
32//!     let mut section = ReportSection::new("Results");
33//!     section.add_content(html! { p { "This is a paragraph in the results section." } });
34//!     
35//!     // Add a plot (assuming you have a Plot object)
36//!     let plot = Plot::new(); // Create and customize your plot
37//!     section.add_plot(plot);
38//!     
39//!     report.add_section(section);
40//!     report.save_to_file("report.html").unwrap();
41//! }
42//! ```
43
44pub mod plots;
45
46use chrono::Local;
47use maud::{html, Markup, PreEscaped};
48use plotly::Plot;
49use rand::{distributions::Alphanumeric, Rng};
50use std::io::Write;
51
52/// Represents a section of the report, containing a title and multiple content blocks.
53pub struct ReportSection {
54    title: String,
55    content_blocks: Vec<Markup>, // Multiple content blocks (text or plots)
56}
57
58impl ReportSection {
59    /// Creates a new section with the given title.
60    ///
61    /// # Arguments
62    ///
63    /// * `title` - A string slice that holds the title of the section.
64    pub fn new(title: &str) -> Self {
65        ReportSection {
66            title: title.to_string(),
67            content_blocks: Vec::new(),
68        }
69    }
70
71    /// Adds a block of content (text, HTML, etc.) to the section.
72    ///
73    /// # Arguments
74    ///
75    /// * `content` - A Markup object representing the content to be added.
76    pub fn add_content(&mut self, content: Markup) {
77        self.content_blocks.push(content);
78    }
79
80    /// Adds a Plotly plot to the section, with responsive sizing.
81    ///
82    /// # Arguments
83    ///
84    /// * `plot` - A Plot object to be added to the section.
85    pub fn add_plot(&mut self, plot: Plot) {
86        let plot_id: String = rand::thread_rng()
87            .sample_iter(&Alphanumeric)
88            .take(10)
89            .map(char::from)
90            .collect();
91
92        self.content_blocks.push(html! {
93            div class="plot-wrapper" {
94                div id=(plot_id.clone()) class="plot-container" {
95                    (PreEscaped(plot.to_inline_html(Some(&plot_id))))
96                }
97            }
98            script {
99                (PreEscaped(format!(r#"
100                    function resizePlot() {{
101                        let plotDiv = document.getElementById('{plot_id}');
102                        if (plotDiv) {{
103                            let width = window.innerWidth * 0.8;
104                            Plotly.relayout(plotDiv, {{ width: width }});
105                        }}
106                    }}
107                    window.addEventListener('resize', resizePlot);
108                    resizePlot(); // Call initially
109                "#)))
110            }
111        });
112    }
113
114    /// Render the section as HTML
115    fn render(&self) -> Markup {
116        html! {
117            div {
118                h2 { (self.title) }
119                @for block in &self.content_blocks {
120                    (block)
121                }
122            }
123        }
124    }
125}
126
127/// Represents the entire report, containing multiple sections and metadata.
128pub struct Report {
129    software_name: String,
130    version: String,
131    software_logo: Option<String>,
132    title: String,
133    sections: Vec<ReportSection>,
134}
135
136impl Report {
137    /// Creates a new report with the given metadata.
138    ///
139    /// # Arguments
140    ///
141    /// * `software_name` - The name of the software generating the report.
142    /// * `version` - The version of the software.
143    /// * `software_logo` - An optional path to the software's logo image.
144    /// * `title` - The title of the report.
145    pub fn new(
146        software_name: &str,
147        version: &str,
148        software_logo: Option<&str>,
149        title: &str,
150    ) -> Self {
151        Report {
152            software_name: software_name.to_string(),
153            version: version.to_string(),
154            software_logo: software_logo.map(|s| s.to_string()),
155            title: title.to_string(),
156            sections: Vec::new(),
157        }
158    }
159
160    /// Adds a section to the report.
161    ///
162    /// # Arguments
163    ///
164    /// * `section` - A ReportSection to be added to the report.
165    pub fn add_section(&mut self, section: ReportSection) {
166        self.sections.push(section);
167    }
168
169    /// Render the entire report as HTML
170    fn render(&self) -> Markup {
171        let current_date = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
172
173        html! {
174            (maud::DOCTYPE)
175            html {
176                head {
177                    title { (self.title) }
178                    script src="https://cdn.plot.ly/plotly-latest.min.js" {}
179                    script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.4/jquery.min.js" {}
180                    script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js" {}
181                    link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/jquery.dataTables.min.css" {}
182                    script src="https://cdn.datatables.net/colresize/1.0.0/dataTables.colResize.min.js" {}
183                    link rel="stylesheet" href="https://cdn.datatables.net/colResize/1.0.0/css/colResize.dataTables.min.css" {}
184                    script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js" {}
185
186                    // JavaScript for DataTables and CSV export
187                    script {
188                        (PreEscaped(r#"
189                            $(document).ready(function() {
190                                let table = $('#dataTable').DataTable({
191                                    paging: true,
192                                    searching: true,
193                                    ordering: true,
194                                    scrollX: true,
195                                    autoWidth: false,  // Ensures DataTables doesn't override widths
196                                    colResize: {
197                                        enable: true,  // Enable column resizing
198                                        resizeTable: true
199                                    }
200                                });
201
202                                $('#downloadCsv').on('click', function() {
203                                    let csv = [];
204                                    let headers = [];
205                                    $('#dataTable thead th').each(function() {
206                                        headers.push($(this).text());
207                                    });
208                                    csv.push(headers.join(','));
209
210                                    $('#dataTable tbody tr').each(function() {
211                                        let row = [];
212                                        $(this).find('td').each(function() {
213                                            row.push('"' + $(this).text() + '"');
214                                        });
215                                        csv.push(row.join(','));
216                                    });
217
218                                    let csvContent = csv.join('\n');
219                                    let blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
220                                    saveAs(blob, 'table_data.csv');
221                                });
222                            });
223                        "#))
224                    }
225
226                    // JavaScript for tabs
227                    script {
228                        (PreEscaped(r#"
229                            function showTab(tabId) {
230                                document.querySelectorAll('.tab-content').forEach(function(tab) {
231                                    tab.classList.remove('active');
232                                });
233                    
234                                document.querySelectorAll('.tab').forEach(function(tab) {
235                                    tab.classList.remove('active');
236                                });
237                    
238                                document.getElementById(tabId).classList.add('active');
239                                document.querySelector(`[data-tab='${tabId}']`).classList.add('active');
240                            }
241                        "#))
242                    }
243
244
245                    // CSS styles
246                    // CSS for the table container
247                    style {
248                        (PreEscaped("
249                            .table-container {
250                                width: 100%;
251                                overflow-x: auto; /* Enable horizontal scrolling */
252                                white-space: nowrap; /* Prevent line breaks in cells */
253                                border: 1px solid #ddd; /* Optional: Add a border */
254                                padding: 10px;
255                            }
256                            table {
257                                width: 100%;
258                                border-collapse: collapse;
259                            }
260                            table.display {
261                                width: 100% 
262                                table-layout: fixed;
263                                border-collapse: collapse;
264                            }
265
266                            .dataTables_scrollHeadInner {
267                                width: 100% !important;
268                            }
269                        "))
270                    }
271
272                    // CSS for the plot container
273                    style {
274                        (PreEscaped("
275                            .plot-wrapper {
276                                width: 100%;
277                                display: flex;
278                                justify-content: center;
279                                align-items: center;
280                                position: relative;
281                            }
282
283                            .plot-container {
284                                width: 100%;
285                                // max-width: 1200px; /* Prevents it from getting too large */
286                                height: 600px; /* Adjust as needed */
287                                position: relative;
288                                overflow: hidden; /* Prevents content from spilling */
289                                // border: 1px solid #ccc; /* Optional: Helps visualize layout */
290                            }
291                        "))
292                    }
293
294                    // CSS for the report
295                    style {
296                        (PreEscaped("
297                            body {
298                                font-family: Arial, sans-serif;
299                            }
300                            .banner {
301                                display: flex;
302                                align-items: center;
303                                justify-content: space-between;
304                                padding: 15px;
305                                background: linear-gradient(135deg, #4a90e2, #145da0);
306                                border-radius: 12px;
307                                box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
308                                color: white;
309                                margin-bottom: 20px;
310                                max-width: 100%;
311                                overflow: hidden;
312                            }
313                            .banner img {
314                                max-height: 100px;
315                                width: auto;
316                                height: auto;
317                                margin-right: 15px;
318                            }
319                            .banner-text h2 {
320                                font-size: 36px;
321                                margin: 0;
322                                white-space: nowrap;
323                            }
324                            .banner-text p {
325                                font-size: 16px;
326                                margin: 0;
327                                opacity: 0.8;
328                            }
329                            .tabs {
330                                display: flex;
331                                border-bottom: 2px solid #ddd;
332                            }
333                            .tab {
334                                padding: 10px 20px;
335                                cursor: pointer;
336                                font-size: 16px;
337                                font-weight: bold;
338                                color: #444;
339                                transition: 0.3s;
340                            }
341                            .tab:hover {
342                                color: #000;
343                            }
344                            .tab.active {
345                                border-bottom: 3px solid #007bff;
346                                color: #007bff;
347                            }
348                            .tab-content {
349                                display: none;
350                                padding: 20px;
351                            }
352                            .tab-content.active {
353                                display: block;
354                            }
355                        "))
356                    }
357                }
358
359                body {
360                    div class="banner" {
361                        @if let Some(ref logo) = self.software_logo {
362                            img src=(logo) alt="Software Logo";
363                        }
364                        div class="banner-text" {
365                            h2 { (self.software_name) " v" (self.version) }
366                            p class="timestamp" { "Generated on: " (current_date) }
367                        }
368                    }
369
370                    div class="tabs" {
371                        @for (i, section) in self.sections.iter().enumerate() {
372                            button class="tab" data-tab=(format!("tab{}", i)) onclick=(format!("showTab('tab{}')", i)) {
373                                (section.title.clone())
374                            }
375                        }
376                    }
377
378                    @for (i, section) in self.sections.iter().enumerate() {
379                        div id=(format!("tab{}", i)) class={@if i == 0 { "tab-content active" } @else { "tab-content" }} {
380                            (section.render())
381                        }
382                    }
383                }
384            }
385        }
386    }
387
388    /// Saves the report to an HTML file.
389    ///
390    /// # Arguments
391    ///
392    /// * `filename` - The name of the file to save the report to.
393    ///
394    /// # Returns
395    ///
396    /// A Result indicating success or an IO error.
397    pub fn save_to_file(&self, filename: &str) -> std::io::Result<()> {
398        let mut file = std::fs::File::create(filename)?;
399        file.write_all(self.render().into_string().as_bytes())?;
400        Ok(())
401    }
402}
403
404impl ToString for Report {
405    fn to_string(&self) -> String {
406        self.render().into_string()
407    }
408}
409
410#[cfg(test)]
411
412mod tests {
413    use super::*;
414    use crate::plots::plot_scatter;
415    use maud::html;
416
417    #[test]
418    fn test_report() {
419        let mut report = Report::new("Redeem", "1.0", Some("logo.png"), "My Report");
420
421        let mut section1 = ReportSection::new("Section 1");
422        section1.add_content(html! {
423            p { "This is the first section of the report." }
424        });
425
426        // create table
427        let table = html! {
428            table class="display" id="dataTable" {
429                thead {
430                    tr {
431                        th { "Name" }
432                        th { "Age" }
433                        th { "City" }
434                        th { "Country" }
435                        th { "Occupation" }
436                        th { "Salary" }
437                        th { "Join Date" }
438                        th { "Active" }
439                        th { "Actions" }
440                        th { "Actions" }
441                        th { "Actions" }
442                    }
443                }
444                tbody {
445                    tr {
446                        td { "JohnMichaelbrunovalentinemark Beckham" }
447                        td { "30" }
448                        td { "New York" }
449                        td { "USA" }
450                        td { "Engineer" }
451                        td { "100,000" }
452                        td { "2022-01-01" }
453                        td { "Yes" }
454                        td { "Edit | Delete" }
455                        td { "Edit | Delete" }
456                        td { "Edit | Delete" }
457                    }
458                    tr {
459                        td { "Jane Smith" }
460                        td { "25" }
461                        td { "Los Angeles" }
462                        td { "USA" }
463                        td { "Designer" }
464                        td { "80,000" }
465                        td { "2022-02-15" }
466                        td { "No" }
467                        td { "Edit | Delete" }
468                        td { "Edit | Delete" }
469                        td { "Edit | Delete" }
470                    }
471                }
472            }
473        };
474        section1.add_content(table.clone());
475
476        report.add_section(section1);
477
478        // Add a scatter plot
479        let x = vec![
480            vec![1.0, 2.0, 3.0, 4.0, 5.0],
481            vec![2.0, 7.0, 3.0, 9.0, 10.0],
482            vec![1.0, 12.0, 13.0, 14.0, 15.0],
483        ];
484        let y = vec![
485            vec![1.0, 2.0, 3.0, 4.0, 5.0],
486            vec![6.0, 7.0, 8.0, 9.0, 10.0],
487            vec![11.0, 12.0, 13.0, 14.0, 15.0],
488        ];
489        let labels = vec![
490            "file1".to_string(),
491            "file2".to_string(),
492            "file3".to_string(),
493        ];
494        let title = "Scatter Plot";
495        let x_title = "X";
496        let y_title = "Y";
497
498        let plot = plot_scatter(&x, &y, labels, title, x_title, y_title).unwrap();
499
500        let mut section2 = ReportSection::new("Section 2");
501        section2.add_plot(plot.clone());
502
503        // add some content latin
504        section2.add_content(html! {
505            p { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac nisl..." }
506        });
507
508        section2.add_content(table);
509
510        // add another plot (the same one)
511        section2.add_plot(plot);
512
513        report.add_section(section2);
514
515        report.save_to_file("report.html").unwrap();
516    }
517}