Example: Solar Savings Estimator

This example builds a solar savings calculator that uses a radio group, a slider, and a chart driven by a hidden data sheet.

Download the example spreadsheet to follow along or upload directly to NExS.

What We’re Building

A homeowner’s solar estimator that:

  • Takes a monthly electric bill as a typed input
  • Lets the user pick how sunny their location is with a radio group
  • Sets the system size with a slider
  • Calculates system cost, yearly savings, payback period, and 25-year savings
  • Charts net savings year by year so the break-even point is easy to see

The workbook also shows a useful pattern: the visible sheet stays simple, while the assumptions and chart data live on separate sheets that users never see.

Step 1: Create the Excel Spreadsheet

The workbook has three worksheets plus NExS.app.

Calculator (the visible view)

B C D E F
2 Solar Savings Estimator
3 See what solar panels could save you.
4
5 Monthly electric bill $180
6
7 Sunshine where you live 0 Sunny
8 1 Average
9 0 Cloudy
10
11 System size (kW) (slider) 6.0
12
13
14 System cost =Assumptions!B18
15 Savings per year =Assumptions!B17
16 Payback (years) =IF(F15>0,F14/F15,“”)
17 Savings over 25 years =25*F15-F14
18
19–31 (chart area)

Notes on the layout:

  • B2:F2 and B3:F3 are merged for the title and subtitle.
  • C7:C9 hold the radio group. Exactly one cell is 1, and the rest are 0.
  • C11:E11 is merged to give the slider room. The slider writes its value into F11.
  • B19:F31 is left empty. The chart is drawn over these cells.

Assumptions (hidden from users)

All the constants and the intermediate math live here, so the Calculator sheet only shows inputs and results.

A B
1 Assumptions
3 Electricity rate ($/kWh) 0.18
4 Installed cost ($ per watt) 2.40
5 Fixed install cost 2000
6 System efficiency 80%
8 Peak sun hours per day
9 Sunny 5.5
10 Average 4.5
11 Cloudy 3.5
13 Calculations
14 Sun hours used =IF(Calculator!C7=1,B9,IF(Calculator!C9=1,B11,B10))
15 Yearly production (kWh) =Calculator!F11*B14*365*B6
16 Yearly usage (kWh) =Calculator!F5/B3*12
17 Yearly savings =MIN(B15,B16)*B3
18 System cost =B5+Calculator!F11*1000*B4

B14 turns the radio group into a single number: it uses the Sunny or Cloudy hours when that option is selected, and falls back to Average otherwise. B17 caps savings at the household’s actual usage, so oversizing the system doesn’t inflate the result.

Data (chart source)

A B
1 Savings year by year [Net savings ($)]
2 Year Net savings
3 1 =A3*Calculator!$F$15-Calculator!$F$14
4 2 =A4*Calculator!$F$15-Calculator!$F$14
… … …
27 25 =A27*Calculator!$F$15-Calculator!$F$14

Fill the formula in B3 down to B27. Each row is the cumulative savings minus the up-front cost, so the bars start negative and cross zero in the payback year.

Formatting

  • F5, F14, F15, F17: Currency, 0 decimals ($#,##0)
  • F11, F16: Number, 1 decimal (0.0)
  • C7:C9: Custom format "●";;"○", which shows a filled dot for 1 and an empty dot for 0, so the radio cells still look right in Excel
  • Assumptions B3:B4: Currency, 2 decimals. B6: Percentage
  • Data B3:B27: Currency, 0 decimals
  • B2, B17, and the result values: Bold

Step 2: Create the NExS.app Worksheet

Create a new worksheet named NExS.app:

A B C D
1 # NExS app configuration
2 app Solar Savings
3 view Calculator!B2:F31
4 name Main
5 input F5
6 radio C7:C9
7 slider
8 cell C11
9 linkedCell F11
10 min 2
11 max 12
12 step 0.5
13 chart B19:F31
14 source Data!A1:B27
15 type column
16 legend none
17 end

What each part does:

  • view: Only Calculator!B2:F31 is shown. The Assumptions and Data sheets still calculate, but users never see them.
  • input F5: The monthly bill is a typed input.
  • radio C7:C9: The three sunshine cells become one radio group. Selecting one sets it to 1 and the others to 0. See Radio Groups.
  • slider: Drawn in C11 and linked to F11, it ranges from 2 to 12 kW in 0.5 kW steps. F11 doesn’t need its own input line, because the slider’s linkedCell makes it editable. See Sliders.
  • chart: A column chart over B19:F31, fed from the Data sheet. With one data series a legend adds nothing, so it’s turned off. See Charts.
  • # rows are comments, and end marks the end of the configuration.

Tip: The NExS Excel Add-in generated this NExS.app sheet. You can build it by hand as shown above, or let the add-in write it for you.

Step 3: Upload and Test

  1. Save as .xlsx
  2. Upload to platform.nexs.com
  3. Drag the slider and watch the chart’s break-even bar move
  4. Switch between Sunny and Cloudy. A cloudier location produces less power, so payback takes longer

Step 4: Embed on Your Website

<!-- Replace this UUID with your own app's ID if building from scratch -->
<script class="nexs"
  src="https://static.nexs.com/js/nexs_embed.js"
  data-nexs-app-url="https://platform.nexs.com/app/17eadd5c-e49c-49a4-ba11-6db81add35b2">
</script>

Custom UI with Declarative API

Use the same workbook to drive your own interface. Every control is bound with the nexs__input class (see the Declarative API Reference):

  • Monthly bill: a text input bound to F5. A text input receives the cell’s raw value (180), so a short nexsappupdated listener replaces it with the formatted text ($180) whenever the app updates, unless the user is typing in it.
  • System size: a range input bound to F11, with the same min, max, and step as the slider in NExS.app. The kW label next to it is a nexs__output_formatted span on F11, so it always shows the value the app is using. The range input only sends its value when the user releases it, so a small tooltip above the thumb follows the input event to show the value while dragging.
  • Sunshine: three radio buttons with value="1", each bound to its own cell (C7, C8, C9). The workbook’s radio group spans three cells, and a declarative radio only sets its own cell to 1, so a short listener sets the other two cells to 0.

Try It

This custom page is running live against the published Solar Savings app. The NExS app itself is hidden, and only the custom controls are shown. To see the original spreadsheet view, open the Solar Savings app in a new window.

Solar Savings Estimator

Sunshine Where You Live
6.0 kW
Savings Over 25 Years
System cost:
Savings per year:
Payback: years

The Code

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Solar Savings Estimator</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      max-width: 440px;
      margin: 40px auto;
      padding: 20px;
    }
    h1 {
      color: #333;
      text-align: center;
    }
    .card {
      background: #f8f9fa;
      border-radius: 12px;
      padding: 24px;
      box-shadow: 0 2px 8px rgba(0,0,0,0.1);
    }
    .field {
      margin-bottom: 20px;
    }
    .field-label {
      display: block;
      font-weight: 600;
      margin-bottom: 8px;
      color: #555;
    }
    .field input[type="text"] {
      width: 100%;
      padding: 12px;
      font-size: 18px;
      border: 2px solid #ddd;
      border-radius: 8px;
      box-sizing: border-box;
    }
    .field input[type="range"] {
      width: 100%;
      margin: 0;
      accent-color: #f5a623;
    }
    /* Tooltip that follows the slider thumb while dragging */
    .slider-wrap {
      position: relative;
    }
    .slider-tip {
      position: absolute;
      bottom: calc(100% + 8px);
      /* --pct is 0 to 1; 16px is roughly the thumb width */
      left: calc(var(--pct, 0.4) * (100% - 16px) + 8px);
      transform: translateX(-50%);
      padding: 4px 8px;
      font-size: 13px;
      font-weight: 600;
      white-space: nowrap;
      color: white;
      background: #333;
      border-radius: 4px;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.15s;
    }
    .slider-tip::after {
      content: "";
      position: absolute;
      top: 100%;
      left: 50%;
      transform: translateX(-50%);
      border: 5px solid transparent;
      border-top-color: #333;
    }
    .slider-wrap:hover .slider-tip,
    .slider-wrap:focus-within .slider-tip {
      opacity: 1;
    }
    .sun-options {
      display: flex;
      gap: 10px;
    }
    /* Radio buttons styled as buttons: the input is hidden, the label is the button */
    .sun-option {
      flex: 1;
      position: relative;
      padding: 12px;
      font-size: 16px;
      text-align: center;
      border: 2px solid #ddd;
      border-radius: 8px;
      background: white;
      cursor: pointer;
    }
    .sun-option input {
      position: absolute;
      opacity: 0;
      pointer-events: none;
    }
    .sun-option:has(input:checked) {
      background: #f5a623;
      color: white;
      border-color: #f5a623;
    }
    .sun-option:has(input:focus-visible) {
      outline: 2px solid #333;
      outline-offset: 2px;
    }
    .results {
      background: #f5a623;
      color: white;
      border-radius: 8px;
      padding: 20px;
      text-align: center;
    }
    .results .label {
      font-size: 14px;
      opacity: 0.9;
    }
    .results .amount {
      font-size: 36px;
      font-weight: bold;
      margin: 5px 0;
    }
    .results .detail {
      font-size: 14px;
      opacity: 0.9;
      line-height: 1.6;
    }
  </style>
</head>
<body>

<h1>Solar Savings Estimator</h1>

<div class="card">
  <div class="field">
    <label class="field-label" for="bill">Monthly Electric Bill</label>
    <input type="text"
           id="bill"
           class="nexs__input"
           data-nexs-cell-addr="F5"
           placeholder="$180">
  </div>

  <div class="field">
    <span class="field-label">Sunshine Where You Live</span>
    <div class="sun-options">
      <label class="sun-option">
        <input type="radio" name="sun" class="nexs__input"
               data-nexs-cell-addr="C7" value="1"> Sunny
      </label>
      <label class="sun-option">
        <input type="radio" name="sun" class="nexs__input"
               data-nexs-cell-addr="C8" value="1" checked> Average
      </label>
      <label class="sun-option">
        <input type="radio" name="sun" class="nexs__input"
               data-nexs-cell-addr="C9" value="1"> Cloudy
      </label>
    </div>
  </div>

  <div class="field">
    <label class="field-label" for="size">
      System Size:
      <span class="nexs__output_formatted" data-nexs-cell-addr="F11">6.0</span> kW
    </label>
    <div class="slider-wrap">
      <input type="range"
             id="size"
             class="nexs__input"
             data-nexs-cell-addr="F11"
             min="2" max="12" step="0.5" value="6">
      <output class="slider-tip" for="size">6.0 kW</output>
    </div>
  </div>

  <div class="results">
    <div class="label">Savings Over 25 Years</div>
    <div class="amount">
      <span class="nexs__output_formatted" data-nexs-cell-addr="F17"></span>
    </div>
    <div class="detail">
      System cost: <span class="nexs__output_formatted" data-nexs-cell-addr="F14"></span><br>
      Savings per year: <span class="nexs__output_formatted" data-nexs-cell-addr="F15"></span><br>
      Payback: <span class="nexs__output_formatted" data-nexs-cell-addr="F16"></span> years
    </div>
  </div>
</div>

<!-- NExS app (handles calculations) -->
<!-- Replace this UUID with your own app's ID if building from scratch -->
<script class="nexs"
  src="https://static.nexs.com/js/nexs_embed.js"
  data-nexs-app-url="https://platform.nexs.com/app/17eadd5c-e49c-49a4-ba11-6db81add35b2">
</script>

<script>
  // Each radio sets its own cell to 1. Clear the other two so
  // exactly one sunshine cell is 1, as the workbook expects.
  const sunRadios = document.querySelectorAll('input[name="sun"]');
  sunRadios.forEach(radio => {
    radio.addEventListener('change', () => {
      const others = [...sunRadios].filter(r => r !== radio);
      NEXS.setInputs(others.flatMap(r => [r.dataset.nexsCellAddr, 0]));
    });
  });

  // The range input only updates F11 when released. Show the value
  // in a tooltip above the thumb while the user drags.
  const slider = document.getElementById('size');
  const tip = document.querySelector('.slider-tip');
  function updateTip() {
    tip.textContent = Number(slider.value).toFixed(1) + ' kW';
    tip.style.setProperty('--pct', (slider.value - slider.min) / (slider.max - slider.min));
  }
  slider.addEventListener('input', updateTip);
  updateTip();

  // Text inputs receive the cell's raw value (180). After each update,
  // show the formatted text instead ($180), except while the user is typing.
  document.addEventListener('nexsappupdated', () => {
    document.querySelectorAll('input[type="text"].nexs__input').forEach(input => {
      if (input !== document.activeElement) {
        input.value = NEXS.cell(input.dataset.nexsCellAddr).text.trim();
      }
    });
  });
</script>

</body>
</html>

To hide the spreadsheet view and show only your custom controls, as the demo above does, wrap the NExS script in <div style="display:none;">. See Hiding the NExS App.

Enhancements

Make the Electricity Rate an Input

Electricity prices vary a lot by region. Move the rate onto the Calculator sheet so users can enter their own:

B F
12 Electricity rate ($/kWh) 0.18

Then point Assumptions!B3 at it with =Calculator!F12, and add it to the view:

A B C
input F5, F12

Show a Payback Status Light

Add an LED in E16 that turns green when the system pays for itself within 10 years:

Formula in E16:

=IF(F16<=10,"green","red")

NExS.app:

A B C D
led
cell E16

What You Learned

  • Keeping assumptions and intermediate math on a separate sheet that isn’t part of the view
  • Turning a set of cells into a radio group, and reading the selection with IF
  • Using a custom number format so radio cells display as dots in Excel
  • Linking a slider to an input cell with min, max, and step
  • Drawing a chart over a cell range from a hidden data sheet
  • Binding a custom UI with nexs__input text, range, and radio inputs, and keeping a multi-cell radio group in sync with NEXS.setInputs

Next Steps