use crate::artist::{Artist, AxesImage, Collection, Line2D, Patch, QuadMesh};
use std::borrow::Cow;
use crate::axis::axis::{Axis, AxisSide};
use crate::axis::dates::{AutoDateLocator, ConciseDateFormatter};
use crate::axis::scale::{AsinhScale, LinearScale, LogScale, LogitScale, Scale, SymlogScale};
use crate::axis::ticker::{
AsinhFormatterMathtext, AsinhLocator, AutoLocator, LogFormatterMathtext, LogLocator,
LogitFormatterMathtext, LogitLocator, ScalarFormatter, SymlogFormatterMathtext, SymlogLocator,
};
use crate::core::color::{DEFAULT_COLOR_CYCLE, Rgba};
use crate::core::rcparams::RcParams;
use crate::core::{Affine2D, Bbox, Path};
use crate::render::{ClippedRenderer, GraphicsContext, Renderer};
use crate::text::FontSource;
use crate::figure::richtext::layout_rich_text;
const DEFAULT_MARGIN: f64 = 0.05;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum ScaleSpec {
Linear,
Log { base: f64 },
Symlog { base: f64, linthresh: f64 },
Logit,
Asinh { linear_width: f64 },
}
const DEFAULT_TITLE_SIZE: f64 = 12.0;
const DEFAULT_TITLE_PAD: f64 = 6.0;
fn default_prop_cycle() -> Vec<Rgba> {
DEFAULT_COLOR_CYCLE
.iter()
.filter_map(|hex| Rgba::from_hex(hex))
.collect()
}
const DEFAULT_ANNOTATION_SIZE: f64 = 10.0;
const ANNOTATION_ARROW_GAP: f64 = 4.0;
const ANNOTATION_ARROW_SHRINK: f64 = 3.0;
const ANNOTATION_HEAD_LEN: f64 = 8.0;
const ANNOTATION_HEAD_HALF_WIDTH: f64 = 3.5;
const SCOPE_BG: Rgba = Rgba::new(0.035, 0.05, 0.04, 1.0);
const SCOPE_GRID: Rgba = Rgba::new(0.0, 0.9, 0.35, 0.16);
const SCOPE_GRID_CENTER: Rgba = Rgba::new(0.0, 0.9, 0.35, 0.32);
const SCOPE_BEZEL: Rgba = Rgba::new(0.0, 0.9, 0.35, 0.55);
const SCOPE_TEXT: Rgba = Rgba::new(0.25, 1.0, 0.45, 0.92);
const SCOPE_X_DIVS: usize = 10;
const SCOPE_Y_DIVS: usize = 4;
const SCOPE_TEXT_SIZE: f64 = 8.0;
const SCOPE_TEXT_PAD: f64 = 3.0;
const SCOPE_CYCLE: [Rgba; 4] = [
Rgba::new(0.22, 1.0, 0.08, 1.0), Rgba::new(1.0, 0.69, 0.0, 1.0), Rgba::new(0.0, 0.9, 1.0, 1.0), Rgba::new(1.0, 0.36, 0.88, 1.0), ];
fn format_scope_value(v: f64) -> String {
if !v.is_finite() {
return format!("{v}");
}
let a = v.abs();
if a != 0.0 && !(0.01..10_000.0).contains(&a) {
format!("{v:.1e}")
} else if a >= 100.0 {
format!("{v:.0}")
} else {
format!("{v:.2}")
}
}
#[derive(Debug, Clone)]
struct SecondaryXAxis {
scale: f64,
offset: f64,
label: Option<String>,
}
#[derive(Debug, Clone)]
struct Annotation {
text: String,
xy: (f64, f64),
text_at: Option<(f64, f64)>,
color: Rgba,
size: f64,
}
impl ScaleSpec {
fn transform(self, value: f64) -> f64 {
match self {
ScaleSpec::Linear => value,
ScaleSpec::Log { base } => LogScale::new(base).transform(value),
ScaleSpec::Symlog { base, linthresh } => {
SymlogScale::new(base, linthresh, 1.0).transform(value)
}
ScaleSpec::Logit => LogitScale::new().transform(value),
ScaleSpec::Asinh { linear_width } => AsinhScale::new(linear_width).transform(value),
}
}
fn inverse(self, value: f64) -> f64 {
match self {
ScaleSpec::Linear => value,
ScaleSpec::Log { base } => LogScale::new(base).inverse(value),
ScaleSpec::Symlog { base, linthresh } => {
SymlogScale::new(base, linthresh, 1.0).inverse(value)
}
ScaleSpec::Logit => LogitScale::new().inverse(value),
ScaleSpec::Asinh { linear_width } => AsinhScale::new(linear_width).inverse(value),
}
}
fn is_linear(self) -> bool {
matches!(self, ScaleSpec::Linear)
}
fn limit_range(self, limits: (f64, f64), minpos: f64) -> (f64, f64) {
match self {
ScaleSpec::Linear => guard_range(limits),
ScaleSpec::Log { base } => guard_log_range(limits, base, minpos),
ScaleSpec::Symlog { .. } => guard_range(limits),
ScaleSpec::Logit => guard_logit_range(limits, minpos),
ScaleSpec::Asinh { .. } => guard_range(limits),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct DataToScaled {
x: ScaleSpec,
y: ScaleSpec,
}
impl DataToScaled {
pub(crate) const fn new(x: ScaleSpec, y: ScaleSpec) -> Self {
Self { x, y }
}
pub(crate) fn map_point(&self, x: f64, y: f64) -> [f64; 2] {
[self.x.transform(x), self.y.transform(y)]
}
pub(crate) fn inverse_point(&self, x: f64, y: f64) -> [f64; 2] {
[self.x.inverse(x), self.y.inverse(y)]
}
pub(crate) fn is_linear(&self) -> bool {
self.x.is_linear() && self.y.is_linear()
}
pub(crate) fn map_path(&self, path: &Path) -> Path {
let vertices = path
.vertices()
.iter()
.map(|&[x, y]| self.map_point(x, y))
.collect();
let codes = path.codes().map(|codes| codes.to_vec());
Path::new(vertices, codes)
}
pub(crate) fn map_path_cow<'a>(&self, path: &'a Path) -> Cow<'a, Path> {
if self.is_linear() {
Cow::Borrowed(path)
} else {
Cow::Owned(self.map_path(path))
}
}
pub(crate) fn map_points_cow<'a>(&self, points: &'a [[f64; 2]]) -> Cow<'a, [[f64; 2]]> {
if self.is_linear() {
Cow::Borrowed(points)
} else {
Cow::Owned(points.iter().map(|&[x, y]| self.map_point(x, y)).collect())
}
}
pub(crate) fn map_limits(
&self,
xlim: (f64, f64),
ylim: (f64, f64),
) -> ((f64, f64), (f64, f64)) {
let [x0, y0] = self.map_point(xlim.0, ylim.0);
let [x1, y1] = self.map_point(xlim.1, ylim.1);
((x0, x1), (y0, y1))
}
}
#[cfg(test)]
fn scaled_tick_position(value: f64, limits: (f64, f64), scale: ScaleSpec) -> f64 {
let mapper = DataToScaled::new(scale, ScaleSpec::Linear);
let (scaled_limits, _) = mapper.map_limits(limits, (0.0, 1.0));
let scaled = mapper.map_point(value, 0.0)[0];
let denom = scaled_limits.1 - scaled_limits.0;
if denom == 0.0 {
0.0
} else {
(scaled - scaled_limits.0) / denom
}
}
pub struct Axes {
position: Bbox,
pub(crate) layout_envelope: Option<Bbox>,
pub(crate) sticky_x: Vec<f64>,
pub(crate) sticky_y: Vec<f64>,
xlim: Option<(f64, f64)>,
ylim: Option<(f64, f64)>,
margins: f64,
xscale: ScaleSpec,
yscale: ScaleSpec,
facecolor: Rgba,
edgecolor: Rgba,
linewidth: f64,
title_color: Rgba,
prop_cycle: Vec<Rgba>,
pub(crate) legend_facecolor: Rgba,
pub(crate) legend_edgecolor: Rgba,
pub(crate) legend_labelcolor: Rgba,
pub(crate) lines: Vec<Line2D>,
pub(crate) patches: Vec<Patch>,
pub(crate) collections: Vec<Collection>,
pub(crate) images: Vec<AxesImage>,
pub(crate) meshes: Vec<QuadMesh>,
pub(crate) extra_data_bbox: Option<Bbox>,
xaxis: Axis,
yaxis: Axis,
xaxis_hidden: bool,
pub(crate) xlim_link: Option<usize>,
secondary_x: Option<SecondaryXAxis>,
title: Option<String>,
annotations: Vec<Annotation>,
frame: bool,
pub(crate) aspect_equal: bool,
pub(crate) axis_visible: bool,
scope: bool,
pub(crate) prop_cycle_index: usize,
pub(crate) span_lines: Vec<SpanLine>,
pub(crate) span_rects: Vec<SpanRect>,
pub(crate) legend: Vec<crate::figure::legend::LegendEntry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SpanOrientation {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct SpanLine {
pub(crate) orientation: SpanOrientation,
pub(crate) value: f64,
pub(crate) color: Rgba,
pub(crate) linewidth: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct SpanRect {
pub(crate) orientation: SpanOrientation,
pub(crate) lo: f64,
pub(crate) hi: f64,
pub(crate) facecolor: Rgba,
}
enum DrawableArtist<'a> {
Line(&'a Line2D),
Patch(&'a Patch),
Collection(&'a Collection),
}
impl DrawableArtist<'_> {
fn zorder(&self) -> f64 {
match self {
DrawableArtist::Line(line) => line.zorder(),
DrawableArtist::Patch(patch) => patch.zorder(),
DrawableArtist::Collection(collection) => collection.zorder(),
}
}
fn visible(&self) -> bool {
match self {
DrawableArtist::Line(line) => line.visible(),
DrawableArtist::Patch(patch) => patch.visible(),
DrawableArtist::Collection(collection) => collection.visible(),
}
}
fn draw_scaled(
&self,
renderer: &mut dyn Renderer,
transform: &Affine2D,
mapper: &DataToScaled,
) {
match self {
DrawableArtist::Line(line) => {
let points = line.points();
let path = Path::from_polyline(&points);
let path = mapper.map_path_cow(&path);
line.draw_path(renderer, path.as_ref(), transform);
}
DrawableArtist::Patch(patch) => {
let path = mapper.map_path_cow(patch.path());
patch.draw_path(renderer, path.as_ref(), transform);
}
DrawableArtist::Collection(collection) => {
let offsets = mapper.map_points_cow(collection.offsets());
collection.draw_with_offsets(renderer, offsets.as_ref(), transform);
}
}
}
}
impl Axes {
#[must_use]
pub fn new(position: Bbox) -> Self {
Self {
position,
xlim: None,
ylim: None,
layout_envelope: None,
sticky_x: Vec::new(),
sticky_y: Vec::new(),
margins: DEFAULT_MARGIN,
xscale: ScaleSpec::Linear,
yscale: ScaleSpec::Linear,
facecolor: Rgba::WHITE,
edgecolor: Rgba::BLACK,
linewidth: 0.8,
title_color: Rgba::BLACK,
prop_cycle: default_prop_cycle(),
legend_facecolor: Rgba::WHITE,
legend_edgecolor: Rgba::from_u8(128, 128, 128, 255),
legend_labelcolor: Rgba::BLACK,
lines: Vec::new(),
patches: Vec::new(),
collections: Vec::new(),
images: Vec::new(),
meshes: Vec::new(),
extra_data_bbox: None,
xaxis: Axis::new(AxisSide::Bottom),
yaxis: Axis::new(AxisSide::Left),
xaxis_hidden: false,
xlim_link: None,
secondary_x: None,
title: None,
annotations: Vec::new(),
frame: true,
aspect_equal: false,
axis_visible: true,
scope: false,
prop_cycle_index: 0,
span_lines: Vec::new(),
span_rects: Vec::new(),
legend: Vec::new(),
}
}
pub fn oscilloscope(&mut self) -> &mut Self {
self.scope = true;
self.facecolor = SCOPE_BG;
self.axis_visible = false;
self.frame = false;
self
}
const FALLBACK_CYCLE_COLOR: Rgba = Rgba::new(0.121_568_63, 0.466_666_67, 0.705_882_35, 1.0);
#[must_use]
pub fn cycle_color(&self, index: usize) -> Rgba {
if self.scope {
return SCOPE_CYCLE[index % SCOPE_CYCLE.len()];
}
if self.prop_cycle.is_empty() {
return Self::FALLBACK_CYCLE_COLOR;
}
self.prop_cycle[index % self.prop_cycle.len()]
}
pub fn next_cycle_color(&mut self) -> Rgba {
let color = self.cycle_color(self.prop_cycle_index);
self.prop_cycle_index += 1;
color
}
pub fn set_prop_cycle_reset(&mut self) -> &mut Self {
self.prop_cycle_index = 0;
self
}
pub fn plot(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
let color = self.next_cycle_color();
self.stick_x_extremes(x);
self.lines
.push(Line2D::new(x.to_vec(), y.to_vec()).with_color(color));
self.lines.last_mut().expect("just pushed a line")
}
pub fn plot_with_color(&mut self, x: &[f64], y: &[f64], color: Rgba) -> &mut Line2D {
self.stick_x_extremes(x);
self.lines
.push(Line2D::new(x.to_vec(), y.to_vec()).with_color(color));
self.lines.last_mut().expect("just pushed a line")
}
pub fn add_line(&mut self, line: Line2D) -> &mut Line2D {
self.lines.push(line);
self.lines.last_mut().expect("just pushed a line")
}
#[must_use]
pub fn line_count(&self) -> usize {
self.lines.len()
}
pub fn set_x_tick_labels_visible(&mut self, visible: bool) -> &mut Self {
self.xaxis.set_tick_labels_visible(visible);
self
}
pub fn set_line_data(&mut self, line: usize, x: &[f64], y: &[f64]) -> Result<(), String> {
let count = self.lines.len();
let entry = self
.lines
.get_mut(line)
.ok_or_else(|| format!("line index {line} out of range (axes has {count} lines)"))?;
entry.set_data(x, y);
Ok(())
}
pub fn set_collection_offsets(
&mut self,
collection: usize,
x: &[f64],
y: &[f64],
) -> Result<(), String> {
let count = self.collections.len();
let entry = self.collections.get_mut(collection).ok_or_else(|| {
format!("collection index {collection} out of range (axes has {count} collections)")
})?;
let n = x.len().min(y.len());
entry.set_offsets((0..n).map(|i| [x[i], y[i]]).collect());
Ok(())
}
pub fn set_image_data(
&mut self,
image: usize,
data: &[f64],
nrows: usize,
ncols: usize,
extent: Option<[f64; 4]>,
) -> Result<(), String> {
let count = self.images.len();
let entry = self
.images
.get_mut(image)
.ok_or_else(|| format!("image index {image} out of range (axes has {count} images)"))?;
entry.set_data(data.to_vec(), nrows, ncols)?;
if let Some(extent) = extent {
entry.set_extent(extent);
}
Ok(())
}
pub fn add_patch(&mut self, patch: Patch) -> &mut Patch {
self.patches.push(patch);
self.patches.last_mut().expect("just pushed a patch")
}
pub(crate) fn include_data_bbox(&mut self, xmin: f64, ymin: f64, xmax: f64, ymax: f64) {
let bbox = Bbox::from_extents(xmin, ymin, xmax, ymax);
self.extra_data_bbox = Some(match self.extra_data_bbox {
Some(b) => b.union(&bbox),
None => bbox,
});
}
pub fn set_xlim(&mut self, min: f64, max: f64) -> &mut Self {
self.xlim = Some((min, max));
self
}
pub fn set_ylim(&mut self, min: f64, max: f64) -> &mut Self {
self.ylim = Some((min, max));
self
}
pub fn set_title(&mut self, title: impl Into<String>) -> &mut Self {
self.title = Some(title.into());
self
}
pub fn text(&mut self, x: f64, y: f64, s: impl Into<String>) -> &mut Self {
self.annotations.push(Annotation {
text: s.into(),
xy: (x, y),
text_at: None,
color: Rgba::BLACK,
size: DEFAULT_ANNOTATION_SIZE,
});
self
}
pub fn annotate(
&mut self,
s: impl Into<String>,
xy: (f64, f64),
xytext: (f64, f64),
) -> &mut Self {
self.annotations.push(Annotation {
text: s.into(),
xy,
text_at: Some(xytext),
color: Rgba::BLACK,
size: DEFAULT_ANNOTATION_SIZE,
});
self
}
pub(crate) fn configure_as_twinx(&mut self, source: usize) {
self.facecolor = Rgba::TRANSPARENT;
self.frame = false;
self.xaxis_hidden = true;
self.yaxis = Axis::new(AxisSide::Right)
.with_color(self.yaxis.color())
.with_tick_direction(self.yaxis.tick_direction());
self.xlim_link = Some(source);
}
pub fn secondary_xaxis_linear(
&mut self,
scale: f64,
offset: f64,
label: Option<&str>,
) -> &mut Self {
self.secondary_x = Some(SecondaryXAxis {
scale,
offset,
label: label.map(str::to_string),
});
self
}
pub fn set_xlabel(&mut self, label: impl Into<String>) -> &mut Self {
self.xaxis = std::mem::replace(&mut self.xaxis, Axis::new(AxisSide::Bottom))
.with_label(label.into());
self
}
pub fn set_ylabel(&mut self, label: impl Into<String>) -> &mut Self {
self.yaxis =
std::mem::replace(&mut self.yaxis, Axis::new(AxisSide::Left)).with_label(label.into());
self
}
pub fn set_facecolor(&mut self, color: Rgba) -> &mut Self {
self.facecolor = color;
self
}
pub fn set_edgecolor(&mut self, color: Rgba) -> &mut Self {
self.edgecolor = color;
self
}
pub fn set_linewidth(&mut self, width: f64) -> &mut Self {
self.linewidth = width;
self
}
pub fn set_title_color(&mut self, color: Rgba) -> &mut Self {
self.title_color = color;
self
}
pub fn set_prop_cycle(&mut self, colors: Vec<Rgba>) -> &mut Self {
self.prop_cycle = colors;
self
}
pub fn xaxis_mut(&mut self) -> &mut Axis {
&mut self.xaxis
}
pub fn yaxis_mut(&mut self) -> &mut Axis {
&mut self.yaxis
}
pub fn grid(&mut self, on: bool) -> &mut Self {
self.xaxis.set_grid(on);
self.yaxis.set_grid(on);
self
}
pub fn grid_with(&mut self, color: Rgba, linewidth: f64, alpha: f64) -> &mut Self {
self.xaxis
.set_grid(true)
.set_grid_style(color, linewidth, alpha);
self.yaxis
.set_grid(true)
.set_grid_style(color, linewidth, alpha);
self
}
pub fn set_axis_color(&mut self, color: Rgba) -> &mut Self {
self.edgecolor = color;
self.xaxis.set_color(color);
self.yaxis.set_color(color);
self
}
pub(crate) fn apply_rcparams(&mut self, rc: &RcParams) {
self.facecolor = rc.axes_facecolor;
self.edgecolor = rc.axes_edgecolor;
self.linewidth = rc.axes_linewidth;
self.title_color = rc.text_color;
self.prop_cycle = rc.axes_prop_cycle.clone();
self.legend_facecolor = rc.legend_facecolor;
self.legend_edgecolor = rc.legend_edgecolor;
self.legend_labelcolor = rc.legend_labelcolor;
for (axis, direction) in [
(&mut self.xaxis, rc.xtick_direction),
(&mut self.yaxis, rc.ytick_direction),
] {
axis.set_color(rc.text_color);
axis.set_grid(rc.axes_grid);
axis.set_grid_style(rc.grid_color, rc.grid_linewidth, rc.grid_alpha);
axis.set_tick_direction(direction);
}
}
fn gathered_sticky_edges(&self) -> (Vec<f64>, Vec<f64>) {
let mut sx = self.sticky_x.clone();
let mut sy = self.sticky_y.clone();
let extents = self
.images
.iter()
.filter_map(Artist::data_extents)
.chain(self.meshes.iter().filter_map(Artist::data_extents));
for e in extents {
sx.push(e.xmin());
sx.push(e.xmax());
sy.push(e.ymin());
sy.push(e.ymax());
}
(sx, sy)
}
fn stick_x_extremes(&mut self, x: &[f64]) {
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for &v in x {
if v.is_finite() {
lo = lo.min(v);
hi = hi.max(v);
}
}
if lo <= hi {
self.sticky_x.push(lo);
self.sticky_x.push(hi);
}
}
pub fn set_margins(&mut self, margins: f64) -> &mut Self {
self.margins = margins;
self
}
pub fn set_xscale_linear(&mut self) -> &mut Self {
self.xscale = ScaleSpec::Linear;
self.xaxis
.set_scale(Box::new(LinearScale::new()))
.set_locator(Box::new(AutoLocator::new()))
.set_formatter(Box::new(ScalarFormatter::new()));
self
}
pub fn set_yscale_linear(&mut self) -> &mut Self {
self.yscale = ScaleSpec::Linear;
self.yaxis
.set_scale(Box::new(LinearScale::new()))
.set_locator(Box::new(AutoLocator::new()))
.set_formatter(Box::new(ScalarFormatter::new()));
self
}
pub fn set_xaxis_date(&mut self) -> &mut Self {
self.xscale = ScaleSpec::Linear;
self.xaxis
.set_scale(Box::new(LinearScale::new()))
.set_locator(Box::new(AutoDateLocator::new()))
.set_formatter(Box::new(ConciseDateFormatter::new()));
self
}
pub fn set_yaxis_date(&mut self) -> &mut Self {
self.yscale = ScaleSpec::Linear;
self.yaxis
.set_scale(Box::new(LinearScale::new()))
.set_locator(Box::new(AutoDateLocator::new()))
.set_formatter(Box::new(ConciseDateFormatter::new()));
self
}
pub fn set_xscale_log(&mut self, base: f64) -> &mut Self {
assert!(
base.is_finite() && base > 1.0,
"log scale base must be finite and > 1"
);
self.xscale = ScaleSpec::Log { base };
self.xaxis
.set_scale(Box::new(LogScale::new(base)))
.set_locator(Box::new(LogLocator::new(base)))
.set_formatter(Box::new(LogFormatterMathtext::new(base)));
self
}
pub fn set_yscale_log(&mut self, base: f64) -> &mut Self {
assert!(
base.is_finite() && base > 1.0,
"log scale base must be finite and > 1"
);
self.yscale = ScaleSpec::Log { base };
self.yaxis
.set_scale(Box::new(LogScale::new(base)))
.set_locator(Box::new(LogLocator::new(base)))
.set_formatter(Box::new(LogFormatterMathtext::new(base)));
self
}
pub fn set_xscale_symlog(&mut self, base: f64, linthresh: f64) -> &mut Self {
assert!(
base.is_finite() && base > 1.0,
"symlog scale base must be finite and > 1"
);
assert!(
linthresh.is_finite() && linthresh > 0.0,
"symlog scale linthresh must be finite and > 0"
);
self.xscale = ScaleSpec::Symlog { base, linthresh };
self.xaxis
.set_scale(Box::new(SymlogScale::new(base, linthresh, 1.0)))
.set_locator(Box::new(SymlogLocator::new(base, linthresh)))
.set_formatter(Box::new(SymlogFormatterMathtext::new(base, linthresh)));
self
}
pub fn set_yscale_symlog(&mut self, base: f64, linthresh: f64) -> &mut Self {
assert!(
base.is_finite() && base > 1.0,
"symlog scale base must be finite and > 1"
);
assert!(
linthresh.is_finite() && linthresh > 0.0,
"symlog scale linthresh must be finite and > 0"
);
self.yscale = ScaleSpec::Symlog { base, linthresh };
self.yaxis
.set_scale(Box::new(SymlogScale::new(base, linthresh, 1.0)))
.set_locator(Box::new(SymlogLocator::new(base, linthresh)))
.set_formatter(Box::new(SymlogFormatterMathtext::new(base, linthresh)));
self
}
pub fn set_xscale_asinh(&mut self, linear_width: f64) -> &mut Self {
assert!(
linear_width.is_finite() && linear_width > 0.0,
"asinh scale linear_width must be finite and > 0"
);
self.xscale = ScaleSpec::Asinh { linear_width };
self.xaxis
.set_scale(Box::new(AsinhScale::new(linear_width)))
.set_locator(Box::new(AsinhLocator::new(linear_width)))
.set_formatter(Box::new(AsinhFormatterMathtext::new(10.0, linear_width)));
self
}
pub fn set_yscale_asinh(&mut self, linear_width: f64) -> &mut Self {
assert!(
linear_width.is_finite() && linear_width > 0.0,
"asinh scale linear_width must be finite and > 0"
);
self.yscale = ScaleSpec::Asinh { linear_width };
self.yaxis
.set_scale(Box::new(AsinhScale::new(linear_width)))
.set_locator(Box::new(AsinhLocator::new(linear_width)))
.set_formatter(Box::new(AsinhFormatterMathtext::new(10.0, linear_width)));
self
}
pub fn set_xscale_logit(&mut self) -> &mut Self {
self.xscale = ScaleSpec::Logit;
self.xaxis
.set_scale(Box::new(LogitScale::new()))
.set_locator(Box::new(LogitLocator::new()))
.set_formatter(Box::new(LogitFormatterMathtext::new()));
self
}
pub fn set_yscale_logit(&mut self) -> &mut Self {
self.yscale = ScaleSpec::Logit;
self.yaxis
.set_scale(Box::new(LogitScale::new()))
.set_locator(Box::new(LogitLocator::new()))
.set_formatter(Box::new(LogitFormatterMathtext::new()));
self
}
pub fn semilogx(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_xscale_log(10.0);
self.plot(x, y)
}
pub fn semilogy(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_yscale_log(10.0);
self.plot(x, y)
}
pub fn loglog(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_xscale_log(10.0).set_yscale_log(10.0);
self.plot(x, y)
}
pub fn symlogx(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_xscale_symlog(10.0, 1.0);
self.plot(x, y)
}
pub fn symlogy(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_yscale_symlog(10.0, 1.0);
self.plot(x, y)
}
pub fn asinhx(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_xscale_asinh(1.0);
self.plot(x, y)
}
pub fn asinhy(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_yscale_asinh(1.0);
self.plot(x, y)
}
pub fn logitx(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_xscale_logit();
self.plot(x, y)
}
pub fn logity(&mut self, x: &[f64], y: &[f64]) -> &mut Line2D {
self.set_yscale_logit();
self.plot(x, y)
}
pub fn set_aspect_equal(&mut self) -> &mut Self {
self.aspect_equal = true;
self
}
pub fn set_axis_off(&mut self) -> &mut Self {
self.axis_visible = false;
self
}
pub fn set_axis_on(&mut self) -> &mut Self {
self.axis_visible = true;
self
}
#[must_use]
pub fn position(&self) -> Bbox {
self.position
}
#[must_use]
pub fn data_limits(&self) -> Option<Bbox> {
let mut acc: Option<Bbox> = None;
let line_extents = self.lines.iter().filter_map(Artist::data_extents);
let patch_extents = self.patches.iter().filter_map(Artist::data_extents);
let collection_extents = self.collections.iter().filter_map(Artist::data_extents);
let image_extents = self.images.iter().filter_map(Artist::data_extents);
let mesh_extents = self.meshes.iter().filter_map(Artist::data_extents);
for e in line_extents
.chain(patch_extents)
.chain(collection_extents)
.chain(image_extents)
.chain(mesh_extents)
.chain(self.extra_data_bbox)
{
acc = Some(match acc {
Some(a) => a.union(&e),
None => e,
});
}
acc
}
#[must_use]
pub fn effective_limits(&self) -> ((f64, f64), (f64, f64)) {
let data = self.data_limits();
let (sticky_x, sticky_y) = self.gathered_sticky_edges();
let x_margin = if self.scope { 0.0 } else { self.margins };
let xlim = self.xlim.unwrap_or_else(|| {
data.map_or((0.0, 1.0), |b| {
expand_range_sticky(b.xmin(), b.xmax(), x_margin, &sticky_x)
})
});
let ylim = self.ylim.unwrap_or_else(|| {
data.map_or((0.0, 1.0), |b| {
expand_range_sticky(b.ymin(), b.ymax(), self.margins, &sticky_y)
})
});
(guard_range(xlim), guard_range(ylim))
}
pub(crate) fn scale_limited_effective_limits(&self) -> ((f64, f64), (f64, f64)) {
let (xlim, ylim) = self.effective_limits();
self.clamp_limits_to_scale(xlim, ylim)
}
pub(crate) fn clamp_limits_to_scale(
&self,
xlim: (f64, f64),
ylim: (f64, f64),
) -> ((f64, f64), (f64, f64)) {
let (xminpos, yminpos) = self.min_positive_data();
(
self.xscale.limit_range(xlim, xminpos),
self.yscale.limit_range(ylim, yminpos),
)
}
fn min_positive_data(&self) -> (f64, f64) {
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
for line in &self.lines {
for [x, y] in line.points() {
if x.is_finite() && x > 0.0 {
xmin = xmin.min(x);
}
if y.is_finite() && y > 0.0 {
ymin = ymin.min(y);
}
}
}
for patch in &self.patches {
for &[x, y] in patch.path().vertices() {
if x.is_finite() && x > 0.0 {
xmin = xmin.min(x);
}
if y.is_finite() && y > 0.0 {
ymin = ymin.min(y);
}
}
}
for collection in &self.collections {
for &[x, y] in collection.offsets() {
if x.is_finite() && x > 0.0 {
xmin = xmin.min(x);
}
if y.is_finite() && y > 0.0 {
ymin = ymin.min(y);
}
}
}
if let Some(bbox) = self.extra_data_bbox {
for x in [bbox.xmin(), bbox.xmax()] {
if x.is_finite() && x > 0.0 {
xmin = xmin.min(x);
}
}
for y in [bbox.ymin(), bbox.ymax()] {
if y.is_finite() && y > 0.0 {
ymin = ymin.min(y);
}
}
}
let xmin = if xmin.is_finite() { xmin } else { 1.0 };
let ymin = if ymin.is_finite() { ymin } else { 1.0 };
(xmin, ymin)
}
#[must_use]
pub fn trans_data(&self, axes_px: &Bbox, xlim: (f64, f64), ylim: (f64, f64)) -> Affine2D {
let (xmin, xmax) = xlim;
let (ymin, ymax) = ylim;
let sx = axes_px.width() / (xmax - xmin);
let sy = axes_px.height() / (ymax - ymin);
Affine2D::from_translation(-xmin, -ymin)
.scale(sx, sy)
.translate(axes_px.xmin(), axes_px.ymin())
}
pub(crate) fn data_to_scaled(&self) -> DataToScaled {
DataToScaled::new(self.xscale, self.yscale)
}
#[must_use]
pub(crate) fn pixel_rect_and_trans_data_in(
&self,
fig_w_px: f64,
fig_h_px: f64,
xlim_override: Option<(f64, f64)>,
rect_override: Option<Bbox>,
) -> (Bbox, Affine2D) {
let mut axes_px = rect_override.unwrap_or_else(|| {
Bbox::from_extents(
self.position.xmin() * fig_w_px,
self.position.ymin() * fig_h_px,
self.position.xmax() * fig_w_px,
self.position.ymax() * fig_h_px,
)
});
let (xlim, ylim) = self.limits_with_override(xlim_override);
let mapper = self.data_to_scaled();
let (scaled_xlim, scaled_ylim) = mapper.map_limits(xlim, ylim);
if self.aspect_equal {
axes_px = equalize_aspect(&axes_px, scaled_xlim, scaled_ylim);
}
let td = self.trans_data(&axes_px, scaled_xlim, scaled_ylim);
(axes_px, td)
}
pub(crate) fn layout_insets(
&self,
font: &FontSource,
s: f64,
xlim_override: Option<(f64, f64)>,
) -> (f64, f64, f64, f64) {
let (mut left, mut right, mut bottom, mut top) = (0.0f64, 0.0f64, 0.0f64, 0.0f64);
if self.axis_visible {
let (xlim, ylim) = self.limits_with_override(xlim_override);
if !self.xaxis_hidden {
bottom = self.xaxis.decoration_extent(xlim, font, s);
let (lo, hi) = self.xaxis.end_label_overhangs(xlim, font, s);
left = left.max(lo);
right = right.max(hi);
}
let y_extent = self.yaxis.decoration_extent(ylim, font, s);
if self.yaxis_is_right() {
right = right.max(y_extent);
} else {
left = left.max(y_extent);
}
let (y_lo, y_hi) = self.yaxis.end_label_overhangs(ylim, font, s);
bottom = bottom.max(y_lo);
top = self.secondary_extent(xlim, font, s).max(y_hi);
}
if let Some(title) = &self.title
&& !title.is_empty()
{
let rich = layout_rich_text(font, title, DEFAULT_TITLE_SIZE * s);
top += DEFAULT_TITLE_PAD * s + rich.ascent + rich.descent;
}
(left, right, bottom, top)
}
fn secondary_extent(&self, xlim: (f64, f64), font: &FontSource, s: f64) -> f64 {
let Some(sec) = &self.secondary_x else {
return 0.0;
};
let converted = (
sec.scale * xlim.0 + sec.offset,
sec.scale * xlim.1 + sec.offset,
);
let mut top_axis = Axis::new(AxisSide::Top);
if let Some(label) = &sec.label {
top_axis = top_axis.with_label(label.clone());
}
top_axis.decoration_extent(converted, font, s)
}
fn yaxis_is_right(&self) -> bool {
self.yaxis.side() == AxisSide::Right
}
pub(crate) fn limits_with_override(
&self,
xlim_override: Option<(f64, f64)>,
) -> ((f64, f64), (f64, f64)) {
let (xlim, ylim) = self.scale_limited_effective_limits();
match xlim_override {
Some(over) => {
let (x, y) = self.clamp_limits_to_scale(over, ylim);
(x, y)
}
None => (xlim, ylim),
}
}
pub fn draw(
&self,
renderer: &mut dyn Renderer,
fig_w_px: f64,
fig_h_px: f64,
font: &FontSource,
) {
self.draw_with(renderer, fig_w_px, fig_h_px, font, None, None);
}
pub(crate) fn draw_with(
&self,
renderer: &mut dyn Renderer,
fig_w_px: f64,
fig_h_px: f64,
font: &FontSource,
xlim_override: Option<(f64, f64)>,
rect_override: Option<Bbox>,
) {
let (axes_px, td) =
self.pixel_rect_and_trans_data_in(fig_w_px, fig_h_px, xlim_override, rect_override);
let (xlim, ylim) = self.limits_with_override(xlim_override);
let mapper = self.data_to_scaled();
let rect = rect_path(&axes_px);
let fill_gc = GraphicsContext::new();
renderer.draw_path(&fill_gc, &rect, &Affine2D::identity(), Some(self.facecolor));
if self.scope {
self.draw_scope_graticule(renderer, &axes_px);
}
{
let clipped = &mut ClippedRenderer::new(renderer, axes_px);
if mapper.is_linear() {
for image in &self.images {
if image.visible() {
image.draw(clipped, &td);
}
}
}
if mapper.is_linear() {
for mesh in &self.meshes {
if mesh.visible() {
mesh.draw(clipped, &td);
}
}
}
for span in &self.span_rects {
let rect = match span.orientation {
SpanOrientation::Horizontal => {
rect_path(&Bbox::from_extents(xlim.0, span.lo, xlim.1, span.hi))
}
SpanOrientation::Vertical => {
rect_path(&Bbox::from_extents(span.lo, ylim.0, span.hi, ylim.1))
}
};
clipped.draw_path(
&GraphicsContext::new(),
&mapper.map_path(&rect),
&td,
Some(span.facecolor),
);
}
let mut artists: Vec<DrawableArtist<'_>> =
Vec::with_capacity(self.lines.len() + self.patches.len() + self.collections.len());
artists.extend(self.lines.iter().map(DrawableArtist::Line));
artists.extend(self.patches.iter().map(DrawableArtist::Patch));
artists.extend(self.collections.iter().map(DrawableArtist::Collection));
let mut order: Vec<usize> = (0..artists.len())
.filter(|&i| artists[i].visible())
.collect();
order.sort_by(|&a, &b| {
artists[a]
.zorder()
.partial_cmp(&artists[b].zorder())
.unwrap_or(std::cmp::Ordering::Equal)
});
for i in order {
artists[i].draw_scaled(clipped, &td, &mapper);
}
for span in &self.span_lines {
let points = match span.orientation {
SpanOrientation::Horizontal => [[xlim.0, span.value], [xlim.1, span.value]],
SpanOrientation::Vertical => [[span.value, ylim.0], [span.value, ylim.1]],
};
let path = Path::from_polyline(&points);
let gc = GraphicsContext::new()
.with_stroke(span.color)
.with_line_width(span.linewidth);
clipped.draw_path(&gc, &mapper.map_path(&path), &td, None);
}
}
if self.frame && self.axis_visible {
let frame_gc = GraphicsContext::new()
.with_stroke(self.edgecolor)
.with_line_width(self.linewidth);
renderer.draw_path(&frame_gc, &rect, &Affine2D::identity(), None);
}
if self.axis_visible {
if !self.xaxis_hidden {
self.xaxis.draw(renderer, &axes_px, xlim, font);
}
self.yaxis.draw(renderer, &axes_px, ylim, font);
if let Some(sec) = &self.secondary_x {
let mut top = Axis::new(AxisSide::Top);
if let Some(label) = &sec.label {
top = top.with_label(label.clone());
}
let converted = (
sec.scale * xlim.0 + sec.offset,
sec.scale * xlim.1 + sec.offset,
);
top.draw(renderer, &axes_px, converted, font);
}
}
self.draw_legend(renderer, &axes_px, font);
for ann in &self.annotations {
self.draw_annotation(renderer, ann, &td, font);
}
if let Some(title) = &self.title
&& !title.is_empty()
{
let s = renderer.decoration_scale();
let rich = layout_rich_text(font, title, DEFAULT_TITLE_SIZE * s);
let cx = (axes_px.xmin() + axes_px.xmax()) / 2.0;
let x = cx - rich.width / 2.0;
let secondary_clearance = self.secondary_extent(xlim, font, s);
let y = axes_px.ymax() + DEFAULT_TITLE_PAD * s + secondary_clearance;
let shift = Affine2D::from_translation(x, y);
for path in &rich.paths {
renderer.draw_path(
&GraphicsContext::new(),
&path.transformed(&shift),
&Affine2D::identity(),
Some(self.title_color),
);
}
}
if self.scope {
self.draw_scope_overlay(renderer, &axes_px, xlim, ylim, font);
}
}
}
impl Axes {
fn draw_scope_graticule(&self, renderer: &mut dyn Renderer, axes_px: &Bbox) {
let mut stroke = |points: [[f64; 2]; 2], color: Rgba| {
let gc = GraphicsContext::new()
.with_stroke(color)
.with_line_width(1.0);
renderer.draw_path(
&gc,
&Path::from_polyline(&points),
&Affine2D::identity(),
None,
);
};
for i in 1..SCOPE_X_DIVS {
let x = axes_px.xmin() + axes_px.width() * i as f64 / SCOPE_X_DIVS as f64;
let color = if 2 * i == SCOPE_X_DIVS {
SCOPE_GRID_CENTER
} else {
SCOPE_GRID
};
stroke([[x, axes_px.ymin()], [x, axes_px.ymax()]], color);
}
for j in 1..SCOPE_Y_DIVS {
let y = axes_px.ymin() + axes_px.height() * j as f64 / SCOPE_Y_DIVS as f64;
let color = if 2 * j == SCOPE_Y_DIVS {
SCOPE_GRID_CENTER
} else {
SCOPE_GRID
};
stroke([[axes_px.xmin(), y], [axes_px.xmax(), y]], color);
}
}
fn draw_scope_overlay(
&self,
renderer: &mut dyn Renderer,
axes_px: &Bbox,
xlim: (f64, f64),
ylim: (f64, f64),
font: &FontSource,
) {
let bezel_gc = GraphicsContext::new()
.with_stroke(SCOPE_BEZEL)
.with_line_width(1.0);
renderer.draw_path(&bezel_gc, &rect_path(axes_px), &Affine2D::identity(), None);
let s = renderer.decoration_scale();
let size = SCOPE_TEXT_SIZE * s;
let pad = SCOPE_TEXT_PAD * s;
let mut draw = |text: &str, x: f64, baseline_y: f64| {
let rich = layout_rich_text(font, text, size);
let shift = Affine2D::from_translation(x, baseline_y);
for path in &rich.paths {
renderer.draw_path(
&GraphicsContext::new(),
&path.transformed(&shift),
&Affine2D::identity(),
Some(SCOPE_TEXT),
);
}
};
let right_align = |text: &str| {
let rich = layout_rich_text(font, text, size);
axes_px.xmax() - pad - rich.width
};
let top = format_scope_value(ylim.1);
let bottom = format_scope_value(ylim.0);
let span = format!("\u{394}{}", format_scope_value(xlim.1 - xlim.0));
let ascent = layout_rich_text(font, &top, size).ascent;
draw(&top, right_align(&top), axes_px.ymax() - pad - ascent);
draw(&bottom, right_align(&bottom), axes_px.ymin() + pad);
draw(&span, axes_px.xmin() + pad, axes_px.ymin() + pad);
}
fn draw_annotation(
&self,
renderer: &mut dyn Renderer,
ann: &Annotation,
td: &Affine2D,
font: &FontSource,
) {
let mapper = self.data_to_scaled();
let to_display = |(x, y): (f64, f64)| {
let [sx, sy] = mapper.map_point(x, y);
td.transform_point((sx, sy))
};
let s = renderer.decoration_scale();
let anchor = to_display(ann.text_at.unwrap_or(ann.xy));
let rich = layout_rich_text(font, &ann.text, ann.size * s);
let shift = Affine2D::from_translation(anchor.0, anchor.1);
for path in &rich.paths {
renderer.draw_path(
&GraphicsContext::new(),
&path.transformed(&shift),
&Affine2D::identity(),
Some(ann.color),
);
}
if ann.text_at.is_some() {
let target = to_display(ann.xy);
let from = if target.0 < anchor.0 {
anchor
} else {
(anchor.0 + rich.width, anchor.1)
};
draw_annotation_arrow(renderer, from, target, ann.color, s);
}
}
}
fn draw_annotation_arrow(
renderer: &mut dyn Renderer,
from: (f64, f64),
to: (f64, f64),
color: Rgba,
s: f64,
) {
let (gap, shrink, head_len) = (
ANNOTATION_ARROW_GAP * s,
ANNOTATION_ARROW_SHRINK * s,
ANNOTATION_HEAD_LEN * s,
);
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
let len = dx.hypot(dy);
if len <= gap + shrink + head_len {
return;
}
let (ux, uy) = (dx / len, dy / len);
let tail = (from.0 + ux * gap, from.1 + uy * gap);
let tip = (to.0 - ux * shrink, to.1 - uy * shrink);
let head_base = (tip.0 - ux * head_len, tip.1 - uy * head_len);
let gc = GraphicsContext::new()
.with_stroke(color)
.with_line_width(1.0);
let shaft = Path::from_polyline(&[[tail.0, tail.1], [head_base.0, head_base.1]]);
renderer.draw_path(&gc, &shaft, &Affine2D::identity(), None);
let (px, py) = (-uy, ux);
let head = Path::from_polyline(&[
[tip.0, tip.1],
[
head_base.0 + px * ANNOTATION_HEAD_HALF_WIDTH * s,
head_base.1 + py * ANNOTATION_HEAD_HALF_WIDTH * s,
],
[
head_base.0 - px * ANNOTATION_HEAD_HALF_WIDTH * s,
head_base.1 - py * ANNOTATION_HEAD_HALF_WIDTH * s,
],
[tip.0, tip.1],
]);
renderer.draw_path(
&GraphicsContext::new(),
&head,
&Affine2D::identity(),
Some(color),
);
}
fn rect_path(bbox: &Bbox) -> Path {
let (x0, y0) = (bbox.xmin(), bbox.ymin());
let (x1, y1) = (bbox.xmax(), bbox.ymax());
Path::from_polyline(&[[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]])
}
fn equalize_aspect(rect: &Bbox, xlim: (f64, f64), ylim: (f64, f64)) -> Bbox {
let xrange = xlim.1 - xlim.0;
let yrange = ylim.1 - ylim.0;
let (w, h) = (rect.width(), rect.height());
let scale_x = w / xrange;
let scale_y = h / yrange;
let scale = scale_x.min(scale_y);
let new_w = xrange * scale;
let new_h = yrange * scale;
let cx = (rect.xmin() + rect.xmax()) / 2.0;
let cy = (rect.ymin() + rect.ymax()) / 2.0;
Bbox::from_extents(
cx - new_w / 2.0,
cy - new_h / 2.0,
cx + new_w / 2.0,
cy + new_h / 2.0,
)
}
fn expand_range(min: f64, max: f64, margin: f64) -> (f64, f64) {
let span = max - min;
let pad = span * margin;
(min - pad, max + pad)
}
fn expand_range_sticky(min: f64, max: f64, margin: f64, sticky: &[f64]) -> (f64, f64) {
let (mut lo, mut hi) = expand_range(min, max, margin);
let lo_bound = sticky
.iter()
.copied()
.filter(|s| s.is_finite() && *s <= min)
.fold(f64::NEG_INFINITY, f64::max);
if lo < lo_bound {
lo = lo_bound;
}
let hi_bound = sticky
.iter()
.copied()
.filter(|s| s.is_finite() && *s >= max)
.fold(f64::INFINITY, f64::min);
if hi > hi_bound {
hi = hi_bound;
}
(lo, hi)
}
fn guard_range((min, max): (f64, f64)) -> (f64, f64) {
let magnitude = min.abs().max(max.abs());
if (max - min).abs() > f64::EPSILON * magnitude {
return (min, max);
}
let pad = if magnitude > f64::MIN_POSITIVE {
magnitude * 0.05
} else {
0.5
};
(min - pad, max + pad)
}
fn guard_log_range((a, b): (f64, f64), base: f64, minpos: f64) -> (f64, f64) {
let reversed = a > b;
let (mut lo, mut hi) = if reversed { (b, a) } else { (a, b) };
let minpos = if minpos.is_finite() && minpos > 0.0 {
minpos
} else {
1.0
};
if !lo.is_finite() || !hi.is_finite() || hi <= 0.0 {
lo = minpos;
hi = minpos * base;
} else {
if lo <= 0.0 {
lo = minpos.min(hi / base);
}
if hi <= lo {
lo = (lo / base).max(f64::MIN_POSITIVE);
hi *= base;
}
}
if reversed { (hi, lo) } else { (lo, hi) }
}
fn guard_logit_range((a, b): (f64, f64), minpos: f64) -> (f64, f64) {
let reversed = a > b;
let (mut lo, mut hi) = if reversed { (b, a) } else { (a, b) };
let eps = if minpos.is_finite() && minpos > 0.0 && minpos < 0.5 {
minpos.min(1e-7)
} else {
1e-7
};
if !lo.is_finite() || !hi.is_finite() || hi <= 0.0 || lo >= 1.0 {
lo = eps;
hi = 1.0 - eps;
} else {
if lo <= 0.0 {
lo = eps;
}
if hi >= 1.0 {
hi = 1.0 - eps;
}
if hi <= lo {
lo = eps;
hi = 1.0 - eps;
}
}
if reversed { (hi, lo) } else { (lo, hi) }
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn trans_data_maps_corners_to_axes_pixels() {
let axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let axes_px = Bbox::from_extents(50.0, 60.0, 250.0, 360.0);
let xlim = (-1.0, 3.0);
let ylim = (10.0, 20.0);
let td = axes.trans_data(&axes_px, xlim, ylim);
let ll = td.transform_point((xlim.0, ylim.0));
approx(ll.0, axes_px.xmin());
approx(ll.1, axes_px.ymin());
let ur = td.transform_point((xlim.1, ylim.1));
approx(ur.0, axes_px.xmax());
approx(ur.1, axes_px.ymax());
let lr = td.transform_point((xlim.1, ylim.0));
approx(lr.0, axes_px.xmax());
approx(lr.1, axes_px.ymin());
let ul = td.transform_point((xlim.0, ylim.1));
approx(ul.0, axes_px.xmin());
approx(ul.1, axes_px.ymax());
}
#[test]
fn data_to_scaled_linear_is_exact_identity_for_points_paths_and_limits() {
let mapper = DataToScaled::new(ScaleSpec::Linear, ScaleSpec::Linear);
let path = Path::unit_rectangle();
assert_eq!(mapper.map_point(-3.0, 4.5), [-3.0, 4.5]);
assert_eq!(mapper.map_path(&path), path);
assert_eq!(
mapper.map_limits((-1.0, 3.0), (10.0, 20.0)),
((-1.0, 3.0), (10.0, 20.0))
);
}
#[test]
fn data_to_scaled_log_maps_points_paths_and_limits() {
let mapper = DataToScaled::new(ScaleSpec::Log { base: 10.0 }, ScaleSpec::Linear);
let path = Path::from_polyline(&[[1.0, 2.0], [10.0, 4.0], [100.0, 8.0]]);
let scaled = mapper.map_path(&path);
assert_eq!(mapper.map_point(1000.0, 5.0), [3.0, 5.0]);
assert_eq!(scaled.vertices(), &[[0.0, 2.0], [1.0, 4.0], [2.0, 8.0]]);
assert_eq!(scaled.codes(), path.codes());
assert_eq!(
mapper.map_limits((1.0, 1000.0), (2.0, 8.0)),
((0.0, 3.0), (2.0, 8.0))
);
}
#[test]
fn data_to_scaled_symlog_maps_negative_zero_and_positive_points() {
let scale = ScaleSpec::Symlog {
base: 10.0,
linthresh: 1.0,
};
let mapper = DataToScaled::new(scale, ScaleSpec::Linear);
let path = Path::from_polyline(&[[-100.0, 2.0], [0.0, 4.0], [100.0, 8.0]]);
let scaled = mapper.map_path(&path);
approx(mapper.map_point(0.0, 5.0)[0], 0.0);
approx(
mapper.map_point(100.0, 5.0)[0],
-mapper.map_point(-100.0, 5.0)[0],
);
approx(scaled.vertices()[1][0], 0.0);
assert!(scaled.vertices()[0][0] < 0.0);
assert!(scaled.vertices()[2][0] > 0.0);
assert_eq!(scaled.codes(), path.codes());
}
#[test]
fn data_to_scaled_logit_maps_probability_points_and_limits() {
let mapper = DataToScaled::new(ScaleSpec::Logit, ScaleSpec::Linear);
let path = Path::from_polyline(&[[0.1, 2.0], [0.5, 4.0], [0.9, 8.0]]);
let scaled = mapper.map_path(&path);
approx(mapper.map_point(0.5, 5.0)[0], 0.0);
approx(
mapper.map_point(0.9, 5.0)[0],
-mapper.map_point(0.1, 5.0)[0],
);
approx(scaled.vertices()[1][0], 0.0);
assert!(scaled.vertices()[0][0] < 0.0);
assert!(scaled.vertices()[2][0] > 0.0);
assert_eq!(scaled.codes(), path.codes());
let ((x0, x1), ylim) = mapper.map_limits((0.1, 0.9), (2.0, 8.0));
approx(x0, -x1);
assert_eq!(ylim, (2.0, 8.0));
}
#[test]
fn data_to_scaled_asinh_maps_negative_zero_and_positive_points() {
let scale = ScaleSpec::Asinh { linear_width: 2.0 };
let mapper = DataToScaled::new(scale, ScaleSpec::Linear);
let path = Path::from_polyline(&[[-100.0, 2.0], [0.0, 4.0], [100.0, 8.0]]);
let scaled = mapper.map_path(&path);
approx(mapper.map_point(0.0, 5.0)[0], 0.0);
approx(
mapper.map_point(100.0, 5.0)[0],
-mapper.map_point(-100.0, 5.0)[0],
);
approx(scaled.vertices()[1][0], 0.0);
assert!(scaled.vertices()[0][0] < 0.0);
assert!(scaled.vertices()[2][0] > 0.0);
assert_eq!(scaled.codes(), path.codes());
}
#[test]
fn log_tick_position_uses_scaled_coordinate_fraction() {
let scale = ScaleSpec::Log { base: 10.0 };
approx(scaled_tick_position(10.0, (1.0, 1000.0), scale), 1.0 / 3.0);
approx(scaled_tick_position(100.0, (1.0, 1000.0), scale), 2.0 / 3.0);
}
#[test]
fn symlog_tick_position_uses_scaled_coordinate_fraction() {
let scale = ScaleSpec::Symlog {
base: 10.0,
linthresh: 1.0,
};
approx(scaled_tick_position(0.0, (-100.0, 100.0), scale), 0.5);
assert!(scaled_tick_position(-10.0, (-100.0, 100.0), scale) < 0.5);
assert!(scaled_tick_position(10.0, (-100.0, 100.0), scale) > 0.5);
}
#[test]
fn logit_tick_position_uses_scaled_coordinate_fraction() {
approx(
scaled_tick_position(0.5, (0.01, 0.99), ScaleSpec::Logit),
0.5,
);
assert!(scaled_tick_position(0.1, (0.01, 0.99), ScaleSpec::Logit) < 0.5);
assert!(scaled_tick_position(0.9, (0.01, 0.99), ScaleSpec::Logit) > 0.5);
}
#[test]
fn asinh_tick_position_uses_scaled_coordinate_fraction() {
let scale = ScaleSpec::Asinh { linear_width: 2.0 };
approx(scaled_tick_position(0.0, (-100.0, 100.0), scale), 0.5);
assert!(scaled_tick_position(-10.0, (-100.0, 100.0), scale) < 0.5);
assert!(scaled_tick_position(10.0, (-100.0, 100.0), scale) > 0.5);
}
#[test]
fn symlog_effective_limits_preserve_negative_zero_positive_domain() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(
&[-100.0, -1.0, 0.0, 1.0, 100.0],
&[-1.0, 0.0, 1.0, 0.0, -1.0],
);
axes.set_xscale_symlog(10.0, 1.0).set_xlim(-100.0, 100.0);
let (xlim, _) = axes.scale_limited_effective_limits();
assert_eq!(xlim, (-100.0, 100.0));
let (axes_px, td) = axes.pixel_rect_and_trans_data_in(200.0, 100.0, None, None);
assert!(axes_px.width().is_finite());
assert!(td.transform_point((0.0, 0.0)).0.is_finite());
}
#[test]
fn logit_effective_limits_clamp_to_open_probability_domain() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(&[0.01, 0.5, 0.99], &[0.0, 1.0, 2.0]);
axes.set_xscale_logit().set_xlim(-1.0, 2.0);
let (xlim, _) = axes.scale_limited_effective_limits();
assert!(xlim.0 > 0.0, "logit lower bound must be open: {xlim:?}");
assert!(xlim.1 < 1.0, "logit upper bound must be open: {xlim:?}");
let (axes_px, td) = axes.pixel_rect_and_trans_data_in(200.0, 100.0, None, None);
assert!(axes_px.width().is_finite());
assert!(
td.transform_point((LogitScale::new().transform(xlim.0), 0.0))
.0
.is_finite()
);
}
#[test]
fn asinh_effective_limits_preserve_negative_zero_positive_domain() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(
&[-100.0, -1.0, 0.0, 1.0, 100.0],
&[-1.0, 0.0, 1.0, 0.0, -1.0],
);
axes.set_xscale_asinh(2.0).set_xlim(-100.0, 100.0);
let (xlim, _) = axes.scale_limited_effective_limits();
assert_eq!(xlim, (-100.0, 100.0));
let (axes_px, td) = axes.pixel_rect_and_trans_data_in(200.0, 100.0, None, None);
assert!(axes_px.width().is_finite());
assert!(td.transform_point((0.0, 0.0)).0.is_finite());
}
#[test]
fn symlog_wrappers_set_expected_axis_scales() {
let mut x_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
x_axes.symlogx(&[-10.0, 0.0, 10.0], &[1.0, 2.0, 3.0]);
assert_eq!(
x_axes.xscale,
ScaleSpec::Symlog {
base: 10.0,
linthresh: 1.0
}
);
assert_eq!(x_axes.yscale, ScaleSpec::Linear);
let mut y_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
y_axes.symlogy(&[-1.0, 0.0, 1.0], &[-10.0, 0.0, 10.0]);
assert_eq!(y_axes.xscale, ScaleSpec::Linear);
assert_eq!(
y_axes.yscale,
ScaleSpec::Symlog {
base: 10.0,
linthresh: 1.0
}
);
}
#[test]
fn logit_wrappers_set_expected_axis_scales() {
let mut x_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
x_axes.logitx(&[0.01, 0.5, 0.99], &[1.0, 2.0, 3.0]);
assert_eq!(x_axes.xscale, ScaleSpec::Logit);
assert_eq!(x_axes.yscale, ScaleSpec::Linear);
let mut y_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
y_axes.logity(&[1.0, 2.0, 3.0], &[0.01, 0.5, 0.99]);
assert_eq!(y_axes.xscale, ScaleSpec::Linear);
assert_eq!(y_axes.yscale, ScaleSpec::Logit);
}
#[test]
fn asinh_wrappers_set_expected_axis_scales() {
let mut x_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
x_axes.asinhx(&[-10.0, 0.0, 10.0], &[1.0, 2.0, 3.0]);
assert_eq!(x_axes.xscale, ScaleSpec::Asinh { linear_width: 1.0 });
assert_eq!(x_axes.yscale, ScaleSpec::Linear);
let mut y_axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
y_axes.asinhy(&[-1.0, 0.0, 1.0], &[-10.0, 0.0, 10.0]);
assert_eq!(y_axes.xscale, ScaleSpec::Linear);
assert_eq!(y_axes.yscale, ScaleSpec::Asinh { linear_width: 1.0 });
}
#[test]
fn date_axis_setters_keep_linear_scale() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.set_xaxis_date();
assert_eq!(axes.xscale, ScaleSpec::Linear);
assert_eq!(axes.yscale, ScaleSpec::Linear);
axes.set_yscale_log(10.0).set_yaxis_date();
assert_eq!(axes.xscale, ScaleSpec::Linear);
assert_eq!(axes.yscale, ScaleSpec::Linear);
}
#[derive(Default)]
struct GeometryRecorder {
paths: Vec<Vec<[f64; 2]>>,
origins: Vec<[f64; 2]>,
}
impl Renderer for GeometryRecorder {
fn draw_path(
&mut self,
_gc: &GraphicsContext,
path: &Path,
transform: &Affine2D,
_fill: Option<Rgba>,
) {
let (ox, oy) = transform.transform_point((0.0, 0.0));
self.origins.push([ox, oy]);
self.paths.push(
path.vertices()
.iter()
.map(|&[x, y]| {
let (tx, ty) = transform.transform_point((x, y));
[tx, ty]
})
.collect(),
);
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
}
#[test]
fn draw_scaled_maps_line_patch_and_collection_geometry() {
let mapper = DataToScaled::new(ScaleSpec::Log { base: 10.0 }, ScaleSpec::Linear);
let id = Affine2D::identity();
let line = Line2D::new(vec![1.0, 10.0, 100.0], vec![2.0, 4.0, 8.0]);
let patch = Patch::rectangle(1.0, 2.0, 9.0, 3.0);
let collection = Collection::scatter(vec![[1.0, 2.0], [100.0, 8.0]]);
let mut line_renderer = GeometryRecorder::default();
DrawableArtist::Line(&line).draw_scaled(&mut line_renderer, &id, &mapper);
assert_eq!(
line_renderer.paths[0],
vec![[0.0, 2.0], [1.0, 4.0], [2.0, 8.0]]
);
let mut patch_renderer = GeometryRecorder::default();
DrawableArtist::Patch(&patch).draw_scaled(&mut patch_renderer, &id, &mapper);
assert_eq!(patch_renderer.paths[0][0], [0.0, 2.0]);
assert_eq!(patch_renderer.paths[0][1], [1.0, 2.0]);
let mut collection_renderer = GeometryRecorder::default();
DrawableArtist::Collection(&collection).draw_scaled(&mut collection_renderer, &id, &mapper);
assert_eq!(collection_renderer.origins, vec![[0.0, 2.0], [2.0, 8.0]]);
}
#[test]
fn scale_limited_effective_limits_clamp_nonpositive_log_bounds() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(&[1.0, 10.0, 100.0], &[0.0, 1.0, 2.0]);
axes.set_xscale_log(10.0).set_xlim(-10.0, 100.0);
let (xlim, _) = axes.scale_limited_effective_limits();
assert!(xlim.0 > 0.0, "log lower bound must be positive: {xlim:?}");
assert_eq!(xlim.1, 100.0);
let (axes_px, td) = axes.pixel_rect_and_trans_data_in(200.0, 100.0, None, None);
assert!(axes_px.width().is_finite());
assert!(td.transform_point((xlim.0.log10(), 0.0)).0.is_finite());
}
#[test]
fn pixel_rect_and_trans_data_linear_matches_direct_affine() {
let mut axes = Axes::new(Bbox::from_extents(0.1, 0.2, 0.9, 0.8));
axes.set_xlim(-1.0, 3.0).set_ylim(10.0, 20.0);
let (axes_px, td) = axes.pixel_rect_and_trans_data_in(500.0, 400.0, None, None);
let direct = axes.trans_data(&axes_px, (-1.0, 3.0), (10.0, 20.0));
let points = [[-1.0, 10.0], [3.0, 20.0], [1.5, 13.0]];
for [x, y] in points {
assert_eq!(td.transform_point((x, y)), direct.transform_point((x, y)));
}
}
#[test]
fn autoscale_pads_y_but_keeps_line_x_tight() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let xs: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let ys: Vec<f64> = xs.iter().map(|&x| (x / 10.0) * 2.0 - 1.0).collect();
axes.plot(&xs, &ys);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 0.0);
approx(xlim.1, 10.0);
approx(ylim.0, -1.1);
approx(ylim.1, 1.1);
}
#[test]
fn bars_and_hist_sit_on_the_zero_baseline() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.bar(&[0.0, 1.0, 2.0], &[3.0, 7.0, 5.0]);
let (_, ylim) = axes.effective_limits();
approx(ylim.0, 0.0); assert!(ylim.1 > 7.0);
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.hist(&[0.0, 0.1, 0.2, 0.9, 1.0], 4);
let (_, ylim) = axes.effective_limits();
approx(ylim.0, 0.0);
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.barh(&[0.0, 1.0], &[4.0, 6.0]);
let (xlim, _) = axes.effective_limits();
approx(xlim.0, 0.0); }
#[test]
fn images_stay_flush_with_their_extent() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.imshow(&[0.0, 1.0, 2.0, 3.0], 2, 2);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 0.0);
approx(xlim.1, 2.0);
approx(ylim.0, 0.0);
approx(ylim.1, 2.0);
}
#[test]
fn image_sticky_edges_track_a_changed_extent() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.imshow(&[0.0, 1.0, 2.0, 3.0], 2, 2)
.set_extent([10.0, 30.0, -5.0, 5.0]);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 10.0);
approx(xlim.1, 30.0);
approx(ylim.0, -5.0);
approx(ylim.1, 5.0);
}
#[test]
fn contourf_and_hist2d_meshes_stay_flush() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let z: Vec<f64> = (0..9).map(f64::from).collect();
axes.contourf(&z, 3, 3);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 0.0);
approx(xlim.1, 2.0);
approx(ylim.0, 0.0);
approx(ylim.1, 2.0);
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.hist2d(&[0.0, 1.0, 2.0, 3.0], &[0.0, 1.0, 2.0, 3.0], 4);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 0.0);
approx(xlim.1, 3.0);
approx(ylim.0, 0.0);
approx(ylim.1, 3.0);
}
#[test]
fn contour_lines_stay_flush_with_their_grid() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let z = [0.0, 1.0, 2.0, 0.0, 1.0, 2.0, 0.0, 1.0, 2.0];
axes.contour(&z, 3, 3);
let (xlim, ylim) = axes.effective_limits();
approx(xlim.0, 0.0);
approx(xlim.1, 2.0);
approx(ylim.0, 0.0);
approx(ylim.1, 2.0);
}
#[test]
fn sticky_edges_do_not_shrink_data_beyond_them() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.bar(&[0.0, 1.0], &[2.0, 3.0]);
axes.plot(&[0.0, 1.0], &[-5.0, 3.0]);
let (_, ylim) = axes.effective_limits();
assert!(ylim.0 <= -5.0, "data below a sticky edge stays visible");
}
#[test]
fn no_data_falls_back_to_unit_limits() {
let axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let (xlim, ylim) = axes.effective_limits();
assert_eq!(xlim, (0.0, 1.0));
assert_eq!(ylim, (0.0, 1.0));
}
#[test]
fn oscilloscope_switches_to_the_phosphor_cycle() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let tab_c0 = axes.cycle_color(0);
axes.oscilloscope();
let scope_c0 = axes.cycle_color(0);
assert_ne!(tab_c0, scope_c0, "scope mode swaps the trace cycle");
assert!(scope_c0.g > 0.9 && scope_c0.g > scope_c0.r && scope_c0.g > scope_c0.b);
let colors: Vec<_> = (0..4).map(|i| axes.cycle_color(i)).collect();
for i in 0..4 {
for j in (i + 1)..4 {
assert_ne!(colors[i], colors[j]);
}
}
assert_eq!(axes.cycle_color(4), colors[0], "cycle wraps at four");
}
#[test]
fn scope_value_formatting_stays_compact() {
assert_eq!(format_scope_value(0.49), "0.49");
assert_eq!(format_scope_value(-0.51), "-0.51");
assert_eq!(format_scope_value(123.4), "123");
assert_eq!(format_scope_value(0.0), "0.00");
assert_eq!(format_scope_value(12345.0), "1.2e4");
assert_eq!(format_scope_value(0.0004), "4.0e-4");
}
#[test]
fn explicit_limits_override_autoscale() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(&[0.0, 1.0], &[0.0, 1.0]);
axes.set_xlim(-5.0, 5.0);
let (xlim, _) = axes.effective_limits();
assert_eq!(xlim, (-5.0, 5.0));
}
#[test]
fn zero_width_range_is_guarded() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.set_xlim(3.0, 3.0);
let (xlim, _) = axes.effective_limits();
assert!(xlim.1 > xlim.0, "range should be widened: {xlim:?}");
}
#[derive(Default)]
struct StrokeRecorder {
strokes: Vec<Option<Rgba>>,
}
impl Renderer for StrokeRecorder {
fn draw_path(
&mut self,
gc: &GraphicsContext,
_path: &Path,
_transform: &Affine2D,
_fill: Option<Rgba>,
) {
self.strokes.push(gc.stroke);
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
}
fn line_stroke(line: &Line2D) -> Rgba {
let mut r = StrokeRecorder::default();
line.draw(&mut r, &Affine2D::identity());
r.strokes[0].expect("line strokes with a color")
}
#[test]
fn successive_plots_advance_the_color_cycle() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(&[0.0, 1.0], &[0.0, 1.0]);
axes.plot(&[0.0, 1.0], &[1.0, 0.0]);
assert_eq!(
line_stroke(&axes.lines[0]),
Rgba::from_hex("#1f77b4").unwrap()
);
assert_eq!(
line_stroke(&axes.lines[1]),
Rgba::from_hex("#ff7f0e").unwrap()
);
}
#[test]
fn explicit_override_on_returned_line_wins() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
let x = vec![0.0, 1.0];
let y = vec![0.0, 1.0];
*axes.plot(&x, &y) = Line2D::new(x.clone(), y.clone()).with_color(Rgba::RED);
assert_eq!(line_stroke(&axes.lines[0]), Rgba::RED);
}
#[test]
fn plot_with_color_does_not_advance_the_cycle() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot_with_color(&[0.0, 1.0], &[0.0, 1.0], Rgba::RED);
axes.plot(&[0.0, 1.0], &[1.0, 0.0]);
assert_eq!(line_stroke(&axes.lines[0]), Rgba::RED);
assert_eq!(
line_stroke(&axes.lines[1]),
Rgba::from_hex("#1f77b4").unwrap()
);
}
#[test]
fn aspect_equal_yields_equal_data_units_per_pixel() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.set_xlim(-1.0, 1.0).set_ylim(-1.0, 1.0);
let (fig_w, fig_h) = (400.0, 200.0);
let (_, td_default) = axes.pixel_rect_and_trans_data_in(fig_w, fig_h, None, None);
let sx =
td_default.transform_point((1.0, 0.0)).0 - td_default.transform_point((0.0, 0.0)).0;
let sy =
td_default.transform_point((0.0, 1.0)).1 - td_default.transform_point((0.0, 0.0)).1;
assert!(
(sx - sy).abs() > 1.0,
"default (non-equal) scales should differ: sx={sx}, sy={sy}"
);
axes.set_aspect_equal();
let (_, td_equal) = axes.pixel_rect_and_trans_data_in(fig_w, fig_h, None, None);
let ex = td_equal.transform_point((1.0, 0.0)).0 - td_equal.transform_point((0.0, 0.0)).0;
let ey = td_equal.transform_point((0.0, 1.0)).1 - td_equal.transform_point((0.0, 0.0)).1;
approx(ex, ey);
}
#[test]
fn default_aspect_leaves_rect_unchanged() {
let axes = Axes::new(Bbox::from_extents(0.1, 0.2, 0.9, 0.8));
let (rect, _) = axes.pixel_rect_and_trans_data_in(400.0, 200.0, None, None);
approx(rect.xmin(), 40.0);
approx(rect.xmax(), 360.0);
approx(rect.ymin(), 40.0);
approx(rect.ymax(), 160.0);
}
#[test]
fn axis_off_then_on_toggles_visibility() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
assert!(axes.axis_visible);
axes.set_axis_off();
assert!(!axes.axis_visible);
axes.set_axis_on();
assert!(axes.axis_visible);
}
#[test]
fn reset_returns_cycle_to_start() {
let mut axes = Axes::new(Bbox::from_extents(0.0, 0.0, 1.0, 1.0));
axes.plot(&[0.0, 1.0], &[0.0, 1.0]);
axes.set_prop_cycle_reset();
axes.plot(&[0.0, 1.0], &[1.0, 0.0]);
assert_eq!(
line_stroke(&axes.lines[1]),
Rgba::from_hex("#1f77b4").unwrap()
);
}
}