Skip to main content

revue/widget/chart/
waveline.rs

1//! Waveline Chart Widget
2//!
3//! A smooth, flowing line chart for visualizing continuous data like audio waveforms,
4//! signal processing data, or any oscillating values.
5//!
6//! # Features
7//!
8//! - Smooth curve interpolation
9//! - Multiple display modes (line, filled, mirrored)
10//! - Configurable amplitude and baseline
11//! - Gradient fills
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use revue::widget::{waveline, WaveStyle};
17//!
18//! let audio_data: Vec<f64> = get_audio_samples();
19//! let wave = waveline(audio_data)
20//!     .style(WaveStyle::Filled)
21//!     .color(Color::CYAN)
22//!     .baseline(0.5);
23//! ```
24
25use crate::render::Cell;
26use crate::style::Color;
27use crate::widget::traits::{RenderContext, View, WidgetProps};
28use crate::{impl_props_builders, impl_styled_view};
29
30/// Display style for the waveline
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub enum WaveStyle {
33    /// Simple line
34    #[default]
35    Line,
36    /// Filled area under the line
37    Filled,
38    /// Mirrored (centered, like audio visualization)
39    Mirrored,
40    /// Bars instead of smooth line
41    Bars,
42    /// Dots at data points
43    Dots,
44    /// Smooth bezier curve
45    Smooth,
46}
47
48/// Interpolation method for smooth curves
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum Interpolation {
51    /// No interpolation (connect points directly)
52    #[default]
53    Linear,
54    /// Smooth bezier curves
55    Bezier,
56    /// Catmull-Rom spline
57    CatmullRom,
58    /// Step function
59    Step,
60}
61
62/// Waveline chart widget
63#[derive(Debug, Clone)]
64pub struct Waveline {
65    /// Data points (values between 0.0 and 1.0 work best)
66    data: Vec<f64>,
67    /// Display style
68    style: WaveStyle,
69    /// Interpolation method
70    interpolation: Interpolation,
71    /// Primary color
72    color: Color,
73    /// Secondary color for gradients
74    gradient_color: Option<Color>,
75    /// Baseline position (0.0 = bottom, 1.0 = top, 0.5 = center)
76    baseline: f64,
77    /// Amplitude multiplier
78    amplitude: f64,
79    /// Show zero line
80    show_baseline: bool,
81    /// Baseline color
82    baseline_color: Color,
83    /// Background color
84    bg_color: Option<Color>,
85    /// Height in rows
86    height: Option<u16>,
87    /// Maximum data points to display
88    max_points: Option<usize>,
89    /// Label
90    label: Option<String>,
91    /// CSS styling properties (id, classes)
92    props: WidgetProps,
93}
94
95impl Default for Waveline {
96    fn default() -> Self {
97        Self::new(Vec::new())
98    }
99}
100
101impl Waveline {
102    /// Create a new waveline chart
103    pub fn new(data: Vec<f64>) -> Self {
104        Self {
105            data,
106            style: WaveStyle::Line,
107            interpolation: Interpolation::Linear,
108            color: Color::CYAN,
109            gradient_color: None,
110            baseline: 0.5,
111            amplitude: 1.0,
112            show_baseline: false,
113            baseline_color: Color::rgb(80, 80, 80),
114            bg_color: None,
115            height: None,
116            max_points: None,
117            label: None,
118            props: WidgetProps::new(),
119        }
120    }
121
122    /// Set data points
123    pub fn data(mut self, data: Vec<f64>) -> Self {
124        self.data = data;
125        self
126    }
127
128    /// Set display style
129    pub fn style(mut self, style: WaveStyle) -> Self {
130        self.style = style;
131        self
132    }
133
134    /// Set interpolation method
135    pub fn interpolation(mut self, method: Interpolation) -> Self {
136        self.interpolation = method;
137        self
138    }
139
140    /// Set line/fill color
141    pub fn color(mut self, color: Color) -> Self {
142        self.color = color;
143        self
144    }
145
146    /// Set gradient colors
147    pub fn gradient(mut self, start: Color, end: Color) -> Self {
148        self.color = start;
149        self.gradient_color = Some(end);
150        self
151    }
152
153    /// Set baseline position (0.0 = bottom, 1.0 = top)
154    pub fn baseline(mut self, position: f64) -> Self {
155        self.baseline = position.clamp(0.0, 1.0);
156        self
157    }
158
159    /// Set amplitude multiplier
160    pub fn amplitude(mut self, amp: f64) -> Self {
161        self.amplitude = amp;
162        self
163    }
164
165    /// Show or hide baseline
166    pub fn show_baseline(mut self, show: bool) -> Self {
167        self.show_baseline = show;
168        self
169    }
170
171    /// Set baseline color
172    pub fn baseline_color(mut self, color: Color) -> Self {
173        self.baseline_color = color;
174        self
175    }
176
177    /// Set background color
178    pub fn bg(mut self, color: Color) -> Self {
179        self.bg_color = Some(color);
180        self
181    }
182
183    /// Set height
184    pub fn height(mut self, height: u16) -> Self {
185        self.height = Some(height);
186        self
187    }
188
189    /// Set maximum points to display
190    pub fn max_points(mut self, max: usize) -> Self {
191        self.max_points = Some(max);
192        self
193    }
194
195    /// Set label
196    pub fn label(mut self, label: impl Into<String>) -> Self {
197        self.label = Some(label.into());
198        self
199    }
200
201    fn get_color(&self, ratio: f64) -> Color {
202        if let Some(end) = self.gradient_color {
203            let r = (self.color.r as f64 * (1.0 - ratio) + end.r as f64 * ratio) as u8;
204            let g = (self.color.g as f64 * (1.0 - ratio) + end.g as f64 * ratio) as u8;
205            let b = (self.color.b as f64 * (1.0 - ratio) + end.b as f64 * ratio) as u8;
206            Color::rgb(r, g, b)
207        } else {
208            self.color
209        }
210    }
211
212    fn get_interpolated_value(&self, data: &[f64], x: usize, width: usize) -> f64 {
213        if data.is_empty() {
214            return 0.0;
215        }
216        let ratio = x as f64 / (width - 1).max(1) as f64;
217        let idx = ratio * (data.len() - 1) as f64;
218        let idx_floor = idx.floor() as usize;
219        let idx_ceil = (idx_floor + 1).min(data.len() - 1);
220        let t = idx - idx_floor as f64;
221
222        match self.interpolation {
223            Interpolation::Linear => data[idx_floor] * (1.0 - t) + data[idx_ceil] * t,
224            Interpolation::Step => data[idx_floor],
225            Interpolation::Bezier | Interpolation::CatmullRom => {
226                let p0_idx = idx_floor.saturating_sub(1);
227                let p3_idx = (idx_ceil + 1).min(data.len() - 1);
228
229                let p0 = data[p0_idx];
230                let p1 = data[idx_floor];
231                let p2 = data[idx_ceil];
232                let p3 = data[p3_idx];
233
234                let t2 = t * t;
235                let t3 = t2 * t;
236
237                0.5 * ((2.0 * p1)
238                    + (-p0 + p2) * t
239                    + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
240                    + (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
241            }
242        }
243    }
244}
245
246impl View for Waveline {
247    fn render(&self, ctx: &mut RenderContext) {
248        let area = ctx.area;
249        let height = self.height.unwrap_or(area.height);
250
251        if area.width < 2 || height < 1 {
252            return;
253        }
254
255        let mut chart_y = area.y;
256        let mut chart_height = height.min(area.height);
257
258        // Background
259        if let Some(bg) = self.bg_color {
260            for y in area.y..area.y + chart_height {
261                for x in area.x..area.x + area.width {
262                    let mut cell = Cell::new(' ');
263                    cell.bg = Some(bg);
264                    ctx.buffer.set(x, y, cell);
265                }
266            }
267        }
268
269        // Label
270        if let Some(ref label) = self.label {
271            ctx.buffer
272                .put_str_styled(area.x, chart_y, label, Some(Color::WHITE), self.bg_color);
273            chart_y += 1;
274            chart_height = chart_height.saturating_sub(1);
275        }
276
277        if chart_height < 1 || self.data.is_empty() {
278            return;
279        }
280
281        // Determine data range
282        let data = if let Some(max) = self.max_points {
283            if self.data.len() > max {
284                &self.data[self.data.len() - max..]
285            } else {
286                &self.data[..]
287            }
288        } else {
289            &self.data[..]
290        };
291
292        let width = area.width as usize;
293
294        // Draw baseline
295        if self.show_baseline {
296            let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
297            let y = chart_y + baseline_row;
298            for x in area.x..area.x + area.width {
299                let mut cell = Cell::new('─');
300                cell.fg = Some(self.baseline_color);
301                ctx.buffer.set(x, y, cell);
302            }
303        }
304
305        match self.style {
306            WaveStyle::Line | WaveStyle::Smooth => {
307                for x in 0..width {
308                    let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
309                        .clamp(-1.0, 1.0);
310                    let y_ratio = self.baseline + val * (1.0 - self.baseline);
311                    let y = chart_y + ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
312
313                    if y >= chart_y && y < chart_y + chart_height {
314                        let screen_x = area.x + x as u16;
315                        let mut cell = Cell::new('●');
316                        cell.fg = Some(self.get_color(y_ratio));
317                        ctx.buffer.set(screen_x, y, cell);
318                    }
319                }
320            }
321            WaveStyle::Filled => {
322                let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
323
324                for x in 0..width {
325                    let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
326                        .clamp(-1.0, 1.0);
327                    let y_ratio = self.baseline + val * (1.0 - self.baseline);
328                    let y = ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
329
330                    let screen_x = area.x + x as u16;
331
332                    let (start_y, end_y) = if y <= baseline_row {
333                        (y, baseline_row)
334                    } else {
335                        (baseline_row, y)
336                    };
337
338                    for dy in start_y..=end_y {
339                        if dy < chart_height {
340                            let screen_y = chart_y + dy;
341                            let ch = if dy == y { '█' } else { '▓' };
342                            let ratio = 1.0 - dy as f64 / (chart_height - 1) as f64;
343                            let mut cell = Cell::new(ch);
344                            cell.fg = Some(self.get_color(ratio));
345                            ctx.buffer.set(screen_x, screen_y, cell);
346                        }
347                    }
348                }
349            }
350            WaveStyle::Mirrored => {
351                let center_y = chart_height / 2;
352
353                for x in 0..width {
354                    let val = (self.get_interpolated_value(data, x, width).abs() * self.amplitude)
355                        .clamp(0.0, 1.0);
356                    let half_height = (val * center_y as f64) as u16;
357
358                    let screen_x = area.x + x as u16;
359
360                    // Draw upper half
361                    for dy in 0..=half_height {
362                        let screen_y = chart_y + center_y.saturating_sub(dy);
363                        if screen_y >= chart_y {
364                            let intensity = 1.0 - dy as f64 / center_y as f64;
365                            let ch = if dy == half_height { '▀' } else { '█' };
366                            let mut cell = Cell::new(ch);
367                            cell.fg = Some(self.get_color(0.5 + intensity * 0.5));
368                            ctx.buffer.set(screen_x, screen_y, cell);
369                        }
370                    }
371
372                    // Draw lower half
373                    for dy in 0..=half_height {
374                        let screen_y = chart_y + center_y + dy;
375                        if screen_y < chart_y + chart_height {
376                            let intensity = 1.0 - dy as f64 / center_y as f64;
377                            let ch = if dy == half_height { '▄' } else { '█' };
378                            let mut cell = Cell::new(ch);
379                            cell.fg = Some(self.get_color(0.5 + intensity * 0.5));
380                            ctx.buffer.set(screen_x, screen_y, cell);
381                        }
382                    }
383                }
384            }
385            WaveStyle::Bars => {
386                let baseline_row = ((1.0 - self.baseline) * (chart_height - 1) as f64) as u16;
387                let bar_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
388
389                for x in 0..width {
390                    let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
391                        .clamp(-1.0, 1.0);
392                    let y_ratio = self.baseline + val * (1.0 - self.baseline);
393                    let target_y = ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
394
395                    let screen_x = area.x + x as u16;
396
397                    if val >= 0.0 {
398                        for dy in target_y..=baseline_row {
399                            if dy < chart_height {
400                                let screen_y = chart_y + dy;
401                                let ch = if dy == target_y {
402                                    let frac = (y_ratio * 8.0).fract();
403                                    bar_chars[(frac * 8.0) as usize % 8]
404                                } else {
405                                    '█'
406                                };
407                                let mut cell = Cell::new(ch);
408                                cell.fg = Some(self.get_color(y_ratio));
409                                ctx.buffer.set(screen_x, screen_y, cell);
410                            }
411                        }
412                    } else {
413                        for dy in baseline_row..=target_y {
414                            if dy < chart_height {
415                                let screen_y = chart_y + dy;
416                                let ch = if dy == target_y {
417                                    let frac = 1.0 - (y_ratio * 8.0).fract();
418                                    bar_chars[(frac * 8.0) as usize % 8]
419                                } else {
420                                    '█'
421                                };
422                                let mut cell = Cell::new(ch);
423                                cell.fg = Some(self.get_color(y_ratio));
424                                ctx.buffer.set(screen_x, screen_y, cell);
425                            }
426                        }
427                    }
428                }
429            }
430            WaveStyle::Dots => {
431                for x in 0..width {
432                    let val = (self.get_interpolated_value(data, x, width) * self.amplitude)
433                        .clamp(-1.0, 1.0);
434                    let y_ratio = self.baseline + val * (1.0 - self.baseline);
435                    let y = chart_y + ((1.0 - y_ratio) * (chart_height - 1) as f64) as u16;
436
437                    if y >= chart_y && y < chart_y + chart_height {
438                        let screen_x = area.x + x as u16;
439                        let mut cell = Cell::new('⣿');
440                        cell.fg = Some(self.get_color(y_ratio));
441                        ctx.buffer.set(screen_x, y, cell);
442                    }
443                }
444            }
445        }
446    }
447
448    crate::impl_view_meta!("Waveline");
449}
450
451impl_styled_view!(Waveline);
452impl_props_builders!(Waveline);
453
454// Convenience constructors
455
456/// Create a new waveline chart
457pub fn waveline(data: Vec<f64>) -> Waveline {
458    Waveline::new(data)
459}
460
461/// Create an audio waveform visualization
462pub fn audio_waveform(samples: Vec<f64>) -> Waveline {
463    Waveline::new(samples)
464        .style(WaveStyle::Mirrored)
465        .color(Color::CYAN)
466        .gradient(Color::BLUE, Color::CYAN)
467}
468
469/// Create a signal wave visualization
470pub fn signal_wave(data: Vec<f64>) -> Waveline {
471    Waveline::new(data)
472        .style(WaveStyle::Line)
473        .interpolation(Interpolation::CatmullRom)
474        .color(Color::GREEN)
475        .show_baseline(true)
476}
477
478/// Create a filled area wave
479pub fn area_wave(data: Vec<f64>) -> Waveline {
480    Waveline::new(data)
481        .style(WaveStyle::Filled)
482        .color(Color::MAGENTA)
483        .baseline(1.0)
484}
485
486/// Create a bar spectrum visualization
487pub fn spectrum(data: Vec<f64>) -> Waveline {
488    Waveline::new(data)
489        .style(WaveStyle::Bars)
490        .color(Color::YELLOW)
491        .baseline(1.0)
492}
493
494/// Generate sine wave data
495pub fn sine_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
496    (0..samples)
497        .map(|i| {
498            let t = i as f64 / samples as f64 * std::f64::consts::PI * 2.0 * frequency;
499            t.sin() * amplitude
500        })
501        .collect()
502}
503
504/// Generate square wave data
505pub fn square_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
506    (0..samples)
507        .map(|i| {
508            let t = i as f64 / samples as f64 * frequency;
509            if t.fract() < 0.5 {
510                amplitude
511            } else {
512                -amplitude
513            }
514        })
515        .collect()
516}
517
518/// Generate sawtooth wave data
519pub fn sawtooth_wave(samples: usize, frequency: f64, amplitude: f64) -> Vec<f64> {
520    (0..samples)
521        .map(|i| {
522            let t = i as f64 / samples as f64 * frequency;
523            (t.fract() * 2.0 - 1.0) * amplitude
524        })
525        .collect()
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn test_waveline_creation() {
534        let data = vec![0.0, 0.5, 1.0, 0.5, 0.0];
535        let wave = waveline(data.clone());
536
537        assert_eq!(wave.data, data);
538    }
539
540    #[test]
541    fn test_waveline_styles() {
542        let data = vec![0.5; 10];
543        let wave = waveline(data)
544            .style(WaveStyle::Mirrored)
545            .color(Color::RED)
546            .amplitude(0.8);
547
548        assert_eq!(wave.style, WaveStyle::Mirrored);
549        assert_eq!(wave.color, Color::RED);
550        assert_eq!(wave.amplitude, 0.8);
551    }
552
553    #[test]
554    fn test_sine_wave_generation() {
555        let data = sine_wave(100, 2.0, 1.0);
556        assert_eq!(data.len(), 100);
557        assert!(data.iter().all(|&v| v >= -1.0 && v <= 1.0));
558    }
559
560    #[test]
561    fn test_interpolation() {
562        let wave = waveline(vec![0.0, 1.0, 0.0]).interpolation(Interpolation::CatmullRom);
563
564        assert_eq!(wave.interpolation, Interpolation::CatmullRom);
565    }
566}