diff --git a/docs/architecture.md b/docs/architecture.md index e7823f4f..5c6b5c9b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -285,7 +285,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun | `CoreComponents` | Forms, buttons, icons, tables, flash messages | | `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) | | `RecordComponents` | Record cards, cover images, artist images, labels, grids, shared show-page sections (title, external links, genres, releases, timestamps, debug) | -| `ChartComponents` | SVG charts for stats | +| `ChartComponents` | Charts for stats dashboard | | `StatsComponents` | Stats dashboard widgets | | `ScrobbleComponents` | Scrobble activity displays | | `SearchComponents` | Search result rendering | diff --git a/lib/music_library_web/components/chart_components.ex b/lib/music_library_web/components/chart_components.ex index cc591d13..69858b81 100644 --- a/lib/music_library_web/components/chart_components.ex +++ b/lib/music_library_web/components/chart_components.ex @@ -1,24 +1,29 @@ defmodule MusicLibraryWeb.ChartComponents do + @moduledoc """ + Chart components for the stats dashboard. + """ + use MusicLibraryWeb, :live_component @doc """ - Renders a horizontal bar chart. + Renders a horizontal bar chart using CSS Grid. + + The label column auto-sizes to the widest label (up to 130px max), + bars fill remaining space proportionally, and values are displayed + to the right of each bar. ## 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" + color_class="bg-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 :datum_click, :any, default: nil, doc: "the function for handling phx-click on each datum" @@ -26,71 +31,38 @@ defmodule MusicLibraryWeb.ChartComponents do def vertical_bar_chart(assigns) when assigns.data != [] 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""" -
- - <%!-- 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) %> +
+
+ <%= for datum <- @data do %> + <% percentage = bar_percentage(@value_fn.(datum), @max_value) %> + <% label = to_string(@label_fn.(datum)) %> - <%!-- Label --%> - - {truncate_label(@label_fn.(datum), 20)} - - - <%!-- Bar --%> - - {@label_fn.(datum)}: {@value_fn.(datum)} - - - <%!-- Value label --%> - - {@value_fn.(datum)} - + <%= if @datum_click do %> +
+ <.bar_row + label={label} + percentage={percentage} + value={@value_fn.(datum)} + color_class={@color_class} + grouped + /> +
+ <% else %> + <.bar_row + label={label} + percentage={percentage} + value={@value_fn.(datum)} + color_class={@color_class} + /> + <% end %> <% end %> - +
""" end @@ -103,20 +75,46 @@ defmodule MusicLibraryWeb.ChartComponents do """ end + attr :label, :string, required: true + attr :percentage, :float, required: true + attr :value, :any, required: true + attr :color_class, :string, required: true + attr :grouped, :boolean, default: false + + defp bar_row(assigns) do + ~H""" +
+ {@label} +
+
+
+
+
+
+ {@value} +
+ """ + 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 + defp bar_percentage(_value, max_value) when max_value == 0, do: 0.0 + defp bar_percentage(value, max_value), do: value / max_value * 100 end diff --git a/lib/music_library_web/live/stats_live/index.ex b/lib/music_library_web/live/stats_live/index.ex index f357bb6c..c6eaaf34 100644 --- a/lib/music_library_web/live/stats_live/index.ex +++ b/lib/music_library_web/live/stats_live/index.ex @@ -300,9 +300,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
<.vertical_bar_chart data={@records_by_artist} - width={600} - height={26 * length(@records_by_artist)} - color_class="fill-red-500" + color_class="bg-red-500" label_fn={fn datum -> datum.name end} value_fn={fn datum -> datum.count end} datum_click={ @@ -322,9 +320,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
<.vertical_bar_chart data={@records_by_genre} - width={600} - height={26 * length(@records_by_genre)} - color_class="fill-zinc-500" + color_class="bg-zinc-500" label_fn={fn {genre, _count} -> genre end} value_fn={fn {_genre, count} -> count end} datum_click={ @@ -344,9 +340,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
<.vertical_bar_chart data={@records_by_release_year} - width={600} - height={26 * length(@records_by_release_year)} - color_class="fill-red-500" + color_class="bg-red-500" label_fn={fn {year, _count} -> year end} value_fn={fn {_year, count} -> count end} datum_click={