1use gpui::SharedString;
4
5use crate::time;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct Linear {
10 pub domain: (f64, f64),
11 pub range: (f32, f32),
12}
13
14impl Linear {
15 pub fn at(&self, value: f64) -> f32 {
16 let (d0, d1) = self.domain;
17 let t = if d1 == d0 {
18 0.5
19 } else {
20 (value - d0) / (d1 - d0)
21 };
22 self.range.0 + (self.range.1 - self.range.0) * t as f32
23 }
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
29pub struct Band {
30 pub count: usize,
31 pub range: (f32, f32),
32 pub padding: f32,
33}
34
35impl Band {
36 pub fn step(&self) -> f32 {
37 (self.range.1 - self.range.0) / self.count.max(1) as f32
38 }
39
40 pub fn start(&self, index: usize) -> f32 {
42 self.range.0 + self.step() * (index as f32 + self.padding / 2.0)
43 }
44
45 pub fn width(&self) -> f32 {
47 self.step() * (1.0 - self.padding)
48 }
49
50 pub fn center(&self, index: usize) -> f32 {
51 self.range.0 + self.step() * (index as f32 + 0.5)
52 }
53
54 pub fn index(&self, position: f32) -> Option<usize> {
56 let index = ((position - self.range.0) / self.step()).floor();
57 (index >= 0.0 && (index as usize) < self.count).then_some(index as usize)
58 }
59}
60
61#[derive(Clone, Debug, PartialEq)]
63pub struct Ticks {
64 pub domain: (f64, f64),
65 pub values: Vec<f64>,
66 pub labels: Vec<SharedString>,
67}
68
69pub fn linear(lo: f64, hi: f64, count: usize, nice: bool) -> Ticks {
73 let (lo, hi) = widen(lo, hi, 1.0);
74 let (factor, power) = increment(lo, hi, count);
75 let value = |index: f64| match power < 0 {
76 true => index / (10f64.powi(-power) / factor),
77 false => index * factor * 10f64.powi(power),
78 };
79 let step = value(1.0);
80 let (first, last) = match nice {
81 true => ((lo / step).floor(), (hi / step).ceil()),
82 false => ((lo / step - 1e-9).ceil(), (hi / step + 1e-9).floor()),
83 };
84 let values: Vec<f64> = (first as i64..=last as i64)
85 .map(|index| value(index as f64))
86 .collect();
87 let magnitude = lo.abs().max(hi.abs());
88 let labels = values
89 .iter()
90 .map(|&v| number(v, power, magnitude).into())
91 .collect();
92 Ticks {
93 domain: match nice {
94 true => (value(first), value(last)),
95 false => (lo, hi),
96 },
97 values,
98 labels,
99 }
100}
101
102pub fn temporal(lo: f64, hi: f64, count: usize) -> Ticks {
104 let (lo, hi) = widen(lo, hi, time::DAY);
105 let (values, unit) = time::ticks(lo, hi, count);
106 let labels = values
107 .iter()
108 .map(|&ms| time::label(ms, unit).into())
109 .collect();
110 Ticks {
111 domain: (lo, hi),
112 values,
113 labels,
114 }
115}
116
117fn widen(lo: f64, hi: f64, unit: f64) -> (f64, f64) {
118 match (lo == hi, lo == 0.0) {
119 (false, _) => (lo, hi),
120 (true, true) => (0.0, unit),
121 (true, false) => (
122 lo - unit.max(lo.abs() / 10.0),
123 hi + unit.max(hi.abs() / 10.0),
124 ),
125 }
126}
127
128fn increment(lo: f64, hi: f64, count: usize) -> (f64, i32) {
130 let raw = (hi - lo) / count.max(1) as f64;
131 let mut power = raw.log10().floor() as i32;
132 let error = raw / 10f64.powi(power);
133 let factor = if error >= 50f64.sqrt() {
134 10.0
135 } else if error >= 10f64.sqrt() {
136 5.0
137 } else if error >= 2f64.sqrt() {
138 2.0
139 } else {
140 1.0
141 };
142 if factor == 10.0 {
143 power += 1;
144 return (1.0, power);
145 }
146 (factor, power)
147}
148
149fn number(value: f64, power: i32, magnitude: f64) -> String {
152 const UNITS: [(f64, &str, i32); 4] =
153 [(1e12, "T", 12), (1e9, "G", 9), (1e6, "M", 6), (1e3, "k", 3)];
154 if magnitude >= 1e4
155 && value != 0.0
156 && let Some(&(unit, suffix, exponent)) = UNITS.iter().find(|(unit, ..)| magnitude >= *unit)
157 {
158 let decimals = (exponent - power).max(0) as usize;
159 return format!("{}{suffix}", fixed(value / unit, decimals));
160 }
161 grouped(&fixed(value, (-power).max(0) as usize))
162}
163
164pub fn value(value: f64) -> String {
167 if value.is_nan() {
168 return "–".into();
169 }
170 if value.fract() == 0.0 && value.abs() < 1e15 {
171 return grouped(&fixed(value, 0));
172 }
173 let significant = 3 - value.abs().log10().floor() as i32 - 1;
174 let decimals = significant.clamp(2, 12) as usize;
175 let text = fixed(value, decimals);
176 let text = text.trim_end_matches('0').trim_end_matches('.');
177 grouped(text)
178}
179
180fn fixed(value: f64, decimals: usize) -> String {
181 let text = format!("{value:.decimals$}");
182 match text
183 .trim_start_matches('-')
184 .chars()
185 .all(|c| c == '0' || c == '.')
186 {
187 true => text.trim_start_matches('-').to_string(),
188 false => text,
189 }
190}
191
192fn grouped(text: &str) -> String {
194 let (sign, rest) = match text.strip_prefix('-') {
195 Some(rest) => ("-", rest),
196 None => ("", text),
197 };
198 let (whole, fraction) = rest.split_at(rest.find('.').unwrap_or(rest.len()));
199 let mut out = String::with_capacity(text.len() + whole.len() / 3);
200 out.push_str(sign);
201 for (index, digit) in whole.chars().enumerate() {
202 if index > 0 && (whole.len() - index) % 3 == 0 {
203 out.push(',');
204 }
205 out.push(digit);
206 }
207 out.push_str(fraction);
208 out
209}