Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 382 additions & 3 deletions src/component/axis.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,390 @@
//! Chart axis component for rendering axis lines, ticks, and labels

use makepad_widgets::*;
use crate::scale::{Scale, Tick};
use crate::core::TickOptions;

/// Chart axis component for rendering axis lines and labels
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
pub struct ChartAxis {
// Placeholder - to be implemented
/// Axis orientation
orientation: AxisOrientation,
/// Axis position
position: AxisPosition,
/// Tick options
tick_options: TickOptions,
/// Label formatter
label_formatter: Option<Box<dyn Fn(f64) -> String + Send + Sync>>,
/// Axis line color
line_color: Vec4,
/// Tick color
tick_color: Vec4,
/// Label color
label_color: Vec4,
/// Line width
line_width: f64,
/// Tick length
tick_length: f64,
/// Label font size
label_font_size: f64,
/// Show axis line
show_line: bool,
/// Show ticks
show_ticks: bool,
/// Show labels
show_labels: bool,
/// Padding between axis and labels
label_padding: f64,
}

#[derive(Clone, Debug, Default, PartialEq)]
pub enum AxisOrientation {
#[default]
Horizontal, // X axis
Vertical, // Y axis
}

#[derive(Clone, Debug, Default, PartialEq)]
pub enum AxisPosition {
#[default]
Bottom, // For horizontal axis
Top, // For horizontal axis
Left, // For vertical axis
Right, // For vertical axis
}

impl Default for ChartAxis {
fn default() -> Self {
Self::new()
}
}

impl ChartAxis {
/// Create a new chart axis
pub fn new() -> Self {
Self {
orientation: AxisOrientation::Horizontal,
position: AxisPosition::Bottom,
tick_options: TickOptions::default(),
label_formatter: None,
line_color: vec4(0.7, 0.7, 0.7, 1.0),
tick_color: vec4(0.7, 0.7, 0.7, 1.0),
label_color: vec4(0.3, 0.3, 0.3, 1.0),
line_width: 1.0,
tick_length: 5.0,
label_font_size: 10.0,
show_line: true,
show_ticks: true,
show_labels: true,
label_padding: 5.0,
}
}

/// Set axis orientation
pub fn with_orientation(mut self, orientation: AxisOrientation) -> Self {
self.orientation = orientation;
self
}

/// Set axis position
pub fn with_position(mut self, position: AxisPosition) -> Self {
self.position = position;
self
}

/// Set tick options
pub fn with_tick_options(mut self, options: TickOptions) -> Self {
self.tick_options = options;
self
}

/// Set custom label formatter
pub fn with_label_formatter<F: Fn(f64) -> String + Send + Sync + 'static>(mut self, formatter: F) -> Self {
self.label_formatter = Some(Box::new(formatter));
self
}

/// Set line color
pub fn with_line_color(mut self, color: Vec4) -> Self {
self.line_color = color;
self
}

/// Set label color
pub fn with_label_color(mut self, color: Vec4) -> Self {
self.label_color = color;
self
}

/// Set label font size
pub fn with_label_font_size(mut self, size: f64) -> Self {
self.label_font_size = size;
self
}

/// Set whether to show the axis line
pub fn show_line(mut self, show: bool) -> Self {
self.show_line = show;
self
}

/// Set whether to show ticks
pub fn show_ticks(mut self, show: bool) -> Self {
self.show_ticks = show;
self
}

/// Set whether to show labels
pub fn show_labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}

/// Format a tick value using the custom formatter or default
fn format_value(&self, value: f64) -> String {
if let Some(ref formatter) = self.label_formatter {
formatter(value)
} else {
format_number_default(value)
}
}

/// Render axis labels for given ticks
pub fn render_labels(
&self,
cx: &mut Cx2d,
ticks: &[Tick],
chart_area: Rect,
scale: &dyn Scale,
) {
if !self.show_labels || ticks.is_empty() {
return;
}

let text_style = TextStyle {
font_size: self.label_font_size,
..Default::default()
};

match self.orientation {
AxisOrientation::Horizontal => {
self.render_horizontal_labels(cx, ticks, chart_area, scale, text_style);
}
AxisOrientation::Vertical => {
self.render_vertical_labels(cx, ticks, chart_area, scale, text_style);
}
}
}

fn render_horizontal_labels(
&self,
cx: &mut Cx2d,
ticks: &[Tick],
chart_area: Rect,
scale: &dyn Scale,
text_style: TextStyle,
) {
let y = match self.position {
AxisPosition::Bottom => chart_area.bottom + self.tick_length + self.label_padding,
AxisPosition::Top => chart_area.top - self.tick_length - self.label_padding - self.label_font_size,
_ => chart_area.bottom + self.tick_length + self.label_padding,
};

for tick in ticks {
let x = scale.get_pixel_for_value(tick.value);

// Center the label on the tick position
let label_x = x - 25.0; // Approximate half width for centering

cx.draw_text(
&tick.label,
Rect {
pos: dvec2(label_x, y),
size: dvec2(50.0, self.label_font_size + 2.0),
},
text_style.clone(),
self.label_color,
);
}
}

fn render_vertical_labels(
&self,
cx: &mut Cx2d,
ticks: &[Tick],
chart_area: Rect,
scale: &dyn Scale,
text_style: TextStyle,
) {
for tick in ticks {
let y = scale.get_pixel_for_value(tick.value);

// Calculate x position based on axis position
let (x, alignment) = match self.position {
AxisPosition::Left => {
let x = chart_area.left - self.tick_length - self.label_padding - 40.0;
(x, LabelAlignment::Right)
}
AxisPosition::Right => {
let x = chart_area.right + self.tick_length + self.label_padding;
(x, LabelAlignment::Left)
}
_ => {
let x = chart_area.left - self.tick_length - self.label_padding - 40.0;
(x, LabelAlignment::Right)
}
};

// Center vertically on the tick position
let label_y = y - self.label_font_size / 2.0;

cx.draw_text(
&tick.label,
Rect {
pos: dvec2(x, label_y),
size: dvec2(40.0, self.label_font_size + 2.0),
},
text_style.clone(),
self.label_color,
);
}
}

/// Render axis line
pub fn render_line(&self, cx: &mut Cx2d, chart_area: Rect) {
if !self.show_line {
return;
}

// The line rendering would be done by the grid element
// This is a placeholder for future line-specific rendering
let _ = (cx, chart_area);
}
}

/// Label alignment for text positioning
#[derive(Clone, Debug, Default)]
pub enum LabelAlignment {
#[default]
Center,
Left,
Right,
}

/// Format a number with reasonable precision
fn format_number_default(value: f64) -> String {
if value == 0.0 {
return "0".to_string();
}

let abs_value = value.abs();

if abs_value >= 1_000_000.0 {
format!("{:.1}M", value / 1_000_000.0)
} else if abs_value >= 1_000.0 {
format!("{:.1}K", value / 1_000.0)
} else if abs_value >= 1.0 {
// Check if it's a whole number
if value.fract() == 0.0 {
format!("{}", value as i64)
} else {
format!("{:.1}", value)
}
} else if abs_value >= 0.01 {
format!("{:.2}", value)
} else {
format!("{:.3}", value)
}
}

/// Axis renderer for drawing complete axes with labels
pub struct AxisRenderer {
/// X axis configuration
pub x_axis: ChartAxis,
/// Y axis configuration
pub y_axis: ChartAxis,
}

impl AxisRenderer {
/// Create a new axis renderer with default settings
pub fn new() -> Self {
Self {}
Self {
x_axis: ChartAxis::new()
.with_orientation(AxisOrientation::Horizontal)
.with_position(AxisPosition::Bottom),
y_axis: ChartAxis::new()
.with_orientation(AxisOrientation::Vertical)
.with_position(AxisPosition::Left),
}
}

/// Render both axes
pub fn render(
&self,
cx: &mut Cx2d,
x_ticks: &[Tick],
y_ticks: &[Tick],
chart_area: Rect,
x_scale: &dyn Scale,
y_scale: &dyn Scale,
) {
self.x_axis.render_labels(cx, x_ticks, chart_area, x_scale);
self.y_axis.render_labels(cx, y_ticks, chart_area, y_scale);
}
}

impl Default for AxisRenderer {
fn default() -> Self {
Self::new()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_chart_axis_new() {
let axis = ChartAxis::new();
assert!(axis.show_line);
assert!(axis.show_ticks);
assert!(axis.show_labels);
}

#[test]
fn test_chart_axis_orientation() {
let axis = ChartAxis::new()
.with_orientation(AxisOrientation::Vertical);
assert_eq!(axis.orientation, AxisOrientation::Vertical);
}

#[test]
fn test_chart_axis_position() {
let axis = ChartAxis::new()
.with_position(AxisPosition::Left);
assert_eq!(axis.position, AxisPosition::Left);
}

#[test]
fn test_format_number_default() {
assert_eq!(format_number_default(0.0), "0");
assert_eq!(format_number_default(100.0), "100");
assert_eq!(format_number_default(1500.0), "1.5K");
assert_eq!(format_number_default(1_500_000.0), "1.5M");
assert_eq!(format_number_default(0.5), "0.50");
}

#[test]
fn test_axis_renderer_new() {
let renderer = AxisRenderer::new();
assert_eq!(renderer.x_axis.orientation, AxisOrientation::Horizontal);
assert_eq!(renderer.y_axis.orientation, AxisOrientation::Vertical);
}

#[test]
fn test_label_formatter() {
let axis = ChartAxis::new()
.with_label_formatter(|v| format!("${:.0}", v));
assert_eq!(axis.format_value(100.0), "$100");
}
}
Loading