use crate::core::color::{ColorScale, Rgba};
use crate::core::{Affine2D, Bbox, Path};
use crate::render::{GraphicsContext, JoinStyle, 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,
join: JoinStyle,
visible: bool,
zorder: f64,
label: Option<String>,
color_scale: Option<ColorScale>,
}
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,
join: JoinStyle::Round,
visible: true,
zorder: 1.0,
label: None,
color_scale: None,
}
}
#[must_use]
pub fn color_scale(&self) -> Option<&ColorScale> {
self.color_scale.as_ref()
}
pub(crate) fn with_color_scale(mut self, scale: ColorScale) -> Self {
self.color_scale = Some(scale);
self
}
#[must_use]
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.set_label(label);
self
}
pub fn set_marker(&mut self, marker: Path) -> &mut Self {
self.marker = marker;
self
}
pub fn set_sizes(&mut self, sizes: Vec<f64>) -> &mut Self {
self.sizes = sizes;
self
}
pub fn set_facecolors(&mut self, facecolors: Vec<Rgba>) -> &mut Self {
self.facecolors = facecolors;
self
}
pub fn set_edgecolors(&mut self, edgecolors: Vec<Rgba>) -> &mut Self {
self.edgecolors = edgecolors;
self
}
pub fn set_linewidth(&mut self, linewidth: f64) -> &mut Self {
self.linewidth = linewidth;
self
}
pub fn set_joinstyle(&mut self, join: JoinStyle) -> &mut Self {
self.join = join;
self
}
pub fn set_zorder(&mut self, zorder: f64) -> &mut Self {
self.zorder = zorder;
self
}
pub fn set_visible(&mut self, visible: bool) -> &mut Self {
self.visible = visible;
self
}
#[must_use]
pub fn with_marker(mut self, marker: Path) -> Self {
self.set_marker(marker);
self
}
#[must_use]
pub fn with_sizes(mut self, sizes: Vec<f64>) -> Self {
self.set_sizes(sizes);
self
}
#[must_use]
pub fn with_facecolors(mut self, facecolors: Vec<Rgba>) -> Self {
self.set_facecolors(facecolors);
self
}
#[must_use]
pub fn with_edgecolors(mut self, edgecolors: Vec<Rgba>) -> Self {
self.set_edgecolors(edgecolors);
self
}
#[must_use]
pub fn linewidth(mut self, linewidth: f64) -> Self {
self.set_linewidth(linewidth);
self
}
#[must_use]
pub fn with_joinstyle(mut self, join: JoinStyle) -> Self {
self.set_joinstyle(join);
self
}
#[must_use]
pub fn with_zorder(mut self, zorder: f64) -> Self {
self.set_zorder(zorder);
self
}
#[must_use]
pub fn with_visible(mut self, visible: bool) -> Self {
self.set_visible(visible);
self
}
pub(crate) fn legend_sample(&self) -> Option<(Rgba, Collection)> {
let face = self.facecolors.first().copied();
let edge = self.edgecolors.first().copied();
let color = face.or(edge)?;
let (lo, hi) = self
.sizes
.iter()
.copied()
.filter(|s| s.is_finite())
.fold(None, |range: Option<(f64, f64)>, s| {
Some(range.map_or((s, s), |(lo, hi)| (lo.min(s), hi.max(s))))
})
.unwrap_or((DEFAULT_SIZE, DEFAULT_SIZE));
let sample = Collection {
offsets: Vec::new(),
marker: self.marker.clone(),
sizes: vec![0.5 * (lo + hi)],
facecolors: face.into_iter().collect(),
edgecolors: edge.into_iter().collect(),
linewidth: self.linewidth,
join: self.join,
visible: true,
zorder: self.zorder,
label: None,
color_scale: None,
};
Some((color, sample))
}
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;
}
if self.sizes.len() <= 1 && self.facecolors.len() <= 1 && self.edgecolors.len() <= 1 {
let size = renderer.points_to_pixels(self.size_at(0));
let gc = GraphicsContext {
line_width: self.linewidth,
stroke: self.edgecolor_at(0),
join: self.join,
..GraphicsContext::new()
};
let points: Vec<[f64; 2]> = offsets
.iter()
.copied()
.filter(|[x, y]| x.is_finite() && y.is_finite())
.collect();
if !points.is_empty() {
renderer.draw_markers(
&gc,
&self.marker,
&Affine2D::from_scale(size, size),
&Path::from_polyline(&points),
transform,
self.facecolor_at(0),
);
}
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,
join: self.join,
..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 {
let scale = crate::portable::spec::FlatScale::of(self.color_scale.as_ref());
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,
join: self.join,
visible: self.visible,
zorder: self.zorder,
label: self.label.clone(),
cmap: scale.cmap,
norm: scale.norm,
bad: scale.bad,
over: scale.over,
under: scale.under,
}
}
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,
join: spec.join,
visible: spec.visible,
zorder: spec.zorder,
label: spec.label.clone(),
color_scale: crate::portable::spec::FlatScale {
cmap: spec.cmap.clone(),
norm: spec.norm.clone(),
bad: spec.bad,
over: spec.over,
under: spec.under,
}
.into_scale()?,
})
}
}
#[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);
}
#[test]
fn setters_restyle_in_place_without_copying_offsets() {
let n = 1_000_000;
let offsets: Vec<[f64; 2]> = (0..n).map(|i| [i as f64, (i % 7) as f64]).collect();
let mut coll = Collection::scatter(offsets);
let before = coll.offsets().as_ptr();
let square = MarkerStyle::from_char('s').unwrap().path().clone();
coll.set_marker(square)
.set_sizes(vec![2.0])
.set_facecolors(vec![Rgba::RED])
.set_edgecolors(vec![Rgba::BLACK])
.set_linewidth(0.25)
.set_zorder(4.0)
.set_visible(false);
assert_eq!(coll.offsets().as_ptr(), before);
assert_eq!(coll.offsets().len(), n);
assert_eq!(coll.zorder(), 4.0);
assert!(!coll.visible());
}
}