[![test](https://github.com/elinverd/luminous/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/elinverd/luminous/actions/workflows/test.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/luminous)](https://hex.pm/packages/luminous)
# Luminous
Luminous is a framework for creating dashboards within [Phoenix Live
View](https://www.phoenixframework.org/).
Dashboards are defined by the client application (framework consumer)
using elixir code and consist of Panels (`Luminous.Panel`) which are
responsible for visualizing the results of multiple client-side
queries (`Luminous.Query`).
Three different types of Panels are currently offered out of the box
by Luminous:
- `Luminous.Panel.Chart` for visualizing 2-d data (including time
series) using the [chartjs](https://www.chartjs.org/) library
(embedded in a JS hook). Currently, only `:line` and `:bar`are
supported.
- `Luminous.Panel.Stat` for displaying single or multiple numerical or
other values (e.g. strings)
- `Luminous.Panel.Table` for displaying tabular data
A client application can implement its own custom panels by
implementing the `Luminous.Panel` behaviour.
Dashboards are parameterized by:
- a date range (using the [flatpickr](https://flatpickr.js.org/) library)
- user-defined variables (`Luminous.Variable`) in the form of dropdown menus
All panels are refreshed whenever at least one of these paramaters
(date range, variables) change. The parameter values are available to
client-side queries.
## Features
- Date range selection and automatic asynchronous (i.e. non-blocking
for the UI) refresh of all dashboard panel queries
- User-facing variable dropdowns (with single- or multi- selection)
whose selected values are available to panel queries
- Client-side zoom in charts with automatic update of the entire
dashboard with the new date range
- Panel data downloads depending on the panel type (CSV, PNG)
- Stat panels (show single or multiple stats)
- Table panels using [tabulator](https://tabulator.info/)
- Summary statistics in charts
## Installation
The package can be installed from `hex.pm` as follows:
```elixir
def deps do
[
{:luminous, "~> 2.6.1"}
]
end
```
In order to be able to use the provided components, the library's
`javascript` and `CSS` files must be imported to your project:
In `assets/js/app.js`:
```javascript
import { ChartJSHook, TableHook, TimeRangeHook, MultiSelectVariableHook } from "luminous"
let Hooks = {
TimeRangeHook: new TimeRangeHook(),
ChartJSHook: new ChartJSHook(),
TableHook: new TableHook(),
MultiSelectVariableHook: new MultiSelectVariableHook()
}
...
let liveSocket = new LiveSocket("/live", Socket, {
...
hooks: Hooks
})
...
```
Finally, in `assets/css/app.css`:
```CSS
@import "../../deps/luminous/dist/luminous.css";
```
## Usage
### Live View
The dashboard live view is defined client-side like so:
```elixir
defmodule ClientApp.DashboardLive do
alias ClientApp.Router.Helpers, as: Routes
use Luminous.Live,
title: "My Title",
time_zone: "Europe/Paris",
panels: [
...
],
variables: [
...
]
# the dashboard can be rendered by leveraging the corresponding functionality
# from `Luminous.Components`
def render(assigns) do
~H"""
<Luminous.Components.dashboard dashboard={@dashboard} />
"""
end
# we also need to implement the function that generates the LV path
@impl Luminous.Dashboard
def dashboard_path(socket, url_params) do
Routes.dashboard_path(socket, :index, url_params)
end
end
```
The client-side dashboard can also (optionally) implement the
`Luminous.TimeRange` behaviour in order to override the dashboard's
default time range value which is "today".
### Panels and Queries
Client-side queries must be included in a module that implements the
`Luminous.Query` behaviour:
```elixir
defmodule ClientApp.DashboardLive do
defmodule Queries do
@behaviour Luminous.Query
@impl true
def query(:my_query, _time_range, _variables) do
[
[{:time, ~U[2022-08-19T10:00:00Z]}, {"foo", 10}, {"bar", 100}],
[{:time, ~U[2022-08-19T11:00:00Z]}, {"foo", 11}, {"bar", 101}]
]
end
end
use Luminous.Live,
...
panels: [
Panel.define!(
type: Luminous.Panel.Chart,
id: :simple_time_series,
title: "Simple Time Series",
queries: [
Luminous.Query.define(:my_query, Queries)
],
description: """
This will be rendered as a tooltip
when hovering over the panel's title
"""
),
],
...
end
```
A panel may include multiple queries. When a panel is automatically
refreshed, the execution flow is as follows:
- for each query:
- execute the user query callback
- execute the panel's `transform/2` callback with the query result output
- aggregate the transformed query results
- update the dashboard state variable with the panel's data
(possible server-side re-rendering)
- send a JS event to the browser (for panel hooks)
The above flow needs to be understood when implementing custom
panels. If the client application uses the panels provided by
luminous, then the panel refresh flow is handled automatically and
only `use Luminous.Live` with the appropriate options is necessary.
### Variables
Variables represent user-facing elements in the form of dropdowns in
which the user can select single (variable type: `:single`) or
multiple (variable type: `:multi`) values.
Variable selections trigger the refresh of all panels in the
dashboard. The state of all variables is available within the `query`
callback that is implemented by the client application.
Just like queries, variables must be included in a module that
implements the `Luminous.Variable` behaviour:
```elixir
defmodule ClientApp.DashboardLive do
defmodule Variables do
@behaviour Luminous.Variable
@impl true
def variable(:simple_var, _assigns), do: ["hour", "day", "week"]
def variable(:descriptive_var, _assigns) do
[
%{label: "Visible Value 1", value: "val1"},
%{label: "Visible Value 2", value: "val2"},
]
end
end
use Luminous.Live,
...
variables: [
Luminous.Variable.define!(id: :simple_var, label: "Select one value", module: Variables),
Luminous.Variable.define!(id: :descriptive_var, label: "Select one value", module: Variables),
],
...
end
```
The variable callback will receive the live view socket assigns as the
second argument, however it is important to note that the `variable/2`
callback is executed once when the dashboard is loaded for populating
the dropdown values.
A `Variable` can be marked as hidden by passing `hidden: true` to
`Variable.define!/1`. Hidden variables are a means for framework
clients to store some kind of state expecially in the case of custom
Panels (a typical use case is pagination). As such, hidden variables
are not rendered as dropdowns in the dashboard and are not included in
URL params.
### Demo
Luminous provides a demo dashboard that showcases some of Luminous'
capabilities. The demo dashboard can be inspected live using the
project's development server (run `mix run` in the project and then
visit [this page](http://localhost:5000)).