use crate::render::portable::utils::minify_js;
use crate::spec::DatasetSpecs;
use crate::utils::column_store::DatasetSummary;
use crate::utils::column_type::IsNa;
use anyhow::Result;
use itertools::Itertools;
use serde::Serialize;
use serde_json::json;
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::str::FromStr;
use tera::{Context, Tera};
pub(crate) fn render_plots<P: AsRef<Path>>(
output_path: P,
dataset: &DatasetSpecs,
summary: &DatasetSummary,
records_length: usize,
debug: bool,
) -> Result<()> {
let path = Path::new(output_path.as_ref()).join("plots");
fs::create_dir(&path)?;
let numeric_indices: Vec<usize> = (0..summary.headers.len())
.filter(|index| summary.column_at(*index).column_type.is_numeric())
.collect();
let mut numeric_plots: HashMap<usize, Option<Vec<BinnedPlotRecord>>> = HashMap::new();
for chunk in numeric_indices.chunks(column_batch_size(records_length)) {
for (offset, values) in read_columns(dataset, chunk)?.into_iter().enumerate() {
numeric_plots.insert(chunk[offset], generate_numeric_plot(&values));
}
}
let mut plots = Vec::new();
for (index, column) in summary.headers.iter().enumerate() {
let mut templates = Tera::default();
let mut context = Context::new();
context.insert("title", &column);
context.insert("index", &index);
if summary.column_at(index).column_type.is_numeric() {
templates.add_raw_template(
"plot.js.tera",
include_str!("../../../templates/numeric_plot.js.tera"),
)?;
context.insert(
"table",
&json!(numeric_plots.remove(&index).unwrap()).to_string(),
);
} else {
let plot = generate_nominal_plot(&summary.column_at(index).value_counts);
templates.add_raw_template(
"plot.js.tera",
include_str!("../../../templates/nominal_plot.js.tera"),
)?;
context.insert("table", &json!(plot).to_string());
}
let js = templates.render("plot.js.tera", &context)?;
plots.push(js);
}
let js_plots = plots.join("\n");
let file_path = path.join(Path::new(&"plots".to_string()).with_extension("js"));
let mut file = fs::File::create(file_path)?;
let minified = minify_js(&js_plots, debug)?;
file.write_all(&minified)?;
Ok(())
}
const COLUMN_BUFFER_CELLS: usize = 4_000_000;
pub(crate) fn column_batch_size(records_length: usize) -> usize {
(COLUMN_BUFFER_CELLS / records_length.max(1)).max(1)
}
pub(crate) fn read_columns(dataset: &DatasetSpecs, columns: &[usize]) -> Result<Vec<Vec<String>>> {
let mut buffers: Vec<Vec<String>> = columns.iter().map(|_| Vec::new()).collect();
for record in dataset.reader()?.records()?.skip(dataset.header_rows - 1) {
for (buffer, &column) in buffers.iter_mut().zip(columns) {
buffer.push(record.get(column).unwrap().to_string());
}
}
Ok(buffers)
}
fn binned_counts(values: &[f32], min: f32, max: f32, num_bins: usize) -> Vec<u32> {
let bin_width = (max - min) / num_bins as f32;
let mut counts = vec![0u32; num_bins];
for &v in values {
let idx = ((v - min) / bin_width) as usize;
counts[idx.min(num_bins - 1)] += 1;
}
counts
}
fn counts_to_records(counts: &[u32], min: f32, max: f32) -> Vec<BinnedPlotRecord> {
let bin_width = (max - min) / counts.len() as f32;
counts
.iter()
.enumerate()
.map(|(i, &value)| BinnedPlotRecord {
bin_start: min + i as f32 * bin_width,
bin_end: min + (i + 1) as f32 * bin_width,
value,
})
.collect()
}
fn refined_bins(values: &[f32], min: f32, max: f32) -> Vec<BinnedPlotRecord> {
let mut num_bins = NUMERIC_BINS;
let mut counts = binned_counts(values, min, max, num_bins);
for _ in 0..MAX_BIN_REFINEMENT_ROUNDS {
let max_idx = counts
.iter()
.enumerate()
.max_by_key(|(_, &c)| c)
.map(|(i, _)| i)
.unwrap_or(0);
let doubled = binned_counts(values, min, max, num_bins * 2);
let left = doubled[max_idx * 2];
let right = doubled[max_idx * 2 + 1];
let total = left + right;
if total == 0 {
break;
}
let ratio = left as f32 / total as f32;
if (ratio - 0.5).abs() <= 0.1 {
break;
}
num_bins *= 2;
counts = doubled;
}
counts_to_records(&counts, min, max)
}
fn generate_numeric_plot(values: &[String]) -> Option<Vec<BinnedPlotRecord>> {
let mut numbers = Vec::new();
let mut nan = 0u32;
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for value in values {
if let Ok(number) = f32::from_str(value) {
numbers.push(number);
min = min.min(number);
max = max.max(number);
} else {
nan += 1;
}
}
if min == max {
return None;
}
let mut result = refined_bins(&numbers, min, max);
if nan > 0 {
result.push(BinnedPlotRecord {
bin_start: f32::NAN,
bin_end: f32::NAN,
value: nan,
});
}
Some(result)
}
fn generate_nominal_plot(value_counts: &HashMap<String, usize>) -> Option<Vec<PlotRecord>> {
let mut counts: HashMap<&str, u32> = HashMap::new();
for (value, count) in value_counts {
let key = if value.as_str().is_na() {
"NA"
} else {
value.as_str()
};
*counts.entry(key).or_insert(0) += *count as u32;
}
let mut plot_data = counts
.iter()
.map(|(key, value)| PlotRecord {
key: key.to_string(),
value: *value,
})
.collect_vec();
let unique_values = counts.values().unique().count();
if unique_values <= 1 {
return None;
};
if plot_data.len() > MAX_NOMINAL_BINS {
plot_data.sort_by_key(|b| std::cmp::Reverse(b.value));
plot_data = plot_data.into_iter().take(MAX_NOMINAL_BINS).collect();
}
Some(plot_data)
}
const MAX_NOMINAL_BINS: usize = 10;
const NUMERIC_BINS: usize = 20;
const MAX_BIN_REFINEMENT_ROUNDS: usize = 3;
#[derive(Serialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
struct PlotRecord {
key: String,
value: u32,
}
#[derive(Serialize, Debug, Clone, PartialEq)]
struct BinnedPlotRecord {
bin_start: f32,
bin_end: f32,
value: u32,
}
#[cfg(test)]
mod tests {
use crate::render::portable::plot::{generate_nominal_plot, PlotRecord};
use crate::spec::DatasetSpecs;
use crate::utils::column_store::DatasetSummary;
use std::str::FromStr;
#[test]
fn test_nominal_plot_generation() {
let dataset = DatasetSpecs {
path: "tests/data/uniform_datatypes.csv"
.to_string()
.parse()
.unwrap(),
separator: char::from_str(",").unwrap(),
header_rows: 1,
links: None,
offer_excel: false,
};
let summary = DatasetSummary::build(&dataset).unwrap();
let mut records = generate_nominal_plot(&summary.column_at(0).value_counts).unwrap();
records.sort_unstable();
let mut expected = vec![
PlotRecord {
key: String::from("George"),
value: 2,
},
PlotRecord {
key: String::from("Delia"),
value: 1,
},
PlotRecord {
key: String::from("Winnie"),
value: 1,
},
];
expected.sort_unstable();
assert_eq!(records, expected);
}
}