feat(lapaloma-cartesian): multi-series en LapalomaChartElement
- Element ahora mantiene Vec<ChartSeriesItem> con DataBuffer + StrokeStyle + nombre opcional por serie. Builder add_series y add_series_named. - En paint(), una pasada por cada serie reusando el mismo scratch. N series = N paint_path (no N × por punto). Cumple P3 del ARCHITECTURE.md por serie. - `lapaloma_chart(data, vp, stroke)` queda como helper retrocompat para el caso una-serie. - Demo: 3 series simultáneas (sin, cos, mix) con colores nórdicos + leyenda textual en el header. 46 tests verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
|
||||
//! pipeline cartesian.
|
||||
//!
|
||||
//! Owns un `DataBuffer` y un `ChartViewport`. En `paint()` arma el
|
||||
//! `WindowCanvas` adapter de `lapaloma-render` y delega a una
|
||||
//! [`LineSeries`]. Dibuja además los ejes (línea base + ticks).
|
||||
//! Los labels llegan cuando `draw_text` esté implementado en el
|
||||
//! WindowCanvas.
|
||||
//! Mantiene un `Vec<ChartSeriesItem>` con `DataBuffer + StrokeStyle`
|
||||
//! por serie. En `paint()` arma el `WindowCanvas` adapter, pinta el
|
||||
//! background, los ejes con sus labels, y delega a una `LineSeries`
|
||||
//! por cada item. Cada serie emite un solo `paint_path` — N series
|
||||
//! = N draw calls totales (no N × por punto).
|
||||
//!
|
||||
//! Todas las series comparten viewport. Para Y axis dual (necesario
|
||||
//! cuando series tienen rangos muy distintos) la API se va a
|
||||
//! extender con `y_axis_id` por serie y un viewport segundario.
|
||||
|
||||
use std::panic;
|
||||
|
||||
@@ -15,50 +19,61 @@ use gpui::{
|
||||
};
|
||||
|
||||
use lapaloma_core::buffer::DataBuffer;
|
||||
use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle, WindowCanvas};
|
||||
|
||||
use lapaloma_core::scale::nice_step;
|
||||
use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle, WindowCanvas};
|
||||
|
||||
use crate::axis::{decimate_labels, format_tick, ticks_nice, AxisStyle};
|
||||
use crate::coord_system::CoordinateSystem;
|
||||
use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
|
||||
use crate::viewport::ChartViewport;
|
||||
|
||||
/// Aproximación del ancho de glifo monoespaciado en función del
|
||||
/// font size. Suficiente para alinear/decimar labels — no hace
|
||||
/// falta exactitud de subpixel acá.
|
||||
const MONO_GLYPH_RATIO: f32 = 0.55;
|
||||
|
||||
/// Cuántos ticks objetivo por eje. Wilkinson nice numbers ajusta
|
||||
/// al múltiplo más cercano del step ideal.
|
||||
const TARGET_TICKS_X: usize = 8;
|
||||
const TARGET_TICKS_Y: usize = 6;
|
||||
|
||||
/// Chart cartesiano de una sola serie. Para múltiples series va a
|
||||
/// venir un `LapalomaChart` que componga varios `Series` boxed.
|
||||
pub struct LapalomaChartElement {
|
||||
/// Aproximación del ancho de glifo monoespaciado en función del
|
||||
/// font size. Suficiente para alinear / decimar labels.
|
||||
const MONO_GLYPH_RATIO: f32 = 0.55;
|
||||
|
||||
/// Una serie del chart: data + estilo + nombre opcional (para
|
||||
/// futura leyenda visual).
|
||||
#[derive(Clone)]
|
||||
pub struct ChartSeriesItem {
|
||||
pub data: DataBuffer,
|
||||
pub viewport: ChartViewport,
|
||||
pub stroke: StrokeStyle,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
impl ChartSeriesItem {
|
||||
pub fn new(data: DataBuffer, stroke: StrokeStyle) -> Self {
|
||||
Self { data, stroke, name: None }
|
||||
}
|
||||
|
||||
pub fn named(data: DataBuffer, stroke: StrokeStyle, name: impl Into<String>) -> Self {
|
||||
Self { data, stroke, name: Some(name.into()) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LapalomaChartElement {
|
||||
pub series: Vec<ChartSeriesItem>,
|
||||
pub viewport: ChartViewport,
|
||||
pub background: Option<Color>,
|
||||
pub axis_color: Color,
|
||||
pub axis_style: AxisStyle,
|
||||
/// Margen para X axis (espacio reservado abajo).
|
||||
pub margin_bottom: f32,
|
||||
/// Margen para Y axis (espacio reservado a izquierda).
|
||||
pub margin_left: f32,
|
||||
pub margin_top: f32,
|
||||
pub margin_right: f32,
|
||||
/// Scratch buffer reusable entre frames.
|
||||
/// Scratch reusable entre series y entre frames.
|
||||
scratch: Vec<f32>,
|
||||
}
|
||||
|
||||
impl LapalomaChartElement {
|
||||
pub fn new(data: DataBuffer, viewport: ChartViewport, stroke: StrokeStyle) -> Self {
|
||||
/// Construye un chart vacío con un viewport. Las series se
|
||||
/// agregan con `add_series` / `add_series_named`.
|
||||
pub fn new(viewport: ChartViewport) -> Self {
|
||||
Self {
|
||||
data,
|
||||
series: Vec::new(),
|
||||
viewport,
|
||||
stroke,
|
||||
background: None,
|
||||
axis_color: Color::rgba(0.6, 0.6, 0.65, 0.8),
|
||||
axis_style: AxisStyle::default(),
|
||||
@@ -70,6 +85,21 @@ impl LapalomaChartElement {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_series(mut self, data: DataBuffer, stroke: StrokeStyle) -> Self {
|
||||
self.series.push(ChartSeriesItem::new(data, stroke));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_series_named(
|
||||
mut self,
|
||||
data: DataBuffer,
|
||||
stroke: StrokeStyle,
|
||||
name: impl Into<String>,
|
||||
) -> Self {
|
||||
self.series.push(ChartSeriesItem::named(data, stroke, name));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, color: Color) -> Self {
|
||||
self.background = Some(color);
|
||||
self
|
||||
@@ -88,8 +118,6 @@ impl LapalomaChartElement {
|
||||
self
|
||||
}
|
||||
|
||||
/// Calcula el rect del área de plot (datos), descontando los
|
||||
/// márgenes reservados para ejes.
|
||||
fn plot_rect(&self, bounds: Rect) -> Rect {
|
||||
Rect::new(
|
||||
bounds.x + self.margin_left,
|
||||
@@ -99,7 +127,6 @@ impl LapalomaChartElement {
|
||||
)
|
||||
}
|
||||
|
||||
/// Dibuja línea base + ticks + labels de ambos ejes.
|
||||
fn paint_axes(&self, canvas: &mut dyn Canvas, cs: &CoordinateSystem) {
|
||||
let plot = cs.plot;
|
||||
let style = self.axis_style;
|
||||
@@ -107,7 +134,6 @@ impl LapalomaChartElement {
|
||||
let tick_stroke = StrokeStyle::new(style.tick_width_px, self.axis_color);
|
||||
let tlen = style.tick_length_px;
|
||||
|
||||
// Líneas base.
|
||||
canvas.stroke_line(
|
||||
Point::new(plot.x, plot.bottom()),
|
||||
Point::new(plot.right(), plot.bottom()),
|
||||
@@ -119,10 +145,9 @@ impl LapalomaChartElement {
|
||||
axis_stroke,
|
||||
);
|
||||
|
||||
// === X axis ===
|
||||
// X axis ticks + labels.
|
||||
let x_ticks = ticks_nice(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
|
||||
let x_step = nice_step(self.viewport.x_min, self.viewport.x_max, TARGET_TICKS_X);
|
||||
|
||||
let mut x_pos: Vec<f32> = Vec::with_capacity(x_ticks.len());
|
||||
let mut x_lbl: Vec<String> = Vec::with_capacity(x_ticks.len());
|
||||
let mut x_widths: Vec<f32> = Vec::with_capacity(x_ticks.len());
|
||||
@@ -155,7 +180,7 @@ impl LapalomaChartElement {
|
||||
);
|
||||
}
|
||||
|
||||
// === Y axis ===
|
||||
// Y axis ticks + labels con decimación vertical.
|
||||
let y_ticks = ticks_nice(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
|
||||
let y_step = nice_step(self.viewport.y_min, self.viewport.y_max, TARGET_TICKS_Y);
|
||||
let y_label_pitch = style.label_size_px + style.label_min_spacing_px;
|
||||
@@ -171,9 +196,6 @@ impl LapalomaChartElement {
|
||||
Point::new(plot.x, py),
|
||||
tick_stroke,
|
||||
);
|
||||
|
||||
// Decimación vertical: si el tick anterior con label está
|
||||
// muy cerca, saltamos sólo el label (el tick se queda).
|
||||
let label_ok = match prev_py {
|
||||
None => true,
|
||||
Some(p) => (py - p).abs() >= y_label_pitch,
|
||||
@@ -266,22 +288,26 @@ impl Element for LapalomaChartElement {
|
||||
|
||||
self.paint_axes(&mut canvas, &cs);
|
||||
|
||||
let series = LineSeries::new(&self.data, self.stroke);
|
||||
self.scratch.clear();
|
||||
let mut ctx = PaintCtx {
|
||||
cs,
|
||||
mode: RenderMode::UiRich,
|
||||
scratch: &mut self.scratch,
|
||||
};
|
||||
series.paint(&mut ctx, &mut canvas);
|
||||
// Una pasada de paint por serie. Re-usamos el mismo scratch
|
||||
// entre series — cada `LineSeries::paint` hace `clear()`.
|
||||
for item in &self.series {
|
||||
let series = LineSeries::new(&item.data, item.stroke);
|
||||
let mut ctx = PaintCtx {
|
||||
cs,
|
||||
mode: RenderMode::UiRich,
|
||||
scratch: &mut self.scratch,
|
||||
};
|
||||
series.paint(&mut ctx, &mut canvas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper builder-style para uso ergonómico desde `Render::render`.
|
||||
/// Helper de una sola línea. Para multi-series usar
|
||||
/// `LapalomaChartElement::new(viewport).add_series(...).add_series(...)`.
|
||||
pub fn lapaloma_chart(
|
||||
data: DataBuffer,
|
||||
viewport: ChartViewport,
|
||||
stroke: StrokeStyle,
|
||||
) -> LapalomaChartElement {
|
||||
LapalomaChartElement::new(data, viewport, stroke)
|
||||
LapalomaChartElement::new(viewport).add_series(data, stroke)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user