use std::collections::HashMap;
use std::sync::Arc;
use crate::color::Color;
use crate::plot::geom::resolve::{cap_from_str, join_from_str};
use crate::plot::scale::ScaleRegistry;
use crate::scales::chrome::LegendSide;
use crate::scales::value::{LinetypeStep, Value};
use crate::stroke::{Cap, Join};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LegendId(u32);
impl LegendId {
pub(crate) fn new(raw: u32) -> Self {
Self(raw)
}
pub fn raw(self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LegendKey {
Point,
Line,
Rect,
Text,
}
#[derive(Clone, Debug)]
pub enum AestheticSource {
Scaled(String),
Fixed(Value),
}
#[derive(Clone, Debug)]
pub struct LegendKeySpec {
pub kind: LegendKey,
pub bindings: HashMap<String, AestheticSource>,
}
impl LegendKeySpec {
pub fn point() -> Self {
Self {
kind: LegendKey::Point,
bindings: HashMap::new(),
}
}
pub fn line() -> Self {
Self {
kind: LegendKey::Line,
bindings: HashMap::new(),
}
}
pub fn rect() -> Self {
Self {
kind: LegendKey::Rect,
bindings: HashMap::new(),
}
}
pub fn text() -> Self {
Self {
kind: LegendKey::Text,
bindings: HashMap::new(),
}
}
pub fn scaled(mut self, aesthetic: impl Into<String>, scale_name: impl Into<String>) -> Self {
self.bindings
.insert(aesthetic.into(), AestheticSource::Scaled(scale_name.into()));
self
}
pub fn fixed(mut self, aesthetic: impl Into<String>, value: impl Into<Value>) -> Self {
self.bindings
.insert(aesthetic.into(), AestheticSource::Fixed(value.into()));
self
}
}
#[derive(Clone, Debug)]
pub struct Legend {
pub(crate) side: LegendSide,
pub(crate) title: Option<String>,
pub(crate) domain_scale: String,
pub(crate) body: LegendBody,
pub(crate) open_lower: bool,
pub(crate) open_upper: bool,
pub(crate) bin_spacing: BinSpacing,
pub(crate) theme_variant: Option<String>,
pub(crate) merge: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BinSpacing {
#[default]
Proportional,
Equal,
}
#[derive(Clone, Debug)]
pub enum LegendBody {
Stack(StackBody),
Colorbar(ColorbarSpec),
}
#[derive(Clone, Debug)]
pub struct StackBody {
pub keys: Vec<LegendKeySpec>,
pub binned: bool,
}
#[derive(Clone, Debug)]
pub struct ColorbarSpec {
pub samples: usize,
pub stepped: bool,
pub bindings: HashMap<String, AestheticSource>,
}
impl Default for ColorbarSpec {
fn default() -> Self {
Self {
samples: 64,
stepped: false,
bindings: HashMap::new(),
}
}
}
impl Legend {
pub fn new(domain_scale: impl Into<String>) -> Self {
Self {
side: LegendSide::Right,
title: None,
domain_scale: domain_scale.into(),
body: LegendBody::Stack(StackBody {
keys: Vec::new(),
binned: false,
}),
open_lower: false,
open_upper: false,
bin_spacing: BinSpacing::Proportional,
theme_variant: None,
merge: true,
}
}
pub fn colorbar(domain_scale: impl Into<String>) -> Self {
Self {
side: LegendSide::Right,
title: None,
domain_scale: domain_scale.into(),
body: LegendBody::Colorbar(ColorbarSpec::default()),
open_lower: false,
open_upper: false,
bin_spacing: BinSpacing::Proportional,
theme_variant: None,
merge: true,
}
}
pub fn theme_variant(mut self, name: impl Into<String>) -> Self {
self.theme_variant = Some(name.into());
self
}
pub fn side(mut self, s: LegendSide) -> Self {
self.side = s;
self
}
pub fn title(mut self, t: impl Into<String>) -> Self {
self.title = Some(t.into());
self
}
pub fn key(mut self, k: LegendKeySpec) -> Self {
if let LegendBody::Stack(stack) = &mut self.body {
stack.keys.push(k);
}
self
}
pub fn binned(mut self) -> Self {
match &mut self.body {
LegendBody::Stack(stack) => stack.binned = true,
LegendBody::Colorbar(spec) => spec.stepped = true,
}
self
}
pub fn open_lower(mut self) -> Self {
self.open_lower = true;
self
}
pub fn open_upper(mut self) -> Self {
self.open_upper = true;
self
}
pub fn equal_bins(mut self) -> Self {
self.bin_spacing = BinSpacing::Equal;
self
}
pub fn bin_spacing(mut self, spacing: BinSpacing) -> Self {
self.bin_spacing = spacing;
self
}
pub fn samples(mut self, n: usize) -> Self {
if let LegendBody::Colorbar(spec) = &mut self.body {
spec.samples = n.max(2);
}
self
}
pub fn scaled(mut self, aesthetic: impl Into<String>, scale_name: impl Into<String>) -> Self {
if let LegendBody::Colorbar(spec) = &mut self.body {
spec.bindings
.insert(aesthetic.into(), AestheticSource::Scaled(scale_name.into()));
}
self
}
pub fn fixed(mut self, aesthetic: impl Into<String>, value: impl Into<Value>) -> Self {
if let LegendBody::Colorbar(spec) = &mut self.body {
spec.bindings
.insert(aesthetic.into(), AestheticSource::Fixed(value.into()));
}
self
}
pub fn is_compatible_with(
&self,
other: &Legend,
registry: &ScaleRegistry,
locale: &crate::scales::Locale,
) -> bool {
if self.side != other.side
|| self.title != other.title
|| self.theme_variant != other.theme_variant
|| self.open_lower != other.open_lower
|| self.open_upper != other.open_upper
|| self.bin_spacing != other.bin_spacing
{
return false;
}
let bodies_agree = match (&self.body, &other.body) {
(LegendBody::Stack(a), LegendBody::Stack(b)) => a.binned == b.binned,
(LegendBody::Colorbar(a), LegendBody::Colorbar(b)) => {
a.stepped == b.stepped
&& (a.stepped || a.samples == b.samples)
&& colorbar_gradients_agree(
(a, &self.domain_scale),
(b, &other.domain_scale),
registry,
locale,
)
}
_ => false,
};
if !bodies_agree {
return false;
}
self.domain_scale == other.domain_scale
|| match (
registry.get(&self.domain_scale),
registry.get(&other.domain_scale),
) {
(Some(a), Some(b)) => a.legend_equivalent_to(b, locale),
_ => false,
}
}
}
enum StopSource<'a> {
Scale(&'a str),
Fixed(&'a Value),
}
fn stop_sources<'a>(
spec: &'a ColorbarSpec,
domain_scale: &'a str,
) -> Vec<(&'a str, StopSource<'a>)> {
let mut out: Vec<(&str, StopSource)> = spec
.bindings
.iter()
.map(|(aesthetic, source)| {
let src = match source {
AestheticSource::Scaled(name) => StopSource::Scale(name.as_str()),
AestheticSource::Fixed(v) => StopSource::Fixed(v),
};
(aesthetic.as_str(), src)
})
.collect();
if !spec.bindings.contains_key("fill") && !spec.bindings.contains_key("color") {
out.push(("fill", StopSource::Scale(domain_scale)));
}
out.sort_by_key(|(aesthetic, _)| *aesthetic);
out
}
fn colorbar_gradients_agree(
a: (&ColorbarSpec, &str),
b: (&ColorbarSpec, &str),
registry: &ScaleRegistry,
locale: &crate::scales::Locale,
) -> bool {
let (mine, theirs) = (stop_sources(a.0, a.1), stop_sources(b.0, b.1));
mine.len() == theirs.len()
&& mine
.iter()
.zip(theirs.iter())
.all(|((a_aes, a_src), (b_aes, b_src))| {
a_aes == b_aes && stop_sources_agree(a_src, b_src, registry, locale)
})
}
fn stop_sources_agree(
a: &StopSource,
b: &StopSource,
registry: &ScaleRegistry,
locale: &crate::scales::Locale,
) -> bool {
match (a, b) {
(StopSource::Scale(x), StopSource::Scale(y)) => {
x == y
|| match (registry.get(x), registry.get(y)) {
(Some(sx), Some(sy)) => sx.visual_equivalent_to(sy, locale),
_ => false,
}
}
(StopSource::Fixed(x), StopSource::Fixed(y)) => x.key_eq(y),
_ => false,
}
}
pub fn collapse_legends(
legends: &[Legend],
registry: &ScaleRegistry,
locale: &crate::scales::Locale,
) -> Vec<Legend> {
let mut out: Vec<Legend> = Vec::with_capacity(legends.len());
for legend in legends {
let target = legend.merge.then(|| {
out.iter()
.position(|kept| kept.is_compatible_with(legend, registry, locale))
});
match target.flatten() {
Some(idx) => {
if let (LegendBody::Stack(kept), LegendBody::Stack(incoming)) =
(&mut out[idx].body, &legend.body)
{
kept.keys.extend(incoming.keys.iter().cloned());
}
}
None => out.push(legend.clone()),
}
}
out
}
#[derive(Clone, Debug, Default)]
pub struct ResolvedKey {
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub size_pt: Option<f64>,
pub shape: Option<Arc<str>>,
pub fill_opacity: Option<f64>,
pub stroke_opacity: Option<f64>,
pub linewidth_pt: Option<f64>,
pub linetype: Option<Arc<[LinetypeStep]>>,
pub dash_offset_pt: Option<f64>,
pub cap: Option<Cap>,
pub join: Option<Join>,
pub corner_radius_pt: Option<f64>,
pub angle: Option<f64>,
pub start_marker: EndpointMarkerKey,
pub end_marker: EndpointMarkerKey,
pub text: Option<Arc<str>>,
pub weight: Option<u16>,
pub italic: Option<bool>,
pub family: Option<Arc<str>>,
pub letter_spacing_pt: Option<f64>,
pub underline: Option<bool>,
pub strikethrough: Option<bool>,
pub text_stroke: Option<Color>,
pub text_linewidth_pt: Option<f64>,
}
#[derive(Clone, Debug, Default)]
pub struct EndpointMarkerKey {
pub shape: Option<Arc<str>>,
pub size_pt: Option<f64>,
pub fill: Option<Color>,
pub invert: Option<bool>,
}
impl ResolvedKey {
pub(super) fn apply(&mut self, aesthetic: &str, value: Value) {
match aesthetic {
"fill" | "color" => {
if let Some(c) = value.as_color() {
self.fill = Some(c);
}
}
"stroke" => {
if let Some(c) = value.as_color() {
self.stroke = Some(c);
}
}
"size" => {
if let Some(n) = value.as_number() {
self.size_pt = Some(n);
}
}
"shape" => {
if let Some(s) = value.as_str() {
self.shape = Some(Arc::from(s));
}
}
"fill_opacity" => {
if let Some(n) = value.as_number() {
self.fill_opacity = Some(n);
}
}
"stroke_opacity" => {
if let Some(n) = value.as_number() {
self.stroke_opacity = Some(n);
}
}
"linewidth" => {
if let Some(n) = value.as_number() {
self.linewidth_pt = Some(n);
}
}
"linetype" => {
if let Some(p) = value.as_linetype() {
self.linetype = Some(Arc::from(p.to_vec()));
}
}
"dash_offset" => {
if let Some(n) = value.as_number() {
self.dash_offset_pt = Some(n);
}
}
"corner_radius" => {
if let Some(n) = value.as_number() {
self.corner_radius_pt = Some(n);
}
}
"angle" => {
if let Some(n) = value.as_number() {
self.angle = Some(n);
}
}
"cap" => {
if let Some(c) = value.as_str().and_then(cap_from_str) {
self.cap = Some(c);
}
}
"join" => {
if let Some(j) = value.as_str().and_then(join_from_str) {
self.join = Some(j);
}
}
"text" => {
if let Some(s) = value.as_str() {
self.text = Some(Arc::from(s));
}
}
"weight" => {
if let Some(n) = value.as_number() {
self.weight = Some((n.round() as i64).clamp(1, 1000) as u16);
}
}
"italic" => match value {
Value::Bool(b) => self.italic = Some(b),
Value::String(ref s) => self.italic = Some(matches!(&**s, "italic" | "oblique")),
_ => {}
},
"family" => {
if let Some(s) = value.as_str() {
self.family = Some(Arc::from(s));
}
}
"letter_spacing" => {
if let Some(n) = value.as_number() {
self.letter_spacing_pt = Some(n);
}
}
"underline" => {
if let Some(b) = value.as_bool() {
self.underline = Some(b);
}
}
"strikethrough" => {
if let Some(b) = value.as_bool() {
self.strikethrough = Some(b);
}
}
"text_stroke" => {
if let Some(c) = value.as_color() {
self.text_stroke = Some(c);
}
}
"text_linewidth" => {
if let Some(n) = value.as_number() {
self.text_linewidth_pt = Some(n);
}
}
"start_marker" | "start_marker_size" | "start_marker_fill" | "start_marker_invert" => {
self.start_marker.apply(&aesthetic["start_".len()..], value)
}
"end_marker" | "end_marker_size" | "end_marker_fill" | "end_marker_invert" => {
self.end_marker.apply(&aesthetic["end_".len()..], value)
}
_ => {}
}
}
}
impl EndpointMarkerKey {
fn apply(&mut self, suffix: &str, value: Value) {
match suffix {
"marker" => {
if let Some(s) = value.as_str() {
self.shape = Some(Arc::from(s));
}
}
"marker_size" => {
if let Some(n) = value.as_number() {
self.size_pt = Some(n);
}
}
"marker_fill" => {
if let Some(c) = value.as_color() {
self.fill = Some(c);
}
}
"marker_invert" => {
if let Value::Bool(b) = value {
self.invert = Some(b);
}
}
_ => {}
}
}
}
pub(super) fn resolve_key(
spec: &LegendKeySpec,
registry: &ScaleRegistry,
row: &Value,
) -> ResolvedKey {
let mut resolved = ResolvedKey::default();
for (aesthetic, source) in &spec.bindings {
let value = match source {
AestheticSource::Scaled(name) => match registry.get(name) {
Some(scale) => scale.map(row),
None => continue,
},
AestheticSource::Fixed(v) => v.clone(),
};
resolved.apply(aesthetic, value);
}
resolved
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::rgb;
use crate::plot::scale;
use crate::scales::Locale;
fn build_registry() -> ScaleRegistry {
let mut reg = ScaleRegistry::new();
reg.insert(
"category_color",
scale::discrete([
Value::String(Arc::from("A")),
Value::String(Arc::from("B")),
Value::String(Arc::from("C")),
])
.range_colors([
rgb(1.0, 0.0, 0.0),
rgb(0.0, 1.0, 0.0),
rgb(0.0, 0.0, 1.0),
]),
);
reg.insert(
"category_size",
scale::discrete([
Value::String(Arc::from("A")),
Value::String(Arc::from("B")),
Value::String(Arc::from("C")),
])
.range_numbers([4.0, 8.0, 12.0]),
);
reg
}
#[test]
fn geom_style_aesthetics_reach_the_resolved_key() {
let spec = LegendKeySpec::point()
.fixed("fill_opacity", 0.25_f64)
.fixed("stroke_opacity", 0.75_f64)
.fixed("dash_offset", 3.0_f64)
.fixed("corner_radius", 4.0_f64)
.fixed("angle", 1.5_f64);
let resolved = resolve_key(&spec, &build_registry(), &Value::String(Arc::from("A")));
assert_eq!(resolved.fill_opacity, Some(0.25));
assert_eq!(resolved.stroke_opacity, Some(0.75));
assert_eq!(resolved.dash_offset_pt, Some(3.0));
assert_eq!(resolved.corner_radius_pt, Some(4.0));
assert_eq!(resolved.angle, Some(1.5));
assert_eq!(resolved.fill_opacity, Some(0.25));
assert_eq!(resolved.stroke_opacity, Some(0.75));
}
#[test]
fn text_aesthetics_reach_the_resolved_key() {
let spec = LegendKeySpec::text()
.fixed("text", Value::String(Arc::from("Aa")))
.fixed("weight", 700.0_f64)
.fixed("family", Value::String(Arc::from("Helvetica")))
.fixed("letter_spacing", 1.5_f64)
.fixed("underline", Value::Bool(true))
.fixed("strikethrough", Value::Bool(true))
.fixed("text_stroke", Value::Color(rgb(1.0, 0.0, 0.0)))
.fixed("text_linewidth", 2.0_f64);
let resolved = resolve_key(&spec, &build_registry(), &Value::String(Arc::from("A")));
assert_eq!(resolved.text.as_deref(), Some("Aa"));
assert_eq!(resolved.weight, Some(700));
assert_eq!(resolved.family.as_deref(), Some("Helvetica"));
assert_eq!(resolved.letter_spacing_pt, Some(1.5));
assert_eq!(resolved.underline, Some(true));
assert_eq!(resolved.strikethrough, Some(true));
assert_eq!(resolved.text_stroke, Some(rgb(1.0, 0.0, 0.0)));
assert_eq!(resolved.text_linewidth_pt, Some(2.0));
}
#[test]
fn the_italic_aesthetic_reads_both_vocabularies() {
let reg = build_registry();
let row = Value::String(Arc::from("A"));
for (bound, expected) in [
(Value::Bool(true), Some(true)),
(Value::String(Arc::from("italic")), Some(true)),
(Value::String(Arc::from("oblique")), Some(true)),
(Value::String(Arc::from("normal")), Some(false)),
] {
let spec = LegendKeySpec::text().fixed("italic", bound.clone());
assert_eq!(
resolve_key(&spec, ®, &row).italic,
expected,
"italic bound to {bound:?}"
);
}
}
#[test]
fn endpoint_marker_aesthetics_reach_the_resolved_key() {
let spec = LegendKeySpec::line()
.fixed("start_marker", Value::String(Arc::from("arrow-open")))
.fixed("start_marker_size", 6.0_f64)
.fixed("start_marker_fill", Value::Color(rgb(1.0, 0.0, 0.0)))
.fixed("start_marker_invert", Value::Bool(true))
.fixed("end_marker", Value::String(Arc::from("arrow-closed")))
.fixed("end_marker_size", 9.0_f64);
let resolved = resolve_key(&spec, &build_registry(), &Value::String(Arc::from("A")));
assert_eq!(resolved.start_marker.shape.as_deref(), Some("arrow-open"));
assert_eq!(resolved.start_marker.size_pt, Some(6.0));
assert_eq!(resolved.start_marker.fill, Some(rgb(1.0, 0.0, 0.0)));
assert_eq!(resolved.start_marker.invert, Some(true));
assert_eq!(resolved.end_marker.shape.as_deref(), Some("arrow-closed"));
assert_eq!(resolved.end_marker.size_pt, Some(9.0));
assert_eq!(resolved.end_marker.fill, None);
assert_eq!(resolved.end_marker.invert, None);
}
#[test]
fn cap_and_join_aesthetics_reach_the_resolved_key() {
let spec = LegendKeySpec::line()
.fixed("cap", Value::String(Arc::from("square")))
.fixed("join", Value::String(Arc::from("bevel")));
let resolved = resolve_key(&spec, &build_registry(), &Value::String(Arc::from("A")));
assert_eq!(resolved.cap, Some(Cap::Square));
assert_eq!(resolved.join, Some(Join::Bevel));
}
#[test]
fn legend_is_compatible_with_matching_triple() {
let reg = build_registry();
let loc = Locale::default();
let a = Legend::new("x").side(LegendSide::Right).title("T");
let b = Legend::new("x").side(LegendSide::Right).title("T");
let c = Legend::new("x").side(LegendSide::Right).title("U");
assert!(a.is_compatible_with(&b, ®, &loc));
assert!(!a.is_compatible_with(&c, ®, &loc));
}
#[test]
fn legends_over_equivalent_scales_are_compatible() {
let reg = build_registry();
let loc = Locale::default();
let color = Legend::new("category_color").title("Category");
let size = Legend::new("category_size").title("Category");
assert!(color.is_compatible_with(&size, ®, &loc));
}
#[test]
fn legends_over_differently_trained_scales_are_not_compatible() {
let mut reg = build_registry();
reg.insert(
"other_cats",
scale::discrete([Value::String(Arc::from("A")), Value::String(Arc::from("B"))])
.range_numbers([4.0, 8.0]),
);
let loc = Locale::default();
let three = Legend::new("category_color").title("Category");
let two = Legend::new("other_cats").title("Category");
assert!(!three.is_compatible_with(&two, ®, &loc));
}
#[test]
fn unresolvable_domain_scales_only_match_by_name() {
let reg = build_registry();
let loc = Locale::default();
let a = Legend::new("missing").title("T");
let same = Legend::new("missing").title("T");
let other = Legend::new("also_missing").title("T");
assert!(a.is_compatible_with(&same, ®, &loc));
assert!(!a.is_compatible_with(&other, ®, &loc));
}
#[test]
fn collapse_folds_keys_of_equivalent_scales() {
let reg = build_registry();
let loc = Locale::default();
let legends = vec![
Legend::new("category_color")
.title("Category")
.key(LegendKeySpec::rect().scaled("fill", "category_color")),
Legend::new("category_size")
.title("Category")
.key(LegendKeySpec::point().scaled("size", "category_size")),
];
let collapsed = collapse_legends(&legends, ®, &loc);
assert_eq!(collapsed.len(), 1);
let LegendBody::Stack(stack) = &collapsed[0].body else {
panic!("stack body");
};
assert_eq!(stack.keys.len(), 2);
assert_eq!(collapsed[0].domain_scale, "category_color");
}
#[test]
fn collapse_keeps_non_merging_legends_apart() {
let reg = build_registry();
let loc = Locale::default();
let mut second = Legend::new("category_size")
.title("Category")
.key(LegendKeySpec::point().scaled("size", "category_size"));
second.merge = false;
let legends = vec![
Legend::new("category_color")
.title("Category")
.key(LegendKeySpec::rect().scaled("fill", "category_color")),
second,
];
assert_eq!(collapse_legends(&legends, ®, &loc).len(), 2);
}
fn colorbar_registry() -> ScaleRegistry {
let mut reg = ScaleRegistry::new();
let palette = [rgb(1.0, 1.0, 0.0), rgb(0.0, 0.0, 1.0)];
reg.insert("ramp", scale::continuous(0.0..=100.0).range_colors(palette));
reg.insert(
"same_ramp",
scale::continuous(0.0..=100.0).range_colors(palette),
);
reg.insert(
"other_ramp",
scale::continuous(0.0..=100.0).range_colors([rgb(0.0, 0.0, 0.0), rgb(1.0, 1.0, 1.0)]),
);
reg.insert(
"ramp_opacity",
scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
);
reg
}
#[test]
fn collapse_folds_identical_colorbars() {
let reg = colorbar_registry();
let loc = Locale::default();
let legends = vec![
Legend::colorbar("ramp").title("Value"),
Legend::colorbar("ramp").title("Value"),
];
let collapsed = collapse_legends(&legends, ®, &loc);
assert_eq!(collapsed.len(), 1);
assert!(matches!(collapsed[0].body, LegendBody::Colorbar(_)));
}
#[test]
fn collapse_folds_colorbars_over_equivalent_scales() {
let reg = colorbar_registry();
let loc = Locale::default();
let legends = vec![
Legend::colorbar("ramp").title("Value"),
Legend::colorbar("same_ramp").title("Value"),
];
assert_eq!(collapse_legends(&legends, ®, &loc).len(), 1);
}
#[test]
fn collapse_keeps_colorbars_with_different_palettes_apart() {
let reg = colorbar_registry();
let loc = Locale::default();
let legends = vec![
Legend::colorbar("ramp").title("Value"),
Legend::colorbar("other_ramp").title("Value"),
];
assert_eq!(collapse_legends(&legends, ®, &loc).len(), 2);
}
#[test]
fn collapse_keeps_colorbars_with_different_bindings_apart() {
let reg = colorbar_registry();
let loc = Locale::default();
let plain = Legend::colorbar("ramp").title("Value");
let faded = Legend::colorbar("ramp")
.title("Value")
.scaled("fill_opacity", "ramp_opacity");
assert!(!plain.is_compatible_with(&faded, ®, &loc));
let also_faded = Legend::colorbar("ramp")
.title("Value")
.scaled("fill_opacity", "ramp_opacity");
assert!(faded.is_compatible_with(&also_faded, ®, &loc));
}
#[test]
fn collapse_keeps_stepped_and_smooth_colorbars_apart() {
let reg = colorbar_registry();
let loc = Locale::default();
let smooth = Legend::colorbar("ramp").title("Value");
let stepped = Legend::colorbar("ramp").title("Value").binned();
assert!(!smooth.is_compatible_with(&stepped, ®, &loc));
assert!(stepped.is_compatible_with(
&Legend::colorbar("ramp").title("Value").binned().samples(8),
®,
&loc
));
assert!(!smooth.is_compatible_with(
&Legend::colorbar("ramp").title("Value").samples(8),
®,
&loc
));
}
#[test]
fn collapse_keeps_a_colorbar_and_a_stack_apart() {
let reg = build_registry();
let loc = Locale::default();
let bar = Legend::colorbar("category_color").title("Category");
let stack = Legend::new("category_color")
.title("Category")
.key(LegendKeySpec::rect().scaled("fill", "category_color"));
assert!(!bar.is_compatible_with(&stack, ®, &loc));
}
#[test]
fn collapse_separates_legends_with_different_theme_variants() {
let reg = build_registry();
let loc = Locale::default();
let legends = vec![
Legend::new("category_color")
.title("Category")
.theme_variant("hero")
.key(LegendKeySpec::rect().scaled("fill", "category_color")),
Legend::new("category_color")
.title("Category")
.key(LegendKeySpec::point().scaled("size", "category_size")),
];
assert_eq!(collapse_legends(&legends, ®, &loc).len(), 2);
}
#[test]
fn legends_with_different_open_flags_are_not_compatible() {
let reg = build_registry();
let loc = Locale::default();
let a = Legend::new("x").open_lower();
let b = Legend::new("x");
assert!(!a.is_compatible_with(&b, ®, &loc));
let c = Legend::new("x").open_upper();
assert!(!a.is_compatible_with(&c, ®, &loc));
}
#[test]
fn legends_with_different_bin_spacing_are_not_compatible() {
let reg = build_registry();
let a = Legend::new("x").equal_bins();
let b = Legend::new("x");
assert!(!a.is_compatible_with(&b, ®, &Locale::default()));
}
#[test]
fn bin_spacing_default_is_proportional() {
let legend = Legend::new("x");
assert_eq!(legend.bin_spacing, BinSpacing::Proportional);
}
}