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}