Using Plotters with Yew
The Rust plotting crate Plotters provides a WASM backend through the crate plotters-canvas. Does this allow you to create plots in your Yew web app? Yes, it does. But you need the help of the crate web-sys. You are probably using web-sys already, if you are using callbacks on events in Yew.
To start, make sure your web-sys dependency includes the HtmlCanvasElement feature in your Cargo.toml.
[dependencies]
yew = "0.20"
web-sys = { version = "0.3", features =["HtmlCanvasElement"] }
plotters = "0.3"
plotters-canvas = "0.3"
The next step is to write your Yew component which uses Plotters to draw on a canvas. At the moment (Yew version 0.20), there is no easy way to realise this using a function component. Instead we have to resort to a struct component.
This means first defining the enum with messages, which can be used by the component:
use plotters::prelude::*;
use plotters_canvas::CanvasBackend;
use yew::prelude::*;
use web_sys::HtmlCanvasElement;
pub enum PlotMsg {
Redraw,
Nothing,
}
Next are the component’s properties. We could use Rust’s unit () here. But most likely you will extend this component to do something useful requiring properties. So we just create an empty properties struct:
#[derive(Properties, PartialEq)]
pub struct PlotProps {
}
Next is the component itself. The struct will have only one field at the moment, which is a reference to the canvas.
pub struct Plot {
canvas : NodeRef,
}
Next is the implementation of the Component trait for Plot, such that Plot becomes a yew component, which can be used inside an html! macro. For our purpose, we need to implement the create(..), update(…) and view(…) functions. The basic template is as follows:
impl Component for Plot {
type Message = PlotMsg;
type Properties = PlotProperties;
fn create(ctx: &Context<Self>) -> Self {
Plot {
canvas : NodeRef::default(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
true
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! (
<div>
<canvas ref = {self.canvas.clone()}/>
</div>
)
}
The update(…) function will contain all the logic to make the plot. The first step is to check what update(…) has to do based on the message it receives. When it receives Redraw, it needs to redraw the plot and return false, such that element is rerendered:
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PlotMsg::Redraw => {
false
},
_ => true,
}
}
The next step is to create a backend for the plot. The node reference is used for this. The node is cast to and HtmlCanvasElement. This element can be used to create the backend from. However, remember that a canvas has two sizes: The size of the element and the size of the drawing area. By default the drawing area is 300 by 150. I prefer to set the width and height to the bounding client, such that I can control the size easily using CSS.
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PlotMsg::Redraw => {
let element : HtmlCanvasElement = self.canvas.cast().unwrap();
let rect = element.get_bounding_client_rect();
element.set_height(rect.height() as u32);
element.set_width(rect.width() as u32);
let backend = CanvasBackend::with_canvas_object(element).unwrap();
false
},
_ => true,
}
}
Now we can basically copy the example on the Plotters side to create a plot.. So lets do that…
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
PlotMsg::Redraw => {
let element : HtmlCanvasElement = self.canvas.cast().unwrap();
let rect = element.get_bounding_client_rect();
element.set_height(rect.height() as u32);
element.set_width(rect.width() as u32);
let backend = CanvasBackend::with_canvas_object(element).unwrap();
let drawing_area = backend.into_drawing_area();
drawing_area.fill(&RGBColor(200,200,200)).unwrap();
let mut chart = ChartBuilder::on(&drawing_area)
.caption("y=x^2", ("sans-serif", 14).into_font())
.margin(5)
.x_label_area_size(30)
.y_label_area_size(30)
.build_cartesian_2d(-1f32..1f32, -0.1f32..1f32).unwrap();
chart.configure_mesh().draw().unwrap();
chart
.draw_series(LineSeries::new(
(-50..=50).map(|x| x as f32 / 50.0).map(|x| (x, x * x)),
&RED,
)).unwrap()
.label("y = x^2")
.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], &RED));
false
},
_ => true,
}
}
When you run your app, nothing happens. This is because nothing sends a Redraw message to the component. To do this, we need to update the create(…) function.
fn create(ctx: &Context<Self>) -> Self {
ctx.link().send_message(PlotMsg::Redraw);
Plot {
canvas : NodeRef::default(),
}
}
And that’s all you need…