ggplot_rs/scale/
transform.rs1use crate::data::Value;
2
3#[derive(Clone, Debug)]
5#[non_exhaustive]
6pub enum ScaleTransform {
7 Identity,
8 Log10,
9 Log2,
10 Ln,
11 Sqrt,
12 Reverse,
13 Logit,
15 Probit,
17 PseudoLog,
19 Reciprocal,
21 Exp,
23 BoxCox(f64),
25}
26
27impl ScaleTransform {
28 pub fn apply(&self, value: f64) -> f64 {
30 match self {
31 ScaleTransform::Identity => value,
32 ScaleTransform::Log10 => {
33 if value > 0.0 {
34 value.log10()
35 } else {
36 f64::NEG_INFINITY
37 }
38 }
39 ScaleTransform::Log2 => {
40 if value > 0.0 {
41 value.log2()
42 } else {
43 f64::NEG_INFINITY
44 }
45 }
46 ScaleTransform::Ln => {
47 if value > 0.0 {
48 value.ln()
49 } else {
50 f64::NEG_INFINITY
51 }
52 }
53 ScaleTransform::Sqrt => {
54 if value >= 0.0 {
55 value.sqrt()
56 } else {
57 f64::NAN
58 }
59 }
60 ScaleTransform::Reverse => -value,
61 ScaleTransform::Logit => {
62 if value <= 0.0 {
63 f64::NEG_INFINITY
64 } else if value >= 1.0 {
65 f64::INFINITY
66 } else {
67 (value / (1.0 - value)).ln()
68 }
69 }
70 ScaleTransform::Probit => qnorm(value),
71 ScaleTransform::PseudoLog => (value / 2.0).asinh(),
72 ScaleTransform::Reciprocal => {
73 if value != 0.0 {
74 1.0 / value
75 } else {
76 f64::NAN
77 }
78 }
79 ScaleTransform::Exp => value.exp(),
80 ScaleTransform::BoxCox(lambda) => {
81 if value <= 0.0 {
82 f64::NAN
83 } else if lambda.abs() < 1e-9 {
84 value.ln()
85 } else {
86 (value.powf(*lambda) - 1.0) / lambda
87 }
88 }
89 }
90 }
91
92 pub fn inverse(&self, value: f64) -> f64 {
94 match self {
95 ScaleTransform::Identity => value,
96 ScaleTransform::Log10 => 10f64.powf(value),
97 ScaleTransform::Log2 => 2f64.powf(value),
98 ScaleTransform::Ln => value.exp(),
99 ScaleTransform::Sqrt => value * value,
100 ScaleTransform::Reverse => -value,
101 ScaleTransform::Logit => 1.0 / (1.0 + (-value).exp()),
102 ScaleTransform::Probit => pnorm(value),
103 ScaleTransform::PseudoLog => 2.0 * value.sinh(),
104 ScaleTransform::Reciprocal => {
105 if value != 0.0 {
106 1.0 / value
107 } else {
108 f64::NAN
109 }
110 }
111 ScaleTransform::Exp => value.ln(),
112 ScaleTransform::BoxCox(lambda) => {
113 if lambda.abs() < 1e-9 {
114 value.exp()
115 } else {
116 (value * lambda + 1.0).powf(1.0 / lambda)
117 }
118 }
119 }
120 }
121
122 pub fn transform_value(&self, value: &Value) -> Value {
124 match value.as_f64() {
125 Some(f) => {
126 let t = self.apply(f);
127 if t.is_finite() || (f.is_infinite() && t.is_infinite()) {
129 Value::Float(t)
130 } else {
131 Value::Na
132 }
133 }
134 None => value.clone(),
135 }
136 }
137
138 pub fn is_identity(&self) -> bool {
139 matches!(self, ScaleTransform::Identity)
140 }
141
142 pub(crate) fn breaks_in_data_space(&self, data_min: f64, data_max: f64, n: usize) -> Vec<f64> {
152 match self {
153 ScaleTransform::Log10 => super::util::log_breaks(data_min, data_max, n, 10.0),
154 ScaleTransform::Log2 => super::util::log_breaks(data_min, data_max, n, 2.0),
155 ScaleTransform::Ln => {
156 super::util::log_breaks(data_min, data_max, n, std::f64::consts::E)
157 }
158 ScaleTransform::Sqrt => super::util::extended_breaks(data_min, data_max, n),
161 _ => super::util::extended_breaks(data_min, data_max, n),
162 }
163 }
164
165 pub(crate) fn uses_data_space_breaks(&self) -> bool {
170 matches!(
171 self,
172 ScaleTransform::Log10
173 | ScaleTransform::Log2
174 | ScaleTransform::Ln
175 | ScaleTransform::Sqrt
176 )
177 }
178}
179
180fn pnorm(x: f64) -> f64 {
182 0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
183}
184
185fn erf(x: f64) -> f64 {
186 let sign = if x < 0.0 { -1.0 } else { 1.0 };
187 let x = x.abs();
188 let t = 1.0 / (1.0 + 0.327_591_1 * x);
189 let y = 1.0
190 - (((((1.061_405_429 * t - 1.453_152_027) * t) + 1.421_413_741) * t - 0.284_496_736) * t
191 + 0.254_829_592)
192 * t
193 * (-x * x).exp();
194 sign * y
195}
196
197fn qnorm(p: f64) -> f64 {
199 if p <= 0.0 {
200 return f64::NEG_INFINITY;
201 }
202 if p >= 1.0 {
203 return f64::INFINITY;
204 }
205 if p < 0.5 {
206 -rational_approx((-2.0 * p.ln()).sqrt())
207 } else if p > 0.5 {
208 rational_approx((-2.0 * (1.0 - p).ln()).sqrt())
209 } else {
210 0.0
211 }
212}
213
214fn rational_approx(t: f64) -> f64 {
215 let c0 = 2.515_517;
216 let c1 = 0.802_853;
217 let c2 = 0.010_328;
218 let d1 = 1.432_788;
219 let d2 = 0.189_269;
220 let d3 = 0.001_308;
221 t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
222}
223
224#[cfg(test)]
225mod tests {
226 use super::ScaleTransform::*;
227
228 fn roundtrip(t: super::ScaleTransform, v: f64, tol: f64) {
229 let back = t.inverse(t.apply(v));
230 assert!(
231 (back - v).abs() < tol,
232 "{t:?}: {v} -> {} -> {back}",
233 t.apply(v)
234 );
235 }
236
237 #[test]
238 fn transforms_roundtrip() {
239 roundtrip(Logit, 0.3, 1e-9);
240 roundtrip(Probit, 0.3, 1e-2); roundtrip(PseudoLog, -4.0, 1e-9);
242 roundtrip(PseudoLog, 0.0, 1e-9);
243 roundtrip(Reciprocal, 2.5, 1e-9);
244 roundtrip(Exp, 1.7, 1e-9);
245 roundtrip(BoxCox(0.5), 4.0, 1e-9);
246 roundtrip(BoxCox(0.0), 4.0, 1e-9); }
248
249 #[test]
250 fn transforms_domain_edges() {
251 assert_eq!(Logit.apply(0.0), f64::NEG_INFINITY);
252 assert_eq!(Logit.apply(1.0), f64::INFINITY);
253 assert!(Reciprocal.apply(0.0).is_nan());
254 assert!(BoxCox(0.5).apply(-1.0).is_nan());
255 assert_eq!(Probit.apply(0.5), 0.0);
256 assert!((BoxCox(0.0).apply(std::f64::consts::E) - 1.0).abs() < 1e-9);
258 }
259}