use super::*;
#[derive(Debug, Clone)]
struct ColorbarMeasurementSpec {
vmin: f64,
vmax: f64,
value_scale: AxisScale,
label: Option<String>,
tick_font_size: f32,
label_font_size: f32,
show_log_subticks: bool,
}
impl Plot {
fn render_image_with_mode(&self, mode: RenderExecutionMode) -> Result<Image> {
self.render_image_with_mode_and_diagnostics(mode)
.map(|(image, _)| image)
}
pub(crate) fn validate_axis_scale_ranges_for_render(
&self,
series_list: &[PlotSeries],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Result<()> {
if !Self::needs_cartesian_axes_for_series(series_list) {
return Ok(());
}
self.layout
.x_scale
.validate_range(x_min, x_max)
.map_err(|message| {
PlottingError::InvalidInput(format!("Invalid x-axis range: {message}"))
})?;
self.layout
.y_scale
.validate_range(y_min, y_max)
.map_err(|message| {
PlottingError::InvalidInput(format!("Invalid y-axis range: {message}"))
})?;
Ok(())
}
pub(crate) fn scaled_x_pixel(
value: f64,
min: f64,
max: f64,
plot_area: tiny_skia::Rect,
scale: &AxisScale,
) -> f32 {
if (max - min).abs() < f64::EPSILON {
plot_area.left() + plot_area.width() * 0.5
} else {
let normalized = scale.normalized_position(value, min, max);
plot_area.left() + normalized as f32 * plot_area.width()
}
}
pub(crate) fn scaled_y_pixel(
value: f64,
min: f64,
max: f64,
plot_area: tiny_skia::Rect,
scale: &AxisScale,
) -> f32 {
if (max - min).abs() < f64::EPSILON {
plot_area.top() + plot_area.height() * 0.5
} else {
let normalized = scale.normalized_position(value, min, max);
plot_area.bottom() - normalized as f32 * plot_area.height()
}
}
pub(crate) fn minor_tick_values_for_scale(
major_ticks: &[f64],
min: f64,
max: f64,
scale: &AxisScale,
requested_count: usize,
) -> Vec<f64> {
let (range_min, range_max) = if min <= max { (min, max) } else { (max, min) };
let mut ticks = match scale {
AxisScale::Log => Self::log_minor_tick_values_for_range(range_min, range_max),
AxisScale::Linear | AxisScale::SymLog { .. } => {
crate::axes::generate_minor_ticks(major_ticks, requested_count)
}
};
ticks.retain(|tick| {
tick.is_finite()
&& *tick >= range_min
&& *tick <= range_max
&& !major_ticks
.iter()
.any(|major| Self::tick_values_overlap(*major, *tick))
});
ticks.sort_by(|left, right| left.partial_cmp(right).unwrap());
ticks.dedup_by(|left, right| Self::tick_values_overlap(*left, *right));
ticks
}
fn log_minor_tick_values_for_range(min: f64, max: f64) -> Vec<f64> {
if min <= 0.0 || max <= 0.0 || min >= max {
return Vec::new();
}
let min_exp = min.log10().floor() as i32;
let max_exp = max.log10().ceil() as i32;
let mut ticks = Vec::new();
for exp in min_exp..=max_exp {
let decade = 10.0_f64.powi(exp);
for multiplier in 2..=9 {
let tick = decade * multiplier as f64;
if tick >= min && tick <= max {
ticks.push(tick);
}
}
}
ticks
}
fn tick_values_overlap(left: f64, right: f64) -> bool {
(left - right).abs() <= left.abs().max(right.abs()).max(1.0) * 1e-10
}
pub(crate) fn grid_tick_pixels(
major_pixels: &[f32],
minor_pixels: &[f32],
mode: &GridMode,
) -> Vec<f32> {
match mode {
GridMode::MajorOnly => major_pixels.to_vec(),
GridMode::MinorOnly => minor_pixels.to_vec(),
GridMode::Both => major_pixels
.iter()
.chain(minor_pixels.iter())
.copied()
.collect(),
}
}
pub(super) fn render_image_with_mode_and_series_renderer<F>(
&self,
mode: RenderExecutionMode,
draw_series: F,
) -> Result<(Image, RenderDiagnostics)>
where
F: FnOnce(
&Plot,
&[PlotSeries],
&mut SkiaRenderer,
tiny_skia::Rect,
f64,
f64,
f64,
f64,
RenderScale,
RenderExecutionMode,
) -> Result<()>,
{
self.validate_runtime_environment()?;
let snapshot_series = self.snapshot_series(0.0);
if !snapshot_series.is_empty() {
Self::validate_series_list(&snapshot_series)?;
}
let total_points = Self::calculate_total_points_for_series(&snapshot_series);
let has_mixed_coordinates = Self::has_mixed_coordinate_series(&snapshot_series);
const LARGE_DATASET_THRESHOLD: usize = 1_000_000;
if total_points > LARGE_DATASET_THRESHOLD {
log::warn!(
"Rendering {} points (>1M). DataShader optimization is available for large datasets.",
total_points
);
}
#[cfg(feature = "parallel")]
{
if mode.allows_parallel() {
let series_count = snapshot_series.len();
let parallel_safe_for_markers = snapshot_series.iter().all(|series| match &series
.series_type
{
SeriesType::Line { .. } => {
series.marker_style.is_none()
&& series.x_errors.is_none()
&& series.y_errors.is_none()
}
SeriesType::Heatmap { .. } => false,
_ => true,
});
if !has_mixed_coordinates
&& parallel_safe_for_markers
&& self
.render
.parallel_renderer
.should_use_parallel(series_count, total_points)
{
let image = self.render_with_parallel()?;
let diagnostics = RenderDiagnostics {
render_mode: match mode {
RenderExecutionMode::Reference => "reference",
RenderExecutionMode::Optimized => "optimized",
},
used_parallel: true,
..RenderDiagnostics::default()
};
return Ok((image, diagnostics));
}
}
}
let (scaled_width, scaled_height) = self.config_canvas_size();
let mut renderer =
SkiaRenderer::new(scaled_width, scaled_height, self.display.theme.clone())?;
renderer.set_text_engine_mode(self.display.text_engine);
renderer.set_render_mode_diagnostics(match mode {
RenderExecutionMode::Reference => "reference",
RenderExecutionMode::Optimized => "optimized",
});
let render_scale = self.render_scale();
let dpi = render_scale.dpi();
renderer.set_render_scale(render_scale);
let (x_min, x_max, y_min, y_max) =
self.effective_main_panel_bounds_for_series(&snapshot_series)?;
self.validate_axis_scale_ranges_for_render(&snapshot_series, x_min, x_max, y_min, y_max)?;
let bar_categories: Option<Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
if let SeriesType::Bar { categories, .. } = &s.series_type {
Some(categories.clone())
} else {
None
}
});
let violin_data: Vec<(String, f64)> = self
.series_mgr
.series
.iter()
.filter_map(|s| {
if let SeriesType::Violin { data } = &s.series_type {
data.config
.category
.clone()
.map(|cat| (cat, data.config.x_position))
} else {
None
}
})
.collect();
let (violin_categories, violin_positions): (Vec<String>, Vec<f64>) =
violin_data.into_iter().unzip();
let is_violin_categorical = !violin_categories.is_empty();
let bar_categories = bar_categories.or_else(|| {
if is_violin_categorical {
Some(violin_categories.clone())
} else {
None
}
});
let content = self.create_plot_content(y_min, y_max);
let (layout, x_ticks, y_ticks) = self.compute_layout_with_configured_ticks(
&renderer,
(scaled_width, scaled_height),
&content,
dpi,
x_min,
x_max,
y_min,
y_max,
)?;
let plot_area = Self::plot_area_from_layout(&layout)?;
let x_tick_pixels: Vec<f32> = x_ticks
.iter()
.map(|&tick| Self::scaled_x_pixel(tick, x_min, x_max, plot_area, &self.layout.x_scale))
.collect();
let y_tick_pixels: Vec<f32> = y_ticks
.iter()
.map(|&tick| Self::scaled_y_pixel(tick, y_min, y_max, plot_area, &self.layout.y_scale))
.collect();
let x_minor_ticks = Self::minor_tick_values_for_scale(
&x_ticks,
x_min,
x_max,
&self.layout.x_scale,
self.layout.tick_config.minor_ticks_x,
);
let y_minor_ticks = Self::minor_tick_values_for_scale(
&y_ticks,
y_min,
y_max,
&self.layout.y_scale,
self.layout.tick_config.minor_ticks_y,
);
let x_minor_tick_pixels: Vec<f32> = x_minor_ticks
.iter()
.map(|&tick| Self::scaled_x_pixel(tick, x_min, x_max, plot_area, &self.layout.x_scale))
.collect();
let y_minor_tick_pixels: Vec<f32> = y_minor_ticks
.iter()
.map(|&tick| Self::scaled_y_pixel(tick, y_min, y_max, plot_area, &self.layout.y_scale))
.collect();
let draw_axes = Self::needs_cartesian_axes_for_series(&snapshot_series);
if self.layout.grid_style.visible && draw_axes {
let grid_color = self.layout.grid_style.effective_color();
let grid_width_px = self.line_width_px(self.layout.grid_style.line_width);
let grid_x_pixels = Self::grid_tick_pixels(
&x_tick_pixels,
&x_minor_tick_pixels,
&self.layout.tick_config.grid_mode,
);
let grid_y_pixels = Self::grid_tick_pixels(
&y_tick_pixels,
&y_minor_tick_pixels,
&self.layout.tick_config.grid_mode,
);
renderer.draw_grid(
&grid_x_pixels,
&grid_y_pixels,
plot_area,
grid_color,
self.layout.grid_style.line_style.clone(),
grid_width_px,
)?;
}
let categorical_x_tick_pixels = Self::categorical_x_tick_pixels(
plot_area,
x_min,
x_max,
bar_categories.as_ref().map(Vec::len),
&violin_positions,
);
let draw_ticks = draw_axes && self.layout.tick_config.enabled;
if draw_ticks {
let x_axis_ticks = categorical_x_tick_pixels
.as_deref()
.unwrap_or(x_tick_pixels.as_slice());
let x_axis_minor_ticks = if categorical_x_tick_pixels.is_some() {
&[][..]
} else {
x_minor_tick_pixels.as_slice()
};
renderer.draw_axes_with_minor_ticks(
plot_area,
x_axis_ticks,
&y_tick_pixels,
x_axis_minor_ticks,
&y_minor_tick_pixels,
&self.layout.tick_config.direction,
&self.layout.tick_config.sides,
self.display.theme.foreground,
)?;
}
let tick_size_px = pt_to_px(self.display.config.typography.tick_size(), dpi);
if draw_axes && is_violin_categorical {
renderer.draw_axis_labels_at_categorical_violin(
&layout.plot_area,
&violin_categories,
&violin_positions,
x_min,
x_max,
y_min,
y_max,
&y_ticks,
layout.xtick_baseline_y,
layout.ytick_right_x,
tick_size_px,
self.display.theme.foreground,
dpi,
self.layout.tick_config.enabled,
!draw_ticks,
)?;
} else if draw_axes {
if let Some(ref categories) = bar_categories {
renderer.draw_axis_labels_at_categorical(
&layout.plot_area,
categories,
x_min,
x_max,
y_min,
y_max,
&y_ticks,
layout.xtick_baseline_y,
layout.ytick_right_x,
tick_size_px,
self.display.theme.foreground,
dpi,
self.layout.tick_config.enabled,
!draw_ticks,
)?;
} else {
renderer.draw_axis_labels_at_scaled(
&layout.plot_area,
x_min,
x_max,
y_min,
y_max,
&x_ticks,
&y_ticks,
layout.xtick_baseline_y,
layout.ytick_right_x,
tick_size_px,
self.display.theme.foreground,
dpi,
self.layout.tick_config.enabled,
!draw_ticks,
&self.layout.x_scale,
&self.layout.y_scale,
)?;
}
}
if let Some(ref pos) = layout.title_pos {
if let Some(ref title) = self.display.title {
let title_str = title.resolve(0.0);
renderer.draw_title_at(pos, &title_str, self.display.theme.foreground)?;
}
}
if let Some(ref pos) = layout.xlabel_pos {
if let Some(ref xlabel) = self.display.xlabel {
let xlabel_str = xlabel.resolve(0.0);
renderer.draw_xlabel_at(pos, &xlabel_str, self.display.theme.foreground)?;
}
}
if let Some(ref pos) = layout.ylabel_pos {
if let Some(ref ylabel) = self.display.ylabel {
let ylabel_str = ylabel.resolve(0.0);
renderer.draw_ylabel_at(pos, &ylabel_str, self.display.theme.foreground)?;
}
}
draw_series(
self,
&snapshot_series,
&mut renderer,
plot_area,
x_min,
x_max,
y_min,
y_max,
render_scale,
mode,
)?;
let legend_items = self.collect_legend_items();
if !legend_items.is_empty() && self.layout.legend.enabled {
let legend = self
.layout
.legend
.to_legend(self.display.config.typography.legend_size());
renderer.draw_legend_full(&legend_items, &legend, plot_area, None)?;
}
let diagnostics = renderer.render_diagnostics().clone();
Ok((renderer.into_image(), diagnostics))
}
fn render_image_with_mode_and_diagnostics(
&self,
mode: RenderExecutionMode,
) -> Result<(Image, RenderDiagnostics)> {
self.render_image_with_mode_and_series_renderer(
mode,
|plot,
snapshot_series,
renderer,
plot_area,
x_min,
x_max,
y_min,
y_max,
render_scale,
mode| {
if !plot.render_series_collection_auto_datashader(
snapshot_series,
renderer,
plot_area,
x_min,
x_max,
y_min,
y_max,
render_scale,
mode,
)? {
plot.render_series_collection_normal(
snapshot_series,
renderer,
plot_area,
x_min,
x_max,
y_min,
y_max,
render_scale,
mode,
)?;
}
Ok(())
},
)
}
pub fn render(&self) -> Result<Image> {
if self.is_reactive() {
let resolved = self.resolved_plot(0.0);
let image = resolved.render()?;
self.mark_reactive_sources_rendered();
return Ok(image);
}
self.render_image_with_mode(RenderExecutionMode::Reference)
}
#[cfg(test)]
pub(super) fn render_optimized_for_test(&self) -> Result<Image> {
if self.is_reactive() {
return self.resolved_plot(0.0).render_optimized_for_test();
}
self.render_image_with_mode(RenderExecutionMode::Optimized)
}
#[cfg(test)]
pub(super) fn render_optimized_for_test_with_diagnostics(
&self,
) -> Result<(Image, RenderDiagnostics)> {
if self.is_reactive() {
return self
.resolved_plot(0.0)
.render_optimized_for_test_with_diagnostics();
}
self.render_image_with_mode_and_diagnostics(RenderExecutionMode::Optimized)
}
pub fn render_at(&self, time: f64) -> Result<Image> {
if !self.is_reactive() {
return self.render();
}
let resolved = self.resolved_plot(time);
let image = resolved.render()?;
self.mark_reactive_sources_rendered();
Ok(image)
}
pub fn render_png_bytes(&self) -> Result<Vec<u8>> {
#[cfg(not(target_arch = "wasm32"))]
{
let reactive = self.is_reactive();
let result = self
.save_png_bytes_with_backend()
.map(|(png_bytes, _, _)| png_bytes);
if result.is_ok() && reactive {
self.mark_reactive_sources_rendered();
}
result
}
#[cfg(target_arch = "wasm32")]
{
self.render()?.encode_png()
}
}
#[cfg(not(target_arch = "wasm32"))]
#[doc(hidden)]
pub fn benchmark_render_png_bytes_with_diagnostics(
&self,
) -> Result<(Vec<u8>, RenderDiagnostics)> {
let reactive = self.is_reactive();
let result = self
.save_png_bytes_with_backend()
.map(|(png_bytes, _, diagnostics)| (png_bytes, diagnostics));
if result.is_ok() && reactive {
self.mark_reactive_sources_rendered();
}
result
}
pub fn is_reactive(&self) -> bool {
self.display
.title
.as_ref()
.is_some_and(|title| title.is_reactive())
|| self
.display
.xlabel
.as_ref()
.is_some_and(|label| label.is_reactive())
|| self
.display
.ylabel
.as_ref()
.is_some_and(|label| label.is_reactive())
|| self.series_mgr.series.iter().any(PlotSeries::is_reactive)
}
pub fn render_to_renderer(&self, renderer: &mut SkiaRenderer, dpi: f32) -> Result<()> {
if self.is_reactive() {
return self.resolved_plot(0.0).render_to_renderer(renderer, dpi);
}
if let Some(err) = self.pending_ingestion_error() {
return Err(err);
}
let image = self
.clone()
.dpi(dpi.round().max(1.0) as u32)
.set_output_pixels(renderer.width(), renderer.height())
.render_image_with_mode(RenderExecutionMode::Reference)?;
renderer.draw_subplot(image, 0, 0)
}
pub(super) fn create_plot_content_at_time(
&self,
y_min: f64,
y_max: f64,
time: f64,
) -> PlotContent {
let y_ticks = generate_ticks(y_min, y_max, 6);
let max_ytick_chars = y_ticks
.iter()
.map(|&v| {
if v.abs() < 0.001 {
1 } else if v.abs() > 1000.0 {
format!("{:.0e}", v).len()
} else {
format!("{:.1}", v).len()
}
})
.max()
.unwrap_or(5);
PlotContent {
title: self.display.title.as_ref().map(|t| t.resolve(time)),
xlabel: self.display.xlabel.as_ref().map(|t| t.resolve(time)),
ylabel: self.display.ylabel.as_ref().map(|t| t.resolve(time)),
show_tick_labels: self.layout.tick_config.enabled && self.needs_cartesian_axes(),
max_ytick_chars,
max_xtick_chars: 0, }
}
pub(super) fn create_plot_content(&self, y_min: f64, y_max: f64) -> PlotContent {
self.create_plot_content_at_time(y_min, y_max, 0.0)
}
fn colorbar_measurement_spec(&self) -> Option<ColorbarMeasurementSpec> {
self.series_mgr
.series
.iter()
.find_map(|series| match &series.series_type {
SeriesType::Heatmap { data } if data.config.colorbar => {
Some(ColorbarMeasurementSpec {
vmin: data.vmin,
vmax: data.vmax,
value_scale: data.config.value_scale.clone(),
label: data.config.colorbar_label.clone(),
tick_font_size: data.config.colorbar_tick_font_size,
label_font_size: data.config.colorbar_label_font_size,
show_log_subticks: data.config.colorbar_log_subticks,
})
}
SeriesType::Contour { data } if data.config.colorbar => {
let (vmin, vmax) = if data.levels.is_empty() {
(0.0, 1.0)
} else {
(
data.levels.first().copied().unwrap_or(0.0),
data.levels.last().copied().unwrap_or(1.0),
)
};
Some(ColorbarMeasurementSpec {
vmin,
vmax,
value_scale: AxisScale::Linear,
label: data.config.colorbar_label.clone(),
tick_font_size: data.config.colorbar_tick_font_size,
label_font_size: data.config.colorbar_label_font_size,
show_log_subticks: false,
})
}
_ => None,
})
}
fn measure_colorbar_right_margin(
&self,
renderer: &SkiaRenderer,
spec: &ColorbarMeasurementSpec,
) -> Result<f32> {
let ticks = crate::render::skia::compute_colorbar_ticks(
spec.vmin,
spec.vmax,
&spec.value_scale,
spec.show_log_subticks,
);
let max_label_width =
Self::measure_tick_label_extent(renderer, &ticks.major_labels, spec.tick_font_size)?
.map(|(width, _)| width)
.unwrap_or(0.0);
let rotated_label_width = if let Some(label) = spec.label.as_deref() {
renderer.measure_text(label, spec.label_font_size)?.1
} else {
0.0
};
let layout = crate::render::skia::compute_colorbar_layout_metrics(
COLORBAR_WIDTH_PX,
spec.tick_font_size,
max_label_width,
spec.label.as_ref().map(|_| rotated_label_width),
);
let outer_padding = spec.tick_font_size.max(4.0) * 0.5;
Ok(COLORBAR_MARGIN_PX + layout.total_extent + outer_padding)
}
pub(super) fn measure_layout_text(
&self,
renderer: &SkiaRenderer,
content: &PlotContent,
dpi: f32,
) -> Result<Option<MeasuredDimensions>> {
let render_scale = RenderScale::new(dpi);
let title_size_px =
render_scale.points_to_pixels(self.display.config.typography.title_size());
let label_size_px =
render_scale.points_to_pixels(self.display.config.typography.label_size());
let mut measurements = MeasuredDimensions::default();
if let Some(title) = content.title.as_deref() {
measurements.title = Some(renderer.measure_text(title, title_size_px)?);
}
if let Some(xlabel) = content.xlabel.as_deref() {
measurements.xlabel = Some(renderer.measure_text(xlabel, label_size_px)?);
}
if let Some(ylabel) = content.ylabel.as_deref() {
measurements.ylabel = Some(renderer.measure_text(ylabel, label_size_px)?);
}
Ok(Some(measurements))
}
pub(super) fn measure_layout_text_with_ticks(
&self,
renderer: &SkiaRenderer,
content: &PlotContent,
dpi: f32,
x_tick_labels: &[String],
y_tick_labels: &[String],
) -> Result<Option<MeasuredDimensions>> {
let tick_size_px =
RenderScale::new(dpi).points_to_pixels(self.display.config.typography.tick_size());
let mut measurements = self
.measure_layout_text(renderer, content, dpi)?
.unwrap_or_default();
if content.show_tick_labels {
measurements.xtick =
Self::measure_tick_label_extent(renderer, x_tick_labels, tick_size_px)?;
measurements.ytick =
Self::measure_tick_label_extent(renderer, y_tick_labels, tick_size_px)?;
}
if let Some(spec) = self.colorbar_measurement_spec() {
measurements.right_margin = Some(self.measure_colorbar_right_margin(renderer, &spec)?);
}
Ok(Some(measurements))
}
pub(super) fn compute_layout_from_measurements(
&self,
canvas_size: (u32, u32),
content: &PlotContent,
dpi: f32,
measurements: Option<&MeasuredDimensions>,
) -> PlotLayout {
match &self.display.config.margins {
MarginConfig::ContentDriven {
edge_buffer,
center_plot,
} => LayoutCalculator::new(LayoutConfig {
edge_buffer_pt: *edge_buffer,
center_plot: *center_plot,
..Default::default()
})
.compute(
canvas_size,
content,
&self.display.config.typography,
&self.display.config.spacing,
dpi,
measurements,
),
MarginConfig::Fixed { .. }
| MarginConfig::Auto { .. }
| MarginConfig::Proportional { .. } => {
self.compute_layout_with_explicit_margins(canvas_size, content, dpi, measurements)
}
}
}
fn compute_layout_with_explicit_margins(
&self,
canvas_size: (u32, u32),
content: &PlotContent,
dpi: f32,
measurements: Option<&MeasuredDimensions>,
) -> PlotLayout {
let render_scale = RenderScale::from_canvas_size(canvas_size.0, canvas_size.1, dpi);
let typography = &self.display.config.typography;
let spacing = &self.display.config.spacing;
let title_pad = render_scale.points_to_pixels(spacing.title_pad);
let label_pad = render_scale.points_to_pixels(spacing.label_pad);
let tick_pad_px = render_scale.points_to_pixels(spacing.tick_pad);
let title_size_px = render_scale.points_to_pixels(typography.title_size());
let label_size_px = render_scale.points_to_pixels(typography.label_size());
let tick_size_px = render_scale.points_to_pixels(typography.tick_size());
let measured_title = measurements.and_then(|m| m.title);
let measured_xlabel = measurements.and_then(|m| m.xlabel);
let measured_ylabel = measurements.and_then(|m| m.ylabel);
let measured_xtick = measurements.and_then(|m| m.xtick);
let measured_ytick = measurements.and_then(|m| m.ytick);
let measured_right_margin = measurements.and_then(|m| m.right_margin);
let title_height = if content.title.is_some() {
measured_title
.map(|(_, height)| height)
.unwrap_or_else(|| crate::core::layout::estimate_text_height(title_size_px))
} else {
0.0
};
let xlabel_height = if content.xlabel.is_some() {
measured_xlabel
.map(|(_, height)| height)
.unwrap_or_else(|| crate::core::layout::estimate_text_height(label_size_px))
} else {
0.0
};
let ylabel_width = if content.ylabel.is_some() {
measured_ylabel
.map(|(_, height)| height)
.unwrap_or_else(|| crate::core::layout::estimate_text_height(label_size_px))
} else {
0.0
};
let (xtick_height, ytick_width, tick_pad) = if content.show_tick_labels {
(
measured_xtick
.map(|(_, height)| height)
.unwrap_or_else(|| crate::core::layout::estimate_text_height(tick_size_px)),
measured_ytick.map(|(width, _)| width).unwrap_or_else(|| {
crate::core::layout::estimate_tick_label_width(
content.max_ytick_chars.max(5),
tick_size_px,
)
}),
tick_pad_px,
)
} else {
(0.0, 0.0, 0.0)
};
let computed_margins = self.display.config.compute_margins(
content.title.is_some(),
content.xlabel.is_some(),
content.ylabel.is_some(),
);
let plot_area_rect =
calculate_plot_area_config(canvas_size.0, canvas_size.1, &computed_margins, dpi);
let canvas_width = canvas_size.0 as f32;
let canvas_height = canvas_size.1 as f32;
let configured_right_margin = canvas_width - plot_area_rect.right();
let effective_right_margin =
configured_right_margin.max(measured_right_margin.unwrap_or(0.0));
let margins = crate::core::layout::ComputedMarginsPixels {
left: plot_area_rect.left(),
right: effective_right_margin,
top: plot_area_rect.top(),
bottom: canvas_height - plot_area_rect.bottom(),
};
let plot_area = crate::core::layout::LayoutRect {
left: plot_area_rect.left(),
top: plot_area_rect.top(),
right: canvas_width - effective_right_margin,
bottom: plot_area_rect.bottom(),
};
let top_outer_gap = if content.title.is_some() {
(margins.top - title_height - title_pad).max(0.0)
} else {
0.0
};
let bottom_content_height = tick_pad
+ xtick_height
+ if content.xlabel.is_some() {
label_pad + xlabel_height
} else {
0.0
};
let bottom_outer_gap = (margins.bottom - bottom_content_height).max(0.0);
let left_content_width = ytick_width
+ tick_pad
+ if content.ylabel.is_some() {
label_pad + ylabel_width
} else {
0.0
};
let left_outer_gap = (margins.left - left_content_width).max(0.0);
PlotLayout {
plot_area,
title_pos: content
.title
.as_ref()
.map(|_| crate::core::layout::TextPosition {
x: plot_area.center_x(),
y: top_outer_gap,
size: title_size_px,
}),
xlabel_pos: content
.xlabel
.as_ref()
.map(|_| crate::core::layout::TextPosition {
x: plot_area.center_x(),
y: canvas_height - bottom_outer_gap - xlabel_height,
size: label_size_px,
}),
ylabel_pos: content
.ylabel
.as_ref()
.map(|_| crate::core::layout::TextPosition {
x: left_outer_gap + ylabel_width / 2.0,
y: plot_area.center_y(),
size: label_size_px,
}),
xtick_baseline_y: plot_area.bottom + tick_pad,
ytick_right_x: plot_area.left - tick_pad,
margins,
}
}
pub(super) fn plot_area_from_layout(layout: &PlotLayout) -> Result<tiny_skia::Rect> {
tiny_skia::Rect::from_ltrb(
layout.plot_area.left,
layout.plot_area.top,
layout.plot_area.right,
layout.plot_area.bottom,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid plot area from layout".to_string(),
position: None,
})
}
pub(super) fn configured_major_ticks(
&self,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> (Vec<f64>, Vec<f64>) {
(
crate::axes::generate_ticks_for_scale(
x_min,
x_max,
self.layout.tick_config.major_ticks_x,
&self.layout.x_scale,
),
crate::axes::generate_ticks_for_scale(
y_min,
y_max,
self.layout.tick_config.major_ticks_y,
&self.layout.y_scale,
),
)
}
pub(super) fn compute_layout_with_configured_ticks(
&self,
renderer: &SkiaRenderer,
canvas_size: (u32, u32),
content: &PlotContent,
dpi: f32,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
) -> Result<(PlotLayout, Vec<f64>, Vec<f64>)> {
if !content.show_tick_labels {
let measurements =
self.measure_layout_text_with_ticks(renderer, content, dpi, &[], &[])?;
let layout = self.compute_layout_from_measurements(
canvas_size,
content,
dpi,
measurements.as_ref(),
);
return Ok((layout, Vec::new(), Vec::new()));
}
let (x_ticks, y_ticks) = self.configured_major_ticks(x_min, x_max, y_min, y_max);
let x_labels = crate::render::skia::format_tick_labels(&x_ticks);
let y_labels = crate::render::skia::format_tick_labels(&y_ticks);
let measurements =
self.measure_layout_text_with_ticks(renderer, content, dpi, &x_labels, &y_labels)?;
let layout =
self.compute_layout_from_measurements(canvas_size, content, dpi, measurements.as_ref());
Ok((layout, x_ticks, y_ticks))
}
fn measure_tick_label_extent(
renderer: &SkiaRenderer,
labels: &[String],
tick_size_px: f32,
) -> Result<Option<(f32, f32)>> {
let mut max_width: f32 = 0.0;
let mut max_height: f32 = 0.0;
for label in labels {
let (width, height) = renderer.measure_label_text(label, tick_size_px)?;
max_width = max_width.max(width);
max_height = max_height.max(height);
}
if labels.is_empty() {
Ok(None)
} else {
Ok(Some((max_width, max_height)))
}
}
pub(super) fn render_with_datashader(&self, series_list: &[PlotSeries]) -> Result<Image> {
let mut x_values = Vec::new();
let mut y_values = Vec::new();
for series in series_list {
match &series.series_type {
SeriesType::Line { x_data, y_data } | SeriesType::Scatter { x_data, y_data } => {
let x_data = x_data.resolve_cow(0.0);
let y_data = y_data.resolve_cow(0.0);
for (&x, &y) in x_data.iter().zip(y_data.iter()) {
if x.is_finite() && y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::ErrorBars { x_data, y_data, .. }
| SeriesType::ErrorBarsXY { x_data, y_data, .. } => {
let x_data = x_data.resolve_cow(0.0);
let y_data = y_data.resolve_cow(0.0);
for (&x, &y) in x_data.iter().zip(y_data.iter()) {
if x.is_finite() && y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::Bar { values, .. } => {
let values = values.resolve_cow(0.0);
for (i, &value) in values.iter().enumerate() {
if value.is_finite() {
x_values.push(i as f64);
y_values.push(value);
}
}
}
SeriesType::Heatmap { data } => {
for (row, row_values) in data.values.iter().enumerate() {
for (col, &value) in row_values.iter().enumerate() {
if value.is_finite() {
x_values.push(col as f64);
y_values.push(row as f64);
}
}
}
}
SeriesType::Histogram { .. } => {
if let Ok(hist_data) = series.series_type.histogram_data_at(0.0) {
for (i, &count) in hist_data.counts.iter().enumerate() {
if count > 0.0 {
let x_center =
(hist_data.bin_edges[i] + hist_data.bin_edges[i + 1]) / 2.0;
x_values.push(x_center);
y_values.push(count);
}
}
}
}
SeriesType::BoxPlot { data, .. } => {
if data.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
}
SeriesType::Kde { data } => {
for (&x, &y) in data.x.iter().zip(data.y.iter()) {
if x.is_finite() && y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::Ecdf { data } => {
for (&x, &y) in data.x.iter().zip(data.y.iter()) {
if x.is_finite() && y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::Violin { data } => {
for &y in &data.kde.x {
let x = 0.5; if y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::Boxen { data } => {
for boxen_box in &data.boxes {
let rect = crate::plots::distribution::boxen_rect(
boxen_box,
0.5,
data.config.orient,
);
for (x, y) in rect {
if x.is_finite() && y.is_finite() {
x_values.push(x);
y_values.push(y);
}
}
}
}
SeriesType::Contour { data } => {
for level in &data.lines {
for &(x1, y1, x2, y2) in &level.segments {
if x1.is_finite() && y1.is_finite() {
x_values.push(x1);
y_values.push(y1);
}
if x2.is_finite() && y2.is_finite() {
x_values.push(x2);
y_values.push(y2);
}
}
}
}
SeriesType::Pie { .. } => {
x_values.push(0.5);
y_values.push(0.5);
}
SeriesType::Radar { data } => {
for series_data in &data.series {
for &(x, y) in &series_data.polygon {
x_values.push(x);
y_values.push(y);
}
}
}
SeriesType::Polar { data } => {
for point in &data.points {
x_values.push(point.x);
y_values.push(point.y);
}
}
}
}
if x_values.is_empty() {
return Err(PlottingError::EmptyDataSet);
}
let (canvas_width, canvas_height) = self.config_canvas_size();
let mut datashader =
DataShader::with_canvas_size(canvas_width as usize, canvas_height as usize);
let (x_min, x_max, y_min, y_max) = self.effective_data_bounds_for_series(series_list)?;
datashader.aggregate_with_bounds(&x_values, &y_values, x_min, x_max, y_min, y_max)?;
let ds_image = datashader.render();
let image = Image {
width: ds_image.width as u32,
height: ds_image.height as u32,
pixels: ds_image.pixels,
};
Ok(image)
}
pub fn auto_optimize(self) -> Self {
self.auto_optimize_with_extra_points(0)
}
pub(crate) fn auto_optimize_with_extra_points(mut self, extra_points: usize) -> Self {
if self.render.backend.is_some() {
self.render.auto_optimized = true;
return self;
}
let series_points: usize = self
.series_mgr
.series
.iter()
.map(|s| match &s.series_type {
SeriesType::Line { x_data, .. } => x_data.len(),
SeriesType::Scatter { x_data, .. } => x_data.len(),
SeriesType::Bar { values, .. } => values.len(),
SeriesType::Histogram { data, .. } => data.len(),
SeriesType::BoxPlot { data, .. } => data.len(),
SeriesType::ErrorBars { x_data, .. } => x_data.len(),
SeriesType::ErrorBarsXY { x_data, .. } => x_data.len(),
SeriesType::Heatmap { data } => data.n_rows * data.n_cols,
SeriesType::Kde { data } => data.x.len(),
SeriesType::Ecdf { data } => data.x.len(),
SeriesType::Violin { data } => data.data.len(),
SeriesType::Boxen { data } => data.boxes.len() * 4,
SeriesType::Contour { data } => data.x.len() * data.y.len(),
SeriesType::Pie { data } => data.values.len(),
SeriesType::Radar { data } => data.series.iter().map(|s| s.values.len()).sum(),
SeriesType::Polar { data } => data.points.len(),
})
.sum();
let total_points = series_points + extra_points;
let selected_backend =
if Self::has_mixed_coordinate_series(&self.series_mgr.series) || total_points < 1000 {
BackendType::Skia
} else if total_points < 100_000 {
#[cfg(feature = "parallel")]
{
BackendType::Parallel
}
#[cfg(not(feature = "parallel"))]
{
BackendType::Skia
}
} else {
#[cfg(feature = "gpu")]
{
BackendType::GPU
}
#[cfg(not(feature = "gpu"))]
{
BackendType::DataShader
}
};
self.render.backend = Some(selected_backend);
self.render.auto_optimized = true;
self
}
pub fn backend(mut self, backend: BackendType) -> Self {
self.render.backend = Some(backend);
self
}
#[cfg(feature = "gpu")]
pub fn gpu(mut self, enabled: bool) -> Self {
self.render.enable_gpu = enabled;
if enabled {
self.render.backend = Some(BackendType::GPU);
}
self
}
pub fn get_backend_name(&self) -> &'static str {
match self.render.backend {
Some(BackendType::Skia) => "skia",
Some(BackendType::Parallel) => "parallel",
Some(BackendType::GPU) => "gpu",
Some(BackendType::DataShader) => "datashader",
None => "auto",
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save<P: AsRef<Path>>(self, path: P) -> Result<()> {
let reactive = self.is_reactive();
let (png_bytes, _, _) = self.save_png_bytes_with_backend()?;
let result = crate::export::write_bytes_atomic(path, &png_bytes);
if result.is_ok() && reactive {
self.mark_reactive_sources_rendered();
}
result
}
#[cfg(not(target_arch = "wasm32"))]
#[doc(hidden)]
pub fn benchmark_save_png_bytes(&self) -> Result<(Vec<u8>, &'static str)> {
self.benchmark_save_png_bytes_with_diagnostics()
.map(|(png, backend, _)| (png, backend))
}
#[cfg(not(target_arch = "wasm32"))]
#[doc(hidden)]
pub fn benchmark_save_png_bytes_with_diagnostics(
&self,
) -> Result<(Vec<u8>, &'static str, RenderDiagnostics)> {
let reactive = self.is_reactive();
let result = self.save_png_bytes_with_backend();
if result.is_ok() && reactive {
self.mark_reactive_sources_rendered();
}
result
}
#[cfg(not(target_arch = "wasm32"))]
fn save_png_bytes_with_backend(&self) -> Result<(Vec<u8>, &'static str, RenderDiagnostics)> {
if self.is_reactive() {
return self.resolved_plot(0.0).save_png_bytes_with_backend();
}
let (image, diagnostics) =
self.render_image_with_mode_and_diagnostics(RenderExecutionMode::Reference)?;
Ok((image.encode_png()?, "skia", diagnostics))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn save_with_size<P: AsRef<Path>>(
mut self,
path: P,
width: u32,
height: u32,
) -> Result<()> {
self = self.set_output_pixels(width, height);
self.save(path)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn export_svg<P: AsRef<Path>>(self, path: P) -> Result<()> {
let svg_content = self.render_to_svg()?;
crate::export::write_bytes_atomic(path, svg_content.as_bytes())
}
pub fn render_to_svg(&self) -> Result<String> {
if self.is_reactive() {
let svg = self.resolved_plot(0.0).render_to_svg()?;
self.mark_reactive_sources_rendered();
return Ok(svg);
}
use crate::axes::TickLayout;
use crate::export::SvgRenderer;
self.validate_runtime_environment()?;
let snapshot_series = self.snapshot_series(0.0);
if !snapshot_series.is_empty() {
Self::validate_series_list(&snapshot_series)?;
}
let (width_px, height_px) = self.config_canvas_size();
let width = width_px as f32;
let height = height_px as f32;
let mut svg = SvgRenderer::new(width, height);
let render_scale = self.render_scale();
svg.set_render_scale(render_scale);
svg.set_text_engine_mode(self.display.text_engine);
let (x_min, x_max, y_min, y_max) =
self.effective_main_panel_bounds_for_series(&snapshot_series)?;
self.validate_axis_scale_ranges_for_render(&snapshot_series, x_min, x_max, y_min, y_max)?;
let content = self.create_plot_content(y_min, y_max);
let mut measurement_renderer =
SkiaRenderer::new(width_px, height_px, self.display.theme.clone())?;
measurement_renderer.set_text_engine_mode(self.display.text_engine);
measurement_renderer.set_render_scale(render_scale);
let x_major_measurement_layout = TickLayout::compute(
x_min,
x_max,
0.0,
1.0,
&self.layout.x_scale,
self.layout.tick_config.major_ticks_x,
);
let y_major_measurement_layout = TickLayout::compute_y_axis(
y_min,
y_max,
0.0,
1.0,
&self.layout.y_scale,
self.layout.tick_config.major_ticks_y,
);
let measured_dimensions = self.measure_layout_text_with_ticks(
&measurement_renderer,
&content,
self.display.config.figure.dpi,
&x_major_measurement_layout.labels,
&y_major_measurement_layout.labels,
)?;
let layout = self.compute_layout_from_measurements(
(width_px, height_px),
&content,
self.display.config.figure.dpi,
measured_dimensions.as_ref(),
);
let plot_left = layout.plot_area.left;
let plot_right = layout.plot_area.right;
let plot_top = layout.plot_area.top;
let plot_bottom = layout.plot_area.bottom;
let plot_width = layout.plot_area.width();
let plot_height = layout.plot_area.height();
let plot_area = tiny_skia::Rect::from_ltrb(plot_left, plot_top, plot_right, plot_bottom)
.ok_or(PlottingError::InvalidData {
message: "Invalid plot area from layout".to_string(),
position: None,
})?;
svg.draw_rectangle(0.0, 0.0, width, height, self.display.theme.background, true);
let bar_categories: Option<&Vec<String>> = self.series_mgr.series.iter().find_map(|s| {
if let SeriesType::Bar { categories, .. } = &s.series_type {
Some(categories)
} else {
None
}
});
let y_tick_layout = TickLayout::compute_y_axis(
y_min,
y_max,
plot_top,
plot_bottom,
&self.layout.y_scale,
self.layout.tick_config.major_ticks_y,
);
let x_tick_layout = if bar_categories.is_none() {
Some(TickLayout::compute(
x_min,
x_max,
plot_left,
plot_right,
&self.layout.x_scale,
self.layout.tick_config.major_ticks_x,
))
} else {
None
};
let y_minor_ticks = Self::minor_tick_values_for_scale(
&y_tick_layout.data_positions,
y_min,
y_max,
&self.layout.y_scale,
self.layout.tick_config.minor_ticks_y,
);
let y_minor_tick_pixels: Vec<f32> = y_minor_ticks
.iter()
.map(|&tick| Self::scaled_y_pixel(tick, y_min, y_max, plot_area, &self.layout.y_scale))
.collect();
let x_minor_tick_pixels: Vec<f32> = x_tick_layout
.as_ref()
.map(|layout| {
Self::minor_tick_values_for_scale(
&layout.data_positions,
x_min,
x_max,
&self.layout.x_scale,
self.layout.tick_config.minor_ticks_x,
)
.iter()
.map(|&tick| {
Self::scaled_x_pixel(tick, x_min, x_max, plot_area, &self.layout.x_scale)
})
.collect()
})
.unwrap_or_default();
let draw_axes = Self::needs_cartesian_axes_for_series(&snapshot_series);
if self.layout.grid_style.visible && draw_axes {
let grid_color = self.layout.grid_style.effective_color();
let grid_width_px = self.line_width_px(self.layout.grid_style.line_width);
let grid_y_pixels = Self::grid_tick_pixels(
&y_tick_layout.pixel_positions,
&y_minor_tick_pixels,
&self.layout.tick_config.grid_mode,
);
if bar_categories.is_some() {
svg.draw_grid(
&[], &grid_y_pixels,
plot_left,
plot_right,
plot_top,
plot_bottom,
grid_color,
self.layout.grid_style.line_style.clone(),
grid_width_px,
);
} else {
let x_tick_layout = x_tick_layout
.as_ref()
.expect("non-categorical SVG render should have x tick layout");
let grid_x_pixels = Self::grid_tick_pixels(
&x_tick_layout.pixel_positions,
&x_minor_tick_pixels,
&self.layout.tick_config.grid_mode,
);
svg.draw_grid(
&grid_x_pixels,
&grid_y_pixels,
plot_left,
plot_right,
plot_top,
plot_bottom,
grid_color,
self.layout.grid_style.line_style.clone(),
grid_width_px,
);
}
}
if draw_axes && !self.layout.tick_config.enabled {
svg.draw_axes(
plot_left,
plot_right,
plot_top,
plot_bottom,
&[],
&[],
&self.layout.tick_config.direction,
&TickSides::none(),
self.display.theme.foreground,
);
}
let tick_size_px = pt_to_px(
self.display.config.typography.tick_size(),
self.display.config.figure.dpi,
);
if draw_axes {
if let Some(categories) = bar_categories {
let x_range = x_max - x_min;
let category_x_tick_positions: Vec<f32> = (0..categories.len())
.map(|index| {
if x_range.abs() < f64::EPSILON {
plot_left + plot_width * 0.5
} else {
plot_left + (((index as f64) - x_min) / x_range) as f32 * plot_width
}
})
.collect();
if self.layout.tick_config.enabled {
svg.draw_axes_with_minor_ticks(
plot_left,
plot_right,
plot_top,
plot_bottom,
&category_x_tick_positions,
&y_tick_layout.pixel_positions,
&[],
&y_minor_tick_pixels,
&self.layout.tick_config.direction,
&self.layout.tick_config.sides,
self.display.theme.foreground,
);
svg.draw_tick_labels(
&[],
&[],
&y_tick_layout.pixel_positions,
&y_tick_layout.labels,
plot_left,
plot_right,
plot_top,
plot_bottom,
layout.xtick_baseline_y,
layout.ytick_right_x,
self.display.theme.foreground,
tick_size_px,
)?;
for (category, &x) in categories.iter().zip(category_x_tick_positions.iter()) {
svg.draw_text_centered(
category,
x,
layout.xtick_baseline_y,
tick_size_px,
self.display.theme.foreground,
)?;
}
}
} else {
let x_tick_layout = x_tick_layout
.as_ref()
.expect("non-categorical SVG render should have x tick layout");
if self.layout.tick_config.enabled {
svg.draw_axes_with_minor_ticks(
plot_left,
plot_right,
plot_top,
plot_bottom,
&x_tick_layout.pixel_positions,
&y_tick_layout.pixel_positions,
&x_minor_tick_pixels,
&y_minor_tick_pixels,
&self.layout.tick_config.direction,
&self.layout.tick_config.sides,
self.display.theme.foreground,
);
svg.draw_tick_labels(
&x_tick_layout.pixel_positions,
&x_tick_layout.labels,
&y_tick_layout.pixel_positions,
&y_tick_layout.labels,
plot_left,
plot_right,
plot_top,
plot_bottom,
layout.xtick_baseline_y,
layout.ytick_right_x,
self.display.theme.foreground,
tick_size_px,
)?;
}
}
}
let clip_id = svg.add_clip_rect(plot_left, plot_top, plot_width, plot_height);
svg.start_clip_group(&clip_id);
let legend_items = self.collect_legend_items();
let render_scale = self.render_scale();
let inset_rects = self.inset_rects_for_series(&snapshot_series, plot_area, render_scale)?;
for (idx, series) in snapshot_series.iter().enumerate() {
let default_color = self.display.theme.get_color(idx);
let inset_rect = inset_rects[idx];
let (series_area, series_bounds) = if let Some(inset_rect) = inset_rect {
(inset_rect, self.raw_bounds_for_single_series(series)?)
} else {
(plot_area, (x_min, x_max, y_min, y_max))
};
if let Some(inset_rect) = inset_rect {
let inset_clip_id = svg.add_clip_rect(
inset_rect.x(),
inset_rect.y(),
inset_rect.width(),
inset_rect.height(),
);
svg.start_clip_group(&inset_clip_id);
self.render_series_svg(
&mut svg,
series,
default_color,
series_area,
series_bounds.0,
series_bounds.1,
series_bounds.2,
series_bounds.3,
)?;
svg.end_group();
} else {
self.render_series_svg(
&mut svg,
series,
default_color,
series_area,
series_bounds.0,
series_bounds.1,
series_bounds.2,
series_bounds.3,
)?;
}
}
svg.end_group();
if let Some(ref pos) = layout.title_pos {
if let Some(ref title) = self.display.title {
let title_str = title.resolve(0.0);
svg.draw_text_centered(
&title_str,
pos.x,
pos.y,
pos.size,
self.display.theme.foreground,
)?;
}
}
if let Some(ref pos) = layout.xlabel_pos {
if let Some(ref xlabel) = self.display.xlabel {
let xlabel_str = xlabel.resolve(0.0);
svg.draw_text_centered(
&xlabel_str,
pos.x,
pos.y,
pos.size,
self.display.theme.foreground,
)?;
}
}
if let Some(ref pos) = layout.ylabel_pos {
if let Some(ref ylabel) = self.display.ylabel {
let ylabel_str = ylabel.resolve(0.0);
svg.draw_text_rotated(
&ylabel_str,
pos.x,
pos.y,
pos.size,
self.display.theme.foreground,
-90.0,
)?;
}
}
if !legend_items.is_empty() && self.layout.legend.enabled {
let legend = self
.layout
.legend
.to_legend(self.display.config.typography.legend_size());
let plot_bounds = (plot_left, plot_top, plot_right, plot_bottom);
svg.draw_legend_full(&legend_items, &legend, plot_bounds, None)?;
}
Ok(svg.to_svg_string())
}
#[cfg(all(feature = "pdf", not(target_arch = "wasm32")))]
pub fn save_pdf<P: AsRef<Path>>(self, path: P) -> Result<()> {
self.save_pdf_with_size(path, None)
}
#[cfg(all(feature = "pdf", not(target_arch = "wasm32")))]
pub fn save_pdf_with_size<P: AsRef<Path>>(
mut self,
path: P,
size: Option<(f64, f64)>,
) -> Result<()> {
use crate::export::svg_to_pdf::page_sizes;
let (width_mm, height_mm) = size.unwrap_or(page_sizes::PLOT_DEFAULT);
let width_px = page_sizes::mm_to_px(width_mm) as u32;
let height_px = page_sizes::mm_to_px(height_mm) as u32;
self = self.set_output_pixels(width_px, height_px);
let svg_content = self.render_to_svg()?;
let pdf_data = crate::export::svg_to_pdf(&svg_content)?;
crate::export::write_bytes_atomic(path, &pdf_data)
}
#[cfg(all(feature = "animation", not(target_arch = "wasm32")))]
pub fn render_frame(&self, width: u32, height: u32) -> Result<Vec<u8>> {
let sized_plot = self.clone().set_output_pixels(width, height);
let image = sized_plot.render()?;
let rgba_data = &image.pixels;
let pixels = (width * height) as usize;
let mut rgb_data = vec![0u8; pixels * 3];
for i in 0..pixels {
rgb_data[i * 3] = rgba_data[i * 4]; rgb_data[i * 3 + 1] = rgba_data[i * 4 + 1]; rgb_data[i * 3 + 2] = rgba_data[i * 4 + 2]; }
Ok(rgb_data)
}
}