Home Posts Post Search Tag Search

LiveView 23 - Chapter 8: Build an Interactive Dashboard
Published on: 2026-01-15 Tags: elixir, Blog, Side Project, LiveView, Html/CSS, Phoenix, queries

Chapter 8: Build a Interactive Dashboard

We now have a lot user generated content within the site but we don’t have a way to see things through the lense of an admin. We can’t see things as they come in so let’s work on that now. We will build an interactive Dashboard that will allow us to very the data.

The Plan

We want to be able to:

View the health of out products
Survey Component
Real Time list of users

We will want to start off with the average rating of a product and a chart to show that. It will be it’s own liveview.

Start by using as CRC pattern to define the core module
Wrap it in a live view and then use SVG graphics to chart the results.
Make the chart interactive so that we can get a better and more refined results for the graph.
Then use the __using__ macro to make our chart helper easier to use.

We will need 3 things to make this happen:

First the Admin.DashboardLive. 
The route
The Survey data that will come from Admin.SurveyResultsLive

Define the Admin.Dashboard Concepts with Components

First let’s create the liveview with the file pento/lib/pento_web/live/admin/dashboard_live.ex

defmodule PentoWeb.Admin.DashboardLive do
    use PentoWeb, :live_view

    def mount(_params, _session, socket) do
        {:ok,
        socket
        |> assign(:survey_results_component_id, "survey-results")}
    end
end
Then we can create the route
live("/admin/dashboard", Admin.DashboardLive)

You want to make sure that this is within the “/“ scope and that we are withing a pipe that requires browser and the :require_authenticated_user, we can go into the requirements for an admin later. Next we can add that into our site menu.

<a href="/admin/dashboard" class="btn btn-ghost"> Dashboard </a>

Now that we have this I think we should talk about when we add in the Admin id what we would want to have in place to differentiate.

A new plug that will authorize an admin or redirects if not.
A new live_session block that will use a different on_mount that authorizes an admin. Maybe even an other UserAuth.on_mount/4 that will use the :admin to pattern match.

Now we can add in the html.heex but again in the current version on this we will use the live_view file that we just created and a render function.

def render(assigns) do
    ~H```elixir
    <section class="row">
        <h1 class="font-heavy text-3x1"> Admin Dashboard </h1>
    </section>
    '''
end

Now we can start up the server and head to the new dashboard.

Represent Dashboard Concepts with Components

We can model each part of the dashboard with a component.

Create a Component Module

We can start off with the survey results module. We will create a new file with the :live_component and the render it with the :survey_results_component_id, this is because we will need a live component not just a functional one as we will need the state of the parent to be able to make it inter-actable. Create this file pento/lib/pento_web/live admin/survey_results_live.ex

defmodule PentoWeb.Admin.SurveyResultsLive do
    use PentoWeb, :live_component

    def render(assigns) do
        ~H"""
        <section class="ml-8">
            <h2 class="font-light text-2xl">Survey Results</h2>
        </section>
        """
    end
end

Now we can add that to the dashboard. Replace the contents of the render of html.heex with

def render(assigns) do
    ~H"""
    <div class="container mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">Admin Dashboard</h1>
    <.live_component
        module={PentoWeb.Admin.SurveyResultsLive}
        id={@survey_results_component_id}
    />
    </div>
    """
end

Fetch Survey Results Data

Okay so now that we have the basic version of the layout let’s start to get data from the demographics and ratings. We will want to create a new function for the Catalog context that will use SQL.

Shape the Data with Ecto

For now we just need to know that we want the data to come to the charts looking like a set of Tuples

[
    {"Tic-Tac-Toe", 3.4285714285714284},
    {"Table Tennis", 2.5714285714285716},
    {"Chess", 2.625}
] 

For this we want it to return things that we know will work and can be repeatable. So this is a job for the Core. Use the file that we created pento/lib/pento/catalog/product query.ex

def with_average_ratings(query \\ base()) do
    query
    |> join_ratings
    |> average_ratings
end

defp join_ratings(query) do
    query
    |> join(:inner, [p], r in Rating, on: r.product_id == p.id)
end

defp average_ratings(query) do
    query
    |> group_by([p], p.id)
    |> select([p, r], {p.name, fragment("?::float", avg(r.stars))})
    |> order_by([p, r], [{:asc, p.name}])
end

Test Drive the Query

iex -S mix
iex> alias Pento.Catalog.Product
Pento.Catalog.Product
iex> alias Pento.Repo
Pento.Repo
iex> Product.Query.with_average_ratings() |> Repo.all()
...
[
{"Chess", 5.0}
{"Table Tennis", 2.0},
{"Tic-Tac-Toe", 1.0},
]

This is what we want and as such it should always work as long as the database is setup as we want.

Extend the Catalog Context

Now we get into the part that might not always work or will need some work, the boundary. We might get a {:ok, result} or {:error, reason}. So we have a way to leverage the new query so let’s put it to use within the catalog.ex

@doc """
Returns all the products with a user rating and organizes them by
their rating, this will return their average

## Examples

    iex> products_with_average_ratings()
    [{"Backgammon", 3.0},
    {"Checkers", 4.0},
    {"Chess", 5.0},
    {"Go", 2.0}]
"""
def products_with_average_ratings do
    Product.Query.with_average_ratings()
    |> Repo.all()
end                

Initialize the Admin.SurveyResultsLive Component State

Okay so now that we have the query and the way in which to use it within the Catalog we can now use it to set the products for our dashboard.

Head to pento/lib/pento_web/live/admin/survey_results_live.ex

alias Pento.Catalog

@impl true
def update(assigns, socket) do
    {:ok,
    socket
    |> assign(assigns)
    |> assign_products_with_average_ratings()}
end

def assign_products_with_average_ratings(socket) do
    socket
    |> assign(
    :products_with_average_ratings,
    Catalog.products_with_average_ratings()
    )
end

Here is a bit of code that you could add to your render (html.heex) that will show the products and rating for the products that you have with ratings in the db.

<.table
id="products"
rows={@products_with_average_ratings}
>
    <:col :let={{product, _rating}} label="Name">{product}</:col>
    <:col :let={{_product, rating}} label="Rating">{rating}</:col>
</.table>

Render SVG Charts with Contex

Now we want to turn this into a better version with a bar graph. For this we would like to make it server side with a new dependency so let’s head to the mix.exs and add a new one for this very purpose.

defp deps do
    [
        ...,
        {:contex, "~> 0.4.0"},
        ...
    ]

Then ofc get the new dependencies

mix deps.get
Resolving Hex dependencies...
...
New:
contex 0.4.0
nimble_strftime 0.1.1
* Getting contex (Hex package)
* Getting nimble_strftime (Hex package)
...

Initialize the Dataset

Context (using a CRC) provides us with a Dataset module to produce structs describing the state of the chart. Then the reducer manipulates the data, and the converter will make it into different charts.

Let’s head back to survey_results_live.ex and start to leverage the new data and library.

defmodule PentoWeb.Admin.SurveyResultsLive do
  use PentoWeb, :live_component

  alias Pento.Catalog

  ...

  @impl true
  def update(assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_products_with_average_ratings()
     |> assign_dataset()}
  end

  def assign_products_with_average_ratings(socket) do
    socket
    |> assign(
      :products_with_average_ratings,
      Catalog.products_with_average_ratings()
    )
  end

  def assign_dataset(
        %{
          assigns: %{
            products_with_average_ratings: products_with_average_ratings
          }
        } = socket
      ) do
    socket
    |> assign(
      :dataset,
      make_bar_chart_dataset(products_with_average_ratings)
    )
  end

  defp make_bar_chart_dataset(data) do
    Contex.Dataset.new(data)
  end
end

We have now used the new query that we made before to assigns the dataset to the socket, with the mar_bar_chart_dataset function that we also just made.

Initialize the BarChart

Now we can use that new data set into a bar chart with an other Contex function.

defp assign_chart(%{assigns: %{dataset: dataset}} = socket) do
    socket
    |> assign(:chart, make_bar_chart(dataset))
end

defp make_bar_chart(dataset) do
    Context.BarChart.new(dataset)
end

This is how the data will look like

    %Contex.Dataset{
        data: [
            {"Tic-Tac-Toe", 3.4285714285714284},
            {"Table Tennis", 2.5714285714285716},
            {"Chess", 2.625}
        ],
        headers: nil,
        title: nil
    }

Now we can add those new datasets to the assigns.

def update(assigns, socket) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign_products_with_average_ratings()
      |> assign_dataset()
      |> assign_chart}
end

The dataset for the BarChart will have columns that will look like

column_map: %{category_col: 0, value_cols: [1]}

This will help to make the columns for the bar chart.

Transform the Chart to SVG

Now we can add some SVG (Scalable Vector Graphics) This will involve some new helper functions as well as some Contex.Plot built-in functions. We will start off with the helper and plotters.

defmodule PentoWeb.Admin.SurveyResultsLive do
  ...
  alias Contex.Plot
  ...

  def update(assigns, socket) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign_products_with_average_ratings()
      |> assign_dataset()
      |> assign_chart
      |> assign_chart_svg()}
  end

  ...

  defp assign_chart(%{assigns: %{dataset: dataset}} = socket) do
    socket
    |> assign(:chart, make_bar_chart(dataset))
  end

  defp assign_chart_svg(%{assigns: %{chart: chart}} = socket) do
    socket
    |> assign(:chart_svg, render_bar_chart(chart))
  end

  defp make_bar_chart(dataset) do
    Contex.BarChart.new(dataset)
  end

  defp render_bar_chart(chart) do
    # This will make a 
    Plot.new(500, 400, chart)
    |> Plot.titles(title(), subtitle())
    |> Plot.axis_labels(x_axis(), y_axis())
    |> Plot.to_svg()
  end

  defp title do
    "Product Ratings"
  end

  defp subtitle do
    "average star ratings per product"
  end

  defp x_axis do
    "products"
  end

  defp y_axis do
    "stars"
  end
end

This is all that we need in order to get the data ready for the chart. We will need to now render the chart on our page with the render or html.heex Put this at the bottom of the section block.

      <div id="survey-results-chart" class="w-full overflow-x-auto">
        {@chart_svg}
      </div>

They also created some style for the chart that we can put into the assets/app.css, put this in its entirety into that file

survey-component-container {
  background-color: #fefefe;
  padding: 20px;
  border: 1px solid #888;
  width: 80%;
  margin-bottom: 20px;
}

.survey-component-container label {
  padding: 10px;
}

.survey-component-container input {
  margin-right: 10px;
}

.survey-component-container select {
  margin-right: 10px;
}

.survey-component-container h4 {
  font-weight: bold;
}

.fa.fa-star.checked {
  color: orange;
}

.fa.fa-star {
  padding: 3px;
}

.survey-component-container ul li {
  list-style: none;
}

.fa.fa-check.survey {
  color: green;
}

.exc-tick {
  stroke: grey;
}

.exc-tick text {
  fill: grey;
  stroke: none;
  font-size: 1.0rem;
}

.exc-grid {
  stroke: lightgrey;
}

.exc-legend {
  stroke: black;
}

.exc-legend text {
  fill: grey;
  font-size: 1.5rem;
  stroke: none;
}

.exc-title {
  fill: darkslategray;
  font-size: 1.5rem;
  stroke: none;
  padding-bottom: 10px;
}

.exc-subtitle {
  fill: darkgrey;
  font-size: 1rem;
  stroke: none;
}

.exc-domain {
  stroke: rgb(207, 207, 207);
}

.exc-barlabel-in {
  fill: white;
  font-size: 1.0rem;
}

.exc-barlabel-out {
  fill: grey;
  font-size: 0.7rem;
}

.float-container {
  padding: 20px;
}

.float-child {
  width: 33%;
  float: left;
  padding: 20px;
}

#survey-results-component {
  border: 1px solid;
}

#survey-results-chart {
  padding-right: 200px;
}

.survey-results-filters {
  padding-left: 2000px;
}

.user-activity-component,
.product-sales-component {
  border: 1px solid;
  padding: 10px;
  margin-top: 30px;
  margin-bottom: 30px;
}

.user-activity-component h2,
h3 {
  background: rebeccapurple;
  color: white;
  padding: 10px;
}

.user-activity-component ul,
p {
  padding-left: 20px;
}

Add Filters to Make Charts Interactive

Now we get to move on to make it more interactive. We will work on ways of filtering out data within our data set.

Filter By Age Group

We will want to build a form that will test for various age groups so that we can filter out data that we don’t wan’t. It will be in the form of a dropdown that we can choose from.

Establish Test Data

For this to work better we will want to build a large variety of data that we can use for this. In this case we go back to the pento/priv/repo/rating_seeds.exs (You will need to make this file),

# ---
# Excerpted from "Programming Phoenix LiveView",
# published by The Pragmatic Bookshelf.
# Copyrights apply to this code. It may not be used to create training material,
# courses, books, articles, and the like. Contact us if you are in doubt.
# We make no guarantees that this code is fit for any purpose.
# Visit https://pragprog.com/titles/liveview for more book information.
# ---
# Run this script with: mix run priv/repo/rating_seeds.exs
alias Pento.{Repo, Accounts, Survey}
alias Pento.Accounts.{User, Scope}
alias Pento.Survey.{Demographic, Rating}
alias Pento.Catalog.Product

users =
  for i <- 1..43,
      do:
        Accounts.register_user(%{
          email: "user#{i}@example.com",
          password: "passwordpassword"
        })

user_ids = Repo.all(User) |> Enum.map(& &1.id)

product_ids = Repo.all(Product) |> Enum.map(& &1.id)
genders = ["male", "female", "other", "prefer not to say"]
years = 1950..2005 |> Enum.to_list()
stars = 1..5 |> Enum.to_list()

for user_id <- user_ids do
  scope = %Scope{user: %User{id: user_id}}
  gender = Enum.random(genders)
  year_of_birth = Enum.random(years)

  Survey.create_demographic(scope, %{
    gender: gender,
    year_of_birth: year_of_birth
  })
end

# Create ratings for users and products
for user_id <- user_ids,
    product_id <- product_ids,
    Enum.random([true, false, false]) do
  # Check if rating already exists
  existing = Repo.get_by(Rating, user_id: user_id, product_id: product_id)

  unless existing do
    scope = %Scope{user: %User{id: user_id}}

    %Rating{}
    |> Rating.changeset(
      %{
        user_id: user_id,
        product_id: product_id,
        stars: Enum.random(stars)
      },
      scope
    )
    |> Repo.insert()
  end
end

Then you can run the script with

mix run priv/repo/rating_seeds.exs

Build the Age Group Query Filters

Now we can start to use more of the query.ex to add more filters to data set and then get the right users, with a variable for the age group.

  def join_users(query \\ base()) do
    query
    |> join(:left, [p, r], u in User, on: r.user_id == u.id)
  end

  def join_demographics(query \\ base()) do
    query
    |> join(:left, [p, r, u], d in Demographic, on: d.user_id == u.id)
  end

  def filter_by_age_group(query \\ base(), filter) do
    query
    |> apply_age_group_filter(filter)
  end

To go over this a bit we have:

p for product, r for results, u for user and d for demographics
join(:left, [p,r,u, d], d in Demographic, on: d.user_id == u.id)

means:

a :left join
that returns [products, results, users, and demographics]
where the id on the user is the same as the user_id on the demographic

Now we can build the reducer to filter by the age group.

  def filter_by_age_group(query \\ base(), filter) do
    query
    |> apply_age_group_filter(filter)
  end

  defp apply_age_group_filter(query, "18 and under") do
    birth_year = DateTime.utc_now().year - 18

    query
    |> where([p, r, u, d], d.year_of_birth >= ^birth_year)
  end

  defp apply_age_group_filter(query, "18 to 25") do
    birth_year_max = DateTime.utc_now().year - 18
    birth_year_min = DateTime.utc_now().year - 25

    query
    |> where(
      [p, r, u, d],
      d.year_of_birth >= ^birth_year_min and d.year_of_birth <= ^birth_year_max
    )
  end

  defp apply_age_group_filter(query, "25 to 35") do
    birth_year_max = DateTime.utc_now().year - 25
    birth_year_min = DateTime.utc_now().year - 35

    query
    |> where(
      [p, r, u, d],
      d.year_of_birth >= ^birth_year_min and d.year_of_birth <= ^birth_year_max
    )
  end

  defp apply_age_group_filter(query, "35 and up") do
    birth_year = DateTime.utc_now().year - 35

    query
    |> where([p, r, u, d], d.year_of_birth <= ^birth_year)
  end

  defp apply_age_group_filter(query, _filter) do
    query
  end

This is a lot of the same code but we are doing it based off the different age groups that we want to filter by. Now we can go back to the catalog.ex and add in the new queries.

  @doc """
  Returns all the ratings for a set of products based off the age group
  that we filter it with.

  ## Examples

    iex> products_with_average_ratings(%{age_group_filter: "18-25"})
    [{"Backgammon", 3.0},
    {"Checkers", 4.0},
    {"Chess", 5.0},
    {"Go", 2.0}]
  """
  def products_with_average_ratings(%{
        age_group_filter: age_group_filter
      }) do
    Product.Query.with_average_ratings()
    |> Product.Query.join_users()
    |> Product.Query.join_demographics()
    |> Product.Query.filter_by_age_group(age_group_filter)
    |> Repo.all()
  end

Your Turn: Test Drive the Query

iex -S mix
iex(1)> alias Pento.Catalog
Pento.Catalog
iex(2)> Catalog.products_with_average_ratings(%{age_group_filter: "18-25"})
[debug] QUERY OK source="products" db=1.2ms decode=1.3ms queue=1.6ms idle=1173.7ms
SELECT p0."name", avg(r1."stars")::float FROM "products" AS p0 INNER JOIN "ratings" AS r1 ON r1."product_id" = p0."id" LEFT OUTER JOIN "users" AS u2 ON r1."user_id" = u2."id" LEFT OUTER JOIN "demographics" AS d3 ON d3."user_id" = u2."id" GROUP BY p0."id" ORDER BY p0."name" []
↳ :elixir.eval_external_handler/3, at: src/elixir.erl:386
[
  {"Backgammon", 3.375},
  {"Checkers", 3.2666666666666666},
  {"Chess", 3.857142857142857},
  {"Go", 2.6315789473684212}
]
That should be how the data is returned to you with the new function.

Add the Age Group Filter to Component State

Now that we have this query and function setup we will need to be able to use it to filter data. With that being said we will want to set the initial state of the query for when we first join the page and then update it as we change the form. We will want to do the following:

Set the initial state to "all"
Display a drop down for the different filters.
Respond to the event that it changes with an other call to Pento.Catalog.products_with_average_ratings(filter)

Let’s add the age_group_filter to the assigns. pento/lib/pento_web/live/admin survey_results_live.ex

@impl true
  def update(assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_age_group_filter()
     |> assign_products_with_average_ratings()
     |> assign_dataset()
     |> assign_chart()
     |> assign_chart_svg()}
  end

  def assign_age_group_filter(socket) do
    socket
    |> assign(:age_group_filter, "all")
  end

  def assign_products_with_average_ratings(
        %{assigns: %{age_group_filter: age_group_filter}} = socket
      ) do
    socket
    |> assign(
      :products_with_average_ratings,
      Catalog.products_with_average_ratings(%{age_group_filter: age_group_filter})
    )
  end

This took the standard way to assign the ratings and then made it so we can leverage the filter that we will set. This will not change anything on the page until we have dealt with more events and then set the form for the age-group.

Send Age Group Filter Events

Let’s start out by building the html for the form that we will build.

<section class="ml-8">
    <h2 class="font-light text-2xl">Survey Results</h2>
    <div id="survey-results-component">
    <div class="container">
        <div class="form-control w-full">
        <.form
            for={%{}}
            as={:age_group_filter}
            phx-change="age_group_filter"
            phx-target={@myself}
            id="age-group-form"
        >
            <label class="label">
            <span class="label-text text-sm">By age group:</span>
            </label>
            <select
            name="age_group_filter"
            id="age_group_filter"
            class="select select-bordered select-sm w-full"
            >
            <%= for age_group <-
["all", "18 and under", "18 to 25", "25 to 35", "35 and up"] do %>
                <option value={age_group} selected={@age_group_filter == age_group}>
                {age_group}
                </option>
            <% end %>
            </select>
        </.form>
        </div>
    </div>
    </div>
    <div id="survey-results-chart">
    {@chart_svg}
    </div>
</section>

For this we are not having to run a changeset for each change and as such we are asking it to build a map for us. We also didn’t need to create a form for the filter as again there is no need. We do need to worry about what kind of data that we are getting from the form. Open up an iex session with these inputs.

iex
iex> i %{}
Term
%{}
...
Implemented protocols:
    Collectable, Enumerable, IEx.Info, ... Phoenix.HTML.FormData, ...
iex> i Pento.Catalog.change_product(%Pento.Catalog.Product{}, %{id: 1})
Term
#Ecto.Changeset<...>
Implemented protocols
    IEx.Info, Inspect, Jason.Encoder, ..., Phoenix.HTML.FormData, ...

Notice how both the %{} and the change_product both implement the Phoenix.HTML.FormData

Okay now that we have the right data set and we are leveraging the phx-change to deal with any change to the form we will not be able to start the event-handler for the “age_group_filer” event.

@impl true
  def handle_event("age_group_filter", %{"age_group_filter" => age_group}, socket) do
    {:noreply,
     socket
     |> assign_age_group_filter(age_group)
     |> assign_products_with_average_ratings()
     |> assign_dataset()
     |> assign_chart()
     |> assign_chart_svg()}
  end

  def assign_age_group_filter(socket, age_group) do
    socket
    |> assign(:age_group_filter, age_group)
  end

Okay last but not least there is an event that will crash the page and that is when nil results are returned as the Context will break. So we need to add in some logic for that. First we head to

#query.ex
def with_zero_ratings(query \\ base()) do
    query
    |> select([p], {p.name, 0})
end

# catalog.ex
@doc """
    Returns all the products when we pass in a set of values that will not
    return any products

    ## Examples

    iex> products_with_zero_ratings()
    [{"Backgammon", 3.0},
    {"Checkers", 4.0},
    {"Chess", 5.0},
    {"Go", 2.0}]
    """
def products_with_zero_ratings do
    Product.Query.with_zero_ratings()
    |> Repo.all()
end


# survey_results_live.ex
def assign_products_with_average_ratings(
        %{assigns: %{age_group_filter: age_group_filter}} =
          socket
      ) do
    assign(
      socket,
      :products_with_average_ratings,
      get_products_with_average_ratings(%{age_group_filter: age_group_filter})
    )
end

defp get_products_with_average_ratings(filter) do
    case Catalog.products_with_average_ratings(filter) do
      [] ->
        Catalog.products_with_zero_ratings()

      products ->
        products
    end
end

Refactor Chart Code with Macros

Here is where the good stuff happens. We are going to start to use macros to make even more happen. We will can start to pull out some code in a using macro.

Refactor with using

At the very top od the every live view we have the line use PentoWeb, :liveview. The use directive calls the _using macro that in turns injects code into our live view modules. That is calling the pento/lib/pento_web.ex

  def live_view do
    quote do
      use Phoenix.LiveView

      unquote(html_helpers())
    end
  end

  ...

  @doc """
  When used, dispatch to the appropriate controller/live_view/etc.
  """
  defmacro __using__(which) when is_atom(which) do
    apply(__MODULE__, which, [])
  end

You can see at the end there is a defmacro using, again this is a way to inject code into a file or block of code.

    use PentoWeb, :live_view 
    # calls 
    __using__ function with a which value of :live_view
    # calls 
    live_view
    # calls 
    quote do
        use Phoenix.LiveView

        unquote(html_helpers())

Extract Common Helpers

First we want to define a new file that will hold everything that we want in the code. pento/lib/pento_web/bar_chart.ex

defmodule PentoWeb.BarChart do
  alias Contex.{Dataset, BarChart, Plot}

  def make_bar_chart_dataset(data) do
    Dataset.new(data)
  end

  def make_bar_chart(dataset) do
    BarChart.new(dataset)
  end

  def render_bar_chart(chart, title, subtitle, x_axis, y_axis) do
    Plot.new(500, 400, chart)
    |> Plot.titles(title, subtitle)
    |> Plot.axis_labels(x_axis, y_axis)
    |> Plot.to_svg()
  end
end

This took all our chart-specific functions and added them all into one. There might be some things that we don’t want hard coded within this so we didn’t include the x_axis and y_axis names etc.

Import the Charting Module

Okay so now we need to deal with the code that will import the code to live. Quote is responsible for injecting code, so we can head to pento_web.ex and add in a function chart_helpers.

  defp chart_helpers do
    quote do
      import PentoWeb.BarChart
    end
  end
This is a way to be sure that when we call the macro we will use the right import

Inject the Code with using

Now we can make the using definition so that it will call the chart_helpers and the other code we need.

  def chart_live do
    quote do
      use Phoenix.LiveComponent

      unquote(html_helpers())
      unquote(chart_helpers())
    end
  end

This just added a way to inject the code that is within the bar_chart.ex

Use the Macro

Now we can leverage that within the survey_results_live.ex add this line to the top

use PentoWeb, :chart_live

We now can remove the make_bar_chart_dataset/1, make_bar_chart/2 and render_bar_chart/5 so that the assign_chart_svg/1 looks like this.

  defp assign_chart_svg(%{assigns: %{chart: chart}} = socket) do
    socket
    |> assign(:chart_svg, render_bar_chart(chart, title(), subtitle(), x_axis(), y_axis()))
  end

That is all you need, you were able to make a new macro that injects all the code needed to deal with a bar graph.