defmodule Contex.ScaleUtils do
@moduledoc """
Here are common functions that can be shared between multiple scales.
"""
alias Contex.Utils
@doc """
Makes sure that a range of numerics is
a tuple of floats, in the right order.
"""
def validate_range({f, t}, _label) when is_number(f) and is_number(t) do
ff = as_float(f)
tt = as_float(t)
if tt < ff do
{tt, ff}
else
{ff, tt}
end
end
def validate_range(v, label),
do:
throw("#{label} - a range should be in the form {0.0, 1.0} but you supplied #{inspect(v)}")
def as_float(n) when is_number(n) do
case(n) do
i when is_integer(i) -> i * 1.0
f -> f
end
end
@doc """
Validates a range, that could be nil.
"""
def validate_range_nil(nil, _label), do: nil
def validate_range_nil(r, label), do: validate_range(r, label)
def validate_option(o, option_name, possible_options)
when is_binary(option_name) and is_list(possible_options) do
if o in possible_options do
o
else
throw(
"Option #{option_name} cannot be set to #{o} - valid values are #{inspect(possible_options)} "
)
end
end
@doc """
Rescales a value from domain to range.
Expects
(can be refactored in Lin)
"""
def rescale_value(v, domain_min, domain_width, range_min, range_width) do
if domain_width > 0.0 do
ratio = (v - domain_min) / domain_width
ratio * range_width + range_min
else
0.0
end
end
@doc """
Finds the area where a data-set is defined,
as to properly place minimums and maximums.
Returns a domain, e.g. {-3, 22}
"""
def extents(data) do
Enum.reduce(data, {nil, nil}, fn x, {min, max} ->
{Utils.safe_min(x, min), Utils.safe_max(x, max)}
end)
end
@doc """
Formats ticks.
(can be refactored in Lin)
"""
def format_tick_text(tick, _, custom_tick_formatter) when is_function(custom_tick_formatter),
do: custom_tick_formatter.(tick)
def format_tick_text(tick, _, _) when is_integer(tick), do: to_string(tick)
def format_tick_text(tick, display_decimals, _) when display_decimals > 0 do
:erlang.float_to_binary(tick, decimals: display_decimals)
end
def format_tick_text(tick, _, _), do: :erlang.float_to_binary(tick, [:compact, decimals: 0])
@doc """
Computes settings to display values.
%{
nice_domain: {min_nice, max_nice},
interval_size: rounded_interval_size,
interval_count: adjusted_interval_count,
display_decimals: display_decimals
}
(can be refactored in Lin)
"""
def compute_nice_settings(
min_d,
max_d,
explicit_ticks,
interval_count
)
when is_number(min_d) and is_number(max_d) and is_number(interval_count) and
interval_count > 1 do
width = max_d - min_d
width = if width == 0.0, do: 1.0, else: width
unrounded_interval_size = width / interval_count
order_of_magnitude = :math.ceil(:math.log10(unrounded_interval_size) - 1)
power_of_ten = :math.pow(10, order_of_magnitude)
rounded_interval_size =
lookup_axis_interval(unrounded_interval_size / power_of_ten) * power_of_ten
min_nice = rounded_interval_size * Float.floor(min_d / rounded_interval_size)
max_nice = rounded_interval_size * Float.ceil(max_d / rounded_interval_size)
adjusted_interval_count = round(1.0001 * (max_nice - min_nice) / rounded_interval_size)
display_decimals = guess_display_decimals(order_of_magnitude)
# If I have a list of explicit ticks
computed_ticks =
case explicit_ticks do
ei when is_list(ei) ->
ei
|> Enum.filter(fn v -> v >= min_d && v <= max_d end)
_ ->
0..adjusted_interval_count
|> Enum.map(fn i -> min_d + i * rounded_interval_size end)
end
%{
nice_domain: {min_nice, max_nice},
ticks: computed_ticks,
display_decimals: display_decimals
}
end
@axis_interval_breaks [0.05, 0.1, 0.2, 0.25, 0.4, 0.5, 1.0, 2.0, 2.5, 4.0, 5.0, 10.0, 20.0]
defp lookup_axis_interval(raw_interval) when is_float(raw_interval) do
Enum.find(@axis_interval_breaks, 10.0, fn x -> x >= raw_interval end)
end
defp guess_display_decimals(power_of_ten) when power_of_ten > 0 do
0
end
defp guess_display_decimals(power_of_ten) do
1 + -1 * round(power_of_ten)
end
end