1use std::sync::{Arc, Mutex, OnceLock};
4
5use crate::style::{Length, Style};
6use crate::utils::gradient::{ColorGradient, GradientRange};
7use crate::widgets::Overflow;
8
9mod layout;
10mod node;
11mod reconcile;
12
13pub use layout::measure_sparkline;
14pub use node::SparklineNode;
15pub use reconcile::reconcile_sparkline;
16
17pub(crate) use node::SparklineCacheKey;
18
19static SPARKLINE_CACHE: OnceLock<Mutex<SparklineVisualCache>> = OnceLock::new();
20
21#[derive(Clone, Debug)]
22pub(crate) struct SparklineVisualCache {
23 entries: Vec<(SparklineCacheKey, Arc<node::SparklineRenderOutput>)>,
24}
25
26impl SparklineVisualCache {
27 fn new() -> Self {
28 Self {
29 entries: Vec::new(),
30 }
31 }
32
33 fn get(&self, key: &SparklineCacheKey) -> Option<Arc<node::SparklineRenderOutput>> {
34 self.entries
35 .iter()
36 .find(|(k, _)| k == key)
37 .map(|(_, v)| Arc::clone(v))
38 }
39
40 fn insert(&mut self, key: SparklineCacheKey, value: Arc<node::SparklineRenderOutput>) {
41 if let Some(idx) = self.entries.iter().position(|(k, _)| k == &key) {
42 self.entries.remove(idx);
43 }
44 self.entries.push((key, value));
45 if self.entries.len() > 100 {
46 self.entries.remove(0);
47 }
48 }
49}
50
51pub(crate) fn get_cached_output(
52 key: &SparklineCacheKey,
53) -> Option<Arc<node::SparklineRenderOutput>> {
54 let cache_mutex = SPARKLINE_CACHE.get_or_init(|| Mutex::new(SparklineVisualCache::new()));
55 if let Ok(cache) = cache_mutex.lock() {
56 return cache.get(key);
57 }
58 None
59}
60
61pub(crate) fn insert_cached_output(
62 key: SparklineCacheKey,
63 output: Arc<node::SparklineRenderOutput>,
64) {
65 let cache_mutex = SPARKLINE_CACHE.get_or_init(|| Mutex::new(SparklineVisualCache::new()));
66 if let Ok(mut cache) = cache_mutex.lock() {
67 cache.insert(key, output);
68 }
69}
70
71pub(crate) const DEFAULT_BARS: [char; 8] = [' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
72pub(crate) const SHADE_BARS: [char; 5] = [' ', '░', '▒', '▓', '█'];
73pub(crate) const LINE_NORTH: u8 = 0b0001;
74pub(crate) const LINE_EAST: u8 = 0b0010;
75pub(crate) const LINE_SOUTH: u8 = 0b0100;
76pub(crate) const LINE_WEST: u8 = 0b1000;
77pub(crate) const LINE_POINT: u8 = 0b1_0000;
78
79use crate::core::element::Element;
80
81impl From<Sparkline> for Element {
82 fn from(val: Sparkline) -> Self {
83 Element::new(crate::core::element::ElementKind::Sparkline(val))
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
89pub enum SparklineVariant {
90 #[default]
92 Bars,
93 Braille,
95 Line,
97}
98
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
101pub enum SparklineBarsPreset {
102 #[default]
104 Blocks,
105 Shades,
107}
108
109impl SparklineBarsPreset {
110 pub(crate) fn glyphs(self) -> &'static [char] {
111 match self {
112 Self::Blocks => &DEFAULT_BARS,
113 Self::Shades => &SHADE_BARS,
114 }
115 }
116}
117
118#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
120pub enum SparklineAggregation {
121 #[default]
123 Average,
124 Min,
126 Max,
128 First,
130 Last,
132}
133
134#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
136pub enum SparklineZeroPolicy {
137 #[default]
139 Empty,
140 MinGlyph,
142}
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
146pub enum SparklineLinePreset {
147 #[default]
149 Unicode,
150 Ascii,
157}
158
159#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
161pub struct SparklineLineGlyphs {
162 pub rising: char,
164 pub falling: char,
166 pub flat: char,
168 pub peak: char,
170 pub valley: char,
172}
173
174impl SparklineLineGlyphs {
175 pub const UNICODE: Self = Self {
177 rising: '╱',
178 falling: '╲',
179 flat: '─',
180 peak: '╮',
181 valley: '╰',
182 };
183
184 pub const ASCII: Self = Self {
186 rising: '/',
187 falling: '\\',
188 flat: '-',
189 peak: '^',
190 valley: 'v',
191 };
192}
193
194impl Default for SparklineLineGlyphs {
195 fn default() -> Self {
196 Self::UNICODE
197 }
198}
199
200impl SparklineLinePreset {
201 pub(crate) fn glyphs(self) -> SparklineLineGlyphs {
202 match self {
203 Self::Unicode => SparklineLineGlyphs::UNICODE,
204 Self::Ascii => SparklineLineGlyphs::ASCII,
205 }
206 }
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub(crate) enum PointTrend {
211 Rising,
212 Falling,
213 Flat,
214 Turn,
215}
216
217#[derive(Clone)]
219pub struct Sparkline {
220 pub data: Arc<[u64]>,
222 pub min: Option<u64>,
224 pub max: Option<u64>,
226 pub bars: Vec<char>,
228 pub variant: SparklineVariant,
230 pub max_points: Option<usize>,
232 pub aggregation: SparklineAggregation,
234 pub zero_policy: SparklineZeroPolicy,
236 pub line_glyphs: SparklineLineGlyphs,
238 pub chart_height: u16,
240 pub mirror_x: bool,
242 pub mirror_y: bool,
244 pub style: Style,
246 pub rising_style: Style,
248 pub falling_style: Style,
250 pub flat_style: Style,
252 pub turn_style: Style,
254 pub gradient: Option<ColorGradient>,
256 pub height_gradient: Option<ColorGradient>,
258 pub gradient_range: Option<GradientRange>,
260 pub width: Length,
263 pub height: Length,
266 pub overflow: Overflow,
268}
269
270impl Sparkline {
271 pub fn new(data: impl IntoIterator<Item = u64>) -> Self {
273 Self {
274 data: data.into_iter().collect::<Vec<_>>().into(),
275 min: None,
276 max: None,
277 bars: DEFAULT_BARS.to_vec(),
278 variant: SparklineVariant::Bars,
279 max_points: None,
280 aggregation: SparklineAggregation::Average,
281 zero_policy: SparklineZeroPolicy::default(),
282 line_glyphs: SparklineLineGlyphs::default(),
283 chart_height: 1,
284 mirror_x: false,
285 mirror_y: false,
286 style: Style::default(),
287 rising_style: Style::default(),
288 falling_style: Style::default(),
289 flat_style: Style::default(),
290 turn_style: Style::default(),
291 gradient: None,
292 height_gradient: None,
293 gradient_range: None,
294 width: Length::Auto,
295 height: Length::Auto,
296 overflow: Overflow::Auto,
297 }
298 }
299
300 pub fn data(mut self, data: impl IntoIterator<Item = u64>) -> Self {
302 self.data = data.into_iter().collect::<Vec<_>>().into();
303 self
304 }
305
306 pub fn data_arc(mut self, data: Arc<[u64]>) -> Self {
308 self.data = data;
309 self
310 }
311
312 pub fn min(mut self, min: u64) -> Self {
314 self.min = Some(min);
315 self
316 }
317
318 pub fn max(mut self, max: u64) -> Self {
320 self.max = Some(max);
321 self
322 }
323
324 pub fn variant(mut self, variant: SparklineVariant) -> Self {
326 self.variant = variant;
327 self
328 }
329
330 pub fn line(mut self) -> Self {
332 self.variant = SparklineVariant::Line;
333 self
334 }
335
336 pub fn braille(mut self) -> Self {
338 self.variant = SparklineVariant::Braille;
339 self
340 }
341
342 pub fn bars(mut self, bars: impl IntoIterator<Item = char>) -> Self {
344 self.bars = bars.into_iter().collect();
345 self
346 }
347
348 pub fn bars_preset(mut self, preset: SparklineBarsPreset) -> Self {
350 self.bars = preset.glyphs().to_vec();
351 self
352 }
353
354 pub fn line_preset(mut self, preset: SparklineLinePreset) -> Self {
356 self.line_glyphs = preset.glyphs();
357 self
358 }
359
360 pub fn line_glyphs(mut self, glyphs: SparklineLineGlyphs) -> Self {
362 self.line_glyphs = glyphs;
363 self
364 }
365
366 pub fn chart_height(mut self, rows: u16) -> Self {
370 self.chart_height = rows.max(1);
371 self
372 }
373
374 pub fn mirror_x(mut self, mirror: bool) -> Self {
376 self.mirror_x = mirror;
377 self
378 }
379
380 pub fn mirror_y(mut self, mirror: bool) -> Self {
392 self.mirror_y = mirror;
393 self
394 }
395
396 pub fn max_points(mut self, max_points: usize) -> Self {
398 self.max_points = Some(max_points.max(1));
399 self
400 }
401
402 pub fn aggregation(mut self, aggregation: SparklineAggregation) -> Self {
404 self.aggregation = aggregation;
405 self
406 }
407
408 pub fn zero_policy(mut self, policy: SparklineZeroPolicy) -> Self {
410 self.zero_policy = policy;
411 self
412 }
413
414 pub fn gradient(mut self, gradient: ColorGradient) -> Self {
416 self.gradient = Some(gradient);
417 self
418 }
419
420 pub fn height_gradient(mut self, gradient: ColorGradient) -> Self {
422 self.height_gradient = Some(gradient);
423 self
424 }
425
426 pub fn gradient_range(mut self, min: u64, max: u64) -> Self {
428 self.gradient_range = Some(GradientRange::new(min, max));
429 self
430 }
431
432 pub fn style(mut self, style: Style) -> Self {
434 self.style = style;
435 self
436 }
437
438 pub fn rising_style(mut self, style: Style) -> Self {
440 self.rising_style = style;
441 self
442 }
443
444 pub fn falling_style(mut self, style: Style) -> Self {
446 self.falling_style = style;
447 self
448 }
449
450 pub fn flat_style(mut self, style: Style) -> Self {
452 self.flat_style = style;
453 self
454 }
455
456 pub fn turn_style(mut self, style: Style) -> Self {
458 self.turn_style = style;
459 self
460 }
461
462 pub fn width(mut self, width: Length) -> Self {
464 self.width = width;
465 self
466 }
467
468 pub fn height(mut self, height: Length) -> Self {
472 self.height = height;
473 self
474 }
475
476 pub fn overflow(mut self, overflow: Overflow) -> Self {
485 self.overflow = overflow;
486 self
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::{Sparkline, SparklineLinePreset, SparklineVariant};
493 use crate::core::element::{Element, ElementKind};
494 use crate::style::Length;
495
496 fn into_node(el: Element) -> super::node::SparklineNode {
497 match el.kind {
498 ElementKind::Sparkline(spark) => spark.into(),
499 _ => panic!("expected sparkline element"),
500 }
501 }
502
503 #[test]
504 fn default_bars_render_expected_ramp() {
505 let node = into_node(
506 Sparkline::new([0, 1, 2, 3, 4, 5, 6, 7])
507 .min(0)
508 .max(7)
509 .into(),
510 );
511 let content: String = node.output.rows[0]
512 .iter()
513 .map(|s| s.content.as_ref())
514 .collect();
515 assert_eq!(content, " ▂▃▄▅▆▇█");
516 }
517
518 #[test]
519 fn chart_height_renders_multi_row_bars() {
520 let node = into_node(
521 Sparkline::new([0, 25, 50, 75, 100])
522 .min(0)
523 .max(100)
524 .chart_height(3)
525 .into(),
526 );
527 assert_eq!(node.output.rows.len(), 3);
528 assert_eq!(node.height, Length::Auto);
530 }
531
532 #[test]
533 fn line_variant_renders_turn_glyphs() {
534 let node = into_node(Sparkline::new([1, 3, 2, 4, 4, 1]).line().into());
535 let content: String = node.output.rows[0]
536 .iter()
537 .map(|s| s.content.as_ref())
538 .collect();
539 assert_eq!(content, "╱╮╰╱╲╲");
540 }
541
542 #[test]
543 fn braille_variant_packs_two_samples_per_cell() {
544 let node = into_node(
545 Sparkline::new([0, 1, 2, 3, 4])
546 .variant(SparklineVariant::Braille)
547 .min(0)
548 .max(4)
549 .into(),
550 );
551
552 let content: String = node.output.rows[0]
553 .iter()
554 .map(|s| s.content.as_ref())
555 .collect();
556 assert_eq!(content.chars().count(), 3);
557 }
558
559 #[test]
560 fn line_ascii_preset_is_available() {
561 let node = into_node(
562 Sparkline::new([1, 2, 1])
563 .variant(SparklineVariant::Line)
564 .line_preset(SparklineLinePreset::Ascii)
565 .into(),
566 );
567 let content: String = node.output.rows[0]
568 .iter()
569 .map(|s| s.content.as_ref())
570 .collect();
571 assert_eq!(content, "/^\\");
572 }
573
574 #[test]
575 fn mirror_y_braille_flips_fill_direction() {
576 let normal_node = into_node(
577 Sparkline::new([1])
578 .variant(SparklineVariant::Braille)
579 .min(0)
580 .max(4)
581 .into(),
582 );
583 let mirrored_node = into_node(
584 Sparkline::new([1])
585 .variant(SparklineVariant::Braille)
586 .min(0)
587 .max(4)
588 .mirror_y(true)
589 .into(),
590 );
591
592 let normal: String = normal_node.output.rows[0]
593 .iter()
594 .map(|s| s.content.as_ref())
595 .collect();
596 let mirrored: String = mirrored_node.output.rows[0]
597 .iter()
598 .map(|s| s.content.as_ref())
599 .collect();
600
601 assert_eq!(normal, "⡀");
602 assert_eq!(mirrored, "⠁");
603 }
604
605 #[test]
606 fn data_arc_preserves_shared_slice() {
607 use std::sync::Arc;
608
609 let data: Arc<[u64]> = Arc::from([1u64, 2, 3, 4]);
610 let spark = Sparkline::new([]).data_arc(Arc::clone(&data));
611 assert!(Arc::ptr_eq(&spark.data, &data));
612 assert_eq!(spark.data.as_ref(), &[1, 2, 3, 4]);
613 }
614}