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
154 changes: 154 additions & 0 deletions examples/transform.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//! Example demonstrating coordinate transforms.
//!
//! The plot axes stay linear. Each series or reference line can choose how its
//! own x/y values are converted before drawing. `Transform::axes()` uses
//! normalized plot positions, so `0.4` means 40% across the plot area.

use std::f64::consts::{E, TAU};

use iced::Element;
use iced_plot::{
Color, HLine, LineStyle, MarkerStyle, PlotUiMessage, PlotWidget, PlotWidgetBuilder,
PositionTransform, Series, Transform, VLine,
};

fn main() -> iced::Result {
iced::application(new, update, view)
.font(include_bytes!("fonts/FiraCodeNerdFont-Regular.ttf"))
.default_font(iced::Font::with_name("FiraCode Nerd Font"))
.run()
}

fn update(widget: &mut PlotWidget, message: PlotUiMessage) {
widget.update(message);
}

fn view(widget: &PlotWidget) -> Element<'_, PlotUiMessage> {
widget.view()
}

fn sample(count: usize, mut f: impl FnMut(f64) -> [f64; 2]) -> Vec<[f64; 2]> {
(0..count)
.map(|i| {
let t = i as f64 / (count.saturating_sub(1).max(1)) as f64;
f(t)
})
.collect()
}

fn new() -> PlotWidget {
let identity = Series::line_only(
sample(160, |t| {
let x = t * 2.0;
[x, (TAU * t).sin() * 0.55]
}),
LineStyle::solid().with_pixel_width(2.0),
)
.with_transform(PositionTransform::new(Some(Transform::identity()), None))
.with_label("identity data")
.with_color(Color::from_rgb(0.2, 0.6, 1.0));

let affine_y = Series::line_only(
sample(160, |t| {
let x = t * 2.0;
let normalized = ((TAU * t).cos() * 0.5 + 0.5).clamp(0.0, 1.0);
[x, normalized]
}),
LineStyle::dashed(8.0).with_pixel_width(2.0),
)
.with_transform_y(Transform::affine(2.0, -1.0))
.with_label("y affine: y * 2 - 1")
.with_color(Color::from_rgb(0.9, 0.45, 0.15));

let log_x = Series::line_only(
sample(160, |t| {
let x_plot = t * 2.0;
[10.0_f64.powf(x_plot), 1.05 + (TAU * t).sin() * 0.25]
}),
LineStyle::solid().with_pixel_width(2.0),
)
.with_transform_x(Transform::log(10.0))
.with_label("x log10(raw)")
.with_color(Color::from_rgb(0.35, 0.85, 0.4));

let exp_y = Series::line_only(
sample(160, |t| {
let x = t * 2.0;
let y_after_transform = 1.25 + x * 0.45;
[x, y_after_transform.ln()]
}),
LineStyle::dotted(5.0).with_pixel_width(2.5),
)
.with_transform_y(Transform::exp(E))
.with_label("y exp(raw)")
.with_color(Color::from_rgb(0.85, 0.2, 0.55));

let composed_x = Series::line_only(
sample(160, |t| [t, 2.45 + (TAU * t).cos() * 0.22]),
LineStyle::dashed(5.0).with_pixel_width(2.0),
)
.with_transform_x(Transform::affine(99.0, 1.0).then(Transform::log(10.0)))
.with_label("x (raw * 99 + 1) then log10")
.with_color(Color::from_rgb(0.65, 0.45, 0.95));

let axes_line = Series::line_only(
vec![[0.05, 0.88], [0.95, 0.88]],
LineStyle::solid().with_pixel_width(3.0),
)
.with_axes_transform()
.with_label("axes line at 88%")
.with_color(Color::from_rgb(0.0, 0.9, 0.95));

let mixed_axes_x = Series::line_only(
vec![[0.5, -1.25], [0.5, 3.15]],
LineStyle::dotted(3.0).with_pixel_width(3.0),
)
.with_transform_x(Transform::axes())
.with_label("x axes=50%, y data")
.with_color(Color::from_rgb(0.95, 0.8, 0.15));

let axes_marker = Series::new(
vec![[0.92, 0.14]],
MarkerStyle::star(14.0),
LineStyle::solid(),
)
.with_transform(PositionTransform::new(
Some(Transform::axes()),
Some(Transform::axes()),
))
.with_label("right-lower marker")
.with_color(Color::from_rgb(1.0, 0.2, 0.2));

let center_vline = VLine::new(0.25)
.with_axes_transform()
.with_label("vline x=25% axes")
.with_color(Color::from_rgb(0.8, 0.8, 0.8))
.with_width(1.5)
.with_style(LineStyle::dashed(4.0));

let center_hline = HLine::new(0.5)
.with_axes_transform()
.with_label("hline y=50% axes")
.with_color(Color::from_rgb(0.8, 0.8, 0.8))
.with_width(1.5)
.with_style(LineStyle::dashed(4.0));

PlotWidgetBuilder::new()
.with_x_label("plot x after transform")
.with_y_label("plot y after transform")
.with_x_lim(-0.2, 2.2)
.with_y_lim(-1.4, 3.3)
.add_series(identity)
.add_series(affine_y)
.add_series(log_x)
.add_series(exp_y)
.add_series(composed_x)
.add_series(axes_line)
.add_series(mixed_axes_x)
.add_series(axes_marker)
.add_vline(center_vline)
.add_hline(center_hline)
.with_cursor_overlay(true)
.build()
.unwrap()
}
34 changes: 3 additions & 31 deletions src/axis_scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,47 +17,19 @@ pub enum AxisScale {
impl AxisScale {
/// Transform raw data value into plot-space value.
pub(crate) fn data_to_plot(self, value: f64) -> Option<f64> {
match self {
Self::Linear => value.is_finite().then_some(value),
Self::Log { base } => (value.is_finite() && value > 0.0)
.then(|| value.log(base))
.filter(|v| v.is_finite()),
}
crate::transform::data_value_to_plot(value, self, None)
}

/// Transform plot-space value into raw data value.
pub(crate) fn plot_to_data(self, value: f64) -> Option<f64> {
match self {
Self::Linear => value.is_finite().then_some(value),
Self::Log { base } => {
if !value.is_finite() {
return None;
}
let out = base.powf(value);
(out.is_finite() && out > 0.0).then_some(out)
}
}
crate::transform::plot_value_to_data(value, self)
}
}

pub(crate) fn data_point_to_plot(
point: [f64; 2],
x_scale: AxisScale,
y_scale: AxisScale,
) -> Option<[f64; 2]> {
Some([
x_scale.data_to_plot(point[0])?,
y_scale.data_to_plot(point[1])?,
])
}

pub(crate) fn plot_point_to_data(
point: [f64; 2],
x_scale: AxisScale,
y_scale: AxisScale,
) -> Option<[f64; 2]> {
Some([
x_scale.plot_to_data(point[0])?,
y_scale.plot_to_data(point[1])?,
])
crate::transform::plot_point_to_data(point, x_scale, y_scale)
}
18 changes: 18 additions & 0 deletions src/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,24 @@ impl Camera {
self.position - self.render_offset
}

pub(crate) fn x_range(&self) -> [f64; 2] {
[
self.position.x - self.half_extents.x,
self.position.x + self.half_extents.x,
]
}

pub(crate) fn y_range(&self) -> [f64; 2] {
[
self.position.y - self.half_extents.y,
self.position.y + self.half_extents.y,
]
}

pub(crate) fn axis_ranges(&self) -> ([f64; 2], [f64; 2]) {
(self.x_range(), self.y_range())
}

/// Convert screen coordinates to render coordinates (without offset)
pub fn screen_to_render(&self, screen_pos: DVec2, screen_size: DVec2) -> DVec2 {
let ndc_x = (screen_pos.x / screen_size.x) * 2.0 - 1.0;
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub(crate) mod reference_lines;
pub(crate) mod series;
pub(crate) mod style;
pub(crate) mod ticks;
pub(crate) mod transform;

// Iced re-exports.
pub use iced::Color;
Expand All @@ -63,3 +64,4 @@ pub use ticks::{
Tick, TickFormatter, TickProducer, default_formatter, default_tick_producer, log_formatter,
log_tick_producer,
};
pub use transform::{CoordinateSystem, PositionTransform, Transform};
49 changes: 37 additions & 12 deletions src/plot_renderer/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
plot_state::PlotState,
plot_widget::{world_to_screen_position_x, world_to_screen_position_y},
point::{MARKER_SIZE_WORLD, MarkerType},
transform::{data_point_to_plot_with_transform, data_value_to_plot_with_axis_range},
};
use iced::{
Color, Rectangle,
Expand Down Expand Up @@ -177,13 +178,17 @@ fn draw_lines(frame: &mut Frame, state: &PlotState, bounds: Rectangle) {

fn draw_reference_lines(frame: &mut Frame, state: &PlotState, bounds: Rectangle) {
for vline in state.vlines.iter() {
let Some(x) = world_to_screen_position_x(
state.x_axis_scale.data_to_plot(vline.x).unwrap_or_default(),
&state.camera,
&bounds,
let Some(vx_plot) = data_value_to_plot_with_axis_range(
vline.x,
state.x_axis_scale,
vline.transform.as_ref(),
Some(state.camera.x_range()),
) else {
continue;
};
let Some(x) = world_to_screen_position_x(vx_plot, &state.camera, &bounds) else {
continue;
};
draw_styled_line_segment(
frame,
iced::Point::new(x, 0.0),
Expand All @@ -200,13 +205,17 @@ fn draw_reference_lines(frame: &mut Frame, state: &PlotState, bounds: Rectangle)
}

for hline in state.hlines.iter() {
let Some(y) = world_to_screen_position_y(
state.y_axis_scale.data_to_plot(hline.y).unwrap_or_default(),
&state.camera,
&bounds,
let Some(hy_plot) = data_value_to_plot_with_axis_range(
hline.y,
state.y_axis_scale,
hline.transform.as_ref(),
Some(state.camera.y_range()),
) else {
continue;
};
let Some(y) = world_to_screen_position_y(hy_plot, &state.camera, &bounds) else {
continue;
};
draw_styled_line_segment(
frame,
iced::Point::new(0.0, y),
Expand Down Expand Up @@ -280,10 +289,26 @@ fn draw_highlights(frame: &mut Frame, state: &PlotState, bounds: Rectangle) {
if marker_style.marker_type == MarkerType::Square
&& let Size::World(size) = marker_style.size
{
let top_left =
world_to_canvas_point([highlight.x, highlight.y + size], &state.camera, &bounds);
let bottom_right =
world_to_canvas_point([highlight.x + size, highlight.y], &state.camera, &bounds);
let Some(top_left_plot) = data_point_to_plot_with_transform(
[highlight.x, highlight.y + size],
state.x_axis_scale,
state.y_axis_scale,
&highlight.transform,
Some(state.camera.axis_ranges()),
) else {
continue;
};
let Some(bottom_right_plot) = data_point_to_plot_with_transform(
[highlight.x + size, highlight.y],
state.x_axis_scale,
state.y_axis_scale,
&highlight.transform,
Some(state.camera.axis_ranges()),
) else {
continue;
};
let top_left = world_to_canvas_point(top_left_plot, &state.camera, &bounds);
let bottom_right = world_to_canvas_point(bottom_right_plot, &state.camera, &bounds);
frame.fill_rectangle(
iced::Point::new(
top_left.x.min(bottom_right.x),
Expand Down
16 changes: 12 additions & 4 deletions src/plot_renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ mod shader;
pub(crate) use shader::{PlotRenderer, RenderParams};

use crate::{
axis_scale::data_point_to_plot, plot_state::PlotState, plot_widget::HighlightPoint,
series::Size,
plot_state::PlotState, plot_widget::HighlightPoint, series::Size,
transform::data_point_to_plot_with_transform,
};
use iced::Color;

Expand Down Expand Up @@ -72,10 +72,12 @@ fn highlight_marker_plot_position(
highlight: &HighlightPoint,
state: &PlotState,
) -> Option<[f64; 2]> {
data_point_to_plot(
data_point_to_plot_with_transform(
[highlight.x, highlight.y],
state.x_axis_scale,
state.y_axis_scale,
&highlight.transform,
Some(state.camera.axis_ranges()),
)
}

Expand All @@ -89,5 +91,11 @@ fn highlight_mask_plot_position(highlight: &HighlightPoint, state: &PlotState) -
world[1] += half;
}

data_point_to_plot(world, state.x_axis_scale, state.y_axis_scale)
data_point_to_plot_with_transform(
world,
state.x_axis_scale,
state.y_axis_scale,
&highlight.transform,
Some(state.camera.axis_ranges()),
)
}
Loading