use crate::{
core::{
ComputedMargins, CoordinateTransform, LayoutRect, Legend, LegendItem, LegendItemType,
LegendPosition, LegendSpacingPixels, LegendStyle, PlottingError, RenderScale, Result,
SpacingConfig, TextPosition, TickFormatter, find_best_position,
plot::{Image, TextEngineMode, TickDirection, TickSides},
pt_to_px,
},
render::{
Color, FontConfig, FontFamily, LineStyle, MarkerStyle, TextRenderer, Theme,
typst_text::{self, TypstBackendKind, TypstTextAnchor},
},
};
use std::borrow::Cow;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use tiny_skia::*;
mod annotations;
mod primitives;
mod utils;
pub use self::utils::{
ColorbarTicks, calculate_plot_area, calculate_plot_area_config, calculate_plot_area_dpi,
compute_colorbar_ticks, format_log_tick_label, format_tick_label, format_tick_labels,
format_tick_labels_for_scale, generate_minor_ticks, generate_ticks, map_data_to_pixels,
map_data_to_pixels_scaled,
};
pub(crate) use self::utils::{
colorbar_major_label_anchor_center_from_top, colorbar_major_label_top,
compute_colorbar_layout_metrics,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct ClipMaskKey {
x_bits: u32,
y_bits: u32,
width_bits: u32,
height_bits: u32,
}
impl ClipMaskKey {
fn new((x, y, width, height): (f32, f32, f32, f32)) -> Self {
Self {
x_bits: x.to_bits(),
y_bits: y.to_bits(),
width_bits: width.to_bits(),
height_bits: height.to_bits(),
}
}
}
pub struct SkiaRenderer {
width: u32,
height: u32,
pixmap: Pixmap,
paint: Paint<'static>,
theme: Theme,
text_renderer: TextRenderer,
font_config: FontConfig,
render_scale: RenderScale,
text_engine_mode: TextEngineMode,
clip_mask_cache: HashMap<ClipMaskKey, Arc<Mask>>,
}
impl SkiaRenderer {
pub fn new(width: u32, height: u32, theme: Theme) -> Result<Self> {
Self::with_font_family(width, height, theme, FontFamily::SansSerif)
}
pub fn with_font_family(
width: u32,
height: u32,
theme: Theme,
font_family: FontFamily,
) -> Result<Self> {
let mut pixmap = Pixmap::new(width, height).ok_or(PlottingError::OutOfMemory)?;
let bg_color = theme.background.to_tiny_skia_color();
pixmap.fill(bg_color);
let paint = Paint::default();
let text_renderer = TextRenderer::new();
let font_config = FontConfig::new(font_family, 12.0);
Ok(Self {
width,
height,
pixmap,
paint,
theme,
text_renderer,
font_config,
render_scale: RenderScale::from_canvas_size(width, height, crate::core::REFERENCE_DPI),
text_engine_mode: TextEngineMode::Plain,
clip_mask_cache: HashMap::new(),
})
}
pub fn set_render_scale(&mut self, render_scale: RenderScale) {
self.render_scale = render_scale;
}
pub fn render_scale(&self) -> RenderScale {
self.render_scale
}
pub fn set_dpi_scale(&mut self, dpi_scale: f32) {
self.set_render_scale(RenderScale::from_reference_scale(dpi_scale));
}
pub fn dpi_scale(&self) -> f32 {
self.render_scale.reference_scale()
}
fn points_to_pixels(&self, points: f32) -> f32 {
self.render_scale.points_to_pixels(points)
}
fn logical_pixels_to_pixels(&self, logical_pixels: f32) -> f32 {
self.render_scale.logical_pixels_to_pixels(logical_pixels)
}
fn scaled_dash_pattern(&self, style: &LineStyle) -> Option<Vec<f32>> {
style.to_dash_array().map(|pattern| {
pattern
.into_iter()
.map(|segment| self.logical_pixels_to_pixels(segment))
.collect()
})
}
pub fn set_text_engine_mode(&mut self, mode: TextEngineMode) {
self.text_engine_mode = mode;
}
pub fn text_engine_mode(&self) -> TextEngineMode {
self.text_engine_mode
}
fn vertical_tick_span(
spine_y: f32,
tick_size: f32,
tick_direction: &TickDirection,
top: bool,
) -> (f32, f32) {
match tick_direction {
TickDirection::Inside => {
if top {
(spine_y, spine_y + tick_size)
} else {
(spine_y, spine_y - tick_size)
}
}
TickDirection::Outside => {
if top {
(spine_y, spine_y - tick_size)
} else {
(spine_y, spine_y + tick_size)
}
}
TickDirection::InOut => (spine_y - tick_size / 2.0, spine_y + tick_size / 2.0),
}
}
fn horizontal_tick_span(
spine_x: f32,
tick_size: f32,
tick_direction: &TickDirection,
right: bool,
) -> (f32, f32) {
match tick_direction {
TickDirection::Inside => {
if right {
(spine_x, spine_x - tick_size)
} else {
(spine_x, spine_x + tick_size)
}
}
TickDirection::Outside => {
if right {
(spine_x, spine_x + tick_size)
} else {
(spine_x, spine_x - tick_size)
}
}
TickDirection::InOut => (spine_x - tick_size / 2.0, spine_x + tick_size / 2.0),
}
}
fn x_label_center(plot_area: &LayoutRect, x_value: f64, x_min: f64, x_max: f64) -> f32 {
let x_range = x_max - x_min;
if x_range.abs() < f64::EPSILON {
plot_area.center_x()
} else {
plot_area.left + ((x_value - x_min) as f32 / x_range as f32) * plot_area.width()
}
}
fn y_label_center(plot_area: &LayoutRect, y_value: f64, y_min: f64, y_max: f64) -> f32 {
let y_range = y_max - y_min;
if y_range.abs() < f64::EPSILON {
plot_area.center_y()
} else {
plot_area.bottom - ((y_value - y_min) as f32 / y_range as f32) * plot_area.height()
}
}
pub fn draw_axes(
&mut self,
plot_area: Rect,
x_ticks: &[f32],
y_ticks: &[f32],
tick_direction: &TickDirection,
tick_sides: &TickSides,
color: Color,
) -> Result<()> {
let axis_width = self.logical_pixels_to_pixels(1.5);
let tick_size = self.logical_pixels_to_pixels(5.0);
let tick_width = self.logical_pixels_to_pixels(1.0);
self.draw_line(
plot_area.left(),
plot_area.bottom(),
plot_area.right(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.left(),
plot_area.top(),
plot_area.left(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.left(),
plot_area.top(),
plot_area.right(),
plot_area.top(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.right(),
plot_area.top(),
plot_area.right(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
for &x in x_ticks {
if x >= plot_area.left() && x <= plot_area.right() {
if tick_sides.bottom {
let (tick_start, tick_end) = Self::vertical_tick_span(
plot_area.bottom(),
tick_size,
tick_direction,
false,
);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.top {
let (tick_start, tick_end) =
Self::vertical_tick_span(plot_area.top(), tick_size, tick_direction, true);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
tick_width,
LineStyle::Solid,
)?;
}
}
}
for &y in y_ticks {
if y >= plot_area.top() && y <= plot_area.bottom() {
if tick_sides.left {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.left(),
tick_size,
tick_direction,
false,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.right {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.right(),
tick_size,
tick_direction,
true,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
tick_width,
LineStyle::Solid,
)?;
}
}
}
Ok(())
}
pub fn draw_axes_with_config(
&mut self,
plot_area: Rect,
x_major_ticks: &[f32],
y_major_ticks: &[f32],
x_minor_ticks: &[f32],
y_minor_ticks: &[f32],
tick_direction: &TickDirection,
tick_sides: &TickSides,
color: Color,
dpi_scale: f32,
) -> Result<()> {
let render_scale = RenderScale::from_reference_scale(dpi_scale);
let axis_width = render_scale.logical_pixels_to_pixels(1.5);
let major_tick_size = render_scale.logical_pixels_to_pixels(8.0);
let minor_tick_size = render_scale.logical_pixels_to_pixels(4.0);
let major_tick_width = render_scale.logical_pixels_to_pixels(1.5);
let minor_tick_width = render_scale.logical_pixels_to_pixels(1.0);
self.draw_line(
plot_area.left(),
plot_area.bottom(),
plot_area.right(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.left(),
plot_area.top(),
plot_area.left(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.left(),
plot_area.top(),
plot_area.right(),
plot_area.top(),
color,
axis_width,
LineStyle::Solid,
)?;
self.draw_line(
plot_area.right(),
plot_area.top(),
plot_area.right(),
plot_area.bottom(),
color,
axis_width,
LineStyle::Solid,
)?;
for &x in x_major_ticks {
if x >= plot_area.left() && x <= plot_area.right() {
if tick_sides.bottom {
let (tick_start, tick_end) = Self::vertical_tick_span(
plot_area.bottom(),
major_tick_size,
tick_direction,
false,
);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
major_tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.top {
let (tick_start, tick_end) = Self::vertical_tick_span(
plot_area.top(),
major_tick_size,
tick_direction,
true,
);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
major_tick_width,
LineStyle::Solid,
)?;
}
}
}
for &x in x_minor_ticks {
if x >= plot_area.left() && x <= plot_area.right() {
if tick_sides.bottom {
let (tick_start, tick_end) = Self::vertical_tick_span(
plot_area.bottom(),
minor_tick_size,
tick_direction,
false,
);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
minor_tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.top {
let (tick_start, tick_end) = Self::vertical_tick_span(
plot_area.top(),
minor_tick_size,
tick_direction,
true,
);
self.draw_line(
x,
tick_start,
x,
tick_end,
color,
minor_tick_width,
LineStyle::Solid,
)?;
}
}
}
for &y in y_major_ticks {
if y >= plot_area.top() && y <= plot_area.bottom() {
if tick_sides.left {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.left(),
major_tick_size,
tick_direction,
false,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
major_tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.right {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.right(),
major_tick_size,
tick_direction,
true,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
major_tick_width,
LineStyle::Solid,
)?;
}
}
}
for &y in y_minor_ticks {
if y >= plot_area.top() && y <= plot_area.bottom() {
if tick_sides.left {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.left(),
minor_tick_size,
tick_direction,
false,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
minor_tick_width,
LineStyle::Solid,
)?;
}
if tick_sides.right {
let (tick_start, tick_end) = Self::horizontal_tick_span(
plot_area.right(),
minor_tick_size,
tick_direction,
true,
);
self.draw_line(
tick_start,
y,
tick_end,
y,
color,
minor_tick_width,
LineStyle::Solid,
)?;
}
}
}
Ok(())
}
pub fn draw_datashader_image(
&mut self,
image: &crate::data::DataShaderImage,
plot_area: Rect,
) -> Result<()> {
let mut datashader_pixmap = Pixmap::new(image.width as u32, image.height as u32)
.ok_or(PlottingError::OutOfMemory)?;
if image.pixels.len() != (image.width * image.height * 4) {
return Err(PlottingError::RenderError(
"Invalid DataShader image pixel data".to_string(),
));
}
let pixmap_data = datashader_pixmap.data_mut();
for (i, chunk) in image.pixels.chunks_exact(4).enumerate() {
let r = chunk[0];
let g = chunk[1];
let b = chunk[2];
let a = chunk[3];
let alpha_f = a as f32 / 255.0;
let premult_r = (r as f32 * alpha_f) as u8;
let premult_g = (g as f32 * alpha_f) as u8;
let premult_b = (b as f32 * alpha_f) as u8;
pixmap_data[i * 4] = premult_b;
pixmap_data[i * 4 + 1] = premult_g;
pixmap_data[i * 4 + 2] = premult_r;
pixmap_data[i * 4 + 3] = a;
}
let transform = Transform::from_scale(
plot_area.width() / image.width as f32,
plot_area.height() / image.height as f32,
)
.post_translate(plot_area.x(), plot_area.y());
self.pixmap.draw_pixmap(
0,
0,
datashader_pixmap.as_ref(),
&PixmapPaint::default(),
transform,
None,
);
Ok(())
}
pub fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) -> Result<()> {
match self.text_engine_mode {
TextEngineMode::Plain => {
let config = FontConfig::new(self.font_config.family.clone(), size);
self.text_renderer
.render_text(&mut self.pixmap, text, x, y, &config, color)
}
#[cfg(feature = "typst-math")]
TextEngineMode::Typst => {
let size_pt = self.typst_size_pt(size);
let rendered =
typst_text::render_raster(text, size_pt, color, 0.0, "Skia text rendering")?;
let (draw_x, draw_y) = typst_text::anchored_top_left(
x,
y,
rendered.width,
rendered.height,
TypstTextAnchor::TopLeft,
);
self.draw_typst_raster(&rendered, draw_x, draw_y);
Ok(())
}
}
}
pub fn draw_text_rotated(
&mut self,
text: &str,
x: f32,
y: f32,
size: f32,
color: Color,
) -> Result<()> {
match self.text_engine_mode {
TextEngineMode::Plain => {
let config = FontConfig::new(self.font_config.family.clone(), size);
self.text_renderer
.render_text_rotated(&mut self.pixmap, text, x, y, &config, color)
}
#[cfg(feature = "typst-math")]
TextEngineMode::Typst => {
let size_pt = self.typst_size_pt(size);
let rendered = typst_text::render_raster(
text,
size_pt,
color,
-90.0,
"Skia rotated text rendering",
)?;
let (draw_x, draw_y) = typst_text::anchored_top_left(
x,
y,
rendered.width,
rendered.height,
TypstTextAnchor::Center,
);
self.draw_typst_raster(&rendered, draw_x, draw_y);
Ok(())
}
}
}
pub fn draw_text_centered(
&mut self,
text: &str,
center_x: f32,
y: f32,
size: f32,
color: Color,
) -> Result<()> {
match self.text_engine_mode {
TextEngineMode::Plain => {
let config = FontConfig::new(self.font_config.family.clone(), size);
self.text_renderer.render_text_centered(
&mut self.pixmap,
text,
center_x,
y,
&config,
color,
)
}
#[cfg(feature = "typst-math")]
TextEngineMode::Typst => {
let size_pt = self.typst_size_pt(size);
let rendered = typst_text::render_raster(
text,
size_pt,
color,
0.0,
"Skia centered text rendering",
)?;
let (draw_x, draw_y) = typst_text::anchored_top_left(
center_x,
y,
rendered.width,
rendered.height,
TypstTextAnchor::TopCenter,
);
self.draw_typst_raster(&rendered, draw_x, draw_y);
Ok(())
}
}
}
pub fn measure_text(&self, text: &str, size: f32) -> Result<(f32, f32)> {
match self.text_engine_mode {
TextEngineMode::Plain => {
let config = FontConfig::new(self.font_config.family.clone(), size);
self.text_renderer.measure_text(text, &config)
}
#[cfg(feature = "typst-math")]
TextEngineMode::Typst => {
let size_pt = self.typst_size_pt(size);
typst_text::measure_text(
text,
size_pt,
self.theme.foreground,
0.0,
TypstBackendKind::Raster,
"Skia text measurement",
)
}
}
}
pub(crate) fn measure_text_ink_center_from_top(&self, text: &str, size: f32) -> Result<f32> {
match self.text_engine_mode {
TextEngineMode::Plain => {
let config = FontConfig::new(self.font_config.family.clone(), size);
self.text_renderer
.measure_text_ink_center_from_top(text, &config)
}
#[cfg(feature = "typst-math")]
TextEngineMode::Typst => Ok(self.measure_text(text, size)?.1 / 2.0),
}
}
pub(crate) fn measure_label_text(&self, text: &str, size: f32) -> Result<(f32, f32)> {
let label_snippet = self.generated_label(text);
self.measure_text(&label_snippet, size)
}
fn generated_label<'a>(&self, text: &'a str) -> Cow<'a, str> {
#[cfg(feature = "typst-math")]
if self.text_engine_mode.uses_typst() {
return Cow::Owned(typst_text::literal_text_snippet(text));
}
Cow::Borrowed(text)
}
pub fn draw_axis_labels(
&mut self,
plot_area: Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_label: &str,
y_label: &str,
color: Color,
label_size: f32,
dpi: f32,
spacing: &SpacingConfig,
) -> Result<()> {
let tick_size = label_size * 0.7; let render_scale = RenderScale::new(dpi);
let tick_pad_px = pt_to_px(spacing.tick_pad, dpi);
let label_pad_px = pt_to_px(spacing.label_pad, dpi);
let char_width_estimate = render_scale.logical_pixels_to_pixels(4.0);
let x_ticks = generate_ticks(x_min, x_max, 5);
let y_ticks = generate_ticks(y_min, y_max, 5);
let x_labels = format_tick_labels(&x_ticks);
let y_labels = format_tick_labels(&y_ticks);
for (tick_value, label_text) in x_ticks.iter().zip(x_labels.iter()) {
let x_pixel = plot_area.left()
+ (*tick_value - x_min) as f32 / (x_max - x_min) as f32 * plot_area.width();
let text_width_estimate = label_text.len() as f32 * char_width_estimate / 2.0;
let label_x = (x_pixel - text_width_estimate)
.max(0.0)
.min(self.width() as f32 - text_width_estimate * 2.0);
let label_y = (plot_area.bottom() + tick_pad_px + tick_size)
.min(self.height() as f32 - tick_size - 5.0);
let label_snippet = self.generated_label(label_text);
self.draw_text(&label_snippet, label_x, label_y, tick_size, color)?;
}
for (tick_value, label_text) in y_ticks.iter().zip(y_labels.iter()) {
let y_pixel = plot_area.bottom()
- (*tick_value - y_min) as f32 / (y_max - y_min) as f32 * plot_area.height();
let text_width_estimate = label_text.len() as f32 * char_width_estimate;
let label_x = (plot_area.left() - text_width_estimate - tick_pad_px).max(5.0);
let label_snippet = self.generated_label(label_text);
self.draw_text(
&label_snippet,
label_x,
y_pixel - tick_size / 3.0,
tick_size,
color,
)?;
}
let x_label_x =
plot_area.left() + plot_area.width() / 2.0 - x_label.len() as f32 * char_width_estimate;
let x_label_y = plot_area.bottom() + tick_pad_px + tick_size + label_pad_px + label_size;
self.draw_text(x_label, x_label_x, x_label_y, label_size, color)?;
let estimated_tick_width = 4.0 * char_width_estimate;
let y_label_x = plot_area.left() - tick_pad_px - estimated_tick_width - label_pad_px;
let y_label_y = plot_area.top() + plot_area.height() / 2.0;
self.draw_text_rotated(y_label, y_label_x, y_label_y, label_size, color)?;
self.draw_plot_border(plot_area, color, render_scale.reference_scale())?;
Ok(())
}
pub fn draw_axis_labels_legacy(
&mut self,
plot_area: Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_label: &str,
y_label: &str,
color: Color,
label_size: f32,
dpi_scale: f32,
) -> Result<()> {
let tick_size = label_size * 0.7;
let render_scale = RenderScale::from_reference_scale(dpi_scale);
let tick_offset_y = render_scale.logical_pixels_to_pixels(20.0);
let x_label_offset = render_scale.logical_pixels_to_pixels(50.0);
let y_label_offset = render_scale.logical_pixels_to_pixels(25.0);
let char_width_estimate = render_scale.logical_pixels_to_pixels(4.0);
let x_ticks = generate_ticks(x_min, x_max, 5);
let y_ticks = generate_ticks(y_min, y_max, 5);
let x_labels = format_tick_labels(&x_ticks);
let y_labels = format_tick_labels(&y_ticks);
for (tick_value, label_text) in x_ticks.iter().zip(x_labels.iter()) {
let x_pixel = plot_area.left()
+ (*tick_value - x_min) as f32 / (x_max - x_min) as f32 * plot_area.width();
let text_width_estimate = label_text.len() as f32 * char_width_estimate / 2.0;
let label_x = (x_pixel - text_width_estimate)
.max(0.0)
.min(self.width() as f32 - text_width_estimate * 2.0);
let label_y =
(plot_area.bottom() + tick_offset_y).min(self.height() as f32 - tick_size - 5.0);
let label_snippet = self.generated_label(label_text);
self.draw_text(&label_snippet, label_x, label_y, tick_size, color)?;
}
for (tick_value, label_text) in y_ticks.iter().zip(y_labels.iter()) {
let y_pixel = plot_area.bottom()
- (*tick_value - y_min) as f32 / (y_max - y_min) as f32 * plot_area.height();
let text_width_estimate = label_text.len() as f32 * char_width_estimate;
let label_x = (plot_area.left()
- text_width_estimate
- render_scale.logical_pixels_to_pixels(15.0))
.max(5.0);
let label_snippet = self.generated_label(label_text);
self.draw_text(
&label_snippet,
label_x,
y_pixel - tick_size / 3.0,
tick_size,
color,
)?;
}
let x_label_x =
plot_area.left() + plot_area.width() / 2.0 - x_label.len() as f32 * char_width_estimate;
let x_label_y = plot_area.bottom() + x_label_offset;
self.draw_text(x_label, x_label_x, x_label_y, label_size, color)?;
let y_label_x = plot_area.left() - y_label_offset;
let y_label_y = plot_area.top() + plot_area.height() / 2.0;
self.draw_text_rotated(y_label, y_label_x, y_label_y, label_size, color)?;
self.draw_plot_border(plot_area, color, dpi_scale)?;
Ok(())
}
pub fn draw_axis_labels_with_ticks(
&mut self,
plot_area: Rect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_major_ticks: &[f64],
y_major_ticks: &[f64],
x_label: &str,
y_label: &str,
color: Color,
label_size: f32,
dpi_scale: f32,
) -> Result<()> {
let tick_size = label_size * 0.7; let render_scale = RenderScale::from_reference_scale(dpi_scale);
let tick_offset_y = render_scale.logical_pixels_to_pixels(25.0);
let x_label_offset = render_scale.logical_pixels_to_pixels(55.0);
let y_label_offset = render_scale.logical_pixels_to_pixels(50.0);
let y_tick_offset = render_scale.logical_pixels_to_pixels(15.0);
let char_width_estimate = render_scale.logical_pixels_to_pixels(4.0);
let x_labels = format_tick_labels(x_major_ticks);
let y_labels = format_tick_labels(y_major_ticks);
for (tick_value, label_text) in x_major_ticks.iter().zip(x_labels.iter()) {
let x_pixel = plot_area.left()
+ (*tick_value - x_min) as f32 / (x_max - x_min) as f32 * plot_area.width();
let text_width_estimate = label_text.len() as f32 * char_width_estimate / 2.0;
let label_x = (x_pixel - text_width_estimate)
.max(0.0)
.min(self.width() as f32 - text_width_estimate * 2.0);
let label_y =
(plot_area.bottom() + tick_offset_y).min(self.height() as f32 - tick_size - 5.0); let label_snippet = self.generated_label(label_text);
self.draw_text(&label_snippet, label_x, label_y, tick_size, color)?;
}
for (tick_value, label_text) in y_major_ticks.iter().zip(y_labels.iter()) {
let y_pixel = plot_area.bottom()
- (*tick_value - y_min) as f32 / (y_max - y_min) as f32 * plot_area.height();
let text_width_estimate = label_text.len() as f32 * char_width_estimate;
let label_x = (plot_area.left() - text_width_estimate - y_tick_offset).max(5.0); let label_snippet = self.generated_label(label_text);
self.draw_text(
&label_snippet,
label_x,
y_pixel + tick_size * 0.3,
tick_size,
color,
)?;
}
let x_label_x =
plot_area.left() + plot_area.width() / 2.0 - x_label.len() as f32 * char_width_estimate;
let x_label_y = plot_area.bottom() + x_label_offset;
self.draw_text(x_label, x_label_x, x_label_y, label_size, color)?;
let estimated_text_width = y_label.len() as f32 * label_size * 0.8;
let improved_y_label_offset = (estimated_text_width * 0.6).max(y_label_offset);
let y_label_x = plot_area.left() - improved_y_label_offset;
let y_label_y = plot_area.top() + plot_area.height() / 2.0;
self.draw_text_rotated(y_label, y_label_x, y_label_y, label_size, color)?;
self.draw_plot_border(plot_area, color, dpi_scale)?;
Ok(())
}
pub fn draw_axis_labels_with_categories(
&mut self,
plot_area: Rect,
categories: &[String],
y_min: f64,
y_max: f64,
y_major_ticks: &[f64],
x_label: &str,
y_label: &str,
color: Color,
label_size: f32,
dpi_scale: f32,
) -> Result<()> {
let tick_size = label_size * 0.7;
let render_scale = RenderScale::from_reference_scale(dpi_scale);
let tick_offset_y = render_scale.logical_pixels_to_pixels(25.0);
let x_label_offset = render_scale.logical_pixels_to_pixels(55.0);
let y_label_offset = render_scale.logical_pixels_to_pixels(50.0);
let y_tick_offset = render_scale.logical_pixels_to_pixels(15.0);
let char_width_estimate = render_scale.logical_pixels_to_pixels(4.0);
let n_categories = categories.len();
if n_categories > 0 {
let x_min = -0.5_f64;
let x_max = n_categories as f64 - 0.5;
let x_range = x_max - x_min;
for (i, category) in categories.iter().enumerate() {
let x_data = i as f64;
let x_center =
plot_area.left() + ((x_data - x_min) / x_range) as f32 * plot_area.width();
let text_width_estimate = category.len() as f32 * char_width_estimate / 2.0;
let label_x = (x_center - text_width_estimate)
.max(0.0)
.min(self.width() as f32 - text_width_estimate * 2.0);
let label_y = (plot_area.bottom() + tick_offset_y)
.min(self.height() as f32 - tick_size - 5.0);
self.draw_text(category, label_x, label_y, tick_size, color)?;
}
}
let y_labels = format_tick_labels(y_major_ticks);
for (tick_value, label_text) in y_major_ticks.iter().zip(y_labels.iter()) {
let y_pixel = plot_area.bottom()
- (*tick_value - y_min) as f32 / (y_max - y_min) as f32 * plot_area.height();
let text_width_estimate = label_text.len() as f32 * char_width_estimate;
let label_x = (plot_area.left() - text_width_estimate - y_tick_offset).max(5.0);
let label_snippet = self.generated_label(label_text);
self.draw_text(
&label_snippet,
label_x,
y_pixel + tick_size * 0.3,
tick_size,
color,
)?;
}
let x_label_x =
plot_area.left() + plot_area.width() / 2.0 - x_label.len() as f32 * char_width_estimate;
let x_label_y = plot_area.bottom() + x_label_offset;
self.draw_text(x_label, x_label_x, x_label_y, label_size, color)?;
let estimated_text_width = y_label.len() as f32 * label_size * 0.8;
let improved_y_label_offset = (estimated_text_width * 0.6).max(y_label_offset);
let y_label_x = plot_area.left() - improved_y_label_offset;
let y_label_y = plot_area.top() + plot_area.height() / 2.0;
self.draw_text_rotated(y_label, y_label_x, y_label_y, label_size, color)?;
self.draw_plot_border(plot_area, color, dpi_scale)?;
Ok(())
}
pub fn draw_plot_border(
&mut self,
plot_area: Rect,
color: Color,
dpi_scale: f32,
) -> Result<()> {
let border_width =
RenderScale::from_reference_scale(dpi_scale).logical_pixels_to_pixels(1.5);
let mut paint = tiny_skia::Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let stroke = tiny_skia::Stroke {
width: border_width,
..tiny_skia::Stroke::default()
};
let path = tiny_skia::PathBuilder::from_rect(plot_area);
self.pixmap.stroke_path(
&path,
&paint,
&stroke,
tiny_skia::Transform::identity(),
None,
);
Ok(())
}
pub fn draw_title(
&mut self,
title: &str,
_plot_area: Rect,
color: Color,
title_size: f32,
dpi: f32,
_spacing: &SpacingConfig,
) -> Result<()> {
let canvas_center_x = self.width() as f32 / 2.0;
let top_padding = RenderScale::new(dpi).logical_pixels_to_pixels(8.0);
let title_y = top_padding + title_size;
self.draw_text_centered(title, canvas_center_x, title_y, title_size, color)
}
pub fn draw_title_at(&mut self, pos: &TextPosition, text: &str, color: Color) -> Result<()> {
self.draw_text_centered(text, pos.x, pos.y, pos.size, color)
}
pub fn draw_xlabel_at(&mut self, pos: &TextPosition, text: &str, color: Color) -> Result<()> {
self.draw_text_centered(text, pos.x, pos.y, pos.size, color)
}
pub fn draw_ylabel_at(&mut self, pos: &TextPosition, text: &str, color: Color) -> Result<()> {
self.draw_text_rotated(text, pos.x, pos.y, pos.size, color)
}
pub fn draw_axis_labels_at(
&mut self,
plot_area: &LayoutRect,
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
x_ticks: &[f64],
y_ticks: &[f64],
xtick_baseline_y: f32,
ytick_right_x: f32,
tick_size: f32,
color: Color,
dpi: f32,
show_tick_labels: bool,
draw_border: bool,
) -> Result<()> {
let render_scale = RenderScale::new(dpi);
let skia_plot_area = Rect::from_ltrb(
plot_area.left,
plot_area.top,
plot_area.right,
plot_area.bottom,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid plot area dimensions".to_string(),
position: None,
})?;
let x_labels = format_tick_labels(x_ticks);
let y_labels = format_tick_labels(y_ticks);
if show_tick_labels {
for (tick_value, label_text) in x_ticks.iter().zip(x_labels.iter()) {
let x_pixel = Self::x_label_center(plot_area, *tick_value, x_min, x_max);
let label_snippet = self.generated_label(label_text);
let (text_width, _) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (x_pixel - text_width / 2.0)
.max(0.0)
.min(self.width() as f32 - text_width);
self.draw_text(&label_snippet, label_x, xtick_baseline_y, tick_size, color)?;
}
for (tick_value, label_text) in y_ticks.iter().zip(y_labels.iter()) {
let y_pixel = Self::y_label_center(plot_area, *tick_value, y_min, y_max);
let label_snippet = self.generated_label(label_text);
let (text_width, text_height) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (ytick_right_x - text_width).max(0.0);
let centered_y = y_pixel - text_height / 2.0;
self.draw_text(&label_snippet, label_x, centered_y, tick_size, color)?;
}
}
if draw_border {
self.draw_plot_border(skia_plot_area, color, render_scale.reference_scale())?;
}
Ok(())
}
pub fn draw_axis_labels_at_categorical(
&mut self,
plot_area: &LayoutRect,
categories: &[String],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
y_ticks: &[f64],
xtick_baseline_y: f32,
ytick_right_x: f32,
tick_size: f32,
color: Color,
dpi: f32,
show_tick_labels: bool,
draw_border: bool,
) -> Result<()> {
let render_scale = RenderScale::new(dpi);
let skia_plot_area = Rect::from_ltrb(
plot_area.left,
plot_area.top,
plot_area.right,
plot_area.bottom,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid plot area dimensions".to_string(),
position: None,
})?;
if show_tick_labels {
let n_categories = categories.len();
if n_categories > 0 {
for (i, category) in categories.iter().enumerate() {
let x_center = Self::x_label_center(plot_area, i as f64, x_min, x_max);
let label_snippet = self.generated_label(category);
let (text_width, _) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (x_center - text_width / 2.0)
.max(0.0)
.min(self.width() as f32 - text_width);
self.draw_text(&label_snippet, label_x, xtick_baseline_y, tick_size, color)?;
}
}
let y_labels = format_tick_labels(y_ticks);
for (tick_value, label_text) in y_ticks.iter().zip(y_labels.iter()) {
let y_pixel = Self::y_label_center(plot_area, *tick_value, y_min, y_max);
let label_snippet = self.generated_label(label_text);
let (text_width, text_height) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (ytick_right_x - text_width).max(0.0);
let centered_y = y_pixel - text_height / 2.0;
self.draw_text(&label_snippet, label_x, centered_y, tick_size, color)?;
}
}
if draw_border {
self.draw_plot_border(skia_plot_area, color, render_scale.reference_scale())?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn draw_axis_labels_at_categorical_violin(
&mut self,
plot_area: &LayoutRect,
categories: &[String],
x_positions: &[f64],
x_min: f64,
x_max: f64,
y_min: f64,
y_max: f64,
y_ticks: &[f64],
xtick_baseline_y: f32,
ytick_right_x: f32,
tick_size: f32,
color: Color,
dpi: f32,
show_tick_labels: bool,
draw_border: bool,
) -> Result<()> {
let render_scale = RenderScale::new(dpi);
let skia_plot_area = Rect::from_ltrb(
plot_area.left,
plot_area.top,
plot_area.right,
plot_area.bottom,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid plot area dimensions".to_string(),
position: None,
})?;
if show_tick_labels {
for (category, &x_pos) in categories.iter().zip(x_positions.iter()) {
let x_center = Self::x_label_center(plot_area, x_pos, x_min, x_max);
let label_snippet = self.generated_label(category);
let (text_width, _) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (x_center - text_width / 2.0)
.max(0.0)
.min(self.width() as f32 - text_width);
self.draw_text(&label_snippet, label_x, xtick_baseline_y, tick_size, color)?;
}
let y_labels = format_tick_labels(y_ticks);
for (tick_value, label_text) in y_ticks.iter().zip(y_labels.iter()) {
let y_pixel = Self::y_label_center(plot_area, *tick_value, y_min, y_max);
let label_snippet = self.generated_label(label_text);
let (text_width, text_height) = self.measure_text(&label_snippet, tick_size)?;
let label_x = (ytick_right_x - text_width).max(0.0);
let centered_y = y_pixel - text_height / 2.0;
self.draw_text(&label_snippet, label_x, centered_y, tick_size, color)?;
}
}
if draw_border {
self.draw_plot_border(skia_plot_area, color, render_scale.reference_scale())?;
}
Ok(())
}
pub fn draw_title_legacy(
&mut self,
title: &str,
plot_area: Rect,
color: Color,
title_size: f32,
dpi_scale: f32,
) -> Result<()> {
let title_offset =
RenderScale::from_reference_scale(dpi_scale).logical_pixels_to_pixels(30.0);
let canvas_center_x = self.width() as f32 / 2.0;
let title_y = (plot_area.top() - title_offset).max(title_size + 5.0);
self.draw_text_centered(title, canvas_center_x, title_y, title_size, color)
}
pub fn draw_legend(&mut self, legend_items: &[(String, Color)], plot_area: Rect) -> Result<()> {
if legend_items.is_empty() {
return Ok(());
}
let legend_size = 12.0;
let legend_spacing = 20.0;
let legend_x = plot_area.right() - 150.0;
let mut legend_y = plot_area.top() + 30.0;
let legend_bg = Rect::from_xywh(
legend_x - 10.0,
legend_y - 15.0,
140.0,
legend_items.len() as f32 * legend_spacing + 10.0,
)
.ok_or(PlottingError::InvalidData {
message: "Invalid legend dimensions".to_string(),
position: None,
})?;
self.draw_rectangle(
legend_bg.left(),
legend_bg.top(),
legend_bg.width(),
legend_bg.height(),
Color::new_rgba(255, 255, 255, 200),
true,
)?;
for (label, color) in legend_items {
let color_rect = Rect::from_xywh(legend_x, legend_y - 8.0, 12.0, 12.0).ok_or(
PlottingError::InvalidData {
message: "Invalid legend item dimensions".to_string(),
position: None,
},
)?;
self.draw_rectangle(
color_rect.left(),
color_rect.top(),
color_rect.width(),
color_rect.height(),
*color,
true,
)?;
self.draw_text(
label,
legend_x + 20.0,
legend_y,
legend_size,
Color::new_rgba(0, 0, 0, 255),
)?;
legend_y += legend_spacing;
}
Ok(())
}
pub fn draw_legend_positioned(
&mut self,
legend_items: &[(String, Color)],
plot_area: Rect,
position: crate::core::Position,
) -> Result<()> {
if legend_items.is_empty() {
return Ok(());
}
let legend_size = 12.0;
let legend_spacing = 20.0;
let legend_width = 140.0;
let legend_height = legend_items.len() as f32 * legend_spacing + 10.0;
let center_x = plot_area.left() + plot_area.width() / 2.0;
let center_y = plot_area.top() + plot_area.height() / 2.0;
let (legend_x, legend_y) = match position {
crate::core::Position::Best | crate::core::Position::TopRight => (
plot_area.right() - legend_width - 10.0,
plot_area.top() + 10.0,
),
crate::core::Position::TopLeft => (plot_area.left() + 10.0, plot_area.top() + 10.0),
crate::core::Position::TopCenter => {
(center_x - legend_width / 2.0, plot_area.top() + 10.0)
}
crate::core::Position::CenterLeft => {
(plot_area.left() + 10.0, center_y - legend_height / 2.0)
}
crate::core::Position::Center => (
center_x - legend_width / 2.0,
center_y - legend_height / 2.0,
),
crate::core::Position::CenterRight => (
plot_area.right() - legend_width - 10.0,
center_y - legend_height / 2.0,
),
crate::core::Position::BottomLeft => (
plot_area.left() + 10.0,
plot_area.bottom() - legend_height - 10.0,
),
crate::core::Position::BottomCenter => (
center_x - legend_width / 2.0,
plot_area.bottom() - legend_height - 10.0,
),
crate::core::Position::BottomRight => (
plot_area.right() - legend_width - 10.0,
plot_area.bottom() - legend_height - 10.0,
),
crate::core::Position::Custom { x, y } => (x, y),
};
let legend_bg =
Rect::from_xywh(legend_x - 10.0, legend_y - 5.0, legend_width, legend_height).ok_or(
PlottingError::InvalidData {
message: "Invalid legend dimensions".to_string(),
position: None,
},
)?;
self.draw_rectangle(
legend_bg.left(),
legend_bg.top(),
legend_bg.width(),
legend_bg.height(),
Color::new_rgba(255, 255, 255, 200),
true,
)?;
let mut item_y = legend_y + 10.0;
for (label, color) in legend_items {
let color_rect = Rect::from_xywh(legend_x, item_y - 8.0, 12.0, 12.0).ok_or(
PlottingError::InvalidData {
message: "Invalid legend item dimensions".to_string(),
position: None,
},
)?;
self.draw_rectangle(
color_rect.left(),
color_rect.top(),
color_rect.width(),
color_rect.height(),
*color,
true,
)?;
self.draw_text(
label,
legend_x + 20.0,
item_y,
legend_size,
Color::new_rgba(0, 0, 0, 255),
)?;
item_y += legend_spacing;
}
Ok(())
}
fn draw_legend_line_handle(
&mut self,
x: f32,
y: f32,
length: f32,
color: Color,
style: &LineStyle,
width: f32,
) -> Result<()> {
self.draw_line(x, y, x + length, y, color, width, style.clone())
}
fn draw_legend_scatter_handle(
&mut self,
x: f32,
y: f32,
length: f32,
color: Color,
marker: &MarkerStyle,
size: f32,
) -> Result<()> {
let center_x = x + length / 2.0;
self.draw_marker(center_x, y, size, *marker, color)
}
fn draw_legend_bar_handle(
&mut self,
x: f32,
y: f32,
length: f32,
height: f32,
color: Color,
) -> Result<()> {
let rect_y = y - height / 2.0;
self.draw_rectangle(x, rect_y, length, height, color, true)
}
fn draw_legend_line_marker_handle(
&mut self,
x: f32,
y: f32,
length: f32,
color: Color,
line_style: &LineStyle,
line_width: f32,
marker: &MarkerStyle,
marker_size: f32,
) -> Result<()> {
self.draw_legend_line_handle(x, y, length, color, line_style, line_width)?;
self.draw_legend_scatter_handle(x, y, length, color, marker, marker_size)
}
fn draw_legend_handle(
&mut self,
item: &LegendItem,
x: f32,
y: f32,
spacing: &LegendSpacingPixels,
) -> Result<()> {
let handle_length = spacing.handle_length;
let handle_height = spacing.handle_height;
match &item.item_type {
LegendItemType::Line { style, width } => {
let scaled_width = self.points_to_pixels(*width);
self.draw_legend_line_handle(x, y, handle_length, item.color, style, scaled_width)?;
}
LegendItemType::Scatter { marker, size } => {
let scaled_size = self.points_to_pixels(*size);
self.draw_legend_scatter_handle(
x,
y,
handle_length,
item.color,
marker,
scaled_size,
)?;
}
LegendItemType::LineMarker {
line_style,
line_width,
marker,
marker_size,
} => {
let scaled_line_width = self.points_to_pixels(*line_width);
let scaled_marker_size = self.points_to_pixels(*marker_size);
self.draw_legend_line_marker_handle(
x,
y,
handle_length,
item.color,
line_style,
scaled_line_width,
marker,
scaled_marker_size,
)?;
}
LegendItemType::Bar | LegendItemType::Histogram => {
self.draw_legend_bar_handle(x, y, handle_length, handle_height, item.color)?;
}
LegendItemType::Area { edge_color } => {
self.draw_legend_bar_handle(x, y, handle_length, handle_height, item.color)?;
if let Some(edge) = edge_color {
let rect_y = y - handle_height / 2.0;
let scaled_edge_width = self.logical_pixels_to_pixels(1.0);
self.draw_rectangle_outline(
x,
rect_y,
handle_length,
handle_height,
*edge,
scaled_edge_width,
)?;
}
}
LegendItemType::ErrorBar => {
let center_x = x + handle_length / 2.0;
let error_height = handle_height * 0.8;
let half_error = error_height / 2.0;
let cap_width = handle_height * 0.5;
let half_cap = cap_width / 2.0;
let error_line_width = self.logical_pixels_to_pixels(1.5);
self.draw_line(
center_x,
y - half_error,
center_x,
y + half_error,
item.color,
error_line_width,
LineStyle::Solid,
)?;
self.draw_line(
center_x - half_cap,
y - half_error,
center_x + half_cap,
y - half_error,
item.color,
error_line_width,
LineStyle::Solid,
)?;
self.draw_line(
center_x - half_cap,
y + half_error,
center_x + half_cap,
y + half_error,
item.color,
error_line_width,
LineStyle::Solid,
)?;
let marker_size = handle_height * 0.4;
self.draw_marker(center_x, y, marker_size, MarkerStyle::Circle, item.color)?;
}
}
if item.has_error_bars && !matches!(item.item_type, LegendItemType::ErrorBar) {
let center_x = x + handle_length / 2.0;
let error_height = handle_height * 0.7; let half_error = error_height / 2.0;
let cap_width = handle_height * 0.4;
let half_cap = cap_width / 2.0;
let overlay_line_width = self.logical_pixels_to_pixels(1.0);
self.draw_line(
center_x,
y - half_error,
center_x,
y + half_error,
item.color,
overlay_line_width,
LineStyle::Solid,
)?;
self.draw_line(
center_x - half_cap,
y - half_error,
center_x + half_cap,
y - half_error,
item.color,
overlay_line_width,
LineStyle::Solid,
)?;
self.draw_line(
center_x - half_cap,
y + half_error,
center_x + half_cap,
y + half_error,
item.color,
overlay_line_width,
LineStyle::Solid,
)?;
}
Ok(())
}
fn draw_rectangle_outline(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
color: Color,
line_width: f32,
) -> Result<()> {
let x2 = x + width;
let y2 = y + height;
self.draw_line(x, y, x2, y, color, line_width, LineStyle::Solid)?;
self.draw_line(x2, y, x2, y2, color, line_width, LineStyle::Solid)?;
self.draw_line(x2, y2, x, y2, color, line_width, LineStyle::Solid)?;
self.draw_line(x, y2, x, y, color, line_width, LineStyle::Solid)
}
fn draw_rounded_rectangle_outline(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
corner_radius: f32,
color: Color,
line_width: f32,
) -> Result<()> {
let max_radius = (width.min(height) / 2.0).max(0.0);
let radius = corner_radius.min(max_radius);
if radius < 0.1 {
return self.draw_rectangle_outline(x, y, width, height, color, line_width);
}
let mut pb = PathBuilder::new();
pb.move_to(x + radius, y);
pb.line_to(x + width - radius, y);
pb.quad_to(x + width, y, x + width, y + radius);
pb.line_to(x + width, y + height - radius);
pb.quad_to(x + width, y + height, x + width - radius, y + height);
pb.line_to(x + radius, y + height);
pb.quad_to(x, y + height, x, y + height - radius);
pb.line_to(x, y + radius);
pb.quad_to(x, y, x + radius, y);
pb.close();
let path = pb.finish().ok_or(PlottingError::RenderError(
"Failed to create rounded rectangle outline path".to_string(),
))?;
let mut paint = Paint::default();
paint.set_color(color.to_tiny_skia_color());
paint.anti_alias = true;
let stroke = Stroke {
width: line_width,
line_cap: LineCap::Round,
line_join: LineJoin::Round,
..Stroke::default()
};
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
Ok(())
}
fn draw_legend_frame(
&mut self,
x: f32,
y: f32,
width: f32,
height: f32,
style: &LegendStyle,
) -> Result<()> {
if !style.visible {
return Ok(());
}
let radius = style.effective_corner_radius();
if style.shadow {
let (shadow_dx, shadow_dy) = style.shadow_offset;
if radius > 0.0 {
self.draw_rounded_rectangle(
x + shadow_dx,
y + shadow_dy,
width,
height,
radius,
style.shadow_color,
true,
)?;
} else {
self.draw_rectangle(
x + shadow_dx,
y + shadow_dy,
width,
height,
style.shadow_color,
true,
)?;
}
}
let face_color = style.effective_face_color();
if radius > 0.0 {
self.draw_rounded_rectangle(x, y, width, height, radius, face_color, true)?;
} else {
self.draw_rectangle(x, y, width, height, face_color, true)?;
}
if let Some(edge_color) = style.edge_color {
if radius > 0.0 {
self.draw_rounded_rectangle_outline(
x,
y,
width,
height,
radius,
edge_color,
style.border_width,
)?;
} else {
self.draw_rectangle_outline(x, y, width, height, edge_color, style.border_width)?;
}
}
Ok(())
}
fn calculate_legend_dimensions(
&self,
items: &[LegendItem],
legend: &Legend,
char_width: f32,
) -> (f32, f32) {
legend.calculate_size(items, char_width)
}
pub fn draw_legend_full(
&mut self,
items: &[LegendItem],
legend: &Legend,
plot_area: Rect,
data_bboxes: Option<&[(f32, f32, f32, f32)]>,
) -> Result<()> {
if items.is_empty() || !legend.enabled {
return Ok(());
}
let spacing = legend.spacing.to_pixels(legend.font_size);
let char_width = legend.font_size * 0.6;
let (legend_width, legend_height) =
self.calculate_legend_dimensions(items, legend, char_width);
let plot_bounds = (
plot_area.left(),
plot_area.top(),
plot_area.right(),
plot_area.bottom(),
);
let position = if matches!(legend.position, LegendPosition::Best) {
let bboxes = data_bboxes.unwrap_or(&[]);
if bboxes.iter().map(|b| 1).sum::<usize>() > 100000 {
LegendPosition::UpperRight
} else {
find_best_position(
(legend_width, legend_height),
plot_bounds,
bboxes,
&legend.spacing,
legend.font_size,
)
}
} else {
legend.position
};
let resolved_legend = Legend {
position,
..legend.clone()
};
let (legend_x, legend_y) =
resolved_legend.calculate_position((legend_width, legend_height), plot_bounds);
self.draw_legend_frame(
legend_x,
legend_y,
legend_width,
legend_height,
&legend.style,
)?;
let item_x = legend_x + spacing.border_pad;
let mut item_y = legend_y + spacing.border_pad + legend.font_size / 2.0;
if let Some(ref title) = legend.title {
let title_x = legend_x + legend_width / 2.0;
self.draw_text_centered(title, title_x, item_y, legend.font_size, legend.text_color)?;
item_y += legend.font_size + spacing.label_spacing;
}
let items_per_col = items.len().div_ceil(legend.columns);
let max_label_len = items.iter().map(|item| item.label.len()).max().unwrap_or(0);
let label_width = max_label_len as f32 * char_width;
let col_width = spacing.handle_length + spacing.handle_text_pad + label_width;
for col in 0..legend.columns {
let col_x = item_x + col as f32 * (col_width + spacing.column_spacing);
let mut row_y = item_y;
for row in 0..items_per_col {
let idx = col * items_per_col + row;
if idx >= items.len() {
break;
}
let item = &items[idx];
self.draw_legend_handle(item, col_x, row_y, &spacing)?;
let text_x = col_x + spacing.handle_length + spacing.handle_text_pad;
let centered_y = row_y - legend.font_size * 0.65;
self.draw_text(
&item.label,
text_x,
centered_y,
legend.font_size,
legend.text_color,
)?;
row_y += legend.font_size + spacing.label_spacing;
}
}
Ok(())
}
pub fn draw_colorbar(
&mut self,
colormap: &crate::render::ColorMap,
vmin: f64,
vmax: f64,
x: f32,
y: f32,
width: f32,
height: f32,
value_scale: &crate::axes::AxisScale,
label: Option<&str>,
foreground_color: Color,
tick_font_size: f32,
label_font_size: Option<f32>,
show_log_subticks: bool,
) -> Result<()> {
let label_font_size = label_font_size.unwrap_or(tick_font_size * 1.1);
let num_segments = (height as usize).max(50);
let segment_height = height / num_segments as f32;
for i in 0..num_segments {
let normalized = 1.0 - (i as f64 / (num_segments - 1).max(1) as f64);
let color = colormap.sample(normalized);
let segment_y = y + i as f32 * segment_height;
self.draw_solid_rectangle(x, segment_y, width, segment_height + 0.5, color)?;
}
let stroke_width = 1.0;
self.draw_rectangle(x, y, width, height, foreground_color, false)?;
let ticks = compute_colorbar_ticks(vmin, vmax, value_scale, show_log_subticks);
let mut measured_major_labels = Vec::with_capacity(ticks.major_labels.len());
let mut max_label_width: f32 = 0.0;
for label_text in &ticks.major_labels {
let label_snippet = self.generated_label(label_text);
let (text_width, _) = self.measure_text(&label_snippet, tick_font_size)?;
let ink_center_from_top =
self.measure_text_ink_center_from_top(&label_snippet, tick_font_size)?;
max_label_width = max_label_width.max(text_width);
measured_major_labels.push((label_snippet, ink_center_from_top));
}
let rotated_label_width = if let Some(label_text) = label {
Some(self.measure_text(label_text, label_font_size)?.1)
} else {
None
};
let log_decade_base_center = matches!(value_scale, crate::axes::AxisScale::Log)
.then(|| self.measure_text_ink_center_from_top("10", tick_font_size))
.transpose()?;
let layout = compute_colorbar_layout_metrics(
width,
tick_font_size,
max_label_width,
rotated_label_width,
);
for minor_value in &ticks.minor_values {
let t = value_scale
.normalized_position(*minor_value, vmin, vmax)
.clamp(0.0, 1.0);
let tick_y = y + height * (1.0 - t as f32);
self.draw_line(
x + width,
tick_y,
x + width + layout.minor_tick_width,
tick_y,
foreground_color,
stroke_width * 0.8,
LineStyle::Solid,
)?;
}
for ((value, _), (label_text, ink_center_from_top)) in ticks
.major_values
.iter()
.zip(ticks.major_labels.iter())
.zip(measured_major_labels.iter())
{
let t = value_scale
.normalized_position(*value, vmin, vmax)
.clamp(0.0, 1.0);
let tick_y = y + height * (1.0 - t as f32);
self.draw_line(
x + width,
tick_y,
x + width + layout.major_tick_width,
tick_y,
foreground_color,
stroke_width,
LineStyle::Solid,
)?;
let anchor_center = colorbar_major_label_anchor_center_from_top(
value_scale,
label_text,
*ink_center_from_top,
log_decade_base_center,
);
let label_y = colorbar_major_label_top(tick_y, anchor_center);
self.draw_text(
label_text,
x + layout.tick_label_x_offset,
label_y,
tick_font_size,
foreground_color,
)?;
}
if let Some((label, label_center_x_offset)) =
label.zip(layout.rotated_label_center_x_offset)
{
let label_x = x + label_center_x_offset;
let label_y = y + height / 2.0;
self.draw_text_rotated(label, label_x, label_y, label_font_size, foreground_color)?;
}
Ok(())
}
pub fn into_image(self) -> Image {
Image {
width: self.width,
height: self.height,
pixels: self.pixmap.data().to_vec(),
}
}
pub fn save_png<P: AsRef<Path>>(&self, path: P) -> Result<()> {
crate::export::write_bytes_atomic(path, &self.encode_png_bytes()?)
}
pub fn encode_png_bytes(&self) -> Result<Vec<u8>> {
let image = Image {
width: self.width,
height: self.height,
pixels: self.pixmap.clone().take_demultiplied(),
};
crate::export::encode_rgba_png(&image)
}
pub fn export_svg<P: AsRef<Path>>(&self, path: P, width: u32, height: u32) -> Result<()> {
let svg_content = format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<svg width="{}" height="{}" xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill="{}"/>
<text x="50%" y="50%" text-anchor="middle" font-family="Arial" font-size="16">
Ruviz Plot ({} x {})
</text>
</svg>"#,
width, height, self.theme.background, width, height
);
crate::export::write_bytes_atomic(path, svg_content.as_bytes())
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn draw_subplot(
&mut self,
subplot_image: crate::core::plot::Image,
x: u32,
y: u32,
) -> Result<()> {
let subplot_pixmap = tiny_skia::Pixmap::from_vec(
subplot_image.pixels,
tiny_skia::IntSize::from_wh(subplot_image.width, subplot_image.height).ok_or_else(
|| PlottingError::InvalidInput("Invalid subplot dimensions".to_string()),
)?,
)
.ok_or_else(|| PlottingError::RenderError("Failed to create subplot pixmap".to_string()))?;
self.pixmap.draw_pixmap(
x as i32,
y as i32,
subplot_pixmap.as_ref(),
&tiny_skia::PixmapPaint::default(),
tiny_skia::Transform::identity(),
None,
);
Ok(())
}
}
#[cfg(test)]
mod tests;