LillaOst 2 / 2: Plotters + Fake + localStorage

jcbellido September 01, 2021 [Code] #LillaOst #Rust

Second and final part of Rust-y parenting: WebAssembly. This covers some details about generating data visualization in real time, build artificial entries for testing and a couple details about how did I approached storage.

New LillaOst Plotters

I'm strongly considering releasing this version of LillaOst as OpenSource. Perhaps the code can be helpful for someone starting with rust and WebAssembly. We'll see.

Reports and graphs

Both versions of LillaOst use the plotters library to render their graphs. There has been an evolution between the previous plotters implementation and the current one as can be seen in the previous image.

and not only in LO but also in the library itself. Plotters' team is working on a new manual that is shaping up great.

Integrating plotters

The idea is to use the plotters_canvas::CanvasBackend. Checking Yew's examples I found that the webgl sample has the scaffolding I needed to access a canvas element in the current document. In more detail, the idea is to write a component that draws one of the graphs (see figure above). Perhaps the most important parts are:

(...)
use plotters::prelude::*;
use plotters_canvas::CanvasBackend;
use web_sys::HtmlCanvasElement;

(...)
impl Component for SummaryFeedingsIndividual {
    (...)
    fn view(&self) -> Html {
        let div_id = format!("canvas-container-{}", self.props.person_id);
        let canvas_id = format!("canvas-{}", self.props.person_id);

        html! {
        <div id={div_id} class="block">
            <p>
                { format!("{}: feedings last 30 days", self.person_name).clone() }
            </p>
            <canvas id={canvas_id} ref=self.canvas_ref.clone()>
            </canvas>
            <script>
                {
                    format!(
                    "
                    var cont = document.getElementById('canvas-container-{}');
                    var canv = document.getElementById('canvas-{}');
                    canv.width = cont.offsetWidth;
                    canv.height = cont.offsetWidth / 1.777;"
                    , self.props.person_id, self.props.person_id)
                }
            </script>
        </div>
        }
    }

    fn rendered(&mut self, first_render: bool) {
        let data = your_smart_data_collection_strategy(&a, &b, &c);
        (...)
        let canvas = self.canvas_ref.cast::<HtmlCanvasElement>().unwrap();
        let _res = self.draw(canvas, &data);        
    }
}

Let me unpack this code a little, starting with the view method of the component. Here I wanted a somewhat FullHD-ish aspect ratio in the graphs. If you check the <script> block, what we're doing is turning the canvas into something close to a 16:9 aspect ratio. Then, when creating the canvas we're assigning a reference stored in the component itself. This reference is used in the rendered method to reach the canvas in the document from the component's code.

About the chart, it's essentially based on samples and ideas contained in the manual. The current implementation usually looks decent in the devices I tested it but it needs more work to be ready to go to a wider audience.

Generating passable demo data: Fake

In projects that include any form of persistence is useful to have a way of generate structured data on demand. Ideally we want to go beyond using rand(something, else) we want to be able to specify a sensible range of values and types for our fields. Perhaps you need:

  1. Fake (but believable) Name / Surname / emails
  2. Random addresses including ZIP, Post, Latitude, Longitude ... but only on a particular territory
  3. Primitive types in a particular range
  4. A car license plate

A couple years ago I was working with Python on some localization tool when I needed a couple thousand sentences mixed with dates that had to look believable. My usual approach was to have a collection of lorem ipsums and some randomization thrown there but that time I decided look around a bit more and discovered the Faker Python Library. Rust has, at least, one version of the same library Fake(crate) / Fake(sources).

At the moment LO data model is not incredibly complex check my current definition for a generic event, for instance:

#[derive(Clone, Debug, Deserialize, Serialize, Dummy)]
pub struct Generic {
    #[dummy(faker = "0..5")]
    pub index: u16,
    #[dummy(faker = "Sentence(0..12)")]
    pub extra: String,
}

impl Generic {
    pub fn new_fake() -> Self {
        Faker.fake()
    }
}

and it's precisely that new_fake() what's being called from the settings page when we request the creation of fake data:

Fake data population

Persistence: using localStorage

Before going forward, I think it's good to repeat that I'm not a professional web developer. I program tools for a living. Native stuff. Desktop thingies. With that out of the way, let's talk about persistence.

This version of LO is a conscious simplification of the previous implementation. The first version had many moving parts as can be seen in the list under "sharing LillaOst". The goal for this version was to have exactly one artifact. Just a web app. But that made solving the persistence more complicated.

An option is to use Window.localStorage. yew_service makes it trivial to start using it. This approach has, at least, one problem: it's synchronous. As inelegant as it might be, for the volume of data I'm expecting to use in this version of LO, this hasn't been a real problem.

Performance

A group of colleagues have been testing the web app in different stages. I was particularly interested in the performance of the storage. That's why the self-testing features include a "Generate 6000 fake events". According to the feedback I got the web performance is completely acceptable or, at least, no one commented on it.

We've been focused on testing on mobile and the devices we used are mid-range to higher mid-range. A collection of old iPhones, a couple Nokias, Samsungs, and some Xiaomis. And not even one has reported bad performance. In any case, I want to turn the persistence async as soon as possible.

Conclusion and next steps

This article covers the main components of LillaOst's new implementation. It's been a long road with a ton of changes but I think I'm in a better position now to make this project evolve and cover my needs (and perhaps others too).

One of my goals when starting this push was to simplify the moving parts, to reduce complexity. I think it's fair to say that the project is simpler now. But almost to a fault. My main concern right now is that I've abandoned one of the most useful features: sharing the notes and events with someone else. In other words, I think the App is a bit too simple right now.

Sharing data through LillaOst

My original implementation was quite simplistic. A product of a period of extreme stress and urgent need. Everything was open and reachable from anywhere. It's more an appliance than a Web App. When transforming the concept of LillaOst into a shareable product, I'm facing a range of issues that are common in the "Web Development" world but are completely new for me. Browsers can be (and are) weird.

I went for Netlify after a friend of mine introduced it to me. We were working for an institutional client.