Repository: Joylei/plotters-iced Branch: master Commit: 3c5877de843a Files: 21 Total size: 66.7 KB Directory structure: gitextract_ofu0l0m2/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── test.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples/ │ ├── cpu-monitor.rs │ ├── large-data.rs │ ├── mouse_events.rs │ └── split-chart/ │ ├── Cargo.toml │ ├── index.html │ └── src/ │ └── main.rs └── src/ ├── backend/ │ └── mod.rs ├── chart.rs ├── error.rs ├── lib.rs ├── renderer.rs ├── sample/ │ └── lttb.rs ├── sample.rs ├── utils.rs └── widget.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: interval: "daily" target-branch: "master" ================================================ FILE: .github/workflows/test.yml ================================================ name: Test and Build on: push: branches: [master] pull_request: branches: [master] env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] rust: [stable] steps: - uses: actions/checkout@master - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust }} components: rustfmt override: true - name: Install dependencies run: | if [ "$RUNNER_OS" == "Linux" ]; then sudo apt-get -qq update sudo apt-get install -y libxkbcommon-dev fi shell: bash - name: Verify versions run: rustc --version && rustup --version && cargo --version - name: Cargo Build run: cargo build --verbose - name: Build example run: cargo build --examples continue-on-error: true - name: Run tests run: cargo test --verbose - name: Check code style run: cargo fmt -- --check ================================================ FILE: .gitignore ================================================ target/* Cargo.lock .cargo/* examples/split-chart/dist/* .idea .DS_store .history ================================================ FILE: Cargo.toml ================================================ [package] name = "plotters-iced" version = "0.11.0" description = "Iced backend for Plotters" readme = "README.md" license = "MIT" edition = "2021" resolver = "2" homepage = "https://github.com/Joylei/plotters-iced" repository = "https://github.com/Joylei/plotters-iced.git" documentation = "https://docs.rs/crate/plotters-iced/" keywords = ["plotters", "chart", "plot", "iced", "backend"] categories = ["visualization"] authors = ["Joylei "] [workspace] members = [".", "examples/split-chart"] [dependencies] plotters = { version = "0.3", default-features = false } plotters-backend = "0.3" iced_widget = { version = "0.13", features = ["canvas"] } iced_graphics = "0.13" once_cell = "1" [dev-dependencies] plotters = { version = "0.3", default-features = false, features = [ "chrono", "area_series", "line_series", "point_series", ] } iced = { version = "0.13", features = ["canvas", "tokio"] } chrono = { version = "0.4", default-features = false } rand = "0.8" tokio = { version = "1", features = ["rt"], default-features = false } [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] sysinfo = { version = "0.30", default-features = false } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022, Joylei Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # plotters-iced [![Test and Build](https://github.com/joylei/plotters-iced/workflows/Test%20and%20Build/badge.svg?branch=master)](https://github.com/joylei/plotters-iced/actions?query=workflow%3A%22Test+and+Build%22) [![Documentation](https://docs.rs/plotters-iced/badge.svg)](https://docs.rs/plotters-iced) [![Crates.io](https://img.shields.io/crates/v/plotters-iced.svg)](https://crates.io/crates/plotters-iced) [![License](https://img.shields.io/crates/l/plotters-iced.svg)](https://github.com/joylei/plotters-iced/blob/master/LICENSE) This is an implementation of an Iced backend for Plotters, for both native and wasm applications. This backend has been optimized as for speed. Note that some specific plotting features supported in the Bitmap backend may not be implemented there, though. ## Showcase ![CPU Monitor Example](./images/plotter_iced_demo.png) ![WASM Example](./images/split-chart-web.png) ## What is Plotters? Plotters is an extensible Rust drawing library that can be used to plot data on nice-looking graphs, rendering them through a plotting backend (eg. to a Bitmap image raw buffer, to your GUI backend, to an SVG file, etc.). **For more details on Plotters, please check the following links:** - For an introduction of Plotters, see: [Plotters on Crates.io](https://crates.io/crates/plotters); - Check the main repository on [GitHub](https://github.com/38/plotters); - You can also visit the Plotters [homepage](https://plotters-rs.github.io/); ## How to install? Include `plotters-iced` in your `Cargo.toml` dependencies: ```toml [dependencies] plotters-iced = "0.11" iced = { version = "0.13", features = ["canvas", "tokio"] } plotters="0.3" ``` ## How to use? First, import `Chart` and `ChartWidget`: ```rust,ignore use plotters_iced::{Chart, ChartWidget, DrawingBackend, ChartBuilder}; ``` Then, derive `Chart` trait and build your chart, and let `plotters-iced` takes care the rest: ```rust,ignore struct MyChart; impl Chart for MyChart { type State = (); fn build_chart(&self, state: &Self::State, builder: ChartBuilder) { //build your chart here, please refer to plotters for more details } } ``` Finally, render your chart view: ```rust,ignore impl MyChart { fn view(&mut self)->Element { ChartWidget::new(self) .width(Length::Fixed(200)) .height(Length::Fixed(200)) .into() } } ``` _If you are looking for a full example of an implementation, please check [cpu-monitor.rs](./examples/cpu-monitor.rs)._ ## How to run the examples? ### Example #1: `cpu-monitor` This example samples your CPU load every second, and renders it in a real-time chart: ```sh cargo run --release --example cpu-monitor ``` From this example, you'll learn: - how to build charts by `plotters-iced` - how to feed data to charts - how to make layouts of charts responsive - how to use fonts with charts ### Example #2: `split-chart` This example shows you how to split drawing area. - run the native version ```sh cargo run --release --example split-chart ``` - run the web version ```sh cd examples/split-chart trunk serve ``` ## Are there any limitations? ### Limitation #1: No image rendering No image rendering for native and wasm applications. ### Limitation #2: Limited text rendering for native applications Only TTF font family are supported for text rendering, which is a limitation of `Iced`, please look at [cpu-monitor.rs](./examples/cpu-monitor.rs). As well, font transforms are not supported,which is also a limitation of `Iced`. ## Credits - [plotters-conrod](https://github.com/valeriansaliou/plotters-conrod) ================================================ FILE: examples/cpu-monitor.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT extern crate iced; extern crate plotters; extern crate sysinfo; use chrono::{DateTime, Utc}; use iced::{ alignment::{Horizontal, Vertical}, font, widget::{ canvas::{Cache, Frame, Geometry}, Column, Container, Row, Scrollable, Space, Text, }, Alignment, Element, Font, Length, Size, Task, }; use plotters::prelude::ChartBuilder; use plotters_backend::DrawingBackend; use plotters_iced::{Chart, ChartWidget, Renderer}; use std::{ collections::VecDeque, time::{Duration, Instant}, }; use sysinfo::{CpuRefreshKind, RefreshKind, System}; const PLOT_SECONDS: usize = 60; //1 min const TITLE_FONT_SIZE: u16 = 22; const SAMPLE_EVERY: Duration = Duration::from_millis(1000); const FONT_BOLD: Font = Font { family: font::Family::Name("Noto Sans"), weight: font::Weight::Bold, ..Font::DEFAULT }; fn main() { iced::application("CPU Monitor Example", State::update, State::view) .antialiasing(true) .default_font(Font::with_name("Noto Sans")) .subscription(|_| { const FPS: u64 = 50; iced::time::every(Duration::from_millis(1000 / FPS)).map(|_| Message::Tick) }) .run_with(State::new) .unwrap(); } #[derive(Debug)] enum Message { /// message that cause charts' data lazily updated Tick, FontLoaded(Result<(), font::Error>), } struct State { chart: SystemChart, } impl State { fn new() -> (Self, Task) { ( Self { chart: Default::default(), }, Task::batch([ font::load(include_bytes!("./fonts/notosans-regular.ttf").as_slice()) .map(Message::FontLoaded), font::load(include_bytes!("./fonts/notosans-bold.ttf").as_slice()) .map(Message::FontLoaded), ]), ) } fn update(&mut self, message: Message) { match message { Message::Tick => { self.chart.update(); } _ => {} } } fn view(&self) -> Element<'_, Message> { let content = Column::new() .spacing(20) .align_x(Alignment::Start) .width(Length::Fill) .height(Length::Fill) .push( Text::new("Iced test chart") .size(TITLE_FONT_SIZE) .font(FONT_BOLD), ) .push(self.chart.view()); Container::new(content) //.style(style::Container) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) .into() } } struct SystemChart { sys: System, last_sample_time: Instant, items_per_row: usize, processors: Vec, chart_height: f32, } impl Default for SystemChart { fn default() -> Self { Self { sys: System::new_with_specifics( RefreshKind::new().with_cpu(CpuRefreshKind::new().with_cpu_usage()), ), last_sample_time: Instant::now(), items_per_row: 3, processors: Default::default(), chart_height: 300.0, } } } impl SystemChart { #[inline] fn is_initialized(&self) -> bool { !self.processors.is_empty() } #[inline] fn should_update(&self) -> bool { !self.is_initialized() || self.last_sample_time.elapsed() > SAMPLE_EVERY } fn update(&mut self) { if !self.should_update() { return; } //eprintln!("refresh..."); self.sys.refresh_cpu(); self.last_sample_time = Instant::now(); let now = Utc::now(); let data = self.sys.cpus().iter().map(|v| v.cpu_usage() as i32); //check if initialized if !self.is_initialized() { eprintln!("init..."); let mut processors: Vec<_> = data .map(|percent| CpuUsageChart::new(vec![(now, percent)].into_iter())) .collect(); self.processors.append(&mut processors); } else { //eprintln!("update..."); for (percent, p) in data.zip(self.processors.iter_mut()) { p.push_data(now, percent); } } } fn view(&self) -> Element { if !self.is_initialized() { Text::new("Loading...") .align_x(Horizontal::Center) .align_y(Vertical::Center) .into() } else { let mut col = Column::new() .width(Length::Fill) .height(Length::Shrink) .align_x(Alignment::Center); let chart_height = self.chart_height; let mut idx = 0; for chunk in self.processors.chunks(self.items_per_row) { let mut row = Row::new() .spacing(15) .padding(20) .width(Length::Fill) .height(Length::Shrink) .align_y(Alignment::Center); for item in chunk { row = row.push(item.view(idx, chart_height)); idx += 1; } while idx % self.items_per_row != 0 { row = row.push(Space::new(Length::Fill, Length::Fixed(50.0))); idx += 1; } col = col.push(row); } Scrollable::new(col).height(Length::Shrink).into() } } } struct CpuUsageChart { cache: Cache, data_points: VecDeque<(DateTime, i32)>, limit: Duration, } impl CpuUsageChart { fn new(data: impl Iterator, i32)>) -> Self { let data_points: VecDeque<_> = data.collect(); Self { cache: Cache::new(), data_points, limit: Duration::from_secs(PLOT_SECONDS as u64), } } fn push_data(&mut self, time: DateTime, value: i32) { let cur_ms = time.timestamp_millis(); self.data_points.push_front((time, value)); loop { if let Some((time, _)) = self.data_points.back() { let diff = Duration::from_millis((cur_ms - time.timestamp_millis()) as u64); if diff > self.limit { self.data_points.pop_back(); continue; } } break; } self.cache.clear(); } fn view(&self, idx: usize, chart_height: f32) -> Element { Column::new() .width(Length::Fill) .height(Length::Shrink) .spacing(5) .align_x(Alignment::Center) .push(Text::new(format!("Processor {}", idx))) .push(ChartWidget::new(self).height(Length::Fixed(chart_height))) .into() } } impl Chart for CpuUsageChart { type State = (); // fn update( // &mut self, // event: Event, // bounds: Rectangle, // cursor: Cursor, // ) -> (event::Status, Option) { // self.cache.clear(); // (event::Status::Ignored, None) // } #[inline] fn draw( &self, renderer: &R, bounds: Size, draw_fn: F, ) -> Geometry { renderer.draw_cache(&self.cache, bounds, draw_fn) } fn build_chart(&self, _state: &Self::State, mut chart: ChartBuilder) { use plotters::prelude::*; const PLOT_LINE_COLOR: RGBColor = RGBColor(0, 175, 255); // Acquire time range let newest_time = self .data_points .front() .unwrap_or(&(DateTime::from_timestamp(0, 0).unwrap(), 0)) .0; let oldest_time = newest_time - chrono::Duration::seconds(PLOT_SECONDS as i64); let mut chart = chart .x_label_area_size(0) .y_label_area_size(28) .margin(20) .build_cartesian_2d(oldest_time..newest_time, 0..100) .expect("failed to build chart"); chart .configure_mesh() .bold_line_style(plotters::style::colors::BLUE.mix(0.1)) .light_line_style(plotters::style::colors::BLUE.mix(0.05)) .axis_style(ShapeStyle::from(plotters::style::colors::BLUE.mix(0.45)).stroke_width(1)) .y_labels(10) .y_label_style( ("sans-serif", 15) .into_font() .color(&plotters::style::colors::BLUE.mix(0.65)) .transform(FontTransform::Rotate90), ) .y_label_formatter(&|y: &i32| format!("{}%", y)) .draw() .expect("failed to draw chart mesh"); chart .draw_series( AreaSeries::new( self.data_points.iter().map(|x| (x.0, x.1)), 0, PLOT_LINE_COLOR.mix(0.175), ) .border_style(ShapeStyle::from(PLOT_LINE_COLOR).stroke_width(2)), ) .expect("failed to draw chart data"); } } ================================================ FILE: examples/large-data.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT extern crate iced; extern crate plotters; extern crate rand; extern crate tokio; use chrono::{DateTime, Utc}; use iced::{ font, widget::{ canvas::{Cache, Frame, Geometry}, Column, Container, Text, }, Alignment, Element, Font, Length, Size, Task, }; use plotters::prelude::ChartBuilder; use plotters_backend::DrawingBackend; use plotters_iced::{ sample::lttb::{DataPoint, LttbSource}, Chart, ChartWidget, Renderer, }; use rand::Rng; use std::time::Duration; use std::{collections::VecDeque, time::Instant}; const TITLE_FONT_SIZE: u16 = 22; const FONT_BOLD: Font = Font { family: font::Family::Name("Noto Sans"), weight: font::Weight::Bold, ..Font::DEFAULT }; fn main() { iced::application("Large Data Example", State::update, State::view) .antialiasing(true) .default_font(Font::with_name("Noto Sans")) .run_with(State::new) .unwrap(); } struct Wrapper<'a>(&'a DateTime, &'a f32); impl DataPoint for Wrapper<'_> { #[inline] fn x(&self) -> f64 { self.0.timestamp() as f64 } #[inline] fn y(&self) -> f64 { *self.1 as f64 } } #[derive(Debug)] enum Message { FontLoaded(Result<(), font::Error>), DataLoaded(Vec<(DateTime, f32)>), Sampled(Vec<(DateTime, f32)>), } struct State { chart: Option, } impl State { fn new() -> (Self, Task) { ( Self { chart: None }, Task::batch([ font::load(include_bytes!("./fonts/notosans-regular.ttf").as_slice()) .map(Message::FontLoaded), font::load(include_bytes!("./fonts/notosans-bold.ttf").as_slice()) .map(Message::FontLoaded), Task::perform(tokio::task::spawn_blocking(generate_data), |data| { Message::DataLoaded(data.unwrap()) }), ]), ) } fn update(&mut self, message: Message) -> Task { match message { Message::DataLoaded(data) => Task::perform( tokio::task::spawn_blocking(move || { let now = Instant::now(); let sampled: Vec<_> = (&data[..]) .cast(|v| Wrapper(&v.0, &v.1)) .lttb(1000) .map(|w| (*w.0, *w.1)) .collect(); dbg!(now.elapsed().as_millis()); sampled }), |data| Message::Sampled(data.unwrap()), ), Message::Sampled(sampled) => { self.chart = Some(ExampleChart::new(sampled.into_iter())); Task::none() } _ => Task::none(), } } fn view(&self) -> Element<'_, Message> { let content = Column::new() .spacing(20) .align_x(Alignment::Start) .width(Length::Fill) .height(Length::Fill) .push( Text::new("Iced test chart") .size(TITLE_FONT_SIZE) .font(FONT_BOLD), ) .push(match self.chart { Some(ref chart) => chart.view(), None => Text::new("Loading...").into(), }); Container::new(content) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) .into() } } struct ExampleChart { cache: Cache, data_points: VecDeque<(DateTime, f32)>, } impl ExampleChart { fn new(data: impl Iterator, f32)>) -> Self { let data_points: VecDeque<_> = data.collect(); Self { cache: Cache::new(), data_points, } } fn view(&self) -> Element { let chart = ChartWidget::new(self) .width(Length::Fill) .height(Length::Fill); chart.into() } } impl Chart for ExampleChart { type State = (); // fn update( // &mut self, // event: Event, // bounds: Rectangle, // cursor: Cursor, // ) -> (event::Status, Option) { // self.cache.clear(); // (event::Status::Ignored, None) // } #[inline] fn draw( &self, renderer: &R, bounds: Size, draw_fn: F, ) -> Geometry { renderer.draw_cache(&self.cache, bounds, draw_fn) } fn build_chart(&self, _state: &Self::State, mut chart: ChartBuilder) { use plotters::prelude::*; const PLOT_LINE_COLOR: RGBColor = RGBColor(0, 175, 255); // Acquire time range let newest_time = self .data_points .back() .unwrap() .0 .checked_add_signed(chrono::Duration::from_std(Duration::from_secs(10)).unwrap()) .unwrap(); //let oldest_time = newest_time - chrono::Duration::seconds(PLOT_SECONDS as i64); let oldest_time = self .data_points .front() .unwrap() .0 .checked_sub_signed(chrono::Duration::from_std(Duration::from_secs(10)).unwrap()) .unwrap(); //dbg!(&newest_time); //dbg!(&oldest_time); let mut chart = chart .x_label_area_size(0) .y_label_area_size(28) .margin(20) .build_cartesian_2d(oldest_time..newest_time, -10.0_f32..110.0_f32) .expect("failed to build chart"); chart .configure_mesh() .bold_line_style(plotters::style::colors::BLUE.mix(0.1)) .light_line_style(plotters::style::colors::BLUE.mix(0.05)) .axis_style(ShapeStyle::from(plotters::style::colors::BLUE.mix(0.45)).stroke_width(1)) .y_labels(10) .y_label_style( ("Noto Sans", 15) .into_font() .color(&plotters::style::colors::BLUE.mix(0.65)) .transform(FontTransform::Rotate90), ) .y_label_formatter(&|y| format!("{}", y)) .draw() .expect("failed to draw chart mesh"); chart .draw_series( AreaSeries::new( self.data_points.iter().cloned(), 0_f32, PLOT_LINE_COLOR.mix(0.175), ) .border_style(ShapeStyle::from(PLOT_LINE_COLOR).stroke_width(2)), ) .expect("failed to draw chart data"); } } fn generate_data() -> Vec<(DateTime, f32)> { let total = 10_000_000; let mut data = Vec::new(); let mut rng = rand::thread_rng(); let time_range = (24 * 3600 * 30) as f32; let interval = (3600 * 12) as f32; let start = Utc::now() .checked_sub_signed( chrono::Duration::from_std(Duration::from_secs_f32(time_range)).unwrap(), ) .unwrap(); while data.len() < total { let secs = rng.gen_range(0.1..time_range); let time = start .checked_sub_signed(chrono::Duration::from_std(Duration::from_secs_f32(secs)).unwrap()) .unwrap(); let value = (((secs % interval) - interval / 2.0) / (interval / 2.0) * std::f32::consts::PI).sin() * 50_f32 + 50_f32; data.push((time, value)); } data.sort_by_cached_key(|x| x.0); //dbg!(&data[..100]); data } ================================================ FILE: examples/mouse_events.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Grey // License: MIT use iced::{ event, mouse::Cursor, widget::{ canvas::{self, Cache, Frame, Geometry}, Column, Container, Text, }, Alignment, Element, Length, Point, Size, }; use plotters::{ coord::{types::RangedCoordf32, ReverseCoordTranslate}, prelude::*, }; use plotters_iced::{Chart, ChartWidget, Renderer}; use std::cell::RefCell; #[derive(Default)] struct State { chart: ArtChart, } impl State { fn update(&mut self, message: Message) { match message { Message::MouseEvent(event, point) => { self.chart.set_current_position(point); match event { iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left) => { self.chart.set_down(true); } iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left) => { self.chart.set_down(false); } _ => { // Do nothing } } } } } fn view(&self) -> Element { let content = Column::new() .spacing(20) .width(Length::Fill) .height(Length::Fill) .push(Text::new("Click below!").size(20)) .push(self.chart.view()) .align_x(Alignment::Center) .padding(15); Container::new(content) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) .into() } } #[derive(Default)] struct ArtChart { cache: Cache, points: Vec<(f32, f32)>, lines: Vec<((f32, f32), (f32, f32))>, is_down: bool, current_position: Option<(f32, f32)>, initial_down_position: Option<(f32, f32)>, spec: RefCell>>, } impl ArtChart { fn view(&self) -> Element { let chart = ChartWidget::new(self) .width(Length::Fill) .height(Length::Fill); chart.into() } fn set_current_position(&mut self, p: Point) { if let Some(spec) = self.spec.borrow().as_ref() { self.current_position = spec.reverse_translate((p.x as i32, p.y as i32)); self.cache.clear(); } } fn nearby(p0: (f32, f32), p1: (f32, f32)) -> bool { let delta = (p1.0 - p0.0, p1.1 - p0.1); (delta.0 * delta.0 + delta.1 * delta.1).sqrt() <= 1.0 } fn set_down(&mut self, new_is_down: bool) { if !self.is_down && new_is_down { self.initial_down_position = self.current_position; } if self.is_down && !new_is_down { if let Some((initial_p, current_p)) = self.initial_down_position.zip(self.current_position) { if Self::nearby(initial_p, current_p) { self.points.push(current_p); } else { self.lines.push((initial_p, current_p)); } } } self.is_down = new_is_down; } } impl Chart for ArtChart { type State = (); fn draw( &self, renderer: &R, bounds: Size, draw_fn: F, ) -> Geometry { renderer.draw_cache(&self.cache, bounds, draw_fn) } fn build_chart(&self, _state: &Self::State, mut builder: ChartBuilder) { use plotters::style::colors; const POINT_COLOR: RGBColor = colors::RED; const LINE_COLOR: RGBColor = colors::BLUE; const HOVER_COLOR: RGBColor = colors::YELLOW; const PREVIEW_COLOR: RGBColor = colors::GREEN; let mut chart = builder .x_label_area_size(28_i32) .y_label_area_size(28_i32) .margin(20_i32) .build_cartesian_2d(0_f32..100_f32, 0_f32..100_f32) .expect("Failed to build chart"); chart .configure_mesh() .bold_line_style(colors::BLACK.mix(0.1)) .light_line_style(colors::BLACK.mix(0.05)) .axis_style(ShapeStyle::from(colors::BLACK.mix(0.45)).stroke_width(1)) .y_labels(10) .y_label_style( ("sans-serif", 15) .into_font() .color(&colors::BLACK.mix(0.65)) .transform(FontTransform::Rotate90), ) .y_label_formatter(&|y| format!("{}", y)) .draw() .expect("Failed to draw chart mesh"); chart .draw_series( self.points .iter() .map(|p| Circle::new(*p, 5_i32, POINT_COLOR.filled())), ) .expect("Failed to draw points"); for line in &self.lines { chart .draw_series(LineSeries::new( vec![line.0, line.1].into_iter(), LINE_COLOR.filled(), )) .expect("Failed to draw line"); } if self.is_down { if let Some((initial_p, current_p)) = self.initial_down_position.zip(self.current_position) { if Self::nearby(initial_p, current_p) { chart .draw_series(std::iter::once(Circle::new( current_p, 5_i32, PREVIEW_COLOR.filled(), ))) .expect("Failed to draw preview point"); } else { chart .draw_series(LineSeries::new( vec![initial_p, current_p].into_iter(), PREVIEW_COLOR.filled(), )) .expect("Failed to draw preview line"); } } } else if let Some(current_p) = self.current_position { chart .draw_series(std::iter::once(Circle::new( current_p, 5_i32, HOVER_COLOR.filled(), ))) .expect("Failed to draw hover point"); } *self.spec.borrow_mut() = Some(chart.as_coord_spec().clone()); } fn update( &self, _state: &mut Self::State, event: canvas::Event, bounds: iced::Rectangle, cursor: Cursor, ) -> (event::Status, Option) { if let Cursor::Available(point) = cursor { match event { canvas::Event::Mouse(evt) if bounds.contains(point) => { let p_origin = bounds.position(); let p = point - p_origin; return ( event::Status::Captured, Some(Message::MouseEvent(evt, Point::new(p.x, p.y))), ); } _ => {} } } (event::Status::Ignored, None) } } #[derive(Debug)] enum Message { MouseEvent(iced::mouse::Event, iced::Point), } fn main() -> iced::Result { iced::application("Art", State::update, State::view) .antialiasing(true) .run() } ================================================ FILE: examples/split-chart/Cargo.toml ================================================ [package] name = "split-chart" version = "0.1.0" authors = ["Joylei "] edition = "2021" publish = false [dependencies] iced = { version = "0.13", features = ["canvas"] } plotters-iced = { path = "../../" } plotters = { version = "0.3", default-features = false, features = [ "chrono", "area_series", "line_series", "point_series", ] } [target.'cfg(target_arch = "wasm32")'.dependencies] iced.version = "0.13" iced.features = ["canvas", "debug", "webgl"] console_error_panic_hook = "0.1" console_log = "1.0" ================================================ FILE: examples/split-chart/index.html ================================================ split chart example ================================================ FILE: examples/split-chart/src/main.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT /*! - run the native version ```sh cargo run --release --example split-chart ``` - run the web version with [trunk](https://trunkrs.dev/) ```sh cd examples trunk serve ``` */ extern crate iced; extern crate plotters; use iced::{ widget::{Column, Container, Text}, window, Alignment, Element, Length, }; use plotters::{coord::Shift, prelude::*}; use plotters_backend::DrawingBackend; use plotters_iced::{plotters_backend, Chart, ChartWidget, DrawingArea}; const TITLE_FONT_SIZE: u16 = 22; // antialiasing issue: https://github.com/iced-rs/iced/issues/1159 fn main() { #[cfg(target_arch = "wasm32")] { console_log::init().expect("Initialize logger"); std::panic::set_hook(Box::new(console_error_panic_hook::hook)); } let app = iced::application("Split Chart Example", State::update, State::view) .antialiasing(cfg!(not(target_arch = "wasm32"))) .subscription(|_| window::frames().map(|_| Message::Tick)); app.run().unwrap(); } #[allow(unused)] #[derive(Debug)] enum Message { Tick, } #[derive(Default)] struct State { chart: MyChart, } impl State { fn update(&mut self, _message: Message) {} fn view(&self) -> Element<'_, Message> { let content = Column::new() .spacing(20) .align_x(Alignment::Start) .width(Length::Fill) .height(Length::Fill) .push(Text::new("Iced test chart").size(TITLE_FONT_SIZE)) .push(self.chart.view()); Container::new(content) .padding(5) .center_x(Length::Fill) .center_y(Length::Fill) .into() } } #[allow(unused)] #[derive(Default)] struct MyChart; impl MyChart { fn view(&self) -> Element { let chart = ChartWidget::new(self) .width(Length::Fill) .height(Length::Fill); chart.into() } } impl Chart for MyChart { type State = (); // leave it empty fn build_chart(&self, _state: &Self::State, _builder: ChartBuilder) {} fn draw_chart(&self, _state: &Self::State, root: DrawingArea) { let children = root.split_evenly((2, 2)); for (i, area) in children.iter().enumerate() { let builder = ChartBuilder::on(area); draw_chart(builder, i + 1); } } } fn draw_chart(mut chart: ChartBuilder, power: usize) { let mut chart = chart .margin(30) .caption(format!("y=x^{}", power), ("sans-serif", 22)) .x_label_area_size(30) .y_label_area_size(30) .build_cartesian_2d(-1f32..1f32, -1.2f32..1.2f32) .unwrap(); chart .configure_mesh() .x_labels(3) .y_labels(3) // .y_label_style( // ("sans-serif", 15) // .into_font() // .color(&plotters::style::colors::BLACK.mix(0.8)) // .transform(FontTransform::RotateAngle(30.0)), // ) .draw() .unwrap(); chart .draw_series(LineSeries::new( (-50..=50) .map(|x| x as f32 / 50.0) .map(|x| (x, x.powf(power as f32))), &RED, )) .unwrap(); } ================================================ FILE: src/backend/mod.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use std::collections::HashSet; use iced_graphics::core::text::Paragraph; use iced_widget::{ canvas, core::{ alignment::{Horizontal, Vertical}, font, text, Font, Size, }, text::Shaping, }; use once_cell::unsync::Lazy; use plotters_backend::{ text_anchor, //FontTransform, BackendColor, BackendCoord, BackendStyle, BackendTextStyle, DrawingBackend, DrawingErrorKind, FontFamily, FontStyle, }; use crate::error::Error; use crate::utils::{cvt_color, cvt_stroke, CvtPoint}; /// The Iced drawing backend pub(crate) struct IcedChartBackend<'a, B> { frame: &'a mut canvas::Frame, backend: &'a B, shaping: Shaping, } impl<'a, B> IcedChartBackend<'a, B> where B: text::Renderer, { pub fn new(frame: &'a mut canvas::Frame, backend: &'a B, shaping: Shaping) -> Self { Self { frame, backend, shaping, } } } impl<'a, B> DrawingBackend for IcedChartBackend<'a, B> where B: text::Renderer, { type ErrorType = Error; fn get_size(&self) -> (u32, u32) { let Size { width, height } = self.frame.size(); (width as u32, height as u32) } fn ensure_prepared(&mut self) -> Result<(), DrawingErrorKind> { Ok(()) } fn present(&mut self) -> Result<(), DrawingErrorKind> { Ok(()) } #[inline] fn draw_pixel( &mut self, point: BackendCoord, color: BackendColor, ) -> Result<(), DrawingErrorKind> { if color.alpha == 0.0 { return Ok(()); } self.frame .fill_rectangle(point.cvt_point(), Size::new(1.0, 1.0), cvt_color(&color)); Ok(()) } #[inline] fn draw_line( &mut self, from: BackendCoord, to: BackendCoord, style: &S, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let line = canvas::Path::line(from.cvt_point(), to.cvt_point()); self.frame.stroke(&line, cvt_stroke(style)); Ok(()) } #[inline] fn draw_rect( &mut self, upper_left: BackendCoord, bottom_right: BackendCoord, style: &S, fill: bool, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let height = (bottom_right.1 - upper_left.1) as f32; let width = (bottom_right.0 - upper_left.0) as f32; let upper_left = upper_left.cvt_point(); if fill { self.frame.fill_rectangle( upper_left, Size::new(width, height), cvt_color(&style.color()), ); } else { let rect = canvas::Path::rectangle(upper_left, Size::new(width, height)); self.frame.stroke(&rect, cvt_stroke(style)); } Ok(()) } #[inline] fn draw_path>( &mut self, path: I, style: &S, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let path = canvas::Path::new(move |builder| { for (i, point) in path.into_iter().enumerate() { if i > 0 { builder.line_to(point.cvt_point()); } else { builder.move_to(point.cvt_point()); } } }); self.frame.stroke(&path, cvt_stroke(style)); Ok(()) } #[inline] fn draw_circle( &mut self, center: BackendCoord, radius: u32, style: &S, fill: bool, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let circle = canvas::Path::circle(center.cvt_point(), radius as f32); if fill { self.frame.fill(&circle, cvt_color(&style.color())); } else { self.frame.stroke(&circle, cvt_stroke(style)); } Ok(()) } #[inline] fn fill_polygon>( &mut self, vert: I, style: &S, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let path = canvas::Path::new(move |builder| { for (i, point) in vert.into_iter().enumerate() { if i > 0 { builder.line_to(point.cvt_point()); } else { builder.move_to(point.cvt_point()); } } builder.close(); }); self.frame.fill(&path, cvt_color(&style.color())); Ok(()) } #[inline] fn draw_text( &mut self, text: &str, style: &S, pos: BackendCoord, ) -> Result<(), DrawingErrorKind> { if style.color().alpha == 0.0 { return Ok(()); } let horizontal_alignment = match style.anchor().h_pos { text_anchor::HPos::Left => Horizontal::Left, text_anchor::HPos::Right => Horizontal::Right, text_anchor::HPos::Center => Horizontal::Center, }; let vertical_alignment = match style.anchor().v_pos { text_anchor::VPos::Top => Vertical::Top, text_anchor::VPos::Center => Vertical::Center, text_anchor::VPos::Bottom => Vertical::Bottom, }; let font = style_to_font(style); let pos = pos.cvt_point(); //let (w, h) = self.estimate_text_size(text, style)?; let text = canvas::Text { content: text.to_owned(), position: pos, color: cvt_color(&style.color()), size: (style.size() as f32).into(), line_height: Default::default(), font, horizontal_alignment, vertical_alignment, shaping: self.shaping, }; //TODO: fix rotation until text rotation is supported by Iced // let rotate = match style.transform() { // FontTransform::None => None, // FontTransform::Rotate90 => Some(90.0), // FontTransform::Rotate180 => Some(180.0), // FontTransform::Rotate270 => Some(270.0), // FontTransform::RotateAngle(angle) => Some(angle), // }; // if let Some(rotate) = rotate { // dbg!(rotate); // self.frame.with_save(move |frame| { // frame.fill_text(text); // frame.translate(Vector::new(pos.x + w as f32 / 2.0, pos.y + h as f32 / 2.0)); // let angle = 2.0 * std::f32::consts::PI * rotate / 360.0; // frame.rotate(angle); // }); // } else { // self.frame.fill_text(text); // } self.frame.fill_text(text); Ok(()) } #[inline] fn estimate_text_size( &self, text: &str, style: &S, ) -> Result<(u32, u32), DrawingErrorKind> { let font = style_to_font(style); let bounds = self.frame.size(); let horizontal_alignment = match style.anchor().h_pos { text_anchor::HPos::Left => Horizontal::Left, text_anchor::HPos::Right => Horizontal::Right, text_anchor::HPos::Center => Horizontal::Center, }; let vertical_alignment = match style.anchor().v_pos { text_anchor::VPos::Top => Vertical::Top, text_anchor::VPos::Center => Vertical::Center, text_anchor::VPos::Bottom => Vertical::Bottom, }; let p = B::Paragraph::with_text(iced_widget::core::text::Text { content: text, bounds, size: self.backend.default_size(), line_height: Default::default(), font, horizontal_alignment, vertical_alignment, shaping: self.shaping, wrapping: iced_widget::core::text::Wrapping::Word, }); let size = p.min_bounds(); Ok((size.width as u32, size.height as u32)) } #[inline] fn blit_bitmap( &mut self, _pos: BackendCoord, (_iw, _ih): (u32, u32), _src: &[u8], ) -> Result<(), DrawingErrorKind> { // Not supported yet (rendering ignored) // Notice: currently Iced has limitations, because widgets are not rendered in the order of creation, and different primitives go to different render pipelines. Ok(()) } } fn style_to_font(style: &S) -> Font { // iced font family requires static str static mut FONTS: Lazy> = Lazy::new(HashSet::new); Font { family: match style.family() { FontFamily::Serif => font::Family::Serif, FontFamily::SansSerif => font::Family::SansSerif, FontFamily::Monospace => font::Family::Monospace, FontFamily::Name(s) => { let s = unsafe { if !FONTS.contains(s) { FONTS.insert(String::from(s)); } FONTS.get(s).unwrap().as_str() }; font::Family::Name(s) } }, weight: match style.style() { FontStyle::Bold => font::Weight::Bold, _ => font::Weight::Normal, }, ..Font::DEFAULT } } ================================================ FILE: src/chart.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use iced_widget::canvas::Cache; use iced_widget::core::event::Status; use iced_widget::core::mouse::Interaction; use iced_widget::core::Rectangle; use iced_widget::{ canvas::{Event, Frame, Geometry}, core::{mouse::Cursor, Size}, }; use plotters::{chart::ChartBuilder, coord::Shift, drawing::DrawingArea}; use plotters_backend::DrawingBackend; /// graphics renderer pub trait Renderer { /// draw frame fn draw(&self, bounds: Size, draw_fn: F) -> Geometry; /// draw frame with cache fn draw_cache(&self, cache: &Cache, bounds: Size, draw_fn: F) -> Geometry; } impl Chart for &C where C: Chart, { type State = C::State; #[inline] fn build_chart(&self, state: &Self::State, builder: ChartBuilder) { C::build_chart(self, state, builder); } #[inline] fn draw_chart(&self, state: &Self::State, root: DrawingArea) { C::draw_chart(self, state, root); } #[inline] fn draw(&self, renderer: &R, size: Size, f: F) -> Geometry { C::draw(self, renderer, size, f) } #[inline] fn update( &self, state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (Status, Option) { C::update(self, state, event, bounds, cursor) } #[inline] fn mouse_interaction( &self, state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> Interaction { C::mouse_interaction(self, state, bounds, cursor) } } /// Chart View Model /// /// ## Example /// ```rust,ignore /// use plotters::prelude::*; /// use plotters_iced::{Chart,ChartWidget}; /// struct MyChart; /// impl Chart for MyChart { /// type State = (); /// fn build_chart(&self, state: &Self::State, builder: ChartBuilder) { /// //build your chart here, please refer to plotters for more details /// } /// } /// /// impl MyChart { /// fn view(&mut self)->Element { /// ChartWidget::new(self) /// .width(Length::Fixed(200)) /// .height(Length::Fixed(200)) /// .into() /// } /// } /// ``` pub trait Chart { /// state data of chart type State: Default + 'static; /// draw chart with [`ChartBuilder`] /// /// for simple chart, you impl this method fn build_chart(&self, state: &Self::State, builder: ChartBuilder); /// override this method if you want more freedom of drawing area /// /// ## Example /// ```rust,ignore /// use plotters::prelude::*; /// use plotters_iced::{Chart,ChartWidget}; /// /// struct MyChart{} /// /// impl Chart for MyChart { /// // leave it empty /// fn build_chart(&self, state: &Self::State, builder: ChartBuilder){} /// fn draw_chart(&self, state: &Self::State, root: DrawingArea){ /// let children = root.split_evenly((3,3)); /// for (area, color) in children.into_iter().zip(0..) { /// area.fill(&Palette99::pick(color)).unwrap(); /// } /// } /// } /// ``` #[inline] fn draw_chart(&self, state: &Self::State, root: DrawingArea) { let builder = ChartBuilder::on(&root); self.build_chart(state, builder); } /// draw on [`iced_widget::canvas::Canvas`] /// /// override this method if you want to use [`iced_widget::canvas::Cache`] /// /// ## Example /// ```rust,ignore /// /// impl Chart for CpuUsageChart { /// /// #[inline] /// fn draw(&self, renderer: &R, bounds: Size, draw_fn: F) -> Geometry { /// R::draw_cache(renderer, &self.cache, size, draw_fn) /// } /// //... /// } /// ``` #[inline] fn draw(&self, renderer: &R, size: Size, f: F) -> Geometry { R::draw(renderer, size, f) } /// react on event #[allow(unused_variables)] #[inline] #[allow(unused)] fn update( &self, state: &mut Self::State, event: Event, bounds: Rectangle, cursor: Cursor, ) -> (Status, Option) { (Status::Ignored, None) } /// Returns the current mouse interaction of the [`Chart`] #[inline] #[allow(unused)] fn mouse_interaction( &self, state: &Self::State, bounds: Rectangle, cursor: Cursor, ) -> Interaction { Interaction::Idle } } ================================================ FILE: src/error.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use std::error::Error as StdError; use std::fmt; #[derive(Debug)] /// Indicates that some error occurred within the Iced backend pub enum Error {} impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "{self:?}") } } impl StdError for Error {} ================================================ FILE: src/lib.rs ================================================ #![doc = include_str!("../README.md")] #![warn(missing_docs)] pub extern crate plotters_backend; #[doc(no_inline)] pub use plotters::{chart::ChartBuilder, drawing::DrawingArea}; #[doc(no_inline)] pub use plotters_backend::DrawingBackend; #[doc(inline)] pub use chart::Chart; #[doc(inline)] pub use chart::Renderer; #[doc(inline)] pub use error::Error; pub use widget::ChartWidget; mod backend; mod chart; mod error; mod renderer; /// data point sampling pub mod sample; mod utils; mod widget; ================================================ FILE: src/renderer.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use iced_widget::{ canvas::{Cache, Frame, Geometry}, core::{Layout, Size, Vector}, text::Shaping, }; use plotters::prelude::DrawingArea; use crate::backend::IcedChartBackend; use crate::Chart; /// Graphics Renderer pub trait Renderer: iced_widget::core::Renderer + iced_widget::core::text::Renderer + iced_graphics::geometry::Renderer { /// draw a [Chart] fn draw_chart( &mut self, state: &C::State, chart: &C, layout: Layout<'_>, shaping: Shaping, ) where C: Chart; } impl crate::chart::Renderer for iced_widget::renderer::Renderer { fn draw(&self, size: Size, f: F) -> Geometry { let mut frame = Frame::new(self, size); f(&mut frame); frame.into_geometry() } fn draw_cache(&self, cache: &Cache, size: Size, f: F) -> Geometry { cache.draw(self, size, f) } } impl Renderer for iced_widget::renderer::Renderer { fn draw_chart( &mut self, state: &C::State, chart: &C, layout: Layout<'_>, shaping: Shaping, ) where C: Chart, { let bounds = layout.bounds(); if bounds.width < 1.0 || bounds.height < 1.0 { return; } let geometry = chart.draw(self, bounds.size(), |frame| { let backend = IcedChartBackend::new(frame, self, shaping); let root: DrawingArea<_, _> = backend.into(); chart.draw_chart(state, root); }); let translation = Vector::new(bounds.x, bounds.y); iced_widget::core::Renderer::with_translation(self, translation, |renderer| { iced_graphics::geometry::Renderer::draw_geometry(renderer, geometry); }); } } ================================================ FILE: src/sample/lttb.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT // //! Largest-Triangle-Three-Buckets algorithm (LTTB) //! //! ## Known limitations //! - X-values must be in a strictly increasing order // original version: https://github.com/sveinn-steinarsson/flot-downsample // modified based on https://github.com/jeromefroe/lttb-rs /// data point for [`LttbSource`] pub trait DataPoint { /// x value for sampling, must be in a strictly increasing order fn x(&self) -> f64; /// y value for sampling fn y(&self) -> f64; } impl DataPoint for &D { #[inline] fn x(&self) -> f64 { (*self).x() } #[inline] fn y(&self) -> f64 { (*self).y() } } /// data source for lttb sampling /// /// ## Known limitations /// - X-values must be in a strictly increasing order pub trait LttbSource { /// data item of [`LttbSource`] type Item; /// length of [`LttbSource`] fn len(&self) -> usize; /// is [`LttbSource`] empty #[inline] fn is_empty(&self) -> bool { self.len() == 0 } /// data item at index `i` fn item_at(&self, i: usize) -> Self::Item; /// map data item to another type. /// - if the data item type of [`LttbSource`] is not [`DataPoint`], lttb sampling can be used after casting fn cast(self, f: F) -> Cast where Self: Sized, T: DataPoint, F: Fn(Self::Item) -> T, { Cast { s: self, f } } /// lttb sampling fn lttb(self, threshold: usize) -> LttbIterator where Self: Sized, Self::Item: DataPoint, { let is_sample = !(threshold >= self.len() || threshold < 3); let every = if is_sample { ((self.len() - 2) as f64) / ((threshold - 2) as f64) } else { 0_f64 }; LttbIterator { source: self, is_sample, idx: 0, a: 0, threshold, every, } } } /// map data item to another type pub struct Cast where S: LttbSource, T: DataPoint, F: Fn(S::Item) -> T, { s: S, f: F, } impl LttbSource for Cast where S: LttbSource, T: DataPoint, F: Fn(S::Item) -> T, { type Item = T; #[inline] fn len(&self) -> usize { self.s.len() } #[inline] fn item_at(&self, i: usize) -> Self::Item { let item = self.s.item_at(i); (self.f)(item) } } impl<'a, S: LttbSource> LttbSource for &'a S { type Item = S::Item; #[inline] fn len(&self) -> usize { (*self).len() } #[inline] fn item_at(&self, i: usize) -> Self::Item { (*self).item_at(i) } } /// iterator for [`LttbSource`] pub struct LttbIterator { source: S, is_sample: bool, idx: usize, threshold: usize, every: f64, a: usize, } impl LttbIterator where S::Item: DataPoint, { fn next_no_sample(&mut self) -> Option { if self.idx < self.source.len() { let item = self.source.item_at(self.idx); self.idx += 1; Some(item) } else { None } } fn next_sample(&mut self) -> Option { if self.idx < self.threshold { if self.idx == 0 { self.idx += 1; Some(self.source.item_at(0)) } else if self.idx + 1 == self.threshold { self.idx += 1; Some(self.source.item_at(self.source.len() - 1)) } else { let every = self.every; let i = self.idx - 1; // Calculate point average for next bucket (containing c). let mut avg_x = 0f64; let mut avg_y = 0f64; let avg_range_start = (((i + 1) as f64) * every) as usize + 1; let mut end = (((i + 2) as f64) * every) as usize + 1; if end >= self.source.len() { end = self.source.len(); } let avg_range_end = end; let avg_range_length = (avg_range_end - avg_range_start) as f64; for i in 0..(avg_range_end - avg_range_start) { let idx = avg_range_start + i; let item = self.source.item_at(idx); avg_x += item.x(); avg_y += item.y(); } avg_x /= avg_range_length; avg_y /= avg_range_length; // Get the range for this bucket. let range_offs = ((i as f64) * every) as usize + 1; let range_to = (((i + 1) as f64) * every) as usize + 1; // Point a. let item = self.source.item_at(self.a); let point_a_x = item.x(); let point_a_y = item.y(); let mut max_area = -1f64; let mut next_a = range_offs; for i in 0..(range_to - range_offs) { let idx = range_offs + i; // Calculate triangle area over three buckets. let item = self.source.item_at(idx); let area = ((point_a_x - avg_x) * (item.y() - point_a_y) - (point_a_x - item.x()) * (avg_y - point_a_y)) .abs() * 0.5; if area > max_area { max_area = area; next_a = idx; // Next a is this b. } } let item = self.source.item_at(next_a); // Pick this point from the bucket. self.a = next_a; // This a is the next a (chosen b). self.idx += 1; Some(item) } } else { None } } #[inline] fn remaining(&self) -> usize { if self.is_sample { self.threshold - self.idx } else { self.source.len() - self.idx } } } impl Iterator for LttbIterator where S::Item: DataPoint, { type Item = S::Item; fn next(&mut self) -> Option { if self.is_sample { self.next_sample() } else { self.next_no_sample() } } #[inline] fn size_hint(&self) -> (usize, Option) { let size = self.remaining(); (size, Some(size)) } } impl ExactSizeIterator for LttbIterator where S::Item: DataPoint, { #[inline] fn len(&self) -> usize { self.remaining() } } impl<'a, T> LttbSource for &'a [T] { type Item = &'a T; #[inline] fn len(&self) -> usize { (*self).len() } #[inline] fn item_at(&self, i: usize) -> Self::Item { &self[i] } } impl LttbSource for [T] { type Item = T; #[inline] fn len(&self) -> usize { (*self).len() } #[inline] fn item_at(&self, i: usize) -> Self::Item { self[i].clone() } } #[cfg(test)] mod tests { use super::*; #[derive(Debug, PartialEq, Clone)] pub struct DataPoint { pub x: f64, pub y: f64, } impl DataPoint { pub fn new(x: f64, y: f64) -> Self { DataPoint { x, y } } } impl super::DataPoint for DataPoint { fn x(&self) -> f64 { self.x } fn y(&self) -> f64 { self.y } } #[test] fn lttb_test() { let mut dps = vec![]; dps.push(DataPoint::new(0.0, 10.0)); dps.push(DataPoint::new(1.0, 12.0)); dps.push(DataPoint::new(2.0, 8.0)); dps.push(DataPoint::new(3.0, 10.0)); dps.push(DataPoint::new(4.0, 12.0)); let mut expected = vec![]; expected.push(DataPoint::new(0.0, 10.0)); expected.push(DataPoint::new(2.0, 8.0)); expected.push(DataPoint::new(4.0, 12.0)); let result: Vec = dps.as_slice().lttb(3).cloned().collect(); assert_eq!(expected, result); } } ================================================ FILE: src/sample.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT // pub mod lttb; ================================================ FILE: src/utils.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use iced_widget::canvas; use iced_widget::core::{Color, Point}; use plotters_backend::{BackendColor, BackendCoord, BackendStyle}; pub(crate) trait AndExt { fn and Self>(self, f: F) -> Self where Self: Sized; } impl AndExt for T { #[inline(always)] fn and Self>(self, f: F) -> Self where Self: Sized, { f(self) } } #[inline] pub(crate) fn cvt_color(color: &BackendColor) -> Color { let ((r, g, b), a) = (color.rgb, color.alpha); Color::from_rgba8(r, g, b, a as f32) } #[inline] pub(crate) fn cvt_stroke(style: &S) -> canvas::Stroke { canvas::Stroke::default() .with_color(cvt_color(&style.color())) .with_width(style.stroke_width() as f32) } pub(crate) trait CvtPoint { fn cvt_point(self) -> Point; } impl CvtPoint for BackendCoord { #[inline] fn cvt_point(self) -> Point { Point::new(self.0 as f32, self.1 as f32) } } impl CvtPoint for [f64; 2] { #[inline] fn cvt_point(self) -> Point { Point::new(self[0] as f32, self[1] as f32) } } ================================================ FILE: src/widget.rs ================================================ // plotters-iced // // Iced backend for Plotters // Copyright: 2022, Joylei // License: MIT use core::marker::PhantomData; use iced_widget::{ canvas::Event, core::{ event, mouse::Cursor, renderer::Style, widget::{tree, Tree}, Element, Layout, Length, Rectangle, Shell, Size, Widget, }, text::Shaping, }; use crate::renderer::Renderer; use super::Chart; /// Chart container, turns [`Chart`]s to [`Widget`]s pub struct ChartWidget<'a, Message, Theme, Renderer, C> where C: Chart, { chart: C, width: Length, height: Length, shaping: Shaping, _marker: PhantomData<&'a (Renderer, Theme, Message)>, } impl<'a, Message, Theme, Renderer, C> ChartWidget<'a, Message, Theme, Renderer, C> where C: Chart + 'a, { /// create a new [`ChartWidget`] pub fn new(chart: C) -> Self { Self { chart, width: Length::Fill, height: Length::Fill, shaping: Default::default(), _marker: Default::default(), } } /// set width pub fn width(mut self, width: Length) -> Self { self.width = width; self } /// set height pub fn height(mut self, height: Length) -> Self { self.height = height; self } /// set text shaping pub fn text_shaping(mut self, shaping: Shaping) -> Self { self.shaping = shaping; self } } impl<'a, Message, Theme, Renderer, C> Widget for ChartWidget<'a, Message, Theme, Renderer, C> where C: Chart, Renderer: self::Renderer, { fn size(&self) -> Size { Size::new(self.width, self.height) } fn tag(&self) -> tree::Tag { struct Tag(T); tree::Tag::of::>() } fn state(&self) -> tree::State { tree::State::new(C::State::default()) } #[inline] fn layout( &self, _tree: &mut Tree, _renderer: &Renderer, limits: &iced_widget::core::layout::Limits, ) -> iced_widget::core::layout::Node { let size = limits.resolve(self.width, self.height, Size::ZERO); iced_widget::core::layout::Node::new(size) } #[inline] fn draw( &self, tree: &Tree, renderer: &mut Renderer, _theme: &Theme, _style: &Style, layout: Layout<'_>, _cursor_position: Cursor, _viewport: &Rectangle, ) { let state = tree.state.downcast_ref::(); renderer.draw_chart(state, &self.chart, layout, self.shaping); } #[inline] fn on_event( &mut self, tree: &mut Tree, event: iced_widget::core::Event, layout: Layout<'_>, cursor: Cursor, _renderer: &Renderer, _clipboard: &mut dyn iced_widget::core::Clipboard, shell: &mut Shell<'_, Message>, _rectangle: &Rectangle, ) -> event::Status { let bounds = layout.bounds(); let canvas_event = match event { iced_widget::core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), iced_widget::core::Event::Keyboard(keyboard_event) => { Some(Event::Keyboard(keyboard_event)) } _ => None, }; if let Some(canvas_event) = canvas_event { let state = tree.state.downcast_mut::(); let (event_status, message) = self.chart.update(state, canvas_event, bounds, cursor); if let Some(message) = message { shell.publish(message); } return event_status; } event::Status::Ignored } fn mouse_interaction( &self, tree: &Tree, layout: Layout<'_>, cursor: Cursor, _viewport: &Rectangle, _renderer: &Renderer, ) -> iced_widget::core::mouse::Interaction { let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); self.chart.mouse_interaction(state, bounds, cursor) } } impl<'a, Message, Theme, Renderer, C> From> for Element<'a, Message, Theme, Renderer> where Message: 'a, C: Chart + 'a, Renderer: self::Renderer, { fn from(widget: ChartWidget<'a, Message, Theme, Renderer, C>) -> Self { Element::new(widget) } }