To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

Commit b713fcae authored by holukas's avatar holukas
Browse files

preparing v0.16.0

parent e0706cee
......@@ -33,23 +33,24 @@ Raw data flags from EddyPro
"""
import numpy as np
class Completeness:
"""Number of records"""
class Completeness():
"""
Number of records
"""
def __init__(self, drp_ssitc_flag_col, lne_ssitc_flag0_lim, lne_ssitc_flag1_lim,
def __init__(self, drp_completeness_flag_col, lne_completeness_flag0_lim, lne_completeness_flag1_lim,
col_list_pretty, col_dict_tuples, vars_selected_df, tab_data_df):
# Get settings
ssitc_flag_pretty = drp_ssitc_flag_col.currentText()
ssitc_flag_pretty_idx = col_list_pretty.index(ssitc_flag_pretty)
self.flag_col = col_dict_tuples[ssitc_flag_pretty_idx]
self.flag0_lim = int(lne_ssitc_flag0_lim.text())
self.flag1_lim = int(lne_ssitc_flag1_lim.text())
flag_pretty = drp_completeness_flag_col.currentText()
flag_pretty_idx = col_list_pretty.index(flag_pretty)
self.flag_col = col_dict_tuples[flag_pretty_idx]
self.flag0_lim = float(lne_completeness_flag0_lim.text())
self.flag1_lim = float(lne_completeness_flag1_lim.text())
self.tab_data_df = tab_data_df
self.vars_selected_df = vars_selected_df.copy()
def add_qcf_ssitc(self):
def add_qcf_scf(self):
self._set_colnames()
self._init_new_cols()
self._add_aux_cols()
......@@ -57,10 +58,10 @@ class Completeness:
return self.vars_selected_df
def _set_colnames(self):
self.values_col = (f"_AUX_SSITC_VALS_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.flag0_lim_col = (f"_AUX_SSITC_FLAG0_LIM_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.flag1_lim_col = (f"_AUX_SSITC_FLAG1_LIM_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.qcf_col = (f"QCF_SSITC_{self.flag_col[0]}", "[2=bad]")
self.values_col = (f"_AUX_COMPLETENESS_VALS_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.flag0_lim_col = (f"_AUX_COMPLETENESS_FLAG0_LIM_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.flag1_lim_col = (f"_AUX_COMPLETENESS_FLAG1_LIM_{self.flag_col[0]}", f"{self.flag_col[1]}")
self.qcf_col = (f"_QCF_COMPLETENESS_{self.flag_col[0]}", "[2=bad]")
def _init_new_cols(self):
self.vars_selected_df[self.values_col] = np.nan
......@@ -74,14 +75,15 @@ class Completeness:
self.vars_selected_df[self.values_col] = self.tab_data_df[self.flag_col]
def _calc_qcfcol(self):
flag_filter = (self.vars_selected_df[self.values_col] >= self.vars_selected_df[self.flag1_lim_col])
# All averaging intervals missing more than the flag 1 limit are rejected
flag_filter = (self.vars_selected_df[self.values_col] < self.vars_selected_df[self.flag1_lim_col])
self.vars_selected_df.loc[flag_filter, [self.qcf_col]] = 2
# Here < is needed because there is only 0-1-2 in the _lim_col_data
# and we give the non-closed limit, i.e. value must be < than limit.
flag_filter = (self.vars_selected_df[self.values_col] < self.vars_selected_df[self.flag1_lim_col])
#
flag_filter = (self.vars_selected_df[self.values_col] >= self.vars_selected_df[self.flag1_lim_col]) & \
(self.vars_selected_df[self.values_col] <= self.vars_selected_df[self.flag0_lim_col])
self.vars_selected_df.loc[flag_filter, [self.qcf_col]] = 1
flag_filter = (self.vars_selected_df[self.values_col] < self.vars_selected_df[self.flag0_lim_col])
flag_filter = (self.vars_selected_df[self.values_col] > self.vars_selected_df[self.flag0_lim_col])
self.vars_selected_df.loc[flag_filter, [self.qcf_col]] = 0
......@@ -122,7 +124,7 @@ class RawDataFlag8:
def _set_colnames(self):
self.multiflags_col = (f"_AUX_RAWDATA_{self.statflag_col[0]}_VALS_{self.flagvar}", f"{self.statflag_col[1]}")
self.flag_col = (f"_AUX_RAWDATA_{self.statflag_col[0]}_{self.flagvar}", f"[flag]")
self.qcf_col = (f"QCF_RAWDATA_{self.statflag_col[0]}_{self.flagvar}", "[2=bad]")
self.qcf_col = (f"_QCF_RAWDATA_{self.statflag_col[0]}_{self.flagvar}", "[2=bad]")
def _get_var_col_from_drp_pretty(self, drp):
pretty = drp.currentText()
......@@ -132,8 +134,9 @@ class RawDataFlag8:
def _calc_qcfcol(self):
# Set bad values = 2 in case of hard flags
filter_rejectval = (self.vars_selected_df[self.flag_col] == 1) # Bad values = 1
filter_keepval = (self.vars_selected_df[self.flag_col] == 0) # Good values = 0
filter_rejectval = self.vars_selected_df[self.flag_col] == 1 # Bad values
filter_keepval = self.vars_selected_df[self.flag_col] == 0 # Good values
filter_missingval = (self.vars_selected_df[self.flag_col] == 9) # Missing values
if '_hf' in str(self.statflag_col[0]):
self.vars_selected_df.loc[filter_rejectval, [self.qcf_col]] = 2 # for hard flags, 1=bad becomes 2=bad
......@@ -141,6 +144,7 @@ class RawDataFlag8:
self.vars_selected_df.loc[filter_rejectval, [self.qcf_col]] = 1 # for soft flags, 1=OK, no 2
self.vars_selected_df.loc[filter_keepval, [self.qcf_col]] = 0 # flag within limits, good values
self.vars_selected_df.loc[filter_missingval, [self.qcf_col]] = np.nan # Missing values stay missing
def _add_aux_cols(self):
self.vars_selected_df[self.multiflags_col] = self.tab_data_df[self.statflag_col].copy()
......
from gui import gui_elements
from modboxes.Extensions._Corrections import StorageCorrection
def connect(self, opt_tab_idx):
# (ModBox) Buttons
self.btn_cr_cat_Corrections_showStack.clicked.connect((lambda: self.view_stk_option(tab_idx=opt_tab_idx)))
# Option: Storage Correction
self.btn_cr_opt_StorageCorrection.clicked.connect(
(lambda: self.view_stk_refinement(tab_idx=self.cr_ref_StorageCorrNEE_stackTab)))
self.btn_cr_ref_StorageCorrection_showPlot.clicked.connect(lambda: self.call_cr_StorageCorrection())
return self
class AddControls:
"""
Creates gui for outlier detection
"""
def __init__(self, grd_Categories, stk_Options, stk_Refinements, ctx):
super(AddControls, self).__init__()
self.ctx = ctx
# (1) Categories
self.grd_Categories = grd_Categories
self.category = '_Corrections'
self.add_category()
# (2) Options & Refinements
opt_frame, opt_layout = gui_elements.add_frame_grid()
self.add_options(layout=opt_layout)
# Linear Interpolation
self.cr_ref_StorageCorrNEE_stackTab = 15
self.btn_cr_opt_StorageCorrection, \
self.drp_cr_ref_StorageCorrection_Flux, \
self.drp_cr_ref_StorageCorrection_StorageTerm, \
self.btn_cr_ref_StorageCorrection_showPlot = \
StorageCorrection.AddControls(opt_stack=stk_Options, opt_frame=opt_frame, opt_layout=opt_layout,
ref_stack=stk_Refinements).get_handles()
def get_handles(self):
return self.btn_cr_cat_Corrections_showStack, \
self.btn_cr_opt_StorageCorrection, \
self.cr_ref_StorageCorrNEE_stackTab, \
self.drp_cr_ref_StorageCorrection_Flux, \
self.drp_cr_ref_StorageCorrection_StorageTerm, \
self.btn_cr_ref_StorageCorrection_showPlot
def add_category(self):
# Category button_qual_A in categories grid (for stack selection)
self.btn_cr_cat_Corrections_showStack = gui_elements.add_iconbutton_to_grid(grid_layout=self.grd_Categories,
txt=self.category,
css_id='',
row=1, col=0, rowspan=1, colspan=1,
icon=self.ctx.icon_cat_corrections)
return self
def add_options(self, layout):
_header = gui_elements.grd_Label(lyt=layout,
txt=self.category,
css_id='lbl_Header2',
row=0, col=0, rowspan=1, colspan=1)
layout.setRowStretch(2, 1)
return self
import gui.gui_elements
import gui.plotfuncs
import modboxes.default.Plots._shared
from gui import gui_elements
class AddControls():
"""
Creates the gui control elements and their handles for usage.
"""
def __init__(self, opt_stack, opt_frame, opt_layout, ref_stack):
self.opt_stack = opt_stack
self.opt_frame = opt_frame
self.opt_layout = opt_layout
self.ref_stack = ref_stack
self.add_option()
self.add_refinements()
# self.get_handles()
def get_handles(self):
return self.btn_cr_opt_StorageCorrection, \
self.drp_cr_ref_StorageCorrection_Flux, \
self.drp_cr_ref_StorageCorrection_StorageTerm, \
self.btn_cr_ref_StorageCorrection_showPlot
def add_option(self):
# Option Button: [3] Running Median
self.btn_cr_opt_StorageCorrection = gui_elements.add_button_to_grid(grid_layout=self.opt_layout,
txt='Storage Correction', css_id='',
row=1, col=0, rowspan=1, colspan=1)
self.opt_stack.addWidget(self.opt_frame)
def add_refinements(self):
ref_frame, ref_layout = gui_elements.add_frame_grid()
self.ref_stack.addWidget(ref_frame)
gui.gui_elements.add_header_to_grid_top(layout=ref_layout, txt='CR: Storage Correction')
self.drp_cr_ref_StorageCorrection_Flux = \
gui_elements.grd_LabelDropdownPair(txt='Flux',
css_ids=['', ''],
layout=ref_layout,
row=1, col=0,
orientation='horiz')
self.drp_cr_ref_StorageCorrection_StorageTerm = \
gui_elements.grd_LabelDropdownPair(txt='Storage',
css_ids=['', ''],
layout=ref_layout,
row=2, col=0,
orientation='horiz')
self.btn_cr_ref_StorageCorrection_showPlot = gui_elements.add_button_to_grid(grid_layout=ref_layout,
txt='Show In Plot', css_id='',
row=3, col=0, rowspan=1, colspan=2)
ref_layout.setRowStretch(4, 1)
return self
class Call:
def __init__(self, data_df, focus_df, drp_flux, drp_storage, col_list_pretty, col_dict_tuples,
fig, ax, focus_col):
super(Call, self).__init__()
self.data_df = data_df
self.focus_df = focus_df
self.col_list_pretty = col_list_pretty
self.col_dict_tuples = col_dict_tuples
self.selected_flux = drp_flux.currentText()
self.selected_storage = drp_storage.currentText()
self.fig = fig # needed for redraw
self.ax = ax # adds to existing axis
self.focus_df = focus_df.copy()
self.focus_col = focus_col
self.marker_col = ('cr_StorageCorrection', 'marker')
# There are 3 lines to remove each update, line 0 is the main plot which is not removed.
self.ax = gui.plotfuncs.remove_prev_lines(ax=self.ax)
self.get_data()
self.storage_correction()
def get_data(self):
# index in list of all variables
selected_flux_pretty_string_ix = self.col_list_pretty.index(self.selected_flux)
selected_storage_pretty_string_ix = self.col_list_pretty.index(self.selected_storage)
# full column name (tuple), can be used to directly access data in df
self.selected_flux_tuple = self.col_dict_tuples[selected_flux_pretty_string_ix]
self.selected_storage_tuple = self.col_dict_tuples[selected_storage_pretty_string_ix]
self.focus_df[self.selected_flux_tuple] = self.data_df[self.selected_flux_tuple]
self.focus_df[self.selected_storage_tuple] = self.data_df[self.selected_storage_tuple]
def storage_correction(self):
self.focus_df[self.marker_col] = \
self.focus_df[self.selected_flux_tuple] + self.focus_df[self.selected_storage_tuple]
self.color = '#eab839' # yellow
self.in_plot()
def get_results(self):
return self.focus_df, self.marker_col
def in_plot(self):
# time focus_series are added to an existing plot, no new axis
self.prev_line = modboxes.default.Plots._shared.add_to_existing_ax(self.fig, add_to_ax=self.ax, x=self.focus_df.index,
y=self.focus_df[self.marker_col], linestyle='', linewidth=0)
# modboxes for Quality Control
from gui import gui_elements
from modboxes.Extensions._Pipelines import QC_ETH_default
def connect(self, opt_tab_idx):
self.btn_pn_cat_Pipelines_showStack.clicked.connect((lambda: self.view_stk_option(tab_idx=opt_tab_idx)))
self.btn_pn_opt_PipeQC.clicked.connect((lambda: self.view_stk_refinement(
tab_idx=self.pn_ref_PipeQC_stackTab)))
self.btn_pn_ref_PipeQC_generateOverallFlag.clicked.connect(lambda: self.call_pipe_PipeQC())
# self.btn_pn_ref_PipeQC_detectFlagCols.clicked.connect(lambda: self.call_pipe_PipeQC())
return self
class AddControls:
""" Creates GUI for the category _Pipelines """
def __init__(self, grd_Categories, stk_Options, stk_Refinements, ctx):
super(AddControls, self).__init__()
self.ctx = ctx
# (1) Category
self.grd_Categories = grd_Categories
self.category = '_Pipelines'
self.add_category()
# (2) Options & Refinements
opt_frame, opt_layout = gui_elements.add_frame_grid()
self.add_options(layout=opt_layout)
# Default Flag EddyPro
self.pn_ref_PipeQC_stackTab = 26 # index of the tab in refinements box, called in main
self.btn_pn_opt_PipeQC, \
self.drp_pn_ref_PipeQC_flux, \
self.drp_pn_ref_PipeQC_foundFlag_SSITC, \
self.drp_pn_ref_PipeQC_foundFlag_OutAbsLim, \
self.drp_pn_ref_PipeQC_foundFlag_scf, \
self.btn_pn_ref_PipeQC_generateOverallFlag = \
QC_ETH_default.AddControls(opt_stack=stk_Options, opt_frame=opt_frame, opt_layout=opt_layout,
ref_stack=stk_Refinements).get_handles()
def get_handles(self):
return self.btn_pn_cat_Pipelines_showStack, \
self.pn_ref_PipeQC_stackTab, \
self.btn_pn_opt_PipeQC, \
self.drp_pn_ref_PipeQC_flux, \
self.drp_pn_ref_PipeQC_foundFlag_SSITC, \
self.drp_pn_ref_PipeQC_foundFlag_OutAbsLim, \
self.drp_pn_ref_PipeQC_foundFlag_scf, \
self.btn_pn_ref_PipeQC_generateOverallFlag
def add_category(self):
# Category button_qual_A (for stack selection)
self.btn_pn_cat_Pipelines_showStack = gui_elements.add_iconbutton_to_grid(grid_layout=self.grd_Categories,
txt=self.category,
css_id='btn_cat_QualityControl',
row=7, col=0, rowspan=1, colspan=1,
icon=self.ctx.icon_cat_pipelines)
def add_options(self, layout):
_header = gui_elements.grd_Label(lyt=layout,
txt=self.category,
css_id='lbl_Header2',
row=0, col=0, rowspan=1, colspan=1)
layout.setRowStretch(2, 1)
......@@ -163,56 +163,3 @@ class Call:
'detected flux': detected_flux}, highlight=False) # Log info
logger.log(name='', dict=qc_settings_dict, highlight=False) # Log info
return qc_settings_dict
def run_pipeline(self):
""" Run methods using Amp instance """
# todo next: stability, raw data flags, ustar? (manual),
# Eddypro default flag (SSITC)
data_df, qcflag_test1_col = \
_DefaultFlagEddyPro.Call.pipeline(data_df=self.data_df,
qcflag_col=self.ssitc_col)
# Absolute limits
data_df, qcflag_test2_col = \
AbsoluteLimit.Call.pipeline(data_df=data_df,
upper_limit=self.qc_settings['abs_upper_limit'],
lower_limit=self.qc_settings['abs_lower_limit'],
focus_col=self.flux_col)
# Spectral correction factor
data_df, qcflag_test3_col = \
_SpectralCorrectionFactor.Call.pipeline(data_df=data_df,
upper_limit=self.qc_settings['scf_upper_limit'],
lower_limit=self.qc_settings['scf_lower_limit'],
focus_col=self.flux_col,
qcflag_col=self.scf_col)
# Amp QC flag, overall flag as the sum of all several flags
qcflag_col = (f"QCF_AGG_{self.flux_col[0]}", '[>1=bad]')
data_df[qcflag_col] = data_df[qcflag_test1_col] \
+ data_df[qcflag_test2_col] \
+ data_df[qcflag_test3_col]
added_columns = [qcflag_col, qcflag_test1_col, qcflag_test2_col, qcflag_test3_col]
logger.log(name='+ Added new variables', dict={'qcflag(s)_*': added_columns}, highlight=False) # Log info
return data_df
# def detect_flag_cols(self):
#
# self.drp_abslim.setText(f"{self.flux_col}")
#
# required_cols = {'ssitc': [QCFLAGS_EDDYPRO_SSITC, self.drp_ssitc],
# 'scf': [QCFLAGS_EDDYPRO_SCF, self.drp_scf]}
#
# for data_col in self.data_df.columns:
# # print("")
# for key, val in required_cols.items():
# if any(fnmatch.fnmatch(data_col[0], ids) for ids in val[0]):
# val[1].setText(data_col[0])
#
# # self.drp_ssitc.setText("XXX")
# print("X")
# pass
......@@ -7,19 +7,17 @@ import matplotlib.gridspec as gridspec
# matplotlib.use('Qt5Agg')
import numpy as np
import pandas as pd
# pd.set_option('display.width', 1000)
# pd.set_option('display.max_columns', 15)
from PyQt5 import QtWidgets as qw
import gui.gui_elements
import gui.plotfuncs
import logger
from gui import gui_elements
from gui import plotfuncs
from inout import DataFunctions
# pd.set_option('display.width', 1000)
# pd.set_option('display.max_columns', 15)
from PyQt5 import QtWidgets as qw
from gui import tabs
from gui import gui_elements
from inout import DataFunctions
class addContent(tabs.buildTab):
......
from gui import gui_elements
from modboxes.default.Analyses import GapFinder, BeforeAfterEvent, ClassFinder, RunningLinReg
def connect(self, opt_tab_idx):
"""Connect ModBox buttons to functions."""
self.modbox_analyses.cat_btn_showStack.clicked.connect((lambda: self.view_stk_option(tab_idx=opt_tab_idx)))
# # Option: CLASS FINDER
# self.modbox_analyses.ClassFinder.opt_btn.clicked.connect((
# lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_ClassFinder_stackTab)))
# self.modbox_analyses.ClassFinder.ref_btn_showInPlot.clicked.connect(lambda: self.call_an_ClassFinder())
# Option: RUNNING LINEAR REGRESSION
self.modbox_analyses.RunningLinReg.opt_btn.clicked.connect((
lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_RunningLinRegr_stackTab)))
self.modbox_analyses.RunningLinReg.ref_btn_showInPlot.clicked.connect(
lambda: self.call_an_RunningLinRegr())
# # Option: uSTAR THRESHOLD DETECTION
# self.modbox_analyses.uStarDetection.opt_btn.clicked.connect((
# lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_uStarDetection_stackTab)))
# self.modbox_analyses.uStarDetection.ref_btn_showInNewTab.clicked.connect(
# lambda: self.make_tab(make_tab='USTAR_DETECTION'))
# Option: BEFORE / AFTER EVENT
self.modbox_analyses.BeforeAfterEvent.opt_btn.clicked.connect(
lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_BeforeAfterEvent_stackTab))
self.modbox_analyses.BeforeAfterEvent.ref_btn_showInPlot.clicked.connect(
lambda: self.call_an_BeforeAfterEvent())
# # Option: AGGREGATOR
# self.modbox_analyses.Aggregator.opt_btn.clicked.connect(
# lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_Aggregator_stackTab))
# self.modbox_analyses.Aggregator.ref_btn_showInNewTab.clicked.connect(
# lambda: self.make_tab(make_tab='AGGREGATOR'))
# # Option: NET GHG BALANCE
# self.modbox_analyses.NetGHGBalance.opt_btn.clicked.connect(
# lambda: self.view_stk_refinement(tab_idx=self.modbox_analyses.an_ref_NetGHGBalance_stackTab))
# self.modbox_analyses.NetGHGBalance.ref_btn_showInNewTab.clicked.connect(
# lambda: self.make_tab(make_tab='NET_GHG_BALANCE'))
return self
class AddControls:
"""
Creates gui for analyses
"""
def __init__(self, grd_Categories, stk_Options, stk_Refinements, ctx):
super(AddControls, self).__init__()
self.ctx = ctx
# (1) Categories
self.grd_Categories = grd_Categories
self.category = 'Analyses'
self.add_category()
# (2) Options & Refinements
opt_frame, opt_layout = gui_elements.add_frame_grid()
self.add_options(layout=opt_layout)
# Before / After Event
self.an_ref_BeforeAfterEvent_stackTab = 3 # index of the tab in refinements box, called in main
self.BeforeAfterEvent = BeforeAfterEvent.AddControls(opt_stack=stk_Options, opt_frame=opt_frame,
opt_layout=opt_layout, ref_stack=stk_Refinements)
# Running Linear Regression
self.an_ref_RunningLinRegr_stackTab = 4 # index of the tab in refinements box, called in main
self.RunningLinReg = RunningLinReg.AddControls(opt_stack=stk_Options, opt_frame=opt_frame,
opt_layout=opt_layout, ref_stack=stk_Refinements)
def add_category(self):
# Category button_qual_A in categories grid (for stack selection)
self.cat_btn_showStack = gui_elements.add_iconbutton_to_grid(grid_layout=self.grd_Categories,
txt=self.category, css_id='',
row=0, col=0, rowspan=1, colspan=1,
icon=self.ctx.icon_cat_analyses)
def add_options(self, layout):
_header = gui_elements.grd_Label(lyt=layout,
txt=self.category,
css_id='lbl_Header2',
row=0, col=0, rowspan=1, colspan=1)
layout.setRowStretch(8, 1)
......@@ -20,6 +20,7 @@ from gui import tabs
from inout.DataFunctions import export_to_main
from modboxes.default.Plots.styles.LightTheme import *
# pd.set_option('display.width', 1000)
# pd.set_option('display.max_columns', 15)
# pd.set_option('display.max_rows', 20)
......@@ -277,7 +278,6 @@ class Run(addContent):
tab_data_df=self.tab_data_df) # Return results to main
return main_df
def make_dynamic_axes_dict(self, df):
gs = gridspec.GridSpec(len(df.columns), 1) # rows, cols
gs.update(wspace=0.2, hspace=0.2, left=0.03, right=0.97, top=0.97, bottom=0.03)
......@@ -324,6 +324,7 @@ class Run(addContent):
if str(col[0]).startswith('_'): # Ignore _underscore aux variables
continue
current_color += 1
num_vals = len(plot_df[col].dropna())
ax.plot_date(x=plot_df.index, y=plot_df[col],
color=color_list[current_color], alpha=1, ls='-',
marker='o', markeredgecolor='none', ms=4, zorder=98, label='XXX')
......@@ -334,11 +335,15 @@ class Run(addContent):
if self.marker_isset:
marker_S = plot_df[col].copy()[self.marker_filter]
num_marked_vals = len(marker_S.dropna())
perc_marked_vals = (num_marked_vals / num_vals) * 100
ax.plot_date(x=marker_S.index, y=marker_S,
color='#FFEE58', alpha=1, ls='none', mew=1, mec='#607D8B',
marker='o', ms=5, zorder=98, label='XXX')
marker='o', ms=5, zorder=98, label=f"marked values: {num_marked_vals}"
f" ({perc_marked_vals:.1f}%)")
gui.plotfuncs.default_grid(ax=ax)
gui.plotfuncs.default_legend(ax=ax)