-
Notifications
You must be signed in to change notification settings - Fork 39
CommonWidgets
The common
module is used to control overall application appearance. It contains literals such as:
- Color palette definitions
- Font size definitions
- Literals used to control appearance such as widths and animation durations
It also contains widgets directly derived from Kivy base classes that are customized for QuESt. More complex custom widgets such as those composed of many other widgets can be found in the Proving Grounds
.
The common
files can be found in:
/es_gui/resources/widgets/common.*
The common
module shall contain these "static" values (literals) to make the GUI consistent in behavior and appearance.
Animation durations are defined in the .py file with all caps variable names. They may sometimes also be defined in the .kv file as needed. An example usage of these literals is to make the animations in wizards across QuESt applications consistent.
QuESt primary, secondary, and support colors are specified in hexadecimal in the .kv file using the #set: <name> <value>
notation. These are the colors selected by the creative services consultants to be part of the QuESt brand. These colors are also specified in their RGB form as a list of tuplets in the .py file:
PALETTE = [(primary RGB), (secondary RGB), (support1 RGB), ...]
This list is for sampling the colors in .py code.
Font sizes are specified in the .kv file using the #set
notation:
#:set default_font sp(20)
#:set stnd_font sp(22)
#:set large_font sp(30)
#:set huge_font sp(40)
Font sizes in other .kv files are defined using these variable/aliases. This allows us to modify the font sizes specified by these names at will as needed.
In the common.kv
file, certain properties of the Kivy base widget
class are overridden:
<Widget>:
font_size: default_font
font_name: 'Exo 2'
This is done to adjust the default behavior of all widgets (all widgets are derived from this base class). For example, this allows us to adjust the font size and face of widgets such as the action buttons in the ActionBar or items in a Spinner without explicitly defining them.
The Kivy base TextInput
class is also adjusted:
<TextInput>:
on_focused: self.focused and clock.Clock.schedule_once(lambda dt: self.select_all())
This is so that the contents of a text input field are selected when the widget becomes focused (i.e., when you click on it).
Recycle views are an important widget for displaying (potentially large) sets of data. They are mostly commonly used in QuESt to display a list of choices from which the user selects one or many. The Kivy Recycle View widget implements the model-view-controller pattern; each of these must be specified when using a recycle view.
We use a derived recycle view class called MyRecycleView to customize appearance and behavior. This class is defined in the common.*
files. An example usage of this widget in a .kv file is as follows:
MyRecycleView:
id: building_rv
viewclass: 'BuildingRVEntry'
size_hint_y: 0.8
SelectableRecycleBoxLayout:
multiselect: False
touch_multiselect: False
The recycle view takes the appearance of its child which is the SelectableRecycleBoxLayout in this case. This is essentially a BoxLayout widget with some behavior classes mixed in to achieve the functionality of a (multi-)select list. Inserting a MyRecycleView in a widget hierarchy acts as one might expect; it's equivalent to inserting a BoxLayout. To allow multiple selections, specify the multiselect
and touch_multiselect
properties as True.
The key for using these recycle views is specifying a unique view class defined in the viewclass
property of the MyRecycleView object. We use a custom class called RecycleViewRow
which is derived from a BoxLayout as well as RecycleDataViewBehavior:
class RecycleViewRow(RecycleDataViewBehavior, BoxLayout):
The view class is the representation of each unit of data in the recycle view; in our case, it is represented in a BoxLayout with a label.
In the above example, the view class is specified as "BuildingRVEntry". This class is derived from the RecycleViewRow
as follows:
class BuildingRVEntry(RecycleViewRow):
host_screen = None
def apply_selection(self, rv, index, is_selected):
"""Respond to the selection of items in the view."""
super(BuildingRVEntry, self).apply_selection(rv, index, is_selected)
if is_selected:
self.host_screen.building_selected = rv.data[self.index]
The reason for having a unique view class for each recycle view is to enable interactions between the recycle view and another object. In many implementations, we use the concept of a host_screen
, which is the Screen widget that "hosts" the recycle view (the Screen is the nth generation parent [with respect to widget hierarchy, not inheritance] of the recycle view). The Screen keeps track of the input/selections on it through, e.g., Kivy object properties, which can be reported to other objects. The host_screen
is specified as a class property of this view class.
The apply_selection()
class method is called when views are selected/deselected in the recycle view. If the specific view (row) is selected (is_selected == True
for the instance), the instance can report to its host as in the above example. In this example, the host's building_selected
property is assigned as the data represented by this row:
self.host_screen.building_selected = rv.data[self.index]
It is through this mechanism that the selection(s) from the recycle view are communicated to other objects. Since class properties are shared among all instances of a class, a unique class for each recycle view view class needs to be created. For example, some screens have multiple recycle views on them; the view class for each of these recycle views may share the same host_screen
but implement the apply_selection
differently.
There is a collection of widgets derived from base Kivy widgets for the purpose of small customizations of appearance and behavior. It is preferred to use or derive from these widgets if possible. Some of these widgets includes:
- LeftAlignedText: Label widgets with left-aligned text and no text overflow. Useful for paragraphs.
- TitleTextBase: Label widgets for title text.
- BodyTextBase: Label widgets for body text.
- TileButton: Button widget with quadrilateral appearance. An essential GUI component.
- WizardPrevButton/WizardNextButton: Tile buttons for navigating wizards.
- ToggleTileButton: TileButton with toggle behavior, e.g., for selecting option(s) from a set.
- PlotSpinner: Spinner with no text overflow.
- MyPopup: Standard Popup window.
- WarningPopup: Popup window designed to only display a warning message. Comes with one button to dismiss it.
- LoadingModalView: A ModalView for a loading screen. Its
auto_dismiss
is False by default, so it needs to be dismissed manually (e.g., the loading process it represents finishes - the process then dismisses the instance).
The WizardReportInterface is a Screen widget displayed at the conclusion of a wizard after simulations are completed. Its appearance can be described as follows:
<WizardReportInterface>:
BoxLayout:
orientation: 'horizontal'
BoxLayout: # ScreenManager for actually displaying reports
orientation: 'horizontal'
ReportScreenManager:
[report screens]
BoxLayout: # Toolbar / Report selection
orientation: 'vertical'
[...]
GridLayout: # Toggle selection for each available report
TileButton: # Button to open UI for generating HTML summary report
These reports are for displaying summaries of simulation/optimization results in a quick, visual manner.
Each individual tool/application that makes use of a wizard can import the WizardReportInterface into its own reporting module. For example:
from es_gui.resources.widgets.common import ReportScreen, WizardReportInterface, ReportChartToggle
class BtmCostSavingsReport(WizardReportInterface):
<snip>
The WizardReportInterface itself has little behavior defined. It inherits from the Screen Kivy class and only overrides the on_enter
observer method - this method randomly selects from the defined reports to display one after a short delay when the screen is opened. The majority of the behavior has to be implemented by the inheriting class.
- Define each report type.
- Pass the wizard results data and metadata to an instance property
- For each report:
- Create a toggle button
- Bind the corresponding
add_report()
method call to the button - Add the toggle button to the report selector
- Build the report (ReportScreen)
- Add the report to its report ScreenManager
- Change report ScreenManager's current screen to this report
- Assign
host_screen
class property of generate report menu (HTML report) to this class (instance) - Create an instance of the generate report menu class
- Open the instance
The ReportScreen is a widget derived from the Screen Kivy class designed as a template for report screens in the WizardReportInterface. Its composition is summarized as follows:
<ReportScreen@Screen>:
title: title
desc: desc
desc_bx: desc_bx
chart_bx: chart_bx
BoxLayout:
orientation: 'vertical'
ReportHeaderDesc: # Title text
id: title
BoxLayout: # Report body
orientation: 'horizontal'
id: chart_bx
BoxLayout:
orientation: 'horizontal'
id: desc_bx
ReportBodyText: # Report text
id: desc
Roughly speaking, this translates to:
- Title text at the top
- Summary paragraph/text with key info about the chart/report and numbers highlighted
- The chart/figure
The width of the report body text is optimized for the optimal span (three widths of the alphabet at the default font and application size).
The ReportScreen has no behaviors overriden from its base Kivy class but instead has its appearance defined as above. Each wizard's reporting module is thus responsible for importing the definition and inheriting from the ReportScreen class:
from es_gui.resources.widgets.common import ReportScreen
class BtmCostSavingsReportScreen(ReportScreen):
"""A report screen for the BTM Cost Savings Wizard."""
<snip>
An instance of this class represents a single report. Thus, a new instance is created for each report - consequently, the class should be able to create any desired report. The design of these ReportScreen-derived classes is to encapsulate most of the actual report details. A rough outline of implementing class design is as follows:
- Pass the wizard data and requested report type from required constructor arguments to instance properties
- Check the report type (
if/elif/else
)- Change the screen's (
chart_bx
) orientation if necessary - Instantiate the appropriate Chart object without populating it
- Assign the Chart object to
self.chart
- Bind this screen's
on_enter
observer method to a class method for generating the correct report
- Change the screen's (
- Add the Chart object as a child of the
chart_bx
BoxLayout
Chart objects are custom widgets used to create charts using Kivy's animation and graphics engine. They can be found in the proving grounds module.
- Clear the Chart object's child widgets
There is a class method for each chart/report type. Roughly, these methods:
- Parse the wizard data and distill it into a data format expected by the Chart object. These can be lists of lists, dictionaries, etc.
- Call the
draw_chart
method of the Chart object while providing it necessary input such as the parsed data. - Generate the report text and title, assigning them to the ReportScreen's (this) properties.
- The wizard completes after running optimizations, etc.
- The wizard moves screens to the WizardReportInterface. The passes the data and metadata to it.
- The WizardReportInterface, for each defined report type:
- Creates a ReportScreen instance.
- Adds the instance to its report screen manager's children.
- Creates a report selection toggle button and binds report creation to it.
- Each ReportScreen is for an individual report. Upon its creation it:
- Checks the report type and create an appropriate Chart object
- Binds its
on_enter
observer property to a specific class method for generating the report.
- Upon entering a ReportScreen:
- A
generate_[...]_chart
class method is called to parse wizard data, draw the appropriate chart, and generate report title and text.
- A
The ParameterGridWidget (and associated components) is a widget based on a GridLayout designed for generating input fields prescribed by a static data set. It is composed of ParameterRow objects, themselves being GridLayouts, containing the following items:
- Name: name of the parameter
- Notes: description or other text related to the parameter
- TextInput: text input field for the user to enter a value. Hint text can be provided to set a default value.
- Units: units of the parameter
The constructor of the ParameterRow object accepts a dictionary to populate each of its items. A ParameterGridWidget object has ParameterRow child objects. It also has class methods for obtaining all of its inputs by iterating through its children.
An example is with the ESS parameter interface in QuESt BTM. The API parameters are represented using a ParameterGridWidget. The parameters are defined in a static .json file:
/es_gui/apps/data_manager/_static/btm_cost_savings_model_params.json
[
{
"name": "energy capacity",
"attr name": "Energy_capacity",
"notes": "The maximum amount of energy that the ESS can store.",
"max": 1000,
"default": 100,
"step": 1,
"units": "kWh"
},
{
"name": "power rating",
"attr name": "Power_rating",
"notes": "The maximum rate that at which the ESS can charge or discharge energy.",
"max": 1000,
"default": 100,
"step": 1,
"units": "kW"
},
{
"name": "transformer rating",
"attr name": "Transformer_rating",
"notes": "The maximum amount of power that can be exchanged.",
"max": 1000000000,
"default": 1000000,
"step": 1,
"units": "kW"
},
{
"name": "self-discharge efficiency",
"attr name": "Self_discharge_efficiency",
"notes": "The percentage of stored energy that the ESS retains on an hourly basis.",
"max": 100,
"default": 100,
"step": 1,
"units": "%/h"
},
{
"name": "round trip efficiency",
"attr name": "Round_trip_efficiency",
"notes": "The percentage of energy charged that the ESS actually retains.",
"max": 100,
"default": 85,
"step": 1,
"units": "%"
},
{
"name": "minimum state of charge",
"attr name": "State_of_charge_min",
"notes": "The minimum ESS state of charge as a percentage of energy capacity.",
"max": 100,
"default": 0,
"step": 1,
"units": "%"
},
{
"name": "maximum state of charge",
"attr name": "State_of_charge_max",
"notes": "The maximum ESS state of charge as a percentage of energy capacity.",
"max": 100,
"default": 100,
"step": 1,
"units": "%"
},
{
"name": "initial state of charge",
"attr name": "State_of_charge_init",
"notes": "The percentage of energy capacity that the ESS begins with.",
"max": 100,
"default": 50,
"step": 1,
"units": "%"
}
]
Each object in the JSON is an individual parameter with fields relevant for parameter row creation. This .json file is loaded into Python by the Data Manager. Each parameter is iterated over and populates a ParameterRow object, each of which are added as children to a ParameterGridWidget.
The ResultsViewer is a built-in plotting tool derived from the Screen Kivy class. It uses matplotlib to draw graphs which are rendered as static images in the application GUI. While different QuESt application look at different data, a lot of the plotting needs are common; thus, the ResultsViewer can be defined as a base class to factor out much of the commonality.
The class needs to be inherited from for each specific results viewer:
from es_gui.resources.widgets.common import ResultsViewer
class BtmResultsViewer(ResultsViewer):
"""The screen for displaying plots inside the application or exporting results."""
The key overrides/implementations are:
-
on_pre_enter
: This class method needs to get the collection of solved Optimizer objects from the relevant Optimizer handler. -
_update_toolbar
: The plotable quantities ("vars)" needs to be defined for the variable selection spinner. -
draw_figure
: The logic for each plot type needs to be implemented (if/elif/else
)
Example snippet:
if plot_type == 'load':
for key in results:
df = results[key]
ax.plot((df['Pload'])[start_time:end_time], drawstyle='steps-post', label=textwrap.fill(key, 50))
ax.set_ylabel('kWh')
ax.set_xlabel('ending hour')
ax.set_title('Load (kWh)')
-
export_png
andexport_csv
: The save location for each needs to be set. The superclass can be called to actually do the saving.
Example snippet:
def export_png(self):
"""Exports currently displayed figure to .png file in specified location."""
outdir_root = os.path.join('results', 'btm', 'plots')
super(BtmResultsViewer, self).export_png(outdir_root)