102 lines
3.0 KiB
Elixir
102 lines
3.0 KiB
Elixir
defmodule MusicLibraryWeb.ChartComponents do
|
|
use MusicLibraryWeb, :live_component
|
|
|
|
@doc """
|
|
Renders a horizontal bar chart.
|
|
|
|
## Examples
|
|
<.vertical_bar_chart
|
|
data={[{"Artist 1", 5}, {"Artist 2", 3}]}
|
|
label_fn={&elem(&1, 0)}
|
|
value_fn={&elem(&1, 1)}
|
|
width={400}
|
|
height={300}
|
|
color_class="fill-red-500"
|
|
/>
|
|
"""
|
|
attr :data, :list, required: true
|
|
attr :label_fn, :any, required: true
|
|
attr :value_fn, :any, required: true
|
|
attr :width, :integer, default: 400
|
|
attr :height, :integer, default: 300
|
|
attr :color_class, :string, required: true
|
|
attr :class, :string, default: ""
|
|
attr :label_click, :any, default: nil, doc: "the function for handling phx-click on each label"
|
|
|
|
def vertical_bar_chart(assigns) do
|
|
assigns =
|
|
assigns
|
|
|> assign(:padding, 40)
|
|
|> assign(:max_value, max_value(assigns.data, assigns.value_fn))
|
|
# Account for labels and padding
|
|
|> assign(:chart_width, assigns.width - 150 - 40)
|
|
# Account for bottom padding
|
|
|> assign(:chart_height, assigns.height - 40)
|
|
|> assign(:bar_height, calculate_bar_height(assigns.data, assigns.height - 40))
|
|
|
|
~H"""
|
|
<div class={["w-full", @class]}>
|
|
<svg
|
|
viewBox={"0 0 #{@width} #{@height}"}
|
|
preserveAspectRatio="xMidYMid meet"
|
|
class="w-full h-full"
|
|
>
|
|
<%!-- Bars and labels --%>
|
|
<%= for {datum, index} <- Enum.with_index(@data) do %>
|
|
<% bar_width = @chart_width * @value_fn.(datum) / @max_value %>
|
|
<% y = @padding / 2 + index * (@bar_height + 4) %>
|
|
|
|
<%!-- Label --%>
|
|
<text
|
|
x="140"
|
|
y={y + @bar_height / 2 + 4}
|
|
text-anchor="end"
|
|
class={["text-xs fill-zinc-500 dark:fill-zinc-400", @label_click && "cursor-pointer"]}
|
|
phx-click={@label_click && @label_click.(datum)}
|
|
>
|
|
{truncate_label(@label_fn.(datum), 20)}
|
|
</text>
|
|
|
|
<%!-- Bar --%>
|
|
<rect
|
|
x="150"
|
|
y={y}
|
|
width={bar_width}
|
|
height={@bar_height}
|
|
class={["opacity-80 hover:opacity-100 transition-opacity", @color_class]}
|
|
>
|
|
<title>{@label_fn.(datum)}: {@value_fn.(datum)}</title>
|
|
</rect>
|
|
|
|
<%!-- Value label --%>
|
|
<text
|
|
x={150 + bar_width + 5}
|
|
y={y + @bar_height / 2 + 4}
|
|
class="text-xs fill-zinc-500 dark:fill-zinc-400"
|
|
>
|
|
{@value_fn.(datum)}
|
|
</text>
|
|
<% end %>
|
|
</svg>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp max_value(data, value_fn) do
|
|
max = Enum.max_by(data, value_fn)
|
|
value_fn.(max)
|
|
end
|
|
|
|
defp calculate_bar_height(data, available_height) do
|
|
bar_count = length(data)
|
|
max_height = min(15, (available_height - (bar_count - 1) * 4) / bar_count)
|
|
max(20, max_height) |> trunc()
|
|
end
|
|
|
|
defp truncate_label(label, max_length) when byte_size(label) > max_length do
|
|
String.slice(label, 0, max_length - 2) <> ".."
|
|
end
|
|
|
|
defp truncate_label(label, _), do: label
|
|
end
|