diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe144bb6..8ae22ec9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,41 @@
# Master branch changelog
+## Patch 1.2.f
+### QuESt
+* A quick-start tour is now available from the home index screen of QuESt.
+ *
+ * This tour consists of a sequence of slides explaining the general usage of QuESt and is aimed to help new or returning users get oriented.
+ *
+* Help carousel modal views (or "help" for short) are now available throughout QuESt. They may be accessed from the top navigation bar whenever available.
+ *
+ * Opening the help carousel displays a sequence of slides similar to those from the quick-start tour. The content of the help carousel is relevant to wherever it is accessed. For example, opening the help carousel while in QuESt Data Manager will show help related to that application.
+ *
+* Interfaces for users to import custom data have been implemented ("data importer").
+ * The data importer is designed to accept time series data in substitution for similar data acquired through QuESt Data Manager.
+ * Currently, data importers are available for the QuESt BTM Time-of-Use Cost Savings wizard for load profile and photovoltaic power profile.
+ * The data importer is accessible directly within the relevant application when the corresponding data is selected. Instead of selecting from the QuESt data bank, the data importer may be used.
+ *
+ * Upon opening the data importer, a series of prompts will appear. The file chooser can be used to select the desired CSV file to use.
+ * The drive displayed in the file chooser is based on where QuESt is launched from. Only folders and .csv files will be displayed.
+ *
+ * Folders can be expanded by clicking the dropdown arrow to the left of the folder name. Folders can be entered by clicking on the folder name. Clicking the "..\" at the top of the current directory listing will navigate up one directory level.
+ * To finalize file selection, double-click on the desired file name or click on the file name and click on the 'Select' button.
+ *
+ * Next, the data column must be specified to determine the time series that will be used. The requirements for the data will vary among the different data importers, but generally, for hourly data a time series of length 8,760 (for one standard year) is expected. Please refer to the accompoanying description in the data importer window for specific details.
+ * If the selected time series passes validation, the data importer will report a successful import. Upon returning to the current application, the data importer button will change appearance to indicate that time series data was imported and will be used in the current application.
+ *
+ * Note that any data imported will be made available for future use in the QuESt data bank. The data may be denoted by an "imported/" tag (in the case of load profile data) and the name will be based on the originating CSV file name and the data column specified during the import process.
+ * Selecting from the data bank after import will override the imported data.
+ * Opening the data importer and completing the data import process again will override any previous selections or imports.
+
+### Resolved issues
+* An issue where Inf or NaN values in data would create infeasible models that could not be solved. Specifically, for PJM data where mileage ratios are computed, division by zero would result in Inf values. QuESt will use a "fill forward" interpolation method to attempt to correct these instances.
+
+### Known issues
+* An issue where dragging through slides in help carousels or similar widgets instead of using the arrow navigation buttons does not update the "progress indicator" bubbles for the slides.
+ * Clicking the navigation buttons will properly update the bubbles.
+* An issue where graphics/charts for HTML reports generated in application wizards would not save and render properly. A blank or incomplete figure would be saved and displayed in the report. We have increased the time for rendering and saving each figure during the report generation process in order to address this issue.
+
## Patch 1.2.e
### QuESt
* Reports (HTML) generated through wizards such as those in QuESt Valuation and QuESt BTM will now be separated appropriately.
diff --git a/QuESt.kv b/QuESt.kv
index d92a98ce..394bcd64 100644
--- a/QuESt.kv
+++ b/QuESt.kv
@@ -24,6 +24,9 @@
#:include es_gui/apps/btm/cost_savings.kv
#:include es_gui/apps/btm/reporting.kv
+#:include es_gui/proving_grounds/data_importer.kv
+#:include es_gui/proving_grounds/help_carousel.kv
+
:
name: 'index'
@@ -37,7 +40,6 @@
AnchorLayout:
anchor_x: 'left'
anchor_y: 'top'
- #orientation: 'horizontal'
size_hint_y: 0.15
spacing: 5
@@ -52,16 +54,24 @@
source: os.path.join('es_gui', 'resources', 'logo', 'Quest_Logo_RGB.png')
allow_stretch: False
- # TitleTextBase:
- # size_hint_x: 0.6
- # text: WELCOME_TITLE
- # font_name: 'Rajdhani'
- # color: C(hex_primary)
- # markup: True
-
- BoxLayout:
- orientation: 'vertical'
+ AnchorLayout:
+ anchor_x: 'center'
+ anchor_y: 'center'
size_hint_x: 0.65
+
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+ size_hint: (0.25, 0.8)
+
+ BodyTextBase:
+ font_name: 'Exo 2'
+ text: 'New or returning user?'
+
+ TileButton:
+ text: 'Take a quick tour'
+ background_color: C(hex_pms312)
+ on_release: root.open_intro_help_carousel()
BoxLayout:
size_hint_x: 0.15
diff --git a/README.md b/README.md
index 859f9003..5d5dcb74 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
# QuESt: Optimizing Energy Storage
[![Build Status](https://travis-ci.com/rconcep/snl-quest.svg?branch=master)](https://travis-ci.com/rconcep/snl-quest)
-Current release version: 1.2.e
+Current release version: 1.2.f
-Release date: 10/14/19
+Release date: January 17, 2020
## Contact
For issues and feedback we would appreciate it if you could use the "Issues" feature of this repository. This helps others join the discussion and helps us keep track of and document issues.
@@ -81,13 +81,8 @@ You can find the executable version with each release in the [**Releases**](http
#### OSX, Linux
Currently, we do not offer executable packages of QuESt for OSX or Linux operating systems. They are possible to package but we have not implemented those packaging processes yet. Installing from source code is an option.
-#### Solvers compatible with Pyomo
-When running the executable version of QuESt, a solver compatible for Pyomo is required to be installed and on your system path. Here are a few examples:
-
-##### Installing GLPK (for Windows)
-1. Download and extract the executables for Windows linked [here](http://winglpk.sourceforge.net/).
-2. The glpk_*.dll and glpsol.exe files are in the `w32` and `w64` subdirectories for 32-Bit and 64-Bit Windows, respectively. Select the pair for the appropriate version of Windows that you are using. You can place them in the same directory as the QuESt executable. Alternatively, you can place those files to the `C:\windows\system32` directory in order to have them in your system path.
-3. (When placing the files in your system path) Try running the command ``glpsol`` in the command prompt (Windows) or terminal (OSX). If you receive a message other than something like "command not found," it means the solver is successfully installed.
+#### Solvers
+When running the executable version of QuESt, a solver compatible for Pyomo is still required to be installed and on your system path. Please refer to the [solvers](#install-solvers) section for details.
### Installing from source code (advanced)
For all platforms, you can instead install QuESt using the codebase in this repository.
@@ -114,18 +109,20 @@ You will want to obtain the codebase for QuESt. You can do that by downloading a
4. Install a solver for Pyomo to use. See other sections for instructions on this.
### Solvers for Pyomo
-At least one solver compatible with Pyomo is required to solve optimization problems. For QuESt Valuation, a solver capable of solving linear programs is required. GLPK is a suggested option.
+
+At least one solver compatible with Pyomo is required to solve optimization problems. Currently, a solver capable of solving linear programs is required. GLPK and CBC are suggested options for freely available solvers. Note that this list is not meant to be exhaustive but contains the most common viable options that we have tested.
+
+#### Installing GLPK (for Windows)
+1. Download and extract the executables for Windows linked [here](http://winglpk.sourceforge.net/).
+2. The glpk_*.dll and glpsol.exe files are in the `w32` and `w64` subdirectories for 32-Bit and 64-Bit Windows, respectively. Select the pair for the appropriate version of Windows that you are using. You can place them in the same directory as the QuESt executable.
+ * Alternatively, you can place those files to the `C:\windows\system32` directory in order to have them in your system path. This will make GLPK available for the rest of your system instead of just for QuESt.
+ * (When placing the files in your system path) Try running the command ``glpsol`` in the command prompt (Windows) or terminal (OSX). If you receive a message other than something like "command not found," it means the solver is successfully installed.
#### Installing GLPK (for Windows via Anaconda)
If you've installed Python using Anaconda, you may be able to install several solvers through Anaconda's package manager with the following (according to Pyomo's [installation instructions](https://pyomo.readthedocs.io/en/latest/installation.html)):
``conda install -c conda-forge glpk``
-#### Installing GLPK (for Windows)
-1. Download and extract the executables for Windows linked [here](http://winglpk.sourceforge.net/).
-2. The .dll and glpsol.exe files are in the `w32` and `w64` subdirectories for 32-Bit and 64-Bit Windows, respectively. These files need to be in the search path for Windows. The easiest way to do this is to move those files to the `C:\windows\system32` directory.
-3. Try running the command ``glpsol`` in the command prompt (Windows) or terminal (OSX). If you receive a message other than something like "command not found," it means the solver is successfully installed.
-
#### Installing GLPK (for OSX)
You will need to either build GLPK from source or install it using the [homebrew](https://brew.sh/) package manager. This [blog post](http://arnab-deka.com/posts/2010/02/installing-glpk-on-a-mac/) may be useful.
@@ -133,9 +130,10 @@ You will need to either build GLPK from source or install it using the [homebrew
If you've installed Python using Anaconda, you may be able to install several solvers through Anaconda's package manager with the following (according to Pyomo's [installation instructions](https://pyomo.readthedocs.io/en/latest/installation.html)):
``conda install -c conda-forge glpk``
+
``conda install -c conda-forge coincbc``
-##### Installing IPOPT (for Windows)
+#### Installing IPOPT (for Windows)
1. Download and extract the pre-compiled binaries linked [here](https://www.coin-or.org/download/binary/Ipopt/). Select the latest version appropriate for your system and OS.
2. Add the directory with the `ipopt.exe` executable file to your path system environment variable. For example, if you extracted the archive to `C:\ipopt`, then `C:\ipopt\bin` must be added to your path.
3. Try running the command ``ipopt`` in the command prompt (Windows) or terminal (OSX). If you receive a message other than something like "command not found," it means the solver is successfully installed.
@@ -153,6 +151,15 @@ Alternatively, run ```main.py``` in a Python IDE of your choice.
**NOTE: The current working directory must be where ``main.py`` is located (the root of the repository).**
+### Updating QuESt
+#### Installed from executable
+Download and extract the executable package as previously. You can copy over your `\data\` directory to transfer your data bank to the new version. You can also copy over your `\quest.ini` file to migrate your QuESt settings as well.
+
+#### Installed from source code
+If you cloned the GitHub repository, you can execute a `git pull` command in the terminal/cmd while in the root of the QuESt directory. If you haven't modified any source code, there should be no conflicts. The master branch of the repository is reserved for release versions and is the most stable.
+
+If you downloaded an archive of the master branch, you can download the latest release version as if it were a fresh install. You can drag and drop your old data directory so that you do not have to download all the data again if you would like. You can also move your `/quest.ini` file to migrate your settings.
+
## Frequently Asked Questions
@@ -172,11 +179,17 @@ Scaling may also have the effect of confusing Kivy of where a UI element is and
So far, this issue has been observed on a variety of laptops of both Windows and OSX varieties. Our suggestion is to disable OS level scaling or to connect to an external display and try to launch QuESt on it.
-> How do I update QuESt?
+> Are there any help tutorials/manuals/etc. for QuESt?
+
+We strive to make QuESt as lightweight and intuitive to use as possible through its design. In version 1.2.f, we integrated additional help carousels within QuESt to provide additional details throughout the software. We currently do not intend to make a comprehensive manual but may share presentation materials such as mini tutorials that may be of interest.
-If you cloned the GitHub repository, you can execute a `git pull` command in the terminal/cmd while in the root of the QuESt directory. If you haven't modified any source code, there should be no conflicts.
+> I want to know more about how the algorithms work/how the results are computed.
-If you downloaded an archive of the master branch or a release version archive, you can download the latest release version as if it were a fresh install. You can drag and drop your old data directory so that you do not have to download all the data again if you would like. You can also move your `/quest.ini` file to migrate your settings.
+Please see the [references](#references) for relevant publications describing the models that were implemented into QuESt. As we further develop the API and documentation, we will aggregate formulation details in those documents.
+
+> I'm interested in a tool/capability that is not currently in QuESt.
+
+Feel free to drop us a line! User feedback helps shape our development goals and priorities and we would welcome hearing what users would like to have.
### QuESt Data Manager
@@ -268,7 +281,7 @@ Due to (rolling) data availability, data for certain periods may be absent. For
> Why can I only adjust [x] parameters for my energy storage device?
-To streamline the user experience in the Wizard, we decided to reduce the range of options available. Please try the "Single Run" and "Batch Runs" interfaces for fuller flexibility.
+To streamline the user experience in the Wizard, we decided to reduce the range of options available. Please try the "Batch Runs" interface for fuller flexibility.
> The pro forma report's appearance doesn't seem quite right/there is a bunch of cryptic commands underneath the "Optimization formulation" section.
@@ -295,7 +308,7 @@ This is a known issue. You can try to generate the report again in order to fix
> I want to use my own rate structure / PV profile / load profile.
-We are planning on implementing interfaces for importing your own data. However, it is possible to bring your own data.
+Since version 1.2.f, you can import your own time series data (PV and load profiles) through the user interface. You can also import your own data by adding to the QuESt data bank manually. See below for details.
#### Rate structure
The rate structure files are stored as .json files in `/data/rate_structures/` after being downloaded through QuESt Data Manager. You can add a new file following the format of one downloaded using QuESt Data Manager. The general structure of the .json object is as follows:
@@ -333,3 +346,36 @@ The load profile files are stored as .csv files in `/data/load/` after being dow
The format is basically two columns; the "Date/Time" column gives the month, day, and hour and the second column is the hourly kW load. The "Date/Time" columna is used for parsing the correct data for a selected month, for example. A year is not provided because the building data is simulated based on TMY3 (typical meteorological year).
Once new files are added to the `data` bank appropriately, they should be picked up in the relevant applications when you are prompted to make a selection.
+
+## References
+
+Nguyen, Tu A., David A. Copp, and Raymond H. Byrne. "Stacking Revenue of Energy Storage System from Resilience, T&D Deferral and Arbitrage." 2019 IEEE Power & Energy Society General Meeting (PESGM). IEEE, 2019.
+
+Byrne, Raymond H., Tu A. Nguyen, and Ricky J. Concepcion. "Opportunities for Energy Storage in CAISO." 2018 IEEE Power & Energy Society General Meeting (PESGM). IEEE, 2018.
+[Available online](https://www.osti.gov/servlets/purl/1489129).
+
+Byrne, Raymond H., Tu Anh Nguyen, and Ricky James Concepcion. Opportunities for Energy Storage in CAISO. No. SAND2018-5272C. Sandia National Lab.(SNL-NM), Albuquerque, NM (United States), 2018.
+[Available online](https://www.osti.gov/servlets/purl/1515132).
+
+Concepcion, Ricky J., Felipe Wilches-Bernal, and Raymond H. Byrne. "Revenue Opportunities for Electric Storage Resources in the Southwest Power Pool Integrated Marketplace." 2018 IEEE Power & Energy Society General Meeting (PESGM). IEEE, 2018.
+[Available online](https://www.osti.gov/servlets/purl/1574578).
+
+Wilches-Bernal, Felipe, Ricky J. Concepcion, and Raymond H. Byrne. "Electrical Energy Storage Participation in the NYISO Electricity and Frequency Regulation Markets." 2018 IEEE Power & Energy Society General Meeting (PESGM). IEEE, 2018.
+
+Nguyen, Tu A., and Raymond H. Byrne. "Maximizing the cost-savings for time-of-use and net-metering customers using behind-the-meter energy storage systems." 2017 North American Power Symposium (NAPS). IEEE, 2017.
+[Available online](https://www.osti.gov/servlets/purl/1431654).
+
+Nguyen, Tu A., et al. "Maximizing revenue from electrical energy storage in MISO energy & frequency regulation markets." 2017 IEEE Power & Energy Society General Meeting. IEEE, 2017.
+[Available online](https://www.osti.gov/servlets/purl/1408956).
+
+Byrne, Raymond H., Ricky J. Concepcion, and César A. Silva-Monroy. "Estimating potential revenue from electrical energy storage in PJM." 2016 IEEE Power and Energy Society General Meeting (PESGM). IEEE, 2016.
+[Available online](https://www.osti.gov/servlets/purl/1239334).
+
+Byrne, Raymond H., et al. "The value proposition for energy storage at the Sterling Municipal Light Department." 2017 IEEE Power & Energy Society General Meeting. IEEE, 2017.
+[Available online](https://www.osti.gov/servlets/purl/1427423).
+
+Byrne, Raymond H., et al. "Energy management and optimization methods for grid energy storage systems." IEEE Access 6 (2017): 13231-13260.
+[Available online](https://ieeexplore.ieee.org/abstract/document/8016321).
+
+Byrne, Raymond H., and César A. Silva-Monroy. "Potential revenue from electrical energy storage in ERCOT: The impact of location and recent trends." 2015 IEEE Power & Energy Society General Meeting. IEEE, 2015.
+[Available online](https://www.osti.gov/servlets/purl/1244909).
\ No newline at end of file
diff --git a/es_gui/apps/btm/cost_savings.kv b/es_gui/apps/btm/cost_savings.kv
index 3dc8e8a0..d8e04620 100644
--- a/es_gui/apps/btm/cost_savings.kv
+++ b/es_gui/apps/btm/cost_savings.kv
@@ -292,6 +292,7 @@
content: content
load_profile_rv: load_profile_rv
next_button: next_button
+ open_data_importer_button: open_data_importer_button
BoxLayout:
orientation: 'vertical'
@@ -306,7 +307,7 @@
size_hint_y: 0.8
id: content
opacity: 0
- padding: (WIZ_PADDING_X, 0)
+ padding: (WIZ_PADDING_X, 10)
spacing: 10
WizardBodyText:
@@ -314,24 +315,56 @@
text: 'Select a load profile to represent the demand connected to the energy storage system.'
BoxLayout:
- orientation: 'vertical'
+ orientation: 'horizontal'
size_hint_y: 0.9
- spacing: 10
+ spacing: 20
padding: (0, 10)
- TextInput:
- size_hint_y: 0.1
- on_text: load_profile_rv.filter_rv_data(self.text)
- hint_text: 'Filter by name'
- multiline: False
-
- MyRecycleView:
- id: load_profile_rv
- viewclass: 'LoadProfileRecycleViewRow'
-
- SelectableRecycleBoxLayout:
- multiselect: False
- touch_multiselect: False
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+ size_hint_x: 0.8
+
+ TitleTextBase:
+ size_hint_y: 0.05
+ color: C(hex_secondary)
+ text: 'Data bank'
+ font_size: large_font
+
+ TextInput:
+ size_hint_y: 0.1
+ on_text: load_profile_rv.filter_rv_data(self.text)
+ hint_text: 'Filter by name'
+ multiline: False
+
+ MyRecycleView:
+ id: load_profile_rv
+ viewclass: 'LoadProfileRecycleViewRow'
+
+ SelectableRecycleBoxLayout:
+ multiselect: False
+ touch_multiselect: False
+
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+ size_hint_x: 0.2
+
+ TitleTextBase:
+ size_hint_y: 0.05
+ color: C(hex_secondary)
+ text: 'Use my own'
+ font_size: large_font
+
+ AnchorLayout:
+ anchor_x: 'center'
+ anchor_y: 'top'
+
+ TileButton:
+ id: open_data_importer_button
+ size_hint_y: 0.3
+ text: 'Open data importer'
+ on_release: root.open_data_importer()
BoxLayout:
size_hint_y: 0.05
@@ -362,6 +395,7 @@
:
content: content
pv_profile_rv: pv_profile_rv
+ open_data_importer_button: open_data_importer_button
BoxLayout:
orientation: 'vertical'
@@ -376,7 +410,7 @@
size_hint_y: 0.8
id: content
opacity: 0
- padding: (WIZ_PADDING_X, 0)
+ padding: (WIZ_PADDING_X, 10)
spacing: 10
WizardBodyText:
@@ -389,24 +423,55 @@
text: "If there is no PV connected, feel free to skip this step."
BoxLayout:
- orientation: 'vertical'
+ orientation: 'horizontal'
size_hint_y: 0.9
- spacing: 10
+ spacing: 20
padding: (0, 10)
- TextInput:
- size_hint_y: 0.1
- on_text: pv_profile_rv.filter_rv_data(self.text)
- hint_text: 'Filter by name'
- multiline: False
-
- MyRecycleView:
- id: pv_profile_rv
- viewclass: 'PVProfileRecycleViewRow'
-
- SelectableRecycleBoxLayout:
- multiselect: False
- touch_multiselect: False
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+ size_hint_x: 0.8
+
+ TitleTextBase:
+ size_hint_y: 0.05
+ color: C(hex_secondary)
+ text: 'Data bank'
+ font_size: large_font
+
+ TextInput:
+ size_hint_y: 0.1
+ on_text: pv_profile_rv.filter_rv_data(self.text)
+ hint_text: 'Filter by name'
+ multiline: False
+
+ MyRecycleView:
+ id: pv_profile_rv
+ viewclass: 'PVProfileRecycleViewRow'
+
+ SelectableRecycleBoxLayout:
+ multiselect: False
+ touch_multiselect: False
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+ size_hint_x: 0.2
+
+ TitleTextBase:
+ size_hint_y: 0.05
+ color: C(hex_secondary)
+ text: 'Use my own'
+ font_size: large_font
+
+ AnchorLayout:
+ anchor_x: 'center'
+ anchor_y: 'top'
+
+ TileButton:
+ id: open_data_importer_button
+ size_hint_y: 0.3
+ text: 'Open data importer'
+ on_release: root.open_data_importer()
BoxLayout:
size_hint_y: 0.05
diff --git a/es_gui/apps/btm/cost_savings.py b/es_gui/apps/btm/cost_savings.py
index e8d700b1..95d02d4b 100644
--- a/es_gui/apps/btm/cost_savings.py
+++ b/es_gui/apps/btm/cost_savings.py
@@ -7,6 +7,7 @@
import os
import numpy as np
import threading
+import json
from kivy.uix.screenmanager import Screen, ScreenManager, SlideTransition, ScreenManagerException
from kivy.uix.label import Label
@@ -30,7 +31,9 @@
# from es_gui.apps.valuation.reporting import Report
from .reporting import BtmCostSavingsReport
-from es_gui.resources.widgets.common import BodyTextBase, MyPopup, WarningPopup, TileButton, RecycleViewRow, InputError, BASE_TRANSITION_DUR, BUTTON_FLASH_DUR, ANIM_STAGGER, FADEIN_DUR, SLIDER_DUR, PALETTE, rgba_to_fraction, fade_in_animation, WizardCompletePopup, ParameterRow, ParameterGridWidget
+from es_gui.resources.widgets.common import BodyTextBase, MyPopup, WarningPopup, TileButton, RecycleViewRow, InputError, BASE_TRANSITION_DUR, BUTTON_FLASH_DUR, ANIM_STAGGER, FADEIN_DUR, SLIDER_DUR, PALETTE, rgba_to_fraction, fade_in_animation, slow_blinking_animation, WizardCompletePopup, ParameterRow, ParameterGridWidget
+from es_gui.proving_grounds.data_importer import DataImporter
+from es_gui.apps.data_manager.data_manager import DATA_HOME
from es_gui.tools.btm.readutdata import get_pv_profile_string
@@ -38,7 +41,6 @@ class CostSavingsWizard(Screen):
"""The main screen for the cost savings wizard. This hosts the nested screen manager for the actual wizard."""
def on_enter(self):
ab = self.manager.nav_bar
- ab.reset_nav_bar()
ab.set_title('Time-of-Use Cost Savings')
# self.sm.generate_start()
@@ -279,6 +281,7 @@ class CostSavingsWizardLoadSelect(Screen):
"""The load profile selection screen for the cost savings wizard."""
load_profile_selected = DictProperty()
has_selection = BooleanProperty(False)
+ imported_data_selected = BooleanProperty(False)
def __init__(self, **kwargs):
super(CostSavingsWizardLoadSelect, self).__init__(**kwargs)
@@ -294,7 +297,6 @@ def on_enter(self):
self.load_profile_rv.unfiltered_data = load_profile_options
except KeyError as e:
logging.warning('CostSavings: No load profiles available to select.')
- # TODO: Warning popup
Clock.schedule_once(partial(fade_in_animation, self.content), 0)
@@ -310,10 +312,47 @@ def on_load_profile_selected(self, instance, value):
self.has_selection = False
else:
self.has_selection = True
+ self.imported_data_selected = False
def on_has_selection(self, instance, value):
self.next_button.disabled = not value
+ def on_imported_data_selected(self, instance, value):
+ if value:
+ self.open_data_importer_button.text = 'Data imported'
+ self.open_data_importer_button.background_color = rgba_to_fraction(PALETTE[3])
+ Clock.schedule_once(partial(slow_blinking_animation, self.open_data_importer_button), 0)
+
+ self.load_profile_rv.deselect_all_nodes()
+ else:
+ self.open_data_importer_button.text = 'Open data importer'
+ self.open_data_importer_button.background_color = rgba_to_fraction(PALETTE[0])
+ self.open_data_importer_button.opacity = 1
+ Animation.cancel_all(self.open_data_importer_button, 'opacity')
+
+ def open_data_importer(self):
+ write_directory = os.path.join(DATA_HOME, 'load', 'imported')
+ self.data_importer = DataImporter(
+ write_directory=write_directory,
+ format_description="The data units should be in kilowatts and there should be 8,760 samples (hourly for one standard year). The time series is assumed to run January through December at an hourly resolution."
+ )
+ self.data_importer.title.text = "Import a load time series"
+
+ def _check_data_importer_on_dismissal():
+ try:
+ import_filename = self.data_importer.get_import_selections()
+ except (ValueError, AttributeError):
+ logging.warning('DataImporter: Nothing was imported.')
+ except KeyError:
+ logging.warning('DataImporter: Import process was terminated early.')
+ else:
+ logging.info('DataImporter: Data import complete.')
+ self.load_profile_selected = {'name': 'Custom', 'path': import_filename}
+ self.imported_data_selected = True
+
+ self.data_importer.bind(on_dismiss=lambda t: _check_data_importer_on_dismissal())
+ self.data_importer.open()
+
def _validate_inputs(self):
# TODO: Progress already impeded until a profile is selected so...
return self.load_profile_selected
@@ -368,6 +407,8 @@ def apply_selection(self, rv, index, is_selected):
class CostSavingsWizardPVSelect(Screen):
"""The optional PV profile selection screen for the cost savings wizard."""
pv_profile_selected = DictProperty()
+ has_selection = BooleanProperty(False)
+ imported_data_selected = BooleanProperty(False)
def __init__(self, **kwargs):
super(CostSavingsWizardPVSelect, self).__init__(**kwargs)
@@ -395,7 +436,7 @@ def on_leave(self):
Animation.stop_all(self.content, 'opacity')
self.content.opacity = 0
- def on_load_profile_selected(self, instance, value):
+ def on_pv_profile_selected(self, instance, value):
try:
logging.info('CostSavings: PV profile selection changed to {0}.'.format(value['name']))
except KeyError:
@@ -403,6 +444,75 @@ def on_load_profile_selected(self, instance, value):
self.has_selection = False
else:
self.has_selection = True
+ self.imported_data_selected = False
+
+ def on_imported_data_selected(self, instance, value):
+ if value:
+ self.open_data_importer_button.text = 'Data imported'
+ self.open_data_importer_button.background_color = rgba_to_fraction(PALETTE[3])
+ Clock.schedule_once(partial(slow_blinking_animation, self.open_data_importer_button), 0)
+
+ self.pv_profile_rv.deselect_all_nodes()
+ else:
+ self.open_data_importer_button.text = 'Open data importer'
+ self.open_data_importer_button.background_color = rgba_to_fraction(PALETTE[0])
+ self.open_data_importer_button.opacity = 1
+ Animation.cancel_all(self.open_data_importer_button, 'opacity')
+
+ def open_data_importer(self):
+ write_directory = os.path.join(DATA_HOME, 'pv')
+
+ def _write_pv_profile_json(fname, dataframe):
+ """Writes a generic time series dataframe to a PV profile json.
+
+ Parameters
+ ----------
+ fname : str
+ Name of the file to be saved without an extension
+ dataframe : Pandas DataFrame
+ DataFrame with one Series which is the PV power profile in watts
+
+ Returns
+ -------
+ str
+ The save destination of the resulting file.
+ """
+ pv_profile_template_file = os.path.join('es_gui', 'resources', 'import_templates', 'pv_profile.json')
+
+ with open(pv_profile_template_file, 'r') as f:
+ pv_profile_template = json.load(f)
+
+ ac_output_w = dataframe.iloc[:, 0].tolist()
+ pv_profile_template['outputs']['ac'] = ac_output_w
+
+ save_destination = os.path.join(write_directory, fname + '.json')
+
+ with open(save_destination, 'w') as f:
+ json.dump(pv_profile_template, f)
+
+ return save_destination
+
+ self.data_importer = DataImporter(
+ write_directory=write_directory,
+ write_function=_write_pv_profile_json,
+ format_description="The data units should be in watts and there should be 8,760 samples (hourly for one standard year). The time series is assumed to run January through December at an hourly resolution."
+ )
+ self.data_importer.title.text = "Import a PV power time series"
+
+ def _check_data_importer_on_dismissal():
+ try:
+ import_filename = self.data_importer.get_import_selections()
+ except (ValueError, AttributeError):
+ logging.warning('DataImporter: Nothing was imported.')
+ except KeyError:
+ logging.warning('DataImporter: Import process was terminated early.')
+ else:
+ logging.info('DataImporter: Data import complete.')
+ self.pv_profile_selected = {'name': 'Custom', 'path': import_filename, 'descriptors': get_pv_profile_string(import_filename)}
+ self.imported_data_selected = True
+
+ self.data_importer.bind(on_dismiss=lambda t: _check_data_importer_on_dismissal())
+ self.data_importer.open()
def _validate_inputs(self):
return self.pv_profile_selected
@@ -448,7 +558,6 @@ def apply_selection(self, rv, index, is_selected):
def deselect_node(self):
super(PVProfileRecycleViewRow, self).deselect_node(self)
- print('hi all')
class CostSavingsWizardSystemParameters(Screen):
diff --git a/es_gui/apps/btm/home.py b/es_gui/apps/btm/home.py
index fa3ee221..fc16130d 100644
--- a/es_gui/apps/btm/home.py
+++ b/es_gui/apps/btm/home.py
@@ -7,8 +7,9 @@
from kivy.app import App
# from es_gui.tools.valuation.valuation_dms import ValuationDMS
-from es_gui.resources.widgets.common import WarningPopup
+from es_gui.resources.widgets.common import WarningPopup, NavigationButton
from es_gui.tools.btm.btm_dms import BtmDMS
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
from .op_handler import BtmOptimizerHandler
@@ -34,6 +35,13 @@ def on_enter(self):
ab.reset_nav_bar()
ab.set_title('Behind-the-Meter Applications')
+ help_button = NavigationButton(
+ text='help',
+ on_release=self.open_help_carousel,
+ )
+
+ ab.action_view.add_widget(help_button)
+
# data_manager = App.get_running_app().data_manager
# # Check if any data is available.
@@ -44,3 +52,30 @@ def on_enter(self):
# no_data_popup.bind(on_dismiss=partial(ab.go_to_screen, 'index'))
# no_data_popup.open()
+
+ def open_help_carousel(self, *args):
+ """
+ """
+ help_carousel_view = HelpCarouselModalView()
+ help_carousel_view.title.text = "QuESt BTM"
+
+ slide_01_text = "QuESt BTM is an application with tools for analyzing behind-the-meter energy storage use cases."
+
+ slide_02_text = "The Time-of-Use Cost Savings wizard estimates the cost savings with behind-the-meter energy storage.\n\nYou will need the following data to use this tool:\n* Utility rate structure\n* Load profile (or import your own)\n\nYou may also add a co-located photovoltaic power profile or import your own."
+
+ slide_03_text = "Upon completion of the wizard, you will be taken to the summary report screen. There a number of reports you can browse through that summarize different aspects of the simulation results. A brief synopsis of each component of the results including some key numbers.\n\nThe 'Generate report' button can be used to produce a document that summarizes the wizard run."
+
+ slide_04_text = "This document includes your input selections, a primer on the mathematical model used, and all of the charts from the wizard summary reports.\n\nThe resulting HTML document and images are saved to the /results/*/report directory. You can view the report in a web browser."
+
+ slide_05_text = "You can view simulation results in more detail using the Results Viewer tool. You can plot time series data and export simulation results for external processing."
+
+ slides = [
+ (os.path.join("es_gui", "resources", "help_views", "btm", "01.png"), slide_01_text),
+ (os.path.join("es_gui", "resources", "help_views", "btm", "02.png"), slide_02_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "wizard_report", "01.png"), slide_03_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "wizard_report", "02.png"), slide_04_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "00.png"), slide_05_text),
+ ]
+
+ help_carousel_view.add_slides(slides)
+ help_carousel_view.open()
diff --git a/es_gui/apps/btm/reporting.py b/es_gui/apps/btm/reporting.py
index 8f44041a..22bdbcca 100644
--- a/es_gui/apps/btm/reporting.py
+++ b/es_gui/apps/btm/reporting.py
@@ -15,7 +15,7 @@
from kivy.uix.modalview import ModalView
from kivy.clock import Clock
-from es_gui.tools.charts import BarChart, StackedBarChart, MultisetBarChart, PieChart, DonutChart, format_dollar_string
+from es_gui.proving_grounds.charts import BarChart, StackedBarChart, MultisetBarChart, PieChart, DonutChart, format_dollar_string
from es_gui.resources.widgets.common import TWO_ABC_WIDTH, THREE_ABC_WIDTH, MyPopup, TileButton, PALETTE, rgba_to_fraction, ReportScreen, WizardReportInterface, ReportChartToggle
@@ -546,13 +546,13 @@ def save_figure(self, screen, *args):
chart_save_location = os.path.join(chart_images_dir, 'chart_{n}.png'.format(n=screen.name))
- Clock.schedule_once(partial(screen.chart.export_to_png, chart_save_location), 0.7)
+ Clock.schedule_once(partial(screen.chart.export_to_png, chart_save_location), 1.0)
# Save image name/path for report generator.
self.graphics_locations[screen.name] = os.path.join('images', 'chart_{n}.png'.format(n=screen.name))
def generate_report_screens(self):
- screen_flip_interval = 0.8
+ screen_flip_interval = 1.2
n_charts = len(self.host_report.chart_types.items())
# Draw figures for saving to .png.
diff --git a/es_gui/apps/btm/results_viewer.py b/es_gui/apps/btm/results_viewer.py
index 9a54b616..c60d333c 100644
--- a/es_gui/apps/btm/results_viewer.py
+++ b/es_gui/apps/btm/results_viewer.py
@@ -27,10 +27,6 @@ def __init__(self, **kwargs):
self.time_selector.end_time.bind(on_text_validate=self.draw_figure)
def on_pre_enter(self):
- """Updates the navigation bar's title."""
- ab = self.manager.nav_bar
- ab.set_title('Results Viewer')
-
#Window.bind(on_key_down=self._on_keyboard_down)
self._update_toolbar()
diff --git a/es_gui/apps/data_manager/data_manager.py b/es_gui/apps/data_manager/data_manager.py
index 17c5bfa5..eabbb04f 100644
--- a/es_gui/apps/data_manager/data_manager.py
+++ b/es_gui/apps/data_manager/data_manager.py
@@ -187,6 +187,17 @@ def _scan_btm_load_profile_data_bank(self):
profile_path = load_profile.path
load_profile_data_bank[profile_key] = profile_path
+
+ # Imported.
+ if 'imported' in os.listdir(load_profile_root):
+ imported_root = os.path.join(load_profile_root, 'imported')
+
+ for load_profile in os.scandir(imported_root):
+ if not load_profile.name.startswith('.'):
+ profile_key = '/'.join(['imported', load_profile.name])
+ profile_path = load_profile.path
+
+ load_profile_data_bank[profile_key] = profile_path
self.data_bank['load profiles'] = load_profile_data_bank
diff --git a/es_gui/apps/data_manager/home.py b/es_gui/apps/data_manager/home.py
index d0515944..0afdaa0f 100644
--- a/es_gui/apps/data_manager/home.py
+++ b/es_gui/apps/data_manager/home.py
@@ -1,10 +1,50 @@
from __future__ import absolute_import
+from functools import partial
+import os
+
from kivy.uix.screenmanager import Screen
+from es_gui.resources.widgets.common import NavigationButton
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
+
class DataManagerHomeScreen(Screen):
def on_enter(self):
ab = self.manager.nav_bar
ab.build_data_manager_nav_bar()
ab.set_title('Data Manager')
+
+ help_button = NavigationButton(
+ text='help',
+ on_release=self.open_help_carousel,
+ )
+
+ ab.action_view.add_widget(help_button)
+
+ def open_help_carousel(self, *args):
+ """
+ """
+ help_carousel_view = HelpCarouselModalView()
+ help_carousel_view.title.text = "QuESt Data Manager"
+
+ slide_01_text = "QuESt Data Manager is a collection of tools for acquiring data for use in other QuESt applications. Data acquired here is stored in a data bank accessible throughout the rest of the QuESt suite.\n\nClick on one of the data tools to get started."
+
+ slide_02_text = "Some data sources require registration and credentials. Look out for [font=Modern Pictograms][color=00ADD0]?[/color][/font] symbols for additional information.\n\nThe 'settings' button will open the global settings menu from the navigation bar. Make sure your connection settings are appropriately configured when using QuESt Data Manager as internet access is required to download data."
+
+ slide_03_text = "You can save some of your login information or API keys by entering in the 'QuESt Data Manager' tab in the global settings menu. These values will auto-populate the appropriate fields the next time you launch QuESt. These values are also stored in the quest.ini file in the QuESt installation folder.\n\nNote that QuESt does not store passwords."
+
+ slide_04_text = "Rate structure tables can be modified before saving. You can change the rate for each period. Click on the [font=Modern Pictograms]D[/font] button to copy the value to the next row."
+
+ slide_05_text = "The tables on the right describe the rate schedule for weekdays and weekends. Each row corresponds to a month and each column an hour. The value in each cell matches to a rate in the rates table; you can change each of these as needed. Try using the 'Tab' and arrow keys to navigate each table more quickly.\n\nNote that you cannot change the number of periods."
+
+ slides = [
+ (os.path.join("es_gui", "resources", "help_views", "data_manager", "01.png"), slide_01_text),
+ (os.path.join("es_gui", "resources", "help_views", "data_manager", "02.png"), slide_02_text),
+ (os.path.join("es_gui", "resources", "help_views", "data_manager", "03.png"), slide_03_text),
+ (os.path.join("es_gui", "resources", "help_views", "data_manager", "04.png"), slide_04_text),
+ (os.path.join("es_gui", "resources", "help_views", "data_manager", "05.png"), slide_05_text),
+ ]
+
+ help_carousel_view.add_slides(slides)
+ help_carousel_view.open()
diff --git a/es_gui/apps/data_manager/load.py b/es_gui/apps/data_manager/load.py
index 24bb7789..5fb50f34 100644
--- a/es_gui/apps/data_manager/load.py
+++ b/es_gui/apps/data_manager/load.py
@@ -29,7 +29,6 @@ class DataManagerLoadHomeScreen(Screen):
""""""
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: Hourly Commercial/Residential Load Profiles')
@@ -50,7 +49,6 @@ def __init__(self, **kwargs):
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: Hourly Commercial Load Profiles')
StateRVEntry.host_screen = self
@@ -371,7 +369,6 @@ def __init__(self, **kwargs):
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: Hourly Residential Load Profiles')
LoadTypeRVEntry.host_screen = self
diff --git a/es_gui/apps/data_manager/pv.py b/es_gui/apps/data_manager/pv.py
index 5f5747e9..e8db17ea 100644
--- a/es_gui/apps/data_manager/pv.py
+++ b/es_gui/apps/data_manager/pv.py
@@ -28,7 +28,7 @@
from es_gui.resources.widgets.common import InputError, WarningPopup, ConnectionErrorPopup, MyPopup, RecycleViewRow, FADEIN_DUR, LoadingModalView, PALETTE, rgba_to_fraction, fade_in_animation, DataGovAPIhelp, ParameterRow
from es_gui.apps.data_manager.data_manager import DataManagerException, DATA_HOME
-from es_gui.tools.charts import RateScheduleChart
+from es_gui.proving_grounds.charts import RateScheduleChart
from es_gui.apps.data_manager.utils import check_connection_settings
MAX_WHILE_ATTEMPTS = 7
@@ -51,7 +51,6 @@ def on_pre_enter(self):
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: Photovoltaic Power Profiles')
def open_api_key_help(self):
diff --git a/es_gui/apps/data_manager/rate_structure.py b/es_gui/apps/data_manager/rate_structure.py
index 5716b88a..36d8b103 100644
--- a/es_gui/apps/data_manager/rate_structure.py
+++ b/es_gui/apps/data_manager/rate_structure.py
@@ -29,7 +29,7 @@
from es_gui.resources.widgets.common import BodyTextBase, InputError, WarningPopup, ConnectionErrorPopup, MyPopup, RecycleViewRow, FADEIN_DUR, LoadingModalView, PALETTE, rgba_to_fraction, fade_in_animation, DataGovAPIhelp
from es_gui.apps.data_manager.data_manager import DataManagerException, DATA_HOME, STATE_ABBR_TO_NAME
-from es_gui.tools.charts import RateScheduleChart
+from es_gui.proving_grounds.charts import RateScheduleChart
from es_gui.apps.data_manager.utils import check_connection_settings
@@ -47,7 +47,6 @@ class RateStructureDataScreen(Screen):
""""""
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: Utility Rate Structure Data')
diff --git a/es_gui/apps/data_manager/widgets.py b/es_gui/apps/data_manager/widgets.py
index b5728db8..13f0db45 100644
--- a/es_gui/apps/data_manager/widgets.py
+++ b/es_gui/apps/data_manager/widgets.py
@@ -42,7 +42,7 @@
from es_gui.resources.widgets.common import InputError, WarningPopup, ConnectionErrorPopup, MyPopup, APP_NAME, APP_TAGLINE, RecycleViewRow, FADEIN_DUR, LoadingModalView, PALETTE, rgba_to_fraction, fade_in_animation
from es_gui.apps.data_manager.data_manager import DataManagerException
-from es_gui.tools.charts import RateScheduleChart
+from es_gui.proving_grounds.charts import RateScheduleChart
from es_gui.apps.data_manager.rate_structure import RateStructureDataScreen
from es_gui.apps.data_manager.utils import check_connection_settings
@@ -61,7 +61,6 @@
class DataManagerRTOMOdataScreen(Screen):
def on_enter(self):
ab = self.manager.nav_bar
- ab.build_data_manager_nav_bar()
ab.set_title('Data Manager: ISO/RTO Market and Operations Data')
diff --git a/es_gui/apps/valuation/home.py b/es_gui/apps/valuation/home.py
index 203e1feb..629050f5 100644
--- a/es_gui/apps/valuation/home.py
+++ b/es_gui/apps/valuation/home.py
@@ -7,7 +7,8 @@
from kivy.app import App
from es_gui.tools.valuation.valuation_dms import ValuationDMS
-from es_gui.resources.widgets.common import WarningPopup
+from es_gui.resources.widgets.common import WarningPopup, NavigationButton
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
from .op_handler import ValuationOptimizerHandler
@@ -31,6 +32,13 @@ def on_enter(self):
ab.reset_nav_bar()
ab.set_title('Valuation')
+ help_button = NavigationButton(
+ text='help',
+ on_release=self.open_help_carousel,
+ )
+
+ ab.action_view.add_widget(help_button)
+
# data_manager = App.get_running_app().data_manager
# # Check if any data is available.
@@ -41,3 +49,42 @@ def on_enter(self):
# no_data_popup.bind(on_dismiss=partial(ab.go_to_screen, 'index'))
# no_data_popup.open()
+
+ def open_help_carousel(self, *args):
+ """
+ """
+ help_carousel_view = HelpCarouselModalView()
+ help_carousel_view.title.text = "QuESt Valuation"
+
+ slide_01_text = "QuESt Valuation is an application for estimating the revenue potential for an energy storage system providing ISO/RTO services. It takes a retrospective analysis approach using historical data.\n\nData is required to use the tools in QuESt Valuation. Options in the user interface such as market area selection are entirely based on the contents of your QuESt data bank."
+
+ slide_02_text = "The Wizard mode under the Simulation tab walks you through a series of prompts to set up the analysis. This mode is streamlined for a simpler experience compared to the Batch Runs mode.\n\nYou will need the following data to use this tool:\n* Market data for each ISO/RTO that you want to look at"
+
+ slide_03_text = "Upon completion of the wizard, you will be taken to the summary report screen. There a number of reports you can browse through that summarize different aspects of the simulation results. A brief synopsis of each component of the results including some key numbers.\n\nThe 'Generate report' button can be used to produce a document that summarizes the wizard run."
+
+ slide_04_text = "This document includes your input selections, a primer on the mathematical model used, and all of the charts from the wizard summary reports.\n\nThe resulting HTML document and images are saved to the /results/*/report directory. You can view the report in a web browser."
+
+ slide_05_text = "The Batch Runs mode is more advanced than the Wizard mode. The workflow for using this mode is identical but offers more flexibility and options.\n\nThere are two panels for your input: 'Data' and 'Parameters'. You can toggle between the two using the buttons at the bottom. Note that changing market area or pricing node selections will reset the other input widgets."
+
+ slide_06_text = "The parameters available are more numerous than those in the Wizard mode and may also vary among the market areas. The default values indicated by the hint text for each parameter will be used if you do not enter in a new value.\n\nTry using the 'Tab' key to quickly navigate among the text input fields."
+
+ slide_07_text = "The parameter sweep can be used for parameter sensitivity analysis. The sweep will be applied to each month of data selected.\n\nIn this example, a sweep over the power rating from 5 to 20 MW in ten evenly-spaced points will be performed for each month selected in the 'Data' tab. If four months were selected then a total of 40 models will be solved."
+
+ slide_08_text = "When you are done with your selections, click the 'Go!' button to proceed. The models will be built and solved in the background. Upon completion, a popup will open to inform you of the results and provide any pertinent warnings.\n\nYou can proceed to the QuESt Valuation results viewer via the popup, navigation bar, or QuESt Valuation home screen to look at the results."
+
+ slide_09_text = "You can view simulation results in more detail using the Results Viewer tool. You can plot time series data and export simulation results for external processing."
+
+ slides = [
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "01.png"), slide_01_text),
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "02.png"), slide_02_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "wizard_report", "01.png"), slide_03_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "wizard_report", "02.png"), slide_04_text),
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "03.png"), slide_05_text),
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "04.png"), slide_06_text),
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "05.png"), slide_07_text),
+ (os.path.join("es_gui", "resources", "help_views", "valuation", "06.png"), slide_08_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "00.png"), slide_09_text),
+ ]
+
+ help_carousel_view.add_slides(slides)
+ help_carousel_view.open()
diff --git a/es_gui/apps/valuation/reporting.py b/es_gui/apps/valuation/reporting.py
index 25c89e07..a62742b7 100644
--- a/es_gui/apps/valuation/reporting.py
+++ b/es_gui/apps/valuation/reporting.py
@@ -15,7 +15,7 @@
from kivy.uix.modalview import ModalView
from kivy.clock import Clock
-from es_gui.tools.charts import BarChart, StackedBarChart, MultisetBarChart, PieChart, DonutChart
+from es_gui.proving_grounds.charts import BarChart, StackedBarChart, MultisetBarChart, PieChart, DonutChart
from es_gui.resources.widgets.common import TWO_ABC_WIDTH, THREE_ABC_WIDTH, MyPopup, ReportScreen, PALETTE, WizardReportInterface, ReportChartToggle
@@ -428,13 +428,13 @@ def save_figure(self, screen, *args):
chart_save_location = os.path.join(chart_images_dir, 'chart_{n}.png'.format(n=screen.name))
- Clock.schedule_once(partial(screen.chart.export_to_png, chart_save_location), 0.7)
+ Clock.schedule_once(partial(screen.chart.export_to_png, chart_save_location), 1.0)
# Save image name/path for report generator.
self.graphics_locations[screen.name] = os.path.join('images', 'chart_{n}.png'.format(n=screen.name))
def generate_report_screens(self):
- screen_flip_interval = 0.8
+ screen_flip_interval = 1.2
n_charts = len(self.host_report.chart_types.items())
# Draw figures for saving to .png.
diff --git a/es_gui/apps/valuation/results_viewer.py b/es_gui/apps/valuation/results_viewer.py
index 1e5e9210..e9c3434a 100644
--- a/es_gui/apps/valuation/results_viewer.py
+++ b/es_gui/apps/valuation/results_viewer.py
@@ -29,8 +29,7 @@ def __init__(self, **kwargs):
def on_pre_enter(self):
"""Updates the navigation bar's title."""
ab = self.manager.nav_bar
- ab.build_valuation_results_nav_bar()
- ab.set_title('Results Viewer')
+ # ab.build_valuation_results_nav_bar()
#Window.bind(on_key_down=self._on_keyboard_down)
diff --git a/es_gui/tools/ChartTest.kv b/es_gui/proving_grounds/ChartTest.kv
similarity index 100%
rename from es_gui/tools/ChartTest.kv
rename to es_gui/proving_grounds/ChartTest.kv
diff --git a/es_gui/tools/ChartTestApp.py b/es_gui/proving_grounds/ChartTestApp.py
similarity index 99%
rename from es_gui/tools/ChartTestApp.py
rename to es_gui/proving_grounds/ChartTestApp.py
index c9144bfb..187e9149 100644
--- a/es_gui/tools/ChartTestApp.py
+++ b/es_gui/proving_grounds/ChartTestApp.py
@@ -18,7 +18,7 @@
from kivy.uix.boxlayout import BoxLayout
from kivy.core.text import LabelBase
-import charts as charts
+from es_gui.proving_grounds import charts
import calendar
diff --git a/es_gui/proving_grounds/__init__.py b/es_gui/proving_grounds/__init__.py
new file mode 100644
index 00000000..ba0fc426
--- /dev/null
+++ b/es_gui/proving_grounds/__init__.py
@@ -0,0 +1,2 @@
+"""This package supports and hosts the development of new, complex Kivy widgets for the QuESt GUI.
+"""
\ No newline at end of file
diff --git a/es_gui/tools/charts.py b/es_gui/proving_grounds/charts.py
similarity index 100%
rename from es_gui/tools/charts.py
rename to es_gui/proving_grounds/charts.py
diff --git a/es_gui/proving_grounds/data_importer.kv b/es_gui/proving_grounds/data_importer.kv
new file mode 100644
index 00000000..a11ab6f7
--- /dev/null
+++ b/es_gui/proving_grounds/data_importer.kv
@@ -0,0 +1,141 @@
+:
+ size_hint: (0.9, 0.9)
+ auto_dismiss: False
+ screen_manager: screen_manager
+ title: title
+ padding: 10
+
+ BoxLayout:
+ orientation: 'vertical'
+ padding: 5
+ spacing: 10
+
+ TitleTextBase:
+ size_hint_y: 0.1
+ text: 'Import a time series'
+ color: C(hex_secondary)
+ id: title
+
+ BodyTextBase:
+ size_hint_y: 0.05
+ color: C(hex_white)
+ text: 'Time series data will also be available in the data bank after being imported.'
+
+ ScreenManager:
+ size_hint_y: 0.75
+ id: screen_manager
+
+ DataImporterFileChooserScreen:
+ name: 'FileChooser'
+
+ DataImporterFormatAnalyzerScreen:
+ name: 'FormatAnalyzer'
+
+ AnchorLayout:
+ anchor_x: 'center'
+ anchor_y: 'center'
+ size_hint_y: 0.1
+ padding: 10
+
+ BoxLayout:
+ size_hint_x: 0.2
+
+ Button:
+ text: 'Dismiss'
+ on_release: root.dismiss()
+
+
+:
+ filechooser: filechooser
+ file_chooser_body_text: file_chooser_body_text
+
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+
+ DataImporterFileChooser:
+ id: filechooser
+ size_hint_y: 0.9
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint_y: 0.1
+
+ BodyTextBase:
+ size_hint_x: 0.9
+ id: file_chooser_body_text
+ color: C(hex_white)
+
+ Button:
+ size_hint_x: 0.1
+ text: 'Select'
+ on_release: root.file_selected()
+
+
+:
+ data_col_bx: data_col_bx
+ data_col_rv: data_col_rv
+ import_button: import_button
+ format_analyzer_body_text: format_analyzer_body_text
+
+ BoxLayout:
+ orientation: 'vertical'
+ spacing: 10
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint_y: 0.9
+ spacing: 50
+ padding: (50, 0)
+
+ BoxLayout:
+ id: datetime_col_bx
+ orientation: 'vertical'
+ size_hint_x: 0.5
+ spacing: 5
+
+ BodyTextBase:
+ id: format_analyzer_body_text
+ color: C(hex_white)
+
+ BoxLayout:
+ id: data_col_bx
+ orientation: 'vertical'
+ size_hint_x: 0.5
+ spacing: 5
+ # opacity: 0.05
+
+ TitleTextBase:
+ text: 'Select the data column.'
+ color: C(hex_pms312)
+ size_hint_y: 0.1
+ font_size: large_font
+
+ TextInput:
+ size_hint_y: 0.1
+ on_text: data_col_rv.filter_rv_data(self.text)
+ hint_text: 'Filter by name'
+ multiline: False
+
+ MyRecycleView:
+ id: data_col_rv
+ viewclass: 'DataColumnRecycleViewRow'
+
+ SelectableRecycleBoxLayout:
+ multiselect: False
+ touch_multiselect: False
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint_y: 0.1
+
+ BoxLayout:
+ size_hint_x: 0.9
+
+ Button:
+ id: import_button
+ size_hint_x: 0.1
+ text: 'Import'
+ on_release: root.finalize_selections()
+ disabled: True
+
diff --git a/es_gui/proving_grounds/data_importer.py b/es_gui/proving_grounds/data_importer.py
new file mode 100644
index 00000000..3428a94d
--- /dev/null
+++ b/es_gui/proving_grounds/data_importer.py
@@ -0,0 +1,354 @@
+import logging
+import os
+from datetime import datetime
+
+import pandas as pd
+
+from kivy.uix.modalview import ModalView
+from kivy.uix.filechooser import FileChooserListView
+from kivy.uix.screenmanager import Screen, ScreenManager
+from kivy.properties import StringProperty, BooleanProperty
+
+from es_gui.resources.widgets.common import RecycleViewRow, WarningPopup
+
+
+class DataImporterFileChooser(FileChooserListView):
+ """FileChooserListView for selecting file to import.
+ """
+ def __init__(self, *args, **kwargs):
+ super(DataImporterFileChooser, self).__init__(**kwargs)
+
+ self.filters = ['*.csv',]
+ self.multiselect = False
+
+ def on_submit(self, selection, touch):
+ self.host_view.file_selected()
+
+
+class DataImporterFileChooserScreen(Screen):
+ """DataImporter screen for selecting which file to import.
+ """
+ def __init__(self, *args, **kwargs):
+ super(DataImporterFileChooserScreen, self).__init__(**kwargs)
+
+ DataImporterFileChooser.host_view = self
+
+ def file_selected(self):
+ self._validate_file_selected()
+
+ def _validate_file_selected(self):
+ try:
+ file_selected = self.filechooser.selection[0]
+ except IndexError:
+ pass
+ else:
+ file_selected_ext = file_selected.split('.')[-1]
+
+ if file_selected_ext == 'csv':
+ logging.info('DataImporter: {0} is a valid csv file.'.format(file_selected))
+ self.manager.file_selected = file_selected
+
+ self.manager.current = self.manager.next()
+ else:
+ logging.error('DataImporter: {0} is not a valid csv file.'.format(file_selected))
+
+
+class DataImporterFormatAnalyzerScreen(Screen):
+ """DataImporter screen for selecting which column to use and completing the import process.
+ """
+ datetime_column = StringProperty("")
+ data_column = StringProperty("")
+ has_selections = BooleanProperty(False)
+
+ def __init__(self, **kwargs):
+ super(DataImporterFormatAnalyzerScreen, self).__init__(**kwargs)
+
+ DataColumnRecycleViewRow.data_analyzer_screen = self
+
+ def on_pre_enter(self):
+ """Populates the recycle view with column names based on selected file.
+ """
+ file_selected = self.manager.file_selected
+ self.file_selected_df = pd.read_csv(file_selected)
+
+ column_options = [{'name': column}
+ for column in self.file_selected_df.columns
+ ]
+
+ self.data_col_rv.data = column_options
+ self.data_col_rv.unfiltered_data = column_options
+
+ def on_data_column(self, instance, value):
+ """Checks if data column has been specified.
+ """
+ logging.info('DataImporter: Data column changed to {0}.'.format(self.data_column))
+
+ self.has_selections = (self.data_column != "")
+
+ def on_has_selections(self, instance, value):
+ """Activates the import button if both columns have been specified.
+ """
+ if value:
+ logging.info("DataImporter: All selections have been made.")
+
+ self.import_button.disabled = not value
+
+ def finalize_selections(self):
+ """Validates the specified data based on the selected columns using a validation function.
+
+ Returns
+ -------
+ Pandas DataFrame
+ Dataframe with the data series
+
+ str
+ Name of the data column
+ """
+ try:
+ import_df, data_column_name = self._validate_columns_selected()
+ except ValueError as e:
+ exception_popup = WarningPopup()
+ exception_popup.popup_text.text = e.args[0]
+ exception_popup.open()
+ else:
+ logging.info("DataImporter: Data format validation completed without issues.")
+
+ completion_popup = self.manager.completion_popup
+ completion_popup.open()
+
+ return import_df, data_column_name
+
+ def _validate_columns_selected(self):
+ """Validates the data using a validation function.
+
+ Raises
+ ------
+ ValueError
+ If validation fails based on ``self.validation_function``
+ """
+ self.data_validation_function(self.file_selected_df, self.data_column)
+
+ return self.file_selected_df, self.data_column
+
+ def get_selections(self):
+ """Returns the data import results.
+
+ Returns
+ -------
+ Pandas DataFrame
+ DataFrame containing a single Series for the data
+
+ str
+ Name of the data column
+ """
+ import_df, data_column_name = self._validate_columns_selected()
+
+ return import_df, data_column_name
+
+
+class DataColumnRecycleViewRow(RecycleViewRow):
+ """The representation widget for column names in the data column selector RecycleView."""
+ data_analyzer_screen = None
+
+ def on_touch_down(self, touch):
+ """Add selection on touch down."""
+ if super(DataColumnRecycleViewRow, self).on_touch_down(touch):
+ return True
+ if self.collide_point(*touch.pos) and self.selectable:
+ return self.parent.select_with_touch(self.index, touch)
+
+ def apply_selection(self, rv, index, is_selected):
+ """Respond to the selection of items in the view."""
+ self.selected = is_selected
+
+ if is_selected:
+ self.data_analyzer_screen.data_column = rv.data[self.index]['name']
+
+
+class DataImporter(ModalView):
+ """A ModalView with a series of prompts for importing time series data from a csv file.
+
+ Parameters
+ ----------
+ write_directory : str
+ Path of directory where the imported data will be written
+
+ write_function : func
+ Function describing how to write the imported data to a persistent file
+
+ chooser_description : str
+ Description displayed on the DataImporter file chooser screen
+
+ format_description : str
+ Description displayed on the DataImporter format analyzer screen
+
+ data_validation_function : str
+ Function used to validate the selected data
+
+ Notes
+ -----
+ ``write_function`` should handle saving the persistent object (e.g., csv file) to disk.
+ ``data_validation_function`` should raise a ValueError to indicate failing validation with a relevant reason why it failed.
+ """
+ def __init__(self, write_directory=None, write_function=None, chooser_description=None, format_description=None, data_validation_function=None, **kwargs):
+ super(DataImporter, self).__init__(**kwargs)
+
+ if write_directory is None:
+ self.write_directory = ""
+ else:
+ self.write_directory = write_directory
+
+ if write_function is None:
+ def _write_time_series_csv(fname, dataframe):
+ """Writes a generic time series dataframe to a two-column csv.
+
+ The data is inferred to be at an hourly time resolution for one standard year.
+
+ Parameters
+ ----------
+ fname : str
+ Name of the file to be saved without an extension
+ dataframe : Pandas DataFrame
+ DataFrame containing a single Series of the data
+
+ Returns
+ -------
+ str
+ The save destination of the resulting file.
+ """
+ save_destination = os.path.join(self.write_directory, fname + ".csv")
+
+ data_column_name = dataframe.columns[0]
+
+ datetime_start = datetime(2019, 1, 1, 0)
+ hour_range = pd.date_range(start=datetime_start, periods=len(dataframe), freq="H")
+ dataframe["DateTime"] = hour_range
+
+ dataframe[["DateTime", data_column_name]].to_csv(save_destination, index=False)
+
+ return save_destination
+
+ self.write_function = _write_time_series_csv
+ else:
+ self.write_function = write_function
+
+ if chooser_description is None:
+ self.chooser_description = "Select a .csv file to import data from."
+ else:
+ self.chooser_description = chooser_description
+
+ file_chooser_screen = self.screen_manager.get_screen("FileChooser")
+ file_chooser_screen.file_chooser_body_text.text = self.chooser_description
+
+ if format_description is None:
+ self.format_description = "Specify the data column."
+ else:
+ self.format_description = format_description
+
+ format_analyzer_screen = self.screen_manager.get_screen("FormatAnalyzer")
+ format_analyzer_screen.format_analyzer_body_text.text = self.format_description
+
+ if data_validation_function is None:
+ def _default_data_validation_function(dataframe, data_column_name):
+ if len(dataframe) != 8760:
+ raise ValueError("The length of the time series must be 8760 (got {0}).".format(len(dataframe)))
+
+ data_column = dataframe[data_column_name]
+
+ try:
+ data_column.astype("float")
+ except ValueError:
+ raise ValueError("The selected data column could not be interpeted as numeric float values.")
+
+
+ self.data_validation_function = _default_data_validation_function
+ else:
+ self.data_validation_function = data_validation_function
+
+ # Bind DataImporter dismissal to successful data import.
+ completion_popup = WarningPopup()
+ completion_popup.title = "Success!"
+ completion_popup.popup_text.text = "Data successfully imported."
+ completion_popup.bind(on_dismiss=self.dismiss)
+
+ self.screen_manager.completion_popup = completion_popup
+
+ @property
+ def write_directory(self):
+ """The directory where the imported time series data will be saved."""
+ return self._write_directory
+
+ @write_directory.setter
+ def write_directory(self, value):
+ self._write_directory = value
+
+ @property
+ def write_function(self):
+ """The function used to write the imported data to disk."""
+ return self._write_function
+
+ @write_function.setter
+ def write_function(self, value):
+ self._write_function = value
+
+ @property
+ def chooser_description(self):
+ """Description displayed on the file chooser screen."""
+ return self._chooser_description
+
+ @chooser_description.setter
+ def chooser_description(self, value):
+ self._chooser_description = value
+ file_chooser_screen = self.screen_manager.get_screen("FileChooser")
+ file_chooser_screen.file_chooser_body_text.text = self.chooser_description
+
+ @property
+ def format_description(self):
+ """Description displayed on the format analyzer screen."""
+ return self._format_description
+
+ @format_description.setter
+ def format_description(self, value):
+ self._format_description = value
+ format_analyzer_screen = self.screen_manager.get_screen("FormatAnalyzer")
+ format_analyzer_screen.format_analyzer_body_text.text = self.format_description
+
+ @property
+ def data_validation_function(self):
+ """"The function used to validate the format of the selected data."""
+ return self._data_validation_function
+
+ @data_validation_function.setter
+ def data_validation_function(self, value):
+ self._data_validation_function = value
+ format_analyzer_screen = self.screen_manager.get_screen("FormatAnalyzer")
+ format_analyzer_screen.data_validation_function = self.data_validation_function
+
+ def get_import_selections(self):
+ """Returns the destination of the processed imported data.
+
+ This method pulls the selections from the DataImporter prompts.
+ Using the selected data, it writes a formatted version of the data to disk according to specification.
+
+ Returns
+ -------
+ str
+ The save destination of the file with the imported data.
+ """
+ imported_filename = self.screen_manager.file_selected
+ import_df, data_column_name = self.screen_manager.get_screen("FormatAnalyzer").get_selections()
+
+ # Write imported time series data to write_directory.
+ os.makedirs(self.write_directory, exist_ok=True)
+
+ # Strip non-alphanumeric chars from data column name.
+ delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
+
+ generated_save_name = "_".join([os.path.split(imported_filename)[-1][:-4], data_column_name.translate({ord(i): None for i in delchars})])
+ dataframe = import_df[[data_column_name]]
+
+ save_destination = self.write_function(generated_save_name, dataframe)
+ logging.info('DataImporter: Selected time series saved to {0}'.format(save_destination))
+
+ return save_destination
+
\ No newline at end of file
diff --git a/es_gui/proving_grounds/data_importer_test_app.py b/es_gui/proving_grounds/data_importer_test_app.py
new file mode 100644
index 00000000..112ade38
--- /dev/null
+++ b/es_gui/proving_grounds/data_importer_test_app.py
@@ -0,0 +1,49 @@
+# This is for setting the window parameters like the initial size. Goes before any other import statements.
+from kivy.config import Config
+
+Config.set('graphics', 'height', '720')
+Config.set('graphics', 'width', '1280')
+Config.set('graphics', 'minimum_height', '720')
+Config.set('graphics', 'minimum_width', '1280')
+Config.set('graphics', 'resizable', '1')
+Config.set('kivy', 'desktop', 1)
+
+import os
+
+from kivy.app import App
+from kivy.app import Builder
+from kivy.uix.widget import Widget
+from kivy.uix.boxlayout import BoxLayout
+from kivy.core.text import LabelBase
+
+from es_gui.proving_grounds.data_importer import DataImporter
+
+Builder.load_file(os.path.join('es_gui', 'resources', 'widgets', 'common.kv'))
+
+LabelBase.register(name='Exo 2',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Regular.ttf'),
+ fn_bold=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Bold.ttf'),
+ fn_italic=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Italic.ttf'))
+
+LabelBase.register(name='Open Sans',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Regular.ttf'),
+ fn_bold=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Bold.ttf'),
+ fn_italic=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Italic.ttf'))
+
+LabelBase.register(name='Modern Pictograms',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'modernpictograms', 'ModernPictograms.ttf'))
+
+
+class Home(BoxLayout):
+ def open_data_importer(self):
+ DataImporter().open()
+
+
+
+class DataImporterTestApp(App):
+ def build(self):
+ return Home()
+
+
+if __name__ == '__main__':
+ DataImporterTestApp().run()
\ No newline at end of file
diff --git a/es_gui/proving_grounds/dataimportertest.kv b/es_gui/proving_grounds/dataimportertest.kv
new file mode 100644
index 00000000..731edbfb
--- /dev/null
+++ b/es_gui/proving_grounds/dataimportertest.kv
@@ -0,0 +1,18 @@
+#:kivy 1.11.0
+
+#:include es_gui/proving_grounds/data_importer.kv
+
+:
+ orientation: 'vertical'
+ padding: (25, 25)
+
+ BoxLayout:
+ size_hint_y: 0.9
+
+ BoxLayout:
+ orientation: 'vertical'
+ size_hint_y: 0.1
+
+ Button:
+ text: 'Open Data Importer'
+ on_release: root.open_data_importer()
diff --git a/es_gui/proving_grounds/help_carousel.kv b/es_gui/proving_grounds/help_carousel.kv
new file mode 100644
index 00000000..0cadb3d7
--- /dev/null
+++ b/es_gui/proving_grounds/help_carousel.kv
@@ -0,0 +1,86 @@
+:
+ orientation: 'horizontal'
+ spacing: 20
+ img: img
+ img_caption: img_caption
+
+ Image:
+ allow_stretch: True
+ size_hint_x: 0.8
+ id: img
+
+ BodyTextBase:
+ id: img_caption
+ size_hint_x: 0.2
+ text_size: self.size
+ height: self.texture_size[1]
+ # halign: 'justify'
+ valign: 'top'
+ markup: True
+
+:
+ size_hint: (0.85, 0.85)
+ auto_dismiss: True
+ carousel: carousel
+ title: title
+ slide_progress_bx: slide_progress_bx
+
+ BoxLayout:
+ orientation: 'vertical'
+ padding: 20
+ spacing: 10
+
+ canvas.before:
+ Color:
+ rgb: C(hex_white)
+
+ Rectangle:
+ pos: self.pos
+ size: self.size
+
+ TitleTextBase:
+ size_hint_y: 0.1
+ id: title
+
+ Carousel:
+ id: carousel
+ direction: 'right'
+ anim_type: 'out_circ'
+
+ AnchorLayout:
+ anchor_x: 'center'
+ anchor_y: 'center'
+ size_hint_y: 0.05
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint_x: 0.15
+ spacing: 5
+ id: slide_progress_bx
+
+ AnchorLayout:
+ anchor_y: 'center'
+ anchor_x: 'center'
+ size_hint_y: 0.05
+
+ BoxLayout:
+ orientation: 'horizontal'
+ size_hint_x: 0.30
+ spacing: 5
+
+ TileButton:
+ id: select_data_button
+ text: '<<'
+ on_release: root.change_slide('previous')
+
+ TileButton:
+ id: set_parameters_button
+ text: '>>'
+ on_release: root.change_slide('next')
+
+:
+ group: 'slides'
+ disabled: True
+ color: C(hex_secondary)
+ background_radio_disabled_down: self.background_radio_down
+ background_radio_disabled_normal: self.background_radio_normal
diff --git a/es_gui/proving_grounds/help_carousel.py b/es_gui/proving_grounds/help_carousel.py
new file mode 100644
index 00000000..a41ce19c
--- /dev/null
+++ b/es_gui/proving_grounds/help_carousel.py
@@ -0,0 +1,79 @@
+"""This module is for the HelpCarousel widget.
+
+A HelpCarousel is a modal view. It contains a carousel which hosts a series of slides with accompanying text. The primary purpose of this widget is to provide additional help illustrated with screenshots or other relevant figures without overloading the main user interface with information. The modal view includes previous and next buttons to navigate the slides in addition to a group of radio buttons to indicate progress in the carousel's slide deck. The view does not have a dismiss button but auto_dismiss is enabled; the view can be dismissed by clicking outside of it.
+
+The HelpCarouselModalView is designed to be instantiated then populated using the `add_slides()` class method. This method populates the carousel's slides with pairs of image sources and text.
+"""
+
+import logging
+import os
+
+import pandas as pd
+
+from kivy.uix.checkbox import CheckBox
+from kivy.uix.modalview import ModalView
+from kivy.uix.boxlayout import BoxLayout
+from kivy.properties import StringProperty, BooleanProperty, NumericProperty
+
+
+class HelpCarouselSlide(BoxLayout):
+ """A slide for the HelpCarousel consisting of a large image (80%) and text (20%) in horizontal orientation.
+ """
+ pass
+
+
+class HelpCarouselModalView(ModalView):
+ """A ModalView with a series of prompts for importing time series data from a csv file.
+ """
+ current_slide_index = NumericProperty()
+
+ def add_slides(self, slide_deck):
+ """Adds image and text to a new slide in the carousel slide deck.
+
+ Each slide consists of a large image on the left and accompanying text on the right.
+
+ Parameters
+ ----------
+ slide_deck : list(tuple)
+ Content for each slide (source, caption) where the source is the path to the slide image and the caption is the text
+
+ Notes
+ -----
+ The source is relative to the current working directory (alongside main.py).
+ """
+ self.slide_progress_radio_buttons = []
+
+ for source, caption in slide_deck:
+ slide = HelpCarouselSlide()
+ slide.img.source = source
+ slide.img_caption.text = caption
+
+ self.carousel.add_widget(slide)
+ self.slide_progress_radio_buttons.append(SlideProgressRadioButton())
+
+ for ix, button in enumerate(self.slide_progress_radio_buttons):
+ button.active = ix == 0
+ self.slide_progress_bx.add_widget(button)
+
+ def change_slide(self, direction):
+ """Changes carousel slide in the specified direction.
+ """
+ getattr(self.carousel, 'load_{0}'.format(direction))()
+
+ if direction == 'previous':
+ destination_slide = self.carousel.previous_slide
+ else:
+ destination_slide = self.carousel.next_slide
+
+ if destination_slide is not None:
+ self.current_slide_index = self.carousel.slides.index(destination_slide)
+
+ def on_current_slide_index(self, instance, value):
+ # Changes the active button in the slide progress group to reflect the new slide.
+ self.slide_progress_radio_buttons[value].active = True
+
+
+class SlideProgressRadioButton(CheckBox):
+ """Radio button representing progress within the HelpCarousel slide deck.
+ """
+ pass
\ No newline at end of file
diff --git a/es_gui/proving_grounds/help_carousel_test_app.py b/es_gui/proving_grounds/help_carousel_test_app.py
new file mode 100644
index 00000000..d13d9a5b
--- /dev/null
+++ b/es_gui/proving_grounds/help_carousel_test_app.py
@@ -0,0 +1,48 @@
+# This is for setting the window parameters like the initial size. Goes before any other import statements.
+from kivy.config import Config
+
+Config.set('graphics', 'height', '720')
+Config.set('graphics', 'width', '1280')
+Config.set('graphics', 'minimum_height', '720')
+Config.set('graphics', 'minimum_width', '1280')
+Config.set('graphics', 'resizable', '1')
+Config.set('kivy', 'desktop', 1)
+
+import os
+
+from kivy.app import App
+from kivy.app import Builder
+from kivy.uix.widget import Widget
+from kivy.uix.boxlayout import BoxLayout
+from kivy.core.text import LabelBase
+
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
+
+Builder.load_file(os.path.join('es_gui', 'resources', 'widgets', 'common.kv'))
+
+LabelBase.register(name='Exo 2',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Regular.ttf'),
+ fn_bold=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Bold.ttf'),
+ fn_italic=os.path.join('es_gui', 'resources', 'fonts', 'Exo_2', 'Exo2-Italic.ttf'))
+
+LabelBase.register(name='Open Sans',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Regular.ttf'),
+ fn_bold=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Bold.ttf'),
+ fn_italic=os.path.join('es_gui', 'resources', 'fonts', 'Open_Sans', 'OpenSans-Italic.ttf'))
+
+LabelBase.register(name='Modern Pictograms',
+ fn_regular=os.path.join('es_gui', 'resources', 'fonts', 'modernpictograms', 'ModernPictograms.ttf'))
+
+
+class Home(BoxLayout):
+ def open_help(self):
+ HelpCarouselModalView().open()
+
+
+class HelpCarouselTestApp(App):
+ def build(self):
+ return Home()
+
+
+if __name__ == '__main__':
+ HelpCarouselTestApp().run()
\ No newline at end of file
diff --git a/es_gui/proving_grounds/helpcarouseltest.kv b/es_gui/proving_grounds/helpcarouseltest.kv
new file mode 100644
index 00000000..f0e5143b
--- /dev/null
+++ b/es_gui/proving_grounds/helpcarouseltest.kv
@@ -0,0 +1,18 @@
+#:kivy 1.11.0
+
+#:include es_gui/proving_grounds/help_carousel.kv
+
+:
+ orientation: 'vertical'
+ padding: (25, 25)
+
+ BoxLayout:
+ size_hint_y: 0.9
+
+ BoxLayout:
+ orientation: 'vertical'
+ size_hint_y: 0.1
+
+ Button:
+ text: 'Open Help'
+ on_release: root.open_help()
diff --git a/es_gui/resources/help_views/btm/01.png b/es_gui/resources/help_views/btm/01.png
new file mode 100644
index 00000000..f0ea987d
Binary files /dev/null and b/es_gui/resources/help_views/btm/01.png differ
diff --git a/es_gui/resources/help_views/btm/02.png b/es_gui/resources/help_views/btm/02.png
new file mode 100644
index 00000000..47dfe52d
Binary files /dev/null and b/es_gui/resources/help_views/btm/02.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/00.png b/es_gui/resources/help_views/common/results_viewer/00.png
new file mode 100644
index 00000000..32d2fb95
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/00.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/01.png b/es_gui/resources/help_views/common/results_viewer/01.png
new file mode 100644
index 00000000..7296ab31
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/01.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/02.png b/es_gui/resources/help_views/common/results_viewer/02.png
new file mode 100644
index 00000000..57b466b9
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/02.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/03.png b/es_gui/resources/help_views/common/results_viewer/03.png
new file mode 100644
index 00000000..9cccb044
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/03.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/04.png b/es_gui/resources/help_views/common/results_viewer/04.png
new file mode 100644
index 00000000..4159d623
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/04.png differ
diff --git a/es_gui/resources/help_views/common/results_viewer/05.png b/es_gui/resources/help_views/common/results_viewer/05.png
new file mode 100644
index 00000000..7b3aba19
Binary files /dev/null and b/es_gui/resources/help_views/common/results_viewer/05.png differ
diff --git a/es_gui/resources/help_views/common/wizard_report/01.png b/es_gui/resources/help_views/common/wizard_report/01.png
new file mode 100644
index 00000000..aaa8533f
Binary files /dev/null and b/es_gui/resources/help_views/common/wizard_report/01.png differ
diff --git a/es_gui/resources/help_views/common/wizard_report/02.png b/es_gui/resources/help_views/common/wizard_report/02.png
new file mode 100644
index 00000000..b61ed1d1
Binary files /dev/null and b/es_gui/resources/help_views/common/wizard_report/02.png differ
diff --git a/es_gui/resources/help_views/data_manager/01.png b/es_gui/resources/help_views/data_manager/01.png
new file mode 100644
index 00000000..69047164
Binary files /dev/null and b/es_gui/resources/help_views/data_manager/01.png differ
diff --git a/es_gui/resources/help_views/data_manager/02.png b/es_gui/resources/help_views/data_manager/02.png
new file mode 100644
index 00000000..2a9a99a3
Binary files /dev/null and b/es_gui/resources/help_views/data_manager/02.png differ
diff --git a/es_gui/resources/help_views/data_manager/03.png b/es_gui/resources/help_views/data_manager/03.png
new file mode 100644
index 00000000..1ac8c3f1
Binary files /dev/null and b/es_gui/resources/help_views/data_manager/03.png differ
diff --git a/es_gui/resources/help_views/data_manager/04.png b/es_gui/resources/help_views/data_manager/04.png
new file mode 100644
index 00000000..aea1afe1
Binary files /dev/null and b/es_gui/resources/help_views/data_manager/04.png differ
diff --git a/es_gui/resources/help_views/data_manager/05.png b/es_gui/resources/help_views/data_manager/05.png
new file mode 100644
index 00000000..f2f3b470
Binary files /dev/null and b/es_gui/resources/help_views/data_manager/05.png differ
diff --git a/es_gui/resources/help_views/index/01.png b/es_gui/resources/help_views/index/01.png
new file mode 100644
index 00000000..dce1bed9
Binary files /dev/null and b/es_gui/resources/help_views/index/01.png differ
diff --git a/es_gui/resources/help_views/index/02.png b/es_gui/resources/help_views/index/02.png
new file mode 100644
index 00000000..6341cdcc
Binary files /dev/null and b/es_gui/resources/help_views/index/02.png differ
diff --git a/es_gui/resources/help_views/index/03.png b/es_gui/resources/help_views/index/03.png
new file mode 100644
index 00000000..8a4a0408
Binary files /dev/null and b/es_gui/resources/help_views/index/03.png differ
diff --git a/es_gui/resources/help_views/index/04.png b/es_gui/resources/help_views/index/04.png
new file mode 100644
index 00000000..e2dbde1a
Binary files /dev/null and b/es_gui/resources/help_views/index/04.png differ
diff --git a/es_gui/resources/help_views/index/05.png b/es_gui/resources/help_views/index/05.png
new file mode 100644
index 00000000..3150d2e6
Binary files /dev/null and b/es_gui/resources/help_views/index/05.png differ
diff --git a/es_gui/resources/help_views/valuation/01.png b/es_gui/resources/help_views/valuation/01.png
new file mode 100644
index 00000000..d64b939b
Binary files /dev/null and b/es_gui/resources/help_views/valuation/01.png differ
diff --git a/es_gui/resources/help_views/valuation/02.png b/es_gui/resources/help_views/valuation/02.png
new file mode 100644
index 00000000..77b77da2
Binary files /dev/null and b/es_gui/resources/help_views/valuation/02.png differ
diff --git a/es_gui/resources/help_views/valuation/03.png b/es_gui/resources/help_views/valuation/03.png
new file mode 100644
index 00000000..b919c6c7
Binary files /dev/null and b/es_gui/resources/help_views/valuation/03.png differ
diff --git a/es_gui/resources/help_views/valuation/04.png b/es_gui/resources/help_views/valuation/04.png
new file mode 100644
index 00000000..513dad34
Binary files /dev/null and b/es_gui/resources/help_views/valuation/04.png differ
diff --git a/es_gui/resources/help_views/valuation/05.png b/es_gui/resources/help_views/valuation/05.png
new file mode 100644
index 00000000..f6d75982
Binary files /dev/null and b/es_gui/resources/help_views/valuation/05.png differ
diff --git a/es_gui/resources/help_views/valuation/06.png b/es_gui/resources/help_views/valuation/06.png
new file mode 100644
index 00000000..1c6b9c4d
Binary files /dev/null and b/es_gui/resources/help_views/valuation/06.png differ
diff --git a/es_gui/resources/import_templates/pv_profile.json b/es_gui/resources/import_templates/pv_profile.json
new file mode 100644
index 00000000..d85163f9
--- /dev/null
+++ b/es_gui/resources/import_templates/pv_profile.json
@@ -0,0 +1,52 @@
+{
+ "inputs": {
+ "azimuth": "Custom",
+ "tilt": "Custom",
+ "losses": "Custom",
+ "system_capacity": "Custom",
+ "lon": "Custom",
+ "lat": "Custom",
+ "module_type": -1,
+ "array_type": -1,
+ "radius": "0",
+ "timeframe": "hourly"
+ },
+ "errors": [],
+ "warnings": [],
+ "version": "N/A",
+ "ssc_info": {
+ "version": "N/A",
+ "build": "N/A"
+ },
+ "station_info": {
+ "lat": "Custom",
+ "lon": "Custom",
+ "elev": "Custom",
+ "tz": "",
+ "location": "None",
+ "city": "",
+ "state": "",
+ "solar_resource_file": "",
+ "distance": ""
+ },
+ "outputs": {
+ "ac_monthly": [
+ ],
+ "poa_monthly": [
+ ],
+ "solrad_monthly": [
+ ],
+ "dc_monthly": [],
+ "ac_annual": 0,
+ "solrad_annual": 0,
+ "capacity_factor": "Custom",
+ "ac": [],
+ "poa": [],
+ "dn": [],
+ "dc": [],
+ "df": [],
+ "tamb": [],
+ "tcell": [],
+ "wspd": []
+ }
+}
\ No newline at end of file
diff --git a/es_gui/resources/widgets/common.kv b/es_gui/resources/widgets/common.kv
index f361d8d6..c4c0f510 100644
--- a/es_gui/resources/widgets/common.kv
+++ b/es_gui/resources/widgets/common.kv
@@ -158,7 +158,7 @@
text: root.name
text_size: self.size
font_size: default_font
- color: C(hex_secondary) if root.selected else C(hex_black)
+ color: C(hex_white) if root.selected else C(hex_black)
valign: 'middle'
halign: 'left'
padding: (10, 10)
diff --git a/es_gui/resources/widgets/common.py b/es_gui/resources/widgets/common.py
index e517136d..701fa7a0 100644
--- a/es_gui/resources/widgets/common.py
+++ b/es_gui/resources/widgets/common.py
@@ -15,6 +15,7 @@
from kivy.utils import get_color_from_hex
from kivy.core.window import Window
from kivy.animation import Animation
+from kivy.uix.actionbar import ActionButton
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
@@ -32,6 +33,8 @@
from kivy.uix.screenmanager import Screen
from kivy.uix.togglebutton import ToggleButton
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
+
cwd = os.getcwd()
# Animation durations in seconds #
@@ -66,6 +69,16 @@ def fade_in_animation(content, *args):
anim = Animation(transition='out_expo', duration=FADEIN_DUR, opacity=1)
anim.start(content)
+def slow_blinking_animation(content, *args):
+ """Slow blinking animation (on opacity); to be used with Clock scheduler."""
+ anim = Animation(transition='linear', duration=LOADING_DUR, opacity=0) + Animation(transition='linear', duration=LOADING_DUR, opacity=1)
+ anim.repeat = True
+ anim.start(content)
+
+
+class NavigationButton(ActionButton):
+ pass
+
class LeftAlignedText(Label):
"""Label subclass for left-aligned text labels."""
@@ -357,6 +370,44 @@ def __init__(self, **kwargs):
self.time_selector.start_time.bind(on_text_validate=self.draw_figure)
self.time_selector.end_time.bind(on_text_validate=self.draw_figure)
+
+ def on_enter(self):
+ ab = self.manager.nav_bar
+ ab.set_title('Results Viewer')
+
+ help_button = NavigationButton(
+ text='help (results viewer)',
+ on_release=self.open_help_carousel,
+ )
+
+ ab.action_view.add_widget(help_button)
+
+ def open_help_carousel(self, *args):
+ """
+ """
+ help_carousel_view = HelpCarouselModalView()
+ help_carousel_view.title.text = "Results Viewer"
+
+ slide_01_text = "The Results Viewer is a built-in tool to help you look at QuESt optimization results. The recycle view at the bottom contains each optimization run (model) performed during your current session. Typically each item will correspond to a single month.\n\nYou can select which models you want to view simultaneously. We recommend selecting no more than six at a time."
+
+ slide_02_text = "The 'Select data' spinner is for selecting which quantity to plot. The variety of choices here will differ among QuESt applications. While most selections correspond to line plots of time series, some other plots such as box-and-whisker plots may be available."
+
+ slide_03_text = "Click on the 'Plot/Redraw' to render the plot according to your current selections.\n\nNote that the figure is not interactive."
+
+ slide_04_text = "For time series plots, you can adjust the range of time shown using the 'Hours shown' field. This can be used to look at specific points in time in more detail.\n\nYou can hit the 'Enter' key after changing these values to quickly render the plot."
+
+ slide_05_text = "You can export the currently rendered plot to a PNG image file using the 'Export PNG' button.\n\nYou can also export the detailed table of results for each selected model to a CSV file using the 'Export CSV' button. An individual file will be made for each selected model. These files contain details such as decision variable values at each timestep and other relevant quantities."
+
+ slides = [
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "01.png"), slide_01_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "02.png"), slide_02_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "03.png"), slide_03_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "04.png"), slide_04_text),
+ (os.path.join("es_gui", "resources", "help_views", "common", "results_viewer", "05.png"), slide_05_text),
+ ]
+
+ help_carousel_view.add_slides(slides)
+ help_carousel_view.open()
def on_pre_enter(self):
pass
diff --git a/es_gui/tools/btm/readutdata.py b/es_gui/tools/btm/readutdata.py
index 2b917782..ce38b424 100644
--- a/es_gui/tools/btm/readutdata.py
+++ b/es_gui/tools/btm/readutdata.py
@@ -190,14 +190,19 @@ def read_load_profile(path, month):
if isinstance(month, str):
month = int(month)
- # Parse the Date/Time field.
- load_df['dt split'] = load_df['Date/Time'].str.split()
+ # Assumptions: column 0 is datetime, column 1 is data
+ datetime_column_name = load_df.columns[0]
+ data_column_name = load_df.columns[-1]
- load_df['month'] = load_df['dt split'].apply(lambda x: int(x[0].split('/')[0]))
- load_df['day'] = load_df['dt split'].apply(lambda x: int(x[0].split('/')[-1]))
- load_df['hour'] = load_df['dt split'].apply(lambda x: int(x[1].split(':')[0]))
+ # Overwrite DateTime column (esp. for data obtained from OpenEI)
+ datetime_start = datetime(2019, 1, 1, 0)
+ hour_range = pd.date_range(start=datetime_start, periods=len(load_df), freq="H")
+ load_df[datetime_column_name] = hour_range
- load_profile = load_df.loc[load_df['month'] == month]['Electricity:Facility [kW](Hourly)'].values
+ # Filter by given month.
+ datetime_column = pd.to_datetime(load_df[datetime_column_name])
+ load_df_month = load_df.loc[datetime_column.apply(lambda x: x.month == month)]
+ load_profile = load_df_month[data_column_name].values
return load_profile
@@ -215,7 +220,7 @@ def read_pv_profile(path, month):
df_pv_output = pd.DataFrame(pv_output_w, columns=['kW'])*1e-3
# Apply datetime index for filtering.
- datetime_start = datetime(2019, 1, 1, 1)
+ datetime_start = datetime(2019, 1, 1, 0)
hour_range = pd.date_range(start=datetime_start, periods=len(pv_output_w), freq='H')
df_pv_output['dt'] = hour_range
@@ -230,10 +235,15 @@ def get_pv_profile_string(path):
with open(path) as f:
profile_obj = json.load(f)
- module_type_list = ['Standard', 'Premium', 'Thin Film']
- array_type_list = ['Fixed (open rack)', 'Fixed (roof mounted)', '1-axis', '1-axis (backtracking)', '2-axis']
+ module_type_list = ['Standard', 'Premium', 'Thin Film', 'N/A']
+ array_type_list = ['Fixed (open rack)', 'Fixed (roof mounted)', '1-axis', '1-axis (backtracking)', '2-axis', 'N/A']
query_inputs = profile_obj['inputs']
+
+ # Skip if this is a custom/imported profile.
+ if query_inputs["array_type"] == -1:
+ return ["Custom",]
+
coordinates = 'Location: {lat}, {lon}'.format(lat=query_inputs['lat'], lon=query_inputs['lon'])
system_capacity = 'System Capacity: {0} kW'.format(query_inputs['system_capacity'])
azimuth = 'Azimuth: {0} deg'.format(query_inputs['azimuth'])
diff --git a/main.py b/main.py
index 0e603a77..b1018a65 100644
--- a/main.py
+++ b/main.py
@@ -48,12 +48,13 @@
from kivy.uix.modalview import ModalView
from kivy.uix.popup import Popup
from kivy.uix.boxlayout import BoxLayout
-from kivy.uix.actionbar import ActionBar, ActionButton, ActionGroup
+from kivy.uix.actionbar import ActionBar, ActionGroup
from kivy.properties import ObjectProperty
from kivy.core.text import LabelBase
from es_gui.apps.data_manager.data_manager import DataManager
-from es_gui.resources.widgets.common import MyPopup, WarningPopup, APP_NAME, APP_TAGLINE
+from es_gui.resources.widgets.common import MyPopup, WarningPopup, APP_NAME, APP_TAGLINE, NavigationButton
+from es_gui.proving_grounds.help_carousel import HelpCarouselModalView
dirname = os.path.dirname(__file__)
@@ -100,14 +101,33 @@ class IndexScreen(Screen):
def on_leave(self):
"""Sets NavigationBar.reset_nav_bar() to fire on_enter for the index screen after the first time loading it."""
self.bind(on_enter=self.manager.nav_bar.reset_nav_bar)
+
+ def open_intro_help_carousel(self):
+ """
+ """
+ help_carousel_view = HelpCarouselModalView()
+ help_carousel_view.title.text = "Welcome to QuESt"
+
+ slide_01_text = "QuESt is an application suite for energy storage valuation.\n\nThe list on the left contains the currently available applications. Click on an application to learn a little more about it. Once you have selected an application, click on the 'Get started' button underneath its description to open it."
+
+ slide_02_text = "At the top of the QuESt window is the action bar. The QuESt logo on the left end of the action bar serves as a back button; click on it to return to the previous screen. On the right end of the action bar is the navigation toolbar. The buttons here change depending on the context but several, like those pictured, persist.\n\nYou can use the 'home' button to return to this index screen at any time."
+ slide_03_text = "In QuESt, input data management is separate from the analysis tools. Use the QuESt Data Manager to acquire data before proceeding to other QuESt applications and using their analysis tools."
-class HelpScreen(Screen):
- """The documentation/help screen."""
- def on_enter(self):
- ab = self.manager.nav_bar
- ab.reset_nav_bar()
- ab.set_title('Help')
+ slide_04_text = "In some QuESt applications, it is possible to import and use your own data. Look out for prompts such as these to open the data importer interface. Please refer to each individual application and tool for specific details!"
+
+ slide_05_text = "Looking for more help? Check the navigation bar while in each QuESt application for a 'help' button to open an information carousel like this one for application-specific help."
+
+ slides = [
+ (os.path.join("es_gui", "resources", "help_views", "index", "01.png"), slide_01_text),
+ (os.path.join("es_gui", "resources", "help_views", "index", "02.png"), slide_02_text),
+ (os.path.join("es_gui", "resources", "help_views", "index", "03.png"), slide_03_text),
+ (os.path.join("es_gui", "resources", "help_views", "index", "04.png"), slide_04_text),
+ (os.path.join("es_gui", "resources", "help_views", "index", "05.png"), slide_05_text),
+ ]
+
+ help_carousel_view.add_slides(slides)
+ help_carousel_view.open()
class AboutScreen(ModalView):
@@ -130,7 +150,7 @@ def _go_to_webpage(instance, value):
elif value == 'sandia':
webbrowser.open('http://sandia.gov/')
- version_statement = 'QuESt v1.2.e \n 2019.10.14'
+ version_statement = 'QuESt v1.2.f \n 2020.01.17'
developed_by = '{app_name} is developed by the {ess} and {espr} departments at {sandia}.'.format(app_name=APP_NAME, ess=_ref_link('Energy Storage Technology and Systems', 'sandia-ess'), espr=_ref_link('Electric Power Systems Research', 'sandia-espr'), sandia=_ref_link('Sandia National Laboratories', 'sandia'))
@@ -201,7 +221,6 @@ def __init__(self, **kwargs):
# Add new screens here.
self.add_widget(IndexScreen())
- self.help_popup = HelpPopup()
self.about_screen = AboutScreen()
self.settings_screen = SettingsScreen()
@@ -404,32 +423,6 @@ def set_title(self, title):
self.action_view.action_previous.title = title
-class NavigationButton(ActionButton):
- pass
-
-
-class HelpPopup(MyPopup):
- def __init__(self, **kwargs):
- super(HelpPopup, self).__init__(**kwargs)
-
- self._keyboard = Window.request_keyboard(self._keyboard_closed, self, 'text')
-
- if self._keyboard.widget:
- pass
-
- self._keyboard.bind(on_key_down=self._on_keyboard_down)
-
- def _keyboard_closed(self):
- self._keyboard.unbind(on_key_down=self._on_keyboard_down)
- self._keyboard = None
-
- def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
- if keycode[1] in ('enter', 'numpadenter'):
- self.dismiss()
-
- return True
-
-
class QuEStApp(App):
"""
The App class for launching the application.
diff --git a/patch_note_resources/patch-data-importer.png b/patch_note_resources/patch-data-importer.png
new file mode 100644
index 00000000..2e33e3d6
Binary files /dev/null and b/patch_note_resources/patch-data-importer.png differ
diff --git a/patch_note_resources/patch-data-importer2.png b/patch_note_resources/patch-data-importer2.png
new file mode 100644
index 00000000..51ba9aaa
Binary files /dev/null and b/patch_note_resources/patch-data-importer2.png differ
diff --git a/patch_note_resources/patch-data-importer3.png b/patch_note_resources/patch-data-importer3.png
new file mode 100644
index 00000000..bfaf0c71
Binary files /dev/null and b/patch_note_resources/patch-data-importer3.png differ
diff --git a/patch_note_resources/patch-data-importer4.png b/patch_note_resources/patch-data-importer4.png
new file mode 100644
index 00000000..0fb7dbd2
Binary files /dev/null and b/patch_note_resources/patch-data-importer4.png differ
diff --git a/patch_note_resources/patch-help-views.png b/patch_note_resources/patch-help-views.png
new file mode 100644
index 00000000..b016bc8d
Binary files /dev/null and b/patch_note_resources/patch-help-views.png differ
diff --git a/patch_note_resources/patch-help-views2.png b/patch_note_resources/patch-help-views2.png
new file mode 100644
index 00000000..36065200
Binary files /dev/null and b/patch_note_resources/patch-help-views2.png differ
diff --git a/patch_note_resources/patch-quickstart-tour.png b/patch_note_resources/patch-quickstart-tour.png
new file mode 100644
index 00000000..b2b247d7
Binary files /dev/null and b/patch_note_resources/patch-quickstart-tour.png differ
diff --git a/patch_note_resources/patch-quickstart-tour2.png b/patch_note_resources/patch-quickstart-tour2.png
new file mode 100644
index 00000000..1209ab0b
Binary files /dev/null and b/patch_note_resources/patch-quickstart-tour2.png differ