This section covers the set of recipe functionality concerned with the Actions collection of data for charting/reporting purposes, and the configuration of charts for display to the user. See chart and text panel dimensions and ordering for information about how they are laid out on Workspace homepages and Activity views.
At a high level, Frontline’s charting functionality works by allowing the Recipe builder to use RecipeCollectionStatistic Actions
to populate RecipeCollectionStatistics
, and create Chart
instances to visualise the collected data.
The design of these data-gathering functions is intentionally generic, in order for it to be applicable to a variety of use cases. To illustrate how these functions are used, the following example will be used through the next few sections: we will be building a single Recipe Collection that gathers data for two polls. Participants will use the keyword “football” to report their favourite 1990s footballer, and “veg” to send in their favourite vegetarian dish. Sample responses would be “football Baggio” and “veg guacamole”. The owner of the workspace where this survey is being hosted will then be able to see two separate bar charts of the responses’ frequency, one for each question.
RecipeCollectionStatistics
Frontline supports the collection of data into structures called RecipeCollectionStatistics. This is the storage mechanism for statistics gathered by a recipe and acts as the database queried by Charts. A RecipeCollectionStatistic holds data which can be populated by Recipes and visualised in Charts. A RecipeCollectionStatistic consists:
- A
groupIdentifier
: a top-level grouping that can be used to identify related statistics - A
key
: a unique identifier, to identify a particular statistic - A
value
: the current value for the statistic. - A
dateCreated
: the date the statistic was created
There are two types of RecipeCollectionStatistic:
- NumericRecipeCollectionStatistics
- TextRecipeCollectionStatistics
As the names suggest, the only difference between the two types is the data type of value assignable.
In the poll example, we would use NumericRecipeCollectionStatistic
to gather the frequency of occurrence for each unique answer (which is a numeric value). A snapshot of the data during the campaign could be:
NumericRecipeCollectionStatistic One:
- groupIdentifier: “90s Footballers” // to distinguish this from the other survey
- key: “Asprilla” // one of the valid answers
- value: 22 // the number of people who chose this option
NumericRecipeCollectionStatistic Two:
- groupIdentifier: “90s Footballers”
- key: “Radebe”
- value: 54
NumericRecipeCollectionStatistic Three:
- groupIdentifier: “Favourite Vegetarian Dish”
- key: “Guacamole”
- value: 7
This shows the state of two answers in the Footballers poll, and one answer in the Vegetarian one. Note, however, that the exact same data could be represented in a different way. For instance, we could collect separate TextRecipeCollectionStatistic
instances for each individual respondent’s answer, and count the instances.
TextRecipeCollectionStatistic One:
- groupIdentifier: “90s Footballers”
- key: “Asprilla”
- value: 1 // because this entry represents one vote for Asprilla
To produce the intended bar charts with this second data setup, we would count the number of identical responses. 22 TextRecipeCollectionStatistic instances identical to the one above would show there were 22 responses for Asprilla. This shows the flexibility available to the cookbook builder, and consideration must be taken into choosing an appropriate design for each Recipe Collection. The second setup is far less efficient for this use case, because of the summing requirement, so we will use the first suggested data setup.
RecipeCollectionStatistic Actions
RecipeCollectionStatistics are populated using Actions in the Recipe Builder. All 3 actions allow the builder to specify the groupIdentifier, key and value, using FLang-compatible inputs. For all RecipeCollectionStatistic Actions, the groupIdentifier and key inputs operate in the same way. The difference between the available actions is explained below:
AddNumericRecipeCollectionStatisticAction
: this Action creates a new NumericRecipeCollectionStatistic instance whenever it is performed. This means that even if an instance is found with an identical key and groupIdentifier, a separate (and possibly identical, if the value is the same) instance will be created. This would be the Action used to populate date in the second (inefficient) example setup described aboveUpdateNumericRecipeCollectionStatisticAction
: this Action will find any existing NumericRecipeCollectionStatistic with matching key and groupIdentifier, and update its value based on the specified operation and value. The builder can specify an operation: Increment (add), Decrement (subtract) or Set To (replace the old value with a new value). This is the Action used in the first setup described above, incrementing the NumericRecipeCollectionStatistic’s value by one every time it is executed. Note that if the Increment or Decrement operations are used and no entry is found for the specified key and groupIdentifier, they assume the previous value to be zero. This means that the first run of the action in our example would create a new NumericRecipeCollectionStatistic and set its value to 1.UpdateTextRecipeCollectionStatisticAction
: this Action operates on TextRecipeCollectionStatistics. It finds the TextRecipeCollectionStatistic that matches the key and groupIdentifier (or creates one if none exists) and sets its value to the specified string.
Charts and Text panels
Charts are visualisations of the data held by RecipeCollectionStatistics, defined by the Recipe builder. The types of charts that are currently supported are;
- Line chart
- Bar chart
- Time series chart
- Table panel
Text panels are rectangular shaped UI elements that display a single value, defined by the Recipe builder. The value for the text panel is derived from the RecipeCollectionStatistics.
The collection bound statistics can be referenced by Flang and are stored in a variable collectionData
. Charts and TextPanels can be built from the recipe management page.
collectionData
This represents a list of all the RecipeCollectionStatistics stored against a collection using the RecipeCollectionStatisticActions. Using the example above, a recipe builder can get the number of people who chose Asprilla by typing in collectionData.find { it.groupIdentifier == "90s Footballers" && it.key == "Asprilla" }.value
. As seen from this example, a Recipe builder would need to know the various properties available to the different types of RecipeCollectionStatistics.
The available properties are defined below
NumericRecipeCollectionStatistic {
Date dateCreated
String groupIdentifier
String key
BigDecimal value
}
TextRecipeCollectionStatistic {
Date dateCreated
String groupIdentifier
String key
String value
}
From the recipe management page, a recipe builder can build the bar or line chart by clicking on the “+ Add Chart” button. They will be presented with 3 options : Bar Chart, Line Chart and Text Panel. Clicking on any one of the available options will display a form with which one can create the selected Chart or Text Panel.
How to build a Line or Bar chart
Line and bar charts use a similar configuration. A recipe builder would need to supply the following:-
name
: This is the title that will be displayed above rendered chartX Axis Label
: This is the label displayed alongside the x-axis of the graphY Axis Label
: This is the label displayed alongside the y-axis of the graphFlang Expression
: This is Flang that when evaluated will return a map of values that can be charted. This is the most important bit of the chart. From this expression, a recipe builder can reference thecollectionData
variable described earlier. Using our football example, the returned map should look as follows:-
return [
variables : ["Asprilla", "Football Team 2", "Football Team 3"],
data : [
[22,45,12]
]
]
The main requirements in the returned map are:-
- The presence of the keys variables and
data
variables
should be a list- data should be a list of lists
In the example above, data is a list of one list and would therefore render one bar of one line. But suppose there were two lists in the data key then two bars or two lines would be rendered. This is useful especially when comparing two variables. For this example, the map returned would look as follows.
[
variables : ["Asprilla", "Football Team 2", "Football Team 3"],
data : [
[22,45,12],
[34,20,23]
]
]
To get a proper graph the number of items in the variables list needs to be equal to the number of items in each of the data lists. If this is not the case, then the rendered graphs would look weird.
The map examples above are using hard coded values but when building recipes one would need to transform the data in collectionData
to the desired map format. Again we shall use the football example.
//build the x-axis values
def xValues = collectionData.collect { it.key }
//closure that retrieves the respective y value
def getYValue = { xValue ->
return collectionData.find { it.key.equals(xValue) }.value
}
def yValues = xValues.collect { getYValue(it) }
return [variables:xValues, data:[yValues]]
Since every RecipeCollectionStatistic stores its own dateCreated variable then this makes it possible to build charts where the x-axis is a point in time. This is a more complex graph and needs a very good understanding of groovy. Below is pseudo code that can be used to create a more time based graph. Assume collectionData
stores RecipeCollectionStatistics that represent the humidity over time. Each data point is collected daily.
def xValues = collectionData.collect {
it.dataCreated.format("yyyy/MM/dd")
}
def yValues = collectionData.collect {
it.value
}
return [variables:xValues, data:[[yValues]]]
Please note
- For the Line or Bar Chart to render properly, the array values supplied for both variables and data need to be of the same length. For example, if the
xValues
array is of length 5 then theyValues
array length should be 5 also. - The
xValues
(in Line Charts) andyValues
(in Bar Charts) are used to automatically determine the x-axis labels to display. When the variables value label is very long and does not have spaces in it, the charting library will fail to wrap it and will therefore not display it properly on the x-axis. To cater for this deficiency in the charting library used, a Recipe Builder needs to ensure that thexValues
have spaces when the individial variables value is too long. An example of a bad variables value istotal_time_to_arrive
. An alternative good variables key isTotal Time To Arrive
How to build a Time series chart.
Time series charts are used for showing trends in data received over time for a given metric. For example, you could plot the weather trends over a period of 1 year having collected the data on a daily basis.
For the most part, building a Time series chart is the same as building a Line or Bar chart except for the following differences.
Time series charts require that you provide following additional configuration settings;
- Upper Limit Timestamp. This is the date (represented as a timestamp) beyond which data should not be displayed on the chart. For example you could have collected data over the period of Jan 2012 to Jan 2016 but only wish to show data up to Dec 2012. This setting is optional.
- Lower Limit Timestamp. This is the date (represented as a timestamp) below which data should not be displayed on the chart. For example you could have collected data over the period of Jan 2012 to Jan 2016 but only wish to show data starting from Oct 2016. This setting is optional.
- Date format. On the x-axis of a Time series chart, date labels are displayed. The Date format is used to determine how these date labels will be shown. For example MMM DD will yeild labels like Jul 12, Dec 24, etc. This setting is compulsory.
- Divisor. The divisor is used for specifying the number of labels that you wish to have displayed on the x-axis of the chart. The higher this number, the higher the number of labels shown. This setting is compulsory.
On top of the above, the format for the chart Flang expression for a Time series chart is different from that of a Line or Bar chart. Below are the details of how to specify the chart Flang expression for a Time series chart.
return [ data: [ [ // data for line chart 1 [ time: timestamp_1, value: value_1 ], [ time: timestamp_2, value: value_2 ] .... ], [ // optionally data for line chart 2 [ time: timestamp_1, value: value_1 ], [ time: timestamp_2, value: value_2 ] .... ],
[ // optionally data for line chart 3 [ time: timestamp_1, value: value_1 ], [ time: timestamp_2, value: value_2 ] .... ] ..... ] ]
How to build a Text panel
TextPanels as described earlier aim to display single values within a box in the activities show page. A recipe builder would need to supply the following
name
: The title describing the single data value on displayStatistic Group Identifier
andStatistic Key
: These two values form a unique identifier used to query for data from thecollectionData
. The recipe builder should aim to store, using the RecipeCollectionStatisticActions, RecipeCollectionStatistics that are compoundly unique based on these two values.
For example, using a UpdateNumericRecipeCollectionStatisticAction, a recipe builder can store and update a RecipeCollectionStatistic whose group identifier and key are fruit
and banana
respectively. To then display a Text Panel with this data, a recipe builder will have to create a Text Panel whose Statistic Group Identifier and Statistic Key are fruit
and banana
respectively.
There will be times when more than one RecipeCollecionStatistic with the same group identifier and key are created. At this time the TextPanel will retrieve the first entry it gets when running its query. It is adviced that when the desired goal is to update a record then the UpdateNumericRecipeCollectionStatisticAction or UpdateTextRecipeCollectionStatisticAction be used.
How to sort data used for Bar Chart
Assuming we have our range of X values
def xValues = collectionData.collect { it.key }
Then we need to create a map, where we map the X value to its corresponding Y value
// Map where data will be stored
def map = []
// Retrieving the X values
def xValues = collectionData.collect { it.key }
// Closure that retrieves the corresponding Y value
def getYValue = { xValue ->
return collectionData.find { it.key.equals(xValue) }.value
}
// Retrieve and store the Y values against the X value
xValues.each {
map[it] = getYValue(it)
}
Now we just need to sort the map
// Map where data will be stored
def map = []
// Retrieving the X values
def xValues = collectionData.collect { it.key }
// Closure that retrieves the corresponding Y value
def getYValue = { xValue ->
return collectionData.find { it.key.equals(xValue) }.value
}
// Retrieve and store the Y values against the X value
xValues.each {
map[it] = getYValue(it)
}
// Sorting the map in ascending order of Y value
def sortedMap = map.sort { it.value }
// Sorting the map in descending order of Y value
sortedMap = map.sort { -it.value }
// Return [x:.. ,y ...]
[x:sortedMap.keySet(), y:[[sortedMap.values()]]]
Random chart data generator
To create sample data for chart display, one can try the following
def xList = []
def data = [:]
def x = 15.times {
xList << it + 1
}
xList.each {
data[it] = Math.random()* 10 * it
}
def sortedData = data.sort { it.value }
[x:sortedData.keySet(), y:[sortedData.values()]]
Showing multi dimensional data in charts
Bar and line charts support showing multiple dimensions of data on a single chart. An example can be that you want to show the commute times for various people in a study of traffic trends around different locations.
In order to show multi-dimension data, the x-axis values of your graph will remain as described in earlier sections above, however, the y-axis values will change. The y-axis values will contain a list of data to be plotted for each of the dimensions.
For example if for our fictitious study the participants were John, Mike and Alice, the y-axis values will be [[John's commute times], [Mike's commute times], [Alice's commute times]]. In this case, the flang expression to be used for rendering the chart will be
[
variables: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
data: [
[4,3,4,2,1],
[10,11,12,10,12],
[5,10,10,5,5]
],
series: [title: "Participants", entries: ['John', 'Mike', 'Alice']]
]
The series object above contains information about the keys to associate with each of the data dimensions being displayed in the chart. The names of the keys for each of the data dimension are contained in the entries property of the series object and the title of the container element of the keys is contained in the title property of the series object.
How to add ellipsis to and number format numerical chart labels
You can now format long numbers in your Bar Charts, Line Charts and Time Series Charts. This is done by adding a property named chartConfig to your chart flang expression as shown below. The config contains one parent property axisConfig that has two properties named xAxis representing the X-axis and yAxis representing the Y-axis.
Number formatting is opt-in. If you want to format the long numbers in the axes labels into a readable format i.e from 2000000 to 2,000,000 then add the property below to the axis you want to format as shown below.
[ variables: ["2015", "2016", "2017", "2018"], data: [ [2000, 6345, 3123, 4123], [3455, 6788, 1235, 2344] ], series: ["Import", "Export"], chartConfig: [ axisConfig: [ xAxis: [ numberFormatting: [thousandsSeparator: true ] ], yAxis: [ numberFormatting: [thousandsSeparator: true ] ] ] ] ]
If you want to override the ellipsizing, then you would need to add the following property to the axis you would like this apply this to.
ellipsizing: [
enabled: true,
threshold: 9,
]
Note that the chartConfig is optional and will not break charts in older cookbooks. Here is an example of all these properties in use for a Line Chart.
[ variables: ["2015", "2016", "2017 Anomaly year see reports", "2018"], data: [ [2000, 6345, 13123, 19123], [3455, 6788, 1235, 12344] ], series: ["Import", "Export"], chartConfig: [ axisConfig: [ xAxis: [ ellipsizing: [ enabled: true, threshold: 9, ], numberFormatting: [ thousandsSeparator: true ] ], yAxis: [ ellipsizing: [ enabled: true, threshold: 17, ], numberFormatting: [ thousandsSeparator: false ] ] ] ] ]
Default Chart Behaviour
- Ellipsizing is ON by default and is set to trigger at 9 characters for the x-axis labels and 17 characters for the y-axis labels.
- Number formatting is OFF by default.
How to build a Table Panel
A Table panel can be used to render your data points in the form of a table.
A recipe builder would need to supply the following:-
- name: This is the title that will be displayed above rendered chart
- Flang Expression: The flang expression for a table panel is similar to the other charts where a map is required to be returned .
The main requirements in the returned map are:-
- data: The table rows that will be rendered including the header.
- style: The classes and css that will be applied to the table.
The data map
return [
data: [
['','English','Math', 'Chemistry', 'Swahili'],
['Ross',20,300,null,"20"],
['Rachel',25,70],
['Chandler',null,null,null,60],
['Joe',20,50,60,15],
],
]
In the data map the first array will be render as the header of the table and will take the default styling for table header. Any null
, empty strings, will be rendered as “-”. Rows that have values less than other rows will be filled with “-“. So the above data map will be rendered as:
Adding styles to a Table panel
We can style our Table panel using the style key in the returned map. We can style or add classes to the table element, a row, a column and even a cell.
how to add styles to the table element
We can style the table element using the table key in the style map.
return [
data: [
['','English','Math', 'Chemistry', 'Swahili'],
['Ross',20,300,null,"20"],
['Rachel',25,70],
['Chandler',null,null,null,60],
['Joe',20,50,60,15],
],
style: [
table: [
classes: 'table-bordered table-hover table-striped',
css: 'background-color:#f6b6c3;'
]
]
]
We can add classes to the table element using the classes property and we can write css using the css property.
The table element already has the bootstrap class table-responsive
so we don’t have to add it.
We can style a row in the table.
We can style a table row using tableRow
property in the style map. The tableRow
property is a map containing the row numbers as the keys of the map.
style: [
table: [
classes: 'table-bordered table-hover table-striped',
css: 'background-color:#f6b6c3;'
],
tableRow: [
1: [
classes: 'lead',
css: 'text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2)'
],
2: [
classes: 'info',
css: 'color: #ffc600;',
]
]
]
For each row we can add classes and the write up the css for that row as well. It should be noted that the first row is the header row. So the above code snippet will add the class lead
and the css text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2)
to the header row.
How to style a cell.
You can style a particular cell with the help of the tableRow
property. We now know that the tableRow
property is a map of containing the rows of the table as keys, so if we want to style for e.g the 4th row i.e Chandler, the 5th column i.e 60 to have a class of success
and a color css property of red we can do this with the help of a columns
property available to the rows in the tableRow
map.
style: [
tableRow: [
4: [
columns:[
5: [
classes: 'success',
css: 'color: red;'
]
]
]
]
]
As we have seen in the how to style a row section we can configure a table row’s class and css. A table row also has a third property only available to the tableRow
property columns
which allows us to style a particular cell.
return [
data: [
['','English','Math', 'Chemistry', 'Swahili'],
['Ross',20,300,null,"20"],
['Rachel',25,70],
['Chandler',null,null,null,60],
['Joe',20,50,60,15],
],
style: [
table: [
classes: 'table-bordered table-hover table-striped',
css: 'background-color:#f6b6c3;'
],
tableRow: [
1: [
classes: 'lead',
css: 'text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2)'
],
2: [
classes: 'info',
css: 'color: #ffc600;',
columns: [ 2:[classes: 'warning', css: 'color: red;']]
],
4: [
columns:[
5: [
classes: 'success',
css: 'color: red;'
]
]
]
]
]
]
How to style a column:
We can style a column using the tableColumn
property in the style
map.
style: [
tableColumn: [
3: [
classes: 'bg-primary',
css: 'width: 20%;'
]
]
]
The tableColumn
is very similar to the tableRow
property. It is a map of the table columns each column having a classes
and css
property. So the above snippet will add a class of bg-primary
and a css of width:20%
to the 3rd column.
return [
data: [
['','English','Math', 'Chemistry', 'Swahili'],
['Ross',20,300,null,"20"],
['Rachel',25,70],
['Chandler',null,null,null,60],
['Joe',20,50,60,15],
],
style: [
table: [
classes: 'table-bordered table-hover',
css: ''
],
tableRow: [
1: [
classes: 'lead',
css: 'text-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2)'
],
2: [
classes: 'info',
css: 'color: #ffc600;',
columns: [ 2:[classes: 'warning', css: 'color: red;']]
],
4: [
columns:[
5: [
classes: 'success',
css: 'color: red;'
]
]
]
],
tableColumn: [
3: [
classes: 'bg-primary',
css: 'width: 8%;'
]
]
]
]
N/B The builder should be aware that the css will overwrite each other based on the specificity ,that means the styling for a cell will overwrite the styling of a row and column.
Table panels are sortable by clicking the headers of each column.
N/B The horizontal scroll bar can only be seen when the table panel height is 4 and above.
0 Comments