1use crate::numfmt::{CellFormatter, NumberFormat, NumberFormatSettings};
6use polars::prelude::{AnyValue, DataType, Schema};
7
8const MAX_AXIS_PLACES: i32 = 6;
10const MAX_MANTISSA_PLACES: i32 = 12;
12const SCIENTIFIC_FROM: f64 = 1e15;
14
15#[derive(Clone, Debug, Default, PartialEq)]
18pub struct AxisNumbers {
19 pub format: NumberFormat,
20 pub whole: bool,
21}
22
23impl AxisNumbers {
24 pub fn column(settings: &NumberFormatSettings, schema: Option<&Schema>, column: &str) -> Self {
26 match schema.and_then(|s| s.get(column)) {
27 Some(dtype) => Self {
28 format: table_number_format(settings, column, dtype),
29 whole: dtype.is_integer(),
30 },
31 None => Self::default(),
32 }
33 }
34
35 pub fn columns(
37 settings: &NumberFormatSettings,
38 schema: Option<&Schema>,
39 columns: &[String],
40 ) -> Self {
41 let mut each = columns.iter().map(|c| Self::column(settings, schema, c));
42 let Some(first) = each.next() else {
43 return Self::default();
44 };
45 let whole = first.whole && each.all(|n| n.whole);
46 Self { whole, ..first }
47 }
48
49 pub fn count(settings: &NumberFormatSettings) -> Self {
51 Self {
52 format: table_number_format(settings, "Count", &DataType::UInt64),
53 whole: true,
54 }
55 }
56
57 pub fn measure(settings: &NumberFormatSettings, name: &str) -> Self {
59 Self {
60 format: table_number_format(settings, name, &DataType::Float64),
61 whole: false,
62 }
63 }
64
65 pub fn write(&self, v: f64) -> String {
67 write(&self.format, v, Notation::Table, 0.0)
68 }
69
70 pub fn fractional(self) -> Self {
72 Self {
73 whole: false,
74 ..self
75 }
76 }
77}
78
79#[derive(Clone, Debug)]
83pub struct AxisFormat {
84 format: NumberFormat,
85 full: Notation,
86 short: Option<Notation>,
88 zero_below: f64,
91}
92
93#[derive(Clone, Copy, Debug, PartialEq)]
94enum Notation {
95 Fixed {
97 places: usize,
98 unit: f64,
99 suffix: &'static str,
100 },
101 Scientific { places: usize },
103 Prefixed,
107 Table,
110}
111
112impl AxisFormat {
113 pub fn new(ticks: &[f64], numbers: &AxisNumbers) -> Self {
115 let ticks: Vec<f64> = ticks.iter().copied().filter(|v| v.is_finite()).collect();
116 let top = ticks.iter().fold(0.0_f64, |top, v| top.max(v.abs()));
117 let gap = ticks
119 .windows(2)
120 .map(|w| (w[1] - w[0]).abs())
121 .filter(|gap| *gap > 0.0)
122 .fold(f64::INFINITY, f64::min);
123 let places = if top >= SCIENTIFIC_FROM {
124 None
125 } else if numbers.whole {
126 Some(0)
127 } else {
128 fixed_places(&ticks, top, gap)
129 };
130 let (full, short) = match places {
131 Some(places) => (
132 Notation::Fixed {
133 places,
134 unit: 1.0,
135 suffix: "",
136 },
137 short_notation(&ticks, top, gap),
138 ),
139 None => {
140 let apart = if gap.is_finite() && top > 0.0 {
142 (magnitude(top) - magnitude(gap)).clamp(0, MAX_MANTISSA_PLACES) as usize
143 } else {
144 0
145 };
146 (
147 Notation::Scientific {
148 places: apart.max(2),
149 },
150 Some(Notation::Scientific { places: apart }),
151 )
152 }
153 };
154 Self {
155 format: numbers.format.clone(),
156 full,
157 short,
158 zero_below: if gap.is_finite() { gap * 1e-9 } else { 0.0 },
159 }
160 }
161
162 pub fn log(ticks: &[f64], numbers: &AxisNumbers) -> Self {
168 let ticks: Vec<f64> = ticks.iter().copied().filter(|v| v.is_finite()).collect();
169 let top = ticks.iter().fold(0.0_f64, |top, v| top.max(v.abs()));
170 let full = if top >= SCIENTIFIC_FROM {
171 Notation::Scientific { places: 0 }
172 } else {
173 Notation::Fixed {
174 places: fewest_places(&ticks, 1.0, 0, MAX_AXIS_PLACES),
175 unit: 1.0,
176 suffix: "",
177 }
178 };
179 Self {
180 format: numbers.format.clone(),
181 full,
182 short: (1e3..SCIENTIFIC_FROM)
183 .contains(&top)
184 .then_some(Notation::Prefixed),
185 zero_below: 0.0,
186 }
187 }
188
189 pub fn ends_and_middle([lo, hi]: [f64; 2], numbers: &AxisNumbers) -> Self {
192 Self::new(&[lo, (lo + hi) / 2.0, hi], numbers)
193 }
194
195 pub fn label(&self, v: f64, level: usize) -> Option<String> {
198 let notation = match level {
199 0 => self.full,
200 1 => self.short?,
201 _ => return None,
202 };
203 Some(write(&self.format, v, notation, self.zero_below))
204 }
205}
206
207fn write(format: &NumberFormat, v: f64, notation: Notation, zero_below: f64) -> String {
210 let (places, unit, suffix) = match notation {
211 Notation::Scientific { places } => {
212 let v = if v.abs() < zero_below { 0.0 } else { v };
213 return scientific(v, places, format.decimal_sep);
214 }
215 Notation::Table => {
216 let mut out = String::new();
217 if v.fract() == 0.0 || format.float_precision.is_some() || !v.is_finite() {
218 format.write_f64(v, &mut String::new(), &mut out);
219 } else {
220 format.regroup_decimal(&AnyValue::Float64(v).str_value(), &mut out);
221 }
222 return out;
223 }
224 Notation::Fixed { .. } | Notation::Prefixed if !v.is_finite() => {
225 return v.to_string();
226 }
227 Notation::Prefixed => {
228 let (unit, suffix) = [(1e12, "T"), (1e9, "G"), (1e6, "M"), (1e3, "k")]
229 .into_iter()
230 .find(|(unit, _)| v.abs() >= *unit)
231 .unwrap_or((1.0, ""));
232 let places = fewest_places(&[v], unit, 0, 2);
233 (places, unit, suffix)
234 }
235 Notation::Fixed {
236 places,
237 unit,
238 suffix,
239 } => (places, unit, suffix),
240 };
241 let fixed = NumberFormat {
242 float_precision: Some(places as u8),
243 ..format.clone()
244 };
245 let mut out = String::new();
246 fixed.write_f64(v / unit, &mut String::new(), &mut out);
247 if !out.chars().any(|c| matches!(c, '1'..='9')) {
250 if unit > 1.0 {
251 return "0".to_string();
252 }
253 out.retain(|c| c != '-');
254 }
255 out.push_str(suffix);
256 out
257}
258
259fn fixed_places(ticks: &[f64], top: f64, gap: f64) -> Option<usize> {
264 let figures = if top > 0.0 { 2 - magnitude(top) } else { 0 };
265 let apart = if gap.is_finite() { -magnitude(gap) } else { 0 };
266 let places = figures.max(apart).max(0);
267 (places <= MAX_AXIS_PLACES).then(|| fewest_places(ticks, 1.0, apart.max(0), places))
268}
269
270fn fewest_places(ticks: &[f64], unit: f64, least: i32, most: i32) -> usize {
273 let exact = |places: i32| {
274 ticks.iter().all(|v| {
275 let scaled = v / unit * 10f64.powi(places);
276 (scaled - scaled.round()).abs() <= 1e-9 * scaled.abs().max(1.0)
278 })
279 };
280 (least..most).find(|&p| exact(p)).unwrap_or(most) as usize
281}
282
283fn short_notation(ticks: &[f64], top: f64, gap: f64) -> Option<Notation> {
288 let (unit, suffix) = [(1e12, "T"), (1e9, "G"), (1e6, "M"), (1e3, "k")]
289 .into_iter()
290 .find(|(unit, _)| top >= *unit)?;
291 let figures = 1 - magnitude(top / unit);
292 let apart = if gap.is_finite() {
293 -magnitude(gap / unit)
294 } else {
295 0
296 };
297 let most = figures.max(apart).clamp(0, 2);
298 Some(Notation::Fixed {
299 places: fewest_places(ticks, unit, apart.clamp(0, most), most),
300 unit,
301 suffix,
302 })
303}
304
305fn magnitude(v: f64) -> i32 {
309 (v.log10() + 1e-9).floor() as i32
310}
311
312fn scientific(v: f64, places: usize, decimal_sep: char) -> String {
314 let v = if v == 0.0 { 0.0 } else { v };
316 let text = format!("{v:.places$e}");
317 if decimal_sep == '.' {
318 text
319 } else {
320 text.replacen('.', decimal_sep.encode_utf8(&mut [0; 4]), 1)
321 }
322}
323
324pub fn table_number_format(
326 settings: &NumberFormatSettings,
327 column: &str,
328 dtype: &DataType,
329) -> NumberFormat {
330 match settings.formatter_for(column, dtype) {
331 CellFormatter::Number(format) => format,
332 CellFormatter::Passthrough => NumberFormat::PLAIN,
333 }
334}
335
336pub fn format_bar_value(v: f64, integer: bool, format: &NumberFormat) -> String {
341 let places = format.float_precision.unwrap_or(2);
342 let smallest = 0.5 * 10f64.powi(-i32::from(places));
343 let notation =
344 if !v.is_finite() || v.abs() >= 1e15 || (!integer && v != 0.0 && v.abs() < smallest) {
345 Notation::Scientific { places: 2 }
346 } else {
347 Notation::Fixed {
348 places: if integer { 0 } else { places.into() },
349 unit: 1.0,
350 suffix: "",
351 }
352 };
353 write(format, v, notation, 0.0)
354}
355
356#[cfg(test)]
357mod tests;