use crate::core::color::{
Colormap, DEFAULT_COLORMAP, LinearNorm, Normalize, colormap, default_colormap,
};
use crate::core::{Affine2D, Bbox};
use crate::render::{GraphicsContext, Renderer};
use crate::artist::Artist;
#[derive(Debug, Clone, PartialEq)]
pub struct AxesImage {
data: Vec<f64>,
nrows: usize,
ncols: usize,
extent: [f64; 4],
vmin: f64,
vmax: f64,
cmap_name: String,
origin_upper: bool,
zorder: f64,
visible: bool,
}
impl AxesImage {
#[must_use]
pub fn new(data: Vec<f64>, nrows: usize, ncols: usize) -> Self {
assert_eq!(
data.len(),
nrows * ncols,
"AxesImage: data length {} must equal nrows * ncols = {}",
data.len(),
nrows * ncols
);
let (vmin, vmax) = data_min_max(&data);
Self {
data,
nrows,
ncols,
extent: [0.0, ncols as f64, 0.0, nrows as f64],
vmin,
vmax,
cmap_name: DEFAULT_COLORMAP.to_string(),
origin_upper: true,
zorder: 0.0,
visible: true,
}
}
#[must_use]
pub fn with_extent(mut self, extent: [f64; 4]) -> Self {
self.extent = extent;
self
}
pub fn set_data(&mut self, data: Vec<f64>, nrows: usize, ncols: usize) -> Result<(), String> {
if data.len() != nrows * ncols {
return Err(format!(
"AxesImage::set_data: data length {} must equal nrows * ncols = {}",
data.len(),
nrows * ncols
));
}
self.data = data;
self.nrows = nrows;
self.ncols = ncols;
Ok(())
}
#[must_use]
pub fn with_cmap(mut self, cmap_name: impl Into<String>) -> Self {
self.cmap_name = cmap_name.into();
self
}
#[must_use]
pub fn with_vmin(mut self, vmin: f64) -> Self {
self.vmin = vmin;
self
}
#[must_use]
pub fn with_vmax(mut self, vmax: f64) -> Self {
self.vmax = vmax;
self
}
#[must_use]
pub fn with_origin_upper(mut self, origin_upper: bool) -> Self {
self.origin_upper = origin_upper;
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.zorder = zorder;
self
}
#[must_use]
pub fn set_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn cmap(&mut self, cmap_name: impl Into<String>) -> &mut Self {
self.cmap_name = cmap_name.into();
self
}
pub fn vmin(&mut self, vmin: f64) -> &mut Self {
self.vmin = vmin;
self
}
pub fn vmax(&mut self, vmax: f64) -> &mut Self {
self.vmax = vmax;
self
}
pub fn set_extent(&mut self, extent: [f64; 4]) -> &mut Self {
self.extent = extent;
self
}
#[must_use]
pub fn extent(&self) -> [f64; 4] {
self.extent
}
#[must_use]
pub fn clim(&self) -> (f64, f64) {
(self.vmin, self.vmax)
}
#[must_use]
pub fn colorize_cell(&self, row: usize, col: usize) -> [u8; 4] {
if row >= self.nrows || col >= self.ncols {
return [0, 0, 0, 0];
}
let norm = LinearNorm::new(self.vmin, self.vmax);
let cmap = resolve_cmap(&self.cmap_name);
let value = self.data[row * self.ncols + col];
colorize(value, &norm, cmap.as_ref())
}
}
fn resolve_cmap(name: &str) -> Box<dyn Colormap> {
colormap(name).unwrap_or_else(|| Box::new(default_colormap()))
}
fn colorize(value: f64, norm: &dyn Normalize, cmap: &dyn Colormap) -> [u8; 4] {
if !value.is_finite() {
return [0, 0, 0, 0];
}
cmap.sample(norm.normalize(value)).to_u8_array()
}
fn data_min_max(data: &[f64]) -> (f64, f64) {
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in data {
if v.is_finite() {
min = min.min(v);
max = max.max(v);
}
}
if min <= max { (min, max) } else { (0.0, 1.0) }
}
impl AxesImage {
#[must_use]
pub fn shape(&self) -> (usize, usize) {
(self.nrows, self.ncols)
}
}
impl Artist for AxesImage {
fn draw(&self, renderer: &mut dyn Renderer, transform: &Affine2D) {
if !self.visible || self.nrows == 0 || self.ncols == 0 {
return;
}
let [x0, x1, y0, y1] = self.extent;
let (dx0, dy0) = transform.transform_point((x0, y0));
let (dx1, dy1) = transform.transform_point((x1, y1));
let dev_xmin = dx0.min(dx1);
let dev_xmax = dx0.max(dx1);
let dev_ymin = dy0.min(dy1);
let dev_ymax = dy0.max(dy1);
let w_px = (dev_xmax - dev_xmin).round() as i64;
let h_px = (dev_ymax - dev_ymin).round() as i64;
if w_px <= 0 || h_px <= 0 {
return;
}
let w_px = w_px as usize;
let h_px = h_px as usize;
let norm = LinearNorm::new(self.vmin, self.vmax);
let cmap = resolve_cmap(&self.cmap_name);
let mut rgba = vec![0u8; w_px * h_px * 4];
for ty in 0..h_px {
let fy = (ty as f64 + 0.5) / h_px as f64;
let src_row_frac = if self.origin_upper { fy } else { 1.0 - fy };
let row = ((src_row_frac * self.nrows as f64) as usize).min(self.nrows - 1);
for tx in 0..w_px {
let fx = (tx as f64 + 0.5) / w_px as f64;
let col = ((fx * self.ncols as f64) as usize).min(self.ncols - 1);
let value = self.data[row * self.ncols + col];
let [r, g, b, a] = colorize(value, &norm, cmap.as_ref());
let i = (ty * w_px + tx) * 4;
rgba[i] = r;
rgba[i + 1] = g;
rgba[i + 2] = b;
rgba[i + 3] = a;
}
}
renderer.draw_image(
&GraphicsContext::new(),
dev_xmin,
dev_ymin,
&rgba,
w_px,
h_px,
);
}
fn zorder(&self) -> f64 {
self.zorder
}
fn visible(&self) -> bool {
self.visible
}
fn data_extents(&self) -> Option<Bbox> {
let [x0, x1, y0, y1] = self.extent;
Some(Bbox::from_extents(
x0.min(x1),
y0.min(y1),
x0.max(x1),
y0.max(y1),
))
}
}
#[cfg(feature = "portable")]
impl AxesImage {
pub(crate) fn to_portable(
&self,
bank: &mut crate::portable::data::BankWriter,
) -> crate::portable::spec::ImageSpec {
crate::portable::spec::ImageSpec {
data: bank.push_f64(&self.data),
nrows: self.nrows,
ncols: self.ncols,
extent: self.extent,
vmin: self.vmin,
vmax: self.vmax,
cmap: self.cmap_name.clone(),
origin_upper: self.origin_upper,
zorder: self.zorder,
visible: self.visible,
label: None,
}
}
pub(crate) fn from_portable(
spec: &crate::portable::spec::ImageSpec,
reader: &crate::portable::data::BankReader<'_>,
) -> Result<AxesImage, crate::portable::PortableError> {
let data = reader.f64s(&spec.data)?;
let cells = spec.nrows.checked_mul(spec.ncols);
if cells != Some(data.len()) {
return Err(crate::portable::PortableError::Malformed(format!(
"image declares {}x{} cells but carries {} samples",
spec.nrows,
spec.ncols,
data.len()
)));
}
Ok(AxesImage {
data,
nrows: spec.nrows,
ncols: spec.ncols,
extent: spec.extent,
vmin: spec.vmin,
vmax: spec.vmax,
cmap_name: spec.cmap.clone(),
origin_upper: spec.origin_upper,
zorder: spec.zorder,
visible: spec.visible,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::color::default_colormap;
#[test]
fn defaults_match_matplotlib() {
let img = AxesImage::new(vec![1.0, 2.0, 3.0, 4.0], 2, 2);
assert_eq!(img.extent(), [0.0, 2.0, 0.0, 2.0]);
assert_eq!(img.clim(), (1.0, 4.0));
assert!(img.origin_upper);
assert_eq!(Artist::zorder(&img), 0.0);
}
#[test]
fn min_cell_is_cmap_zero_and_max_cell_is_cmap_one() {
let img = AxesImage::new(vec![0.0, 1.0, 2.0, 3.0], 2, 2);
let cm = default_colormap();
let lo = cm.sample(0.0).to_u8_array();
let hi = cm.sample(1.0).to_u8_array();
assert_eq!(img.colorize_cell(0, 0), lo);
assert_eq!(img.colorize_cell(1, 1), hi);
assert!(lo[0] < 80 && lo[2] > 100 && lo[1] < 30);
assert!(hi[0] > 200 && hi[1] > 200 && hi[2] > 200);
}
#[test]
fn out_of_range_cell_is_transparent() {
let img = AxesImage::new(vec![0.0, 1.0, 2.0, 3.0], 2, 2);
assert_eq!(img.colorize_cell(5, 5), [0, 0, 0, 0]);
}
#[test]
fn data_extents_returns_extent_bbox() {
let img = AxesImage::new(vec![0.0; 6], 2, 3).with_extent([-1.0, 5.0, 2.0, 8.0]);
let e = img.data_extents().expect("image has extents");
assert_eq!((e.xmin(), e.xmax()), (-1.0, 5.0));
assert_eq!((e.ymin(), e.ymax()), (2.0, 8.0));
}
#[test]
fn nonfinite_value_colorizes_transparent() {
let norm = LinearNorm::new(0.0, 1.0);
let cm = default_colormap();
assert_eq!(colorize(f64::NAN, &norm, &cm), [0, 0, 0, 0]);
}
#[test]
fn all_nan_data_falls_back_to_unit_clim() {
let img = AxesImage::new(vec![f64::NAN; 4], 2, 2);
assert_eq!(img.clim(), (0.0, 1.0));
}
}