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,60 +1,45 @@
|
|||||||
//! `lapaloma-demo` — demo visual mínimo de Lapaloma sobre yahweh.
|
//! `lapaloma-demo` — demo visual de Lapaloma sobre yahweh.
|
||||||
//!
|
//!
|
||||||
//! Levanta una ventana 900×560 con un único chart cartesiano
|
//! Ventana 900×560 con un chart cartesiano de **3 series**
|
||||||
//! pre-llenado con `sin(x · 0.04)` sobre 1024 muestras. Pan + zoom
|
//! simultáneas sobre 1024 muestras:
|
||||||
//! interactivos:
|
|
||||||
//!
|
//!
|
||||||
//! - **Click + drag**: pan en X/Y normalizado al tamaño de la
|
//! - `sin(x · 0.04)` — azul nórdico
|
||||||
//! ventana (vía `ChartViewport::pan_fraction`, sin requerir el
|
//! - `cos(x · 0.04)` — naranja
|
||||||
//! plot rect exacto).
|
//! - `0.5·sin(x · 0.02) + 0.5·cos(x · 0.08)` — verde
|
||||||
//! - **Mouse wheel**: zoom exponencial anchor-preserving en la
|
|
||||||
//! posición del cursor (sección 5.3 / 5.4 del ARCHITECTURE.md).
|
|
||||||
//! - **Doble-click**: reset al viewport inicial.
|
|
||||||
//!
|
//!
|
||||||
//! Cadena viva:
|
//! Interacción: click+drag = pan, wheel = zoom, doble-click = reset.
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! DataBuffer (lapaloma-core)
|
|
||||||
//! ↓ LTTB cuando densidad > 3× ancho del plot
|
|
||||||
//! CoordinateSystem (lapaloma-cartesian)
|
|
||||||
//! ↓ project_buffer dominio → pixel, zero-alloc
|
|
||||||
//! LineSeries (lapaloma-cartesian)
|
|
||||||
//! ↓ canvas.stroke_polyline (una sola draw call)
|
|
||||||
//! WindowCanvas (lapaloma-render::gpui_backend)
|
|
||||||
//! ↓ paint_path + paint_glyph
|
|
||||||
//! gpui::Window
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, px, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
|
div, prelude::*, px, ClickEvent, Context, IntoElement, MouseButton, MouseDownEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, Point, Render, ScrollDelta, ScrollWheelEvent, Window,
|
MouseMoveEvent, MouseUpEvent, Point, Render, ScrollDelta, ScrollWheelEvent, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use lapaloma_cartesian::{lapaloma_chart, ChartViewport};
|
use lapaloma_cartesian::{ChartViewport, LapalomaChartElement};
|
||||||
use lapaloma_core::buffer::DataBuffer;
|
use lapaloma_core::buffer::DataBuffer;
|
||||||
use lapaloma_render::{Color, StrokeStyle};
|
use lapaloma_render::{Color, StrokeStyle};
|
||||||
use yahweh_launcher::launch_app;
|
use yahweh_launcher::launch_app;
|
||||||
use yahweh_theme::Theme;
|
use yahweh_theme::Theme;
|
||||||
|
|
||||||
const N_SAMPLES: usize = 1024;
|
const N_SAMPLES: usize = 1024;
|
||||||
const FREQ: f32 = 0.04;
|
|
||||||
|
|
||||||
/// Sensibilidad exponencial del wheel zoom (sección 5.4 del doc).
|
|
||||||
const WHEEL_SENSITIVITY: f64 = 0.0015;
|
const WHEEL_SENSITIVITY: f64 = 0.0015;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
launch_app("Lapaloma — sin(x) demo (drag = pan, wheel = zoom)", (900., 560.), Demo::new);
|
launch_app(
|
||||||
|
"Lapaloma — multi-series (drag = pan, wheel = zoom, dbl-click = reset)",
|
||||||
|
(900., 560.),
|
||||||
|
Demo::new,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Demo {
|
struct Demo {
|
||||||
data: DataBuffer,
|
series_sin: DataBuffer,
|
||||||
|
series_cos: DataBuffer,
|
||||||
|
series_mix: DataBuffer,
|
||||||
viewport: ChartViewport,
|
viewport: ChartViewport,
|
||||||
initial_viewport: ChartViewport,
|
initial_viewport: ChartViewport,
|
||||||
drag: Option<DragAnchor>,
|
drag: Option<DragAnchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot del estado al inicio del drag. Mantener una copia del
|
|
||||||
/// viewport "al click" evita acumular errores frame a frame.
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct DragAnchor {
|
struct DragAnchor {
|
||||||
start_position: Point<gpui::Pixels>,
|
start_position: Point<gpui::Pixels>,
|
||||||
@@ -63,14 +48,20 @@ struct DragAnchor {
|
|||||||
|
|
||||||
impl Demo {
|
impl Demo {
|
||||||
fn new(_cx: &mut Context<Self>) -> Self {
|
fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
let mut data = DataBuffer::with_capacity(N_SAMPLES);
|
let mut sin = DataBuffer::with_capacity(N_SAMPLES);
|
||||||
|
let mut cos = DataBuffer::with_capacity(N_SAMPLES);
|
||||||
|
let mut mix = DataBuffer::with_capacity(N_SAMPLES);
|
||||||
for i in 0..N_SAMPLES {
|
for i in 0..N_SAMPLES {
|
||||||
let x = i as f32;
|
let x = i as f32;
|
||||||
data.push(x, (x * FREQ).sin());
|
sin.push(x, (x * 0.04).sin());
|
||||||
|
cos.push(x, (x * 0.04).cos());
|
||||||
|
mix.push(x, 0.5 * (x * 0.02).sin() + 0.5 * (x * 0.08).cos());
|
||||||
}
|
}
|
||||||
let viewport = ChartViewport::new(0.0, (N_SAMPLES - 1) as f64, -1.1, 1.1);
|
let viewport = ChartViewport::new(0.0, (N_SAMPLES - 1) as f64, -1.3, 1.3);
|
||||||
Self {
|
Self {
|
||||||
data,
|
series_sin: sin,
|
||||||
|
series_cos: cos,
|
||||||
|
series_mix: mix,
|
||||||
viewport,
|
viewport,
|
||||||
initial_viewport: viewport,
|
initial_viewport: viewport,
|
||||||
drag: None,
|
drag: None,
|
||||||
@@ -118,31 +109,20 @@ impl Demo {
|
|||||||
if w <= 0.0 || h <= 0.0 {
|
if w <= 0.0 || h <= 0.0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// dy normalizado a píxeles. ScrollDelta puede venir en
|
|
||||||
// líneas (mouse wheel discreto) o píxeles (trackpad).
|
|
||||||
let dy_px: f32 = match e.delta {
|
let dy_px: f32 = match e.delta {
|
||||||
ScrollDelta::Pixels(p) => p.y.into(),
|
ScrollDelta::Pixels(p) => p.y.into(),
|
||||||
ScrollDelta::Lines(p) => {
|
ScrollDelta::Lines(p) => p.y * 16.0,
|
||||||
let y: f32 = p.y.into();
|
|
||||||
y * 16.0 // line-height aproximada
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let factor = (-dy_px as f64 * WHEEL_SENSITIVITY).exp();
|
let factor = (-dy_px as f64 * WHEEL_SENSITIVITY).exp();
|
||||||
|
|
||||||
let sx: f32 = e.position.x.into();
|
let sx: f32 = e.position.x.into();
|
||||||
let sy: f32 = e.position.y.into();
|
let sy: f32 = e.position.y.into();
|
||||||
let ax = (sx / w).clamp(0.0, 1.0) as f64;
|
let ax = (sx / w).clamp(0.0, 1.0) as f64;
|
||||||
// Invertimos Y: dominio crece para arriba, pantalla crece para abajo.
|
|
||||||
let ay = (1.0 - sy / h).clamp(0.0, 1.0) as f64;
|
let ay = (1.0 - sy / h).clamp(0.0, 1.0) as f64;
|
||||||
|
|
||||||
self.viewport.zoom_uniform(factor, (ax, ay));
|
self.viewport.zoom_uniform(factor, (ax, ay));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_click(&mut self, e: &ClickEvent, _w: &mut Window, cx: &mut Context<Self>) {
|
fn on_click(&mut self, e: &ClickEvent, _w: &mut Window, cx: &mut Context<Self>) {
|
||||||
// Reset sólo si es doble (o triple) click de mouse. Click
|
|
||||||
// simple no hace nada; el drag se maneja por
|
|
||||||
// mouse_down/move/up. Clicks de teclado no aplican acá.
|
|
||||||
if let ClickEvent::Mouse(m) = e {
|
if let ClickEvent::Mouse(m) = e {
|
||||||
if m.up.click_count >= 2 {
|
if m.up.click_count >= 2 {
|
||||||
self.viewport = self.initial_viewport;
|
self.viewport = self.initial_viewport;
|
||||||
@@ -152,13 +132,34 @@ impl Demo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Color helper para usar el mismo hex tanto en `lapaloma_render`
|
||||||
|
/// como en el body de texto del header del demo.
|
||||||
|
const COLOR_SIN: u32 = 0x88c0d0; // azul nórdico
|
||||||
|
const COLOR_COS: u32 = 0xd08770; // naranja
|
||||||
|
const COLOR_MIX: u32 = 0xa3be8c; // verde
|
||||||
|
|
||||||
impl Render for Demo {
|
impl Render for Demo {
|
||||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let theme = Theme::global(cx).clone();
|
let theme = Theme::global(cx).clone();
|
||||||
|
|
||||||
let stroke = StrokeStyle::new(2.0, Color::from_hex(0x88c0d0));
|
|
||||||
let plot_bg = Color::rgba(0.10, 0.12, 0.16, 1.0);
|
let plot_bg = Color::rgba(0.10, 0.12, 0.16, 1.0);
|
||||||
let chart = lapaloma_chart(self.data.clone(), self.viewport, stroke).background(plot_bg);
|
let chart = LapalomaChartElement::new(self.viewport)
|
||||||
|
.background(plot_bg)
|
||||||
|
.add_series_named(
|
||||||
|
self.series_sin.clone(),
|
||||||
|
StrokeStyle::new(2.0, Color::from_hex(COLOR_SIN)),
|
||||||
|
"sin",
|
||||||
|
)
|
||||||
|
.add_series_named(
|
||||||
|
self.series_cos.clone(),
|
||||||
|
StrokeStyle::new(2.0, Color::from_hex(COLOR_COS)),
|
||||||
|
"cos",
|
||||||
|
)
|
||||||
|
.add_series_named(
|
||||||
|
self.series_mix.clone(),
|
||||||
|
StrokeStyle::new(2.0, Color::from_hex(COLOR_MIX)),
|
||||||
|
"mix",
|
||||||
|
);
|
||||||
|
|
||||||
let drag_active = self.drag.is_some();
|
let drag_active = self.drag.is_some();
|
||||||
|
|
||||||
@@ -174,18 +175,33 @@ impl Render for Demo {
|
|||||||
div()
|
div()
|
||||||
.text_color(theme.fg_text)
|
.text_color(theme.fg_text)
|
||||||
.text_size(px(18.))
|
.text_size(px(18.))
|
||||||
.child("Lapaloma — demo cartesian"),
|
.child("Lapaloma — demo cartesian multi-series"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.gap(px(14.))
|
||||||
|
.text_size(px(11.))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(gpui::rgb(COLOR_SIN))
|
||||||
|
.child("■ sin(x · 0.04)"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(gpui::rgb(COLOR_COS))
|
||||||
|
.child("■ cos(x · 0.04)"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(gpui::rgb(COLOR_MIX))
|
||||||
|
.child("■ ½·sin(x · 0.02) + ½·cos(x · 0.08)"),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(theme.fg_muted)
|
.text_color(theme.fg_muted)
|
||||||
.text_size(px(12.))
|
.child(if drag_active { "· dragging" } else { "" }),
|
||||||
.child(format!(
|
),
|
||||||
"{} muestras de sin(x · {}). drag = pan · wheel = zoom · double-click = reset · {}",
|
|
||||||
N_SAMPLES,
|
|
||||||
FREQ,
|
|
||||||
if drag_active { "dragging" } else { "idle" },
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
|
//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
|
||||||
//! pipeline cartesian.
|
//! pipeline cartesian.
|
||||||
//!
|
//!
|
||||||
//! Owns un `DataBuffer` y un `ChartViewport`. En `paint()` arma el
|
//! Mantiene un `Vec<ChartSeriesItem>` con `DataBuffer + StrokeStyle`
|
||||||
//! `WindowCanvas` adapter de `lapaloma-render` y delega a una
|
//! por serie. En `paint()` arma el `WindowCanvas` adapter, pinta el
|
||||||
//! [`LineSeries`]. Dibuja además los ejes (línea base + ticks).
|
//! background, los ejes con sus labels, y delega a una `LineSeries`
|
||||||
//! Los labels llegan cuando `draw_text` esté implementado en el
|
//! por cada item. Cada serie emite un solo `paint_path` — N series
|
||||||
//! WindowCanvas.
|
//! = 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;
|
use std::panic;
|
||||||
|
|
||||||
@@ -15,50 +19,61 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use lapaloma_core::buffer::DataBuffer;
|
use lapaloma_core::buffer::DataBuffer;
|
||||||
use lapaloma_render::{Canvas, Color, Point, Rect, StrokeStyle, WindowCanvas};
|
|
||||||
|
|
||||||
use lapaloma_core::scale::nice_step;
|
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::axis::{decimate_labels, format_tick, ticks_nice, AxisStyle};
|
||||||
use crate::coord_system::CoordinateSystem;
|
use crate::coord_system::CoordinateSystem;
|
||||||
use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
|
use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
|
||||||
use crate::viewport::ChartViewport;
|
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_X: usize = 8;
|
||||||
const TARGET_TICKS_Y: usize = 6;
|
const TARGET_TICKS_Y: usize = 6;
|
||||||
|
|
||||||
/// Chart cartesiano de una sola serie. Para múltiples series va a
|
/// Aproximación del ancho de glifo monoespaciado en función del
|
||||||
/// venir un `LapalomaChart` que componga varios `Series` boxed.
|
/// font size. Suficiente para alinear / decimar labels.
|
||||||
pub struct LapalomaChartElement {
|
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 data: DataBuffer,
|
||||||
pub viewport: ChartViewport,
|
|
||||||
pub stroke: StrokeStyle,
|
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 background: Option<Color>,
|
||||||
pub axis_color: Color,
|
pub axis_color: Color,
|
||||||
pub axis_style: AxisStyle,
|
pub axis_style: AxisStyle,
|
||||||
/// Margen para X axis (espacio reservado abajo).
|
|
||||||
pub margin_bottom: f32,
|
pub margin_bottom: f32,
|
||||||
/// Margen para Y axis (espacio reservado a izquierda).
|
|
||||||
pub margin_left: f32,
|
pub margin_left: f32,
|
||||||
pub margin_top: f32,
|
pub margin_top: f32,
|
||||||
pub margin_right: f32,
|
pub margin_right: f32,
|
||||||
/// Scratch buffer reusable entre frames.
|
/// Scratch reusable entre series y entre frames.
|
||||||
scratch: Vec<f32>,
|
scratch: Vec<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LapalomaChartElement {
|
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 {
|
Self {
|
||||||
data,
|
series: Vec::new(),
|
||||||
viewport,
|
viewport,
|
||||||
stroke,
|
|
||||||
background: None,
|
background: None,
|
||||||
axis_color: Color::rgba(0.6, 0.6, 0.65, 0.8),
|
axis_color: Color::rgba(0.6, 0.6, 0.65, 0.8),
|
||||||
axis_style: AxisStyle::default(),
|
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 {
|
pub fn background(mut self, color: Color) -> Self {
|
||||||
self.background = Some(color);
|
self.background = Some(color);
|
||||||
self
|
self
|
||||||
@@ -88,8 +118,6 @@ impl LapalomaChartElement {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calcula el rect del área de plot (datos), descontando los
|
|
||||||
/// márgenes reservados para ejes.
|
|
||||||
fn plot_rect(&self, bounds: Rect) -> Rect {
|
fn plot_rect(&self, bounds: Rect) -> Rect {
|
||||||
Rect::new(
|
Rect::new(
|
||||||
bounds.x + self.margin_left,
|
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) {
|
fn paint_axes(&self, canvas: &mut dyn Canvas, cs: &CoordinateSystem) {
|
||||||
let plot = cs.plot;
|
let plot = cs.plot;
|
||||||
let style = self.axis_style;
|
let style = self.axis_style;
|
||||||
@@ -107,7 +134,6 @@ impl LapalomaChartElement {
|
|||||||
let tick_stroke = StrokeStyle::new(style.tick_width_px, self.axis_color);
|
let tick_stroke = StrokeStyle::new(style.tick_width_px, self.axis_color);
|
||||||
let tlen = style.tick_length_px;
|
let tlen = style.tick_length_px;
|
||||||
|
|
||||||
// Líneas base.
|
|
||||||
canvas.stroke_line(
|
canvas.stroke_line(
|
||||||
Point::new(plot.x, plot.bottom()),
|
Point::new(plot.x, plot.bottom()),
|
||||||
Point::new(plot.right(), plot.bottom()),
|
Point::new(plot.right(), plot.bottom()),
|
||||||
@@ -119,10 +145,9 @@ impl LapalomaChartElement {
|
|||||||
axis_stroke,
|
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_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 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_pos: Vec<f32> = Vec::with_capacity(x_ticks.len());
|
||||||
let mut x_lbl: Vec<String> = 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());
|
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_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_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;
|
let y_label_pitch = style.label_size_px + style.label_min_spacing_px;
|
||||||
@@ -171,9 +196,6 @@ impl LapalomaChartElement {
|
|||||||
Point::new(plot.x, py),
|
Point::new(plot.x, py),
|
||||||
tick_stroke,
|
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 {
|
let label_ok = match prev_py {
|
||||||
None => true,
|
None => true,
|
||||||
Some(p) => (py - p).abs() >= y_label_pitch,
|
Some(p) => (py - p).abs() >= y_label_pitch,
|
||||||
@@ -266,8 +288,10 @@ impl Element for LapalomaChartElement {
|
|||||||
|
|
||||||
self.paint_axes(&mut canvas, &cs);
|
self.paint_axes(&mut canvas, &cs);
|
||||||
|
|
||||||
let series = LineSeries::new(&self.data, self.stroke);
|
// Una pasada de paint por serie. Re-usamos el mismo scratch
|
||||||
self.scratch.clear();
|
// entre series — cada `LineSeries::paint` hace `clear()`.
|
||||||
|
for item in &self.series {
|
||||||
|
let series = LineSeries::new(&item.data, item.stroke);
|
||||||
let mut ctx = PaintCtx {
|
let mut ctx = PaintCtx {
|
||||||
cs,
|
cs,
|
||||||
mode: RenderMode::UiRich,
|
mode: RenderMode::UiRich,
|
||||||
@@ -276,12 +300,14 @@ impl Element for LapalomaChartElement {
|
|||||||
series.paint(&mut ctx, &mut canvas);
|
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(
|
pub fn lapaloma_chart(
|
||||||
data: DataBuffer,
|
data: DataBuffer,
|
||||||
viewport: ChartViewport,
|
viewport: ChartViewport,
|
||||||
stroke: StrokeStyle,
|
stroke: StrokeStyle,
|
||||||
) -> LapalomaChartElement {
|
) -> LapalomaChartElement {
|
||||||
LapalomaChartElement::new(data, viewport, stroke)
|
LapalomaChartElement::new(viewport).add_series(data, stroke)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user