blob: fa25a8c0c4b1a85c8e7d2b31576e0c04c6f37ae0 [file] [log] [blame]
mod pie_plot;
use pie_plot::PiePlot;
use anyhow::{anyhow, Context};
use avm_stats::{FrameStatistic, StatSortMode};
use egui::Ui;
use log::warn;
use std::fmt::Write;
use wasm_bindgen::JsCast;
use web_sys::HtmlElement;
use crate::views::render_view::RenderView;
use crate::{app_state::AppState, stream::CurrentFrame};
pub struct StatsViewer;
// TODO(comc): Move to common location.
pub fn create_file_download(bytes: &[u8], file_name: &str) -> anyhow::Result<()> {
let uint8arr = js_sys::Uint8Array::new(&unsafe { js_sys::Uint8Array::view(bytes) }.into());
let array = js_sys::Array::new();
array.push(&uint8arr.buffer());
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(
&array,
web_sys::BlobPropertyBag::new().type_("application/octet-stream"),
)
.map_err(|_| anyhow!("Blob error"))?;
let download_url = web_sys::Url::create_object_url_with_blob(&blob).map_err(|_| anyhow!("Blob error"))?;
let window = web_sys::window().context("No window")?;
let document = window.document().context("No document")?;
let body = document.body().context("No body")?;
let dummy = document.create_element("a").map_err(|_| anyhow!("Element error"))?;
let dummy: HtmlElement = dummy
.dyn_into::<web_sys::HtmlElement>()
.map_err(|_| anyhow!("Element error"))?;
body.append_child(&dummy).map_err(|_| anyhow!("Element error"))?;
dummy
.set_attribute("href", &download_url)
.map_err(|_| anyhow!("Element error"))?;
dummy
.set_attribute("download", file_name)
.map_err(|_| anyhow!("Element error"))?;
dummy
.set_attribute("style", "display: none")
.map_err(|_| anyhow!("Element error"))?;
dummy.click();
web_sys::Url::revoke_object_url(&download_url).map_err(|_| anyhow!("Revoke URL error"))?;
Ok(())
}
// TODO(comc): Scroll wheel zoom, export stats, bar chart mode, table mode.
impl RenderView for StatsViewer {
fn title(&self) -> String {
"Frame Stats".into()
}
fn render(&self, ui: &mut Ui, state: &mut AppState) -> anyhow::Result<()> {
let Some(frame) = state.stream.current_frame() else {
// No frame loaded yet.
return Ok(());
};
let stats_settings = &mut state.settings.sharable.stats_settings;
let prev_state = (state.settings.sharable.selected_stat, stats_settings.clone());
let mut selected_stat = state.settings.sharable.selected_stat;
egui::ComboBox::from_label("Statistic")
.selected_text(selected_stat.name())
.show_ui(ui, |ui| {
for stat in &[
FrameStatistic::LumaModes,
FrameStatistic::ChromaModes,
FrameStatistic::BlockSizes,
FrameStatistic::Symbols,
FrameStatistic::PartitionSplit,
] {
ui.selectable_value(&mut selected_stat, *stat, stat.name());
}
});
ui.end_row();
state.settings.sharable.selected_stat = selected_stat;
if matches!(selected_stat, FrameStatistic::PartitionSplit) {
ui.horizontal(|ui| {
ui.label("Block sizes:")
.on_hover_text("Comma separated list of block sizes, e.g. \"64x64,128x128\".");
ui.text_edit_singleline(&mut stats_settings.partition_split_block_sizes);
ui.end_row();
});
}
let mut export_data = false;
ui.horizontal(|ui| {
// TODO(comc): Find a way to implement screenshot on web.
// if ui.button("Save plot").clicked() {
// ui.ctx().send_viewport_cmd(egui::ViewportCommand::Screenshot);
// }
if ui.button("Export data").clicked() {
export_data = true;
}
});
let mut sort_by = stats_settings.sort_by;
egui::ComboBox::from_label("Sort mode")
.selected_text(sort_by.name())
.show_ui(ui, |ui| {
for sort_mode in &[StatSortMode::ByName, StatSortMode::ByValue] {
ui.selectable_value(&mut sort_by, *sort_mode, sort_mode.name());
}
});
ui.end_row();
stats_settings.sort_by = sort_by;
let mut show_relative_total = stats_settings.show_relative_total;
ui.checkbox(&mut show_relative_total, "Show relative total");
ui.end_row();
stats_settings.show_relative_total = show_relative_total;
ui.horizontal(|ui| {
let mut apply_limit_count = stats_settings.apply_limit_count;
let mut limit_count = stats_settings.limit_count;
ui.checkbox(&mut apply_limit_count, "Limit Top N");
ui.add_enabled(apply_limit_count, egui::Slider::new(&mut limit_count, 1..=50));
stats_settings.apply_limit_count = apply_limit_count;
stats_settings.limit_count = limit_count;
});
ui.end_row();
ui.horizontal(|ui| {
let mut apply_limit_frac = stats_settings.apply_limit_frac;
let mut limit_frac = stats_settings.limit_frac * 100.0;
ui.checkbox(&mut apply_limit_frac, "Threshold Percent");
ui.add_enabled(
apply_limit_frac,
egui::Slider::new(&mut limit_frac, 0.0..=50.0).step_by(0.1),
);
stats_settings.apply_limit_frac = apply_limit_frac;
stats_settings.limit_frac = limit_frac / 100.0;
});
ui.end_row();
ui.horizontal(|ui| {
ui.label("Include:");
let mut include_filter_exact_match = stats_settings.include_filter_exact_match;
ui.checkbox(&mut include_filter_exact_match, "Exact match");
ui.text_edit_singleline(&mut stats_settings.include_filter);
stats_settings.include_filter_exact_match = include_filter_exact_match;
});
ui.end_row();
ui.horizontal(|ui| {
ui.label("Exclude:");
let mut exclude_filter_exact_match = stats_settings.exclude_filter_exact_match;
ui.checkbox(&mut exclude_filter_exact_match, "Exact match");
ui.text_edit_singleline(&mut stats_settings.exclude_filter);
stats_settings.exclude_filter_exact_match = exclude_filter_exact_match;
});
ui.end_row();
let decimal_precision = match selected_stat {
FrameStatistic::LumaModes => 0,
FrameStatistic::ChromaModes => 0,
FrameStatistic::BlockSizes => 0,
FrameStatistic::Symbols => 2,
FrameStatistic::PartitionSplit => 0,
};
let pie_plot = PiePlot {
decimal_precision,
..Default::default()
};
let changed = state.settings.sharable.selected_stat != prev_state.0 || *stats_settings != prev_state.1;
let calculate_data = changed || state.settings.cached_stat_data.is_none();
if calculate_data {
state.settings.cached_stat_data = Some(selected_stat.calculate(frame, stats_settings));
}
let _resp = pie_plot.show(ui, state.settings.cached_stat_data.as_ref().unwrap());
if export_data {
if let Some(stream) = &state.stream {
let file_name = format!(
"{}_frame_{:04}_{}.csv",
stream.stream_info.stream_name,
stream.current_frame_index,
selected_stat.name().replace(' ', "_")
);
let data = state.settings.cached_stat_data.as_ref().unwrap();
let total: f64 = data.iter().map(|sample| sample.value).sum();
let header = format!("{},Count,Percent\n", selected_stat.name());
let csv: String = data.iter().fold(String::new(), |mut output, sample| {
let _ = writeln!(output, "{},{},{}", sample.name, sample.value, sample.value / total);
output
});
let mut bytes: Vec<u8> = header.bytes().collect();
bytes.extend(csv.bytes());
if let Err(err) = create_file_download(&bytes, &file_name) {
warn!("Failed to create file download: {err:?}");
}
}
}
// TODO(comc): Find a way to implement screenshot on web.
// let screenshot = ui.ctx().input(|i| {
// for event in &i.raw.events {
// if let egui::Event::Screenshot { image, .. } = event {
// return Some(image.clone());
// }
// }
// None
// });
// if let Some(screenshot) = screenshot {
// let mut bytes = Vec::new();
// let png_encoder = image::codecs::png::PngEncoder::new(Cursor::new(&mut bytes));
// let pixels_per_point = ui.ctx().pixels_per_point();
// let plot = screenshot.region(&resp.response.rect, Some(pixels_per_point));
// match png_encoder.write_image(
// plot.as_raw(),
// plot.width() as u32,
// plot.height() as u32,
// image::ColorType::Rgba8,
// ) {
// Ok(_) => {
// if let Err(err) = create_file_download(&bytes, "test.png") {
// warn!("Failed to create file download: {err:?}");
// }
// }
// Err(err) => {
// warn!("Failed to encode PNG: {err:?}");
// }
// }
// }
Ok(())
}
}