// Continuous scales: port of d3-scale 4 `continuous`, `linearish`, `loggish`
// and `powish` as inlined in vega-scale 7.4.3 (ISC license).
// https://github.com/d3/d3-scale
///|
pub(all) enum ScaleKind {
Linear
/// `Pow(exponent)`; exponent 0.5 behaves as `sqrt`
Pow(Double)
/// `Log(base)`
Log(Double)
}
///|
pub fn ScaleKind::sqrt() -> ScaleKind {
Pow(0.5)
}
///|
let math_e : Double = 2.718281828459045
///|
pub(all) struct Continuous {
kind : ScaleKind
mut domain : Array[Double]
mut range : Array[Double]
mut clamp : Bool
/// `rangeRound`: interpolate with rounding
mut round : Bool
}
///|
pub fn Continuous::new(kind : ScaleKind) -> Continuous {
{
kind,
domain: match kind {
Log(_) => [1.0, 10.0]
_ => [0.0, 1.0]
},
range: [0.0, 1.0],
clamp: false,
round: false,
}
}
///|
fn pow_transform(x : Double, exponent : Double) -> Double {
if exponent == 1.0 {
x
} else if exponent == 0.5 {
if x < 0.0 {
-(-x).sqrt()
} else {
x.sqrt()
}
} else if x < 0.0 {
-@math.pow(-x, exponent)
} else {
@math.pow(x, exponent)
}
}
///|
/// The forward transform of the scale (identity, pow or natural log).
fn Continuous::transform(self : Continuous, x : Double) -> Double {
match self.kind {
Linear => x
Pow(e) => pow_transform(x, e)
Log(_) =>
if self.domain.length() > 0 && self.domain[0] < 0.0 {
-@util.js_log(-x)
} else {
@util.js_log(x)
}
}
}
///|
fn normalize(a : Double, b0 : Double) -> (Double) -> Double {
let b = b0 - a
if b != 0.0 {
fn(x) { (x - a) / b }
} else {
let c = if b.is_nan() { @double.not_a_number } else { 0.5 }
fn(_) { c }
}
}
///|
fn interpolate_number(
a : Double,
b : Double,
round : Bool,
) -> (Double) -> Double {
if round {
fn(t) { @util.js_round(a * (1.0 - t) + b * t) }
} else {
fn(t) { a * (1.0 - t) + b * t }
}
}
///|
/// `bisectRight(a, x, lo, hi)` for an ascending array.
fn bisect_right(
a : Array[Double],
x : Double,
lo0 : Int32,
hi0 : Int32,
) -> Int32 {
let mut lo = lo0
let mut hi = hi0
while lo < hi {
let mid = (lo + hi) >> 1
if a[mid] > x {
hi = mid
} else {
lo = mid + 1
}
}
lo
}
///|
/// Map a domain value to the range; None for NaN input (d3's `unknown`).
pub fn Continuous::map(self : Continuous, x : Double) -> Double? {
if x.is_nan() {
return None
}
let n = if self.domain.length() < self.range.length() {
self.domain.length()
} else {
self.range.length()
}
if n < 2 {
return Some(@double.not_a_number)
}
let cx = if self.clamp {
let mut a = self.domain[0]
let mut b = self.domain[n - 1]
if a > b {
let t = a
a = b
b = t
}
@util.js_max(a, @util.js_min(b, x))
} else {
x
}
let t = self.transform(cx)
let td = self.domain.map(fn(d) { self.transform(d) })
if n > 2 {
// polymap
let j = n - 1
let mut domain = td
let mut range = self.range
if domain[j] < domain[0] {
domain = domain.copy()
domain.rev_in_place()
range = range.copy()
range.rev_in_place()
}
let i = bisect_right(domain, t, 1, j) - 1
let norm = normalize(domain[i], domain[i + 1])
let interp = interpolate_number(range[i], range[i + 1], self.round)
Some(interp(norm(t)))
} else {
// bimap
let d0 = td[0]
let d1 = td[1]
let r0 = self.range[0]
let r1 = self.range[1]
if d1 < d0 {
Some(interpolate_number(r1, r0, self.round)(normalize(d1, d0)(t)))
} else {
Some(interpolate_number(r0, r1, self.round)(normalize(d0, d1)(t)))
}
}
}
// ---- logarithmic helpers ----
///|
fn pow10_text(x : Double) -> Double {
// d3: isFinite(x) ? +("1e" + x) : x < 0 ? 0 : x
if x.is_nan() {
return x
}
if x.is_inf() {
return if x < 0.0 { 0.0 } else { x }
}
if x != x.trunc() || x.abs() >= 1.0e21 {
// "1e1.5" and "1e1e+21" are not numbers in JavaScript
return @double.not_a_number
}
let text = "1e" + @util.js_number_to_string(x)
@string.from_str(text) catch {
_ => if x < 0.0 { 0.0 } else { @double.infinity }
}
}
///|
fn Continuous::base(self : Continuous) -> Double {
match self.kind {
Log(b) => b
_ => 10.0
}
}
///|
fn Continuous::reflected(self : Continuous) -> Bool {
self.domain.length() > 0 && self.domain[0] < 0.0
}
///|
fn Continuous::logs(self : Continuous, x : Double) -> Double {
let base = self.base()
let f = fn(v : Double) {
if base == math_e {
@util.js_log(v)
} else if base == 10.0 {
@util.js_log10(v)
} else if base == 2.0 {
@math.log2(v)
} else {
@util.js_log(v) / @util.js_log(base)
}
}
if self.reflected() {
-f(-x)
} else {
f(x)
}
}
///|
fn Continuous::pows(self : Continuous, x : Double) -> Double {
let base = self.base()
let f = fn(v : Double) {
if base == 10.0 {
pow10_text(v)
} else if base == math_e {
@math.exp(v)
} else {
@math.pow(base, v)
}
}
if self.reflected() {
-f(-x)
} else {
f(x)
}
}
///|
fn Continuous::log_ticks(self : Continuous, count : Double) -> Array[Double] {
let d = self.domain
let mut u = d[0]
let mut v = d[d.length() - 1]
let r = v < u
if r {
let t = u
u = v
v = t
}
let base = self.base()
let mut i = self.logs(u)
let mut j = self.logs(v)
let n = count
let mut z : Array[Double] = []
if base.mod(1.0) == 0.0 && j - i < n {
i = i.floor()
j = j.ceil()
if u > 0.0 {
while i <= j {
let mut k = 1.0
while k < base {
let t = if i < 0.0 { k / self.pows(-i) } else { k * self.pows(i) }
if t < u {
k = k + 1.0
continue
}
if t > v {
break
}
z.push(t)
k = k + 1.0
}
i = i + 1.0
}
} else {
while i <= j {
let mut k = base - 1.0
while k >= 1.0 {
let t = if i > 0.0 { k / self.pows(-i) } else { k * self.pows(i) }
if t < u {
k = k - 1.0
continue
}
if t > v {
break
}
z.push(t)
k = k - 1.0
}
i = i + 1.0
}
}
if z.length().to_double() * 2.0 < n {
z = @stats.ticks(u, v, n)
}
} else {
z = @stats.ticks(i, j, @util.js_min(j - i, n)).map(fn(x) { self.pows(x) })
}
if r {
z.rev_in_place()
}
z
}
///|
/// Approximately `count` nice tick values (d3 `scale.ticks`).
pub fn Continuous::ticks(
self : Continuous,
count~ : Double = 10.0,
) -> Array[Double] {
match self.kind {
Log(_) => self.log_ticks(count)
_ =>
@stats.ticks(self.domain[0], self.domain[self.domain.length() - 1], count)
}
}
///|
/// d3 `scale.nice(count)`: widen the domain to round values.
pub fn Continuous::nice(self : Continuous, count~ : Double = 10.0) -> Unit {
match self.kind {
Log(_) => {
let d = self.domain.copy()
let mut i0 = 0
let mut i1 = d.length() - 1
let mut x0 = d[i0]
let mut x1 = d[i1]
if x1 < x0 {
let t = i0
i0 = i1
i1 = t
let tx = x0
x0 = x1
x1 = tx
}
d[i0] = self.pows(self.logs(x0).floor())
d[i1] = self.pows(self.logs(x1).ceil())
self.domain = d
}
_ => {
let d = self.domain.copy()
let mut i0 = 0
let mut i1 = d.length() - 1
let mut start = d[i0]
let mut stop = d[i1]
let mut prestep = @double.not_a_number
let mut have_prestep = false
if stop < start {
let t = start
start = stop
stop = t
let ti = i0
i0 = i1
i1 = ti
}
let mut iter = 10
while iter > 0 {
iter = iter - 1
let step = @stats.tick_increment(start, stop, count)
if have_prestep && step == prestep {
d[i0] = start
d[i1] = stop
self.domain = d
return
} else if step > 0.0 {
start = (start / step).floor() * step
stop = (stop / step).ceil() * step
} else if step < 0.0 {
start = (start * step).ceil() / step
stop = (stop * step).floor() / step
} else {
break
}
prestep = step
have_prestep = true
}
}
}
}