1use crate::levels::{ending_on, signed_whole, Prices, Rows, Units};
2use crate::money::{none, rounded, unit};
3use crate::table::{at_or_before, union_keys};
4use patterns_macros::{because, source};
5
6pub type Point = (u64, f64);
7
8pub type NamedLine = (String, Vec<Point>);
9
10pub fn named_lines<T, L: Copy>(items: &[(String, T)], line: L, read: fn(&T, L) -> Vec<Point>) -> Vec<NamedLine> {
11 items
12 .iter()
13 .map(|(name, item)| (name.clone(), read(item, line)))
14 .filter(|(_, points)| !points.is_empty())
15 .collect()
16}
17because!(named_lines, "the same line read off every named thing in a list, with anything that has no points left out, which is one chart; the reader is a parameter because what a line means differs per subject while gathering them into a chart never does");
18because!(NamedLine, "one line of a chart as a name and its points, the shape every chart in this library is built from and handed on in, named once so a signature says what it carries rather than spelling the pair out again");
19
20pub fn point_on(points: &[Point], day: u64) -> f64 {
21 points
22 .iter()
23 .rev()
24 .find(|(d, _)| *d <= day)
25 .map(|(_, v)| *v)
26 .unwrap_or(none())
27}
28because!(point_on, "a charted series read at a day, carried forward from its newest point at or before it, zero before it starts, which is how a report reads a chart at a month end; read from the newest backwards so it stops at the point that answers rather than walking the whole series to keep the last one that did");
29
30pub fn pie_slices(amounts: &[f64]) -> Vec<(f64, f64, f64)> {
31 let total: f64 = amounts.iter().map(|a| a.max(none())).sum();
32 if total <= none() {
33 return vec![];
34 }
35 let mut start = none();
36 amounts
37 .iter()
38 .map(|amount| {
39 let fraction = amount.max(none()) / total;
40 let end = start + fraction;
41 let out = (fraction, start, end);
42 start = end;
43 out
44 })
45 .collect()
46}
47because!(pie_slices, "each amount's share of the whole and where its slice starts and ends around the circle, negatives counted as nothing, or no slices when there is nothing to share");
48
49pub fn axis_ceiling(max: f64, headroom: f64, steps: &[f64]) -> f64 {
50 let raised = max.max(unit()) * headroom;
51 let magnitude = DECIMAL_BASE_POWER.powf(raised.log10().floor());
52 steps
53 .iter()
54 .map(|step| magnitude * step)
55 .find(|candidate| *candidate >= raised)
56 .unwrap_or(magnitude * DECIMAL_BASE_POWER)
57}
58because!(axis_ceiling, "the round figure above the tallest bar the axis ends at, the smallest round multiple of a power of ten that clears the bar with its headroom");
59
60const DECIMAL_BASE_POWER: f64 = 10.0;
61because!(DECIMAL_BASE_POWER, ChartConventions, "the base an axis ceiling is rounded in, ten, since readers count in tens");
62
63pub struct ChartConventions;
64source!(ChartConventions, "the drawing rules the retired client used for its bar chart, the one-two-five axis and the pixel floor under a bar");
65
66pub fn bar_height(value: f64, ceiling: f64, height: f64, floor: f64) -> f64 {
67 (value / ceiling * height).max(floor)
68}
69because!(bar_height, "a bar's height in pixels against the axis ceiling, never under the floor that keeps a small bar visible");
70
71pub fn unit_fraction(value: f64, whole: f64) -> f64 {
72 match whole > none() {
73 true => value / whole,
74 false => none(),
75 }
76}
77because!(unit_fraction, "a part over a whole as a plain fraction, or nothing over an empty whole");
78
79pub fn latest_time(lines: &[Vec<Point>]) -> Option<u64> {
80 lines.iter().flat_map(|line| line.iter().map(|(day, _)| *day)).max()
81}
82because!(latest_time, "the newest moment across every line of a chart, the right edge the chart opens on");
83
84pub fn window_from(latest: Option<u64>, days: u64, day_seconds: u64) -> Option<u64> {
85 latest.map(|end| end.saturating_sub(days.saturating_mul(day_seconds)))
86}
87because!(window_from, "the left edge a chart opens on, a count of days back from its newest moment, or nothing when the chart has no points");
88
89pub fn points_of_lines(lines: &[(String, Vec<Point>)]) -> Vec<Vec<Point>> {
90 lines.iter().map(|(_, points)| points.clone()).collect()
91}
92because!(points_of_lines, "the points of every named line without their names, for a reading that spans the lines");
93
94pub fn time_ordered(lines: &[(String, Vec<Point>)]) -> Vec<(String, Vec<Point>)> {
95 lines
96 .iter()
97 .map(|(name, points)| {
98 let mut ordered = points.clone();
99 ordered.sort_by_key(|(day, _)| *day);
100 (name.clone(), ordered)
101 })
102 .collect()
103}
104because!(time_ordered, "every named line with its points oldest first, which a chart insists on, while a report reads its month rows newest first");
105
106pub fn ending_points(rows: &Rows, units: Units, decimals: u32) -> Vec<Point> {
107 rows.iter()
108 .map(|(day, row)| (*day, rounded(signed_whole(row.ending_amount, units), decimals)))
109 .collect()
110}
111because!(ending_points, "a series' level at the end of each of its days as tokens, the line a level chart draws");
112
113pub fn remaining_points(minted: &Rows, burned: &Rows, units: Units, decimals: u32) -> Vec<Point> {
114 union_keys(&[minted, burned])
115 .into_iter()
116 .map(|day| (day, rounded(ending_on(minted, day, units) - ending_on(burned, day, units), decimals)))
117 .collect()
118}
119because!(remaining_points, "what remains of a supply on each day either series moved, minted less burned with both carried forward");
120
121pub fn walked_usd_points(rows: &Rows, prices: &Prices, min_day: u64, units: Units, decimals: u32) -> Vec<Point> {
122 let mut out = Vec::new();
123 for (day, row) in rows {
124 if *day <= min_day {
125 continue;
126 }
127 let price = match at_or_before(prices, *day) {
128 Some(p) => p,
129 None => continue,
130 };
131 out.push((*day, rounded(signed_whole(row.amount, units) * price, decimals)));
132 }
133 out
134}
135because!(walked_usd_points, "a walked series revalued in dollars on each of its days at that day's carried price, from the first day after a floor and only where a price exists");
136
137pub fn after_day(points: Vec<Point>, min_day: u64) -> Vec<Point> {
138 points.into_iter().filter(|(day, _)| *day > min_day).collect()
139}
140because!(after_day, "a series cut to the days after a floor, for a token whose early days the dashboard does not chart");
141
142pub fn count_points(rows: &Rows) -> Vec<Point> {
143 rows.iter().map(|(day, row)| (*day, row.ending_count as f64)).collect()
144}
145because!(count_points, "a series' ending count on each of its days, the line the validator charts draw");
146
147pub fn share_of_supply_points(part: &Rows, minted: &Rows, burned: &Rows, units: Units, percent: f64, decimals: u32) -> Vec<Point> {
148 let mut out = Vec::new();
149 for (day, row) in part {
150 let supply = ending_on(minted, *day, units) - ending_on(burned, *day, units);
151 if supply <= none() {
152 continue;
153 }
154 out.push((*day, rounded((signed_whole(row.ending_amount, units) / supply) * percent, decimals)));
155 }
156 out
157}
158because!(share_of_supply_points, "a series as a share of the supply on each of its days, minted less burned carried to the day, skipping days with no supply");
159
160pub fn fixed_share_points(locked: &Rows, supply: f64, min_day: u64, units: Units, percent: f64, decimals: u32) -> Vec<Point> {
161 if supply <= none() {
162 return vec![];
163 }
164 locked
165 .iter()
166 .filter(|(day, _)| **day > min_day)
167 .map(|(day, row)| (*day, rounded((signed_whole(row.ending_amount, units) / supply) * percent, decimals)))
168 .collect()
169}
170because!(fixed_share_points, "a series on each day after a floor as a share of one fixed figure rather than of a moving one, so the line tells a story about the series and not about the figure");