defmodule Plox do @moduledoc """ TODO: """ use Phoenix.Component alias Phoenix.LiveView.JS alias Plox.Dataset alias Plox.DateScale alias Plox.DateTimeScale alias Plox.Dimensions alias Plox.FixedColorsScale alias Plox.FixedValuesScale alias Plox.Graph alias Plox.GraphDataset alias Plox.GraphScale alias Plox.NumberScale @doc """ Entrypoint component for rendering graphs and plots """ @doc type: :component attr :for, Graph, required: true attr :id, :string, required: true attr :width, :any, required: true, doc: "The total width of the rendered graph in pixels" attr :height, :any, required: true, doc: "The total height of the rendered graph in pixels" attr :margin, :any, default: {35, 70}, doc: """ The amount of space around the plotting area of the graph in which the axis labels are rendered. Accepts one, two, three or four values and interprets them the same was as in CSS. """ attr :padding, :any, default: 0, doc: """ The amount of space inside the plotting area of the graph from the edges to where plotting begins. Accepts one, two, three or four values and interprets them the same was as in CSS. """ slot :legend slot :tooltips slot :inner_block, required: true def graph(assigns) do assigns = assign(assigns, for: nil, graph: Graph.put_dimensions(assigns.for, Dimensions.new(assigns)) ) ~H"""
<.legend :for={legend <- @legend}> <%= render_slot(legend) %>
<%= render_slot(@inner_block, @graph) %> <%= for tooltip <- @tooltips do %> <%= render_slot(tooltip, @graph) %> <% end %>
""" end @doc """ Y-axis labels along the left or right side of the graph """ @doc type: :component attr :scale, :any, required: true attr :ticks, :any attr :step, :any attr :position, :atom, values: [:left, :right], default: :left attr :label_color, :string, default: "#18191A" attr :label_rotation, :integer, default: nil attr :grid_lines, :boolean, default: true attr :line_width, :string, default: "1" attr :line_color, :string, default: "#F2F4F5" slot :inner_block, required: true def y_axis(assigns) do ~H""" <%= for y_value <- GraphScale.values(@scale, scale_opts(assigns)), y_pixel = GraphScale.to_graph_y(@scale, y_value) do %> <.y_label dimensions={@scale.dimensions} y_pixel={y_pixel} position={@position} color={@label_color} rotation={@label_rotation} > <%= render_slot(@inner_block, y_value) %> <.horizontal_line :if={@grid_lines} dimensions={@scale.dimensions} y_pixel={y_pixel} width={@line_width} color={@line_color} /> <% end %> """ end @doc """ X-axis labels along the bottom or top of the graph """ @doc type: :component attr :scale, :any, required: true attr :ticks, :any attr :step, :any attr :position, :atom, values: [:top, :bottom], default: :bottom attr :label_color, :string, default: "#18191A" attr :label_rotation, :integer, default: nil attr :grid_lines, :boolean, default: true attr :line_width, :string, default: "1" attr :line_color, :string, default: "#F2F4F5" slot :inner_block, required: true def x_axis(assigns) do ~H""" <%= for x_value <- GraphScale.values(@scale, scale_opts(assigns)), x_pixel = GraphScale.to_graph_x(@scale, x_value) do %> <.x_label dimensions={@scale.dimensions} x_pixel={x_pixel} position={@position} color={@label_color} rotation={@label_rotation} > <%= render_slot(@inner_block, x_value) %> <.vertical_line :if={@grid_lines} dimensions={@scale.dimensions} x_pixel={x_pixel} width={@line_width} color={@line_color} /> <% end %> """ end defp scale_opts(assigns), do: Map.take(assigns, [:ticks, :step]) @doc """ A connected line plot """ @doc type: :component attr :dataset, :any, required: true attr :x, :atom, default: :x, doc: "The dataset axis key to use for x values" attr :y, :atom, default: :y, doc: "The dataset axis key to use for y values" attr :width, :string, examples: ["1.5", "4"], default: "2" attr :line_style, :atom, values: [:solid, :dashed, :dotted], default: :solid attr :color, :string, default: "#FF9330" attr :type, :atom, values: [:line, :step_line], default: :line def line_plot(%{type: :line} = assigns) do ~H""" GraphDataset.to_graph_points(@x, @y) |> polyline_points()} fill="none" stroke={@color} stroke-width={@width} stroke-dasharray={stroke_dasharray(@line_style)} /> """ end def line_plot(%{type: :step_line} = assigns) do ~H""" step_points(@x, @y) |> polyline_points()} fill="none" stroke={@color} stroke-width={@width} stroke-dasharray={stroke_dasharray(@line_style)} /> """ end defp step_points(%GraphDataset{} = graph_dataset, x_key, y_key) do graph_dataset |> GraphDataset.to_graph_points(x_key, y_key) |> Enum.chunk_every(2, 1) |> Enum.flat_map(fn [point1, point2] -> [point1, %{point2 | y: point1.y}] [point] -> [point] end) end defp polyline_points(points), do: Enum.map_join(points, " ", &"#{&1.x},#{&1.y}") @doc """ Points plot """ @doc type: :component attr :dataset, :any, required: true attr :x, :atom, default: :x, doc: "The dataset axis key to use for x values" attr :y, :atom, default: :y, doc: "The dataset axis key to use for y values" attr :radius, :string, examples: ["8", "24.5"], default: "4" attr :color, :any, examples: ["red", "#FF9330", :color_axis], default: "#FF9330" attr :"phx-click", :any, default: nil attr :"phx-target", :any, default: nil def points_plot(assigns) do ~H""" """ end @doc """ Bar plot """ @doc type: :component attr :dataset, :any, required: true attr :x, :atom, default: :x, doc: "The dataset axis key to use for x values" attr :y, :atom, default: :y, doc: "The dataset axis key to use for y values" attr :width, :string, examples: ["1.5", "4"], default: "16" attr :bar_style, :atom, values: [:round, :square], default: :round attr :color, :any, examples: ["red", "#FF9330", :color_axis], default: "#FF9330" attr :"phx-click", :any, default: nil attr :"phx-target", :any, default: nil # TODO: # support for several groups of bars def bar_plot(assigns) do ~H""" <%= for point <- GraphDataset.to_graph_points(@dataset, @x, @y) do %> <% end %> """ end defp bar_style(:round), do: "round" defp bar_style(:square), do: "butt" @doc """ Tooltip """ @doc type: :component attr :dataset, :any, required: true attr :point_id, :any, required: true attr :x, :atom, default: :x, doc: "The dataset axis key to use for x values" attr :y, :atom, default: :y, doc: "The dataset axis key to use for y values" attr :"phx-click-away", :any attr :"phx-target", :any, default: nil slot :inner_block, required: true def tooltip(assigns) do point = GraphDataset.to_graph_point(assigns.dataset, assigns.x, assigns.y, assigns.point_id) assigns = assign(assigns, point: point) ~H"""
<%= render_slot(@inner_block, @point.data_point.original) %>
""" end @doc """ One-dimensional shaded areas, either horizontal or vertical """ @doc type: :component attr :dataset, :any, required: true attr :area, :atom, required: true, doc: "The dataset axis key to use for the area plots" attr :color, :atom, required: true, doc: "The dataset axis key to use for colors" attr :orientation, :atom, values: [:vertical, :horizontal], default: :horizontal def area_plot(%{orientation: :horizontal} = assigns) do ~H""" <%= for [scalar1, scalar2] <- area_points(@dataset, @area, @orientation), rect_color = GraphDataset.to_color(@dataset, @color, scalar1.data_point) do %> <% end %> """ end def area_plot(%{orientation: :vertical} = assigns) do ~H""" <%= for [scalar1, scalar2] <- area_points(@dataset, @area, @orientation), rect_color = GraphDataset.to_color(@dataset, @color, scalar1.data_point) do %> <% end %> """ end defp area_points(%GraphDataset{} = graph_dataset, key, :horizontal) do graph_dataset |> GraphDataset.to_graph_xs(key) |> Enum.chunk_every(2, 1, :discard) end defp area_points(%GraphDataset{} = graph_dataset, key, :vertical) do graph_dataset |> GraphDataset.to_graph_ys(key) |> Enum.chunk_every(2, 1, :discard) end attr :dimensions, :map, required: true attr :y_pixel, :float, required: true, doc: "Y pixel value for rendering this label" attr :position, :atom, required: true, values: [:left, :right] attr :color, :string, required: true attr :style, :string, default: "font-size: 0.75rem; line-height: 1rem" attr :rotation, :integer, default: nil slot :inner_block, required: true defp y_label(%{position: :left} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end defp y_label(%{position: :right} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end attr :dimensions, :map, required: true attr :x_pixel, :float, required: true, doc: "X pixel value for rendering this label" attr :position, :atom, required: true, values: [:top, :bottom] attr :color, :string, required: true attr :style, :string, default: "font-size: 0.75rem; line-height: 1rem" attr :rotation, :integer, default: nil slot :inner_block, required: true defp x_label(%{position: :bottom} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end defp x_label(%{position: :top} = assigns) do ~H""" <%= render_slot(@inner_block) %> """ end attr :dimensions, :map, required: true attr :y_pixel, :float, required: true attr :width, :string, required: true attr :line_style, :atom, values: [:solid, :dashed, :dotted], default: :solid attr :color, :string, required: true defp horizontal_line(assigns) do ~H""" """ end attr :dimensions, :map, required: true attr :x_pixel, :float, required: true attr :width, :string, required: true attr :line_style, :atom, values: [:solid, :dashed, :dotted], default: :solid attr :color, :string, required: true defp vertical_line(assigns) do ~H""" """ end @doc """ A horizontal or vertical marker line with a label """ @doc type: :component attr :at, :any, required: true attr :scale, :any, required: true attr :width, :string, default: "1.5" attr :orientation, :atom, values: [:vertical, :horizontal], default: :vertical attr :line_style, :atom, values: [:solid, :dashed, :dotted], default: :dotted attr :line_color, :string, default: "#18191A" attr :label_color, :string, default: "#18191A" attr :label_style, :string, default: "font-size: 0.75rem; line-height: 1rem" attr :label_rotation, :integer, default: nil slot :inner_block, required: true def marker(%{orientation: :vertical} = assigns) do x_pixel = GraphScale.to_graph_x(assigns.scale, assigns.at) assigns = assign(assigns, dimensions: assigns.scale.dimensions, x_pixel: x_pixel) ~H""" <%= render_slot(@inner_block) %> """ end def marker(%{orientation: :horizontal} = assigns) do y_pixel = GraphScale.to_graph_y(assigns.scale, assigns.at) assigns = assign(assigns, dimensions: assigns.scale.dimensions, y_pixel: y_pixel) ~H""" <%= render_slot(@inner_block) %> """ end @doc """ Legend row """ @doc type: :component slot :inner_block, required: true def legend(assigns) do ~H"""
<%= render_slot(@inner_block) %>
""" end @doc """ Legend item """ @doc type: :component attr :color, :string, required: true attr :label, :string, required: true def legend_item(assigns) do ~H"""
<.color_bubble color={@color} />

<%= @label %>

""" end @doc """ A colored circle for legends """ @doc type: :component attr :color, :string, required: true def color_bubble(assigns) do ~H"""
""" end defp stroke_dasharray(:solid), do: false defp stroke_dasharray(:dotted), do: "2" defp stroke_dasharray(:dashed), do: "6" defdelegate to_graph(scales_and_datasets), to: Graph, as: :new defdelegate date_scale(range), to: DateScale, as: :new defdelegate datetime_scale(first, last), to: DateTimeScale, as: :new defdelegate number_scale(first, last), to: NumberScale, as: :new defdelegate fixed_colors_scale(color_mapping), to: FixedColorsScale, as: :new defdelegate fixed_values_scale(values), to: FixedValuesScale, as: :new defdelegate dataset(data, aces), to: Dataset, as: :new end