Module buildstock_query.tools.upgrades_visualizer.upgrades_visualizer


Upgrades Visualizer Experimental Stage. :author: Rajendra.Adhikari@nrel.gov

Expand source code
"""
- - - - - - - - -
Upgrades Visualizer
Experimental Stage.
:author: Rajendra.Adhikari@nrel.gov
"""

from functools import reduce
import re
from collections import defaultdict, Counter
import dash_bootstrap_components as dbc
from dash import html, ALL, dcc, ctx
import dash
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
from dash_extensions.enrich import MultiplexerTransform, DashProxy
import dash_mantine_components as dmc
from dash_iconify import DashIconify
from InquirerPy import inquirer
from buildstock_query.tools.upgrades_visualizer.viz_data import VizData
from buildstock_query.tools.upgrades_visualizer.plot_utils import PlotParams, ValueTypes, SavingsTypes
from buildstock_query.tools.upgrades_visualizer.figure import UpgradesPlot
import polars as pl

# os.chdir("/Users/radhikar/Documents/eulpda/EULP-data-analysis/eulpda/smart_query/")
# from: https://github.com/thedirtyfew/dash-extensions/tree/1b8c6466b5b8522690442713eb421f622a1d7a59
# app = DashProxy(transforms=[
#     # TriggerTransform(),  # enable use of Trigger objects
#     MultiplexerTransform(),  # makes it possible to target an output multiple times in callbacks
#     # ServersideOutputTransform(),  # enable use of ServersideOutput objects
#     # NoOutputTransform(),  # enable callbacks without output
#     # BlockingCallbackTransform(),  # makes it possible to skip callback invocations while a callback is running
#     # LogTransform()  # makes it possible to write log messages to a Dash component
# ])
transforms = [MultiplexerTransform()]

# yaml_path = "/Users/radhikar/Documents/eulpda/EULP-data-analysis/notebooks/EUSS-project-file-example.yml"
yaml_path = "/Users/radhikar/Documents/largee/resstock/project_national/fact_sheets_category_1.yml"
opt_sat_path = "/Users/radhikar/Downloads/options_saturations.csv"
default_end_use = "fuel_use_electricity_total_m_btu"


def filter_cols(all_columns, prefixes=[], suffixes=[]):
    cols = []
    for col in all_columns:
        for prefix in prefixes:
            if col.startswith(prefix):
                cols.append(col)
                break
        else:
            for suffix in suffixes:
                if col.endswith(suffix):
                    cols.append(col)
                    break
    return cols


def _get_app(yaml_path: str, opt_sat_path: str, db_name: str = 'euss-tests',
             table_name: str = 'res_test_03_2018_10k_20220607',
             workgroup: str = 'largeee',
             buildstock_type: str = 'resstock'):
    viz_data = VizData(yaml_path=yaml_path, opt_sat_path=opt_sat_path, db_name=db_name,
                       run=table_name, workgroup=workgroup, buildstock_type=buildstock_type)
    return get_app(viz_data)


def get_app(viz_data: VizData):
    upgrades_plot = UpgradesPlot(viz_data)
    upgrade2res = viz_data.upgrade2res
    # upgrade2res_monthly = viz_data.upgrade2res_monthly
    upgrade2name = viz_data.upgrade2name
    resolution = 'annual'
    all_cols = viz_data.upgrade2res[0].columns
    emissions_cols = filter_cols(all_cols, suffixes=['_lb'])
    # end_use_cols = filter_cols(all_cols, ["end_use_", "energy_use__", "fuel_use_"])
    water_usage_cols = filter_cols(all_cols, suffixes=["_gal"])
    load_cols = filter_cols(all_cols, ["load_", "flow_rate_"])
    peak_cols = filter_cols(all_cols, ["peak_"])
    unmet_cols = filter_cols(all_cols, ["unmet_"])
    area_cols = filter_cols(all_cols, suffixes=["_ft_2", ])
    size_cols = filter_cols(all_cols, ["size_"])
    qoi_cols = filter_cols(all_cols, ["qoi_"])
    cost_cols = filter_cols(all_cols, ["upgrade_cost_"])
    build_cols = viz_data.metadata_df.columns
    char_cols = [c.removeprefix(viz_data.main_run._char_prefix) for c in build_cols if 'applicable' not in c]
    char_cols += ['month']
    fuels_types = ['electricity', 'natural_gas', 'propane', 'fuel_oil', 'coal', 'wood_cord', 'wood_pellets']
    change_types = ["any", "no-chng", "bad-chng", "ok-chng", "true-bad-chng", "true-ok-chng"]
    download_csv_df = pl.DataFrame()

    def get_buildings(upgrade):
        return upgrade2res[int(upgrade)]['building_id'].to_list()

    def get_plot(end_use, value_type='mean', savings_type='', change_type='',
                 sync_upgrade=None, filter_bldg=None, group_cols=None, report_upgrade=None):
        filter_bldg = filter_bldg or []
        group_cols = group_cols or []
        sync_upgrade = sync_upgrade or 0
        report_upgrade = int(report_upgrade) if report_upgrade else None

        params = PlotParams(enduses=end_use, value_type=ValueTypes[value_type.lower()],
                            savings_type=SavingsTypes[savings_type.lower().replace(' ', '_')],
                            change_type=change_type, sync_upgrade=sync_upgrade,
                            filter_bldgs=filter_bldg, group_by=group_cols, upgrade=report_upgrade,
                            resolution=resolution)
        return upgrades_plot.get_plot(params)

    external_script = ["https://tailwindcss.com/", {"src": "https://cdn.tailwindcss.com"}]

    app = DashProxy(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], transforms=transforms,
                    external_scripts=external_script)
    app.layout = html.Div([dbc.Container(html.Div([
        dcc.Download(id="download-dataframe-csv"),
        dbc.Row([dbc.Col(html.H1("Upgrades Visualizer"), width='auto'), dbc.Col(html.Sup("beta"))]),
        # Add a row for annual, vs monthly vs seasonal plot radio buttons
        dbc.Row([dbc.Col(dbc.Label("Resolution: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["annual", "monthly"], "annual",
                                        inline=True, id="radio_resolution"))]),

        dbc.Row([dbc.Col(dbc.Label("Visualization Type: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["Mean", "Total", "Count", "Distribution", "Scatter"], "Mean",
                                        id="radio_graph_type",
                                        inline=True,
                                        labelClassName="pr-2"), width='auto'),
                 dbc.Col(dbc.Label("Value Type: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["Absolute", "Savings", "Percent Savings"], "Absolute",  inline=True,
                                        id='radio_savings', labelClassName="pr-2"), width='auto'),
                 ]),
        dbc.Row([dbc.Col(html.Br())]),
        dbc.Row([dbc.Col(dcc.Loading(id='graph-loader', children=[html.Div(id='loader_label')]))]),
        dbc.Row([dbc.Col(dcc.Graph(id='graph'))]),
        dbc.Row([dbc.Col(dbc.Button("Download", id='csv-download'))], justify='end'),
        dcc.Store(id='graph-data-store'),
    ])),
        dbc.Row([dbc.Col(
            dcc.Tabs(id='tab_view_type', value='energy', children=[
                dcc.Tab(id='energy_tab', label='Energy', value='energy', children=[
                    dcc.RadioItems(fuels_types + ['All'], "electricity", id='radio_fuel',  inline=True,
                                   labelClassName="pr-2")]
                        ),
                dcc.Tab(label='Water Usage', value='water', children=[]
                        ),
                dcc.Tab(label='Load', value='load', children=[]
                        ),
                dcc.Tab(label='Peak', value='peak', children=[]
                        ),
                dcc.Tab(label='Unmet Hours', value='unmet_hours', children=[]
                        ),
                dcc.Tab(label='Area', value='area', children=[]
                        ),
                dcc.Tab(label='Size', value='size', children=[]
                        ),
                dcc.Tab(label='QOI', value='qoi', children=[]
                        ),
                dcc.Tab(label='emissions', value='emissions', children=[]
                        ),
                dcc.Tab(label='Upgrade Cost', value='upgrade_cost', children=[]
                        ),
            ])
        )
        ], className="mx-5 mt-5"),
        dbc.Row([dbc.Col(dcc.Dropdown(id='dropdown_enduse'))], className="mx-5 my-1"),
        dbc.Row(
            dbc.Col([
                dbc.Row([dbc.Col(html.Div("Restrict to buildings that have "), width='auto'),
                         dbc.Col(dcc.Dropdown(change_types, "", placeholder="Select change type...",
                                              id='dropdown_chng_type'), width='2'),
                         dbc.Col(html.Div(" in "), width='auto'),
                         dbc.Col(dcc.Dropdown(id='sync_upgrade', value='',
                                              options={}))
                         ],
                        className="flex items-center"),
                dbc.Row([dbc.Col(html.Div("Select:"), style={"padding-left": "12px", "padding-right": "0px"},
                                 width='auto'),
                         dbc.Col(dcc.Dropdown(id='input_building'), width=1),
                         dbc.Col(html.Div("("), width='auto',
                                 style={"padding-left": "0px", "padding-right": "0px"}),
                         dbc.Col(dcc.Checklist(['Lock)'], [],
                                               inline=True, id='chk-lock'),
                                 width='auto', style={"padding-left": "0px", "padding-right": "0px"}),
                         dbc.Col(html.Div(" in "), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='report_upgrade', value='', placeholder="Upgrade ...",
                                              options=viz_data.upgrade2shortname), width=1),
                         dbc.Col(html.Div("grouped by:"), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='drp-group-by', options=char_cols, value=None,
                                              multi=True, placeholder="Select characteristics..."),
                                 width=3),
                         dbc.Col(dbc.Button("<= Copy", id="btn-copy", color="primary", size="sm",
                                            outline=True), class_name="centered-col", style={"padding-right": "0px"}),
                         dbc.Col(html.Div("Extra restriction: "), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='input_building2', disabled=False), width=1),
                         dbc.Col(dcc.Checklist(id='chk-graph', options=['Graph'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-options', options=['Options'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-enduses', options=['Enduses'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-chars', options=['Chars'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dbc.Button("Reset", id="btn-reset", color="primary", size="sm", outline=True),
                                 width='auto'),
                         ]),
                dbc.Row([dbc.Col([
                    dbc.Row(html.Div(id="options_report_header")),
                    dbc.Row(dcc.Loading(id='opt-loader',
                                        children=html.Div(id="options_report"))),
                    dcc.Store("opt_report_store")
                ], width=5),
                    dbc.Col([
                        dbc.Row([dbc.Col(html.Div("View enduse that: "), width='auto'),
                                 dbc.Col(dcc.Dropdown(id='input_enduse_type',
                                                      options=['changed', 'increased', 'decreased', 'are almost zero'],
                                                      value='changed', clearable=False),
                                         width=2),
                                 dbc.Col(html.Div(), width='auto'),
                                 dbc.Col(html.Div("Charecteristics Report:"), width='auto'),
                                 dbc.Col(dcc.Dropdown(id='drp-char-report', options=char_cols, value=None,
                                                      multi=True, placeholder="Select characteristics...")),
                                 ]),
                        dbc.Row([dbc.Col(dcc.Loading(id='enduse_loader',
                                                     children=html.Div(id="enduse_report"))
                                         ),
                                 dbc.Col(dcc.Loading(id='char_loader',
                                                     children=html.Div(id="char_report"))
                                         ),
                                 ]),
                        dcc.Store("enduse_report_store"),
                        dcc.Store("char_report_store")
                    ], width=7)

                ]),
                dbc.Row([
                    dbc.Col(width=5),
                    dbc.Col(
                        dbc.Row([
                            dbc.Col(),
                            dbc.Col(dbc.Button("Download All Characteristics", id="btn-dwn-chars",
                                               color="primary",
                                               size="sm", outline=True), width='auto'),
                        ]), width=7)
                ])
            ]), className="mx-5 my-1"),
        html.Div(id="status_bar"),
        dcc.Download(id="download-chars-csv"),
        dcc.Store("uirevision")
        # dbc.Button("Kill me", id="button110")
    ])

    # download data with button click
    @app.callback(
        Output("download-dataframe-csv", "data"),
        Input("csv-download", "n_clicks"),
        prevent_initial_call=True)
    def download_csv(n_clicks):
        if not n_clicks:
            raise PreventUpdate()
        nonlocal download_csv_df
        return dcc.send_bytes(download_csv_df.write_csv, "graph_data.csv")

    @app.callback(
        Output("download-chars-csv", "data"),
        Input("btn-dwn-chars", "n_clicks"),
        State("input_building", "value"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State('chk-chars', 'value'),
    )
    def download_char(n_clicks, bldg_id, bldg_options, bldg_options2, chk_chars):
        if not n_clicks:
            raise PreventUpdate()

        if "Chars" in chk_chars and bldg_options2:
            bldg_ids = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_ids = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        bldg_ids = [int(b) for b in bldg_ids]
        bdf = viz_data.upgrade2res[0].filter(pl.col("building_id").is_in(set(bldg_ids))).select(char_cols)
        return dcc.send_bytes(bdf.write_csv, f"chars_{n_clicks}.csv")

    def get_elligible_output_columns(category, fuel):
        if category == 'energy':
            elligible_cols = viz_data.get_cleaned_up_end_use_cols(resolution, fuel)
        elif category == 'water':
            elligible_cols = water_usage_cols if resolution == 'annual' else []
        elif category == 'load':
            elligible_cols = load_cols if resolution == 'annual' else []
        elif category == 'peak':
            elligible_cols = peak_cols if resolution == 'annual' else []
        elif category == 'unmet_hours':
            elligible_cols = unmet_cols if resolution == 'annual' else []
        elif category == 'area':
            elligible_cols = area_cols if resolution == 'annual' else []
        elif category == 'size':
            elligible_cols = size_cols if resolution == 'annual' else []
        elif category == 'qoi':
            elligible_cols = qoi_cols if resolution == 'annual' else []
        elif category == 'emissions':
            elligible_cols = emissions_cols if resolution == 'annual' else\
                viz_data.get_emissions_cols(resolution=resolution)
        elif category == 'upgrade_cost':
            elligible_cols = cost_cols if resolution == 'annual' else []
        else:
            raise ValueError(f"Invalid tab {category}")
        return elligible_cols

    @app.callback(
        Output('radio_resolution', 'options'),
        Input('radio_resolution', 'value'),
    )
    def update_resolution(res):
        nonlocal resolution
        resolution = res
        return ['annual', 'monthly']

    @app.callback(
        Output('dropdown_enduse', "options"),
        Output('dropdown_enduse', "value"),
        Input('tab_view_type', "value"),
        Input('radio_fuel', "value"),
        Input('dropdown_enduse', "value"),
        Input('radio_resolution', 'value')
    )
    def update_enduse_options(view_tab, fuel_type, current_enduse, resolution):
        elligible_cols = get_elligible_output_columns(view_tab, fuel_type)
        enduse = current_enduse if current_enduse in elligible_cols else elligible_cols[0]
        return sorted(elligible_cols), enduse

    @app.callback(
        Output("sync_upgrade", 'value'),
        Output("sync_upgrade", 'options'),
        Output("sync_upgrade", 'placeholder'),
        Input('dropdown_chng_type', "value"),
        State("sync_upgrade", "value"),
        State("sync_upgrade", "options")
    )
    def update_sync_upgrade(chng_type, current_sync_upgrade, sync_upgrade_options):
        # print(chng_type, current_sync_upgrade, sync_upgrade_options)
        if chng_type:
            return current_sync_upgrade, upgrade2name, 'respective upgrades. (Click to restrict to specific upgrade)'
        else:
            return '', {}, ' <select a change type on the left first>'

    @app.callback(
        Output('input_building', 'placeholder'),
        Input('input_building', 'options')
    )
    def update_building_placeholder(options):
        return f"{len(options)} Buidlings" if options else "0 buildings."

    @app.callback(
        Output('input_building2', 'placeholder'),
        Output('input_building2', 'value'),
        Input('input_building2', 'value'),
        Input('input_building2', 'options')
    )
    def update_building_placeholder2(value, options):
        return f"{len(options)} Buidlings" if options else "No restriction", None

    @app.callback(
        Output('input_building', 'value'),
        Output('input_building', 'options'),
        Output('report_upgrade', 'value'),
        Input('graph', "selectedData"),
        State('input_building', 'options'),
        State('report_upgrade', 'value')
    )
    def graph_click(selection_data, current_options, current_upgrade):
        if not selection_data or 'points' not in selection_data or len(selection_data['points']) < 1:
            raise PreventUpdate()

        selected_buildings = []
        selected_upgrades = []
        for point in selection_data['points']:
            if not (match := re.search(r"Building: (\d*)", point.get('hovertext', ''))):
                continue
            if bldg := match.groups()[0]:
                upgrade_match = re.search(r"Upgrade (\d*)", point.get('hovertext', ''))
                upgrade = upgrade_match.groups()[0] if upgrade_match else ''
                selected_buildings.append(bldg)
                selected_upgrades.append(upgrade)

        if not selected_buildings:
            raise PreventUpdate()

        selected_upgrade = selected_upgrades[0] or current_upgrade
        if len(selected_buildings) != 1:
            selected_buildings = list(set(selected_buildings))
            return '', selected_buildings, selected_upgrade
        current_options = current_options or selected_buildings
        return selected_buildings[0], current_options, selected_upgrade

    @app.callback(
        Output('chk-graph', 'value'),
        Output('chk-options', 'value'),
        Output('chk-enduses', 'value'),
        Output('input_building2', 'options'),
        Output("uirevision", "data"),
        Input('btn-reset', "n_clicks")
    )
    def reset(n_clicks):
        return [], [], [], [], n_clicks

    @app.callback(
        Output('input_building', 'options'),
        Input('btn-copy', "n_clicks"),
        State('input_building2', 'options'),
    )
    def copy(n_clicks, bldg_options2):
        return bldg_options2 or dash.no_update

    @app.callback(
        Output('input_building', 'value'),
        Output('input_building', 'options'),
        Output('input_building2', 'options'),
        State('input_building', 'value'),
        State('input_building', 'options'),
        State('input_building2', 'options'),
        Input('chk-lock', 'value'),
        Input('dropdown_chng_type', "value"),
        Input('sync_upgrade', 'value'),
        Input('report_upgrade', 'value'),
        Input('btn-reset', "n_clicks")
    )
    def bldg_selection(current_bldg, current_options, current_options2, chk_lock,
                       change_type, sync_upgrade, report_upgrade,
                       reset_click):

        if sync_upgrade and change_type:
            valid_bldgs = set(viz_data.chng2bldg[(int(sync_upgrade), change_type)])
        elif report_upgrade and change_type:
            valid_bldgs = set(viz_data.chng2bldg[(int(report_upgrade), change_type)])
            buildings = get_buildings(report_upgrade)
            valid_bldgs = set(buildings).intersection(valid_bldgs)
        elif report_upgrade:
            buildings = get_buildings(report_upgrade)
            valid_bldgs = set(buildings)
        else:
            valid_bldgs = set(viz_data.upgrade2res[0]['building_id'].to_list())

        base_res = upgrade2res[0].filter(pl.col("building_id").is_in(valid_bldgs))
        valid_bldgs = list(base_res['building_id'].to_list())

        if "btn-reset" != ctx.triggered_id and current_options and len(current_options) > 0 and chk_lock:
            current_options_set = set(current_options)
            valid_bldgs = [b for b in valid_bldgs if b in current_options_set]

        valid_bldgs2 = []

        if current_bldg and (int(current_bldg) not in valid_bldgs):
            current_bldg = valid_bldgs[0] if valid_bldgs else ''
        return current_bldg, valid_bldgs, valid_bldgs2

    def get_char_choices(char):
        if char:
            res0 = upgrade2res[0]
            unique_choices = sorted(list(res0[char].unique()))
            return unique_choices, unique_choices[0]
        else:
            return [], None

    def get_action_button_pairs(id, bldg_list_dict, report_type='opt'):
        buttons = []
        bldg_str = '' if len(bldg_list_dict[id]) > 10 else " [" + ','.join([str(b) for b in bldg_list_dict[id]]) + "]"
        for type in ['check', 'cross']:
            icon_name = "akar-icons:circle-check-fill" if type == 'check' else "gridicons:cross-circle"
            button = html.Div(dmc.ActionIcon(DashIconify(icon=icon_name,
                                                         width=20 if type == "check" else 22,
                                                         height=20 if type == "check" else 22,
                                                         ),
                                             id={'index': id, 'type': f'btn-{type}', 'report_type': report_type},
                                             variant="light"),
                              id=f"div-tooltip-target-{type}-{id}")
            if type == "check":
                tooltip = dbc.Tooltip(f"Select these buildings.{bldg_str}",
                                      target=f"div-tooltip-target-{type}-{id}", delay={'show': 1000})

                col = dbc.Col(html.Div([button, tooltip]),  width='auto', class_name="col-btn-cross",
                              style={"padding-left": "0px", "padding-right": "0px"})
            else:
                tooltip = dbc.Tooltip(f"Select all except these buildings.{bldg_str}",
                                      target=f"div-tooltip-target-{type}-{id}", delay={'show': 1000})
                col = dbc.Col(html.Div([button, tooltip]),  width='auto',  class_name="col-btn-check",
                              style={"padding-left": "0px", "padding-right": "0px"})
            buttons.append(col)
        return buttons

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "opt"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "opt"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State("chk-options", "value"),
        State("opt_report_store", "data"),
    )
    def opt_check_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_options, opt_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Options" in chk_options and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = opt_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "enduse"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "enduse"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State("chk-enduses", "value"),
        State("enduse_report_store", "data"),
    )
    def enduse_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_enduses, opt_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Enduses" in chk_enduses and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = opt_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "char"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "char"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State('chk-chars', 'value'),
        State("char_report_store", "data"),
    )
    def char_report_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_char, char_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Chars" in chk_char and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = char_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output("options_report_header", "children"),
        Output("options_report", 'children'),
        Output("opt_report_store", "data"),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        State('report_upgrade', 'value'),
        Input('chk-options', 'value'),
    )
    def show_opt_report(bldg_id, bldg_options, bldg_options2, report_upgrade, chk_options):
        if not report_upgrade or not bldg_options:
            return "Select an upgrade to see options applied in that upgrade", [''], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Options" not in chk_options:
            raise PreventUpdate()

        if "Options" in chk_options and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        run_obj = viz_data.run_obj(int(report_upgrade))
        applied_options = run_obj.report.get_applied_options(upgrade_id=int(report_upgrade),
                                                             bldg_ids=bldg_list,
                                                             include_base_opt=True)
        opt_only = [{entry.split('|')[0] for entry in opt.keys()} for opt in applied_options]
        reduced_set = list(reduce(set.union, opt_only))

        nested_dict = defaultdict(lambda: defaultdict(Counter))
        bldg_list_dict = defaultdict(list)

        for bldg_id, opt_dict in zip(bldg_list, applied_options):
            for opt_para, value in opt_dict.items():
                opt = opt_para.split('|')[0]
                para = opt_para.split('|')[1]
                nested_dict[opt][para][value] += 1
                bldg_list_dict[opt].append(bldg_id)
                bldg_list_dict[opt + "|" + para].append(bldg_id)
                bldg_list_dict[opt + "|" + para + "<-" + value].append(bldg_id)

        def get_accord_item(opt_name):
            total_count = sum(counter.total() for counter in nested_dict[opt_name].values())
            children = []
            for parameter, counter in nested_dict[opt_name].items():
                contents = []
                new_counter = Counter({"All": counter.total()})
                new_counter.update(counter)
                for from_val, count in new_counter.items():
                    if from_val == "All":
                        but_ids = f"{opt_name}|{parameter}"
                    else:
                        but_ids = f"{opt_name}|{parameter}<-{from_val}"
                    entry = dbc.Row([dbc.Col(f"<-{from_val} ({count})", width="auto"),
                                     *get_action_button_pairs(but_ids, bldg_list_dict)])
                    contents.append(entry)
                children.append(dmc.AccordionItem(contents, label=f"{parameter} ({counter.total()})"))

            accordian = dmc.Accordion(children, multiple=True)
            first_row = dbc.Row([dbc.Col(f"All ({total_count})", width="auto"),
                                 *get_action_button_pairs(opt_name, bldg_list_dict)])
            return dmc.AccordionItem([first_row, accordian], label=f"{opt_name} ({total_count})")
        if reduced_set:
            final_report = dmc.Accordion([get_accord_item(opt_name) for opt_name in reduced_set], multiple=True)
        else:
            final_report = ["No option got applied to the selected building(s)."]
        up_name = upgrade2name[int(report_upgrade)]
        return f"Options applied in {up_name}", final_report, dict(bldg_list_dict)

    @app.callback(
        Output("enduse_report", "children"),
        Output("enduse_report_store", "data"),
        State('report_upgrade', 'value'),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        Input('input_enduse_type', 'value'),
        Input('chk-enduses', 'value'),
    )
    def show_enduse_report(report_upgrade, bldg_id, bldg_options, bldg_options2, enduse_change_type, chk_enduse):
        if not report_upgrade or not bldg_options:
            return ["Select an upgrade to see enuse report."], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Enduses" not in chk_enduse:
            raise PreventUpdate()

        if "Enduses" in chk_enduse and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]

        # print(bldg_list)
        run_obj = viz_data.run_obj(int(report_upgrade))
        dict_changed_enduses = run_obj.report.get_enduses_buildings_map_by_change(upgrade_id=int(report_upgrade),
                                                                                  change_type=enduse_change_type,
                                                                                  bldg_list=bldg_list)
        # print(changed_enduses)

        all_changed_enduses = list(dict_changed_enduses.keys())
        if not all_changed_enduses:
            if bldg_id:
                return f'No enduse has {enduse_change_type} in building {bldg_id} ', {}
            else:
                return f'No enduse has {enduse_change_type} in any of the buildings', {}

        enduses2bldgs = defaultdict(list)
        for end_use, bldgs in dict_changed_enduses.items():
            if end_use in all_changed_enduses:
                enduses2bldgs[end_use].extend([int(bldg_id) for bldg_id in bldgs])

        fuel2bldgs = defaultdict(set)
        fuel2enduses = defaultdict(list)
        for enduse, bldgs in enduses2bldgs.items():
            for fuel in ['all_fuel'] + fuels_types:
                if fuel in enduse:
                    fuel2bldgs[fuel] |= set(bldgs)
                    fuel2enduses[fuel].append(enduse)
                    break
            else:
                fuel2bldgs['other'] |= set(bldgs)
                fuel2enduses['other'].append(enduse)

        for key, val in fuel2bldgs.items():
            fuel2bldgs[key] = list(val)

        enduses2bldgs.update(fuel2bldgs)

        def get_accord_item(fuel):
            total_count = len(enduses2bldgs[fuel])
            contents = [dbc.Row([dbc.Col(f"All {fuel} ({total_count})", width="auto"),
                                 *get_action_button_pairs(fuel, enduses2bldgs, "enduse")])]
            for enduse in fuel2enduses[fuel]:
                count = len(enduses2bldgs[enduse])
                row = dbc.Row([dbc.Col(f"{enduse} ({count})", width="auto"),
                               *get_action_button_pairs(enduse, enduses2bldgs, "enduse")])
                contents.append(row)
            return dmc.AccordionItem(contents, label=f"{fuel} ({total_count})")

        report = dmc.Accordion([get_accord_item(fuel) for fuel in fuel2enduses.keys()],  multiple=True)
        storedict = dict(enduses2bldgs)
        return report, storedict

    @app.callback(
        Output("char_report", "children"),
        Output("char_report_store", "data"),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        Input('drp-char-report', 'value'),
        Input('chk-chars', 'value'),
    )
    def show_char_report(bldg_id, bldg_options, bldg_options2, inp_char: list[str], chk_chars):
        if not (bldg_options or bldg_options2 or bldg_id):
            return [""], {}
        if not inp_char:
            return ["Select a characteristics to see its report."], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Chars" not in chk_chars:
            raise PreventUpdate()

        if "Chars" in chk_chars and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        chars_df = viz_data.bs_res_df.filter(pl.col('building_id').is_in(
            set(bldg_list))).select(inp_char + ['building_id'])
        char2bldgs = chars_df.groupby(inp_char).agg('building_id')
        if (total_len := len(char2bldgs)) > 250:
            return [f"Sorry, this would create more than 200 ({total_len}) rows."], {}
        char_dict = {}
        total_count = 0
        contents = []
        for char_vals, group_df in chars_df.groupby(inp_char):
            bldglist = group_df['building_id'].to_list()
            but_ids = "+".join(char_vals) if isinstance(char_vals, tuple) else char_vals
            char_dict[but_ids] = [int(b) for b in bldglist]
            count = len(bldglist)
            total_count += count
            contents.append(dbc.Row([dbc.Col(f"{char_vals} ({count})", width="auto"),
                                     *get_action_button_pairs(but_ids, char_dict, "char")]))

        report = dmc.Accordion([dmc.AccordionItem(contents, label=f"{inp_char} ({total_count})")])
        return report, char_dict

    @app.callback(
        Output('graph', 'figure'),
        Output('graph', 'config'),
        Output('loader_label', "children"),
        State('tab_view_type', "value"),
        Input('drp-group-by', 'value'),
        Input('radio_fuel', "value"),
        Input('dropdown_enduse', "value"),
        Input('radio_graph_type', "value"),
        Input('radio_savings', "value"),
        Input('dropdown_chng_type', "value"),
        Input('sync_upgrade', 'value'),
        Input('input_building', 'value'),
        Input('input_building', 'options'),
        Input('input_building2', 'options'),
        Input('chk-graph', 'value'),
        State("uirevision", "data"),
        State('report_upgrade', 'value')
    )
    def update_figure(view_tab, grp_by, fuel, enduse, graph_type, savings_type, chng_type,
                      sync_upgrade, selected_bldg, bldg_options, bldg_options2, chk_graph, uirevision,
                      report_upgrade):
        nonlocal download_csv_df
        if dash.callback_context.triggered_id == 'input_building2' and "Graph" not in chk_graph:
            raise PreventUpdate()

        if "Graph" in chk_graph and bldg_options2:
            bldg_options = bldg_options2

        bldg_options = bldg_options or []
        if not enduse:
            full_name = []
        if view_tab == 'energy':
            full_name = viz_data.get_end_use_db_cols(resolution, fuel, enduse)
        else:
            full_name = [enduse]

        if selected_bldg:
            filter_bldg = [int(selected_bldg)]
        else:
            filter_bldg = [int(b) for b in bldg_options]

        new_figure, report_df = get_plot(full_name, graph_type, savings_type,
                                         chng_type, sync_upgrade, filter_bldg, grp_by, report_upgrade)

        uirevision = uirevision or "default"
        new_figure.update_layout(uirevision=uirevision)
        download_csv_df = report_df
        config = {'edits': {"titleText": True, "axisTitleText": True}, 'displayModeBar': True,
                  "modeBarButtonsToRemove": ["Zoom", "ZoomIn", "Pan", "ZoomOut", "AutoScale", "select2d"]}
        return new_figure, config, ""

    return app


def main():
    print("Welcome to Upgrades Visualizer.")
    yaml_path = inquirer.text(message="Please enter path to the buildstock configuration yml file: ",
                              default="").execute()
    opt_sat_path = inquirer.text(message="Please enter path to the options saturation csv file: ",
                                 default="").execute()
    workgroup = inquirer.text(message="Please Athena workgroup name: ",
                              default="rescore").execute()
    db_name = inquirer.text(message="Please enter database_name "
                            "(found in postprocessing:aws:athena in the buildstock configuration file): ",
                            default='').execute()
    table_name = inquirer.text(message="Please enter table name (same as output folder name; found under "
                               "output_directory in the buildstock configuration file). [Enter two names "
                               "separated by comma if baseline and upgrades are in different run] :",
                               default=""
                               ).execute()

    if ',' in table_name:
        table_name = table_name.split(',')
    app = _get_app(yaml_path=yaml_path,
                   opt_sat_path=opt_sat_path,
                   workgroup=workgroup,
                   db_name=db_name,
                   table_name=table_name)
    app.run_server(debug=False, port=8005)


if __name__ == '__main__':
    main()

Functions

def filter_cols(all_columns, prefixes=[], suffixes=[])
Expand source code
def filter_cols(all_columns, prefixes=[], suffixes=[]):
    cols = []
    for col in all_columns:
        for prefix in prefixes:
            if col.startswith(prefix):
                cols.append(col)
                break
        else:
            for suffix in suffixes:
                if col.endswith(suffix):
                    cols.append(col)
                    break
    return cols
def get_app(viz_data: VizData)
Expand source code
def get_app(viz_data: VizData):
    upgrades_plot = UpgradesPlot(viz_data)
    upgrade2res = viz_data.upgrade2res
    # upgrade2res_monthly = viz_data.upgrade2res_monthly
    upgrade2name = viz_data.upgrade2name
    resolution = 'annual'
    all_cols = viz_data.upgrade2res[0].columns
    emissions_cols = filter_cols(all_cols, suffixes=['_lb'])
    # end_use_cols = filter_cols(all_cols, ["end_use_", "energy_use__", "fuel_use_"])
    water_usage_cols = filter_cols(all_cols, suffixes=["_gal"])
    load_cols = filter_cols(all_cols, ["load_", "flow_rate_"])
    peak_cols = filter_cols(all_cols, ["peak_"])
    unmet_cols = filter_cols(all_cols, ["unmet_"])
    area_cols = filter_cols(all_cols, suffixes=["_ft_2", ])
    size_cols = filter_cols(all_cols, ["size_"])
    qoi_cols = filter_cols(all_cols, ["qoi_"])
    cost_cols = filter_cols(all_cols, ["upgrade_cost_"])
    build_cols = viz_data.metadata_df.columns
    char_cols = [c.removeprefix(viz_data.main_run._char_prefix) for c in build_cols if 'applicable' not in c]
    char_cols += ['month']
    fuels_types = ['electricity', 'natural_gas', 'propane', 'fuel_oil', 'coal', 'wood_cord', 'wood_pellets']
    change_types = ["any", "no-chng", "bad-chng", "ok-chng", "true-bad-chng", "true-ok-chng"]
    download_csv_df = pl.DataFrame()

    def get_buildings(upgrade):
        return upgrade2res[int(upgrade)]['building_id'].to_list()

    def get_plot(end_use, value_type='mean', savings_type='', change_type='',
                 sync_upgrade=None, filter_bldg=None, group_cols=None, report_upgrade=None):
        filter_bldg = filter_bldg or []
        group_cols = group_cols or []
        sync_upgrade = sync_upgrade or 0
        report_upgrade = int(report_upgrade) if report_upgrade else None

        params = PlotParams(enduses=end_use, value_type=ValueTypes[value_type.lower()],
                            savings_type=SavingsTypes[savings_type.lower().replace(' ', '_')],
                            change_type=change_type, sync_upgrade=sync_upgrade,
                            filter_bldgs=filter_bldg, group_by=group_cols, upgrade=report_upgrade,
                            resolution=resolution)
        return upgrades_plot.get_plot(params)

    external_script = ["https://tailwindcss.com/", {"src": "https://cdn.tailwindcss.com"}]

    app = DashProxy(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], transforms=transforms,
                    external_scripts=external_script)
    app.layout = html.Div([dbc.Container(html.Div([
        dcc.Download(id="download-dataframe-csv"),
        dbc.Row([dbc.Col(html.H1("Upgrades Visualizer"), width='auto'), dbc.Col(html.Sup("beta"))]),
        # Add a row for annual, vs monthly vs seasonal plot radio buttons
        dbc.Row([dbc.Col(dbc.Label("Resolution: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["annual", "monthly"], "annual",
                                        inline=True, id="radio_resolution"))]),

        dbc.Row([dbc.Col(dbc.Label("Visualization Type: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["Mean", "Total", "Count", "Distribution", "Scatter"], "Mean",
                                        id="radio_graph_type",
                                        inline=True,
                                        labelClassName="pr-2"), width='auto'),
                 dbc.Col(dbc.Label("Value Type: "), width='auto'),
                 dbc.Col(dcc.RadioItems(["Absolute", "Savings", "Percent Savings"], "Absolute",  inline=True,
                                        id='radio_savings', labelClassName="pr-2"), width='auto'),
                 ]),
        dbc.Row([dbc.Col(html.Br())]),
        dbc.Row([dbc.Col(dcc.Loading(id='graph-loader', children=[html.Div(id='loader_label')]))]),
        dbc.Row([dbc.Col(dcc.Graph(id='graph'))]),
        dbc.Row([dbc.Col(dbc.Button("Download", id='csv-download'))], justify='end'),
        dcc.Store(id='graph-data-store'),
    ])),
        dbc.Row([dbc.Col(
            dcc.Tabs(id='tab_view_type', value='energy', children=[
                dcc.Tab(id='energy_tab', label='Energy', value='energy', children=[
                    dcc.RadioItems(fuels_types + ['All'], "electricity", id='radio_fuel',  inline=True,
                                   labelClassName="pr-2")]
                        ),
                dcc.Tab(label='Water Usage', value='water', children=[]
                        ),
                dcc.Tab(label='Load', value='load', children=[]
                        ),
                dcc.Tab(label='Peak', value='peak', children=[]
                        ),
                dcc.Tab(label='Unmet Hours', value='unmet_hours', children=[]
                        ),
                dcc.Tab(label='Area', value='area', children=[]
                        ),
                dcc.Tab(label='Size', value='size', children=[]
                        ),
                dcc.Tab(label='QOI', value='qoi', children=[]
                        ),
                dcc.Tab(label='emissions', value='emissions', children=[]
                        ),
                dcc.Tab(label='Upgrade Cost', value='upgrade_cost', children=[]
                        ),
            ])
        )
        ], className="mx-5 mt-5"),
        dbc.Row([dbc.Col(dcc.Dropdown(id='dropdown_enduse'))], className="mx-5 my-1"),
        dbc.Row(
            dbc.Col([
                dbc.Row([dbc.Col(html.Div("Restrict to buildings that have "), width='auto'),
                         dbc.Col(dcc.Dropdown(change_types, "", placeholder="Select change type...",
                                              id='dropdown_chng_type'), width='2'),
                         dbc.Col(html.Div(" in "), width='auto'),
                         dbc.Col(dcc.Dropdown(id='sync_upgrade', value='',
                                              options={}))
                         ],
                        className="flex items-center"),
                dbc.Row([dbc.Col(html.Div("Select:"), style={"padding-left": "12px", "padding-right": "0px"},
                                 width='auto'),
                         dbc.Col(dcc.Dropdown(id='input_building'), width=1),
                         dbc.Col(html.Div("("), width='auto',
                                 style={"padding-left": "0px", "padding-right": "0px"}),
                         dbc.Col(dcc.Checklist(['Lock)'], [],
                                               inline=True, id='chk-lock'),
                                 width='auto', style={"padding-left": "0px", "padding-right": "0px"}),
                         dbc.Col(html.Div(" in "), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='report_upgrade', value='', placeholder="Upgrade ...",
                                              options=viz_data.upgrade2shortname), width=1),
                         dbc.Col(html.Div("grouped by:"), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='drp-group-by', options=char_cols, value=None,
                                              multi=True, placeholder="Select characteristics..."),
                                 width=3),
                         dbc.Col(dbc.Button("<= Copy", id="btn-copy", color="primary", size="sm",
                                            outline=True), class_name="centered-col", style={"padding-right": "0px"}),
                         dbc.Col(html.Div("Extra restriction: "), style={"padding-right": "0px"}, width='auto'),
                         dbc.Col(dcc.Dropdown(id='input_building2', disabled=False), width=1),
                         dbc.Col(dcc.Checklist(id='chk-graph', options=['Graph'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-options', options=['Options'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-enduses', options=['Enduses'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dcc.Checklist(id='chk-chars', options=['Chars'], value=[],
                                               inline=True), width='auto'),
                         dbc.Col(dbc.Button("Reset", id="btn-reset", color="primary", size="sm", outline=True),
                                 width='auto'),
                         ]),
                dbc.Row([dbc.Col([
                    dbc.Row(html.Div(id="options_report_header")),
                    dbc.Row(dcc.Loading(id='opt-loader',
                                        children=html.Div(id="options_report"))),
                    dcc.Store("opt_report_store")
                ], width=5),
                    dbc.Col([
                        dbc.Row([dbc.Col(html.Div("View enduse that: "), width='auto'),
                                 dbc.Col(dcc.Dropdown(id='input_enduse_type',
                                                      options=['changed', 'increased', 'decreased', 'are almost zero'],
                                                      value='changed', clearable=False),
                                         width=2),
                                 dbc.Col(html.Div(), width='auto'),
                                 dbc.Col(html.Div("Charecteristics Report:"), width='auto'),
                                 dbc.Col(dcc.Dropdown(id='drp-char-report', options=char_cols, value=None,
                                                      multi=True, placeholder="Select characteristics...")),
                                 ]),
                        dbc.Row([dbc.Col(dcc.Loading(id='enduse_loader',
                                                     children=html.Div(id="enduse_report"))
                                         ),
                                 dbc.Col(dcc.Loading(id='char_loader',
                                                     children=html.Div(id="char_report"))
                                         ),
                                 ]),
                        dcc.Store("enduse_report_store"),
                        dcc.Store("char_report_store")
                    ], width=7)

                ]),
                dbc.Row([
                    dbc.Col(width=5),
                    dbc.Col(
                        dbc.Row([
                            dbc.Col(),
                            dbc.Col(dbc.Button("Download All Characteristics", id="btn-dwn-chars",
                                               color="primary",
                                               size="sm", outline=True), width='auto'),
                        ]), width=7)
                ])
            ]), className="mx-5 my-1"),
        html.Div(id="status_bar"),
        dcc.Download(id="download-chars-csv"),
        dcc.Store("uirevision")
        # dbc.Button("Kill me", id="button110")
    ])

    # download data with button click
    @app.callback(
        Output("download-dataframe-csv", "data"),
        Input("csv-download", "n_clicks"),
        prevent_initial_call=True)
    def download_csv(n_clicks):
        if not n_clicks:
            raise PreventUpdate()
        nonlocal download_csv_df
        return dcc.send_bytes(download_csv_df.write_csv, "graph_data.csv")

    @app.callback(
        Output("download-chars-csv", "data"),
        Input("btn-dwn-chars", "n_clicks"),
        State("input_building", "value"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State('chk-chars', 'value'),
    )
    def download_char(n_clicks, bldg_id, bldg_options, bldg_options2, chk_chars):
        if not n_clicks:
            raise PreventUpdate()

        if "Chars" in chk_chars and bldg_options2:
            bldg_ids = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_ids = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        bldg_ids = [int(b) for b in bldg_ids]
        bdf = viz_data.upgrade2res[0].filter(pl.col("building_id").is_in(set(bldg_ids))).select(char_cols)
        return dcc.send_bytes(bdf.write_csv, f"chars_{n_clicks}.csv")

    def get_elligible_output_columns(category, fuel):
        if category == 'energy':
            elligible_cols = viz_data.get_cleaned_up_end_use_cols(resolution, fuel)
        elif category == 'water':
            elligible_cols = water_usage_cols if resolution == 'annual' else []
        elif category == 'load':
            elligible_cols = load_cols if resolution == 'annual' else []
        elif category == 'peak':
            elligible_cols = peak_cols if resolution == 'annual' else []
        elif category == 'unmet_hours':
            elligible_cols = unmet_cols if resolution == 'annual' else []
        elif category == 'area':
            elligible_cols = area_cols if resolution == 'annual' else []
        elif category == 'size':
            elligible_cols = size_cols if resolution == 'annual' else []
        elif category == 'qoi':
            elligible_cols = qoi_cols if resolution == 'annual' else []
        elif category == 'emissions':
            elligible_cols = emissions_cols if resolution == 'annual' else\
                viz_data.get_emissions_cols(resolution=resolution)
        elif category == 'upgrade_cost':
            elligible_cols = cost_cols if resolution == 'annual' else []
        else:
            raise ValueError(f"Invalid tab {category}")
        return elligible_cols

    @app.callback(
        Output('radio_resolution', 'options'),
        Input('radio_resolution', 'value'),
    )
    def update_resolution(res):
        nonlocal resolution
        resolution = res
        return ['annual', 'monthly']

    @app.callback(
        Output('dropdown_enduse', "options"),
        Output('dropdown_enduse', "value"),
        Input('tab_view_type', "value"),
        Input('radio_fuel', "value"),
        Input('dropdown_enduse', "value"),
        Input('radio_resolution', 'value')
    )
    def update_enduse_options(view_tab, fuel_type, current_enduse, resolution):
        elligible_cols = get_elligible_output_columns(view_tab, fuel_type)
        enduse = current_enduse if current_enduse in elligible_cols else elligible_cols[0]
        return sorted(elligible_cols), enduse

    @app.callback(
        Output("sync_upgrade", 'value'),
        Output("sync_upgrade", 'options'),
        Output("sync_upgrade", 'placeholder'),
        Input('dropdown_chng_type', "value"),
        State("sync_upgrade", "value"),
        State("sync_upgrade", "options")
    )
    def update_sync_upgrade(chng_type, current_sync_upgrade, sync_upgrade_options):
        # print(chng_type, current_sync_upgrade, sync_upgrade_options)
        if chng_type:
            return current_sync_upgrade, upgrade2name, 'respective upgrades. (Click to restrict to specific upgrade)'
        else:
            return '', {}, ' <select a change type on the left first>'

    @app.callback(
        Output('input_building', 'placeholder'),
        Input('input_building', 'options')
    )
    def update_building_placeholder(options):
        return f"{len(options)} Buidlings" if options else "0 buildings."

    @app.callback(
        Output('input_building2', 'placeholder'),
        Output('input_building2', 'value'),
        Input('input_building2', 'value'),
        Input('input_building2', 'options')
    )
    def update_building_placeholder2(value, options):
        return f"{len(options)} Buidlings" if options else "No restriction", None

    @app.callback(
        Output('input_building', 'value'),
        Output('input_building', 'options'),
        Output('report_upgrade', 'value'),
        Input('graph', "selectedData"),
        State('input_building', 'options'),
        State('report_upgrade', 'value')
    )
    def graph_click(selection_data, current_options, current_upgrade):
        if not selection_data or 'points' not in selection_data or len(selection_data['points']) < 1:
            raise PreventUpdate()

        selected_buildings = []
        selected_upgrades = []
        for point in selection_data['points']:
            if not (match := re.search(r"Building: (\d*)", point.get('hovertext', ''))):
                continue
            if bldg := match.groups()[0]:
                upgrade_match = re.search(r"Upgrade (\d*)", point.get('hovertext', ''))
                upgrade = upgrade_match.groups()[0] if upgrade_match else ''
                selected_buildings.append(bldg)
                selected_upgrades.append(upgrade)

        if not selected_buildings:
            raise PreventUpdate()

        selected_upgrade = selected_upgrades[0] or current_upgrade
        if len(selected_buildings) != 1:
            selected_buildings = list(set(selected_buildings))
            return '', selected_buildings, selected_upgrade
        current_options = current_options or selected_buildings
        return selected_buildings[0], current_options, selected_upgrade

    @app.callback(
        Output('chk-graph', 'value'),
        Output('chk-options', 'value'),
        Output('chk-enduses', 'value'),
        Output('input_building2', 'options'),
        Output("uirevision", "data"),
        Input('btn-reset', "n_clicks")
    )
    def reset(n_clicks):
        return [], [], [], [], n_clicks

    @app.callback(
        Output('input_building', 'options'),
        Input('btn-copy', "n_clicks"),
        State('input_building2', 'options'),
    )
    def copy(n_clicks, bldg_options2):
        return bldg_options2 or dash.no_update

    @app.callback(
        Output('input_building', 'value'),
        Output('input_building', 'options'),
        Output('input_building2', 'options'),
        State('input_building', 'value'),
        State('input_building', 'options'),
        State('input_building2', 'options'),
        Input('chk-lock', 'value'),
        Input('dropdown_chng_type', "value"),
        Input('sync_upgrade', 'value'),
        Input('report_upgrade', 'value'),
        Input('btn-reset', "n_clicks")
    )
    def bldg_selection(current_bldg, current_options, current_options2, chk_lock,
                       change_type, sync_upgrade, report_upgrade,
                       reset_click):

        if sync_upgrade and change_type:
            valid_bldgs = set(viz_data.chng2bldg[(int(sync_upgrade), change_type)])
        elif report_upgrade and change_type:
            valid_bldgs = set(viz_data.chng2bldg[(int(report_upgrade), change_type)])
            buildings = get_buildings(report_upgrade)
            valid_bldgs = set(buildings).intersection(valid_bldgs)
        elif report_upgrade:
            buildings = get_buildings(report_upgrade)
            valid_bldgs = set(buildings)
        else:
            valid_bldgs = set(viz_data.upgrade2res[0]['building_id'].to_list())

        base_res = upgrade2res[0].filter(pl.col("building_id").is_in(valid_bldgs))
        valid_bldgs = list(base_res['building_id'].to_list())

        if "btn-reset" != ctx.triggered_id and current_options and len(current_options) > 0 and chk_lock:
            current_options_set = set(current_options)
            valid_bldgs = [b for b in valid_bldgs if b in current_options_set]

        valid_bldgs2 = []

        if current_bldg and (int(current_bldg) not in valid_bldgs):
            current_bldg = valid_bldgs[0] if valid_bldgs else ''
        return current_bldg, valid_bldgs, valid_bldgs2

    def get_char_choices(char):
        if char:
            res0 = upgrade2res[0]
            unique_choices = sorted(list(res0[char].unique()))
            return unique_choices, unique_choices[0]
        else:
            return [], None

    def get_action_button_pairs(id, bldg_list_dict, report_type='opt'):
        buttons = []
        bldg_str = '' if len(bldg_list_dict[id]) > 10 else " [" + ','.join([str(b) for b in bldg_list_dict[id]]) + "]"
        for type in ['check', 'cross']:
            icon_name = "akar-icons:circle-check-fill" if type == 'check' else "gridicons:cross-circle"
            button = html.Div(dmc.ActionIcon(DashIconify(icon=icon_name,
                                                         width=20 if type == "check" else 22,
                                                         height=20 if type == "check" else 22,
                                                         ),
                                             id={'index': id, 'type': f'btn-{type}', 'report_type': report_type},
                                             variant="light"),
                              id=f"div-tooltip-target-{type}-{id}")
            if type == "check":
                tooltip = dbc.Tooltip(f"Select these buildings.{bldg_str}",
                                      target=f"div-tooltip-target-{type}-{id}", delay={'show': 1000})

                col = dbc.Col(html.Div([button, tooltip]),  width='auto', class_name="col-btn-cross",
                              style={"padding-left": "0px", "padding-right": "0px"})
            else:
                tooltip = dbc.Tooltip(f"Select all except these buildings.{bldg_str}",
                                      target=f"div-tooltip-target-{type}-{id}", delay={'show': 1000})
                col = dbc.Col(html.Div([button, tooltip]),  width='auto',  class_name="col-btn-check",
                              style={"padding-left": "0px", "padding-right": "0px"})
            buttons.append(col)
        return buttons

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "opt"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "opt"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State("chk-options", "value"),
        State("opt_report_store", "data"),
    )
    def opt_check_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_options, opt_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Options" in chk_options and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = opt_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "enduse"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "enduse"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State("chk-enduses", "value"),
        State("enduse_report_store", "data"),
    )
    def enduse_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_enduses, opt_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Enduses" in chk_enduses and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = opt_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output('status_bar', "children"),
        Output('input_building2', "options"),
        Input({"type": "btn-check", "index": ALL, "report_type": "char"}, "n_clicks"),
        Input({"type": "btn-cross", "index": ALL, "report_type": "char"}, "n_clicks"),
        State("input_building", "options"),
        State("input_building2", "options"),
        State('chk-chars', 'value'),
        State("char_report_store", "data"),
    )
    def char_report_button_click(check_clicks, cross_clicks, bldg_options, bldg_options2, chk_char, char_report):
        triggers = dash.callback_context.triggered_prop_ids
        if len(triggers) != 1:
            raise PreventUpdate()

        if "Chars" in chk_char and bldg_options2:
            bldg_list = [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(b) for b in bldg_options]

        trigger_val = next(iter(triggers.values()))
        buildings = char_report.get(trigger_val['index'], [])
        if trigger_val['type'] == 'btn-check':
            return '', [str(b) for b in buildings]

        bldg_set = set(buildings)
        except_buildings = [str(v) for v in bldg_list if int(v) not in bldg_set]
        return '', except_buildings

    @app.callback(
        Output("options_report_header", "children"),
        Output("options_report", 'children'),
        Output("opt_report_store", "data"),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        State('report_upgrade', 'value'),
        Input('chk-options', 'value'),
    )
    def show_opt_report(bldg_id, bldg_options, bldg_options2, report_upgrade, chk_options):
        if not report_upgrade or not bldg_options:
            return "Select an upgrade to see options applied in that upgrade", [''], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Options" not in chk_options:
            raise PreventUpdate()

        if "Options" in chk_options and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        run_obj = viz_data.run_obj(int(report_upgrade))
        applied_options = run_obj.report.get_applied_options(upgrade_id=int(report_upgrade),
                                                             bldg_ids=bldg_list,
                                                             include_base_opt=True)
        opt_only = [{entry.split('|')[0] for entry in opt.keys()} for opt in applied_options]
        reduced_set = list(reduce(set.union, opt_only))

        nested_dict = defaultdict(lambda: defaultdict(Counter))
        bldg_list_dict = defaultdict(list)

        for bldg_id, opt_dict in zip(bldg_list, applied_options):
            for opt_para, value in opt_dict.items():
                opt = opt_para.split('|')[0]
                para = opt_para.split('|')[1]
                nested_dict[opt][para][value] += 1
                bldg_list_dict[opt].append(bldg_id)
                bldg_list_dict[opt + "|" + para].append(bldg_id)
                bldg_list_dict[opt + "|" + para + "<-" + value].append(bldg_id)

        def get_accord_item(opt_name):
            total_count = sum(counter.total() for counter in nested_dict[opt_name].values())
            children = []
            for parameter, counter in nested_dict[opt_name].items():
                contents = []
                new_counter = Counter({"All": counter.total()})
                new_counter.update(counter)
                for from_val, count in new_counter.items():
                    if from_val == "All":
                        but_ids = f"{opt_name}|{parameter}"
                    else:
                        but_ids = f"{opt_name}|{parameter}<-{from_val}"
                    entry = dbc.Row([dbc.Col(f"<-{from_val} ({count})", width="auto"),
                                     *get_action_button_pairs(but_ids, bldg_list_dict)])
                    contents.append(entry)
                children.append(dmc.AccordionItem(contents, label=f"{parameter} ({counter.total()})"))

            accordian = dmc.Accordion(children, multiple=True)
            first_row = dbc.Row([dbc.Col(f"All ({total_count})", width="auto"),
                                 *get_action_button_pairs(opt_name, bldg_list_dict)])
            return dmc.AccordionItem([first_row, accordian], label=f"{opt_name} ({total_count})")
        if reduced_set:
            final_report = dmc.Accordion([get_accord_item(opt_name) for opt_name in reduced_set], multiple=True)
        else:
            final_report = ["No option got applied to the selected building(s)."]
        up_name = upgrade2name[int(report_upgrade)]
        return f"Options applied in {up_name}", final_report, dict(bldg_list_dict)

    @app.callback(
        Output("enduse_report", "children"),
        Output("enduse_report_store", "data"),
        State('report_upgrade', 'value'),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        Input('input_enduse_type', 'value'),
        Input('chk-enduses', 'value'),
    )
    def show_enduse_report(report_upgrade, bldg_id, bldg_options, bldg_options2, enduse_change_type, chk_enduse):
        if not report_upgrade or not bldg_options:
            return ["Select an upgrade to see enuse report."], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Enduses" not in chk_enduse:
            raise PreventUpdate()

        if "Enduses" in chk_enduse and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]

        # print(bldg_list)
        run_obj = viz_data.run_obj(int(report_upgrade))
        dict_changed_enduses = run_obj.report.get_enduses_buildings_map_by_change(upgrade_id=int(report_upgrade),
                                                                                  change_type=enduse_change_type,
                                                                                  bldg_list=bldg_list)
        # print(changed_enduses)

        all_changed_enduses = list(dict_changed_enduses.keys())
        if not all_changed_enduses:
            if bldg_id:
                return f'No enduse has {enduse_change_type} in building {bldg_id} ', {}
            else:
                return f'No enduse has {enduse_change_type} in any of the buildings', {}

        enduses2bldgs = defaultdict(list)
        for end_use, bldgs in dict_changed_enduses.items():
            if end_use in all_changed_enduses:
                enduses2bldgs[end_use].extend([int(bldg_id) for bldg_id in bldgs])

        fuel2bldgs = defaultdict(set)
        fuel2enduses = defaultdict(list)
        for enduse, bldgs in enduses2bldgs.items():
            for fuel in ['all_fuel'] + fuels_types:
                if fuel in enduse:
                    fuel2bldgs[fuel] |= set(bldgs)
                    fuel2enduses[fuel].append(enduse)
                    break
            else:
                fuel2bldgs['other'] |= set(bldgs)
                fuel2enduses['other'].append(enduse)

        for key, val in fuel2bldgs.items():
            fuel2bldgs[key] = list(val)

        enduses2bldgs.update(fuel2bldgs)

        def get_accord_item(fuel):
            total_count = len(enduses2bldgs[fuel])
            contents = [dbc.Row([dbc.Col(f"All {fuel} ({total_count})", width="auto"),
                                 *get_action_button_pairs(fuel, enduses2bldgs, "enduse")])]
            for enduse in fuel2enduses[fuel]:
                count = len(enduses2bldgs[enduse])
                row = dbc.Row([dbc.Col(f"{enduse} ({count})", width="auto"),
                               *get_action_button_pairs(enduse, enduses2bldgs, "enduse")])
                contents.append(row)
            return dmc.AccordionItem(contents, label=f"{fuel} ({total_count})")

        report = dmc.Accordion([get_accord_item(fuel) for fuel in fuel2enduses.keys()],  multiple=True)
        storedict = dict(enduses2bldgs)
        return report, storedict

    @app.callback(
        Output("char_report", "children"),
        Output("char_report_store", "data"),
        Input('input_building', "value"),
        Input('input_building', "options"),
        Input('input_building2', "options"),
        Input('drp-char-report', 'value'),
        Input('chk-chars', 'value'),
    )
    def show_char_report(bldg_id, bldg_options, bldg_options2, inp_char: list[str], chk_chars):
        if not (bldg_options or bldg_options2 or bldg_id):
            return [""], {}
        if not inp_char:
            return ["Select a characteristics to see its report."], {}

        if dash.callback_context.triggered_id == 'input_building2' and "Chars" not in chk_chars:
            raise PreventUpdate()

        if "Chars" in chk_chars and bldg_options2:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options2]
        else:
            bldg_list = [int(bldg_id)] if bldg_id else [int(b) for b in bldg_options]
        chars_df = viz_data.bs_res_df.filter(pl.col('building_id').is_in(
            set(bldg_list))).select(inp_char + ['building_id'])
        char2bldgs = chars_df.groupby(inp_char).agg('building_id')
        if (total_len := len(char2bldgs)) > 250:
            return [f"Sorry, this would create more than 200 ({total_len}) rows."], {}
        char_dict = {}
        total_count = 0
        contents = []
        for char_vals, group_df in chars_df.groupby(inp_char):
            bldglist = group_df['building_id'].to_list()
            but_ids = "+".join(char_vals) if isinstance(char_vals, tuple) else char_vals
            char_dict[but_ids] = [int(b) for b in bldglist]
            count = len(bldglist)
            total_count += count
            contents.append(dbc.Row([dbc.Col(f"{char_vals} ({count})", width="auto"),
                                     *get_action_button_pairs(but_ids, char_dict, "char")]))

        report = dmc.Accordion([dmc.AccordionItem(contents, label=f"{inp_char} ({total_count})")])
        return report, char_dict

    @app.callback(
        Output('graph', 'figure'),
        Output('graph', 'config'),
        Output('loader_label', "children"),
        State('tab_view_type', "value"),
        Input('drp-group-by', 'value'),
        Input('radio_fuel', "value"),
        Input('dropdown_enduse', "value"),
        Input('radio_graph_type', "value"),
        Input('radio_savings', "value"),
        Input('dropdown_chng_type', "value"),
        Input('sync_upgrade', 'value'),
        Input('input_building', 'value'),
        Input('input_building', 'options'),
        Input('input_building2', 'options'),
        Input('chk-graph', 'value'),
        State("uirevision", "data"),
        State('report_upgrade', 'value')
    )
    def update_figure(view_tab, grp_by, fuel, enduse, graph_type, savings_type, chng_type,
                      sync_upgrade, selected_bldg, bldg_options, bldg_options2, chk_graph, uirevision,
                      report_upgrade):
        nonlocal download_csv_df
        if dash.callback_context.triggered_id == 'input_building2' and "Graph" not in chk_graph:
            raise PreventUpdate()

        if "Graph" in chk_graph and bldg_options2:
            bldg_options = bldg_options2

        bldg_options = bldg_options or []
        if not enduse:
            full_name = []
        if view_tab == 'energy':
            full_name = viz_data.get_end_use_db_cols(resolution, fuel, enduse)
        else:
            full_name = [enduse]

        if selected_bldg:
            filter_bldg = [int(selected_bldg)]
        else:
            filter_bldg = [int(b) for b in bldg_options]

        new_figure, report_df = get_plot(full_name, graph_type, savings_type,
                                         chng_type, sync_upgrade, filter_bldg, grp_by, report_upgrade)

        uirevision = uirevision or "default"
        new_figure.update_layout(uirevision=uirevision)
        download_csv_df = report_df
        config = {'edits': {"titleText": True, "axisTitleText": True}, 'displayModeBar': True,
                  "modeBarButtonsToRemove": ["Zoom", "ZoomIn", "Pan", "ZoomOut", "AutoScale", "select2d"]}
        return new_figure, config, ""

    return app
def main()
Expand source code
def main():
    print("Welcome to Upgrades Visualizer.")
    yaml_path = inquirer.text(message="Please enter path to the buildstock configuration yml file: ",
                              default="").execute()
    opt_sat_path = inquirer.text(message="Please enter path to the options saturation csv file: ",
                                 default="").execute()
    workgroup = inquirer.text(message="Please Athena workgroup name: ",
                              default="rescore").execute()
    db_name = inquirer.text(message="Please enter database_name "
                            "(found in postprocessing:aws:athena in the buildstock configuration file): ",
                            default='').execute()
    table_name = inquirer.text(message="Please enter table name (same as output folder name; found under "
                               "output_directory in the buildstock configuration file). [Enter two names "
                               "separated by comma if baseline and upgrades are in different run] :",
                               default=""
                               ).execute()

    if ',' in table_name:
        table_name = table_name.split(',')
    app = _get_app(yaml_path=yaml_path,
                   opt_sat_path=opt_sat_path,
                   workgroup=workgroup,
                   db_name=db_name,
                   table_name=table_name)
    app.run_server(debug=False, port=8005)