iced_plot 0.5.0

A GPU-accelerated plotting widget for Iced.
Documentation
//! Demonstrates theme and style configuration.
use iced::{
    Border, Color, Element, Length, Theme,
    widget::{column, container, pick_list, row, text},
};
use iced_plot::{
    GridStyle, LineStyle, MarkerStyle, PlotStyle, PlotUiMessage, PlotWidget, PlotWidgetBuilder,
    Series, default_style,
};

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

#[derive(Debug, Clone)]
enum Message {
    Plot(PlotUiMessage),
    ThemeSelected(ThemeChoice),
    StyleSelected(StyleChoice),
}

struct App {
    widget: PlotWidget,
    theme_choice: ThemeChoice,
    style_choice: StyleChoice,
}

impl App {
    fn new() -> Self {
        let theme_choice = ThemeChoice::TokyoNightStorm;
        let style_choice = StyleChoice::Default;

        Self {
            widget: plot_widget(style_choice),
            theme_choice,
            style_choice,
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::Plot(message) => self.widget.update(message),
            Message::ThemeSelected(theme_choice) => {
                self.theme_choice = theme_choice;
            }
            Message::StyleSelected(style_choice) => {
                self.style_choice = style_choice;
                self.widget.set_style(style_choice.style_fn());
            }
        }
    }

    fn view(&self) -> Element<'_, Message> {
        let toolbar = container(
            column![
                text("Application theme and plot style can be configured independently.")
                    .size(16.0),
                row![
                    control(
                        "App theme",
                        ThemeChoice::ALL,
                        self.theme_choice,
                        Message::ThemeSelected
                    ),
                    control(
                        "Plot style",
                        StyleChoice::ALL,
                        self.style_choice,
                        Message::StyleSelected
                    ),
                ]
                .spacing(16.0),
            ]
            .spacing(10.0),
        )
        .padding(12.0)
        .width(Length::Fill);

        column![toolbar, self.widget.view().map(Message::Plot)]
            .spacing(12.0)
            .padding(12.0)
            .into()
    }

    fn theme(&self) -> Theme {
        self.theme_choice.theme()
    }
}

fn control<'a, T: Copy + Eq + std::fmt::Display + 'a>(
    label: &'a str,
    options: &'a [T],
    selected: T,
    on_selected: impl Fn(T) -> Message + 'a,
) -> Element<'a, Message> {
    column![
        text(label).size(14.0),
        pick_list(options, Some(selected), on_selected).width(Length::Fixed(220.0)),
    ]
    .spacing(6.0)
    .into()
}

fn plot_widget(style_choice: StyleChoice) -> PlotWidget {
    let line = Series::line_only(
        (0..320)
            .map(|i| {
                let x = i as f64 * 0.04;
                [x, (x * 0.85).sin() + 0.2 * (x * 0.2).cos()]
            })
            .collect(),
        LineStyle::solid(),
    )
    .with_label("signal")
    .with_color(Color::from_rgb(0.2, 0.72, 0.98));

    let markers = Series::markers_only(
        (0..70)
            .map(|i| {
                let x = i as f64 * 0.18;
                [x, 0.65 * (x * 0.6).cos() - 0.1]
            })
            .collect(),
        MarkerStyle::square(5.5),
    )
    .with_label("samples")
    .with_color(Color::from_rgb(1.0, 0.55, 0.3));

    PlotWidgetBuilder::new()
        .with_x_label("X label")
        .with_cursor_overlay(true)
        .with_crosshairs(true)
        .with_style(style_choice.style_fn())
        .add_series(line)
        .add_series(markers)
        .build()
        .unwrap()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemeChoice {
    Light,
    Dark,
    TokyoNightStorm,
    KanagawaDragon,
    SolarizedDark,
    Nord,
}

impl ThemeChoice {
    const ALL: &'static [Self] = &[
        Self::Light,
        Self::Dark,
        Self::TokyoNightStorm,
        Self::KanagawaDragon,
        Self::SolarizedDark,
        Self::Nord,
    ];

    fn theme(self) -> Theme {
        match self {
            Self::Light => Theme::Light,
            Self::Dark => Theme::Dark,
            Self::TokyoNightStorm => Theme::TokyoNightStorm,
            Self::KanagawaDragon => Theme::KanagawaDragon,
            Self::SolarizedDark => Theme::SolarizedDark,
            Self::Nord => Theme::Nord,
        }
    }
}

impl std::fmt::Display for ThemeChoice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Light => "Light",
            Self::Dark => "Dark",
            Self::TokyoNightStorm => "Tokyo Night Storm",
            Self::KanagawaDragon => "Kanagawa Dragon",
            Self::SolarizedDark => "Solarized Dark",
            Self::Nord => "Nord",
        })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StyleChoice {
    Default,
    Custom1,
    Custom2,
    Custom3,
}

impl StyleChoice {
    const ALL: &'static [Self] = &[Self::Default, Self::Custom1, Self::Custom2, Self::Custom3];

    fn style_fn(self) -> fn(&Theme) -> PlotStyle {
        match self {
            Self::Default => default_style,
            Self::Custom1 => style1,
            Self::Custom2 => style2,
            Self::Custom3 => style3,
        }
    }
}

impl std::fmt::Display for StyleChoice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Default => "Default",
            Self::Custom1 => "Custom 1",
            Self::Custom2 => "Custom 2",
            Self::Custom3 => "Custom 3",
        })
    }
}

fn style1(theme: &Theme) -> PlotStyle {
    let palette = theme.extended_palette();
    let base = default_style(theme);
    let controls_panel = styled_panel(
        base.controls_panel,
        with_alpha(palette.background.base.color, 0.94),
        with_alpha(palette.primary.weak.color, 0.7),
        8.0,
        1.0,
    );

    PlotStyle {
        frame: base.frame.background(palette.background.base.color),
        plot_area: styled_panel(
            base.plot_area,
            with_alpha(palette.background.weak.color, 0.5),
            with_alpha(palette.background.strong.color, 0.65),
            8.0,
            1.0,
        ),
        legend: styled_panel(
            base.legend,
            with_alpha(palette.background.base.color, 0.95),
            with_alpha(palette.secondary.strong.color, 0.55),
            8.0,
            1.0,
        ),
        controls_panel,
        cursor_overlay: controls_panel,
        tooltip: styled_panel(
            base.tooltip,
            with_alpha(palette.background.base.color, 0.9),
            with_alpha(palette.primary.base.color, 0.5),
            6.0,
            1.0,
        ),
        grid: grid_style(
            with_alpha(palette.secondary.strong.color, 0.7),
            with_alpha(palette.secondary.base.color, 0.5),
            with_alpha(palette.secondary.weak.color, 0.2),
        ),
        tick_label_color: palette.secondary.strong.color,
        axis_label_color: palette.primary.strong.color,
    }
}

fn style2(theme: &Theme) -> PlotStyle {
    let palette = theme.extended_palette();
    let base = default_style(theme);

    PlotStyle {
        frame: base
            .frame
            .background(with_alpha(palette.background.weakest.color, 0.95)),
        plot_area: styled_panel(
            base.plot_area,
            with_alpha(palette.background.base.color, 0.88),
            palette.primary.strong.color,
            10.0,
            2.0,
        ),
        legend: restyled_border(base.legend, palette.primary.strong.color, 10.0, 1.0),
        controls_panel: restyled_border(
            base.controls_panel,
            palette.success.strong.color,
            10.0,
            1.0,
        ),
        cursor_overlay: restyled_border(
            base.cursor_overlay,
            palette.secondary.strong.color,
            10.0,
            1.0,
        ),
        tooltip: restyled_border(base.tooltip, palette.warning.base.color, 8.0, 1.0),
        grid: grid_style(
            with_alpha(palette.primary.strong.color, 0.8),
            with_alpha(palette.success.base.color, 0.6),
            with_alpha(palette.secondary.base.color, 0.4),
        ),
        tick_label_color: palette.success.strong.color,
        axis_label_color: palette.primary.strong.color,
    }
}

fn style3(theme: &Theme) -> PlotStyle {
    let palette = theme.extended_palette();
    let base = default_style(theme);

    PlotStyle {
        frame: base
            .frame
            .background(with_alpha(palette.background.base.color, 0.98)),
        plot_area: styled_panel(
            base.plot_area,
            with_alpha(palette.primary.base.color, 0.11),
            palette.primary.strong.color,
            6.0,
            1.0,
        ),
        legend: styled_panel(
            base.legend,
            with_alpha(palette.secondary.base.color, 0.16),
            palette.secondary.strong.color,
            6.0,
            1.0,
        ),
        controls_panel: styled_panel(
            base.controls_panel,
            with_alpha(palette.success.base.color, 0.14),
            palette.success.strong.color,
            6.0,
            1.0,
        ),
        cursor_overlay: styled_panel(
            base.cursor_overlay,
            with_alpha(palette.warning.base.color, 0.18),
            palette.warning.strong.color,
            6.0,
            1.0,
        ),
        tooltip: styled_panel(
            base.tooltip,
            with_alpha(palette.danger.base.color, 0.18),
            palette.danger.strong.color,
            6.0,
            1.0,
        ),
        grid: grid_style(
            with_alpha(palette.primary.strong.color, 0.42),
            with_alpha(palette.warning.strong.color, 0.24),
            with_alpha(palette.danger.base.color, 0.20),
        ),
        tick_label_color: palette.warning.strong.color,
        axis_label_color: palette.danger.strong.color,
    }
}

fn styled_panel(
    style: iced::widget::container::Style,
    background: Color,
    border_color: Color,
    radius: f32,
    width: f32,
) -> iced::widget::container::Style {
    let style = style.background(background);
    restyled_border(style, border_color, radius, width)
}

fn restyled_border(
    style: iced::widget::container::Style,
    color: Color,
    radius: f32,
    width: f32,
) -> iced::widget::container::Style {
    style.border(Border {
        width,
        radius: radius.into(),
        color,
    })
}

fn grid_style(major: Color, minor: Color, sub_minor: Color) -> GridStyle {
    GridStyle {
        major,
        minor,
        sub_minor,
    }
}

fn with_alpha(color: Color, alpha: f32) -> Color {
    Color { a: alpha, ..color }
}