use std::collections::HashMap;
use std::sync::Arc;
use crate::brush::Brush;
use crate::color::{Color, ColorSpace};
use crate::geometry::{Affine, Point, Vec2};
use crate::linetype::{draw_linetype_with_markers, emit_marker_shape};
use crate::path::Path;
use crate::pick::PickId;
use crate::plot::scale::Scale;
use crate::plot::value::{LinetypeStep, Value};
use crate::primitives::PolylineSampler;
use crate::scene::SceneBuilder;
use crate::shape::ShapeRegistry;
use crate::stroke::{Cap, Join, Stroke};
use super::{Channel, GeomContext};
pub(crate) const MAX_PICK_ID: u32 = 0xFF_FFFF;
#[derive(Clone, Copy, Default)]
pub(crate) struct ChannelBind<'a> {
pub ch: Option<&'a Channel>,
pub scale: Option<&'a Scale>,
}
impl<'a> ChannelBind<'a> {
pub(crate) fn from_ctx(
channels: &'a HashMap<String, Channel>,
ctx: &'a GeomContext<'_>,
name: &str,
) -> Self {
Self {
ch: channels.get(name),
scale: ctx.scale_for(name),
}
}
}
#[inline]
pub(crate) fn pt_to_px(pt: f64, dpi: f64) -> f64 {
pt * dpi / 72.0
}
pub(crate) fn offset_px(ch: Option<&Channel>, scale: Option<&Scale>, row: usize, dpi: f64) -> f64 {
resolve_number_channel(ch, scale, row)
.map(|pt| pt_to_px(pt, dpi))
.unwrap_or(0.0)
}
pub(crate) fn resolve_position(raw: Value, scale: Option<&Scale>, band_offset: f64) -> f64 {
let mapped = match scale {
Some(s) => s.map_with_offset(&raw, band_offset),
None => raw,
};
mapped.as_number().unwrap_or(f64::NAN)
}
fn resolve_value(channel: Option<&Channel>, scale: Option<&Scale>, i: usize) -> Option<Value> {
let (raw, bypass_scale) = match channel? {
Channel::Constant(v) => (v.clone(), false),
Channel::Data(col) => (col.get(i), false),
Channel::RawConstant(v) => (v.clone(), true),
Channel::RawData(col) => (col.get(i), true),
};
Some(match (bypass_scale, scale) {
(true, _) | (false, None) => raw,
(false, Some(s)) => s.map(&raw),
})
}
pub(crate) fn resolve_color_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
) -> Option<Color> {
resolve_value(channel, scale, i)?.as_color()
}
pub(crate) fn channel_color_space(scale: Option<&Scale>) -> ColorSpace {
scale.map(Scale::color_space).unwrap_or_default()
}
pub(crate) fn resolve_color_channel_or_theme(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
theme_default: Option<&crate::plot::theme::ThemeColor>,
palette: &crate::plot::theme::Palette,
) -> Option<Color> {
resolve_color_channel(channel, scale, i).or_else(|| theme_default.map(|tc| tc.resolve(palette)))
}
pub(crate) fn resolve_number_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
) -> Option<f64> {
resolve_value(channel, scale, i)?.as_number()
}
pub(crate) fn channel_varies_across(
channel: Option<&Channel>,
scale: Option<&Scale>,
rows: &[usize],
) -> bool {
let Some(channel) = channel else { return false };
if matches!(channel, Channel::Constant(_)) {
return false;
}
let mut first: Option<Value> = None;
for &i in rows {
let v = resolve_value(Some(channel), scale, i);
match (&first, &v) {
(None, Some(_)) => first = v,
(Some(a), Some(b)) if !a.key_eq(b) => return true,
_ => {}
}
}
false
}
pub(crate) fn resolve_number_channel_or(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
default: f64,
) -> f64 {
resolve_number_channel(channel, scale, i).unwrap_or(default)
}
pub(crate) fn resolve_bool_channel_or(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
default: bool,
) -> bool {
match resolve_value(channel, scale, i) {
Some(Value::Bool(b)) => b,
_ => default,
}
}
pub(crate) fn resolve_angle_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
) -> f64 {
resolve_number_channel(channel, scale, i).unwrap_or(0.0)
}
pub(crate) fn resolve_linetype_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
) -> Arc<[LinetypeStep]> {
match resolve_value(channel, scale, i) {
Some(Value::Linetype(p)) => p,
_ => Arc::from(Vec::<LinetypeStep>::new()),
}
}
pub(crate) fn resolve_str_channel_or(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
default: &str,
) -> String {
match resolve_value(channel, scale, i).and_then(|v| v.as_str().map(str::to_owned)) {
Some(s) => s,
None => default.to_string(),
}
}
pub(crate) fn cap_from_str(s: &str) -> Option<Cap> {
match s {
"butt" => Some(Cap::Butt),
"round" => Some(Cap::Round),
"square" => Some(Cap::Square),
_ => None,
}
}
pub(crate) fn join_from_str(s: &str) -> Option<Join> {
match s {
"miter" => Some(Join::Miter),
"round" => Some(Join::Round),
"bevel" => Some(Join::Bevel),
_ => None,
}
}
pub(crate) fn resolve_cap_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
default: Cap,
) -> Cap {
resolve_value(channel, scale, i)
.and_then(|v| v.as_str().and_then(cap_from_str))
.unwrap_or(default)
}
pub(crate) fn resolve_join_channel(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
default: Join,
) -> Join {
resolve_value(channel, scale, i)
.and_then(|v| v.as_str().and_then(join_from_str))
.unwrap_or(default)
}
pub(crate) fn build_stroke_for_pattern(
width_px: f64,
cap: Cap,
join: Join,
pattern: &[LinetypeStep],
offset_pt: f64,
linewidth_pt: f64,
dpi: f64,
) -> Stroke {
let mut s = Stroke::new(width_px).with_caps(cap).with_join(join);
if !pattern.is_empty() {
let pattern_px: Vec<f64> = pattern
.iter()
.map(|step| match step {
LinetypeStep::Dash(p) | LinetypeStep::Gap(p) => pt_to_px(*p, dpi),
LinetypeStep::Marker(_) => pt_to_px(linewidth_pt, dpi),
})
.collect();
let offset_px = pt_to_px(offset_pt, dpi);
s = s.with_dashes(offset_px, pattern_px);
}
s
}
pub(crate) fn override_alpha(color: Option<Color>, alpha: Option<f64>) -> Option<Color> {
let c = color?;
match alpha {
None => Some(c),
Some(a) => {
let [r, g, b, _] = c.components;
Some(Color::new([r, g, b, a as f32]))
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn apply_per_row_offsets(
samples: &mut [Point],
us: &[f64],
row_for_ctrl: &[usize],
x_offset_ch: Option<&Channel>,
x_offset_scale: Option<&Scale>,
y_offset_ch: Option<&Channel>,
y_offset_scale: Option<&Scale>,
dpi: f64,
) {
if x_offset_ch.is_none() && y_offset_ch.is_none() {
return;
}
let n_rows = row_for_ctrl.len();
if n_rows == 0 {
return;
}
let row_x = |row: usize| -> f64 {
resolve_number_channel(x_offset_ch, x_offset_scale, row).unwrap_or(0.0)
};
let row_y = |row: usize| -> f64 {
resolve_number_channel(y_offset_ch, y_offset_scale, row).unwrap_or(0.0)
};
let last = n_rows - 1;
for (idx, &u) in us.iter().enumerate().take(samples.len()) {
let u_clamped = u.clamp(0.0, last as f64);
let lo = u_clamped.floor() as usize;
let hi = (lo + 1).min(last);
let t = u_clamped - lo as f64;
let dx_pt = if lo == hi {
row_x(row_for_ctrl[lo])
} else {
let a = row_x(row_for_ctrl[lo]);
let b = row_x(row_for_ctrl[hi]);
a + t * (b - a)
};
let dy_pt = if lo == hi {
row_y(row_for_ctrl[lo])
} else {
let a = row_y(row_for_ctrl[lo]);
let b = row_y(row_for_ctrl[hi]);
a + t * (b - a)
};
samples[idx].x += pt_to_px(dx_pt, dpi);
samples[idx].y -= pt_to_px(dy_pt, dpi);
}
}
pub(crate) fn band_width_at(scale: Option<&Scale>, raw: &Value) -> f64 {
match scale {
Some(s) => s.band_width_at(raw),
None => 0.0,
}
}
pub(crate) fn resolve_pick_id(
channel: Option<&Channel>,
scale: Option<&Scale>,
i: usize,
) -> PickId {
let n = match resolve_number_channel(channel, scale, i) {
Some(n) => n,
None => return PickId::Skip,
};
if !n.is_finite() || n < 0.0 || n > MAX_PICK_ID as f64 || n.trunc() != n {
return PickId::Skip;
}
let id = n as u32;
if id == 0 {
PickId::Block
} else {
PickId::Id(id)
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_stroke_with_linetype(
scene: &mut dyn SceneBuilder,
path: &Path,
closed: bool,
stroke_color: Color,
marker_fill: Color,
linewidth_px: f64,
linewidth_pt: f64,
cap: Cap,
join: Join,
dash_pattern_pt: &[LinetypeStep],
dash_offset_pt: f64,
xform: Affine,
pick: PickId,
shapes: &ShapeRegistry,
marker_outline_pt: f64,
dpi: f64,
) {
if !linewidth_px.is_finite() || linewidth_px <= 0.0 {
return;
}
if super::linetype::is_marker_free(dash_pattern_pt) {
let stroke_spec = build_stroke_for_pattern(
linewidth_px,
cap,
join,
dash_pattern_pt,
dash_offset_pt,
linewidth_pt,
dpi,
);
scene.stroke(
&stroke_spec,
xform,
&Brush::Solid(stroke_color),
None,
path,
pick,
);
return;
}
let samplers = if closed {
PolylineSampler::from_closed_path(path, 0.5)
} else {
PolylineSampler::from_path(path, 0.5)
};
let solid_stroke_spec = Stroke::new(linewidth_px).with_caps(cap).with_join(join);
let dash_offset_px = pt_to_px(dash_offset_pt, dpi);
draw_linetype_with_markers(
scene,
&samplers,
dash_pattern_pt,
dash_offset_px,
linewidth_px,
marker_fill,
stroke_color,
marker_outline_pt,
&solid_stroke_spec,
xform,
shapes,
dpi,
pick,
closed,
);
}
pub(crate) const MIN_MARKER_OUTLINE_PT: f64 = 0.5;
pub(crate) fn endpoint_marker_outline_px(linewidth_px: f64, dpi: f64) -> f64 {
linewidth_px.max(pt_to_px(MIN_MARKER_OUTLINE_PT, dpi))
}
pub(crate) fn auto_endpoint_clip_pt(
marker_name: &str,
size_pt: f64,
invert: bool,
shapes: &ShapeRegistry,
) -> f64 {
if marker_name.is_empty() || !size_pt.is_finite() || size_pt <= 0.0 {
return 0.0;
}
let Some(shape) = shapes.get(marker_name) else {
return 0.0;
};
let bbox = shape.bounding_box();
let anchor = shape.anchor();
let extent_units = if invert {
anchor.x - bbox.x0
} else {
bbox.x1 - anchor.x
};
extent_units.max(0.0) * size_pt
}
pub(crate) fn endpoint_outward(
clipped: &[Point],
original: &[Point],
at_start: bool,
was_clipped: bool,
) -> Vec2 {
if clipped.len() < 2 {
return Vec2::ZERO;
}
let dir = if was_clipped && !original.is_empty() {
let (clip_pt, orig_pt) = if at_start {
(clipped[0], original[0])
} else {
(clipped[clipped.len() - 1], original[original.len() - 1])
};
orig_pt - clip_pt
} else if at_start {
clipped[0] - clipped[1]
} else {
let n = clipped.len();
clipped[n - 1] - clipped[n - 2]
};
let len_sq = dir.length_squared();
if len_sq < 1e-24 {
Vec2::ZERO
} else {
dir / len_sq.sqrt()
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn emit_endpoint_marker(
scene: &mut dyn SceneBuilder,
placement: Point,
outward: Vec2,
invert: bool,
marker_name: &str,
size_px: f64,
marker_fill: Color,
marker_stroke: Color,
stroke_width_px: f64,
xform: Affine,
shapes: &ShapeRegistry,
pick: PickId,
) {
if marker_name.is_empty() {
return;
}
let Some(shape) = shapes.get(marker_name) else {
return;
};
let dir = if invert { -outward } else { outward };
if dir.length_squared() < 1e-12 {
return;
}
let theta = dir.atan2();
let rot = Affine::rotate(theta);
let scaled_anchor = shape.anchor().to_vec2() * size_px;
let (sn, cs) = theta.sin_cos();
let anchor_world = Vec2::new(
cs * scaled_anchor.x - sn * scaled_anchor.y,
sn * scaled_anchor.x + cs * scaled_anchor.y,
);
let origin = placement.to_vec2() - anchor_world;
let local_unscaled = Affine::translate(origin) * rot;
emit_marker_shape(
scene,
shape,
xform * local_unscaled,
size_px,
marker_fill,
marker_stroke,
stroke_width_px,
pick,
);
}
#[inline]
pub(crate) fn smallest_nonzero(a: f64, b: f64) -> f64 {
match (a > 0.0, b > 0.0) {
(true, true) => a.min(b),
(true, false) => a,
(false, true) => b,
(false, false) => 0.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plot::scale::{self, Direction};
use crate::plot::value::DataColumn;
fn cat(s: &str) -> Value {
Value::String(Arc::from(s))
}
#[test]
fn resolve_position_without_a_scale_passes_the_raw_number_through() {
assert_eq!(resolve_position(Value::Number(0.25), None, 0.0), 0.25);
assert_eq!(resolve_position(Value::Date(7), None, 0.0), 7.0);
}
#[test]
fn resolve_position_without_a_scale_ignores_the_band_offset() {
assert_eq!(resolve_position(Value::Number(0.25), None, 0.5), 0.25);
}
#[test]
fn resolve_position_maps_through_a_bound_scale() {
let s = scale::continuous(0.0..=10.0);
assert_eq!(resolve_position(Value::Number(5.0), Some(&s), 0.0), 0.5);
assert_eq!(resolve_position(Value::Number(0.0), Some(&s), 0.0), 0.0);
}
#[test]
fn resolve_position_folds_a_band_offset_into_the_scaled_fraction() {
let s = scale::discrete([cat("a"), cat("b")]);
assert_eq!(resolve_position(cat("a"), Some(&s), 0.0), 0.25);
assert_eq!(resolve_position(cat("a"), Some(&s), 0.5), 0.5);
}
#[test]
fn resolve_position_is_nan_for_unmappable_input() {
assert!(resolve_position(cat("nope"), None, 0.0).is_nan());
let s = scale::discrete([cat("a")]);
assert!(resolve_position(cat("zzz"), Some(&s), 0.0).is_nan());
let c = scale::continuous(0.0..=1.0);
assert!(resolve_position(cat("a"), Some(&c), 0.0).is_nan());
}
#[test]
fn resolve_color_channel_reads_constants_and_columns() {
let red = crate::color::rgb(1.0, 0.0, 0.0);
let blue = crate::color::rgb(0.0, 0.0, 1.0);
let konst = Channel::Constant(Value::Color(red));
assert_eq!(resolve_color_channel(Some(&konst), None, 3), Some(red));
let col = Channel::Data(DataColumn::Color(vec![red, blue]));
assert_eq!(resolve_color_channel(Some(&col), None, 1), Some(blue));
}
#[test]
fn resolve_color_channel_maps_categories_through_a_palette_scale() {
let red = crate::color::rgb(1.0, 0.0, 0.0);
let blue = crate::color::rgb(0.0, 0.0, 1.0);
let s = scale::discrete([cat("a"), cat("b")]).range_colors([red, blue]);
let col = Channel::Data(DataColumn::String(vec![Arc::from("b"), Arc::from("a")]));
assert_eq!(resolve_color_channel(Some(&col), Some(&s), 0), Some(blue));
assert_eq!(resolve_color_channel(Some(&col), Some(&s), 1), Some(red));
}
#[test]
fn resolve_color_channel_is_none_when_unset_or_not_a_color() {
assert_eq!(resolve_color_channel(None, None, 0), None);
let numeric = Channel::Constant(Value::Number(1.0));
assert_eq!(resolve_color_channel(Some(&numeric), None, 0), None);
}
#[test]
fn resolve_number_channel_or_falls_back_when_unset_or_non_numeric() {
assert_eq!(resolve_number_channel_or(None, None, 0, 4.5), 4.5);
let text = Channel::Constant(cat("big"));
assert_eq!(resolve_number_channel_or(Some(&text), None, 0, 4.5), 4.5);
let ok = Channel::Data(DataColumn::F64(vec![1.0, 2.0]));
assert_eq!(resolve_number_channel_or(Some(&ok), None, 1, 4.5), 2.0);
}
#[test]
fn resolve_number_channel_or_takes_the_scaled_output_not_the_raw_value() {
let s = scale::ordinal([cat("small"), cat("large")]).range_numbers([2.0, 20.0]);
let col = Channel::Data(DataColumn::String(vec![Arc::from("large")]));
assert_eq!(
resolve_number_channel_or(Some(&col), Some(&s), 0, 4.5),
20.0
);
}
#[test]
fn channel_varies_across_is_false_for_unset_and_constant_channels() {
assert!(!channel_varies_across(None, None, &[0, 1, 2]));
let konst = Channel::Constant(Value::Number(1.0));
assert!(!channel_varies_across(Some(&konst), None, &[0, 1, 2]));
}
#[test]
fn channel_varies_across_detects_differing_rows() {
let col = Channel::Data(DataColumn::F64(vec![1.0, 1.0, 2.0]));
assert!(channel_varies_across(Some(&col), None, &[0, 1, 2]));
assert!(!channel_varies_across(Some(&col), None, &[0, 1]));
assert!(!channel_varies_across(Some(&col), None, &[2]));
assert!(!channel_varies_across(Some(&col), None, &[]));
}
#[test]
fn channel_varies_across_is_false_when_the_scale_flattens_the_rows() {
let flat = scale::continuous(0.0..=10.0).range_numbers([7.0]);
let col = Channel::Data(DataColumn::F64(vec![1.0, 5.0, 9.0]));
assert!(channel_varies_across(Some(&col), None, &[0, 1, 2]));
assert!(!channel_varies_across(Some(&col), Some(&flat), &[0, 1, 2]));
}
#[test]
fn channel_varies_across_compares_variants_not_numeric_projections() {
let dates = Channel::Data(DataColumn::Date(vec![1, 1]));
assert!(!channel_varies_across(Some(&dates), None, &[0, 1]));
let mixed = Channel::Data(DataColumn::F64(vec![1.0, 1.0]));
assert!(!channel_varies_across(Some(&mixed), None, &[0, 1]));
}
#[test]
fn pt_to_px_scales_by_dpi_over_seventy_two() {
assert_eq!(pt_to_px(72.0, 96.0), 96.0);
assert_eq!(pt_to_px(12.0, 72.0), 12.0);
assert_eq!(pt_to_px(10.0, 144.0), 20.0);
assert_eq!(pt_to_px(0.0, 96.0), 0.0);
}
#[test]
fn raw_constant_bypasses_a_bound_scale() {
let s = scale::continuous(0.0..=10.0);
let scaled = Channel::Constant(Value::Number(5.0));
let raw = Channel::RawConstant(Value::Number(5.0));
assert_eq!(
resolve_number_channel(Some(&scaled), Some(&s), 0),
Some(0.5)
);
assert_eq!(resolve_number_channel(Some(&raw), Some(&s), 0), Some(5.0));
}
#[test]
fn raw_data_bypasses_a_bound_scale() {
let red = crate::color::rgb(1.0, 0.0, 0.0);
let blue = crate::color::rgb(0.0, 0.0, 1.0);
let palette = scale::discrete([cat("a"), cat("b")]).range_colors([red, blue]);
let raw = Channel::RawData(DataColumn::Color(vec![blue, red]));
assert_eq!(
resolve_color_channel(Some(&raw), Some(&palette), 0),
Some(blue)
);
let raw_positions = Channel::RawData(DataColumn::F64(vec![0.9]));
let s = scale::continuous(0.0..=10.0);
assert_eq!(
resolve_number_channel(Some(&raw_positions), Some(&s), 0),
Some(0.9)
);
}
#[test]
fn raw_channels_do_not_vary_when_their_rows_agree() {
let s = scale::continuous(0.0..=10.0).with_direction(Direction::Reversed);
let raw = Channel::RawData(DataColumn::F64(vec![0.3, 0.3]));
assert!(!channel_varies_across(Some(&raw), Some(&s), &[0, 1]));
let raw_varying = Channel::RawData(DataColumn::F64(vec![0.3, 0.7]));
assert!(channel_varies_across(Some(&raw_varying), Some(&s), &[0, 1]));
}
}