defmodule Contex.Sparkline do alias __MODULE__ alias Contex.{ContinuousScale, Scale} defstruct [:data, :extents, :length, :spot_radius, :spot_colour, :line_width, :line_colour, :fill_colour, :y_transform, :height, :width] def new(data) when is_list(data) do %Sparkline{data: data, extents: ContinuousScale.extents(data), length: length(data)} |> set_default_style end defp set_default_style(%Sparkline{} = sparkline) do %{sparkline | spot_radius: 2, spot_colour: "red", line_width: 1, line_colour: "rgba(0, 200, 50, 0.7)", fill_colour: "rgba(0, 200, 50, 0.2)", height: 20, width: 100} end def draw(%Sparkline{height: height, width: width, line_width: line_width} = sparkline) do vb_width = sparkline.length + 1 height = height + (2 * line_width) {min, max} = sparkline.extents vb_height = max - min scale = ContinuousScale.new_linear() |> ContinuousScale.domain(sparkline.data) |> Scale.set_range(height, 0) sparkline = %{sparkline | y_transform: scale.domain_to_range_fn} output = ~s""" """ {:safe, [output]} end defp get_line_style(%Sparkline{line_colour: line_colour, line_width: line_width}) do ~s|stroke="#{line_colour}" stroke-width="#{line_width}" fill="none" vector-effect="non-scaling-stroke"| end defp get_fill_style(%Sparkline{fill_colour: fill_colour}) do ~s|stroke="none" fill="#{fill_colour}"| end defp get_closed_path(%Sparkline{extents: {min, max}} = sparkline) do height = max - min # Same as the open path, except we drop down, run back to height,height (aka 0,0) and close it... open_path = get_path(sparkline) [open_path, "V #{height} L 0 #{height} Z"] end # This is the IO List approach defp get_path(%Sparkline{y_transform: transform_func} = sparkline) do last_item = Enum.count(sparkline.data) - 1 ["M", sparkline.data |> Enum.map(transform_func) |> Enum.with_index() |> Enum.map(fn {value, i} -> case i < last_item do true -> "#{i} #{value} L " _ -> "#{i} #{value}" end end) ] end end