Add a custom “Charts” page¶
So far the framework has provided the bulk of features, and we’ve merely “employed” it to our ends. But now we will get a little more creative and bring in other tools, namely for the client-side JS.
When viewing a vehicle record, we want a link to another page which shows some charts to visualize its mileage data.
Defining the view callable¶
The view callable is a Pyramid term meaning essentially, what gets called when an incoming request matches a certain routing URL. In most cases (all, in this tutorial) it will be a method of a master view class.
Since we’re defining a whole new page with a separate URL, we will need a new method for the view callable. For now we’ll just make it a stub, can flesh it out later:
class VehicleView(MasterView):
# ..<boilerplate elided>..
# this will be invoked for URL like /vehicles/XXX/charts
def charts(self):
"""
Page to see a vehicle's charts.
"""
vehicle = self.get_instance()
# TODO: fetch chart data
return {"vehicle": vehicle}
Note that a typical Pyramid view callable would return a context dict, which is used to render the template. That’s what we’ve shown here, and it works because we (will) configure the template when we register the view callable.
Many of the master view methods defined by the framework use a slightly different approach to allow for dynamic template lookup. That’s just FYI for now, we won’t go into it any further for the tutorial.
Defining the template¶
The first thing to figure out is where this template file should live and what it should be named.
The master class has a
template_prefix
which dictates where templates should live, and we’ll go with
charts.mako for the name in this case.
So let’s create poser/web/templates/vehicles/charts.mako and give
it stub contents:
<%inherit file="/base.mako" />
<%def name="page_content()">
<h3 class="is-size-3">TODO: show some charts!</h3>
<h4 class="is-size-4">
but at least we have a vehicle:
${h.link_to(vehicle.name, url("vehicles.view", uuid=vehicle.uuid))}
</h4>
</%def>
Note the class="is-size-3" and similar; these are Bulma style
classes.
WuttaWeb uses Mako templates; for more background on how that works see Templates.
Configuring the route¶
Now we’ll add the Pyramid configuration so it knows to route requests
for e.g. /vehicles/XXX/charts URL to our new method. We’ll also
tell it which template to render.
Each master view class has a special
defaults() method
which is where this configuration happens. But it’s a class method
so we can’t override it in the same way we might for an instance
method.
This may not seem very pretty at first glance but it’s not so bad once you get used to it, and it allows for a lot of flexibility:
class VehicleView(MasterView):
# ..<boilerplate elided>..
@classmethod
def defaults(cls, config):
# first apply standard master view config from base class
cls._defaults(config)
# fetch config prefixes
route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
instance_url_prefix = cls.get_instance_url_prefix()
template_prefix = cls.get_template_prefix()
# configure charts route
config.add_route(
f"{route_prefix}.charts",
f"{instance_url_prefix}/charts",
)
# configure charts view callable
config.add_view(
cls,
attr="charts",
route_name=f"{route_prefix}.charts",
permission=f"{permission_prefix}.view",
renderer=f"{template_prefix}/charts.mako",
)
Note that we also added a permission restriction for the charts page. User can only see charts if they have the “view vehicle” permission.
Here are docs for some methods we used within defaults() there:
Adding a link from the “View” page¶
At this point the charts page should actually be working (though not
pretty). If you’re viewing a vehicle record, and append /charts
after the UUID in your browser address bar, you should see it.
So we’ll add a link from the view vehicle page, for easier access. In fact we’ll use a button to make it “pop” a little more.
The framework has a built-in utility for “cross-reference” links such
as this. The original concept was for integrations where data is
synced between systems, and it’s handy for user to hop around and view
data in each system. But the concept is generic enough you can use
get_xref_buttons()
for any reason.
And that’s what we’ll do here:
class VehicleView(MasterView):
# ..<boilerplate elided>..
# nb. these are only shown for the "view vehicle" page, and
# since the charts page requires the "view vehicle" permission,
# we know user is good, hence no perm check here
def get_xref_buttons(self, vehicle):
return [
self.make_button(
"View Charts",
url=self.get_action_url("charts", vehicle),
primary=True,
icon_left="chart-line",
),
]
Now when you view a vehicle record you should see the button to view charts. The framework is getting its icons from FontAwesome.
Improving the Charts page title/header¶
So far we don’t have anything in the page header, things look strangely bare. This was done intentionally just to show where the “lines” are in terms of what happens by default.
There is more than one way to customize the page title, but we’ll focus on doing it in the template here. We want to define 3 separate “title” values:
title()- is for the<title>element within the HTML<head>content_title()- displayed in the colored background headerindex_title()- displayed just above content title
Add these blocks to your charts.mako template:
## this is for the <title> within the <head>
<%def name="title()">Vehicles » ${vehicle.name} » Charts</%def>
## this is for the colored background header
<%def name="content_title()">Charts for ${vehicle.name}</%def>
## link back to view the vehicle, or list vehicles
<%def name="index_title()">
${h.link_to("Vehicles", url("vehicles"))}
»
${h.link_to(vehicle.name, url("vehicles.view", uuid=vehicle.uuid))}
</%def>
Now the header should look more complete.
Fetching the chart data¶
Now let’s overhaul the charts() view callable so it loads the
vehicle’s chart data.
We will be showing two charts: First a bar chart showing the gallons for each fillup, then a line chart showing miles per gallon as of each fillup.
The class method will collect data for both charts:
class VehicleView(MasterView):
# ..<boilerplate elided>..
def charts(self):
"""
Page to see a vehicle's charts.
"""
session = self.Session()
vehicle = self.get_instance()
# fetch all mileage entries for vehicle, oldest to newest
entries = (
session.query(MileageEntry)
.filter(MileageEntry.vehicle == vehicle)
.order_by(MileageEntry.date)
.all()
)
# calculate miles per gallon for each fillup, and collect
# data. it will be rendered as JSON in the template.
gallons_data = []
mileage_data = []
last_entry = None
for entry in entries:
# TODO: this logic is pretty rudimentary, only works if
# all entry fields have values. but proves the concept
if entry.fillup_gallons:
gallons_data.append(
{
"date": entry.date.isoformat(),
"gallons": float(entry.fillup_gallons),
}
)
if entry.odometer and last_entry and last_entry.odometer:
mileage_data.append(
{
"date": entry.date.isoformat(),
"mileage": float(
(entry.odometer - last_entry.odometer)
/ entry.fillup_gallons
),
}
)
last_entry = entry
return {
"vehicle": vehicle,
"gallons_data": gallons_data,
"mileage_data": mileage_data,
}
Displaying the charts¶
All necessary data should be available to the template now, so let’s
overhaul the charts.mako file too.
First let’s replace the page_content() block with a new one:
<%def name="page_content()">
<canvas ref="gallonsChart" width="400" height="60"></canvas>
<br /><br />
<canvas ref="mileageChart" width="400" height="60"></canvas>
</%def>
Then also add these blocks:
<%def name="extra_javascript()">
${parent.extra_javascript()}
<script src="https://cdn.jsdelivr.net/npm/chart.js@^4"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@^2"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@^1"></script>
</%def>
<%def name="modify_vue_vars()">
${parent.modify_vue_vars()}
<script>
## data from view callable, as JSON
ThisPageData.gallonsData = ${json.dumps(gallons_data)|n}
ThisPageData.mileageData = ${json.dumps(mileage_data)|n}
## register hook to setup the charts on page load
ThisPageData.mountedHooks.push(function() {
new Chart(this.$refs.gallonsChart, {
type: 'bar',
data: {
datasets: [{
label: "Gallons",
data: this.gallonsData,
backgroundColor: 'pink',
}],
},
options: {
plugins: {
title: {
text: "Gallons per Fillup",
display: true,
},
},
parsing: {
xAxisKey: 'date',
yAxisKey: 'gallons',
},
scales: {
x: {
type: 'time',
time: {unit: 'week'},
},
}
},
})
new Chart(this.$refs.mileageChart, {
type: 'line',
data: {
datasets: [{
label: "Mileage as of fillup",
data: this.mileageData,
borderColor: 'blue',
tension: 0.1,
}],
},
options: {
plugins: {
title: {
text: "Miles per Gallon",
display: true,
},
},
parsing: {
xAxisKey: 'date',
yAxisKey: 'mileage',
},
scales: {
y: {
min: 0,
},
x: {
type: 'time',
time: {unit: 'week'},
},
}
},
})
})
</script>
</%def>
Now viewing the Charts page you should really see some charts. Although you’ll have to add mileage entries before they show much.
Summary¶
This concludes the tutorial.
The charts page shows how to sort of “work around” the framework in a sense. It’s there for the fundamental stuff but tries to let you add whatever else you might need.
See WuttaWeb for further available documentation.