tawasuyu

tawasuyu/brahman

Rust 212TOML 64JSON 13HTML 12666 Sutichaykuna

⎇ 66d18ab47cd9ae7d14a3131aa703cac00de19260 crates/modules/ui_engine/widgets/lapaloma-cartesian/src/element.rs Rust · 313 siq'ikuna · 10.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(lapaloma-cartesian): multi-series en LapalomaChartElement 66d18ab47 · sergio ·

1//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
2//! pipeline cartesian.
3//!
4//! Mantiene un `Vec<ChartSeriesItem>` con `DataBuffer + StrokeStyle`
5//! por serie. En `paint()` arma el `WindowCanvas` adapter, pinta el
6//! background, los ejes con sus labels, y delega a una `LineSeries`
7//! por cada item. Cada serie emite un solo `paint_path` — N series
8//! = N draw calls totales (no N × por punto).
9//!
10//! Todas las series comparten viewport. Para Y axis dual (necesario
11//! cuando series tienen rangos muy distintos) la API se va a
12//! extender con `y_axis_id` por serie y un viewport segundario.
13
14use std::panic;
15
16use gpui::{
17    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
18    Pixels, Style, Window,
19};
20
21use lapaloma_core::buffer::DataBuffer;
22use lapaloma_core::scale::nice_step;
23use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle, WindowCanvas};
24
25use crate::axis::{decimate_labels, format_tick, ticks_nice, AxisStyle};
26use crate::coord_system::CoordinateSystem;
27use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
28use crate::viewport::ChartViewport;
29
30const TARGET_TICKS_X: usize = 8;
31const TARGET_TICKS_Y: usize = 6;
32
33/// Aproximación del ancho de glifo monoespaciado en función del
34/// font size. Suficiente para alinear / decimar labels.
35const MONO_GLYPH_RATIO: f32 = 0.55;
36
37/// Una serie del chart: data + estilo + nombre opcional (para
38/// futura leyenda visual).
39#[derive(Clone)]
40pub struct ChartSeriesItem {
41    pub data: DataBuffer,
42    pub stroke: StrokeStyle,
43    pub name: Option<String>,
44}
45
46impl ChartSeriesItem {
47    pub fn new(data: DataBuffer, stroke: StrokeStyle) -> Self {
48        Self { data, stroke, name: None }
49    }
50
51    pub fn named(data: DataBuffer, stroke: StrokeStyle, name: impl Into<String>) -> Self {
52        Self { data, stroke, name: Some(name.into()) }
53    }
54}
55
56pub struct LapalomaChartElement {
57    pub series: Vec<ChartSeriesItem>,
58    pub viewport: ChartViewport,
59    pub background: Option<Color>,
60    pub axis_color: Color,
61    pub axis_style: AxisStyle,
62    pub margin_bottom: f32,
63    pub margin_left: f32,
64    pub margin_top: f32,
65    pub margin_right: f32,
66    /// Scratch reusable entre series y entre frames.
67    scratch: Vec<f32>,
68}
69
70impl LapalomaChartElement {
71    /// Construye un chart vacío con un viewport. Las series se
72    /// agregan con `add_series` / `add_series_named`.
73    pub fn new(viewport: ChartViewport) -> Self {
74        Self {
75            series: Vec::new(),
76            viewport,
77            background: None,
78            axis_color: Color::rgba(0.6, 0.6, 0.65, 0.8),
79            axis_style: AxisStyle::default(),
80            margin_bottom: 24.0,
81            margin_left: 32.0,
82            margin_top: 8.0,
83            margin_right: 8.0,
84            scratch: Vec::new(),
85        }
86    }
87
88    pub fn add_series(mut self, data: DataBuffer, stroke: StrokeStyle) -> Self {
89        self.series.push(ChartSeriesItem::new(data, stroke));
90        self
91    }
92
93    pub fn add_series_named(
94        mut self,
95        data: DataBuffer,
96        stroke: StrokeStyle,
97        name: impl Into<String>,
98    ) -> Self {
99        self.series.push(ChartSeriesItem::named(data, stroke, name));
100        self
101    }
102
103    pub fn background(mut self, color: Color) -> Self {
104        self.background = Some(color);
105        self
106    }
107
108    pub fn axis_color(mut self, color: Color) -> Self {
109        self.axis_color = color;
110        self
111    }
112
113    pub fn margins(mut self, top: f32, right: f32, bottom: f32, left: f32) -> Self {
114        self.margin_top = top;
115        self.margin_right = right;
116        self.margin_bottom = bottom;
117        self.margin_left = left;
118        self
119    }
120
121    fn plot_rect(&self, bounds: Rect) -> Rect {
122        Rect::new(
123            bounds.x + self.margin_left,
124            bounds.y + self.margin_top,
125            (bounds.w - self.margin_left - self.margin_right).max(1.0),
126            (bounds.h - self.margin_top - self.margin_bottom).max(1.0),
127        )
128    }
129
130    fn paint_axes(&self, canvas: &mut dyn Canvas, cs: &CoordinateSystem) {
131        let plot = cs.plot;
132        let style = self.axis_style;
133        let axis_stroke = StrokeStyle::new(style.axis_line_width_px, self.axis_color);
134        let tick_stroke = StrokeStyle::new(style.tick_width_px, self.axis_color);
135        let tlen = style.tick_length_px;
136
137        canvas.stroke_line(
138            Point::new(plot.x, plot.bottom()),
139            Point::new(plot.right(), plot.bottom()),
140            axis_stroke,
141        );
142        canvas.stroke_line(
143            Point::new(plot.x, plot.y),
144            Point::new(plot.x, plot.bottom()),
145            axis_stroke,
146        );
147
148        // X axis ticks + labels.
149        let x_ticks = ticks_nice(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
150        let x_step = nice_step(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
151        let mut x_pos: Vec<f32> = Vec::with_capacity(x_ticks.len());
152        let mut x_lbl: Vec<String> = Vec::with_capacity(x_ticks.len());
153        let mut x_widths: Vec<f32> = Vec::with_capacity(x_ticks.len());
154        for v in &x_ticks {
155            let pixel = cs.data_to_pixel(*v, self.viewport.y_min).x;
156            if pixel < plot.x - 0.5 || pixel > plot.right() + 0.5 {
157                continue;
158            }
159            canvas.stroke_line(
160                Point::new(pixel, plot.bottom()),
161                Point::new(pixel, plot.bottom() + tlen),
162                tick_stroke,
163            );
164            let lbl = format_tick(*v, x_step);
165            let w = lbl.len() as f32 * style.label_size_px * MONO_GLYPH_RATIO;
166            x_pos.push(pixel);
167            x_widths.push(w);
168            x_lbl.push(lbl);
169        }
170        let keep_x = decimate_labels(&x_pos, &x_widths, style.label_min_spacing_px);
171        for i in keep_x {
172            let half = x_widths[i] * 0.5;
173            let origin_x = x_pos[i] - half;
174            let origin_y = plot.bottom() + tlen + style.label_offset_px;
175            canvas.draw_text(
176                Point::new(origin_x, origin_y),
177                &x_lbl[i],
178                self.axis_color,
179                style.label_size_px,
180            );
181        }
182
183        // Y axis ticks + labels con decimación vertical.
184        let y_ticks = ticks_nice(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
185        let y_step = nice_step(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
186        let y_label_pitch = style.label_size_px + style.label_min_spacing_px;
187        let mut prev_py: Option<f32> = None;
188
189        for v in &y_ticks {
190            let py = cs.data_to_pixel(self.viewport.x_min, *v).y;
191            if py < plot.y - 0.5 || py > plot.bottom() + 0.5 {
192                continue;
193            }
194            canvas.stroke_line(
195                Point::new(plot.x - tlen, py),
196                Point::new(plot.x, py),
197                tick_stroke,
198            );
199            let label_ok = match prev_py {
200                None => true,
201                Some(p) => (py - p).abs() >= y_label_pitch,
202            };
203            if !label_ok {
204                continue;
205            }
206            let lbl = format_tick(*v, y_step);
207            let w = lbl.len() as f32 * style.label_size_px * MONO_GLYPH_RATIO;
208            let origin_x = plot.x - tlen - style.label_offset_px - w;
209            let origin_y = py - style.label_size_px * 0.5;
210            canvas.draw_text(
211                Point::new(origin_x, origin_y),
212                &lbl,
213                self.axis_color,
214                style.label_size_px,
215            );
216            prev_py = Some(py);
217        }
218    }
219}
220
221impl IntoElement for LapalomaChartElement {
222    type Element = Self;
223    fn into_element(self) -> Self::Element {
224        self
225    }
226}
227
228impl Element for LapalomaChartElement {
229    type RequestLayoutState = ();
230    type PrepaintState = ();
231
232    fn id(&self) -> Option<ElementId> {
233        None
234    }
235
236    fn source_location(&self) -> Option<&'static panic::Location<'static>> {
237        None
238    }
239
240    fn request_layout(
241        &mut self,
242        _id: Option<&GlobalElementId>,
243        _inspector_id: Option<&InspectorElementId>,
244        window: &mut Window,
245        cx: &mut App,
246    ) -> (LayoutId, Self::RequestLayoutState) {
247        let mut style = Style::default();
248        style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
249        style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
250        let id = window.request_layout(style, [], cx);
251        (id, ())
252    }
253
254    fn prepaint(
255        &mut self,
256        _id: Option<&GlobalElementId>,
257        _inspector_id: Option<&InspectorElementId>,
258        _bounds: Bounds<Pixels>,
259        _request_layout: &mut Self::RequestLayoutState,
260        _window: &mut Window,
261        _cx: &mut App,
262    ) -> Self::PrepaintState {
263    }
264
265    fn paint(
266        &mut self,
267        _id: Option<&GlobalElementId>,
268        _inspector_id: Option<&InspectorElementId>,
269        bounds: Bounds<Pixels>,
270        _request_layout: &mut Self::RequestLayoutState,
271        _prepaint: &mut Self::PrepaintState,
272        window: &mut Window,
273        _cx: &mut App,
274    ) {
275        let ox: f32 = bounds.origin.x.into();
276        let oy: f32 = bounds.origin.y.into();
277        let w: f32 = bounds.size.width.into();
278        let h: f32 = bounds.size.height.into();
279        let outer = Rect::new(ox, oy, w, h);
280        let plot = self.plot_rect(outer);
281
282        let cs = CoordinateSystem::new(self.viewport, plot);
283        let mut canvas = WindowCanvas::new(window);
284
285        if let Some(bg) = self.background {
286            canvas.fill_rect(outer, bg);
287        }
288
289        self.paint_axes(&mut canvas, &cs);
290
291        // Una pasada de paint por serie. Re-usamos el mismo scratch
292        // entre series — cada `LineSeries::paint` hace `clear()`.
293        for item in &self.series {
294            let series = LineSeries::new(&item.data, item.stroke);
295            let mut ctx = PaintCtx {
296                cs,
297                mode: RenderMode::UiRich,
298                scratch: &mut self.scratch,
299            };
300            series.paint(&mut ctx, &mut canvas);
301        }
302    }
303}
304
305/// Helper de una sola línea. Para multi-series usar
306/// `LapalomaChartElement::new(viewport).add_series(...).add_series(...)`.
307pub fn lapaloma_chart(
308    data: DataBuffer,
309    viewport: ChartViewport,
310    stroke: StrokeStyle,
311) -> LapalomaChartElement {
312    LapalomaChartElement::new(viewport).add_series(data, stroke)
313}