Show data & tables
There are two different ways to present data in a view:
- For a table-like presentation of data, you can use the TableView
- For a tree-like presentation of grouped data (flat or nested), you can use the DataView
TableView
The TableView
is a trade-off between simplicity and flexibility. If you need more control on styling,
this package might suit your needs. It uses a WebView
under
the hood.
The TableView
provides means to present data in a tabular form. The view has a button
to download the data as CSV.
Example TableView
A table view can be created by passing data to a TableResult
in a view-method
decorated with @TableView
:
from datetime import date
from viktor.views import TableView, TableResult
class Controller(ViktorController):
...
@TableView("Student Grades", duration_guess=1)
def table_view(self, params, **kwargs):
data = [
[128, "John", 6.9, False, date(1994, 6, 3)],
[129, "Jane", 8.1, True, date(1995, 1, 24)],
[130, "Mike", 7.5, False, date(1989, 12, 4)],
]
return TableResult(data)
Custom headers
Custom headers can be set on both rows and columns by passing respectively row_headers
and column_headers
in the
TableResult
:
@TableView("Student Grades", duration_guess=1)
def table_view(self, params, **kwargs):
...
return TableResult(
data,
row_headers=["Person 1", "Person 2", "Person 3"],
column_headers=["Student nr.", "Name", "Grade", "Cum laude", "Date of birth"]
)
If no headers are provided, the default headers (1, 2, 3, ...) will be shown.
Sorting & filtering
Sorting & filtering is automatically enabled on the columns if each of the columns contains data of homogeneous type.
You can disable sorting & filtering by setting enable_sorting_and_filtering=False
on the TableResult
:
return TableResult(..., enable_sorting_and_filtering=False)
Styling
Styling on a per-cell basis can be applied by replacing the value in data with a
TableCell
instance. The following styling can be applied on a per-cell basis:
text_color
: color of the text. Can be anyColor
background_color
: color of the cell background. Can be anyColor
text_style
: style of the text. Can be one of: 'bold', 'italic'
from viktor import Color
from viktor.views import TableView, TableResult, TableCell
class Controller(ViktorController):
...
@TableView("Table", duration_guess=1)
def table_view(self, params, **kwargs):
data = [
[1, 2, 1],
[2, TableCell(3, background_color=Color.green()), 2],
[1, 2, 1],
]
return TableResult(data)
Styling on a per-column/row basis can be applied by setting a TableHeader
on
respectively column_headers
or row_headers
within TableResult
. The following styling can be applied on a
per-column/row basis:
num_decimals
: number of decimals to which the value is rounded (numbers only)align
: alignment of the text within the column/row. Can be one of: 'left', 'center', 'right'
from viktor.views import TableView, TableResult, TableHeader
class Controller(ViktorController):
...
@TableView("Student Grades", duration_guess=1)
def table_view(self, params, **kwargs):
data = [
["John", 6.9],
["Jane", 8.1],
["Mike", 7.5],
]
return TableResult(data, column_headers=[
"Name",
TableHeader("Grade", num_decimals=0)
])
Transposing the data
Sometimes it is preferred to present data in a transposed table (with homogeneous types on rows instead of columns),
for example when the number of columns would otherwise be much larger than the number of rows. This can simply be
achieved by transposing the data yourself, and exchanging row_headers
and column_headers
in the TableResult
, as
shown in the following example:
Sorting & filtering is disabled if one or more columns contain mixed-type data.
from datetime import date
from viktor import Color
from viktor.views import TableView, TableResult, TableCell
class Controller(ViktorController):
...
@TableView("Student Grades", duration_guess=1)
def table_view(self, params, **kwargs):
data = [
[128, "John", 6.9, False, date(1994, 6, 3)],
[129, "Jane", TableCell(8.1, background_color=Color.green()), True, date(1995, 1, 24)],
[130, "Mike", 7.5, False, date(1989, 12, 4)],
]
transposed_data = [list(d) for d in zip(*data)]
return TableResult(
transposed_data,
row_headers=["Student nr.", "Name", "Grade", "Cum laude", "Date of birth"],
column_headers=["Person 1", "Person 2", "Person 3"],
)
Example of transposed table
Using pandas
It is possible to pass a pandas DataFrame
or Styler
object directly into a
TableResult
. The column and index names of the DataFrame
will be taken as columns_headers
and row_headers
respectively:
Setting explicit columns_headers
and row_headers
in TableResult
will override the column and index names of the
pandas DataFrame
.
from datetime import date
import pandas as pd
from viktor.views import TableView, TableResult
class Controller(ViktorController):
...
@TableView("Table", duration_guess=1)
def table_view(self, params, **kwargs):
df = pd.DataFrame(
data=[
[128, "John", 6.9, False, date(1994, 6, 3)],
[129, "Jane", 8.1, True, date(1995, 1, 24)],
[130, "Mike", 7.5, False, date(1989, 12, 4)],
],
columns=["Student nr.", "Name", "Grade", "Cum laude", "Date of birth"],
index=["Person 1", "Person 2", "Person 3"],
)
return TableResult(df)
The following styling properties of a pandas Styler
object are supported:
- Background color (per cell)
- Text color (per cell)
- Text style (per cell, bold or italic)
- Align (per column/row, only if all cells in a row/column have the same alignment)
To make use of pandas styling, you need to add the pandas[output-formatting]
package to your requirements.txt
from datetime import date
import pandas as pd
from viktor.views import TableView, TableResult
class Controller(ViktorController):
...
@TableView("Table", duration_guess=1)
def table_view(self, params, **kwargs):
df = pd.DataFrame(
data=[
[128, "John", 6.9, False, date(1994, 6, 3)],
[129, "Jane", 8.1, True, date(1995, 1, 24)],
[130, "Mike", 7.5, False, date(1989, 12, 4)],
],
columns=["Student nr.", "Name", "Grade", "Cum laude", "Date of birth"],
index=["Person 1", "Person 2", "Person 3"],
)
df = df.transpose()
styler = df.style.highlight_max(color="green", axis='columns', subset=pd.IndexSlice[['Grade'], :])
return TableResult(styler)
DataView
With a DataView
, results can be grouped and be presented in a tree-like form. The data
can be flat, or nested up to a maximum of 3 levels.
Example DataView
The following steps have to be performed to visualize the desired results:
- Create the data group(s) consisting of
DataItem
s for visualization. - Pass the group(s) to a decorated view method that returns a
DataResult
.
from viktor.views import DataGroup, DataItem, DataResult, DataView
class Controller(ViktorController):
...
@DataView("OUTPUT", duration_guess=1)
def visualize_data(self, params, **kwargs):
data = DataGroup(
DataItem('Data item 1', 123)
)
return DataResult(data)
Multilayered DataGroup
Below an example is given of a multilayered output group (maximum of three layers deep):
@DataView("OUTPUT", duration_guess=1)
def visualize_data(self, params, **kwargs):
value_a = 1
value_b = 2 * value_a
value_total = value_a + value_b
data = DataGroup(
group_a=DataItem('Group A', 'some result', subgroup=DataGroup(
sub_group=DataItem('Result', 1, suffix='N')
)),
group_b=DataItem('Group B', '', subgroup=DataGroup(
sub_group=DataItem('Sub group', value_total, prefix='€', subgroup=DataGroup(
value_a=DataItem('Value A', value_a, prefix='€'),
value_b=DataItem('Value B', value_b, prefix='€', explanation_label='this value is a result of multiplying Value A by 2')
))
))
)
return DataResult(data)
The explanation_label
can be used to inform the user. The output result will look like this:
Example DataView
Positional or key-worded DataItem?
In previous examples, we saw a DataGroup created with and without the use of keyword arguments. Using keywords is
necessary to link the corresponding DataItem
to the Summary
. A detailed guide of the Summary
can be found
here.
Dynamic group length
The structure of DataGroup
s allows for dynamic creation/addition of data to the overall result, as long as the
maximum number of items is not exceeded. An example of dynamically creating data items is shown below:
shopping_cart = {
'Apple': 0.20,
'Pineapple': 1.80,
'Milk': 1
}
# loop through products
total_cost = 0
data_items = []
for item, price in shopping_cart.items():
data_items.append(DataItem(item, price, prefix='€'))
total_cost += price
# construct data group
data_group = DataGroup(
DataItem('Shopping cart', total_cost, prefix='€', subgroup=DataGroup(*data_items))
)
Setting warning/error statuses
Sometimes it is desired to warn a user when a result exceeds a certain values. For these cases, the status
and
status_message
arguments of a DataItem
can be considered. Suppose we want to warn the user if our shopping cart of
the example above exceeds 5 euros, we can add the following:
shopping_cart.update({
'Cheese': 2.50
})
# loop through products
...
# construct data group
if total_cost <= 5:
status = DataStatus.SUCCESS
status_msg = ''
else:
status = DataStatus.WARNING
status_msg = 'Mind your wallet!'
data_group = DataGroup(
DataItem('Shopping cart', total_cost, prefix='€', status=status, status_message=status_msg, subgroup=DataGroup(*data_items))
)
Example status message
Testing
mock_View
decorator for easier testing of view methods
Methods decorated with @TableView
and @DataView
need to be mocked within
the context of (automated) testing:
import unittest
from viktor.testing import mock_View
from app.my_entity_type.controller import MyEntityTypeController
class TestMyEntityTypeController(unittest.TestCase):
@mock_View(MyEntityTypeController)
def test_data_view(self):
params = ...
result = MyEntityTypeController().data_view(params=params)
self.assertEqual(result.data, ...)