DEV Community

Jeremiah Say
Jeremiah Say

Posted on

How to build a grid emission factor lookup in Vanilla JS using IEA 2026 regional data

Most Scope 2 carbon calculators I've reviewed do one of two things: hardcode a single global average factor and call it a day, or reach for a third-party API that charges per lookup and introduces a network dependency into a calculation that should be instant and deterministic.

Neither is acceptable if you're building tools that practitioners will use to file regulatory disclosures.

Here's how I structure the grid emission factor lookup at GreenCalculus.com, using a flat data object sourced from IEA 2026, DEFRA 2025, and EPA eGRID 2024 — with zero runtime dependencies.


Why grid factors are harder than they look

The GHG Protocol Scope 2 Guidance requires companies to report two methods wherever data is available:

  • Location-based: average grid emission intensity for the country or subregion where electricity is consumed (kWh × grid factor = kgCO₂e)
  • Market-based: factor from a specific electricity contract, renewable energy certificate (EAC/REGO), or supplier disclosure

Location-based is what you can build a deterministic lookup for. Market-based requires data the user must supply. A well-built calculator handles both paths, makes the distinction visible, and never conflates them.

Second problem: a single national average is often wrong. The US national average from EPA eGRID is 0.386 kg CO₂e/kWh. But a data centre in Upstate New York (hydro + nuclear dominated) sits at 0.125 kg CO₂e/kWh — nearly a 3× difference. Using the national average here would overstate Scope 2 by 200%. For CSRD filers, that's a material error.


The data structure

I maintain all factors in a single PHP object (the "Master Brain") that gets injected into window.gcMasterBrain server-side. Every calculator reads from window.gcMasterBrain.grid[countryCode].factor. No hardcoded numbers anywhere in calculator JS.

Here's the shape of each entry:

// window.gcMasterBrain.grid (subset shown)
const gridFactors = {

  // Europe
  GB: { factor: 0.177, unit: "kg CO2e per kWh", source: "DEFRA_2025", year: 2025, name: "United Kingdom", note: "DEFRA 2025. -15% vs 2024." },
  DE: { factor: 0.364, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Germany" },
  FR: { factor: 0.052, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "France",    note: "~70% nuclear" },
  PL: { factor: 0.635, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Poland",    note: "Coal-heavy mix" },
  SE: { factor: 0.013, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Sweden",    note: "Hydro + nuclear" },
  NO: { factor: 0.009, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Norway",    note: "~99% hydropower" },

  // Asia-Pacific
  IN: { factor: 0.708, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "India" },
  CN: { factor: 0.581, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "China",     note: "Declining as solar/wind scales" },
  JP: { factor: 0.453, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Japan" },
  SG: { factor: 0.408, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Singapore" },
  AU: { factor: 0.510, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "Australia" },
  NZ: { factor: 0.098, unit: "kg CO2e per kWh", source: "IEA_2026",   year: 2026, name: "New Zealand", note: "Geothermal + hydro" },

  // US — EPA eGRID 2024 subregions
  // National average is almost never the right choice for a specific site.
  US:           { factor: 0.386,  source: "EPA_2024", name: "United States (national avg)" },
  US_NYUP:      { factor: 0.1249, source: "EPA_2024", name: "NYUP — Upstate New York",  note: "Hydro 31% + nuclear 31%" },
  US_WECC_CAMX: { factor: 0.2265, source: "EPA_2024", name: "CAMX — California",        note: "Gas 46% + solar 20%" },
  US_ERCT:      { factor: 0.3512, source: "EPA_2024", name: "ERCT — Texas (ERCOT)",     note: "Gas 47% + wind 23%" },
  US_RFCW:      { factor: 0.4563, source: "EPA_2024", name: "RFCW — Ohio Valley",       note: "Coal 31% + gas 32%" },
  US_SRMW:      { factor: 0.6260, source: "EPA_2024", name: "SRMW — SERC Midwest",      note: "Coal 59% — highest subregion" },
};
Enter fullscreen mode Exit fullscreen mode

Every entry carries source and year. These render in the calculator UI next to the result — not hidden in a tooltip, not buried in a footnote. If someone is using this number in a regulatory disclosure, they need to see the citation inline.


The lookup function

/**
 * Look up a grid emission factor.
 *
 * @param {string} countryCode  ISO 3166-1 alpha-2 or US eGRID subregion key
 * @param {object} brain        window.gcMasterBrain (injected server-side)
 * @returns {{ factor: number, source: string, year: number, name: string } | null}
 */
function getGridFactor(countryCode, brain) {
  const key = countryCode.toUpperCase();
  const entry = brain?.grid?.[key] ?? null;

  if (!entry) {
    console.error(`[GC] Grid factor not found for key: "${key}"`);
    return null;
  }

  return entry;
}
Enter fullscreen mode Exit fullscreen mode

Tiny, deterministic, no fetch. The ?? guard means a missing or malformed Master Brain injection fails gracefully — the calculator surfaces an error state rather than silently outputting NaN * 0 = 0 into a user's report.


The calculation

/**
 * Scope 2 location-based electricity emissions.
 *
 * @param {number} kWh          Electricity consumed (kWh)
 * @param {string} countryCode  Grid lookup key
 * @param {object} brain        window.gcMasterBrain
 * @returns {{ kgCO2e: number, tCO2e: number, citation: string } | null}
 */
function calcScope2LocationBased(kWh, countryCode, brain) {
  const grid = getGridFactor(countryCode, brain);
  if (!grid) return null;

  const kgCO2e = kWh * grid.factor;

  return {
    kgCO2e:   +kgCO2e.toFixed(4),
    tCO2e:    +(kgCO2e / 1000).toFixed(6),
    citation: `${grid.source} (${grid.year}) — ${grid.name}`,
    factor:   grid.factor,
    note:     grid.note ?? null,
  };
}
Enter fullscreen mode Exit fullscreen mode

The return object always includes citation. Downstream rendering code has no excuse to display a number without its source.


Rendering the citation inline

function renderScope2Result(result, outputEl, citationEl) {
  if (!result) {
    outputEl.textContent = "";
    citationEl.textContent = "Factor not available for this region.";
    return;
  }

  outputEl.textContent = `${result.tCO2e} tCO₂e`;
  citationEl.textContent = result.citation;

  if (result.note) {
    citationEl.textContent += ` · ${result.note}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

The output element should have font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums in CSS. When a user changes the kWh input, digits update in-place without layout shift — a subtle but important signal that this is a precision instrument, not a toy.


The stale factor problem

Grid factors change every year. IEA publishes in April. DEFRA publishes every June. EPA eGRID publishes in January.

The UK grid factor dropped 15% in the DEFRA 2025 release — from 0.207 to 0.177 kg CO₂e/kWh. Any calculator that hardcoded the 2024 value is now overstating UK Scope 2 by 17%. For a 10 GWh/year operation, that's roughly 300 tCO₂e of phantom emissions in a filed disclosure.

The pattern I use to prevent this:

  1. All factors live in one file with a version constant (GC_MB_VERSION = '2025.6')
  2. The version renders in every calculator's footer: Data: DEFRA 2025 / IEA 2026 / EPA eGRID 2024
  3. A fallback version constant is gated in CI — it must match the Master Brain version on every deploy
  4. The update calendar is hardcoded as a comment in the source file so it cannot be missed

If a factor update deploys with a version bump but the fallback table is not updated in the same commit, a console.error fires on every page load identifying the stale page by post ID. One-click diagnosis.


What this gives you

A lookup that is:

  • Deterministic — same inputs, same output, every time, no network round-trip
  • Auditable — every result carries its source citation inline
  • Maintainable — one file to update annually, propagates to every calculator automatically
  • GHG Protocol compliant — location-based correctly separated from market-based; Scope 3 Category 3 (fuel and energy-related activities) handled as a distinct calculation

The full dataset — 50 countries and US eGRID subregions — is live at greencalculus.com.


Sources: IEA Global Energy Review 2026 · DEFRA GHG Conversion Factors 2025 (DESNZ) · EPA eGRID Summary Tables 2022, published January 2024 · GHG Protocol Scope 2 Guidance

Top comments (0)