use std::collections::BTreeMap;
use std::io::{IsTerminal, Result as IoResult, Write};
use crate::border::BorderType;
use crate::color::{AUTO_SERIES_COLORS, ColorMode, NamedColor, TermColor};
use crate::graphics::GraphicsArea;
use crate::render::{build_rendered_plot, write_ansi, write_plain};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Annotation {
pub(crate) text: String,
pub(crate) color: Option<TermColor>,
}
impl Annotation {
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub const fn color(&self) -> Option<TermColor> {
self.color
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct EdgeDecorations {
pub(crate) tl: Option<String>,
pub(crate) t: Option<String>,
pub(crate) tr: Option<String>,
pub(crate) bl: Option<String>,
pub(crate) b: Option<String>,
pub(crate) br: Option<String>,
}
impl EdgeDecorations {
#[must_use]
pub fn tl(&self) -> Option<&str> {
self.tl.as_deref()
}
#[must_use]
pub fn t(&self) -> Option<&str> {
self.t.as_deref()
}
#[must_use]
pub fn tr(&self) -> Option<&str> {
self.tr.as_deref()
}
#[must_use]
pub fn bl(&self) -> Option<&str> {
self.bl.as_deref()
}
#[must_use]
pub fn b(&self) -> Option<&str> {
self.b.as_deref()
}
#[must_use]
pub fn br(&self) -> Option<&str> {
self.br.as_deref()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PlotAnnotations {
pub(crate) left: BTreeMap<usize, Annotation>,
pub(crate) right: BTreeMap<usize, Annotation>,
pub(crate) deco: EdgeDecorations,
}
impl PlotAnnotations {
#[must_use]
pub const fn left(&self) -> &BTreeMap<usize, Annotation> {
&self.left
}
#[must_use]
pub const fn right(&self) -> &BTreeMap<usize, Annotation> {
&self.right
}
#[must_use]
pub const fn decorations(&self) -> &EdgeDecorations {
&self.deco
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DecorationPosition {
Tl,
T,
Tr,
Bl,
B,
Br,
}
#[non_exhaustive]
pub struct Plot<G: GraphicsArea> {
pub(crate) graphics: G,
pub(crate) title: Option<String>,
pub(crate) xlabel: Option<String>,
pub(crate) ylabel: Option<String>,
pub(crate) border: BorderType,
pub(crate) margin: u16,
pub(crate) padding: u16,
pub(crate) show_labels: bool,
pub(crate) annotations: PlotAnnotations,
pub(crate) auto_color_index: u8,
}
impl<G: GraphicsArea> Plot<G> {
pub const DEFAULT_MARGIN: u16 = 3;
pub const DEFAULT_PADDING: u16 = 1;
#[must_use]
pub fn new(graphics: G) -> Self {
Self {
graphics,
title: None,
xlabel: None,
ylabel: None,
border: BorderType::Solid,
margin: Self::DEFAULT_MARGIN,
padding: Self::DEFAULT_PADDING,
show_labels: true,
annotations: PlotAnnotations::default(),
auto_color_index: 0,
}
}
pub fn annotate_left(&mut self, row: usize, text: impl Into<String>, color: Option<TermColor>) {
self.annotations.left.insert(
row,
Annotation {
text: text.into(),
color,
},
);
}
pub fn annotate_right(
&mut self,
row: usize,
text: impl Into<String>,
color: Option<TermColor>,
) {
self.annotations.right.insert(
row,
Annotation {
text: text.into(),
color,
},
);
}
pub fn set_decoration(&mut self, position: DecorationPosition, text: impl Into<String>) {
let value = Some(text.into());
match position {
DecorationPosition::Tl => self.annotations.deco.tl = value,
DecorationPosition::T => self.annotations.deco.t = value,
DecorationPosition::Tr => self.annotations.deco.tr = value,
DecorationPosition::Bl => self.annotations.deco.bl = value,
DecorationPosition::B => self.annotations.deco.b = value,
DecorationPosition::Br => self.annotations.deco.br = value,
}
}
#[must_use]
pub fn next_color(&mut self) -> NamedColor {
let index = usize::from(self.auto_color_index) % AUTO_SERIES_COLORS.len();
self.auto_color_index = self.auto_color_index.wrapping_add(1);
AUTO_SERIES_COLORS[index]
}
#[must_use]
pub const fn annotations(&self) -> &PlotAnnotations {
&self.annotations
}
#[must_use]
pub fn graphics(&self) -> &G {
&self.graphics
}
pub fn graphics_mut(&mut self) -> &mut G {
&mut self.graphics
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn xlabel(&self) -> Option<&str> {
self.xlabel.as_deref()
}
#[must_use]
pub fn ylabel(&self) -> Option<&str> {
self.ylabel.as_deref()
}
#[must_use]
pub const fn border(&self) -> BorderType {
self.border
}
#[must_use]
pub const fn margin(&self) -> u16 {
self.margin
}
#[must_use]
pub const fn padding(&self) -> u16 {
self.padding
}
#[must_use]
pub const fn show_labels(&self) -> bool {
self.show_labels
}
#[must_use]
pub const fn auto_color_index(&self) -> u8 {
self.auto_color_index
}
pub fn render(&self, writer: &mut impl Write, color: bool) -> IoResult<()> {
let color_mode = if color {
ColorMode::Always
} else {
ColorMode::Never
};
let use_color = should_use_color(color_mode, false);
let rendered = build_rendered_plot(self);
if use_color {
write_ansi(&rendered, writer)
} else {
write_plain(&rendered, writer)
}
}
pub fn render_with_mode(
&self,
writer: &mut impl Write,
color_mode: ColorMode,
writer_is_terminal: bool,
) -> IoResult<()> {
let use_color = should_use_color(color_mode, writer_is_terminal);
let rendered = build_rendered_plot(self);
if use_color {
write_ansi(&rendered, writer)
} else {
write_plain(&rendered, writer)
}
}
pub fn render_with_mode_auto<W: Write + IsTerminal>(
&self,
writer: &mut W,
color_mode: ColorMode,
) -> IoResult<()> {
self.render_with_mode(writer, color_mode, writer.is_terminal())
}
}
const fn should_use_color(color_mode: ColorMode, writer_is_terminal: bool) -> bool {
match color_mode {
ColorMode::Auto => writer_is_terminal,
ColorMode::Always => true,
ColorMode::Never => false,
}
}
#[cfg(test)]
mod tests {
use super::{DecorationPosition, Plot, should_use_color};
use crate::color::{ColorMode, NamedColor, TermColor};
use crate::graphics::GraphicsArea;
#[derive(Debug, Default)]
struct DummyGraphics;
impl GraphicsArea for DummyGraphics {
fn nrows(&self) -> usize {
0
}
fn ncols(&self) -> usize {
0
}
fn render_row(&self, _row: usize, out: &mut crate::graphics::RowBuffer) {
out.clear();
}
}
#[test]
fn annotation_maps_are_deterministic_by_row() {
let mut plot = Plot::new(DummyGraphics);
plot.annotate_left(10, "left high", Some(TermColor::Named(NamedColor::Green)));
plot.annotate_left(2, "left low", Some(TermColor::Named(NamedColor::Blue)));
plot.annotate_right(7, "right mid", Some(TermColor::Named(NamedColor::Red)));
plot.annotate_right(1, "right low", Some(TermColor::Named(NamedColor::Cyan)));
let left_rows: Vec<_> = plot.annotations().left().keys().copied().collect();
let right_rows: Vec<_> = plot.annotations().right().keys().copied().collect();
assert_eq!(left_rows, vec![2, 10]);
assert_eq!(right_rows, vec![1, 7]);
}
#[test]
fn set_decoration_updates_expected_edge_slot() {
let mut plot = Plot::new(DummyGraphics);
plot.set_decoration(DecorationPosition::Tl, "max x");
plot.set_decoration(DecorationPosition::T, "top");
plot.set_decoration(DecorationPosition::Tr, "max y");
plot.set_decoration(DecorationPosition::Bl, "min x");
plot.set_decoration(DecorationPosition::B, "bottom");
plot.set_decoration(DecorationPosition::Br, "min y");
let deco = plot.annotations().decorations();
assert_eq!(deco.tl(), Some("max x"));
assert_eq!(deco.t(), Some("top"));
assert_eq!(deco.tr(), Some("max y"));
assert_eq!(deco.bl(), Some("min x"));
assert_eq!(deco.b(), Some("bottom"));
assert_eq!(deco.br(), Some("min y"));
}
#[test]
fn next_color_cycles_and_wraps_reference_sequence() {
let mut plot = Plot::new(DummyGraphics);
let sequence: Vec<_> = (0..8).map(|_| plot.next_color()).collect();
assert_eq!(
sequence,
vec![
NamedColor::Green,
NamedColor::Blue,
NamedColor::Red,
NamedColor::Magenta,
NamedColor::Yellow,
NamedColor::Cyan,
NamedColor::Green,
NamedColor::Blue,
]
);
}
#[test]
fn annotate_left_replaces_existing_row_annotation() {
let mut plot = Plot::new(DummyGraphics);
plot.annotate_left(3, "first", Some(TermColor::Named(NamedColor::Green)));
plot.annotate_left(3, "second", Some(TermColor::Named(NamedColor::Red)));
let annotation = plot
.annotations()
.left()
.get(&3)
.unwrap_or_else(|| panic!("expected row 3 annotation"));
assert_eq!(annotation.text(), "second");
assert_eq!(annotation.color(), Some(TermColor::Named(NamedColor::Red)));
}
#[test]
fn next_color_handles_u8_overflow_and_palette_wrap() {
let mut plot = Plot::new(DummyGraphics);
plot.auto_color_index = u8::MAX;
assert_eq!(plot.next_color(), NamedColor::Magenta);
assert_eq!(plot.auto_color_index(), 0);
assert_eq!(plot.next_color(), NamedColor::Green);
assert_eq!(plot.auto_color_index(), 1);
}
#[test]
fn render_with_mode_auto_uses_writer_terminal_capability() {
assert!(should_use_color(ColorMode::Auto, true));
assert!(!should_use_color(ColorMode::Auto, false));
assert!(should_use_color(ColorMode::Always, false));
assert!(!should_use_color(ColorMode::Never, true));
}
}