defmodule SelectoComponents.Views.Graph.Form do
use Phoenix.LiveComponent
def render(assigns) do
graph_view_key = current_view_key(assigns[:view])
graph_view = view_state(assigns[:view_config], graph_view_key)
assigns =
assigns
|> Map.put(:graph_view_key, graph_view_key)
|> Map.put(:graph_chart_type, map_get(graph_view, :chart_type, "bar"))
|> Map.put(:graph_x_axis, map_get(graph_view, :x_axis, []))
|> Map.put(:graph_y_axis, map_get(graph_view, :y_axis, []))
|> Map.put(:graph_series, map_get(graph_view, :series, []))
|> Map.put(:graph_options, map_get(graph_view, :options, %{}))
~H"""
<div class="space-y-6">
<!-- Chart Type Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Chart Type</label>
<select
name="chart_type"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="bar" selected={@graph_chart_type == "bar"}>Bar Chart</option>
<option value="line" selected={@graph_chart_type == "line"}>Line Chart</option>
<option value="pie" selected={@graph_chart_type == "pie"}>Pie Chart</option>
<option value="scatter" selected={@graph_chart_type == "scatter"}>Scatter Plot</option>
<option value="area" selected={@graph_chart_type == "area"}>Area Chart</option>
</select>
</div>
<!-- X-Axis Configuration -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-3">X-Axis (Categories)</h3>
<.live_component
module={SelectoComponents.Components.ListPicker}
id={"#{@graph_view_key}_x_axis"}
fieldname="x_axis"
view={@view}
available={
Enum.filter(@columns, fn {_f, _n, format} -> format not in [:component, :link] end)
}
selected_items={@graph_x_axis}
>
<:item_summary :let={{_id, item, config, _index}}>
<% col = Selecto.field(@selecto, item) %>
<span class="truncate"><%= summary_title(config, graph_column_name(col, item)) %></span>
<span class="truncate text-sm font-normal text-base-content/60"><%= graph_x_axis_summary(col, config) %></span>
</:item_summary>
<:item_form :let={{id, item, config, index}}>
<input name={"x_axis[#{id}][field]"} type="hidden" value={item} />
<input name={"x_axis[#{id}][index]"} type="hidden" value={index} />
<.live_component
module={SelectoComponents.Views.Graph.XAxisConfig}
id={"#{@graph_view_key}-x-axis-config-#{id}"}
col={Selecto.field(@selecto, item)}
uuid={id}
item={item}
fieldname="x_axis"
prefix={"x_axis[#{id}]"}
config={config}
/>
</:item_form>
</.live_component>
</div>
<!-- Y-Axis Configuration -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-3">Y-Axis (Values)</h3>
<.live_component
module={SelectoComponents.Components.ListPicker}
id={"#{@graph_view_key}_y_axis"}
fieldname="y_axis"
view={@view}
available={@columns}
selected_items={@graph_y_axis}
>
<:item_summary :let={{_id, item, config, _index}}>
<% col = Selecto.field(@selecto, item) %>
<span class="truncate"><%= summary_title(config, graph_column_name(col, item)) %></span>
<span class="truncate text-sm font-normal text-base-content/60"><%= graph_y_axis_summary(config) %></span>
</:item_summary>
<:item_form :let={{id, item, config, index}}>
<input name={"y_axis[#{id}][field]"} type="hidden" value={item} />
<input name={"y_axis[#{id}][index]"} type="hidden" value={index} />
<.live_component
module={SelectoComponents.Views.Graph.YAxisConfig}
id={"#{@graph_view_key}-y-axis-config-#{id}"}
col={Selecto.field(@selecto, item)}
uuid={id}
item={item}
fieldname="y_axis"
prefix={"y_axis[#{id}]"}
config={config}
/>
</:item_form>
</.live_component>
</div>
<!-- Series Configuration (Optional) -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-3">Series Grouping (Optional)</h3>
<p class="text-sm text-gray-600 mb-3">
Add a secondary grouping to create multiple data series in your chart.
</p>
<.live_component
module={SelectoComponents.Components.ListPicker}
id={"#{@graph_view_key}_series"}
fieldname="series"
view={@view}
available={
Enum.filter(@columns, fn {_f, _n, format} -> format not in [:component, :link] end)
}
selected_items={@graph_series}
>
<:item_summary :let={{_id, item, config, _index}}>
<% col = Selecto.field(@selecto, item) %>
<span class="truncate"><%= summary_title(config, graph_column_name(col, item)) %></span>
<span class="truncate text-sm font-normal text-base-content/60"><%= graph_series_summary(col, config) %></span>
</:item_summary>
<:item_form :let={{id, item, config, index}}>
<input name={"series[#{id}][field]"} type="hidden" value={item} />
<input name={"series[#{id}][index]"} type="hidden" value={index} />
<.live_component
module={SelectoComponents.Views.Graph.SeriesConfig}
id={"#{@graph_view_key}-series-config-#{id}"}
col={Selecto.field(@selecto, item)}
uuid={id}
item={item}
fieldname="series"
prefix={"series[#{id}]"}
config={config}
/>
</:item_form>
</.live_component>
</div>
<!-- Chart Options -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-3">Chart Options</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Chart Title</label>
<input
name="options[title]"
type="text"
value={option_value(@graph_options, :title, "")}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter chart title"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">X-Axis Label</label>
<input
name="options[x_axis_label]"
type="text"
value={option_value(@graph_options, :x_axis_label, "")}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="X-axis label"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Y-Axis Label</label>
<input
name="options[y_axis_label]"
type="text"
value={option_value(@graph_options, :y_axis_label, "")}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Y-axis label"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Y2-Axis Label</label>
<input
name="options[y2_axis_label]"
type="text"
value={option_value(@graph_options, :y2_axis_label, "")}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Secondary Y-axis label"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Legend Position</label>
<select
name="options[legend_position]"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="top" selected={option_value(@graph_options, :legend_position) == "top"}>
Top
</option>
<option
value="bottom"
selected={option_value(@graph_options, :legend_position) == "bottom"}
>
Bottom
</option>
<option value="left" selected={option_value(@graph_options, :legend_position) == "left"}>
Left
</option>
<option
value="right"
selected={option_value(@graph_options, :legend_position) == "right"}
>
Right
</option>
<option value="none" selected={option_value(@graph_options, :legend_position) == "none"}>
Hide Legend
</option>
</select>
</div>
</div>
<div class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-4">
<label class="flex items-center">
<input
name="options[show_grid]"
type="checkbox"
value="true"
checked={option_checked(@graph_options, :show_grid, false)}
class="mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
<span class="text-sm text-gray-700">Show Grid Lines</span>
</label>
<label class="flex items-center">
<input
name="options[enable_animations]"
type="checkbox"
value="true"
checked={option_checked(@graph_options, :enable_animations, true)}
class="mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
<span class="text-sm text-gray-700">Enable Animations</span>
</label>
<label class="flex items-center">
<input
name="options[responsive]"
type="checkbox"
value="true"
checked={option_checked(@graph_options, :responsive, true)}
class="mr-2 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500"
/>
<span class="text-sm text-gray-700">Responsive</span>
</label>
</div>
</div>
</div>
"""
end
defp current_view_key({id, _mod, _name, _opts}) when is_atom(id), do: id
defp current_view_key(_), do: :graph
defp view_state(view_config, view_key) do
view_config
|> map_get(:views, %{})
|> map_get(view_key, %{})
end
defp map_get(map, key, default) when is_map(map) and is_atom(key) do
Map.get(map, key, Map.get(map, Atom.to_string(key), default))
end
defp map_get(map, key, default) when is_map(map) and is_binary(key) do
Map.get(map, key, default)
end
defp map_get(_map, _key, default), do: default
defp option_value(options, key, default \\ nil) do
map_get(options, key, default)
end
defp option_checked(options, key, default) do
case option_value(options, key, default) do
true -> true
false -> false
"true" -> true
"false" -> false
nil -> default
_ -> default
end
end
defp graph_column_name(col, item) do
cond do
is_map(col) and Map.get(col, :name) -> col.name
true -> to_string(item || "")
end
end
defp summary_title(config, field_name) do
case Map.get(config || %{}, "alias", "") do
value when value in [nil, ""] -> field_name
value -> "#{value} / #{field_name}"
end
end
defp graph_x_axis_summary(col, config) do
cond do
Map.get(config || %{}, "format") not in [nil, ""] ->
format_summary_label(Map.get(config, "format"))
Map.get(config || %{}, "sort") not in [nil, ""] ->
"sort #{Map.get(config, "sort")}"
Map.get(col || %{}, :type, :string) in [:string, :text] and
Map.get(config || %{}, "max_length") not in [nil, ""] ->
"max #{Map.get(config, "max_length")}"
true ->
"default"
end
end
defp graph_y_axis_summary(config) do
function = Map.get(config || %{}, "function", "count")
axis = Map.get(config || %{}, "axis", "left")
"#{function} on #{axis} axis"
end
defp graph_series_summary(col, config) do
cond do
Map.get(config || %{}, "format") not in [nil, ""] ->
format_summary_label(Map.get(config, "format"))
Map.get(config || %{}, "max_series") not in [nil, "", "10"] ->
"max #{Map.get(config, "max_series")}"
Map.get(col || %{}, :type, :string) in [:naive_datetime, :utc_datetime, :date] ->
"date grouping"
true ->
"default grouping"
end
end
defp format_summary_label(value) do
value
|> SelectoComponents.Helpers.aggregate_datetime_format_label()
|> String.downcase()
end
end