#![allow(clippy::cast_precision_loss)]
use std::f64::consts::TAU;
use starsight_layer_1::backends::DrawBackend;
use starsight_layer_1::errors::Result;
use starsight_layer_1::paths::PathStyle;
use starsight_layer_1::primitives::Color;
use starsight_layer_2::coords::Coord;
use crate::marks::arc::build_arc_wedge;
use crate::marks::{DataExtent, LegendGlyph, Mark};
#[derive(Clone, Debug)]
pub struct PolarRectMark {
pub theta_min: Vec<f64>,
pub theta_max: Vec<f64>,
pub r_min: Vec<f64>,
pub r_max: Vec<f64>,
pub colors: Vec<Color>,
pub stroke: Option<(Color, f32)>,
pub label: Option<String>,
}
impl PolarRectMark {
#[must_use]
pub fn new(theta_min: Vec<f64>, theta_max: Vec<f64>, r_min: Vec<f64>, r_max: Vec<f64>) -> Self {
let n = theta_min
.len()
.min(theta_max.len())
.min(r_min.len())
.min(r_max.len());
let mut tmin = theta_min;
let mut tmax = theta_max;
let mut rmin = r_min;
let mut rmax = r_max;
tmin.truncate(n);
tmax.truncate(n);
rmin.truncate(n);
rmax.truncate(n);
Self {
theta_min: tmin,
theta_max: tmax,
r_min: rmin,
r_max: rmax,
colors: Vec::new(),
stroke: None,
label: None,
}
}
#[must_use]
pub fn color(mut self, c: Color) -> Self {
self.colors = vec![c];
self
}
#[must_use]
pub fn colors(mut self, colors: Vec<Color>) -> Self {
self.colors = colors;
self
}
#[must_use]
pub fn stroke(mut self, color: Color, width: f32) -> Self {
self.stroke = Some((color, width));
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
fn color_at(&self, i: usize) -> Color {
if self.colors.is_empty() {
Color::from_hex(0x004C_72B0)
} else {
self.colors[i % self.colors.len()]
}
}
}
impl Mark for PolarRectMark {
fn render(&self, coord: &dyn Coord, backend: &mut dyn DrawBackend) -> Result<()> {
let coord = crate::marks::require_polar(coord)?;
if self.theta_min.is_empty() {
return Ok(());
}
let center = coord.center;
let radius = coord.radius;
for i in 0..self.theta_min.len() {
let theta_a = coord.theta_axis.scale.map(self.theta_min[i]);
let theta_b = coord.theta_axis.scale.map(self.theta_max[i]);
let r_in_norm = coord.r_axis.scale.map(self.r_min[i]);
let r_out_norm = coord.r_axis.scale.map(self.r_max[i]);
let a_rad = theta_a * TAU;
let b_rad = theta_b * TAU;
let r_in_px = (r_in_norm * f64::from(radius)) as f32;
let r_out_px = (r_out_norm * f64::from(radius)) as f32;
let path = build_arc_wedge(center, r_in_px, r_out_px, a_rad, b_rad);
backend.draw_path(&path, &PathStyle::fill(self.color_at(i)))?;
if let Some((stroke_color, stroke_width)) = self.stroke {
backend.draw_path(&path, &PathStyle::stroke(stroke_color, stroke_width))?;
}
}
Ok(())
}
fn data_extent(&self) -> Option<DataExtent> {
None
}
fn legend_color(&self) -> Option<Color> {
self.label.as_ref()?;
Some(self.color_at(0))
}
fn legend_label(&self) -> Option<&str> {
self.label.as_deref()
}
fn legend_glyph(&self) -> LegendGlyph {
LegendGlyph::Bar
}
fn wants_axes(&self) -> bool {
false
}
fn prefers_outside_legend(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::PolarRectMark;
use crate::marks::{LegendGlyph, Mark};
use starsight_layer_1::primitives::Color;
#[test]
fn new_truncates_to_shortest_input() {
let mark = PolarRectMark::new(
vec![0.0, 0.5, 1.0],
vec![0.5, 1.0],
vec![0.0, 0.0, 0.0, 0.0],
vec![1.0, 1.0, 1.0],
);
assert_eq!(mark.theta_min.len(), 2);
assert_eq!(mark.theta_max.len(), 2);
assert_eq!(mark.r_min.len(), 2);
assert_eq!(mark.r_max.len(), 2);
}
#[test]
fn default_color_when_unset() {
let mark = PolarRectMark::new(vec![0.0], vec![1.0], vec![0.0], vec![1.0]);
assert_eq!(mark.color_at(0), Color::from_hex(0x004C_72B0));
}
#[test]
fn user_colors_cycle() {
let mark = PolarRectMark::new(
vec![0.0, 0.5, 1.0],
vec![0.5, 1.0, 1.5],
vec![0.0; 3],
vec![1.0; 3],
)
.colors(vec![Color::RED, Color::GREEN]);
assert_eq!(mark.color_at(0), Color::RED);
assert_eq!(mark.color_at(1), Color::GREEN);
assert_eq!(mark.color_at(2), Color::RED);
}
#[test]
fn legend_glyph_is_bar() {
let mark = PolarRectMark::new(vec![0.0], vec![1.0], vec![0.0], vec![1.0])
.color(Color::BLUE)
.label("series");
assert_eq!(mark.legend_glyph(), LegendGlyph::Bar);
assert_eq!(mark.legend_color(), Some(Color::BLUE));
}
#[test]
fn no_legend_when_unlabeled() {
let mark = PolarRectMark::new(vec![0.0], vec![1.0], vec![0.0], vec![1.0]);
assert!(mark.legend_color().is_none());
}
#[test]
fn does_not_want_axes() {
let mark = PolarRectMark::new(vec![0.0], vec![1.0], vec![0.0], vec![1.0]);
assert!(!mark.wants_axes());
}
#[test]
fn data_extent_is_none() {
let mark = PolarRectMark::new(vec![0.0], vec![1.0], vec![0.0], vec![1.0]);
assert!(mark.data_extent().is_none());
}
}