use crate::core::{Affine2D, Bbox, Path, color::Rgba};
use crate::render::{GraphicsContext, Renderer};
use crate::artist::{Artist, MarkerStyle};
const DEFAULT_FACECOLOR: Rgba = Rgba::new(0.121_568_63, 0.466_666_67, 0.705_882_35, 1.0);
const DEFAULT_SIZE: f64 = 6.0;
#[derive(Debug, Clone, PartialEq)]
pub struct Collection {
offsets: Vec<[f64; 2]>,
marker: Path,
sizes: Vec<f64>,
facecolors: Vec<Rgba>,
edgecolors: Vec<Rgba>,
linewidth: f64,
visible: bool,
zorder: f64,
}
impl Collection {
#[must_use]
pub fn scatter(offsets: Vec<[f64; 2]>) -> Self {
let marker = MarkerStyle::from_char('o')
.expect("'o' is a known marker")
.path()
.clone();
Self {
offsets,
marker,
sizes: vec![DEFAULT_SIZE],
facecolors: vec![DEFAULT_FACECOLOR],
edgecolors: Vec::new(),
linewidth: 1.0,
visible: true,
zorder: 1.0,
}
}
#[must_use]
pub fn with_marker(mut self, marker: Path) -> Self {
self.marker = marker;
self
}
#[must_use]
pub fn with_sizes(mut self, sizes: Vec<f64>) -> Self {
self.sizes = sizes;
self
}
#[must_use]
pub fn with_facecolors(mut self, facecolors: Vec<Rgba>) -> Self {
self.facecolors = facecolors;
self
}
#[must_use]
pub fn with_edgecolors(mut self, edgecolors: Vec<Rgba>) -> Self {
self.edgecolors = edgecolors;
self
}
#[must_use]
pub fn linewidth(mut self, linewidth: f64) -> Self {
self.linewidth = linewidth;
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.zorder = zorder;
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
fn facecolor_at(&self, i: usize) -> Option<Rgba> {
broadcast(&self.facecolors, i)
}
fn edgecolor_at(&self, i: usize) -> Option<Rgba> {
broadcast(&self.edgecolors, i)
}
fn size_at(&self, i: usize) -> f64 {
broadcast(&self.sizes, i).unwrap_or(DEFAULT_SIZE)
}
#[must_use]
pub fn offsets(&self) -> &[[f64; 2]] {
&self.offsets
}
pub fn set_offsets(&mut self, offsets: Vec<[f64; 2]>) -> &mut Self {
self.offsets = offsets;
self
}
pub fn draw_with_offsets(
&self,
renderer: &mut dyn Renderer,
offsets: &[[f64; 2]],
transform: &Affine2D,
) {
if !self.visible {
return;
}
for (i, &[x, y]) in offsets.iter().enumerate() {
if !x.is_finite() || !y.is_finite() {
continue;
}
let size = renderer.points_to_pixels(self.size_at(i));
let (dx, dy) = transform.transform_point((x, y));
let point_transform = Affine2D::from_scale(size, size).translate(dx, dy);
let edge = self.edgecolor_at(i);
let gc = GraphicsContext {
line_width: self.linewidth,
stroke: edge,
..GraphicsContext::new()
};
renderer.draw_path(&gc, &self.marker, &point_transform, self.facecolor_at(i));
}
}
}
fn broadcast<T: Copy>(values: &[T], i: usize) -> Option<T> {
if values.is_empty() {
None
} else {
Some(values[i % values.len()])
}
}
impl Artist for Collection {
fn draw(&self, renderer: &mut dyn Renderer, transform: &Affine2D) {
self.draw_with_offsets(renderer, &self.offsets, transform);
}
fn zorder(&self) -> f64 {
self.zorder
}
fn visible(&self) -> bool {
self.visible
}
fn data_extents(&self) -> Option<Bbox> {
let mut xmin = f64::INFINITY;
let mut ymin = f64::INFINITY;
let mut xmax = f64::NEG_INFINITY;
let mut ymax = f64::NEG_INFINITY;
let mut any = false;
for &[x, y] in &self.offsets {
if !x.is_finite() || !y.is_finite() {
continue;
}
xmin = xmin.min(x);
ymin = ymin.min(y);
xmax = xmax.max(x);
ymax = ymax.max(y);
any = true;
}
if any {
Some(Bbox::from_extents(xmin, ymin, xmax, ymax))
} else {
None
}
}
}
#[cfg(feature = "portable")]
impl Collection {
pub(crate) fn to_portable(
&self,
bank: &mut crate::portable::data::BankWriter,
) -> crate::portable::spec::CollectionSpec {
crate::portable::spec::CollectionSpec {
offsets: bank.push_pairs(&self.offsets),
marker: self.marker.to_portable(bank),
sizes: bank.push_f64(&self.sizes),
facecolors: self.facecolors.clone(),
edgecolors: self.edgecolors.clone(),
linewidth: self.linewidth,
visible: self.visible,
zorder: self.zorder,
label: None,
}
}
pub(crate) fn from_portable(
spec: &crate::portable::spec::CollectionSpec,
reader: &crate::portable::data::BankReader<'_>,
) -> Result<Collection, crate::portable::PortableError> {
Ok(Collection {
offsets: reader.pairs(&spec.offsets)?,
marker: Path::from_portable(&spec.marker, reader)?,
sizes: reader.f64s(&spec.sizes)?,
facecolors: spec.facecolors.clone(),
edgecolors: spec.edgecolors.clone(),
linewidth: spec.linewidth,
visible: spec.visible,
zorder: spec.zorder,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct MockRenderer {
calls: Vec<Call>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Call {
translation: (f64, f64),
scale: (f64, f64),
fill: Option<Rgba>,
stroke: Option<Rgba>,
}
impl Renderer for MockRenderer {
fn draw_path(
&mut self,
gc: &GraphicsContext,
_path: &Path,
transform: &Affine2D,
fill: Option<Rgba>,
) {
let [a, _, _, d, e, f] = transform.matrix();
self.calls.push(Call {
translation: (e, f),
scale: (a, d),
fill,
stroke: gc.stroke,
});
}
fn canvas_size(&self) -> (f64, f64) {
(100.0, 100.0)
}
fn points_to_pixels(&self, points: f64) -> f64 {
points * 2.0
}
}
#[test]
fn scatter_draws_once_per_offset() {
let offsets = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0]];
let coll = Collection::scatter(offsets);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls.len(), 5);
}
#[test]
fn single_facecolor_broadcasts_to_all() {
let offsets = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
let coll = Collection::scatter(offsets).with_facecolors(vec![Rgba::RED]);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
let fills: Vec<_> = r.calls.iter().map(|c| c.fill).collect();
assert_eq!(fills, vec![Some(Rgba::RED); 3]);
}
#[test]
fn per_point_facecolors_apply_elementwise() {
let offsets = vec![[0.0, 0.0], [1.0, 0.0], [2.0, 0.0]];
let coll =
Collection::scatter(offsets).with_facecolors(vec![Rgba::RED, Rgba::GREEN, Rgba::BLUE]);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
let fills: Vec<_> = r.calls.iter().map(|c| c.fill).collect();
assert_eq!(
fills,
vec![Some(Rgba::RED), Some(Rgba::GREEN), Some(Rgba::BLUE)]
);
}
#[test]
fn edgecolors_drive_the_stroke() {
let offsets = vec![[0.0, 0.0], [1.0, 0.0]];
let coll = Collection::scatter(offsets).with_edgecolors(vec![Rgba::BLACK]);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
let strokes: Vec<_> = r.calls.iter().map(|c| c.stroke).collect();
assert_eq!(strokes, vec![Some(Rgba::BLACK), Some(Rgba::BLACK)]);
}
#[test]
fn no_edgecolors_means_no_stroke() {
let offsets = vec![[0.0, 0.0]];
let coll = Collection::scatter(offsets);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls[0].stroke, None);
}
#[test]
fn draw_places_markers_at_offsets() {
let offsets = vec![[10.0, 20.0], [30.0, 40.0]];
let coll = Collection::scatter(offsets);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
let translations: Vec<_> = r.calls.iter().map(|c| c.translation).collect();
assert_eq!(translations, vec![(10.0, 20.0), (30.0, 40.0)]);
}
#[test]
fn marker_size_is_in_device_points_not_data_units() {
let coll = Collection::scatter(vec![[2.0, 3.0]]).with_sizes(vec![5.0]);
let data_to_device = Affine2D::from_scale(10.0, 20.0).translate(1.0, 2.0);
let mut r = MockRenderer::default();
coll.draw(&mut r, &data_to_device);
assert_eq!(r.calls.len(), 1);
assert_eq!(r.calls[0].translation, (21.0, 62.0));
assert_eq!(r.calls[0].scale, (10.0, 10.0));
}
#[test]
fn invisible_collection_draws_nothing() {
let coll = Collection::scatter(vec![[0.0, 0.0]]).with_visible(false);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
assert!(r.calls.is_empty());
}
#[test]
fn with_zorder_sets_trait_zorder() {
let coll = Collection::scatter(vec![[0.0, 0.0]]).with_zorder(9.0);
assert_eq!(coll.zorder(), 9.0);
}
#[test]
fn nan_offset_is_skipped_when_drawing() {
let offsets = vec![[0.0, 0.0], [f64::NAN, 1.0], [2.0, 2.0]];
let coll = Collection::scatter(offsets);
let mut r = MockRenderer::default();
coll.draw(&mut r, &Affine2D::identity());
assert_eq!(r.calls.len(), 2);
}
#[test]
fn data_extents_covers_offsets_ignoring_nan() {
let offsets = vec![[1.0, -1.0], [f64::NAN, 5.0], [3.0, 4.0]];
let coll = Collection::scatter(offsets);
let e = coll.data_extents().expect("non-empty");
assert_eq!(e.xmin(), 1.0);
assert_eq!(e.xmax(), 3.0);
assert_eq!(e.ymin(), -1.0);
assert_eq!(e.ymax(), 4.0);
}
#[test]
fn data_extents_empty_is_none() {
let coll = Collection::scatter(vec![]);
assert!(coll.data_extents().is_none());
}
#[test]
fn default_zorder_is_one() {
let coll = Collection::scatter(vec![[0.0, 0.0]]);
assert_eq!(coll.zorder(), 1.0);
}
}