| 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 |
+ |
}
|