d3rs/shape/
line.rs

1//! Line chart rendering
2
3use crate::color::D3Color;
4use crate::scale::Scale;
5use gpui::prelude::*;
6use gpui::*;
7
8/// Curve interpolation types
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum CurveType {
11    /// Linear interpolation (straight lines between points)
12    Linear,
13    /// Step function (horizontal then vertical)
14    Step,
15    /// Step before (vertical then horizontal)
16    StepBefore,
17    /// Step after (horizontal then vertical)
18    StepAfter,
19}
20
21/// Configuration for line chart rendering
22#[derive(Clone)]
23pub struct LineConfig {
24    /// Stroke color for the line
25    pub stroke_color: D3Color,
26    /// Line width in pixels
27    pub stroke_width: f32,
28    /// Opacity of the line (0.0 - 1.0)
29    pub opacity: f32,
30    /// Curve interpolation type
31    pub curve: CurveType,
32    /// Whether to show points at data locations
33    pub show_points: bool,
34    /// Point radius if show_points is true
35    pub point_radius: f32,
36    /// Fill color for points
37    pub point_fill_color: Option<D3Color>,
38}
39
40impl Default for LineConfig {
41    fn default() -> Self {
42        Self {
43            stroke_color: D3Color::from_hex(0x4682b4), // Steel blue
44            stroke_width: 2.0,
45            opacity: 1.0,
46            curve: CurveType::Linear,
47            show_points: false,
48            point_radius: 3.0,
49            point_fill_color: None,
50        }
51    }
52}
53
54impl LineConfig {
55    /// Create a new line configuration with defaults
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Set the stroke color
61    pub fn stroke_color(mut self, color: D3Color) -> Self {
62        self.stroke_color = color;
63        self
64    }
65
66    /// Set the stroke width
67    pub fn stroke_width(mut self, width: f32) -> Self {
68        self.stroke_width = width;
69        self
70    }
71
72    /// Set the opacity
73    pub fn opacity(mut self, opacity: f32) -> Self {
74        self.opacity = opacity.clamp(0.0, 1.0);
75        self
76    }
77
78    /// Set the curve type
79    pub fn curve(mut self, curve: CurveType) -> Self {
80        self.curve = curve;
81        self
82    }
83
84    /// Enable point rendering
85    pub fn show_points(mut self, show: bool) -> Self {
86        self.show_points = show;
87        self
88    }
89
90    /// Set point radius
91    pub fn point_radius(mut self, radius: f32) -> Self {
92        self.point_radius = radius;
93        self
94    }
95
96    /// Set point fill color
97    pub fn point_fill_color(mut self, color: D3Color) -> Self {
98        self.point_fill_color = Some(color);
99        self
100    }
101}
102
103/// Data point for a line chart
104#[derive(Debug, Clone, Copy)]
105pub struct LinePoint {
106    /// X coordinate
107    pub x: f64,
108    /// Y coordinate
109    pub y: f64,
110}
111
112impl LinePoint {
113    /// Create a new line point
114    pub fn new(x: f64, y: f64) -> Self {
115        Self { x, y }
116    }
117}
118
119/// Clip a line segment to the unit rectangle [0,1] x [0,1] using Cohen-Sutherland algorithm
120/// Returns Some((x0, y0, x1, y1)) if the clipped segment is visible, None if entirely outside
121fn clip_line_segment(x0: f32, y0: f32, x1: f32, y1: f32) -> Option<(f32, f32, f32, f32)> {
122    const INSIDE: u8 = 0;
123    const LEFT: u8 = 1;
124    const RIGHT: u8 = 2;
125    const BOTTOM: u8 = 4;
126    const TOP: u8 = 8;
127
128    fn compute_outcode(x: f32, y: f32) -> u8 {
129        let mut code = INSIDE;
130        if x < 0.0 {
131            code |= LEFT;
132        } else if x > 1.0 {
133            code |= RIGHT;
134        }
135        if y < 0.0 {
136            code |= TOP;
137        } else if y > 1.0 {
138            code |= BOTTOM;
139        }
140        code
141    }
142
143    let mut x0 = x0;
144    let mut y0 = y0;
145    let mut x1 = x1;
146    let mut y1 = y1;
147    let mut outcode0 = compute_outcode(x0, y0);
148    let mut outcode1 = compute_outcode(x1, y1);
149
150    loop {
151        if (outcode0 | outcode1) == 0 {
152            // Both points inside
153            return Some((x0, y0, x1, y1));
154        } else if (outcode0 & outcode1) != 0 {
155            // Both points share an outside zone
156            return None;
157        } else {
158            // Calculate intersection
159            let outcode_out = if outcode0 != 0 { outcode0 } else { outcode1 };
160            let (x, y);
161
162            if (outcode_out & TOP) != 0 {
163                x = x0 + (x1 - x0) * (0.0 - y0) / (y1 - y0);
164                y = 0.0;
165            } else if (outcode_out & BOTTOM) != 0 {
166                x = x0 + (x1 - x0) * (1.0 - y0) / (y1 - y0);
167                y = 1.0;
168            } else if (outcode_out & RIGHT) != 0 {
169                y = y0 + (y1 - y0) * (1.0 - x0) / (x1 - x0);
170                x = 1.0;
171            } else {
172                // LEFT
173                y = y0 + (y1 - y0) * (0.0 - x0) / (x1 - x0);
174                x = 0.0;
175            }
176
177            if outcode_out == outcode0 {
178                x0 = x;
179                y0 = y;
180                outcode0 = compute_outcode(x0, y0);
181            } else {
182                x1 = x;
183                y1 = y;
184                outcode1 = compute_outcode(x1, y1);
185            }
186        }
187    }
188}
189
190/// Render a line chart using GPUI's PathBuilder for proper vector line rendering
191///
192/// # Example
193///
194/// ```rust,no_run
195/// use d3rs::prelude::*;
196/// use d3rs::shape::{render_line, LineConfig, LinePoint, CurveType};
197///
198/// let x_scale = LinearScale::new().domain(0.0, 100.0).range(0.0, 400.0);
199/// let y_scale = LinearScale::new().domain(0.0, 100.0).range(300.0, 0.0);
200///
201/// let data = vec![
202///     LinePoint::new(0.0, 20.0),
203///     LinePoint::new(25.0, 50.0),
204///     LinePoint::new(50.0, 30.0),
205///     LinePoint::new(75.0, 80.0),
206///     LinePoint::new(100.0, 60.0),
207/// ];
208///
209/// let config = LineConfig::new()
210///     .stroke_color(D3Color::from_hex(0x4682b4))
211///     .curve(CurveType::Linear)
212///     .show_points(true);
213/// // render_line(&x_scale, &y_scale, &data, &config)
214/// ```
215pub fn render_line<XS, YS>(
216    x_scale: &XS,
217    y_scale: &YS,
218    data: &[LinePoint],
219    config: &LineConfig,
220) -> impl IntoElement
221where
222    XS: Scale<f64, f64>,
223    YS: Scale<f64, f64>,
224{
225    let (x_min, x_max) = x_scale.range();
226    let (y_min, y_max) = y_scale.range();
227    let x_range_span = x_max - x_min;
228
229    // Pre-calculate relative positions for the line (in 0..1 range)
230    // The scale maps domain values to range values (screen coordinates)
231    // We need to normalize to 0..1 where 0 is the top of the plot area
232    let mut relative_points: Vec<(f32, f32)> = Vec::with_capacity(data.len());
233    for point in data {
234        let x_range = x_scale.scale(point.x);
235        let x_rel = ((x_range - x_min) / x_range_span) as f32;
236        let y_range = y_scale.scale(point.y);
237        // y_range is in screen coordinates
238        // For inverted range (typical: range(height, 0)), y_min > y_max
239        // y_range=0 (top) should map to y_rel=0, y_range=y_min (bottom) should map to y_rel=1
240        let y_rel = if y_min > y_max {
241            // Inverted range: y_min is at bottom, y_max (0) is at top
242            (y_range / y_min) as f32
243        } else {
244            // Normal range: y_min is at top (0), y_max is at bottom
245            ((y_range - y_min) / (y_max - y_min)) as f32
246        };
247        relative_points.push((x_rel, y_rel));
248    }
249
250    let stroke_color = config.stroke_color.to_rgba();
251    let stroke_width = config.stroke_width;
252    let opacity = config.opacity;
253    let curve_type = config.curve;
254    let show_points = config.show_points;
255    let point_radius = config.point_radius;
256    let point_fill = config
257        .point_fill_color
258        .as_ref()
259        .unwrap_or(&config.stroke_color)
260        .to_rgba();
261
262    canvas(
263        // Prepaint: pass through the relative points and bounds info
264        move |bounds, _window, _cx| {
265            let width: f32 = bounds.size.width.into();
266            let height: f32 = bounds.size.height.into();
267            let origin_x: f32 = bounds.origin.x.into();
268            let origin_y: f32 = bounds.origin.y.into();
269
270            (relative_points.clone(), width, height, origin_x, origin_y)
271        },
272        // Paint: draw clipped line segments
273        move |_bounds,
274              (rel_points, width, height, origin_x, origin_y): (
275            Vec<(f32, f32)>,
276            f32,
277            f32,
278            f32,
279            f32,
280        ),
281              window,
282              _cx| {
283            if rel_points.len() < 2 {
284                return;
285            }
286
287            // Build segments to draw based on curve type, applying clipping
288            let segments_to_draw: Vec<(f32, f32, f32, f32)> = match curve_type {
289                CurveType::Linear => {
290                    let mut segments = Vec::new();
291                    for i in 1..rel_points.len() {
292                        let (x0, y0) = rel_points[i - 1];
293                        let (x1, y1) = rel_points[i];
294                        if let Some(clipped) = clip_line_segment(x0, y0, x1, y1) {
295                            segments.push(clipped);
296                        }
297                    }
298                    segments
299                }
300                CurveType::Step | CurveType::StepAfter => {
301                    let mut segments = Vec::new();
302                    for i in 1..rel_points.len() {
303                        let (x0, y0) = rel_points[i - 1];
304                        let (x1, y1) = rel_points[i];
305                        // Horizontal then vertical: (x0,y0) -> (x1,y0) -> (x1,y1)
306                        if let Some(clipped) = clip_line_segment(x0, y0, x1, y0) {
307                            segments.push(clipped);
308                        }
309                        if let Some(clipped) = clip_line_segment(x1, y0, x1, y1) {
310                            segments.push(clipped);
311                        }
312                    }
313                    segments
314                }
315                CurveType::StepBefore => {
316                    let mut segments = Vec::new();
317                    for i in 1..rel_points.len() {
318                        let (x0, y0) = rel_points[i - 1];
319                        let (x1, y1) = rel_points[i];
320                        // Vertical then horizontal: (x0,y0) -> (x0,y1) -> (x1,y1)
321                        if let Some(clipped) = clip_line_segment(x0, y0, x0, y1) {
322                            segments.push(clipped);
323                        }
324                        if let Some(clipped) = clip_line_segment(x0, y1, x1, y1) {
325                            segments.push(clipped);
326                        }
327                    }
328                    segments
329                }
330            };
331
332            // Build continuous paths from clipped segments
333            if !segments_to_draw.is_empty() {
334                let mut path_builder = PathBuilder::stroke(px(stroke_width));
335                let mut last_end: Option<(f32, f32)> = None;
336
337                for (x0, y0, x1, y1) in &segments_to_draw {
338                    let start = (origin_x + x0 * width, origin_y + y0 * height);
339                    let end = (origin_x + x1 * width, origin_y + y1 * height);
340
341                    // Check if we need to start a new path segment
342                    let need_move = match last_end {
343                        Some((lx, ly)) => (lx - start.0).abs() > 0.5 || (ly - start.1).abs() > 0.5,
344                        None => true,
345                    };
346
347                    if need_move {
348                        path_builder.move_to(gpui::point(px(start.0), px(start.1)));
349                    }
350                    path_builder.line_to(gpui::point(px(end.0), px(end.1)));
351                    last_end = Some(end);
352                }
353
354                if let Ok(path) = path_builder.build() {
355                    let color_with_opacity = Rgba {
356                        r: stroke_color.r,
357                        g: stroke_color.g,
358                        b: stroke_color.b,
359                        a: stroke_color.a * opacity,
360                    };
361                    window.paint_path(path, color_with_opacity);
362                }
363            }
364
365            // Paint points if enabled (only for points inside the clip region)
366            if show_points {
367                for &(x_rel, y_rel) in &rel_points {
368                    // Only draw points inside the chart area
369                    if (0.0..=1.0).contains(&x_rel) && (0.0..=1.0).contains(&y_rel) {
370                        let px_x = origin_x + x_rel * width;
371                        let px_y = origin_y + y_rel * height;
372                        let point_bounds = Bounds {
373                            origin: gpui::point(px(px_x - point_radius), px(px_y - point_radius)),
374                            size: gpui::size(px(point_radius * 2.0), px(point_radius * 2.0)),
375                        };
376                        let color_with_opacity = Rgba {
377                            r: point_fill.r,
378                            g: point_fill.g,
379                            b: point_fill.b,
380                            a: point_fill.a * opacity,
381                        };
382                        window.paint_quad(PaintQuad {
383                            bounds: point_bounds,
384                            corner_radii: Corners::all(px(point_radius)),
385                            background: color_with_opacity.into(),
386                            border_widths: Edges::default(),
387                            border_color: transparent_black(),
388                            border_style: BorderStyle::default(),
389                        });
390                    }
391                }
392            }
393        },
394    )
395    .size_full()
396    .absolute()
397    .inset_0()
398}