rtplot 0.2.0

A library for generating plots of real-time data.
Documentation
use crate::figure::{FigureConfig, PlotType};
use glium::glutin::dpi::LogicalSize;
use glium::uniform;
use glium::{self, implement_vertex, Surface};
use glium_text_rusttype as glium_text;
use itertools_num::linspace;
use lyon::math::{point, Point};
use lyon::tessellation::basic_shapes::{fill_circle, fill_polyline, stroke_polyline, stroke_quad};
use lyon::tessellation::geometry_builder::{BuffersBuilder, VertexBuffers, VertexConstructor};
use lyon::tessellation::*;
use lyon::tessellation::{FillOptions, StrokeOptions};

pub static VERTEX_SHADER: &str = r#"
    #version 140
    in vec3 position;
    in vec3 rgb;
    out vec3 rgb_frag;
    uniform mat4 projection;
    void main() {
        gl_Position = projection * vec4(position, 1.0);
        rgb_frag = rgb;
    }
"#;

pub static FRAGMENT_SHADER: &str = r#"
    #version 140
    in vec3 rgb_frag;
    out vec4 color;
    void main() {
        color = vec4(rgb_frag, 1.0);
    }
"#;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Vertex {
    position: [f32; 3],
    rgb: [f32; 3],
}

implement_vertex!(Vertex, position, rgb);

impl Vertex {
    pub fn new(x: f32, y: f32, rgb: [u8; 3]) -> Self {
        let rgb: [f32; 3] = [
            f32::from(rgb[0]) / 255.0,
            f32::from(rgb[1]) / 255.0,
            f32::from(rgb[2]) / 255.0,
        ];
        Vertex {
            position: [x, y, 0.0],
            rgb,
        }
    }
}

enum ZDepth {
    Near,
    Far,
}

struct VertexCtor([u8; 3], ZDepth);
impl VertexConstructor<lyon::tessellation::StrokeVertex, Vertex> for VertexCtor {
    fn new_vertex(&mut self, vertex: lyon::tessellation::StrokeVertex) -> Vertex {
        let rgb: [f32; 3] = [
            f32::from(self.0[0]) / 255.0,
            f32::from(self.0[1]) / 255.0,
            f32::from(self.0[2]) / 255.0,
        ];
        let pos = vertex.position.to_array();
        let position = match self.1 {
            ZDepth::Far => [pos[0], pos[1], 0.0],
            ZDepth::Near => [pos[0], pos[1], 1.0],
        };
        Vertex { position, rgb }
    }
}

impl VertexConstructor<lyon::tessellation::FillVertex, Vertex> for VertexCtor {
    fn new_vertex(&mut self, vertex: lyon::tessellation::FillVertex) -> Vertex {
        let rgb: [f32; 3] = [
            f32::from(self.0[0]) / 255.0,
            f32::from(self.0[1]) / 255.0,
            f32::from(self.0[2]) / 255.0,
        ];
        let pos = vertex.position.to_array();
        let position = match self.1 {
            ZDepth::Far => [pos[0], pos[1], 0.0],
            ZDepth::Near => [pos[0], pos[1], 1.0],
        };
        Vertex { position, rgb }
    }
}

pub struct Window<'a> {
    pub events_loop: glium::glutin::event_loop::EventLoop<()>,
    display: glium::Display,
    program: glium::Program,
    draw_parameters: glium::DrawParameters<'a>,
    text_system: glium_text::TextSystem,
    font: glium_text::FontTexture,
}

impl<'a> Default for Window<'a> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> Window<'a> {
    pub fn new() -> Self {
        let events_loop = glium::glutin::event_loop::EventLoop::new();
        let context = glium::glutin::ContextBuilder::new()
            .with_vsync(true)
            .with_double_buffer(Some(true))
            .with_depth_buffer(24)
            .with_multisampling(2);
        let window = glium::glutin::window::WindowBuilder::new()
            .with_inner_size(LogicalSize {
                width: 800.0,
                height: 800.0,
            })
            .with_decorations(true)
            .with_title("Plot");

        let display = glium::Display::new(window, context, &events_loop).unwrap();
        let program =
            glium::Program::from_source(&display, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap();

        let draw_parameters = glium::DrawParameters {
            depth: glium::Depth {
                write: true,
                test: glium::DepthTest::IfLess,
                ..Default::default()
            },
            ..Default::default()
        };

        let text_system = glium_text::TextSystem::new(&display);
        let font = glium_text::FontTexture::new(
            &display,
            ttf_noto_sans::REGULAR,
            70,
            glium_text::FontTexture::ascii_character_list(),
        )
        .unwrap();

        Self {
            events_loop,
            display,
            program,
            draw_parameters,
            text_system,
            font,
        }
    }

    pub fn draw(&mut self, vertices: &[Vertex], config: &FigureConfig) {
        let mut target = self.display.draw();
        let color = (169.0 / 255.0, 169.0 / 255.0, 169.0 / 255.0, 1.0);
        target.clear_color_and_depth(color, 1.0);
        let mut mesh: VertexBuffers<Vertex, u32> = VertexBuffers::new();
        self.draw_text(&mut target, config);
        self.draw_grid(&mut mesh);

        let points: Vec<Point> = vertices
            .iter()
            .map(|x| point(x.position[0], x.position[1]))
            .collect();

        match config.plot_type {
            PlotType::Line => {
                stroke_polyline(
                    points.iter().cloned(),
                    false,
                    &StrokeOptions::tolerance(0.01).with_line_width(0.002),
                    &mut BuffersBuilder::new(&mut mesh, VertexCtor(config.color, ZDepth::Near)),
                )
                .expect("Could not draw line plot");
            }
            PlotType::Dot => {
                for point in points {
                    fill_circle(
                        point,
                        0.01,
                        &FillOptions::tolerance(0.01),
                        &mut BuffersBuilder::new(&mut mesh, VertexCtor(config.color, ZDepth::Near)),
                    )
                    .expect("Could not draw dot plot");
                }
            }
        }

        let (w, h) = self.display.get_framebuffer_dimensions();
        let aspect = w as f32 / h as f32;
        let ortho_mat = cgmath::ortho(-aspect, aspect, -1.0, 1.0, -1.0, 1.0);
        let ortho: &[[f32; 4]; 4] = ortho_mat.as_ref();
        let uniforms = uniform! {
            projection: *ortho,
        };

        let vertex_buffer = glium::VertexBuffer::new(&self.display, &mesh.vertices)
            .expect("Could not create vertex buffer");
        let indices = glium::IndexBuffer::new(
            &self.display,
            glium::index::PrimitiveType::TrianglesList,
            &mesh.indices,
        )
        .expect("Could not create index buffer");

        target
            .draw(
                &vertex_buffer,
                &indices,
                &self.program,
                &uniforms,
                &self.draw_parameters,
            )
            .expect("Could not draw the frame");

        target.finish().expect("Could not finish the frame");
    }

    fn draw_text<S>(&mut self, target: &mut S, config: &FigureConfig)
    where
        S: glium::Surface,
    {
        let (w, h) = self.display.get_framebuffer_dimensions();
        let aspect = w as f32 / h as f32;
        let ortho_mat = cgmath::ortho(-aspect, aspect, -1.0, 1.0, -1.0, 1.0);
        if let Some(text) = config.xlabel {
            let label = glium_text::TextDisplay::new(&self.text_system, &self.font, text);
            let text_width = label.get_width() * 0.1;
            #[rustfmt::skip]
            let matrix = ortho_mat * cgmath::Matrix4::new(
                0.1, 0.0, 0.0, 0.0,
                0.0, 0.1, 0.0, 0.0,
                0.0, 0.0, 0.1, 0.0,
                -text_width / 2.0, -0.90, 0.0, 1.0,
            );
            glium_text::draw(
                &label,
                &self.text_system,
                target,
                matrix,
                (0.0, 0.0, 0.0, 1.0),
            )
            .expect("Could not draw x label");
        }
        if let Some(text) = config.ylabel {
            let label = glium_text::TextDisplay::new(&self.text_system, &self.font, text);
            let text_width = label.get_width() * 0.1;
            #[rustfmt::skip]
            let matrix = ortho_mat * cgmath::Matrix4::new(
                0.1, 0.0, 0.0, 0.0,
                0.0, 0.1, 0.0, 0.0,
                0.0, 1.0, 0.1, 0.0,
                -0.90, -text_width / 2.0, 0.0, 1.0,
            ) * cgmath::Matrix4::from_angle_z(cgmath::Deg(90.0));
            glium_text::draw(
                &label,
                &self.text_system,
                target,
                matrix,
                (0.0, 0.0, 0.0, 1.0),
            )
            .expect("Could not draw y label");
        }
        if let Some([xmin, xmax]) = config.xlim {
            for (coord, tick) in linspace(-0.75, 0.75, 6).zip(linspace(xmin, xmax, 6)) {
                let tick_str = glium_text::TextDisplay::new(
                    &self.text_system,
                    &self.font,
                    &format!("{:.02}", tick),
                );
                let text_width = tick_str.get_width() * 0.05;
                #[rustfmt::skip]
                let matrix = ortho_mat * cgmath::Matrix4::new(
                    0.05, 0.0, 0.0, 0.0,
                    0.0, 0.05, 0.0, 0.0,
                    0.0, 0.0, 0.05, 0.0,
                    coord - text_width / 2.0, -0.80, 0.0, 1.0,
                );
                glium_text::draw(
                    &tick_str,
                    &self.text_system,
                    target,
                    matrix,
                    (0.0, 0.0, 0.0, 1.0),
                )
                .expect("Could not draw x axis values");
            }
        }
        if let Some([ymin, ymax]) = config.ylim {
            for (coord, tick) in linspace(-0.75, 0.75, 5).zip(linspace(ymin, ymax, 5)) {
                let tick_str = glium_text::TextDisplay::new(
                    &self.text_system,
                    &self.font,
                    &format!("{:.02}", tick),
                );
                let text_height = tick_str.get_height() * 0.05;
                #[rustfmt::skip]
                let matrix = ortho_mat * cgmath::Matrix4::new(
                    0.05, 0.0, 0.0, 0.0,
                    0.0, 0.05, 0.0, 0.0,
                    0.0, 0.0, 0.05, 0.0,
                    -0.85, coord - text_height / 2.0, 0.0, 1.0,
                );
                glium_text::draw(
                    &tick_str,
                    &self.text_system,
                    target,
                    matrix,
                    (0.0, 0.0, 0.0, 1.0),
                )
                .expect("Could not draw y axis labels");
            }
        }
    }

    fn draw_grid(&mut self, mesh: &mut VertexBuffers<Vertex, u32>) {
        let mut tessellator = FillTessellator::new();

        for tick in linspace(-0.75, 0.75, 6) {
            fill_polyline(
                [
                    point(tick - 0.001, 0.75),
                    point(tick - 0.001, -0.75),
                    point(tick + 0.001, -0.75),
                    point(tick + 0.001, 0.75),
                ]
                .iter()
                .cloned(),
                &mut tessellator,
                &FillOptions::tolerance(0.01),
                &mut BuffersBuilder::new(mesh, VertexCtor([0x5d, 0x5d, 0x5d], ZDepth::Far)),
            )
            .expect("Could not draw grid");
        }

        for tick in linspace(-0.75, 0.75, 5) {
            fill_polyline(
                [
                    point(0.75, tick - 0.001),
                    point(-0.75, tick - 0.001),
                    point(-0.75, tick + 0.001),
                    point(0.75, tick + 0.001),
                ]
                .iter()
                .cloned(),
                &mut tessellator,
                &FillOptions::tolerance(0.01),
                &mut BuffersBuilder::new(mesh, VertexCtor([0x5d, 0x5d, 0x5d], ZDepth::Far)),
            )
            .expect("Could not draw grid");
        }

        stroke_quad(
            point(-0.75, -0.75),
            point(-0.75, 0.75),
            point(0.75, 0.75),
            point(0.75, -0.75),
            &StrokeOptions::tolerance(0.01).with_line_width(0.001),
            &mut BuffersBuilder::new(mesh, VertexCtor([0, 0, 0], ZDepth::Near)),
        )
        .unwrap();
    }
}