Build a Celebrity Jet Tracking Dashboard in 380 lines with Framework Markup Language — Part 2: Data Connection and Display

Framework Markup Language
12 min readJun 23, 2023

--

Part 2: Connecting Data and Functions

In this article we go over Part 2 of 2 of how to build a fully functional celebrity jet tracker in under an hour using Framework Markup Language, OpenSky and a few other REST API’s.

In this article (Part 2 of 2), we will be wiring up the data, displaying different trends by manipulating data, and driving the UI from these manipulations with the goal of building a fully functioning Jet Tracker:

The finished jet tracking dashboard

If you haven't read Part 1 yet, you can read it here. Alternatively, you can check out a constantly updated version of the dashboard at jet.fml.dev!

note: all jet tracking is being done using publicly available data and tail numbers.

Recap of Part 1

In Part 1, we left off with the following dashboard’s UI created from 280 lines of FML:

At this point, layout, interactive UI, and the overall display and placeholders for our application’s data have been implemented which is the bulk of the work. Next, we will look at wiring in data from various REST API’s, local databases, and combining and manipulating this data in FML to display new trends to the user.

Remember to follow along with an open copy of the FML Wiki to assist you when writing templates.

Connecting To and Displaying Data

In order to provide live data, we need to connect to a mix of non changing and constantly changing datasets. If you want a simpler overview of datasources or their transforms, you can check out our article here before diving in to this section.

For the hardcoded datasets we are going to use for this tutorial, you can download them here and examine their structure.

Connect to Local Datasets

First, we need to connect our three lists to the data above in order to change the dashboard. Starting with the list of users, we can grab the hardcoded dataset using a GET Datasource:

The list built from the datasource
<!--We supply a relative url to the filepath within our fileserver-->
<GET id="userdata" url="resources/planeuserdata.json"/>
<!--We assign the datasource to the list allowing us to build the items as a prototype-->
<LIST id="userselect" ... data="userdata">

Connecting this datasource to the list is easy, we can simply assign the datasource to the list with data=””, and set up bindings based on existing fields in the datasource:

<ITEM ...>
<!--Removing the hardcoded url, we bind to the field imageUrl within the item-->
<!--When given a datasource, list items become prototypes and create a new element for each root item-->
<IMG url="{data.imageUrl}" height="100%"/>
</ITEM>

Next, we will connect our jet list data, again from a hardcoded set like above, but in this case we will derive the dataset from the selected list items individual data, and access a deeper element which is also a list:

The list built from the subset of the initial list
<!--We can derive a datasource from another datasource or data widget for granular control-->
<!--Setting the root as jets allows us to use the sublist to create datarows-->
<DATA id="userjetdata" value="{userselect.data}" root="jets"/>
<!--Assigning the datasource to the list as in the step above-->
<LIST id="taildata" data="userjetdata" ...>
<ITEM id="planeitem">
...
<!--In this case, we bind to tailNumber in the element list-->
<!--data. refers to the datasource of the lists data for each row-->
<TEXT value="{data.tailNumber}" .../>
</ITEM>
</LIST>

Connecting to REST API’s

Finally, we will wire in the live data required to display the current state of the users jet we have selected:

The flight list built from an api call
<!--We bind to the userjetdata list. This will allow the selected item to be passed into the url, and refire it.-->
<!--The bindings {20dpost} and {currenttime} within the url are set up in the next step-->
<!--Bind to the list of jets using {taildata.data.icaoNumber}. The binding will fire the url everytime it is selected-->
<GET id="trackerdata" url="https://opensky-network.org/api/flights/aircraft?icao24={userjetdata.data.icaoNumber}&amp;begin={20dpost}&amp;end={currenttime}"/>

After the first call to this datasource, we recommend copying the data returned and assigning a local datasource (as above) with the copied data until testing is complete.

As you can see, the url requires two epochs. We will create two VAR widgets to calculate the epoch from the current date minus 20 days using SYSTEM global bindings to get the current epoch:

<!--We subtract the current time minus 20 days in milliseconds-->
<VAR id="20dpost" value="={currenttime}-864000"/>
<!--The api expects a 10 digit epoch, so we must take a substring of the SYSTEMS 13 digit epoch-->
<VAR id="currenttime" value="=substring({SYSTEM.epoch}, 0, 10)"/>

We then assign the data to the LIST and bind to its fields:

<GET id="trackerdata" .../>
<LIST id="planelist" data="trackerdata" ...>
<ITEM ...>
<ROW>
<COL ...>
<!--We use an eval with a `??` null aware operator incase the binding does not exist to display N/A to the user-->
<TXT value="={data.estDepartureAirport} ?? 'N/A'" .../>
<!--Convert the firstseen epoch to a date using an eval function -->
<TXT value="=toDate('{data.firstSeen}', '', 'h:mm dd/MM')"/>
</COL>
<ICON .../>
<COL ...>
<!--We do the same as above using null aware to display TBD-->
<TXT value="={data.estArrivalAirport} ?? 'TBD'" style="h6"/>
<!--We create a VAR for an eval that will be use in multiple places-->
<!--The existance of an arrival airport infers if the plane has landed with our dataset-->
<VAR id="hasArrived" value="={data.estArrivalAirport} != null"/>
<!--Using the VAR binding from above, a ternary operation returns what we expect the user to see-->
<TXT value="={hasArrived} ? toDate('{data.lastSeen}000', '', 'h:mm dd/MM') : 'In Flight'"/>
</COL>
</ROW>
<ROW ...>
<COL halign="end" valign="center">
<!--The duration is not in the dataset, so we must add it using transforms-->
<VAR id="durationHours" value="=floor({data.duration}/60/60)"/>
<VAR id="durationMinutes" value="=floor({data.duration}/60%60)"/>
<!--We evaluate hasArrived to display the text and correct color based on the arrival status-->
<TXT value="={hasArrived} ? 'Arrived' : 'In Flight'" color="={hasArrived} ? green : yellow"/>
<TXT value="Duration: {durationHours}h {durationMinutes}m"/>
</COL>
</ROW>
</ITEM>
</LIST>

Manipulating Data Driven UI

Manipulating Datasources with TRANSFORMS

Now, if this was enough data we would be very happy, but we want a few things from this dataset that isn’t offered by Opensky such as the airport name, locations, and more. Best practice would dictate you create an API service to make these calculations and return it to you. In our case, we are going to use built in functions, pretending we do not have the means to create our own query service.

The maniuplated data of flight duration

To do this, we will first use CALC transforms like so:

<!--Transforms will be applied in the order listed-->
<GET id="trackerdata" url="resources/planedata.json">
<!--First we add a duration field to determine the length of the flight-->
<CALC target="duration" operation="eval" source="{data.lastSeen} - {data.firstSeen}"/>
<!--Then we use the previously created field to get a min, max, total and average-->
<CALC target="maxDuration" operation="max" source="duration"/>
<CALC target="minDuration" operation="min" source="duration"/>
<CALC target="avgDuration" operation="avg" source="duration"/>
<CALC target="totalDuration" operation="sum" source="duration"/>
</GET>

Next, we want a SUBQUERY transform after the CALC’s to get the name and location of each airport:

<GET id="trackerdata" ...>
...
<!--The subquery waits for and inserts the returned call into the target from the api-->
<SUBQUERY target="departuredata">
<!--estDepartureAirport will be replaced from each data row iterated through-->
<GET url="https://aircharterguide-api.tuvoli.com/api/v1/airport/details?airportCode={estDepartureAirport}">
<!--The api expects a header of the following to execute-->
<HEADER key="Content-Type" value="application/json; charset=utf-8"/>
</GET>
</SUBQUERY>
<SUBQUERY target="arrivaldata">
<!--We make a second call to get the arrival airport information-->
<GET url="https://aircharterguide-api.tuvoli.com/api/v1/airport/details?airportCode={estArrivalAirport}">
<HEADER key="Content-Type" value="application/json; charset=utf-8"/>
</GET>
</SUBQUERY>
</GET>

This call will insert a sub element of its return body into the data that can be bound to. Finally, we do more CALC transforms to add our extra data based on these calls:

<GET id="trackerdata" ...>
...
<!--We do a calculation based on the subquery data for each row-->
<!--By using multiple dot notation, we can traverse down the tree of data elements to access deeper elements-->
<CALC target="distance" operation="eval" source="round(distance({data.departuredata.Latitude}, {data.departuredata.Longitude}, {data.arrivaldata.Latitude}, {data.arrivaldata.Longitude})/1000,2)"/>
<!--Like the above step, we use calcs to get a min, max, avg, and total distance-->
<CALC target="maxDistance" operation="max" source="distance"/>
<CALC target="minDistance" operation="min" source="distance"/>
<CALC target="avgDistance" operation="avg" source="distance"/>
<CALC target="totalDistance" operation="sum" source="distance"/>
</GET>

Displaying and Manipulating Databindings

Displaying Jet Information From LIST Selections

With all of this data added, we need to display it in the UI in a way that makes sense to the user. We have already gone through small examples of this when we wired the datasource to the flight list, displaying epoch data as a timestamp.

The bound data and image for the seletced jet

Lets start with the easiest example which is the selected celebs name and occupation. We simply replace the placeholder text with bindings from the id=”userselect” LIST’s data:

<TXT value="Name:" .../>
<TXT value="{userselect.data.firstName} {userselect.data.lastName}" .../>
<TXT value="Occupation:" .../>
<TXT value="{userselect.data.occupation}" .../>

Next, we will do the same with the id=”taildata” LIST portion, allowing the selected list element to subset its own data:

<ROW ...>
<ROW id="boxheight" ...>
<SBOX ...>
<TXT value="Price" .../>
<TXT value="=number({taildata.data.MSRP},true,true)" .../>
<TXT value="Cost/Hr:" .../>
<TXT value="=number({taildata.data.hourlyTotal},true,true)" .../>
<TXT value="Fuel:" .../>
<TXT value="{taildata.data.fuelBurnPerHour} gal/hr" .../>
<TXT value="Yeary Additional:" .../>
<TXT value="=number({taildata.data.yearlyOperating},true,true)" .../>
</SBOX>
<SBOX ...>
<TXT value="Manufacturer:" .../>
<TXT value="{taildata.data.manufacturerName}" .../>
<TXT value="Built: " .../>
<TXT value="{taildata.data.built}" .../>
<TXT value="Country: " .../>
<TXT value="{taildata.data.country}" .../>
<TXT value="Owner: " .../>
<TXT value="{taildata.data.owner}" .../>
</SBOX>
<SBOX ...>
<TXT value="Model: " .../>
<TXT value="{taildata.data.model}" .../>
<TXT value="Engines: " .../>
<TXT value="{taildata.data.engines}" .../>
<TXT value="Class ICAO: " .../>
<TXT value="{taildata.data.icaoAircraftClass}" .../>
<TXT value="Type Code: " .../>
<TXT value="{taildata.data.typeCode}" .../>
</SBOX>
</ROW>
<BOX .../>
<IMG url="={taildata.data.aircraftImgURL}" .../>
</ROW>

Preforming Calculations on Selected Data

Next, lets look at creating the lower section bindings where we display the recent flight stats and 20day flight stats.

The bound manipulated data from subqueries and transforms

Starting with the recent flight stats, we replace the placeholders with data from the id=”planelist” LIST data much like we did above, this way we are shown only the selected flights data:

<SCROLL>
<BOX ...>
<TXT value="Departure Airport:" .../>
<TXT value="{planelist.data.departuredata.AirportName}" .../>
<BOX .../>
<TXT value="Arrival Airport:" .../>
<TXT value="={planelist.data.arrivaldata.AirportName} ?? TBD" .../>
</BOX>
<ROW>
<BOX ...>
<TXT value="Distance Flown:" color="{stcol}"/>
<ROW ...>
<TXT value="{planelist.data.distance} km" .../>
...

Now, we notice we have multiple values that are not displayable as their raw data such as distance. To drive this, we will do the calculations in VAR’s, then bind the fields to these VARs:

                    <VAR id="avgDifferenceDistanceCalc" value="=round({planelist.data.distance} - {planelist.data.avgDistance},0)"/>
<VAR id="roundedDifferenceDistanceCalc" value="=round({planelist.data.avgDistance},2)"/>
<TXT value="{roundedDifferenceDistanceCalc} km" .../>
<TXT value="{avgDifferenceDistanceCalc} km" ... />
</COL>
<ICON icon="trending_down" .../>
</ROW>
</SCROLL>

Next, we can drive the individual UI based on these VAR calculations:

     <TXT value="{avgDifferenceDistanceCalc} km" color="={avgDifferenceFuelCalc} &lt; 0 ? 'red' : 'green'" .../>
</COL>
<ICON icon="trending_down" visible="={avgDifferenceDistanceCalc} &lt; 0 ? true : false" .../>
<ICON icon="trending_up" visible="={avgDifferenceDistanceCalc} &gt; 0 ? true : false" color="green" size="30"/>

Then we simply repeat a very similar calculation set for the remainder of this portion of the UI:

<ROW>
<BOX ...>
<TXT value="Flight Cost:" .../>
<VAR id="avgCostCalc" value="={planelist.data.avgDuration}/60/60 * {taildata.data.hourlyTotal}"/>
<VAR id="avgDifferenceCostCalc" value="={flightCostCalc} - {avgCostCalc}"/>
<VAR id="flightCostCalc" value="={durationCalc} * {taildata.data.hourlyTotal}"/>
<ROW ...>
<TXT value="=number({flightCostCalc}, true, true)" .../>
<ROW ...>
<COL ...>
<TXT value="=number({avgCostCalc}, true, true)" .../>
<TXT value="=number({avgDifferenceCostCalc}, true, true)" color="={avgDifferenceCostCalc} &lt; 0 ? 'red' : 'green'" .../>
</COL>
<ICON icon="trending_down" visible="={avgDifferenceCostCalc} &lt; 0 ? true : false" .../>
<ICON icon="trending_up" visible="={avgDifferenceCostCalc} &gt; 0 ? true : false" .../>
</ROW>
</ROW>
</BOX>
<BOX ...>
<TXT value="Carbon Consumed:" .../>
<VAR id="carbonCalc" value="=3.102572 * {fuelCalc} * 3.16"/>
<VAR id="carbonCalcRounded" value="=round({carbonCalc}, 0)"/>
<VAR id="avgCarbonCalc" value="=round(3.102572 * {avgFuelCalc} * 3.16,0)"/>
<VAR id="avgCarbonDifference" value="={carbonCalcRounded} - {avgCarbonCalc}"/>
<ROW ...>
<TXT value="{carbonCalcRounded} kg" .../>
<ROW ...>
<COL ...>
<TXT value="{avgCarbonCalc} kg" .../>
<TXT value="{avgCarbonDifference} kg" color="={avgCarbonDifference} &lt; 0 ? 'red' : 'green'" .../>
</COL>
<ICON icon="trending_down" visible="={avgCarbonDifference} &lt; 0 ? true : false" .../>
<ICON icon="trending_up" visible="={avgCarbonDifference} &gt; 0 ? true : false" .../>
</ROW>
</ROW>
</BOX>
</ROW>

Binding to The Total Statistics

This part is very similar to the above, rather than binding to the LIST’s data, we bind directly to the datasource where we did the transforms.

Data displayed from inner template calculations

<ROW>
<BOX ...>
<VAR id="totalDurationHours" value="=round({trackerdata.data.totalDuration}/60/60,0)"/>
<VAR id="totalDurationMinutes" value="=floor({trackerdata.data.totalDuration}/60%60)"/>
<VAR id="minDurationHours" value="=round({trackerdata.data.minDuration}/60/60,0)"/>
<VAR id="minDurationMinutes" value="=floor({trackerdata.data.minDuration}/60%60)"/>
<VAR id="maxDurationHours" value="=round({trackerdata.data.maxDuration}/60/60,0)"/>
<VAR id="maxDurationMinutes" value="=floor({trackerdata.data.maxDuration}/60%60)"/>
<TEXT value="Flight Duration" .../>
<ROW ...>
<COL ...>
<TXT value="min" .../>
<TXT value="{minDurationHours}h {minDurationMinutes}m".../>
</COL>
<COL ...>
<TXT value="total" .../>
<TXT value="{totalDurationHours}h {totalDurationMinutes}m" .../>
</COL>
<COL ...>
<TXT value="max" .../>
<TXT value="{maxDurationHours}h {maxDurationMinutes}m" .../>
</COL>
</ROW>
</BOX>
...

And again repeating this pattern for the others:

     ...
<BOX ...>
<VAR id="roundedTotalDistance" value="=round({trackerdata.data.totalDistance},0)"/>
<VAR id="roundedMinDistance" value="=round({trackerdata.data.minDistance},0)"/>
<VAR id="roundedMaxDistance" value="=round({trackerdata.data.maxDistance},0)"/>
<TEXT value="Distance Flown" .../>
<ROW ...>
<COL ...>
<TXT value="min" .../>
<TXT value="{roundedMinDistance} km" .../>
</COL>
<COL ...>
<TXT value="total" .../>
<TXT value="{roundedTotalDistance} km" .../>
</COL>
<COL ...>
<TXT value="max" .../>
<TXT value="{roundedMaxDistance} km" .../>
</COL>
</ROW>
</BOX>
</ROW>
<ROW>
<BOX ...>
<VAR id="totalFuelCalc" value="=round({trackerdata.data.totalDuration}/60/60 * {taildata.data.fuelBurnPerHour}, 0)"/>
<VAR id="minFuelCalc" value="=round({trackerdata.data.minDuration}/60/60 * {taildata.data.fuelBurnPerHour}, 0)"/>
<VAR id="maxFuelCalc" value="=round({trackerdata.data.maxDuration}/60/60 * {taildata.data.fuelBurnPerHour}, 0)"/>
<TEXT value="Fuel in Flight" .../>
<ROW ...>
<COL ...>
<TXT value="min" .../>
<TXT value="{minFuelCalc} gal" .../>
</COL>
<COL ...>
<TXT value="total" .../>
<TXT value="{totalFuelCalc} gal" .../>
</COL>
<COL ...>
<TXT value="max" .../>
<TXT value="{maxFuelCalc} gal" .../>
</COL>
</ROW>
</BOX>
<BOX ...>
<VAR id="totalCostCalc" value="={trackerdata.data.totalDuration}/60/60 * {taildata.data.hourlyTotal}"/>
<VAR id="minCostCalc" value="={trackerdata.data.minDuration}/60/60 * {taildata.data.hourlyTotal}"/>
<VAR id="maxCostCalc" value="={trackerdata.data.maxDuration}/60/60 * {taildata.data.hourlyTotal}"/>
<TEXT value="Flight Cost" .../>
<ROW ...>
<COL ...>
<TXT value="min" .../>
<TXT value="=number({minCostCalc}, true, true)" .../>
</COL>
<COL ...>
<TXT value="total" .../>
<TXT value="=number({totalCostCalc}, true, true)" .../>
</COL>
<COL ...>
<TXT value="max" .../>
<TXT value="=number({maxCostCalc}, true, true)" .../>
</COL>
</ROW>
</BOX>
</ROW>

Creating a View From Data Widgets

Connecting Chart Data

Our second last item we need to complete is the chart data. This, like MAP and LIST, is as simple as assigning data to the chart and adding a couple bindings.

The 3 types of charts displaying data

For all of our charts, we will add a datasource with subsets of DATA deriving from the GET widget call, which we will transform to our liking:

<DATA id="chartdata1" value="{trackerdata.data}">
<CALC target="departureDayOfWeek" operation="eval" source="toDate('{data.firstSeen}000', '', 'EEEE')"/>
<CALC target="arrivalDayOfWeek" operation="eval" source="toDate('{data.lastSeen}000', '', 'EEEE')"/>
<CALC target="departureDayCount" source="departureDayOfWeek" operation="total"/>
<CALC target="arrivalDayCount" source="arrivalDayOfWeek" operation="total"/>
<CALC target="duration" operation="eval" source="({data.lastSeen} - {data.firstSeen})/60/60"/>
<CALC target="departureDate" operation="eval" source="toDate('{data.firstSeen}000', '', 'dd/MM h:mm')"/>
<CALC target="departureTimeOfDay" operation="eval" source="toDate('{data.firstSeen}000', '', 'HH')"/>
<CALC target="arrivalTimeOfDay" operation="eval" source="toDate('{data.lastSeen}000', '', 'HH')"/>
<CALC target="departureTimeCount" source="departureTimeOfDay" operation="total"/>
<CALC target="arrivalTimeCount" source="arrivalTimeOfDay" operation="total"/>
<DATA id="distinctDepartureTime">
<DISTINCT field="departureTimeOfDay"/>
</DATA>
<DATA id="distinctArrivalTime">
<DISTINCT field="arrivalTimeOfDay"/>
</DATA>
<DATA id="distinctDepartureDay">
<DISTINCT field="departureDayOfWeek"/>
</DATA>
<DATA id="distinctArrivalDay">
<DISTINCT field="arrivalDayOfWeek"/>
</DATA>
</DATA>

Next, we will connect this data to our line chart:

<CHART ...>
<XAXIS type="category" title="Date" />
<YAXIS type="numeric" title="Duration (h)" />
<SERIES name="Duration per Flight" type="line" data="chartdata" x="{data.departureDate}" y="{data.duration}" color="lightblue" />
</CHART>

Our bar chart, which we also need an additional hardcoded datasource to display 0–24 on the x axis:

<GET id="timehourdata" url="resources/timedata.xml" root="ROWS.ROW"/>
<CHART>
<XAXIS type="category" title="Hour" />
<YAXIS type="numeric" title="Flights" />
<SERIES name="Hour" type="bar" data="timehourdata" x="{data.timeOfDay}" y="0" color="lightblue" />
<SERIES name="Departure Hour" type="bar" data="distinctDepartureTime" x="{data.departureTimeOfDay}" y="{data.departureTimeCount}" color="lightblue" />
<SERIES name="Arrival Hour" type="bar" data="distinctArrivalTime" x="{data.arrivalTimeOfDay}" y="{data.arrivalTimeCount}" color="purple" />
</CHART>

and our two pie charts:

<CHART ...>
<XAXIS title="Departures" />
<SERIES name="Departure Day" data="distinctDepartureDay" x="{data.departureDayOfWeek}" label="{data.departureDayOfWeek}" y="{data.departureCount}" />
</CHART>
<CHART ...>
<XAXIS title="Arrivals" />
<SERIES name="Arrival Day" data="distinctArrivalDay" x="{data.arrivalDayOfWeek}" label="{data.arrivalDayOfWeek}" y="{data.departureCount}" color="purple"/>
</CHART>

Connecting Map Data

Finally, to connect our map to data we want to display 4 specific markers; The selected departure location, the selected arrival location, all arrival locations, and all departure locations.

A map displaying multiple location markers

Because this data already exists, we simply add the new markers and bindings to the map:

<MAP ...>
<MARKER data="trackerdata" latitude="{data.departuredata.Latitude}" longitude="{data.departuredata.Longitude}">
<ICON icon="location_on_outlined" .../>
</MARKER>
<MARKER data="trackerdata" latitude="{data.arrivaldata.Latitude}" longitude="{data.arrivaldata.Longitude}">
<ICON icon="location_on_outlined" color="black"/>
</MARKER>
<MARKER latitude="{planelist.data.departuredata.Latitude}" longitude="{planelist.data.departuredata.Longitude}">
<ICON icon="flight_takeoff" color="purple" size="40"/>
</MARKER>
<MARKER latitude="{planelist.data.arrivaldata.Latitude}" longitude="{planelist.data.arrivaldata.Longitude}">
<ICON icon="flight_land" color="purple" size="40"/>
</MARKER>
</MAP>

And there you have it, a selectable list and repeated list of data both displayed on interaction with the MAP.

Conclusion

In 380 Lines we have created the following, fully functional and robustly feature packed dashboard:

The completed dashboard at the end of Part 2

As you can see, a lot of this heavy lifting can be done in the backend calls, which would free up approximately 80 lines of space, reducing our whole template to 300 lines! Due to our self-imposed constraints for this tutorial, we were able to leverage the widgets within FML to manipulate the data as needed!

With that we have completed the dashboard from start to finish! With 380 lines completed, you can see FML is simple to write and powerful. Remember, for any assistance, suggestions, or feedback:

Thanks for reading!

--

--

Framework Markup Language

Framework Markup Language development team. Build apps faster, deploy cross platform in real time.