defmodule Contex.SimplePie do @moduledoc """ Generates a simple pie chart from an array of tuples like `{"Cat", 10.0}`. Usage: ``` SimplePie.new([{"Cat", 10.0}, {"Dog", 20.0}, {"Hamster", 5.0}]) |> SimplePie.colours(["aa0000", "00aa00", "0000aa"]) # Optional - only if you don't like the defaults |> SimplePie.draw() # Emits svg pie chart ``` The colours are using default from `Contex.CategoryColourScale.new/1` by names in tuples. The size defaults to 50 pixels high and wide. You can override by updating `:height` directly in the `SimplePie` struct before call `draw/1`. The height and width of pie chart is always same, therefore set only height is enough. """ alias __MODULE__ alias Contex.CategoryColourScale defstruct [ :data, :scaled_values, :fill_colours, height: 50 ] @type t() :: %__MODULE__{} @doc """ Create a new SimplePie struct from list of tuples. """ @spec new([{String.t(), number()}]) :: t() def new(data) when is_list(data) do %SimplePie{ data: data, scaled_values: data |> Enum.map(&elem(&1, 1)) |> scale_values(), fill_colours: data |> Enum.map(&elem(&1, 0)) |> CategoryColourScale.new() } end @doc """ Update the colour palette used for the slices. """ @spec colours(t(), CategoryColourScale.colour_palette()) :: t() def colours(%SimplePie{fill_colours: fill_colours} = pie, colours) do custom_fill_colours = CategoryColourScale.set_palette(fill_colours, colours) %SimplePie{pie | fill_colours: custom_fill_colours} end @doc """ Renders the SimplePie to svg, including the svg wrapper, as a string or improper string list that is marked safe. """ @spec draw(t()) :: {:safe, [String.t()]} def draw(%SimplePie{height: height} = chart) do output = ~s""" #{generate_slices(chart)} """ {:safe, [output]} end defp generate_slices(%SimplePie{ data: data, scaled_values: scaled_values, height: height, fill_colours: fill_colours }) do r = height / 2 stroke_circumference = 2 * :math.pi() * r / 2 categories = data |> Enum.map(&elem(&1, 0)) scaled_values |> Enum.zip(categories) |> Enum.map_reduce({0, 0}, fn {value, category}, {idx, offset} -> { ~s""" """, {idx + 1, offset + value} } end) |> elem(0) |> Enum.join() end defp slice_value(value, stroke_circumference) do value * stroke_circumference / 100 end defp scale_values(values) do values |> Enum.map_reduce(Enum.sum(values), &{&1 / &2 * 100, &2}) |> elem(0) end end