Live Chat

We'll need to share your messages (and your email address if you're logged in) with our live chat provider, Drift. Here's their privacy policy.

If you don't want to do this, you can email us instead at contact@anvil.works.

Python Dashboard

Are you new here?

Anvil is a tool for building full-stack web apps with nothing but Python and a drag-and-drop designer. Learn more on our website, or sign up and try it yourself -- it's free!

We’re going to build a data dashboard with Anvil, and publish it on the Web, using nothing but Python.

It will display weather data over time for a chosen day in Cambridge, UK.

To follow along, you need to be able to access the Anvil Editor. Create a free account using the following link:

Create an account


Step 1: Create a control panel

Open the Anvil Editor to get started.

In the top-left there is a ‘Create New App’ button. Click it and select the Material Design theme.

You are now in the Anvil Editor.

First, let’s give the app a sensible name - it will currently be called something like ‘Material Design 1’. Click on the name at the top of the screen and type in a name like ‘Data Dashboard’.

Now let’s start building a UI. The toolbox on the right contains components that you can drag-and-drop onto the Design view in the centre. Drop a Label into the blue bar at the top, where it says ‘Drop title here’. The Properties tool on the right allows you to modify this new component - fill in the text section to put text into the label.

Now for the dashboard’s control panel. Add a Card to the page. This is a container that can hold other components. Inside the card, put a Label, a DatePicker, and a Button.

The Label should say Date: and have its Role set to input-prompt.

The DatePicker needs to know what format to display the date in. Enter %d %b %Y in the format box (this uses the standard Python date format syntax).

The DatePicker must also be told what date to be set to initially. Set its date property to now.

The Button will become the ‘add plot’ button that puts new plots into the dashboard. So name it add_btn, and set its text to ‘Add plot’.

We’ve just designed a UI for adding plots to the dashboard.

The next step is to make it do something. For now, we’ll pop up an alert when the user clicks the ‘add plot’ button.

At the bottom of the Properties tool for the Button is a list of events that we can bind methods to. Click the arrow next to the click event:

You will now see the code that defines the behaviour of this part of your app. The method that’s highlighted will run when the Button is clicked. We want it to pop up a dialog box, so we’ll call the built-in alert function:

def add_btn_click(self, **event_args):
  """This method is called when the button is clicked"""
  alert(str(self.date_picker_1.date), title="New plot")

When you click the button, you’ll get a dialog box displaying the date selected in the DatePicker.

Step 2: Adding plots

We’ve built the basis of our control panel and made it do something. Currently it just pops up an alert. Let’s make it actually create some plots and display them on the page.

We’ll use Plotly to create the plots, so go to the Code view and add an import for graph_objs:

from plotly import graph_objs as go

Now you can change the click handler to add plots to the page. Components can be instantiated in code as well as from the toolbox, so in place of the alert, you can call Plot() to create a plot.

    new_plot = Plot()

For now, let’s set the new plot up to contain a scatter plot with some dummy data.

    new_plot.data = go.Scattergl(
      x=[0, 1, 2, 3, 4],
      y=[0, 1, 4, 9, 16],
    )

We’ll put the plots into a Grid Panel. Grid Panels can be found under ‘See more components…’ in the toolbox; drag-and drop one onto the Form.

Components can be added to the Grid Panel using add_component. All Anvil containers have an add_component method; Grid Panels also allow you to set the width of the component, from 1 to 12.

    self.grid_panel_1.add_component(new_plot, width_xs=6)

When you click the add button in the live app, you should see plots appearing on the screen.

Step 3: Get the data

Now we’re making plots, but it would be nice to have some meaningful data to put in them.

We’re going to use the data from the Cambridge University Digital Technology Group’s weather station.

Daily tab-separated value files are available at URLs such as

https://www.cl.cam.ac.uk/research/dtg/weather/daily-text.cgi?2018-09-26

Step 3a: Request the data

We’ll use a server function to get the data for any given day and return it to the browser. It’ll make an HTTP request using Anvil’s http library.

Click on the + next to ‘Server Modules’ in the panel on the left. Define a function called get_weather_for_day. It should call anvil.http.request with the URL for the day’s weather:

@anvil.server.callable
def get_weather_for_day(dt):
  # Construct a URL based on the datetime object we've been given.
  url = dt.strftime('https://www.cl.cam.ac.uk/research/dtg/weather/daily-text.cgi?%Y-%m-%d')

  # Get the raw data by making an HTTP request.
  raw_data = anvil.http.request(url).get_bytes()
  
  print(raw_data)

You can call this from your click handler using anvil.server.call

    anvil.server.call('get_weather_for_day', self.date_picker_1.date)

When you click the ‘add’ button, you should now see the data printed on the Output console.

Step 3b: Parse the data

At present, the data is a string containing the tab-separated values. It needs to be parsed and cleaned to get it into a good format for plotting.

Given that we’re running in a real Python server environment, the range of possibilities for analysing this data is huge. In order to focus on using Anvil, we’ll just wrangle the existing data into a useful data structure, and we’ll give you that function verbatim:

from datetime import datetime

def parse_data(data, dt):
  """Parse the raw weather data string into a list of times, and a dict of lists of meterological variables."""
  # First, split the data into columns
  all_rows = data.split('\n')
  all_rows = [r.strip() for r in all_rows if r.strip()]

  # Then, exclude every row starting with '#'
  data_rows = [r for r in all_rows if not r.startswith('#')]

  # Then split rows on tab character.
  data_rows = [r.split('\t') for r in data_rows]

  # The headers are the penultimate commented line.
  headers = [r.split('\t') for r in all_rows if r.startswith('#')][-2]
  # Clean the headers a bit
  headers = [h.strip('#').strip() for h in headers]

  # The units are the final commented line.
  units = [r.split('\t') for r in all_rows if r.startswith('#')][-1]
  # Clean the units a bit
  units = [u.strip('#').strip() for u in units]

  # Parse out the date time
  time_data = [datetime.strptime(x[0], '%H:%M').replace(year=dt.year, month=dt.month, day=dt.day) for x in data_rows]

  # Construct the dictionary of y-axis variables
  y_data = {
    '{} ({})'.format(header, units[x+1]): [r[x+1] for r in data_rows]
    for x, header in enumerate(headers[1:])
  }
  
  # These two variables don't scatter plot very well, so let's discard them.
  del y_data['Start ()']
  del y_data['WindDr ()']

  return time_data, y_data

The return values are:

  • time_data: a list of datetimes.
  • y_data: a dictionary whose keys are the column headings and units, e.g. 'Sun (hours)'. Each value is a list of data, with one datapoint per entry in time_data.

So go.Scattergl(x=time_data, y=y_data['Sun (hours)']) would be a Plotly plot showing hours of sunlight against time.

To use this function, copy it into your Server Module and modify get_weather_for_day to return parse_data(raw_data, dt).

Step 4: Plot the data

Now you have a server function to get the data in a useful data structure, you can pass it into Plotly and it will appear on the page.

Modify the click handler to get the return values from the server function and pass them into the plot. You can use random.choice to randomly select a variable to plot (remember to import the random module).

    time_data, y_data = anvil.server.call('get_weather_for_day', self.date_picker_1.date)
    variable = random.choice(y_data.keys())

If you pass the correct variables into the go.Scattergl constructor, you should see weather data plots get created when you click the ‘add’ button.

    new_plot.data = go.Scattergl(
      x=time_data,
      y=y_data[variable],
    )

You can add some labels for the axes and a title as well (make sure you add these before you assign to new_plot.data):

    new_plot.layout.xaxis.title = 'Time'
    new_plot.layout.yaxis.title = variable
    new_plot.layout.title = variable

Here’s what you get when you do that:

Step 5: Cache the data

The weather station produces a new data point every half-hour. So we don’t really need to fetch the data every time the ‘add plot’ button is clicked. We can make the app more efficient by caching the data in global variables.

In the panel on the left, click on the + next to ‘Modules’ and create a module named ‘Data’. Define a couple of variables in here to store the time_data and y_data, then write a function to perform the update and store the results in these global variables:

time_data = []
y_data = {}

def update(dt):
  global time_data
  global y_data
  time_data, y_data = anvil.server.call_s('get_weather_for_day', dt)

(anvil.server.call_s is the same as anvil.server.call but without the spinner.)

Now you only need to fetch the data when the app is started. So remove the server function call from the click handler, and call Data.update(self.date_picker_1.date) inside the __init__ function instead.

Remember to modify your click handler to use Data.time_data and Data.y_data in place of time_data and y_data. And ensure you import your new Data module!

If you run your app again, it should work as before, except adding graphs is a lot faster!

Step 6: Choose the data from the UI

We’re aiming to make a dashboard where each plot has a dropdown menu to select what it shows. To do this, we need to make each plot more self-contained. Create a new Form - select Blank Panel rather than Standard Page because we want to construct the UI entirely from scratch.

Let’s rename the new Form to ‘TimePlot’.

Step 6a: Construct a UI

Drag-and-drop a Card into the new Form and add a Label, DropDown and Spacer. The Label should say ‘y-axis:’, because that’s what the DropDown will select. Set the Label’s role to input-prompt and adjust the sizes of things to suit your aesthetic taste.

Now drag-and-drop a Plot object into the bottom of the card. Your UI should look something like this in the Design view:

Step 6b: Write a plotting function

If you click on ‘Code’ to look at the code for this new Form, you’ll see a class called TimePlot. This class is currently empty.

We want the contents of the plot inside the TimePlot to get updated when the TimePlot is instantiated. But it’s also useful to be able to update the plot whenever we want. So let’s define a method called plot and call it from the __init__ method:

class TimePlot(TimePlotTemplate):
  def __init__(self, **properties):
    # Set Form properties and Data Bindings.
    self.init_components(**properties)

    # Any code you write here will run when the form opens.
    self.plot()
    
  def plot(self):
    # ...

What needs to go in the plot method? It will be the same as the plotting code in the ‘Add’ button click handler. Only now, the Plot is called self.plot_1 rather than new_plot.

So, cut/paste the correct lines from the click handler and replace new_plot with self.plot_1.

  def plot(self):
    variable = random.choice(Data.y_data.keys())
    self.plot_1.layout.xaxis.title = 'Time'
    self.plot_1.layout.yaxis.title = variable
    self.plot_1.layout.title = variable
    
    self.plot_1.data = go.Scattergl(
      x=Data.time_data,
      y=Data.y_data[variable],
    )

Step 6c: What stays in the click handler

Two lines need to stay in the click handler on Form1. We must instantiate the plot - although instead of a Plot component, we want to instantiate an instance of our new TimePlot Form. Then we must add it to the Grid Panel.

from TimePlot import TimePlot
    new_plot = TimePlot()
    self.grid_panel_1.add_component(new_plot, width_xs=6)

If you’ve done everything correctly, your plots should now be appearing inside a Card, with an empty y-axis dropdown above them:

Step 6d: Selecting the y-axis variable

Let’s make that dropdown do something.

We need to set it up inside TimePlot.__init__, before self.plot() gets called. The options should be all the y-axis keys that are available and the initial value should be randomly selected:

    self.drop_down_1.items = Data.y_data.keys()
    self.drop_down_1.selected_value = random.choice(self.drop_down_1.items)

The plot method needs to use the selected value from the DropDown as its variable, rather than choosing randomly:

  def plot(self):
    variable = self.drop_down_1.selected_value
    # ...

And we need to configure an event handler that calls plot every time the dropdown is changed. Bind an event handler to the DropDown’s ‘change’ event in the same way as you did for the Button’s ‘click’ event in section 1, and call self.plot() from there.

Take a look at your live app again - each plot should now have a working DropDown that can select which variable is plotted on the y-axis.

Step 7: Remove plots from the page

Removing plots from the page is simple. Just add a button to the TimePlot Form, style it like a delete button, and configure a ‘click’ event handler.

The event handler should call self.remove_from_parent().

That’s all there is to it!

Step 8: Display data for other days

One more thing remains. The DatePicker on Form1 doesn’t currently do anything.

Create an event handler for its ‘change’ event.

When the DatePicker changes, you need to re-fetch the data for the new date, and update all TimePlot components.

To re-fetch the data, you just need to call Data.update with the DatePicker’s currently selected date.

To update all the TimePlot components, you can iterate over the components in self.grid_panel_1 using a for loop, and call plot on every component that happens to be a TimePlot:

Data.update(self.date_picker_1.date)
for component in self.grid_panel_1.get_components():
  if isinstance(component, TimePlot):
    component.plot()

And now everything on the page does what it should! Run your app again - you’ll find you can select a date from the DatePicker and see all the plots update to show the data for that date.

Congratulations, you’ve just built a data dashboard in Anvil!

Clone the finished app

Every app in Anvil has a URL that allows it to be imported by another Anvil user.

Click the following link to clone the finished app from this workshop:

Clone Finished App


It’s also live on the internet already (find out more).

Extension: Real time data feed

A dashboard is much more useful if it can give you a live feed of the data as it comes in. If you’ve got this far, maybe you can figure out how to get the plots to refresh as new data is uploaded to the Digital Technology Group’s website.

Hint: There’s a Timer component that triggers a ‘tick’ event after a given number of seconds.