~80 data components — 16 SVG chart types, 9 table variants, data grid with 19 sub-components, tree views (standard + 10 enhanced variants), Kanban board, filters, and analytics widgets.

Modules:

  • PhiaUi.Components.Data — charts, tables, data_grid, kanban, filters
  • PhiaUi.Components.TreeEnhanced — icon_tree, checkbox_tree, searchable_tree, file_tree, lazy_tree, virtual_tree (v0.1.11)
import PhiaUi.Components.Data
import PhiaUi.Components.TreeEnhanced

Table of Contents

Charts

Analytics Widgets

Tables

Data Grid

Kanban & Filters

Tree Views


Charts

All charts are pure SVG — zero JavaScript, zero npm packages. They use @keyframes for animations and prefers-reduced-motion guards.

bar_chart

<.bar_chart
  series={[
    %{name: "Revenue", data: [120, 145, 98, 167, 200, 189]},
    %{name: "Expenses", data: [80, 90, 70, 110, 130, 120]}
  ]}
  labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
  height={280}
  animate={true}
/>

line_chart

<.line_chart
  series={[%{name: "Users", data: [100, 120, 115, 140, 180, 165]}]}
  labels={["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]}
  height={240}
  show_dots={true}
  curve={:smooth}
/>

area_chart

<.area_chart
  series={[%{name: "Signups", data: [12, 18, 15, 25, 30, 28]}]}
  labels={["Jan", "Feb", "Mar", "Apr", "May", "Jun"]}
  height={200}
  fill_opacity={0.2}
/>

pie_chart / donut_chart

<.pie_chart
  series={[%{name: "Direct", value: 45}, %{name: "Referral", value: 30}, %{name: "Social", value: 25}]}
  height={240}
/>

<.donut_chart
  series={[%{name: "Chrome", value: 65}, %{name: "Safari", value: 18}, %{name: "Firefox", value: 10}]}
  height={240}
  center_label="Browsers"
/>

Other chart types

<%!-- Radar — compare multiple metrics across categories --%>
<.radar_chart series={@radar_series} labels={@radar_labels} height={300} />

<%!-- Scatter — correlation plots --%>
<.scatter_chart series={@scatter_series} height={280} />

<%!-- Bubble — scatter with size dimension --%>
<.bubble_chart series={@bubble_series} height={280} />

<%!-- Histogram — frequency distribution --%>
<.histogram_chart data={@values} bins={20} height={200} />

<%!-- Waterfall — running total / bridge chart --%>
<.waterfall_chart series={@waterfall_series} labels={@labels} height={240} />

<%!-- Heatmap — intensity grid (GitHub contribution style) --%>
<.heatmap_chart data={@daily_counts} height={120} />

<%!-- Treemap — hierarchical area chart --%>
<.treemap_chart data={@tree_data} height={300} />

<%!-- Timeline chart — Gantt-style event bars --%>
<.timeline_chart series={@events} height={200} />

Analytics Widgets

gauge_chart

Semicircular gauge with zones and threshold marker.

<.gauge_chart
  value={72}
  min={0}
  max={100}
  zones={[{0, 30, "red"}, {30, 70, "orange"}, {70, 100, "green"}]}
  threshold={60}
  animate={true}
/>

sparkline_card

Mini line/area sparkline inside a stat card.

<.sparkline_card
  title="Weekly Revenue"
  value="$24,500"
  delta={"+12%"}
  delta_type={:increase}
  data={[45, 52, 48, 61, 58, 67, 72]}
  trend={:up}
/>

uptime_bar

Segment bar showing historical uptime (GitHub-style).

<.uptime_bar
  segments={@uptime_segments}
  label="API"
  current_status={:up}
/>

badge_delta

Delta value with directional colour.

<.badge_delta value="+12.5%" delta_type={:increase} />
<.badge_delta value="-3.2%" delta_type={:decrease} />
<.badge_delta value="0%" delta_type={:neutral} />

bar_list

Horizontal bar list — great for top-10 breakdowns.

<.bar_list
  items={[
    %{name: "Homepage", value: 4523, href: "/"},
    %{name: "Pricing", value: 3201, href: "/pricing"},
    %{name: "Docs", value: 1987, href: "/docs"}
  ]}
  value_label="Page views"
/>

category_bar

Single horizontal bar split into coloured segments.

<.category_bar
  categories={[
    %{name: "Direct", value: 45, color: "blue"},
    %{name: "Referral", value: 30, color: "violet"},
    %{name: "Social", value: 25, color: "pink"}
  ]}
/>

meter_group

Multiple labeled meter bars stacked vertically.

<.meter_group
  items={[
    %{label: "Storage", value: 68, max: 100, unit: "GB"},
    %{label: "Bandwidth", value: 12, max: 50, unit: "GB"},
    %{label: "API calls", value: 1200, max: 10000, unit: "reqs"}
  ]}
/>

Tables

table

Base sortable table. Used by most table variants.

<.table id="orders" rows={@orders} sort_key={@sort_key} sort_dir={@sort_dir} on_sort="sort">
  <:col :let={order} key="id" label="Order">#<%= order.id %></:col>
  <:col :let={order} key="customer" label="Customer"><%= order.customer %></:col>
  <:col :let={order} key="total" label="Total" align={:right}>
    $<%= order.total %>
  </:col>
  <:col :let={order} key="status" label="Status">
    <.badge variant={status_variant(order.status)}><%= order.status %></.badge>
  </:col>
</.table>

data_table

Column-definition driven table — define columns as data structures.

<.data_table
  id="users-dt"
  rows={@users}
  columns={[
    %{key: "name", label: "Name", sortable: true},
    %{key: "email", label: "Email"},
    %{key: "role", label: "Role"},
    %{key: "created_at", label: "Joined", type: :date}
  ]}
  on_sort="sort_users"
/>

expandable_table

Rows that expand to reveal a detail panel.

<.expandable_table id="orders-exp" rows={@orders}>
  <:col :let={order} label="Order">#<%= order.id %></:col>
  <:col :let={order} label="Total">$<%= order.total %></:col>
  <:row_detail :let={order}>
    <div class="p-4 grid grid-cols-3 gap-4">
      <%= for item <- order.items do %>
        <div><%= item.name %> × <%= item.qty %></div>
      <% end %>
    </div>
  </:row_detail>
</.expandable_table>

responsive_table

Stacks as labeled key-value pairs on mobile.

<.responsive_table id="contacts" rows={@contacts}>
  <:col :let={c} key="name" label="Name"><%= c.name %></:col>
  <:col :let={c} key="email" label="Email"><%= c.email %></:col>
  <:col :let={c} key="phone" label="Phone"><%= c.phone %></:col>
</.responsive_table>

Data Grid

Full-featured data grid: sorting, density, pinning, grouping, aggregation, CSV export, filter chips.

<.data_grid id="products-grid" rows={@products} sort_key={@sort} sort_dir={@dir}
  loading={@loading} sticky_header={true}>
  <:toolbar>
    <.data_grid_density_toggle density={@density} on_change="set_density" />
    <.data_grid_active_filters filters={@active_filters} on_remove="remove_filter" />
    <.data_grid_export_button id="export" target_id="products-grid" filename="products.csv" label="Export CSV" />
  </:toolbar>

  <.data_grid_head sort_key="name" on_sort="sort">Name</.data_grid_head>
  <.data_grid_head sort_key="price" on_sort="sort">Price</.data_grid_head>
  <.data_grid_head sort_key="stock" on_sort="sort">Stock</.data_grid_head>
  <.data_grid_head>Actions</.data_grid_head>

  <:row :let={product}>
    <.data_grid_cell><%= product.name %></.data_grid_cell>
    <.data_grid_cell align={:right}>$<%= product.price %></.data_grid_cell>
    <.data_grid_cell align={:right}><%= product.stock %></.data_grid_cell>
    <.data_grid_cell>
      <.icon_button icon="pencil" label="Edit" phx-click="edit" phx-value-id={product.id} variant="ghost" size="sm" />
    </.data_grid_cell>
  </:row>

  <%!-- Aggregation footer row --%>
  <.data_grid_aggregation_row>
    <.data_grid_cell>Totals</.data_grid_cell>
    <.data_grid_cell align={:right}>$<%= @total_value %></.data_grid_cell>
    <.data_grid_cell align={:right}><%= @total_stock %></.data_grid_cell>
    <.data_grid_cell />
  </.data_grid_aggregation_row>
</.data_grid>

data_grid_column_group

Multi-level column headers spanning multiple columns.

<thead>
  <tr>
    <.data_grid_column_group colspan={2} label="Identity" />
    <.data_grid_column_group colspan={3} label="Work Details" />
  </tr>
  <tr>
    <.data_grid_head>Name</.data_grid_head>
    <.data_grid_head>Email</.data_grid_head>
    <.data_grid_head>Department</.data_grid_head>
    <.data_grid_head>Title</.data_grid_head>
    <.data_grid_head>Salary</.data_grid_head>
  </tr>
</thead>

data_grid_pinned_row

Sticky summary row at the top or bottom of the grid.

<.data_grid_pinned_row position={:bottom}>
  <.data_grid_cell><strong>Total</strong></.data_grid_cell>
  <.data_grid_cell align={:right}><strong>$<%= @grand_total %></strong></.data_grid_cell>
</.data_grid_pinned_row>

data_grid_detail_row

Full-width expandable detail panel inside a row group.

<.data_grid_detail_row colspan={5}>
  <div class="p-4 bg-muted/30 rounded-lg">
    <h4 class="font-medium">Order details</h4>
    <%= for item <- @expanded_order.items do %>
      <div class="flex justify-between py-1"><%= item.name %> <span>$<%= item.price %></span></div>
    <% end %>
  </div>
</.data_grid_detail_row>

data_grid_group_row

Collapsible group header inside the grid body.

<.data_grid_group_row
  label="Electronics"
  count={42}
  expanded={@expanded_groups["electronics"]}
  value="electronics"
  colspan={5}
  on_toggle="toggle_group"
/>

Filter Chips

<%!-- Active filter chip --%>
<.data_grid_filter_chip label="Category: Electronics" value="category" on_remove="remove_filter" />

<%!-- Full active filters bar --%>
<.data_grid_active_filters
  filters={[
    %{label: "Category: Electronics", value: "category"},
    %{label: "Price: > $100", value: "price"}
  ]}
  on_remove="remove_filter"
/>

Kanban Board

<.kanban_board id="tasks-board" on_move="move_card">
  <%= for column <- @columns do %>
    <.kanban_column id={"col-#{column.id}"} title={column.title} count={length(column.cards)}>
      <%= for card <- column.cards do %>
        <.kanban_card
          id={"card-#{card.id}"}
          title={card.title}
          description={card.description}
          assignee={card.assignee}
          due_date={card.due_date}
          priority={card.priority}
          phx-click="open_card"
          phx-value-id={card.id}
        />
      <% end %>
    </.kanban_column>
  <% end %>
</.kanban_board>

Hook: PhiaKanban


Tree Views

tree / tree_item

Zero-JS expand/collapse via native <details>/<summary>. WAI-ARIA roles.

<.tree id="file-browser">
  <.tree_item label="src" expandable={true}>
    <.tree_item label="app.ex" />
    <.tree_item label="router.ex" />
    <.tree_item label="components" expandable={true}>
      <.tree_item label="ui" expandable={true}>
        <.tree_item label="button.ex" />
      </.tree_item>
    </.tree_item>
  </.tree_item>
  <.tree_item label="mix.exs" />
</.tree>

icon_tree (v0.1.11)

Tree with Lucide icons per node, badges, and href links.

<.icon_tree id="nav-tree">
  <.icon_tree_item label="Settings" icon="settings" href="/settings" />
  <.icon_tree_item label="Users" icon="users" expandable={true}>
    <.icon_tree_item label="Admins" icon="shield" badge={@admin_count} href="/users/admins" />
    <.icon_tree_item label="Members" icon="user" badge={@member_count} href="/users/members" />
  </.icon_tree_item>
</.icon_tree>

checkbox_tree (v0.1.11)

Tree with tri-state checkboxes for permission or category selection.

<.checkbox_tree id="permissions-tree">
  <.checkbox_tree_item id="perm-read" label="Read" value="read" checked={true} />
  <.checkbox_tree_item id="perm-write" label="Write" value="write" checked={false} indeterminate={true}>
    <.checkbox_tree_item id="perm-create" label="Create" value="create" checked={false} />
    <.checkbox_tree_item id="perm-update" label="Update" value="update" checked={true} />
  </.checkbox_tree_item>
</.checkbox_tree>

searchable_tree (v0.1.11)

Tree with a debounced search input — filtering logic is in LiveView.

<.searchable_tree id="cat-tree" search_value={@query} on_search="search_categories">
  <%= for cat <- @filtered_categories do %>
    <.icon_tree_item label={cat.name} icon="folder" />
  <% end %>
</.searchable_tree>

file_tree (v0.1.11)

File system browser with extension-based icons.

<.file_tree id="project-files">
  <.file_tree_item label="lib" type={:folder} expanded={true}>
    <.file_tree_item label="app.ex" type={:file} />
    <.file_tree_item label="logo.png" type={:file} />
    <.file_tree_item label="config.json" type={:file} />
  </.file_tree_item>
  <.file_tree_item label="mix.exs" type={:file} />
  <.file_tree_item label="README.md" type={:file} />
</.file_tree>

lazy_tree (v0.1.11)

Tree with deferred children loading — fires a LiveView event on first expand.

<.lazy_tree id="org-tree" on_expand="load_children">
  <.lazy_tree_item id="node-root" label="Engineering" expandable={true} loaded={false} loading={false} />
</.lazy_tree>
# In your LiveView
def handle_event("load_children", %{"id" => id}, socket) do
  children = fetch_children(id)
  {:noreply, assign(socket, tree_nodes: insert_children(socket.assigns.tree_nodes, id, children))}
end

virtual_tree (v0.1.11)

Virtualised tree for very large datasets. Hook: PhiaVirtualTree.

<.virtual_tree
  id="big-tree"
  nodes={@flat_nodes}
  row_height={32}
  height={400}
/>

nodes is a flat list of %{id, label, depth, expandable, expanded} maps. Hook manages DOM virtualization and fires "virtual_tree_select" on click.


filter_bar / filter_builder

<%!-- Simple filter bar with preset filter chips --%>
<.filter_bar filters={@filters} on_change="set_filters" on_clear="clear_filters" />

<%!-- Advanced dynamic filter builder --%>
<.filter_builder
  id="adv-filters"
  fields={[
    %{key: "name", label: "Name", type: :text},
    %{key: "status", label: "Status", type: :select, options: ["active", "inactive"]},
    %{key: "created_at", label: "Created", type: :date}
  ]}
  on_apply="apply_filters"
/>