Server Side Time Series Plots With Elixir Using Contex

Background #

At IoTReady, we are building a virtual IoT platform to help manufacturers track all of their products – whether these are born smart or not. For instance, a typical workflow we track in the Smart Grid industry looks something like this –

  • A manufacturer produces a batch of, say, an insulation product
  • The manufacturer ships certain units of this batch to a distributor
  • An operator buys some units of this batch
  • The operator installs the insulation product and captures notes and media (photos & videos)

At each stage of the flow, our mobile app scans QR codes and captures additional metadata like location and timestamps. Post installation we capture regular weather data for the installation location for analysis and preventive maintenance.

Tech Stack #

Operational dashboards are a lot more fun (and useful) realtime, so we are building ours with Elixir and the Phoenix Framework. These choices deserve their own, longer, blog post. For now, we will focus on our charting library of choice.

We are big fans of Plotly and have used it extensively in the past. However, in this case we wanted to minimise JS code and do things server side as much as we could.

Step up Contex!

We discovered Contex via a blog post on the excellent Elixir School site. However, that post covers bar charts and the Contex documentation is a work-in-progress. Figuring out legends and version-to-documentation mismatches was particularly painful. Hence this post.

Our dashboard is complementary to, rather than a replacement for, BI tools like Redash or Kibana. We needed something easy to use and customise that includes some visualisation.

Quick References #

Data Source #

We use the OpenWeatherMap API to grab basic weather data. Our Ecto schema looks a bit like this:

schema "weather" do
  field :latitude, :float
  field :longitude, :float
  field :temp, :float
  field :humidity, :float
  field :pressure, :float

  timestamps()
end

And the query for getting recent data looks like this:

@doc """
Gets weather data points for a given latitude and longitude tuple.

## Examples
    iex> get_weather({latitude, longitude}, limit)
    {:ok, [%Weather{}]}
"""
def get_weather({latitude, longitude}, limit \\ 100) do
  q = from w in Weather,
      where: [latitude: ^latitude, longitude: ^longitude],
      order_by: [desc: :inserted_at],
      limit: ^limit,
      select: %{temp: w.temp, humidity: w.humidity, pressure: w.pressure, inserted_at: w.inserted_at}
  Repo.all(q)
end

This query returns a list of maps that look like this:

[
  %{
    humidity: 73.0,
    inserted_at: ~N[2021-01-27 17:00:01],
    pressure: 1016.0,
    temp: 297.27
  },
  %{
    humidity: 73.0,
    inserted_at: ~N[2021-01-27 17:00:01],
    pressure: 1016.0,
    temp: 297.27
  }
]

Setting up Contex #

Now that we have our data, time to set up Contex. Since it’s still early days for Contex, it’s best to work with the master branch off the Github repo rather than the 0.3.0 release on Hex. For instance, the 0.3.0 release does not include LinePlot, which we need.

# mix.exs
defp deps do
  [
    ...,
    {:contex, git: "https://github.com/mindok/contex"},
  ]
end

Plotting the data #

It’s easiest to illustrate the plotting flow with code:

# Get the last 100 data points for {latitude, longitude}
weather_data = get_weather({latitude, longitude}) 


plot_options = %{
  top_margin: 5,
  right_margin: 5,
  bottom_margin: 5,
  left_margin: 5,
  show_x_axis: true,
  show_y_axis: true,
}

# Generate the SVG chart
weather_chart =
  weather_data
  # Flatten the map into a list of lists
  |> Enum.map(fn %{inserted_at: timestamp, temp: temp, humidity: humidity, pressure: pressure} ->
    [timestamp, temp, humidity, pressure]
  end)
  # Assign legend titles using list indices
  |> Dataset.new(["Time", "Temperature", "Humidity", "Pressure"])
  # Specify plot type (LinePlot), SVG dimensions, column mapping, title, label and legend
  |> Plot.new(
    LinePlot,
    600,
    300,
    mapping: %{x_col: "Time", y_cols: ["Temperature", "Humidity", "Pressure"]},
    plot_options: plot_options,
    title: "Weather",
    x_label: "Time",
    legend_setting: :legend_right
  )
  # Generate SVG
  |> Plot.to_svg()

# We are using Phoenix LiveView, so assign the chart to the socket
socket
  |> assign(:weather_chart, weather_chart)

After this, all that’s left to do is to embed the SVG in the HTML view and this is all it takes:

<%= @weather_chart %>

We faced some clipping of the legend text but that was easy to fix with this CSS:

.exc-legend {
    font-size: small;
}

Here’s the SVG in all its glory :-)WeatherTime27 Jan 09:0027 Jan 12:0027 Jan 15:0027 Jan 18:0027 Jan 21:0028 Jan 00:0028 Jan 03:0028 Jan 06:00010020030040050060070080090010001100TemperatureHumidityPressure

Where do we go from here #

We think Contex is a pretty good fit for our needs. It’s definitely rough around the edges and there are plenty of use cases we are yet to explore like:

  • interactivity (not a huge deal for us) and
  • realtime updates (big deal).

Realtime updates are easy to implement but we want verify impact on server performance but then again, this is not an immediate concern.

Ideas, questions or corrections? #

Write to us at hello@iotready.co

Scroll to Top