use crate::aes::Aesthetic;
use crate::render::Rect;
use crate::scale::ScaleSet;
use super::{AxisSpan, Coord};
pub struct CoordSf {
x_span: Option<AxisSpan>,
y_span: Option<AxisSpan>,
xlim: Option<(f64, f64)>,
ylim: Option<(f64, f64)>,
expand: bool,
}
impl Default for CoordSf {
fn default() -> Self {
CoordSf {
x_span: None,
y_span: None,
xlim: None,
ylim: None,
expand: true,
}
}
}
impl CoordSf {
pub fn new() -> Self {
Self::default()
}
pub fn xlim(mut self, min: f64, max: f64) -> Self {
self.xlim = Some((min, max));
self
}
pub fn ylim(mut self, min: f64, max: f64) -> Self {
self.ylim = Some((min, max));
self
}
pub fn expand(mut self, on: bool) -> Self {
self.expand = on;
self
}
fn ratio(&self) -> f64 {
match (&self.x_span, &self.y_span) {
(Some(x), Some(y)) if (x.max - x.min).abs() > f64::EPSILON => {
((y.max - y.min) / (x.max - x.min)).abs()
}
_ => 1.0,
}
}
}
impl Coord for CoordSf {
fn set_domains(&mut self, x: Option<AxisSpan>, y: Option<AxisSpan>) {
self.x_span = x;
self.y_span = y;
}
fn transform(&self, point: (f64, f64), plot_area: &Rect) -> (f64, f64) {
let data_aspect = self.ratio();
let pixel_aspect = plot_area.height / plot_area.width;
let (eff_w, eff_h, off_x, off_y) = if pixel_aspect > data_aspect {
let h = plot_area.width * data_aspect;
(plot_area.width, h, 0.0, (plot_area.height - h) / 2.0)
} else {
let w = plot_area.height / data_aspect;
(w, plot_area.height, (plot_area.width - w) / 2.0, 0.0)
};
let (nx, ny) = point;
let px = plot_area.x + off_x + nx * eff_w;
let py = plot_area.y + off_y + (1.0 - ny) * eff_h;
(px, py)
}
fn zoom_x(&self) -> Option<(f64, f64)> {
self.xlim
}
fn zoom_y(&self) -> Option<(f64, f64)> {
self.ylim
}
fn train_scales(&mut self, scales: &mut ScaleSet) {
if !self.expand {
if let Some(s) = scales.get_mut(&Aesthetic::X) {
s.set_expand(0.0, 0.0);
}
if let Some(s) = scales.get_mut(&Aesthetic::Y) {
s.set_expand(0.0, 0.0);
}
}
}
fn supports_axis_reserve(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use crate::data::Value;
use crate::prelude::*;
use crate::theme::LegendPosition;
#[test]
fn fixed_xlim_ylim_expand_false_fills_the_panel_with_no_margin() {
let svg = GGPlot::new(vec![
("x".into(), vec![Value::Float(0.0), Value::Float(100.0)]),
("y".into(), vec![Value::Float(0.0), Value::Float(50.0)]),
])
.aes(Aes::new().x("x").y("y"))
.geom_point()
.theme(crate::theme::presets::theme_void())
.legend_position(LegendPosition::None)
.coord_sf_with(
CoordSf::new()
.xlim(0.0, 100.0)
.ylim(0.0, 50.0)
.expand(false),
)
.render_svg_native_with_size(400, 200)
.unwrap();
assert!(svg.contains("data-plot=\"0.00 0.00 400.00 200.00\""));
}
#[test]
fn data_origin_and_extent_map_to_the_expected_corners() {
let plot = || {
GGPlot::new(vec![
("x".into(), vec![Value::Float(0.0), Value::Float(100.0)]),
("y".into(), vec![Value::Float(0.0), Value::Float(50.0)]),
])
.aes(Aes::new().x("x").y("y"))
.geom_point()
.theme(crate::theme::presets::theme_void())
.legend_position(LegendPosition::None)
.coord_sf_with(
CoordSf::new()
.xlim(0.0, 100.0)
.ylim(0.0, 50.0)
.expand(false),
)
};
let svg = plot().render_svg_native_with_size(400, 200).unwrap();
assert!(svg.contains("cx=\"0.00\" cy=\"200.00\""));
assert!(svg.contains("cx=\"400.00\" cy=\"0.00\""));
}
#[test]
fn a_segment_crossing_the_window_still_renders() {
let svg = GGPlot::new(vec![
("x".into(), vec![Value::Float(-50.0)]),
("y".into(), vec![Value::Float(25.0)]),
("xend".into(), vec![Value::Float(50.0)]),
("yend".into(), vec![Value::Float(25.0)]),
])
.aes(Aes::new().x("x").y("y").xend("xend").yend("yend"))
.geom_segment()
.theme(crate::theme::presets::theme_void())
.legend_position(LegendPosition::None)
.coord_sf_with(
CoordSf::new()
.xlim(0.0, 100.0)
.ylim(0.0, 50.0)
.expand(false),
)
.render_svg_native_with_size(400, 200)
.unwrap();
assert!(svg.contains("<polyline"));
assert!(!svg.contains("NaN"));
}
#[test]
fn svg_root_data_false_omits_data_plot_and_data_domain() {
let plot = || {
GGPlot::new(vec![
("x".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
("y".into(), vec![Value::Float(0.0), Value::Float(1.0)]),
])
.aes(Aes::new().x("x").y("y"))
.geom_point()
.coord_sf()
};
let with_data = plot().render_svg_native_with_size(200, 200).unwrap();
assert!(with_data.contains("data-plot="));
let without_data = plot()
.svg_root_data(false)
.render_svg_native_with_size(200, 200)
.unwrap();
assert!(!without_data.contains("data-plot="));
assert!(!without_data.contains("data-domain="));
}
}