diff --git a/lib/chart/sparkline.ex b/lib/chart/sparkline.ex index efd547b..b8479a6 100644 --- a/lib/chart/sparkline.ex +++ b/lib/chart/sparkline.ex @@ -13,17 +13,49 @@ defmodule Contex.Sparkline do Sparkline.new(data) |> Sparkline.draw() # Emits svg sparkline ``` - The colour defaults to a green line with a faded green fill, but can be overridden - with `colours/3`. Unlike other colours in Contex, these colours are how you would - specify them in CSS - e.g. + You can modify various rendering properties through `style/2`. These properties + map directly to the underlying elements (line & area), giving you great flexibility to + style them in various ways -e.g. + + Use color values & dimensions: + ``` Sparkline.new(data) - |> Sparkline.colours("#fad48e", "#ff9838") + |> Sparkline.style(line_stroke: "#fad48e", area_fill: "#ff9838", height: 50, width: 300) + |> Sparkline.draw() + ``` + + Use classes: + + ``` + Sparkline.new(data) + |> Sparkline.style( + line_stroke: nil, + area_fill: nil, + line_class: "stroke-blue-500", + area_class: "fill-blue-50" + ) + |> Sparkline.draw() + ``` + + Injecting extra_elements: + + ``` + extra_svg = \""" + + + + + + + \""" + + Sparkline.new(data) + |> Sparkline.style(line_stroke: "url(#pretty-gradient)", extra_svg: extra_svg) |> Sparkline.draw() ``` - The size defaults to 20 pixels high and 100 wide. You can override by updating - `:height` and `:width` directly in the `Sparkline` struct before call `draw/1`. + """ alias __MODULE__ alias Contex.{ContinuousLinearScale, Scale} @@ -32,25 +64,45 @@ defmodule Contex.Sparkline do :data, :extents, :length, - :spot_radius, - :spot_colour, - :line_width, - :line_colour, - :fill_colour, :y_transform, :height, - :width + :width, + :extra_svg, + :line_stroke, + :line_class, + :line_stroke_width, + :line_stroke_linecap, + :line_stroke_linejoin, + :line_fill, + :area_stroke, + :area_fill, + :area_class ] @type t() :: %__MODULE__{} + @default_style [ + height: 20, + width: 100, + extra_svg: nil, + line_stroke: "rgba(0, 200, 50, 0.7)", + line_class: nil, + line_stroke_width: 1, + line_stroke_linecap: "round", + line_stroke_linejoin: "round", + line_fill: "none", + area_stroke: "none", + area_fill: "rgba(0, 200, 50, 0.2)", + area_class: nil + ] + @doc """ Create a new sparkline struct from some data. """ @spec new([number()]) :: Contex.Sparkline.t() def new(data) when is_list(data) do %Sparkline{data: data, extents: ContinuousLinearScale.extents(data), length: length(data)} - |> set_default_style + |> style() end @doc """ @@ -65,77 +117,161 @@ defmodule Contex.Sparkline do |> Sparkline.draw() ``` """ + @deprecated "Use style/2 instead" + @since "0.5.0" @spec colours(Contex.Sparkline.t(), String.t(), String.t()) :: Contex.Sparkline.t() - def colours(%Sparkline{} = sparkline, fill, line) do + def colours(%Sparkline{} = sparkline, area_fill, line_stroke) do # TODO: Really need some validation... - %{sparkline | fill_colour: fill, line_colour: line} + style(sparkline, area_fill: area_fill, line_stroke: line_stroke) 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 - } + @doc """ + Override any of the style settings for the sparkline. + + There are 3 elements in a sparkline, wrapping svg, a line and an area. + To control how they are rendered, you can pass ony of the following + parameters: + + * height: 20 + * width: 100 + * extra_svg: nil + * line_stroke: "rgba(0, 200, 50, 0.7)" + * line_class: nil + * line_stroke_width: 1 + * line_stroke_linecap: "round" + * line_stroke_linejoin: "round" + * line_fill: "none" + * area_stroke: "none" + * area_fill: "rgba(0, 200, 50, 0.2)" + * area_class: nil + + Example 1: Set color values & dimensions: + + ``` + Sparkline.new(data) + |> Sparkline.style(line_stroke: "#fad48e", area_fill: "#ff9838", height: 50, width: 300) + |> Sparkline.draw() + ``` + + Example 2: Set classes + + ``` + Sparkline.new(data) + |> Sparkline.style( + line_stroke: nil, + area_fill: nil, + line_class: "stroke-blue-500", + area_class: "fill-blue-50" + ) + |> Sparkline.draw() + ``` + + Example 3: Add a gradient + + ``` + extra_svg = \""" + + + + + + + \""" + + Sparkline.new(data) + |> Sparkline.style(line_stroke: "url(#pretty-gradient)", extra_svg: extra_svg) + |> Sparkline.draw() + ``` + """ + @spec style(Contex.Sparkline.t(), + height: :integer, + width: :integer, + extra_svg: String.t() | nil, + line_stroke: :integer, + line_class: String.t() | nil, + line_stroke_width: :integer, + line_stroke_linecap: String.t() | nil, + line_stroke_linejoin: String.t() | nil, + line_fill: String.t() | nil, + area_stroke: String.t() | nil, + area_fill: String.t() | nil, + area_class: String.t() | nil + ) :: Contex.Sparkline.t() + def style(%Sparkline{} = sparkline, options \\ []) do + props = + @default_style + |> Keyword.merge(options) + |> Enum.into(%{}) + + Map.merge(sparkline, props) end @doc """ Renders the sparkline to svg, including the svg wrapper, as a string or improper string list that is marked safe. """ - def draw(%Sparkline{height: height, width: width, line_width: line_width} = sparkline) do - vb_width = sparkline.length + 1 - vb_height = height - 2 * line_width + def draw(%Sparkline{} = chart) do + vb_width = chart.length - 1 + vb_height = chart.height - 2 * chart.line_stroke_width scale = ContinuousLinearScale.new() - |> ContinuousLinearScale.domain(sparkline.data) - |> Scale.set_range(vb_height, 0) + |> ContinuousLinearScale.domain(chart.data) + |> Scale.set_range(chart.line_stroke_width / 2, vb_height) - sparkline = %{sparkline | y_transform: Scale.domain_to_range_fn(scale)} + chart = %{chart | y_transform: Scale.domain_to_range_fn(scale)} output = ~s""" - - - + + #{chart.extra_svg} + + + + + + """ {: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"| + defp get_line_style(%Sparkline{ + line_stroke: line_stroke, + line_stroke_width: line_stroke_width, + line_class: line_class, + line_fill: line_fill, + line_stroke_linecap: line_stroke_linecap, + line_stroke_linejoin: line_stroke_linejoin + }) do + ~s|stroke="#{line_stroke}" class="#{line_class}" stroke-width="#{line_stroke_width}" fill="#{line_fill}" stroke-linecap="#{line_stroke_linecap}" stroke-linejoin="#{line_stroke_linejoin}" vector-effect="non-scaling-stroke" | end - defp get_fill_style(%Sparkline{fill_colour: fill_colour}) do - ~s|stroke="none" fill="#{fill_colour}"| + defp get_area_style(%Sparkline{ + area_fill: area_fill, + area_stroke: area_stroke, + area_class: area_class + }) do + ~s|stroke="#{area_stroke}" fill="#{area_fill}" class="#{area_class}"| end - defp get_closed_path(%Sparkline{} = sparkline, vb_height) do + defp get_area_path(%Sparkline{} = sparkline) do # 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 #{vb_height} L 0 #{vb_height} Z"] + open_path = get_line_path(sparkline) + [open_path, "V 0 L 0 0 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 + defp get_line_path(%Sparkline{y_transform: transform_func} = sparkline) do + last_item_index = 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}" + |> Enum.with_index(fn value, index -> + case index < last_item_index do + true -> "#{index} #{transform_func.(value)} L " + _ -> "#{index} #{transform_func.(value)} " end end) ] diff --git a/mix.exs b/mix.exs index 1cd3964..8eff2d9 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Contex.MixProject do def project do [ app: :contex, - version: "0.4.0", + version: "0.5.0", elixir: "~> 1.9", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, diff --git a/test/contex_linear_scale_test.exs b/test/contex_linear_scale_test.exs index f200179..d3f5886 100644 --- a/test/contex_linear_scale_test.exs +++ b/test/contex_linear_scale_test.exs @@ -1,4 +1,4 @@ -defmodule ContexAxisTest do +defmodule ContexLinearScaleTest do use ExUnit.Case alias Contex.ContinuousLinearScale