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    let y_range_span = (y_max - y_min).abs();
229
230    // Pre-calculate relative positions for the line (in 0..1 range)
231    let mut relative_points: Vec<(f32, f32)> = Vec::with_capacity(data.len());
232    for point in data {
233        let x_range = x_scale.scale(point.x);
234        let x_rel = ((x_range - x_min) / x_range_span) as f32;
235        let y_range = y_scale.scale(point.y);
236        // Invert Y for screen coordinates
237        let y_rel = 1.0 - ((y_range - y_min) / y_range_span) as f32;
238        relative_points.push((x_rel, y_rel));
239    }
240
241    let stroke_color = config.stroke_color.to_rgba();
242    let stroke_width = config.stroke_width;
243    let opacity = config.opacity;
244    let curve_type = config.curve;
245    let show_points = config.show_points;
246    let point_radius = config.point_radius;
247    let point_fill = config
248        .point_fill_color
249        .as_ref()
250        .unwrap_or(&config.stroke_color)
251        .to_rgba();
252
253    canvas(
254        // Prepaint: pass through the relative points and bounds info
255        move |bounds, _window, _cx| {
256            let width: f32 = bounds.size.width.into();
257            let height: f32 = bounds.size.height.into();
258            let origin_x: f32 = bounds.origin.x.into();
259            let origin_y: f32 = bounds.origin.y.into();
260
261            (relative_points.clone(), width, height, origin_x, origin_y)
262        },
263        // Paint: draw clipped line segments
264        move |_bounds,
265              (rel_points, width, height, origin_x, origin_y): (
266            Vec<(f32, f32)>,
267            f32,
268            f32,
269            f32,
270            f32,
271        ),
272              window,
273              _cx| {
274            if rel_points.len() < 2 {
275                return;
276            }
277
278            // Build segments to draw based on curve type, applying clipping
279            let segments_to_draw: Vec<(f32, f32, f32, f32)> = match curve_type {
280                CurveType::Linear => {
281                    let mut segments = Vec::new();
282                    for i in 1..rel_points.len() {
283                        let (x0, y0) = rel_points[i - 1];
284                        let (x1, y1) = rel_points[i];
285                        if let Some(clipped) = clip_line_segment(x0, y0, x1, y1) {
286                            segments.push(clipped);
287                        }
288                    }
289                    segments
290                }
291                CurveType::Step | CurveType::StepAfter => {
292                    let mut segments = Vec::new();
293                    for i in 1..rel_points.len() {
294                        let (x0, y0) = rel_points[i - 1];
295                        let (x1, y1) = rel_points[i];
296                        // Horizontal then vertical: (x0,y0) -> (x1,y0) -> (x1,y1)
297                        if let Some(clipped) = clip_line_segment(x0, y0, x1, y0) {
298                            segments.push(clipped);
299                        }
300                        if let Some(clipped) = clip_line_segment(x1, y0, x1, y1) {
301                            segments.push(clipped);
302                        }
303                    }
304                    segments
305                }
306                CurveType::StepBefore => {
307                    let mut segments = Vec::new();
308                    for i in 1..rel_points.len() {
309                        let (x0, y0) = rel_points[i - 1];
310                        let (x1, y1) = rel_points[i];
311                        // Vertical then horizontal: (x0,y0) -> (x0,y1) -> (x1,y1)
312                        if let Some(clipped) = clip_line_segment(x0, y0, x0, y1) {
313                            segments.push(clipped);
314                        }
315                        if let Some(clipped) = clip_line_segment(x0, y1, x1, y1) {
316                            segments.push(clipped);
317                        }
318                    }
319                    segments
320                }
321            };
322
323            // Build continuous paths from clipped segments
324            if !segments_to_draw.is_empty() {
325                let mut path_builder = PathBuilder::stroke(px(stroke_width));
326                let mut last_end: Option<(f32, f32)> = None;
327
328                for (x0, y0, x1, y1) in &segments_to_draw {
329                    let start = (origin_x + x0 * width, origin_y + y0 * height);
330                    let end = (origin_x + x1 * width, origin_y + y1 * height);
331
332                    // Check if we need to start a new path segment
333                    let need_move = match last_end {
334                        Some((lx, ly)) => (lx - start.0).abs() > 0.5 || (ly - start.1).abs() > 0.5,
335                        None => true,
336                    };
337
338                    if need_move {
339                        path_builder.move_to(gpui::point(px(start.0), px(start.1)));
340                    }
341                    path_builder.line_to(gpui::point(px(end.0), px(end.1)));
342                    last_end = Some(end);
343                }
344
345                if let Ok(path) = path_builder.build() {
346                    let color_with_opacity = Rgba {
347                        r: stroke_color.r,
348                        g: stroke_color.g,
349                        b: stroke_color.b,
350                        a: stroke_color.a * opacity,
351                    };
352                    window.paint_path(path, color_with_opacity);
353                }
354            }
355
356            // Paint points if enabled (only for points inside the clip region)
357            if show_points {
358                for &(x_rel, y_rel) in &rel_points {
359                    // Only draw points inside the chart area
360                    if x_rel >= 0.0 && x_rel <= 1.0 && y_rel >= 0.0 && y_rel <= 1.0 {
361                        let px_x = origin_x + x_rel * width;
362                        let px_y = origin_y + y_rel * height;
363                        let point_bounds = Bounds {
364                            origin: gpui::point(px(px_x - point_radius), px(px_y - point_radius)),
365                            size: gpui::size(px(point_radius * 2.0), px(point_radius * 2.0)),
366                        };
367                        let color_with_opacity = Rgba {
368                            r: point_fill.r,
369                            g: point_fill.g,
370                            b: point_fill.b,
371                            a: point_fill.a * opacity,
372                        };
373                        window.paint_quad(PaintQuad {
374                            bounds: point_bounds,
375                            corner_radii: Corners::all(px(point_radius)),
376                            background: color_with_opacity.into(),
377                            border_widths: Edges::default(),
378                            border_color: transparent_black(),
379                            border_style: BorderStyle::default(),
380                        });
381                    }
382                }
383            }
384        },
385    )
386    .size_full()
387    .absolute()
388    .inset_0()
389}