Esempio n. 1
0
def make_filter(
    get_uuid: Callable,
    tab: str,
    vtype: str,
    column_values: list,
    multi: bool = True,
    value: Union[str, float] = None,
    open_details: bool = False,
) -> wcc.Selectors:
    return html.Details(
        open=open_details,
        children=[
            html.Summary(vtype),
            wcc.Select(
                id={
                    "id": get_uuid("vitem-filter"),
                    "tab": tab,
                    "vtype": vtype,
                },
                options=[{"label": i, "value": i} for i in column_values],
                value=[value] if value is not None else column_values,
                multi=multi,
                size=min(15, len(column_values)),
            ),
        ],
    )
 def __init__(
     self,
     label: str = None,
     collapsible: bool = False,
     open_details: bool = True,
     wrapper_id: str = None,
     persistence: bool = True,
     persistence_type: str = "session",
     **kwargs: Any
 ) -> None:
     super().__init__()
     if wrapper_id is not None:
         self.id = wrapper_id
     if collapsible:
         children = [html.Summary(label)] if label else []
     else:
         children = [html.Label(label)] if label else []
     children.append(
         BaseSelect(
             persistence=persistence,
             persistence_type=persistence_type,
             **kwargs,
         )
     )
     if collapsible:
         self.children = html.Div(
             style={"fontSize": "15px"},
             children=html.Details(open=open_details, children=children),
         )
     else:
         self.children = html.Div(style={"fontSize": "15px"}, children=children)
Esempio n. 3
0
 def __init__(self,
              children=None,
              id=None,
              summary_id=None,
              title=None,
              **kwargs):
     if children is None:
         children = ["Loading..."]
     if id is None and isinstance(title, str):
         id = title
     if isinstance(title, str):
         title = H4(title,
                    style={
                        "display": "inline-block",
                        "verticalAlign": "middle"
                    })
     contents_id = f"{id}_contents" if id else None
     summary_id = summary_id or f"{id}_summary"
     kwargs["style"] = {"marginBottom": "1rem"}
     super().__init__(
         [
             html.Summary(title, id=summary_id),
             html.Div(
                 children,
                 id=contents_id,
                 style={
                     "marginTop": "0.5rem",
                     "marginLeft": "1.1rem"
                 },
             ),
         ],
         id=id,
         **kwargs,
     )
 def __init__(self,
              children: Any,
              open_details: bool = True,
              label: str = "",
              **kwargs: Any) -> None:
     super().__init__(**kwargs)
     self.open = open_details
     self.children = [
         html.Summary(children=label, className="webviz-header")
     ] + children
 def __init__(self,
              children: Any,
              open_details: bool = True,
              label: str = "",
              className: str = "",
              **kwargs: Any) -> None:
     super().__init__()
     self.className = className + "webviz-selectors"
     self.open = open_details
     if "id" in kwargs:
         self.id = kwargs["id"]
     self.children = [
         html.Summary(children=label, className="webviz-underlined-label"),
         html.Div(style={"padding": "10px"}, children=children),
     ]
Esempio n. 6
0
 def selector_dropdowns(self) -> List[html.Div]:
     """Makes dropdowns for each selector.
     Args:
         dframe - Volumetrics Dataframe
         selectors - List of selector columns
     Return:
         dcc.Dropdown objects
     """
     dropdowns: List[html.Div] = []
     for selector in self.selectors:
         elements = list(self.volumes[selector].unique())
         if selector in ["ENSEMBLE", "SOURCE"]:
             value = elements[0]
         else:
             value = elements
         dropdowns.append(
             html.Div(children=[
                 html.Details(
                     open=True,
                     children=[
                         html.Summary(selector.lower().capitalize()),
                         wcc.Select(
                             id=self.selectors_id[selector],
                             options=[{
                                 "label": i,
                                 "value": i
                             } for i in elements],
                             value=value,
                             multi=True,
                             size=min(20, len(elements)),
                             persistence=True,
                             persistence_type="session",
                         ),
                     ],
                 )
             ]))
     return dropdowns
Esempio n. 7
0
def html_details(
    summary: str,
    children: list,
    open_details: bool = False,
) -> html.Div:
    return html.Div(
        html.Details(
            style={"margin-bottom": "25px"},
            open=open_details,
            children=[
                html.Summary(
                    children=summary,
                    style={
                        "color": "#ff1243",
                        "border-bottom-style": "solid",
                        "border-width": "thin",
                        "border-color": "#ff1243",
                        "font-weight": "bold",
                        "font-size": "20px",
                        "margin-bottom": "15px",
                    },
                )
            ] + children,
        ))
Esempio n. 8
0
 def filter_layout(self) -> Optional[list]:
     """Makes dropdowns for each dataframe column used for filtering."""
     if not self.use_filter:
         return None
     df = self.data
     dropdowns = [html.H4("Set filters")]
     for col in self.filter_cols:
         if df[col].dtype in [np.float64, np.int64]:
             min_val = df[col].min()
             max_val = df[col].max()
             mean_val = df[col].mean()
             dropdowns.append(
                 html.Div(
                     children=[
                         html.Details(
                             open=True,
                             children=[
                                 html.Summary(col.lower().capitalize()),
                                 dcc.RangeSlider(
                                     id=self.uuid(f"filter-{col}"),
                                     min=min_val,
                                     max=max_val,
                                     step=(max_val - min_val) / 10,
                                     marks={
                                         min_val: f"{min_val:.2f}",
                                         mean_val: f"{mean_val:.2f}",
                                         max_val: f"{max_val:.2f}",
                                     },
                                     value=[min_val, max_val],
                                 ),
                             ],
                         )
                     ]
                 )
             )
         else:
             elements = list(self.data[col].unique())
             dropdowns.append(
                 html.Div(
                     children=[
                         html.Details(
                             open=True,
                             children=[
                                 html.Summary(col.lower().capitalize()),
                                 wcc.Select(
                                     id=self.uuid(f"filter-{col}"),
                                     options=[
                                         {"label": i, "value": i} for i in elements
                                     ],
                                     value=elements
                                     if self.filter_defaults is None
                                     else [
                                         element
                                         for element in self.filter_defaults.get(
                                             col, elements
                                         )
                                         if element in elements
                                     ],
                                     size=min(15, len(elements)),
                                 ),
                             ],
                         )
                     ]
                 )
             )
     return dropdowns
def create_results(input_values):
    """Returns a list of Dash components/elements that display
    the restults of the model run.  This list is suitable for
    use as the 'children' of a Div displaying the results.
    'input_values' are a list of the values of all of the Inputs that 
    feed the model calculations.  This comes from a callback. 
    """
    _, inputs, _ = ui_helper.inputs_to_vars(input_values)

    # Run the model
    mod = hp_model.HP_model(**inputs)
    mod.run()

    # Pull out the results from the model object.
    smy = mod.summary
    df_cash_flow = mod.df_cash_flow
    df_mo_en_base = mod.df_mo_en_base
    df_mo_en_hp = mod.df_mo_en_hp
    df_mo_dol_base = mod.df_mo_dol_base
    df_mo_dol_hp = mod.df_mo_dol_hp

    # Lots of special formatting for electric heat so make a variable
    # indicating whether electric heat is being analyzed.
    is_electric = (mod.exist_heat_fuel_id == ui_helper.ELECTRIC_ID)

    # This will be the list of children that is returned.  For each graph
    # or Markdown block, and item is added to this list.
    comps = []

    # ------- Summary of Economic Return and Greenhouse Gas  ------------

    # Add some items and adjust some in the summary dictionary
    smy['npv_abs'] = abs(smy['npv'])
    smy['npv_fmt'] = '${:,.0f}'.format(smy['npv']).replace(
        '$-', '-$')  # formatted version of NPV
    smy['irr'] *= 100.  # convert to %
    smy['fuel_savings'] = -smy['fuel_use_chg']
    smy['npv_indicator'] = 'earn' if smy['npv'] >= 0 else 'lose'
    smy['co2_lbs_saved_life'] = smy['co2_lbs_saved'] * inputs['hp_life']
    smy['co2_driving_miles_saved_life'] = smy[
        'co2_driving_miles_saved'] * inputs['hp_life']
    smy['hp_load_frac'] *= 100.  # convert to %

    if smy['max_hp_reached']:
        smy['max_hp_str'] = '*did* deliver its maximum output capacity at some'
    else:
        smy['max_hp_str'] = '*did not* need to deliver its maximum output capacity at any'

    if np.isnan(smy['irr']):
        md_tmpl = dedent('''
        #### Rate of Return:  Not Available

        The heat pump does not save enough money to allow for a calculation of the
        rate of return (or the installed cost input is zero or less).
        ''')
    else:
        md_tmpl = dedent('''
        #### Rate of Return:  **{irr:.1f}%**

        The rate of return on the investment is estimated to be **{irr:.1f}%**. 
        Compare this *tax-free* return to the rate of return or interest
        of your other investment options.  Usually a value greater than 4%
        indicates a cost-effective investment. 
        ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    if inputs['pct_financed'] > 0:
        md = dedent('''
        Note that this rate of return is a return calculated on the amount that
        you contribute as a down-payment for the loan.
        ''')
        comps.append(dcc.Markdown(md))

    md_tmpl = dedent('''
    #### Net Present Value:  **{npv_fmt}**

    The Net Present Value of installing an air-source heat pump is estimated to 
    be **{npv_fmt}**.
    ''')
    if smy['npv'] > 0:
        md_tmpl += dedent('''
        This means that over the life of the equipment you 
        will earn a total of **\${npv_abs:,.0f}** in today's dollars beyond your
        initial investment accounting for interest.  Any value greater than zero
        indicates a cost-effective investment.
        ''')
    else:
        md_tmpl += dedent('''
        This means that over the life of the equipment you were
        **\${npv_abs:,.0f}** short of paying back your intitial investment
        with interest.
        ''')
    md_tmpl += dedent('''
    You can change the default heat pump life of 14 years in the Advanced 
    Economic Inputs section.
    If you are trying out different heat pumps or different operating procedures
    for the heat pump, you should try to pick the combination that maximizes this
    net present value figure.
    Note that this only includes economic costs and benefits and does not include any
    environmental or social benefits of the heat pump.
    ''')

    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    if not is_electric:
        # formatted fuel savings
        if smy['fuel_savings'] < 50:
            smy['fuel_savings_fmt'] = '{fuel_savings:.2f}'.format(**smy)
        else:
            smy['fuel_savings_fmt'] = '{fuel_savings:,.0f}'.format(**smy)

        md_tmpl = dedent('''
        #### Annual Heating Fuel Savings: **{fuel_savings_fmt} {fuel_unit}** of {fuel_desc}

        This shows how much heating fuel is saved each year by use of the heat pump. The heat pump
        achieves these savings by **serving {hp_load_frac:.0f}%** of the building's space heating
        load.  The amount of load served by the heat pump is affected by:
        
        * your choices concerning lower temperatures in the bedrooms and
        whether doors are open to those rooms,
        * the Outdoor Temperature cutoff below which the heat pump doesn't run,
        * the maximum heating capacity available for the heat pump selected.
        ''')
        comps.append(dcc.Markdown(md_tmpl.format(**smy)))

        md_tmpl = dedent('''
        #### Annual Increase in Electricity Use: **{elec_use_chg:,.0f} kWh**

        Use of the heat pump adds to the electric use of the building.  Shown here is 
        the annual increase in electricity use.
        ''')
        comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    else:
        # Electric Heat.  Heat Pump saves electricity.
        md = dedent(f'''
        #### Annual Savings in Electricity Use: **{-smy["elec_use_chg"]:,.0f} kWh**

        Because the heat pump is more efficient than conventional electric heat, the 
        electricity use of the buildings is reduced by this amount per year.
        ''')
        comps.append(dcc.Markdown(md))

    md_tmpl = dedent('''
        #### Heat Pump Sizing

        This energy model shows that the heat pump {max_hp_str} point during the year.
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    if smy['max_hp_reached']:
        txt = dedent('''
        You might try larger heat pump sizes in the model to see if the Net Present 
        Value benefit of the heat pump increases.
        ''')
    else:
        txt = dedent('''
        You might try smaller heat pump sizes in the model to see if the Net Present 
        Value benefit of the heat pump increases.
        ''')
    comps.append(dcc.Markdown(txt))

    md_tmpl = dedent('''
    #### Seasonal Average Heat Pump COP: **{cop:.1f}**

    The Seasonal Average Heat Pump COP indicates the annual average efficiency of the heat pump.
    Conventional Electric Resistance heat (e.g. electric baseboard) would have a COP of 1.0 (100%).
    Heat Pumps generally have COPs in excess of 2.0 (200%).
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    if not is_electric:
        md_tmpl = dedent('''
        #### Electricity and Fuel Prices

        The average cost for the *additional* electricity needed for the heat pump is
        **${elec_rate_incremental:.4f}/kWh**.  This accounts for any block rates and 
        PCE (Rural Power Cost Assistance) limits that may be present. The fuel price for 
        the fuel saved is **${fuel_price_incremental:.4g}/{fuel_unit}**.
        These values include sales taxes.
        ''')
    else:
        md_tmpl = dedent('''
        #### Electricity Price

        The average rate for the electricity that is saved is **${elec_rate_incremental:.4f}/kWh**. 
        This accounts for any block rates and PCE (Rural Power Cost Assistance) limits that may be present.
        ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    md_tmpl = dedent('''
    #### Greenhouse Gas Emissions

    Installing a heat pump is predicted to save **{co2_lbs_saved:,.0f} pounds of CO2** emissions annually, 
    or {co2_lbs_saved_life:,.0f} pounds over the life of the equipment. This is equivalent to a reduction 
    of **{co2_driving_miles_saved:,.0f} miles driven** by an average passenger vehicle annually, 
    or {co2_driving_miles_saved_life:,.0f} miles over the equipment's life.

    ---
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    # Gather the cash flow results components to display in an Advanced Details
    # components.
    cash_comps = []

    cash_comps.append(
        dcc.Markdown(
            dedent('''
    ### Cash Flow Results

    The graph below shows how the heat pump project impacts cash flow in each of the years
    during the life of the heat pump.  Negative, red, values indicate a net outflow of cash,
    and positive, black, values indicate an net inflow of
    cash.  Sales taxes are included where applicable.
    # ''')))

    # Cash Flow Graph
    df_cash_flow['negative_flow'] = np.where(df_cash_flow.cash_flow < 0,
                                             df_cash_flow.cash_flow, 0)
    df_cash_flow['positive_flow'] = np.where(df_cash_flow.cash_flow > 0,
                                             df_cash_flow.cash_flow, 0)

    negative_flow = go.Bar(
        x=df_cash_flow.index,
        y=df_cash_flow.negative_flow,
        name='Cash Outflow',
        marker=dict(color='#d7191c'),
        hoverinfo='y',
    )

    positive_flow = go.Bar(
        x=df_cash_flow.index,
        y=df_cash_flow.positive_flow,
        name='Cash Inflow',
        marker=dict(color='#000000'),
        hoverinfo='y',
    )

    layout = go.Layout(
        title='Heat Pump Cash Flow',
        xaxis=dict(title='Year', fixedrange=True),
        yaxis=dict(
            title='Annual Cash Flow ($)',
            hoverformat='$,.0f',
            fixedrange=True,
            tickformat='$,.0f',
        ),
        hovermode='closest',
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=[negative_flow, positive_flow],
            layout=layout,
        ),
        config=my_config,
        id='gph-1',
    )
    cash_comps.append(gph)

    # Cumulative Cash Flow Graph

    cash_comps.append(
        dcc.Markdown(
            dedent('''
    The graph below shows the running total of cash flow over the life of the heat pump.
    If the total cash flow exceeds zero (turns black), your portion of the heat pump investment 
    has paid itself back with interest. (The graph technically shows cumulative, discounted cash flow).
    ''')))

    df_cash_flow['cum_negative_flow'] = np.where(
        df_cash_flow.cum_disc_cash_flow < 0, df_cash_flow.cum_disc_cash_flow,
        0)
    df_cash_flow['cum_positive_flow'] = np.where(
        df_cash_flow.cum_disc_cash_flow > 0, df_cash_flow.cum_disc_cash_flow,
        0)

    negative_cash_flow = go.Scatter(
        x=df_cash_flow.index,
        y=df_cash_flow.cum_negative_flow,
        name='Cash Flow ($)',
        fill='tozeroy',
        fillcolor='#d7191c',
        line=dict(color='#ffffff'),
        hoverinfo='y',
        mode='lines',
    )

    positive_cash_flow = go.Scatter(
        x=df_cash_flow.index,
        y=df_cash_flow.cum_positive_flow,
        name='Cash Flow ($)',
        fill='tozeroy',
        fillcolor='#000000',
        line=dict(color='#ffffff'),
        hoverinfo='y',
        mode='lines',
    )

    layout = go.Layout(
        title='Cumulative Cash Flow',
        xaxis=dict(
            title='Year',
            fixedrange=True,
        ),
        yaxis=dict(
            title='Annual Discounted Cash Flow ($)',
            hoverformat='$,.0f',
            fixedrange=True,
            tickformat='$,.0f',
        ),
        hovermode='closest',
        showlegend=False,
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=[negative_cash_flow, positive_cash_flow],
            layout=layout,
        ),
        config=my_config,
        id='gph-2',
    )
    cash_comps.append(gph)

    # Cash Flow Table

    cols = [
        ('initial_cost', 'Initial Cost'),
        ('loan_cost', 'Loan Payments'),
        ('op_cost', 'Operating Cost'),
    ]
    if not is_electric:
        cols += [('fuel_cost', 'Heating Fuel Cost')]
    cols += [('elec_cost', 'Electricity Cost'), ('cash_flow', 'Net Cash Flow'),
             ('cum_disc_cash_flow', 'Cumulative Discounted Cash Flow')]
    old_cols, new_cols = zip(*cols)
    dfc = df_cash_flow[list(old_cols)].copy()
    dfc.columns = new_cols
    dfc.index.name = 'Year'
    cash_comps.append(
        dcc.Markdown(
            dedent('''
        The table below breakdowns the cash flow impacts into categories.  All values are dollars.
        Positive numbers indicate a beneficial impact (inflow of cash); negative values indicate
        a detrimental impact (outflow of cash).
        ''')))
    cash_comps.append(generate_table(dfc))

    cash_results = html.Details(
        style={'marginTop': '2em'},
        children=[
            html.Summary('Click Here for Year-by-Year Cash Flow Information'),
            html.Div(style={'marginTop': '3rem'}, children=cash_comps)
        ])
    comps.append(cash_results)

    comps.append(dcc.Markdown('### Results by Month'))

    md_tmpl = dedent('''
    ##### Monthly Space Heating Load

    This graph shows how the space heating load of the building varies
    across the months, and it shows what portion of that load is served by
    the heat pump versus the existing heating system.  The units are total 
    MMBtu of heating load placed
    on the building's heating system.  Not all of this load may be served
    by the heat pump, due to heat distribution, low-temperature cut-off,
    and capacity limitations of
    the heat pump. This figure does *not* include Domestic Hot Water or any
    other uses of the fuel.
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    hp_load = go.Bar(
        x=df_mo_en_hp.index,
        y=df_mo_en_hp.hp_load_mmbtu,
        name='Heat Pump Load',
        hoverinfo='y',
    )

    exist_load = go.Bar(
        x=df_mo_en_hp.index,
        y=df_mo_en_hp.secondary_load_mmbtu,
        name='Load on Existing System',
        hoverinfo='y',
    )

    layout = go.Layout(
        title='Monthly Space Heating Load',
        xaxis=dict(
            title='Month',
            fixedrange=True,
        ),
        yaxis=dict(
            title='Estimated Space Heating Load, MMBtu',
            hoverformat=',.1f',
            fixedrange=True,
        ),
        barmode='stack',
        hovermode='closest',
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=[hp_load, exist_load],
            layout=layout,
        ),
        config=my_config,
        id='gph-3',
    )
    comps.append(gph)

    md_tmpl = dedent('''
    ##### Monthly Energy Cost Impacts

    This graph shows how the building's monthly energy costs in the first year change due to
    the heat pump.  Both electricity costs and fuel costs are included.
    The dots show the current level of energy cost prior to
    installing the heat pump.  If the heat pump lowers energy cost in the month,
    a green bar drops from the dot down to the new level of energy cost for the
    month.  If the heat pump raises costs in the month (e.g. the added electricity
    cost is more than the fuel cost savings), a red bar extends from the current
    cost dot to the new, higher, energy cost level.  It could be more economical to
    shut off the heat pump in these months, although red bars (cost increases) can
    turn into green bars (savings) in future years if heating fuel costs are projected
    to increase in price faster than electricity (see Advanced Economic Inputs for price
    escalation inputs).  This graph shows monthly energy costs in the *first year* after 
    installing the heat pump.
    
    All energy uses are included in the costs, not just space heating.
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    # calculate the change in dollars between the base scenario and the heat
    # pump scenario.
    df_mo_dol_chg = df_mo_dol_hp - df_mo_dol_base

    df_mo_dol_chg['cost_savings'] = np.where(df_mo_dol_chg.total_dol < 0.0,
                                             -df_mo_dol_chg.total_dol, 0.0)

    # Note: we make these negative values so bars extend downwards
    df_mo_dol_chg['cost_increases'] = np.where(df_mo_dol_chg.total_dol >= 0.0,
                                               -df_mo_dol_chg.total_dol, 0.0)

    hp_cost = go.Bar(
        x=df_mo_dol_hp.index,
        y=df_mo_dol_hp.total_dol,
        name='',
        marker=dict(color='#377eb8'),
        hoverinfo='y',
    )

    cost_savings = go.Bar(
        x=df_mo_dol_chg.index,
        y=df_mo_dol_chg.cost_savings,
        name='Cost Savings',
        marker=dict(color='#4daf4a'),
        hoverinfo='y',
    )

    cost_increases = go.Bar(
        x=df_mo_dol_chg.index,
        y=df_mo_dol_chg.cost_increases,
        name='Cost Increases',
        marker=dict(color='#e41a1c'),
        hoverinfo='y',
    )

    no_hp_costs = go.Scatter(
        x=df_mo_dol_base.index,
        y=df_mo_dol_base.total_dol,
        name='Baseline Energy Costs',
        mode='markers',
        marker=dict(color='#000000', size=12),
        hoverinfo='y',
    )

    layout = go.Layout(
        title='Energy Costs: Heat Pump vs. Baseline',
        xaxis=dict(
            title='Month',
            fixedrange=True,
        ),
        yaxis=dict(
            title='Total Energy Costs',
            hoverformat='$,.0f',
            fixedrange=True,
            tickformat='$,.0f',
        ),
        barmode='stack',
        hovermode='closest',
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=[hp_cost, cost_savings, cost_increases, no_hp_costs],
            layout=layout,
        ),
        config=my_config,
        id='gph-4',
    )
    comps.append(gph)

    # These components will be part of the more detailed monthly components
    # presented.
    monthly_comps = []

    if not is_electric:
        md_tmpl = dedent('''
        ##### Monthly Electricity and Fuel, Before/After

        This graph shows electricity use and fuel use before and after installation of the heat pump.
        This is total electricity and fuel use, including energy uses beyond just space heating.
        ''')
        monthly_comps.append(dcc.Markdown(md_tmpl.format(**smy)))

        elec_no_hp = go.Scatter(
            x=df_mo_dol_base.index,
            y=df_mo_dol_base.elec_kwh,
            name='Monthly kWh (no Heat Pump)',
            line=dict(color='#92c5de', width=2, dash='dash'),
            hoverinfo='y',
        )

        elec_w_hp = go.Scatter(
            x=df_mo_dol_hp.index,
            y=df_mo_dol_hp.elec_kwh,
            name='Monthly kWh (with Heat Pump)',
            mode='lines+markers',
            marker=dict(color='#0571b0'),
            hoverinfo='y',
        )

        fuel_no_hp = go.Scatter(
            x=df_mo_dol_base.index,
            y=df_mo_dol_base.secondary_fuel_units,
            name='Monthly Fuel Usage (no Heat Pump)',
            line=dict(
                color='#f4a582',
                width=2,
                dash='dash',
            ),
            hoverinfo='y',
        )

        fuel_w_hp = go.Scatter(
            x=df_mo_dol_hp.index,
            y=df_mo_dol_hp.secondary_fuel_units,
            name='Monthly Fuel Usage (with Heat Pump)',
            mode='lines+markers',
            marker=dict(color='#ca0020'),
            hoverinfo='y',
        )

        fig = subplots.make_subplots(rows=2, cols=1)

        fig.append_trace(elec_no_hp, 1, 1)
        fig.append_trace(elec_w_hp, 1, 1)
        fig.append_trace(fuel_no_hp, 2, 1)
        fig.append_trace(fuel_w_hp, 2, 1)

        fig['layout'].update(title='Energy Usage: Heat Pump vs. Baseline')

        fig['layout']['xaxis1'].update(title='Month', fixedrange=True)
        fig['layout']['xaxis2'].update(title='Month', fixedrange=True)
        fig['layout']['yaxis1'].update(
            title='Electricity Use (kWh)',
            hoverformat=',.0f',
            fixedrange=True,
        )
        yaxis2_title = 'Fuel Use (%s)' % (smy['fuel_unit'])
        fig['layout']['yaxis2'].update(
            title=yaxis2_title,
            hoverformat='.3g',
            fixedrange=True,
        )
        fig['layout']['hovermode'] = 'closest'

        gph = dcc.Graph(
            figure=fig,
            config=my_config,
            id='gph-5',
        )

    else:
        # Electric Heat Case
        md_tmpl = dedent('''
        ##### Monthly Electricity Use Before and After

        This graph shows electricity use before and after installation of the heat pump.
        This is total electricity use, including all uses of electricity, not just space heating.
        ''')
        comps.append(dcc.Markdown(md_tmpl.format(**smy)))

        elec_no_hp = go.Scatter(
            x=df_mo_dol_base.index,
            y=df_mo_dol_base.elec_kwh,
            name='Monthly kWh (no Heat Pump)',
            line=dict(color='#92c5de', width=2, dash='dash'),
            hoverinfo='y',
        )

        elec_w_hp = go.Scatter(
            x=df_mo_dol_hp.index,
            y=df_mo_dol_hp.elec_kwh,
            name='Monthly kWh (with Heat Pump)',
            mode='lines+markers',
            marker=dict(color='#0571b0'),
            hoverinfo='y',
        )
        layout = go.Layout(
            title='Monthly Electricity Use, Before/After',
            xaxis=dict(
                title='Month',
                fixedrange=True,
            ),
            yaxis=dict(
                title='Electricity (kWh)',
                hoverformat=',.0f',
                fixedrange=True,
            ),
            hovermode='closest',
        )

        gph = dcc.Graph(
            figure=go.Figure(
                data=[elec_no_hp, elec_w_hp],
                layout=layout,
            ),
            config=my_config,
            id='gph-5',
        )

    monthly_comps.append(gph)

    md_tmpl = dedent('''
    ##### Monthly Heat Pump Efficiency

    This graph shows the efficiency of the heat pump in each month.  Heat Pump
    efficiency is measured as "COP"(Coefficient of Performance).  A COP of 2.5
    means 250% efficient, as compared to electric resistance heat, which is 100%
    efficient.  The heat pump's efficiency improves as the temperature outside
    warms.
    ''')
    monthly_comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    efficiency = [
        go.Scatter(
            x=df_mo_en_hp.index,
            y=df_mo_en_hp.cop,
            name='COP',
            mode='lines+markers',
            hoverinfo='y',
        )
    ]

    layout = go.Layout(
        title='Monthly Heat Pump Efficiency, COP',
        xaxis=dict(
            title='Month',
            fixedrange=True,
        ),
        yaxis=dict(
            title='COP',
            hoverformat=',.2f',
            fixedrange=True,
        ),
        hovermode='closest',
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=efficiency,
            layout=layout,
        ),
        config=my_config,
        id='gph-6',
    )
    monthly_comps.append(gph)

    md_tmpl = dedent('''
    ##### Monthly Change in Electricity Peak Demand

    This graph shows how much the peak electricity demand in each month is affected
    by the heat pump.  The heat pump will normally increase the peak demand, except
    when the heat pump is used to avoid conventional electric heat; in that case peak
    demand will decrease and values in the graph below will be negative. Units are kilowatts.
    ''')
    monthly_comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    peak_demand = [
        go.Scatter(
            x=df_mo_en_hp.index,
            y=(df_mo_en_hp.total_kw - df_mo_en_base.total_kw),
            name='Peak Demand',
            mode='lines+markers',
            hoverinfo='y',
        )
    ]

    layout = go.Layout(
        title='Change in Electricity Peak Demand, kW',
        xaxis=dict(
            title='Month',
            fixedrange=True,
        ),
        yaxis=dict(
            title='Peak Demand Change, kW',
            hoverformat=',.2f',
            fixedrange=True,
        ),
        hovermode='closest',
    )

    gph = dcc.Graph(
        figure=go.Figure(
            data=peak_demand,
            layout=layout,
        ),
        config=my_config,
        id='gph-7',
    )
    monthly_comps.append(gph)

    monthly_results = html.Details(
        style={
            'marginTop': '2em',
            'marginBottom': '2em'
        },
        children=[
            html.Summary('Click Here for More Detailed Monthly Information'),
            html.Div(style={'marginTop': '3rem'}, children=monthly_comps)
        ])
    comps.append(monthly_results)

    # Design Heat Load Info

    md_tmpl = dedent('''
    ##### Design Heating Load Information

    An approximate estimate of the design space heating load of this building is
    **{design_heat_load:,.0f} Btu/hour**, not including any domestic hot water load.
    This was based on a Design Outdoor Temperature of **{design_heat_temp:.1f} °F**; 
    approximately 1% of the hours in the year (88 hours) will be colder than this 
    temperature. The design heating load figure does *not* include any safety margin
    and is measured at the output of the heating system.

    The heat pump is estimated to have an output of **{hp_max_capacity_5F:,.0f} Btu/hour at a 5 °F**
    outdoor temperature.  The heat pump will have different maximum output capacities at other
    outdoor temperatures, as the heat pump's efficiency varies with outdoor temperature.
    This energy model shows that the heat pump {max_hp_str} point during the year.
    ''')
    comps.append(dcc.Markdown(md_tmpl.format(**smy)))

    comps.append(html.Hr())
    comps.append(html.P(f'File: {mod.file_name}'))

    # Debug information
    debug = html.Details(style={},
                         children=[
                             html.Summary('Click Here for Debug Output'),
                             html.Div(style={'marginTop': '3rem'},
                                      children=[
                                          dcc.Markdown(f"```\n{mod}\n```"),
                                      ])
                         ])

    comps.append(debug)

    return comps