Skip to main content

max / makeover-layout

Name a chart: an axis and the bars read against it `Meter` is one proportion; a chart is a series of them sharing a maximum, and the shared maximum is the whole difference. Said as a run of meters each bar carries its own `total`, so the axis is stated once per bar with nothing holding the copies together -- and the shared maximum is the only reason two widths can be compared. Both integers are carried rather than the percentage an app computes from them, which is `Meter`'s own argument one layer out. It is also the only shape that survives a compiled template: a residual holds numbers the description hands a renderer, never ones a renderer works out from two of them, so a chart drawn from a supplied percentage could be described and could not be compiled. `Bar::at(place).of(value)` rather than one constructor taking both, because `quasi-declare` stages a constructor's plain arguments all one way and a place beside a magnitude would have the place standing in as a number.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 18:12 UTC
Signed with PGP, not checked
Commit: fa9af7a6ad92ec382ca28a03f76beabf0cd3e2c7
Parent: 8c8ba7d
3 files changed, +174 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.44.1"
3 + version = "0.44.2"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/figure.rs +172
@@ -299,3 +299,175 @@
299 299 (self.width > 0 && self.height > 0).then(|| self.width as f32 / self.height as f32)
300 300 }
301 301 }
302 +
303 + /// A run of magnitudes read against one axis.
304 + ///
305 + /// [`Meter`] is one proportion; this is a series of them that share a maximum,
306 + /// and the shared maximum is the whole difference. A run of meters draws each
307 + /// bar against its own `total`, so a chart said that way states the axis once
308 + /// per bar and nothing holds the copies together. Here the axis is stated once
309 + /// and a bar carries only where it sits on it.
310 + ///
311 + /// # Why the axis and not a percentage per bar
312 + ///
313 + /// [`Meter`]'s reason, one layer out. The app that drew MNW's revenue chart
314 + /// computed `revenue / most * 100.0` and put the percentage in the markup, and
315 + /// what reached the reader was a width with no numbers behind it: a bar at 100%
316 + /// because it is the largest and a bar at 100% because the axis is wrong are the
317 + /// same width and are not the same fact. Carrying both integers keeps the fact,
318 + /// and [`Bar::fraction`] is one call away for a renderer that wants the ratio.
319 + ///
320 + /// It is also the only shape that survives a compiled template. A residual holds
321 + /// numbers the description HANDS a renderer, never ones a renderer works out
322 + /// from two of them, so a chart drawn from a supplied percentage could be
323 + /// described and could not be compiled. See `quasi_router::stage::number_at`.
324 + ///
325 + /// # What is worded here and what is not
326 + ///
327 + /// [`Bar::at`] is where the bar sits on the axis and [`label`](Self::label) is
328 + /// what the magnitudes are, which is [`Meter::label`]'s split exactly. What
329 + /// differs is [`Bar::reading`] and [`Bar::note`]: both arrive already worded,
330 + /// because a magnitude's own units are the app's ("$42.10", not 4210) and a
331 + /// count's noun inflects ("1 sale", "3 sales"). A renderer that pluralised
332 + /// would be growing a lexer for one language.
333 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
334 + pub struct Chart<'a> {
335 + /// The magnitude the axis runs to. Every bar is read against this.
336 + ///
337 + /// Zero means there is no axis, not that every bar is full. A renderer draws
338 + /// nothing rather than dividing by it; see [`is_empty`](Self::is_empty).
339 + ///
340 + /// `usize` because that is what a description counts in -- a pager's offset
341 + /// and page size are the same -- and because it is the only width
342 + /// `quasi_router::stage::number_at` has a stand-in for, which is what lets a
343 + /// chart reach a compiled template at all.
344 + pub most: usize,
345 + /// What the magnitudes are: "revenue", "plays".
346 + ///
347 + /// The noun, not the unit and not the ratio. The unit is already in each
348 + /// [`Bar::reading`], where it belongs, because only the app knows it.
349 + pub label: Option<&'a str>,
350 + /// What the axis means, where it means anything.
351 + pub tone: Tone,
352 + }
353 +
354 + impl<'a> Chart<'a> {
355 + /// An axis running to `most`, untoned and unlabelled.
356 + #[must_use]
357 + pub const fn new(most: usize) -> Self {
358 + Self {
359 + most,
360 + label: None,
361 + tone: Tone::Neutral,
362 + }
363 + }
364 +
365 + /// What the magnitudes are.
366 + #[must_use]
367 + pub const fn label(mut self, label: &'a str) -> Self {
368 + self.label = Some(label);
369 + self
370 + }
371 +
372 + /// What the axis means.
373 + #[must_use]
374 + pub const fn tone(mut self, tone: Tone) -> Self {
375 + self.tone = tone;
376 + self
377 + }
378 +
379 + /// Whether there is an axis to read against.
380 + ///
381 + /// [`Meter::is_empty`]'s case: an axis running to zero is what an app with
382 + /// nothing to chart actually has, and saying so beats dividing by it.
383 + #[must_use]
384 + pub const fn is_empty(&self) -> bool {
385 + self.most == 0
386 + }
387 + }
388 +
389 + /// One magnitude in a [`Chart`], at its place on the axis.
390 + ///
391 + /// Carries no axis of its own on purpose: a bar read against a maximum it
392 + /// states itself is a meter, and a run of those is not a chart.
393 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
394 + pub struct Bar<'a> {
395 + /// Where on the axis this sits: "Mar 3", "Week 12".
396 + ///
397 + /// The position's own name rather than an index, for the reason a pager's
398 + /// jump carries its page number: what a chart shows is a window over a
399 + /// series, and an index into that window is not the point it names.
400 + pub at: &'a str,
401 + /// The magnitude, in the chart's units, read against [`Chart::most`].
402 + pub value: usize,
403 + /// The magnitude as the app words it: "$42.10".
404 + ///
405 + /// Worded rather than derived because the units are the app's. A renderer
406 + /// handed 4210 cannot know it is money, let alone which money.
407 + pub reading: Option<&'a str>,
408 + /// A second fact about this bar, already worded: "3 sales".
409 + ///
410 + /// Worded for the reason [`reading`](Self::reading) is, plus one of its own:
411 + /// a count's noun inflects with the count, and that is language rather than
412 + /// drawing.
413 + pub note: Option<&'a str>,
414 + }
415 +
416 + impl<'a> Bar<'a> {
417 + /// A place on the axis, with no magnitude on it yet.
418 + ///
419 + /// The magnitude arrives through [`of`](Self::of) rather than as a second
420 + /// argument, and that is not stylistic: `quasi-declare` stages a
421 + /// constructor's plain arguments all one way or all the other, so a
422 + /// constructor taking a place AND a magnitude would have the place standing
423 + /// in as a number. Split, the place is a value and `of` is a count, which is
424 + /// the same split a pager's `of` makes and the reason it is spelled the
425 + /// same.
426 + #[must_use]
427 + pub const fn at(at: &'a str) -> Self {
428 + Self {
429 + at,
430 + value: 0,
431 + reading: None,
432 + note: None,
433 + }
434 + }
435 +
436 + /// How far up the axis this bar reaches.
437 + #[must_use]
438 + pub const fn of(mut self, value: usize) -> Self {
439 + self.value = value;
440 + self
441 + }
442 +
443 + /// How the app words this magnitude.
444 + #[must_use]
445 + pub const fn reading(mut self, reading: &'a str) -> Self {
446 + self.reading = Some(reading);
447 + self
448 + }
449 +
450 + /// A second fact about the bar, already worded.
451 + #[must_use]
452 + pub const fn note(mut self, note: &'a str) -> Self {
453 + self.note = Some(note);
454 + self
455 + }
456 +
457 + /// How far up the axis this bar reaches, 0.0 to 1.0, clamped.
458 + ///
459 + /// For drawing, which is what a clamped number is good for, and for the two
460 + /// renderers that draw in cells and pixels rather than in CSS. An empty axis
461 + /// reads as 0.0 rather than dividing by zero.
462 + ///
463 + /// A bar over [`Chart::most`] clamps, and unlike [`Meter`] that is not a
464 + /// fact being lost: `most` is the maximum of the bars, so a bar above it is
465 + /// an axis the app got wrong rather than an over-run worth drawing.
466 + #[must_use]
467 + pub fn fraction(&self, chart: &Chart<'_>) -> f32 {
468 + if chart.most == 0 {
469 + return 0.0;
470 + }
471 + (self.value as f64 / chart.most as f64).min(1.0) as f32
472 + }
473 + }
M src/lib.rs +1 -1
@@ -250,7 +250,7 @@
250 250 pub use depth::{Bevel, Depth, Edge, Fill, State};
251 251 pub use facet::{Facet, FacetValue, Selecting, Standing};
252 252 pub use field::{Accepted, DATE_FORMAT, DATETIME_FORMAT, Family, Field, FieldKind};
253 - pub use figure::{Extent, Figure, Fit, Meter};
253 + pub use figure::{Bar, Chart, Extent, Figure, Fit, Meter};
254 254 pub use readiness::{Awaiting, Loading, Readiness};
255 255 pub use region::{Arrangement, Measure, Paging, Region, Share, Showing, Window};
256 256 pub use text::{Change, Syntax};