use super::*;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ColorbarTicks {
pub major_values: Vec<f64>,
pub major_labels: Vec<String>,
pub minor_values: Vec<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ColorbarLayoutMetrics {
pub major_tick_width: f32,
pub minor_tick_width: f32,
pub tick_label_x_offset: f32,
pub rotated_label_center_x_offset: Option<f32>,
pub total_extent: f32,
}
pub fn calculate_plot_area(canvas_width: u32, canvas_height: u32, margin_fraction: f32) -> Rect {
let margin_x = (canvas_width as f32) * margin_fraction;
let margin_y = (canvas_height as f32) * margin_fraction;
Rect::from_xywh(
margin_x,
margin_y,
(canvas_width as f32) - 2.0 * margin_x,
(canvas_height as f32) - 2.0 * margin_y,
)
.unwrap_or_else(|| {
Rect::from_xywh(
10.0,
10.0,
(canvas_width as f32) - 20.0,
(canvas_height as f32) - 20.0,
)
.unwrap()
})
}
pub fn calculate_plot_area_dpi(canvas_width: u32, canvas_height: u32, dpi_scale: f32) -> Rect {
let render_scale = RenderScale::from_reference_scale(dpi_scale);
let base_margin_left = 100.0; let base_margin_right = 40.0; let base_margin_top = 80.0; let base_margin_bottom = 60.0;
let margin_left = render_scale.logical_pixels_to_pixels(base_margin_left);
let margin_right = render_scale.logical_pixels_to_pixels(base_margin_right);
let margin_top = render_scale.logical_pixels_to_pixels(base_margin_top);
let margin_bottom = render_scale.logical_pixels_to_pixels(base_margin_bottom);
let plot_width = (canvas_width as f32) - margin_left - margin_right;
let plot_height = (canvas_height as f32) - margin_top - margin_bottom;
if plot_width > 100.0 && plot_height > 100.0 {
let plot_x = margin_left;
let plot_y = margin_top;
Rect::from_xywh(plot_x, plot_y, plot_width, plot_height).unwrap_or_else(|| {
Rect::from_xywh(
40.0,
40.0,
(canvas_width as f32) - 80.0,
(canvas_height as f32) - 80.0,
)
.unwrap()
})
} else {
let fallback_margin = (canvas_width.min(canvas_height) as f32) * 0.1;
Rect::from_xywh(
fallback_margin,
fallback_margin,
(canvas_width as f32) - 2.0 * fallback_margin,
(canvas_height as f32) - 2.0 * fallback_margin,
)
.unwrap()
}
}
pub fn calculate_plot_area_config(
canvas_width: u32,
canvas_height: u32,
margins: &ComputedMargins,
dpi: f32,
) -> Rect {
let margin_left = margins.left_px(dpi);
let margin_right = margins.right_px(dpi);
let margin_top = margins.top_px(dpi);
let margin_bottom = margins.bottom_px(dpi);
let plot_width = (canvas_width as f32) - margin_left - margin_right;
let plot_height = (canvas_height as f32) - margin_top - margin_bottom;
if plot_width > 50.0 && plot_height > 50.0 {
let plot_x = margin_left;
let plot_y = margin_top;
Rect::from_xywh(plot_x, plot_y, plot_width, plot_height).unwrap_or_else(|| {
Rect::from_xywh(
40.0,
40.0,
(canvas_width as f32) - 80.0,
(canvas_height as f32) - 80.0,
)
.unwrap()
})
} else {
let fallback_margin = (canvas_width.min(canvas_height) as f32) * 0.1;
Rect::from_xywh(
fallback_margin,
fallback_margin,
(canvas_width as f32) - 2.0 * fallback_margin,
(canvas_height as f32) - 2.0 * fallback_margin,
)
.unwrap()
}
}
pub fn map_data_to_pixels(
data_x: f64,
data_y: f64,
data_x_min: f64,
data_x_max: f64,
data_y_min: f64,
data_y_max: f64,
plot_area: Rect,
) -> (f32, f32) {
let transform = CoordinateTransform::from_plot_area(
plot_area.left(),
plot_area.top(),
plot_area.width(),
plot_area.height(),
data_x_min,
data_x_max,
data_y_min,
data_y_max,
);
transform.data_to_screen(data_x, data_y)
}
pub fn map_data_to_pixels_scaled(
data_x: f64,
data_y: f64,
data_x_min: f64,
data_x_max: f64,
data_y_min: f64,
data_y_max: f64,
plot_area: Rect,
x_scale: &crate::axes::AxisScale,
y_scale: &crate::axes::AxisScale,
) -> (f32, f32) {
let transform = CoordinateTransform::from_plot_area(
plot_area.left(),
plot_area.top(),
plot_area.width(),
plot_area.height(),
data_x_min,
data_x_max,
data_y_min,
data_y_max,
);
transform.data_to_screen_scaled(data_x, data_y, x_scale, y_scale)
}
#[allow(clippy::too_many_arguments)]
pub fn try_map_data_to_pixels_scaled(
data_x: f64,
data_y: f64,
data_x_min: f64,
data_x_max: f64,
data_y_min: f64,
data_y_max: f64,
plot_area: Rect,
x_scale: &crate::axes::AxisScale,
y_scale: &crate::axes::AxisScale,
) -> Option<(f32, f32)> {
let transform = CoordinateTransform::from_plot_area(
plot_area.left(),
plot_area.top(),
plot_area.width(),
plot_area.height(),
data_x_min,
data_x_max,
data_y_min,
data_y_max,
);
transform.try_data_to_screen_scaled(data_x, data_y, x_scale, y_scale)
}
pub use crate::axes::{generate_minor_ticks, generate_ticks};
fn generate_log_colorbar_major_ticks(min: f64, max: f64) -> Vec<f64> {
let (min, max) = if min <= max { (min, max) } else { (max, min) };
if min <= 0.0 || max <= 0.0 {
return vec![min.max(f64::EPSILON), max.max(f64::EPSILON)];
}
let start_exp = min.log10().ceil() as i32;
let end_exp = max.log10().floor() as i32;
let mut ticks = Vec::new();
for exp in start_exp..=end_exp {
let tick = 10.0_f64.powi(exp);
if tick >= min && tick <= max {
ticks.push(tick);
}
}
if ticks.is_empty() {
crate::axes::generate_log_ticks(min, max, 6)
} else {
ticks
}
}
fn generate_log_colorbar_minor_ticks(min: f64, max: f64) -> Vec<f64> {
let (min, max) = if min <= max { (min, max) } else { (max, min) };
if min <= 0.0 || max <= 0.0 {
return Vec::new();
}
let start_exp = min.log10().floor() as i32;
let end_exp = max.log10().ceil() as i32;
let mut ticks = Vec::new();
for exp in start_exp..=end_exp {
let base = 10.0_f64.powi(exp);
for multiplier in 2..=9 {
let tick = base * multiplier as f64;
if tick > min && tick < max {
ticks.push(tick);
}
}
}
ticks.sort_by(|a, b| a.partial_cmp(b).unwrap());
ticks
}
pub(crate) fn compute_colorbar_layout_metrics(
colorbar_width: f32,
tick_font_size: f32,
max_tick_label_width: f32,
rotated_label_width: Option<f32>,
) -> ColorbarLayoutMetrics {
let major_tick_width = colorbar_width * 0.3;
let minor_tick_width = colorbar_width * 0.18;
let tick_label_x_offset = colorbar_width + tick_font_size * 0.5;
let label_gap = tick_font_size.max(4.0) * 0.75;
let tick_label_extent = tick_label_x_offset + max_tick_label_width;
let rotated_label_center_x_offset = rotated_label_width
.map(|width| tick_label_x_offset + max_tick_label_width + label_gap + width / 2.0);
let rotated_label_extent = rotated_label_width
.map(|width| tick_label_x_offset + max_tick_label_width + label_gap + width)
.unwrap_or(0.0);
let total_extent =
(colorbar_width + major_tick_width).max(tick_label_extent.max(rotated_label_extent));
ColorbarLayoutMetrics {
major_tick_width,
minor_tick_width,
tick_label_x_offset,
rotated_label_center_x_offset,
total_extent,
}
}
pub(crate) fn colorbar_major_label_top(tick_center_y: f32, label_center_from_top: f32) -> f32 {
tick_center_y - label_center_from_top
}
fn is_superscript_digit(ch: char) -> bool {
matches!(
ch,
'⁰' | '¹' | '²' | '³' | '⁴' | '⁵' | '⁶' | '⁷' | '⁸' | '⁹' | '⁻'
)
}
pub(crate) fn colorbar_major_label_anchor_center_from_top(
scale: &crate::axes::AxisScale,
label: &str,
rendered_center_from_top: f32,
log_decade_base_center_from_top: Option<f32>,
) -> f32 {
match scale {
crate::axes::AxisScale::Log
if label.starts_with("10")
&& label.chars().skip(2).all(is_superscript_digit)
&& label.chars().count() > 2 =>
{
log_decade_base_center_from_top.unwrap_or(rendered_center_from_top)
}
_ => rendered_center_from_top,
}
}
pub use crate::axes::{
format_log_tick_label, format_tick_label, format_tick_labels, format_tick_labels_for_scale,
};
pub fn compute_colorbar_ticks(
vmin: f64,
vmax: f64,
scale: &crate::axes::AxisScale,
show_log_subticks: bool,
) -> ColorbarTicks {
match scale {
crate::axes::AxisScale::Log => {
let major_values = generate_log_colorbar_major_ticks(vmin, vmax);
let major_labels = format_tick_labels_for_scale(&major_values, scale);
let minor_values = if show_log_subticks {
generate_log_colorbar_minor_ticks(vmin, vmax)
} else {
Vec::new()
};
ColorbarTicks {
major_values,
major_labels,
minor_values,
}
}
_ => {
let major_values = crate::axes::generate_ticks_for_scale(vmin, vmax, 6, scale);
let major_labels = format_tick_labels_for_scale(&major_values, scale);
ColorbarTicks {
major_values,
major_labels,
minor_values: Vec::new(),
}
}
}
}