lib/chart/scale/scale_utils.ex

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