use lazy_static::lazy_static;
use std::sync::RwLock;
use crate::core::error::{Error, Result};
use crate::dataframe::base::DataFrame;
use crate::series::base::Series;
use crate::{read_lock_safe, write_lock_safe};
#[derive(Debug, Clone)]
pub struct JupyterConfig {
pub max_rows: usize,
pub max_columns: usize,
pub interactive: bool,
pub precision: usize,
pub color_scheme: JupyterColorScheme,
pub table_style: TableStyle,
pub syntax_highlighting: bool,
}
impl Default for JupyterConfig {
fn default() -> Self {
Self {
max_rows: 100,
max_columns: 20,
interactive: true,
precision: 6,
color_scheme: JupyterColorScheme::Default,
table_style: TableStyle::default(),
syntax_highlighting: true,
}
}
}
#[derive(Debug, Clone)]
pub enum JupyterColorScheme {
Default,
Dark,
Light,
Custom {
header_bg: String,
header_text: String,
row_bg: String,
alt_row_bg: String,
text_color: String,
border_color: String,
},
}
impl JupyterColorScheme {
pub fn to_css(&self) -> String {
match self {
JupyterColorScheme::Default => r#"
.pandrs-table { background-color: #ffffff; color: #333333; }
.pandrs-header { background-color: #f8f9fa; color: #495057; font-weight: bold; }
.pandrs-row { background-color: #ffffff; }
.pandrs-row:nth-child(even) { background-color: #f8f9fa; }
.pandrs-border { border-color: #dee2e6; }
"#
.to_string(),
JupyterColorScheme::Dark => r#"
.pandrs-table { background-color: #2d3748; color: #e2e8f0; }
.pandrs-header { background-color: #4a5568; color: #f7fafc; font-weight: bold; }
.pandrs-row { background-color: #2d3748; }
.pandrs-row:nth-child(even) { background-color: #4a5568; }
.pandrs-border { border-color: #718096; }
"#
.to_string(),
JupyterColorScheme::Light => r#"
.pandrs-table { background-color: #fefefe; color: #2d3748; }
.pandrs-header { background-color: #edf2f7; color: #2d3748; font-weight: bold; }
.pandrs-row { background-color: #fefefe; }
.pandrs-row:nth-child(even) { background-color: #f7fafc; }
.pandrs-border { border-color: #cbd5e0; }
"#
.to_string(),
JupyterColorScheme::Custom {
header_bg,
header_text,
row_bg,
alt_row_bg,
text_color,
border_color,
} => {
let header_bg = sanitize_css_value(header_bg);
let header_text = sanitize_css_value(header_text);
let row_bg = sanitize_css_value(row_bg);
let alt_row_bg = sanitize_css_value(alt_row_bg);
let text_color = sanitize_css_value(text_color);
let border_color = sanitize_css_value(border_color);
format!(
r#"
.pandrs-table {{ background-color: {}; color: {}; }}
.pandrs-header {{ background-color: {}; color: {}; font-weight: bold; }}
.pandrs-row {{ background-color: {}; }}
.pandrs-row:nth-child(even) {{ background-color: {}; }}
.pandrs-border {{ border-color: {}; }}
"#,
row_bg, text_color, header_bg, header_text, row_bg, alt_row_bg, border_color
)
}
}
}
}
#[derive(Debug, Clone)]
pub struct TableStyle {
pub show_borders: bool,
pub show_index: bool,
pub show_dtypes: bool,
pub show_summary: bool,
pub width: TableWidth,
pub cell_padding: String,
pub font_family: String,
pub font_size: String,
}
impl Default for TableStyle {
fn default() -> Self {
Self {
show_borders: true,
show_index: true,
show_dtypes: false,
show_summary: false,
width: TableWidth::Auto,
cell_padding: "8px 12px".to_string(),
font_family: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace".to_string(),
font_size: "13px".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub enum TableWidth {
Auto,
Fixed(u32),
Percentage(u32),
Full,
}
impl TableWidth {
fn to_css(&self) -> String {
match self {
TableWidth::Auto => "width: auto;".to_string(),
TableWidth::Fixed(px) => format!("width: {}px;", px),
TableWidth::Percentage(pct) => format!("width: {}%;", pct),
TableWidth::Full => "width: 100%;".to_string(),
}
}
}
pub trait JupyterDisplay {
fn to_jupyter_html(&self, config: &JupyterConfig) -> Result<String>;
fn to_interactive_widget(&self, config: &JupyterConfig) -> Result<String>;
fn to_summary_html(&self, config: &JupyterConfig) -> Result<String>;
fn to_data_explorer(&self, config: &JupyterConfig) -> Result<String>;
}
impl JupyterDisplay for DataFrame {
fn to_jupyter_html(&self, config: &JupyterConfig) -> Result<String> {
let mut html = String::new();
html.push_str(&format!(
r#"
<style>
.pandrs-container {{
margin: 10px 0;
font-family: {};
font-size: {};
}}
.pandrs-table {{
border-collapse: collapse;
margin: 10px 0;
{}
}}
.pandrs-table th, .pandrs-table td {{
padding: {};
text-align: left;
border: {};
}}
{}
.pandrs-info {{
margin: 5px 0;
font-size: 12px;
color: #666;
}}
.pandrs-dtype {{
font-style: italic;
color: #888;
font-size: 11px;
}}
</style>
"#,
sanitize_css_value(&config.table_style.font_family),
sanitize_css_value(&config.table_style.font_size),
config.table_style.width.to_css(),
sanitize_css_value(&config.table_style.cell_padding),
if config.table_style.show_borders {
"1px solid var(--pandrs-border-color, #ddd)"
} else {
"none"
},
config.color_scheme.to_css()
));
html.push_str(r#"<div class="pandrs-container">"#);
html.push_str(&format!(
r#"<div class="pandrs-info">DataFrame: {} rows × {} columns</div>"#,
self.row_count(),
self.column_names().len()
));
html.push_str(r#"<table class="pandrs-table">"#);
html.push_str("<thead><tr class='pandrs-header'>");
if config.table_style.show_index {
html.push_str("<th></th>"); }
let columns = self.column_names();
let visible_columns = if columns.len() > config.max_columns {
&columns[..config.max_columns]
} else {
&columns
};
for col_name in visible_columns {
html.push_str(&format!("<th>{}</th>", escape_html(col_name)));
}
if columns.len() > config.max_columns {
html.push_str("<th>...</th>");
}
html.push_str("</tr>");
if config.table_style.show_dtypes {
html.push_str("<tr class='pandrs-header'>");
if config.table_style.show_index {
html.push_str("<td class='pandrs-dtype'></td>");
}
for col_name in visible_columns {
let dtype = self.infer_column_type(col_name);
html.push_str(&format!("<td class='pandrs-dtype'>{}</td>", dtype));
}
if columns.len() > config.max_columns {
html.push_str("<td class='pandrs-dtype'>...</td>");
}
html.push_str("</tr>");
}
html.push_str("</thead>");
html.push_str("<tbody>");
let row_count = self.row_count();
let visible_rows = if row_count > config.max_rows {
config.max_rows / 2
} else {
row_count
};
for i in 0..visible_rows.min(row_count) {
html.push_str(&format!("<tr class='pandrs-row'>"));
if config.table_style.show_index {
html.push_str(&format!("<td><strong>{}</strong></td>", i));
}
for col_name in visible_columns {
let value = self
.get_value_at(i, col_name)
.unwrap_or_else(|_| "NaN".to_string());
let formatted_value = format_value(&value, config.precision);
html.push_str(&format!("<td>{}</td>", escape_html(&formatted_value)));
}
if columns.len() > config.max_columns {
html.push_str("<td>...</td>");
}
html.push_str("</tr>");
}
if row_count > config.max_rows {
html.push_str("<tr class='pandrs-row'>");
if config.table_style.show_index {
html.push_str("<td>...</td>");
}
for _ in 0..visible_columns.len() {
html.push_str("<td>...</td>");
}
if columns.len() > config.max_columns {
html.push_str("<td>...</td>");
}
html.push_str("</tr>");
let remaining_rows = config.max_rows - visible_rows;
let start_idx = row_count - remaining_rows;
for i in start_idx..row_count {
html.push_str(&format!("<tr class='pandrs-row'>"));
if config.table_style.show_index {
html.push_str(&format!("<td><strong>{}</strong></td>", i));
}
for col_name in visible_columns {
let value = self
.get_value_at(i, col_name)
.unwrap_or_else(|_| "NaN".to_string());
let formatted_value = format_value(&value, config.precision);
html.push_str(&format!("<td>{}</td>", escape_html(&formatted_value)));
}
if columns.len() > config.max_columns {
html.push_str("<td>...</td>");
}
html.push_str("</tr>");
}
}
html.push_str("</tbody>");
html.push_str("</table>");
if config.table_style.show_summary {
html.push_str(&format!(
r#"<div class="pandrs-info">
Memory usage: ~{} |
Columns: {} |
Data types: {}
</div>"#,
estimate_memory_usage(self),
columns.len(),
self.get_column_types().join(", ")
));
}
html.push_str("</div>");
Ok(html)
}
fn to_interactive_widget(&self, config: &JupyterConfig) -> Result<String> {
let mut html = String::new();
let widget_id = next_widget_id();
let data_panel = self.to_jupyter_html(config)?;
let info_panel = format!(
"<h4>DataFrame Information</h4><ul><li>Rows: {}</li><li>Columns: {}</li>\
<li>Memory usage: ~{}</li><li>Column types: {}</li></ul>",
self.row_count(),
self.column_names().len(),
estimate_memory_usage(self),
escape_html(&self.get_column_types().join(", ")),
);
let describe_panel = describe_table_html(self);
let plot_panel = quick_viz_html(self);
html.push_str(&format!(
r#"
<div id="pandrs-widget-{widget_id}" class="pandrs-interactive-widget">
<style>
.pandrs-interactive-widget {{
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
margin: 10px 0;
font-family: {font_family};
}}
.pandrs-controls {{
margin-bottom: 10px;
padding: 5px;
background-color: #f8f9fa;
border-radius: 3px;
}}
.pandrs-controls button {{
margin: 2px;
padding: 5px 10px;
border: 1px solid #ccc;
background-color: #fff;
cursor: pointer;
border-radius: 3px;
}}
.pandrs-controls button:hover {{
background-color: #e9ecef;
}}
.pandrs-search {{
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
margin: 0 5px;
}}
</style>
<div class="pandrs-controls">
<button onclick="pandrs_show_panel({widget_id}, 'data')">📊 Data</button>
<button onclick="pandrs_show_panel({widget_id}, 'info')">ℹ️ Info</button>
<button onclick="pandrs_show_panel({widget_id}, 'describe')">📈 Describe</button>
<button onclick="pandrs_show_panel({widget_id}, 'plot')">📊 Plot</button>
<input type="text" class="pandrs-search" placeholder="Search columns..."
onkeyup="pandrs_filter_columns({widget_id}, this.value)">
</div>
<div id="pandrs-panel-data-{widget_id}" class="pandrs-widget-panel">{data_panel}</div>
<div id="pandrs-panel-info-{widget_id}" class="pandrs-widget-panel" style="display:none;">{info_panel}</div>
<div id="pandrs-panel-describe-{widget_id}" class="pandrs-widget-panel" style="display:none;">{describe_panel}</div>
<div id="pandrs-panel-plot-{widget_id}" class="pandrs-widget-panel" style="display:none;">{plot_panel}</div>
</div>
<script>
function pandrs_show_panel(widget_id, panel) {{
['data', 'info', 'describe', 'plot'].forEach(function(name) {{
var el = document.getElementById('pandrs-panel-' + name + '-' + widget_id);
if (el) {{
el.style.display = (name === panel) ? 'block' : 'none';
}}
}});
}}
function pandrs_filter_columns(widget_id, filter) {{
// Column filtering functionality would be implemented here
console.log('Filtering columns by:', filter);
}}
</script>
"#,
widget_id = widget_id,
font_family = sanitize_css_value(&config.table_style.font_family),
data_panel = data_panel,
info_panel = info_panel,
describe_panel = describe_panel,
plot_panel = plot_panel,
));
Ok(html)
}
fn to_summary_html(&self, config: &JupyterConfig) -> Result<String> {
let mut html = String::new();
html.push_str(&format!(
r#"
<div class="pandrs-summary" style="
font-family: {};
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
margin: 10px 0;
background-color: #f8f9fa;
">
<h3 style="margin-top: 0;">DataFrame Summary</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div>
<h4>Basic Information</h4>
<ul style="list-style: none; padding: 0;">
<li><strong>Shape:</strong> {} rows × {} columns</li>
<li><strong>Memory usage:</strong> ~{}</li>
<li><strong>Non-null values:</strong> {}</li>
<li><strong>Null values:</strong> {}</li>
</ul>
</div>
<div>
<h4>Column Information</h4>
<ul style="list-style: none; padding: 0; max-height: 200px; overflow-y: auto;">
"#,
sanitize_css_value(&config.table_style.font_family),
self.row_count(),
self.column_names().len(),
estimate_memory_usage(self),
estimate_non_null_count(self),
estimate_null_count(self)
));
for (i, col_name) in self.column_names().iter().enumerate() {
let col_type = self.infer_column_type(col_name);
html.push_str(&format!(
"<li><strong>{}:</strong> {} <span style='color: #666; font-size: 0.9em'>(col {})</span></li>",
escape_html(col_name),
col_type,
i
));
}
html.push_str(
r#"
</ul>
</div>
</div>
</div>
"#,
);
Ok(html)
}
fn to_data_explorer(&self, config: &JupyterConfig) -> Result<String> {
let widget_id = next_widget_id();
let mut html = String::new();
let data_tab = self.to_jupyter_html(config)?;
let info_tab = self.to_summary_html(config)?;
let stats_tab = describe_table_html(self);
let viz_tab = quick_viz_html(self);
html.push_str(&format!(r#"
<div id="pandrs-explorer-{widget_id}" class="pandrs-data-explorer">
<style>
.pandrs-data-explorer {{
border: 1px solid #ddd;
border-radius: 8px;
padding: 0;
margin: 15px 0;
font-family: {font_family};
background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.pandrs-explorer-header {{
background-color: #007bff;
color: white;
padding: 12px 15px;
border-radius: 8px 8px 0 0;
font-weight: bold;
}}
.pandrs-explorer-tabs {{
display: flex;
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
}}
.pandrs-explorer-tab {{
padding: 10px 20px;
cursor: pointer;
border: none;
background: none;
border-bottom: 3px solid transparent;
font-weight: 500;
}}
.pandrs-explorer-tab.active {{
background-color: white;
border-bottom-color: #007bff;
color: #007bff;
}}
.pandrs-explorer-content {{
padding: 15px;
min-height: 300px;
}}
</style>
<div class="pandrs-explorer-header">
🔍 DataFrame Explorer - {rows} rows × {cols} columns
</div>
<div class="pandrs-explorer-tabs">
<button class="pandrs-explorer-tab active" onclick="pandrs_show_tab(event, {widget_id}, 'data')">
📊 Data
</button>
<button class="pandrs-explorer-tab" onclick="pandrs_show_tab(event, {widget_id}, 'info')">
ℹ️ Info
</button>
<button class="pandrs-explorer-tab" onclick="pandrs_show_tab(event, {widget_id}, 'stats')">
📈 Statistics
</button>
<button class="pandrs-explorer-tab" onclick="pandrs_show_tab(event, {widget_id}, 'viz')">
📊 Visualize
</button>
</div>
<div class="pandrs-explorer-content">
<div id="pandrs-tab-data-{widget_id}" class="pandrs-tab-content">
{data_tab}
</div>
<div id="pandrs-tab-info-{widget_id}" class="pandrs-tab-content" style="display: none;">
{info_tab}
</div>
<div id="pandrs-tab-stats-{widget_id}" class="pandrs-tab-content" style="display: none;">
<h4>Statistical Summary</h4>
{stats_tab}
</div>
<div id="pandrs-tab-viz-{widget_id}" class="pandrs-tab-content" style="display: none;">
<h4>Quick Visualizations</h4>
{viz_tab}
</div>
</div>
</div>
<script>
function pandrs_show_tab(evt, explorer_id, tab_name) {{
// Hide all tabs
['data', 'info', 'stats', 'viz'].forEach(function(name) {{
var element = document.getElementById('pandrs-tab-' + name + '-' + explorer_id);
if (element) element.style.display = 'none';
}});
// Remove active class from all tab buttons
var tabs = document.querySelectorAll('#pandrs-explorer-' + explorer_id + ' .pandrs-explorer-tab');
tabs.forEach(function(tab) {{
tab.classList.remove('active');
}});
// Show selected tab
var selectedTab = document.getElementById('pandrs-tab-' + tab_name + '-' + explorer_id);
if (selectedTab) selectedTab.style.display = 'block';
// Add active class to the clicked button. The triggering
// event is passed explicitly (`evt`) rather than read from
// the global `window.event`, which is a deprecated,
// non-standard, Chrome/IE-only fallback that recent browsers
// are dropping.
if (evt && evt.target) {{
evt.target.classList.add('active');
}}
}}
</script>
"#,
widget_id = widget_id,
font_family = sanitize_css_value(&config.table_style.font_family),
rows = self.row_count(),
cols = self.column_names().len(),
data_tab = data_tab,
info_tab = info_tab,
stats_tab = stats_tab,
viz_tab = viz_tab,
));
Ok(html)
}
}
impl<T> JupyterDisplay for Series<T>
where
T: Clone + std::fmt::Display + std::fmt::Debug,
{
fn to_jupyter_html(&self, config: &JupyterConfig) -> Result<String> {
let mut html = String::new();
html.push_str(&format!(
r#"
<style>
.pandrs-series-container {{
margin: 10px 0;
font-family: {};
font-size: {};
}}
.pandrs-series-table {{
border-collapse: collapse;
margin: 10px 0;
{}
}}
.pandrs-series-table th, .pandrs-series-table td {{
padding: {};
text-align: left;
border: {};
}}
{}
.pandrs-series-info {{
margin: 5px 0;
font-size: 12px;
color: #666;
}}
</style>
"#,
sanitize_css_value(&config.table_style.font_family),
sanitize_css_value(&config.table_style.font_size),
config.table_style.width.to_css(),
sanitize_css_value(&config.table_style.cell_padding),
if config.table_style.show_borders {
"1px solid var(--pandrs-border-color, #ddd)"
} else {
"none"
},
config.color_scheme.to_css()
));
html.push_str(r#"<div class="pandrs-series-container">"#);
let series_name = self.name().map_or("Unnamed", |s| s.as_str());
html.push_str(&format!(
r#"<div class="pandrs-series-info">Series '{}': {} values</div>"#,
escape_html(series_name),
self.len()
));
html.push_str(r#"<table class="pandrs-series-table">"#);
html.push_str("<thead><tr class='pandrs-header'>");
if config.table_style.show_index {
html.push_str("<th>Index</th>");
}
html.push_str(&format!("<th>{}</th>", escape_html(series_name)));
html.push_str("</tr></thead>");
html.push_str("<tbody>");
let values = self.values();
let visible_count = if values.len() > config.max_rows {
config.max_rows / 2
} else {
values.len()
};
for (i, value) in values.iter().enumerate().take(visible_count) {
html.push_str(&format!("<tr class='pandrs-row'>"));
if config.table_style.show_index {
html.push_str(&format!("<td><strong>{}</strong></td>", i));
}
let formatted_value = format!("{}", value);
html.push_str(&format!("<td>{}</td>", escape_html(&formatted_value)));
html.push_str("</tr>");
}
if values.len() > config.max_rows {
html.push_str("<tr class='pandrs-row'>");
if config.table_style.show_index {
html.push_str("<td>...</td>");
}
html.push_str("<td>...</td>");
html.push_str("</tr>");
let remaining_count = config.max_rows - visible_count;
let start_idx = values.len() - remaining_count;
for (i, value) in values.iter().enumerate().skip(start_idx) {
html.push_str(&format!("<tr class='pandrs-row'>"));
if config.table_style.show_index {
html.push_str(&format!("<td><strong>{}</strong></td>", i));
}
let formatted_value = format!("{}", value);
html.push_str(&format!("<td>{}</td>", escape_html(&formatted_value)));
html.push_str("</tr>");
}
}
html.push_str("</tbody>");
html.push_str("</table>");
html.push_str("</div>");
Ok(html)
}
fn to_interactive_widget(&self, config: &JupyterConfig) -> Result<String> {
let mut html = String::new();
let series_name = self.name().map_or("Unnamed", |s| s.as_str());
html.push_str(&format!(
r#"
<div class="pandrs-series-widget">
<div style="margin-bottom: 10px; font-weight: bold;">
Series '{}' Interactive Widget
</div>
{}
</div>
"#,
escape_html(series_name),
self.to_jupyter_html(config)?
));
Ok(html)
}
fn to_summary_html(&self, config: &JupyterConfig) -> Result<String> {
let series_name = self.name().map_or("Unnamed", |s| s.as_str());
Ok(format!(
r#"
<div class="pandrs-series-summary" style="
font-family: {};
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
margin: 10px 0;
background-color: #f8f9fa;
">
<h3 style="margin-top: 0;">Series '{}' Summary</h3>
<ul style="list-style: none; padding: 0;">
<li><strong>Length:</strong> {} values</li>
<li><strong>Non-null values:</strong> {}</li>
<li><strong>Data type:</strong> {}</li>
</ul>
</div>
"#,
sanitize_css_value(&config.table_style.font_family),
escape_html(series_name),
self.len(),
self.len(), std::any::type_name::<T>()
))
}
fn to_data_explorer(&self, config: &JupyterConfig) -> Result<String> {
self.to_summary_html(config)
}
}
pub struct JupyterMagics;
impl JupyterMagics {
pub fn register_magics() -> String {
r#"
# PandRS Magic Commands for Jupyter — illustrative template.
#
# This is NOT a working integration: PandRS is a Rust library and
# has no bridge back into a live IPython kernel. Adapt the method
# bodies below to call into your own PandRS binding before use.
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@magics_class
class PandRSMagics(Magics):
@line_magic
def pandrs_info(self, line):
"""Display PandRS version and configuration info"""
return "PandRS 0.4.1 - High-performance DataFrame library for Rust"
@line_magic
def pandrs_config(self, line):
"""Template only: not wired to a live PandRS configuration."""
raise NotImplementedError(
"pandrs_config is an illustrative template — wire this "
"method to your own PandRS configuration object before use."
)
@cell_magic
def pandrs_sql(self, line, cell):
"""Template only: does not execute SQL against any DataFrame."""
raise NotImplementedError(
"pandrs_sql is an illustrative template — it does not run "
"the cell against any DataFrame. Wire this method to your "
"own PandRS query engine before use. Received:\n" + cell
)
# Register the magic class
ip = get_ipython()
ip.register_magic_functions(PandRSMagics)
"#
.to_string()
}
}
fn escape_html(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn sanitize_css_value(input: &str) -> String {
input.chars().filter(|&c| c != '<' && c != '>').collect()
}
fn format_value(value: &str, precision: usize) -> String {
if let Ok(f) = value.parse::<f64>() {
if f.fract() == 0.0 && f.abs() < 1e10 {
format!("{:.0}", f)
} else {
format!("{:.prec$}", f, prec = precision)
}
} else {
value.to_string()
}
}
fn format_memory_kb(bytes: u64) -> String {
let kb = bytes as f64 / 1024.0;
if kb >= 1024.0 {
format!("{:.2} MB", kb / 1024.0)
} else if kb >= 1.0 {
format!("{:.1} KB", kb)
} else {
format!("{:.2} KB", kb)
}
}
fn estimate_memory_usage(df: &DataFrame) -> String {
let cell_count = df.row_count() * df.column_names().len();
format_memory_kb((cell_count * 8) as u64)
}
fn count_column_nulls(df: &DataFrame, column_name: &str) -> usize {
if df.is_numeric_column(column_name) {
return df
.get_column_numeric_values(column_name)
.map(|values| values.iter().filter(|v| v.is_nan()).count())
.unwrap_or(0);
}
df.get_column_string_values(column_name)
.map(|values| values.iter().filter(|v| v.is_empty()).count())
.unwrap_or(0)
}
static WIDGET_ID_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn next_widget_id() -> u64 {
WIDGET_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
fn describe_table_html(df: &DataFrame) -> String {
let numeric_cols: Vec<String> = df
.column_names()
.iter()
.filter(|c| df.is_numeric_column(c))
.cloned()
.collect();
if numeric_cols.is_empty() {
return "<p>No numeric columns to describe.</p>".to_string();
}
let mut rows = Vec::new();
for col in &numeric_cols {
let Ok(values) = df.get_column_numeric_values(col) else {
continue;
};
let finite: Vec<f64> = values.into_iter().filter(|v| v.is_finite()).collect();
if let Ok(stats) = crate::stats::descriptive::describe(&finite) {
rows.push((col.clone(), stats));
}
}
if rows.is_empty() {
return "<p>No numeric data available to describe.</p>".to_string();
}
let mut html = String::from(
"<table class=\"pandrs-describe-table\"><thead><tr>\
<th></th><th>count</th><th>mean</th><th>std</th><th>min</th>\
<th>25%</th><th>50%</th><th>75%</th><th>max</th></tr></thead><tbody>",
);
for (name, s) in &rows {
html.push_str(&format!(
"<tr><td><strong>{}</strong></td><td>{}</td><td>{:.4}</td><td>{:.4}</td>\
<td>{:.4}</td><td>{:.4}</td><td>{:.4}</td><td>{:.4}</td><td>{:.4}</td></tr>",
escape_html(name),
s.count,
s.mean,
s.std,
s.min,
s.quartiles.q1,
s.quartiles.q2,
s.quartiles.q3,
s.max,
));
}
html.push_str("</tbody></table>");
html
}
fn quick_viz_html(df: &DataFrame) -> String {
use crate::vis::svg::dataframe_ext::SvgVisualize;
let numeric_cols: Vec<String> = df
.column_names()
.iter()
.filter(|c| df.is_numeric_column(c))
.cloned()
.collect();
if numeric_cols.is_empty() {
return "<p>No numeric columns available to visualize.</p>".to_string();
}
let mut html = String::new();
match df.plot_histogram_svg(&numeric_cols[0], 10, None) {
Ok(svg) => html.push_str(&format!(
"<h4>Histogram: {}</h4>{}",
escape_html(&numeric_cols[0]),
svg
)),
Err(e) => html.push_str(&format!(
"<p>Histogram unavailable: {}</p>",
escape_html(&e.to_string())
)),
}
if numeric_cols.len() >= 2 {
if let Ok(svg) = df.plot_scatter_svg(&numeric_cols[0], &numeric_cols[1], None) {
html.push_str(&format!(
"<h4>Scatter: {} vs {}</h4>{}",
escape_html(&numeric_cols[1]),
escape_html(&numeric_cols[0]),
svg
));
}
let col_refs: Vec<&str> = numeric_cols.iter().map(String::as_str).collect();
if let Ok(corr_df) = df.corr_matrix(&col_refs) {
if let Ok(svg) = corr_df.plot_heatmap_svg(None) {
html.push_str(&format!("<h4>Correlation</h4>{}", svg));
}
}
}
html
}
fn estimate_null_count(df: &DataFrame) -> usize {
df.column_names()
.iter()
.map(|col| count_column_nulls(df, col))
.sum()
}
fn estimate_non_null_count(df: &DataFrame) -> usize {
let total = df.row_count() * df.column_names().len();
total.saturating_sub(estimate_null_count(df))
}
impl DataFrame {
fn infer_column_type(&self, column_name: &str) -> String {
let values = match self.get_column_string_values(column_name) {
Ok(v) => v,
Err(_) => return "unknown".to_string(),
};
let sample: Vec<&str> = values
.iter()
.map(String::as_str)
.filter(|v| !v.is_empty())
.collect();
if sample.is_empty() {
return "unknown".to_string();
}
if sample.iter().all(|v| v.parse::<i64>().is_ok()) {
"integer".to_string()
} else if sample.iter().all(|v| v.parse::<f64>().is_ok()) {
"float".to_string()
} else if sample.iter().all(|v| v.parse::<bool>().is_ok()) {
"boolean".to_string()
} else {
"string".to_string()
}
}
fn get_column_types(&self) -> Vec<String> {
self.column_names()
.iter()
.map(|col| self.infer_column_type(col))
.collect()
}
fn get_value_at(&self, row: usize, column: &str) -> Result<String> {
let values = self.get_column_string_values(column)?;
if row < values.len() {
Ok(values[row].clone())
} else {
Err(Error::IndexOutOfBounds {
index: row,
size: values.len(),
})
}
}
pub fn describe_to_json(&self) -> Result<String> {
let numeric_cols: Vec<String> = self
.column_names()
.iter()
.filter(|c| self.is_numeric_column(c))
.cloned()
.collect();
let mut summaries: std::collections::BTreeMap<
String,
crate::stats::descriptive::StatisticalSummary,
> = std::collections::BTreeMap::new();
for col in &numeric_cols {
let Ok(values) = self.get_column_numeric_values(col) else {
continue;
};
let finite: Vec<f64> = values.into_iter().filter(|v| v.is_finite()).collect();
if let Ok(stats) = crate::stats::descriptive::describe(&finite) {
summaries.insert(col.clone(), stats);
}
}
serde_json::to_string_pretty(&summaries).map_err(Error::Json)
}
}
lazy_static! {
static ref JUPYTER_CONFIG: RwLock<Option<JupyterConfig>> = RwLock::new(None);
}
pub fn get_jupyter_config() -> JupyterConfig {
read_lock_safe!(JUPYTER_CONFIG, "jupyter config read")
.ok()
.and_then(|c| c.clone())
.unwrap_or_default()
}
pub fn set_jupyter_config(config: JupyterConfig) {
if let Ok(mut cfg) = write_lock_safe!(JUPYTER_CONFIG, "jupyter config write") {
*cfg = Some(config);
}
}
pub fn init_jupyter() -> Result<()> {
set_jupyter_config(JupyterConfig::default());
println!("🎯 PandRS Jupyter Integration Initialized!");
println!("📚 Enhanced DataFrame display and interactive widgets are now available.");
println!("🔍 Use .to_jupyter_html(), .to_interactive_widget(), or .to_data_explorer() on DataFrames.");
Ok(())
}
pub fn jupyter_dark_mode() -> JupyterConfig {
JupyterConfig {
color_scheme: JupyterColorScheme::Dark,
..Default::default()
}
}
pub fn jupyter_light_mode() -> JupyterConfig {
JupyterConfig {
color_scheme: JupyterColorScheme::Light,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn test_jupyter_config_default() {
let config = JupyterConfig::default();
assert_eq!(config.max_rows, 100);
assert_eq!(config.max_columns, 20);
assert!(config.interactive);
}
#[test]
fn test_color_scheme_css() {
let dark_scheme = JupyterColorScheme::Dark;
let css = dark_scheme.to_css();
assert!(css.contains("background-color: #2d3748"));
}
#[test]
fn test_table_width_css() {
let auto_width = TableWidth::Auto;
assert_eq!(auto_width.to_css(), "width: auto;");
let fixed_width = TableWidth::Fixed(800);
assert_eq!(fixed_width.to_css(), "width: 800px;");
}
#[test]
fn test_escape_html() {
assert_eq!(escape_html("<test>"), "<test>");
assert_eq!(escape_html("AT&T"), "AT&T");
}
#[test]
fn test_format_value() {
assert_eq!(format_value("3.14159", 2), "3.14");
assert_eq!(format_value("42", 2), "42");
assert_eq!(format_value("hello", 2), "hello");
}
#[test]
fn test_jupyter_html_generation() {
let mut data = HashMap::new();
data.insert("col1".to_string(), vec!["1".to_string(), "2".to_string()]);
data.insert("col2".to_string(), vec!["a".to_string(), "b".to_string()]);
let df = DataFrame::from_map(data, None).expect("operation should succeed");
let config = JupyterConfig::default();
let html = df
.to_jupyter_html(&config)
.expect("operation should succeed");
assert!(html.contains("pandrs-table"));
assert!(html.contains("col1"));
assert!(html.contains("col2"));
}
}