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