//! A described chart, derived and filled. //! //! The fourth shape here written for a feature rather than ported from a //! screen, and it exists for [`crate::paged`]'s reason: the feature is what //! makes the screen derivable at all. MNW's `/dashboard/tabs/analytics` drew //! its revenue chart as markup the handler built and the renderer mounted, so //! there was nowhere in a residual for it to go, and it was the one described //! screen left off the seam. //! //! # What had to be true for a chart to compile //! //! The handler computed `revenue / most * 100.0` and put the percentage in the //! markup. A residual holds numbers the description HANDS a renderer and never //! ones a renderer works out from two of them -- `stage::number_at` says so -- //! so a chart drawn from a supplied percentage could be described and could not //! be compiled: the derivation would find the arithmetic where it wanted a //! stand-in, and bake one request's bar heights into the template. //! //! So the member carries both integers and neither renderer divides. The //! webview prints `--most` once on the container and `--value` once per bar, //! and the stylesheet is where they become a height. That is what this file //! proves: the two numbers come back as holes, in the right places, at every //! shape the data takes. //! //! # The axis is outside the loop and the bars are inside it //! //! Which is the second thing worth proving. `most` is one fact about the chart //! and a request varies it; each bar's `value` is one per pass of a loop whose //! length a request also varies. A residual holds those as a hole and a loop //! body, and getting them the other way round -- the axis repeated per bar -- //! would serve the first reader's maximum to everybody after a request that //! changed it. // The fixture data below is built by this module's tests and by nothing else: // the bench itself measures the shapes, not the rows behind them. #![allow(dead_code)] use quasi_declare::declare; use quasi_router::{Bar, Chart}; /// What the screen read, as the screen needs it. pub(crate) struct Takings { pub bars: Vec, pub most: usize, } /// One bucket of revenue: a day, what came in, and how many sales it took. pub(crate) struct Bucket { pub day: String, pub cents: usize, pub money: String, pub sales: String, } impl Takings { /// `days` buckets, the last of them the largest. pub(crate) fn new(days: usize) -> Self { let bars: Vec = (0..days) .map(|n| { let cents = (n + 1) * 137; Bucket { day: format!("Mar {}", n + 1), cents, money: format!("${}.{:02}", cents / 100, cents % 100), sales: if n == 0 { "1 sale".to_string() } else { format!("{} sales", n + 1) }, } }) .collect(); let most = bars.iter().map(|bar| bar.cents).max().unwrap_or(0); Self { bars, most } } /// The degenerate axis: buckets that all came to nothing. /// /// Sayable on purpose, and the case a percentage could not survive -- the /// old markup divided by it and wrote `--fill: NaN%`. pub(crate) fn flat(days: usize) -> Self { let mut takings = Self::new(days); for bar in &mut takings.bars { bar.cents = 0; } takings.most = 0; takings } } declare! { /// Revenue over time, as bars against the largest day. #[staged] pub(crate) shape takings(read: &Takings) -> Node; chart Chart::new(read.most) { for bucket in read.bars.iter() { bar Bar::at(bucket.day.clone()) .of(bucket.cents) .reading(bucket.money.clone()) .note(bucket.sales.clone()); } } } #[cfg(test)] mod tests { use quasi_http::Serves as _; use quasi_router::stage::{Op, Plan, Residual}; use quasi_webview::Webview; use super::*; fn residual() -> Residual { quasi_webview::stage::derive(&Webview::new(), takings_staged) } /// The axis is a hole at the top level and the bars are a loop, which is /// the arrangement the whole member is shaped for. Stated as a shape test /// rather than left to the filling tests below, because the two would still /// agree if the axis were repeated inside the loop -- they only ever fill /// one chart at a time -- and it would be wrong. #[test] fn the_axis_is_outside_the_loop_and_the_bars_are_inside_it() { let residual = residual(); let outside = residual .ops() .iter() .filter(|op| matches!(op, Op::Hole { .. })) .count(); assert_eq!( outside, 1, "the axis is the one hole beside the loop: {:#?}", residual.ops() ); let loops: Vec<&[Op]> = residual .ops() .iter() .filter_map(|op| match op { Op::Loop(body) => Some(&**body), _ => None, }) .collect(); let [body] = loops.as_slice() else { panic!("one loop, for the bars: {:#?}", residual.ops()); }; // The place, the magnitude and the two worded facts, which is every // fact a bar carries. assert_eq!( body.iter() .filter(|op| matches!(op, Op::Hole { .. })) .count(), 4, "a bar's four holes: {body:#?}" ); } /// No percentage reaches the template. This is the property the member /// exists for, so it is asserted on the derived bytes rather than inferred /// from the design. #[test] fn the_residual_holds_no_computed_width() { fn literals(ops: &[Op], out: &mut String) { for op in ops { match op { Op::Lit(text) => out.push_str(text), Op::Branch(body) | Op::Loop(body) => literals(body, out), Op::Arms(arms) => { for arm in arms.iter() { literals(arm, out); } } Op::Hole { .. } => {} } } } let mut baked = String::new(); literals(residual().ops(), &mut baked); assert!(!baked.contains('%'), "a width was baked in: {baked}"); } /// One marker for a hole of either kind, so a render and a replay can be /// compared on their literals and their structure. /// /// A chart is the first shape here holding both kinds at once -- a place and /// two readings stand in as text, a magnitude and the axis as numbers -- and /// `Op::Hole` does not record which it was, deliberately: a filler answers /// by position and never needs to know. So a replay cannot reproduce the /// render's exact bytes at a hole, and normalising both sides is what the /// test can honestly ask for. What it is checking is the literals between /// the holes and the shape around them, which is the whole of what a /// residual adds. const HOLE: &str = "\u{0}"; /// Every stand-in in a render, replaced by [`HOLE`]. fn without_stand_ins(render: &str) -> String { let mut out = String::with_capacity(render.len()); let mut rest = render; loop { let sentinel = quasi_router::stage::find_sentinel(rest); let number = quasi_router::stage::find_number(rest); let (at, width) = match (sentinel, number) { (Some(s), Some(n)) if s <= n => (s, quasi_router::stage::SENTINEL_LEN), (Some(_), Some(n)) => (n, quasi_router::stage::NUMBER_LEN), (Some(s), None) => (s, quasi_router::stage::SENTINEL_LEN), (None, Some(n)) => (n, quasi_router::stage::NUMBER_LEN), (None, None) => { out.push_str(rest); return out; } }; out.push_str(&rest[..at]); out.push_str(HOLE); rest = &rest[at + width..]; } } #[test] fn replaying_the_residual_reproduces_the_staged_render() { let webview = Webview::new(); let residual = residual(); fn replay(ops: &[Op], bars: usize, out: &mut String) { for op in ops { match op { Op::Lit(text) => out.push_str(text), Op::Hole { .. } => out.push_str(HOLE), Op::Branch(body) => replay(body, bars, out), Op::Arms(arms) => replay(&arms[0], bars, out), Op::Loop(body) => { for _ in 0..bars { replay(body, bars, out); } } } } } for bars in [1, 2, 7] { let mut replayed = String::new(); replay(residual.ops(), bars, &mut replayed); assert_eq!( without_stand_ins(&webview.fragment(&takings_staged(&Plan::full(bars)))), replayed, "the residual and the renderer disagree at {bars} bars" ); } } /// The two kinds of stand-in land where they belong: the axis and the /// magnitude as numbers, the place and the two readings as text. /// /// This is what the normalising above gives up, asserted separately rather /// than lost. A magnitude standing in as text would reach the markup as /// `ZQH..HQZ` inside a `--value`, which is not a number and would draw /// nothing. #[test] fn a_magnitude_stands_in_as_a_number_and_a_place_as_text() { let render = Webview::new().fragment(&takings_staged(&Plan::full(1))); for (property, wants_number) in [("--most: ", true), ("--value: ", true)] { let at = render.find(property).expect(property) + property.len(); assert_eq!( quasi_router::stage::read_number(&render[at..]).is_some(), wants_number, "{property} in {render}" ); } let label = render.find("chart-bar-label\">").expect("the label") + "chart-bar-label\">".len(); assert!( quasi_router::stage::read_sentinel(&render[label..]).is_some(), "a place is text: {render}" ); } /// Byte for byte against the renderer, across every shape the data takes: /// no bars, one, several, and the axis that came to nothing. #[test] fn a_filled_chart_is_what_the_renderer_would_have_produced() { let webview = Webview::new(); let residual = residual(); for days in [0, 1, 2, 7, 31] { let read = Takings::new(days); assert_eq!( webview.fragment(&takings(&read)), takings_serve(&residual, &read), "{days} days" ); let flat = Takings::flat(days); assert_eq!( webview.fragment(&takings(&flat)), takings_serve(&residual, &flat), "{days} days, all of them nothing" ); } } /// A magnitude larger than any number a screen counts still fills as /// itself. The numeric stand-in is a sixteen-digit value chosen to be /// bigger than a real count, and revenue in cents is the member most likely /// to reach for that room. #[test] fn a_large_magnitude_is_not_mistaken_for_a_stand_in() { let webview = Webview::new(); let residual = residual(); let mut read = Takings::new(3); read.bars[2].cents = 9_090_905_000_100_002; read.most = read.bars[2].cents; assert_eq!( webview.fragment(&takings(&read)), takings_serve(&residual, &read) ); } }