Source: results.js

/**
 * Presenters and logic to visualize engine result.
 *
 * @license BSD, see LICENSE.md.
 */
import {EngineNumber} from "engine_number";
import {FilterSet} from "user_config";

/**
 * Array of colors used for visualizations
 * @type {string[]}
 */
const COLORS = [
  "#a6cee3",
  "#1f78b4",
  "#b2df8a",
  "#33a02c",
  "#fb9a99",
  "#e31a1c",
  "#505050",
  "#A0A0A0",
];

/**
 * Flag to allow an "All" option in filters to make it easy to return to a
 * default state.
 *
 * @type {boolean}
 */
const ALLOW_REDUNDANT_ALL = true;

/**
 * Flag to indicate if large score displays are active.
 *
 * @type {boolean}
 */
const ALLOW_SCORE_DISPLAY = false;

/**
 * Flag to enforce scenario selection when dimension is not simulations.
 *
 * This constraint prevents displaying all scenarios simultaneously with a
 * non-scenario dimension, which would create difficult to interpret charts.
 *
 * @type {boolean}
 */
const ENFORCE_SCENARIO_CONSTRAINT = true;

/**
 * Get a color from the predefined color palette.
 *
 * @param {number} i - Index into color array.
 * @returns {string} Color hex code.
 */
function getColor(i) {
  if (i >= COLORS.length) {
    return "#333";
  } else {
    return COLORS[i];
  }
}

/**
 * Extract base application/substance name by removing subtype suffix.
 *
 * For "Refrigeration - Commercial", returns "Refrigeration".
 * For "HFC-134a", returns "HFC-134a" (no change).
 *
 * @param {string} fullName - Full name with potential subtype.
 * @returns {string} Base name without subtype.
 */
function getBaseName(fullName) {
  if (fullName.includes(" - ")) {
    return fullName.split(" - ")[0];
  }
  return fullName;
}

/**
 * Add references to application metagroups.
 *
 * Add references to application metagroups by looking for those with subgroups
 * (subapplications) and adding an "- All" option which can be used to filter
 * for all subapplications within the application.
 *
 * @param {Array} applicationNamesRaw - Iterable of string application names.
 */
function getWithMetaApplications(applicationNamesRaw) {
  const applicationNames = Array.of(...applicationNamesRaw);
  const withSubapplications = applicationNames.filter((x) => x.includes(" - "));
  const repeatedApplications = withSubapplications.map((x) => x.split(" - ")[0]);
  const applications = Array.of(...new Set(repeatedApplications));
  const allOptions = applications.map((x) => x + " - All");
  const newAllOptions = allOptions.filter((x) => applicationNames.indexOf(x) == -1);
  return newAllOptions.concat(applicationNames);
}

/**
 * Adds equipment model meta-options to application names.
 *
 * Creates "Application - All" options for applications that have
 * equipment models, similar to how getWithMetaApplications works for subapplications.
 *
 * @param {Array} applicationNamesRaw - Iterable of string application names.
 */
function getWithMetaEquipment(applicationNamesRaw) {
  const applicationNames = Array.of(...applicationNamesRaw);
  const withEquipment = applicationNames.filter((x) => {
    const parts = x.split(" - ");
    return parts.length >= 2 && !parts[1].startsWith("All");
  });
  const baseApplications = withEquipment.map((x) => x.split(" - ")[0]);
  const uniqueApplications = Array.of(...new Set(baseApplications));
  const equipmentAllOptions = uniqueApplications.map((x) => x + " - All");
  const newEquipmentOptions = equipmentAllOptions.filter((x) => applicationNames.indexOf(x) == -1);
  return newEquipmentOptions.concat(applicationNames);
}

/**
 * Adds equipment model meta-options to substance names.
 *
 * Creates "Substance - All" options for substances that have
 * equipment models, similar to how getWithMetaApplications works for subapplications.
 *
 * @param {Array} substanceNamesRaw - Iterable of string substance names.
 */
function getWithMetaSubstanceEquipment(substanceNamesRaw) {
  const substanceNames = Array.of(...substanceNamesRaw);
  const withEquipment = substanceNames.filter((x) => {
    const parts = x.split(" - ");
    return parts.length >= 2 && !parts[1].startsWith("All");
  });
  const baseSubstances = withEquipment.map((x) => x.split(" - ")[0]);
  const uniqueSubstances = Array.of(...new Set(baseSubstances));
  const allOptions = uniqueSubstances.map((x) => x + " - All");
  const newAllOptions = allOptions.filter((x) => substanceNames.indexOf(x) == -1);
  return newAllOptions.concat(substanceNames);
}

/**
 * Main presenter class for displaying simulation results.
 */
class ResultsPresenter {
  /**
   * Create a new ResultsPresenter.
   *
   * @param {HTMLElement} root - Root DOM element for results display
   */
  constructor(root) {
    const self = this;
    self._root = root;
    self._results = null;
    self.resetFilter();

    const scorecardContainer = self._root.querySelector("#scorecards");
    const dimensionsContainer = self._root.querySelector("#dimensions");
    const centerChartContainer = self._root.querySelector("#center-chart");
    const centerChartHolderContainer = self._root.querySelector("#center-chart-holder");

    const onUpdateFilterSet = (x) => self._onUpdateFilterSet(x);
    self._scorecardPresenter = new ScorecardPresenter(scorecardContainer, onUpdateFilterSet);
    self._dimensionPresenter = new DimensionCardPresenter(dimensionsContainer, onUpdateFilterSet);
    self._dimensionManager = new DimensionPresenter(self._dimensionPresenter);
    self._centerChartPresenter = new CenterChartPresenter(centerChartContainer);
    self._titlePreseter = new SelectorTitlePresenter(centerChartHolderContainer, onUpdateFilterSet);
    self._exportPresenter = new ExportPresenter(self._root);
    self._optionsPresenter = new OptionsPanelPresenter(self._root, onUpdateFilterSet);

    self.hide();
  }

  /**
   * Set the filter set and update all UI components.
   *
   * @param {FilterSet} filterSet - The FilterSet to apply.
   */
  setFilter(filterSet) {
    const self = this;
    self._filterSet = filterSet;
    self._onUpdateFilterSet(self._filterSet);
  }

  /**
   * Reset the filters active in the results section.
   */
  resetFilter() {
    const self = this;
    const defaultFilterSet = new FilterSet(
      null,
      null,
      null,
      null,
      "sales:domestic:mt / year",
      "simulations",
      null,
      false,
      null,
    );
    self.setFilter(defaultFilterSet);
  }

  /**
   * Hide the results display.
   */
  hide() {
    const self = this;
    self._root.style.display = "none";
  }

  /**
   * Show simulation results.
   *
   * @param {ReportDataWrapper} results - Results data to display.
   * @param {BackendResult} backendResult - Backend result containing CSV string.
   */
  showResults(results, backendResult) {
    const self = this;
    self._root.style.display = "block";
    self._results = results;
    self._backendResult = backendResult;
    self._updateInternally();
  }

  /**
   * Handle filter set updates.
   *
   * @param {FilterSet} newFilterSet - Updated filter settings.
   * @private
   */
  _onUpdateFilterSet(newFilterSet) {
    const self = this;
    self._filterSet = self._constrainFilterSet(newFilterSet);
    self._updateInternally();
  }

  /**
   * Constrain the filter set to ensure valid selections.
   *
   * Validates that selected values exist in the current results and applies
   * constraints to ensure chart interpretability. This method orchestrates
   * multiple validation steps in sequence.
   *
   * @param {FilterSet} filterSet - The filter set to constrain.
   * @returns {FilterSet} The constrained filter set with all validations applied.
   * @private
   */
  _constrainFilterSet(filterSet) {
    const self = this;

    let constrainedFilterSet = filterSet;

    if (self._results !== null) {
      constrainedFilterSet = self._checkScenarioExists(constrainedFilterSet);
      constrainedFilterSet = self._checkApplicationExists(constrainedFilterSet);
      constrainedFilterSet = self._checkSubstanceExists(constrainedFilterSet);
    }

    constrainedFilterSet = self._validateCustomMetrics(constrainedFilterSet);
    constrainedFilterSet = self._applyScenarioConstraint(constrainedFilterSet);

    return constrainedFilterSet;
  }

  /**
   * Validate that the selected scenario exists in results.
   *
   * Checks if the currently selected scenario (if any) exists in the available
   * scenarios. If the selected scenario is not found, returns a new FilterSet
   * with the scenario reset to null.
   *
   * @param {FilterSet} filterSet - The filter set to validate.
   * @returns {FilterSet} The validated filter set with scenario corrected if needed.
   * @private
   */
  _checkScenarioExists(filterSet) {
    const self = this;
    const selectedScenario = filterSet.getScenario();

    if (selectedScenario === null) {
      return filterSet;
    }

    const availableScenarios = self._results.getScenarios(
      filterSet.getWithScenario(null));

    if (!availableScenarios.has(selectedScenario)) {
      return filterSet.getWithScenario(null);
    }

    return filterSet;
  }

  /**
   * Validate that the selected application exists in results.
   *
   * Checks if the currently selected application (if any) exists in the
   * available applications (including meta-applications and equipment models).
   * If the selected application is not found, returns a new FilterSet with the
   * application reset to null.
   *
   * @param {FilterSet} filterSet - The filter set to validate.
   * @returns {FilterSet} The validated filter set with application corrected if needed.
   * @private
   */
  _checkApplicationExists(filterSet) {
    const self = this;
    const selectedApplication = filterSet.getApplication();

    if (selectedApplication === null) {
      return filterSet;
    }

    const availableApplicationsRaw = self._results.getApplications(
      filterSet.getWithApplication(null));
    const availableApplications = getWithMetaEquipment(
      getWithMetaApplications(availableApplicationsRaw),
    );

    if (!availableApplications.includes(selectedApplication)) {
      return filterSet.getWithApplication(null);
    }

    return filterSet;
  }

  /**
   * Validate that the selected substance exists in results.
   *
   * Checks if the currently selected substance (if any) exists in the available
   * substances (including meta-substances with equipment models). If the
   * selected substance is not found, returns a new FilterSet with the substance
   * reset to null.
   *
   * @param {FilterSet} filterSet - The filter set to validate.
   * @returns {FilterSet} The validated filter set with substance corrected if needed.
   * @private
   */
  _checkSubstanceExists(filterSet) {
    const self = this;
    const selectedSubstance = filterSet.getSubstance();

    if (selectedSubstance === null) {
      return filterSet;
    }

    const availableSubstancesRaw = self._results.getSubstances(
      filterSet.getWithSubstance(null));
    const availableSubstances = getWithMetaSubstanceEquipment(availableSubstancesRaw);

    if (!availableSubstances.includes(selectedSubstance)) {
      return filterSet.getWithSubstance(null);
    }

    return filterSet;
  }

  /**
   * Validate custom metric definitions and provide defaults if needed.
   *
   * Checks if the current metric is a custom metric and validates that it has
   * a proper definition. If the custom definition is missing or empty, switches
   * to a default submetric for the metric family (emissions or sales).
   *
   * @param {FilterSet} filterSet - The filter set to validate.
   * @returns {FilterSet} The validated filter set with custom metrics corrected if needed.
   * @private
   */
  _validateCustomMetrics(filterSet) {
    const self = this;

    if (!filterSet.isCustomMetric()) {
      return filterSet;
    }

    const metricFamily = filterSet.getMetric();
    const customDef = filterSet.getCustomDefinition(metricFamily);

    if (!customDef || customDef.length === 0) {
      const defaultSubmetrics = {
        "emissions": "recharge",
        "sales": "manufacture",
      };

      const defaultSubmetric = defaultSubmetrics[metricFamily];
      if (defaultSubmetric) {
        const currentUnits = filterSet.getUnits();
        const defaultMetric = `${metricFamily}:${defaultSubmetric}:${currentUnits}`;
        return filterSet.getWithMetric(defaultMetric);
      }
    }

    return filterSet;
  }

  /**
   * Apply scenario constraint to prevent difficult to interpret charts.
   *
   * When all simulations are selected (scenario is null) and the dimension is
   * not "simulations", this automatically selects the first available scenario
   * to ensure chart clarity. This constraint is controlled by the
   * ENFORCE_SCENARIO_CONSTRAINT constant.
   *
   * @param {FilterSet} filterSet - The filter set to constrain.
   * @returns {FilterSet} The constrained filter set with scenario selected if needed.
   * @private
   */
  _applyScenarioConstraint(filterSet) {
    const self = this;

    const isAllSimulations = filterSet.getScenario() === null;
    const isSimulationDimension = filterSet.getDimension() === "simulations";
    const needsConstraints = isAllSimulations && !isSimulationDimension;

    if (ENFORCE_SCENARIO_CONSTRAINT && needsConstraints && self._results !== null) {
      const firstScenario = self._results.getFirstScenario(filterSet);
      return filterSet.getWithScenario(firstScenario);
    }

    return filterSet;
  }

  /**
   * Update all sub-presenters with current data.
   *
   * @private
   */
  _updateInternally() {
    const self = this;

    if (!self._getResultsAvailable()) {
      return;
    }

    const years = self._results.getYears(self._filterSet.getWithYear(null));
    self._filterSet = self._filterSet.getWithYear(Math.max(...years));

    self._dimensionManager.updateSubstancesLabel(self._filterSet);

    self._scorecardPresenter.showResults(self._results, self._filterSet);
    self._dimensionPresenter.showResults(self._results, self._filterSet);
    self._centerChartPresenter.showResults(self._results, self._filterSet);
    self._titlePreseter.showResults(self._results, self._filterSet);
    self._exportPresenter.showResults(self._results, self._filterSet, self._backendResult);
    self._optionsPresenter.showResults(self._results, self._filterSet);
  }

  /**
   * Determine if results are available to display.
   *
   * @returns {boolean} True if results are available and false otherwise.
   */
  _getResultsAvailable() {
    const self = this;
    return self._results !== null;
  }
}

/**
 * Presenter for configuring and executing results exports.
 */
class ExportPresenter {
  /**
   * Create a new ExportPresenter.
   *
   * @param {HTMLElement} root - Root DOM element.
   */
  constructor(root) {
    const self = this;
    self._root = root;
  }

  /**
   * Update export data with new results.
   *
   * @param {Object} results - Results data to export.
   * @param {FilterSet} filterSet - Current filter settings.
   * @param {BackendResult} backendResult - Backend result containing CSV string.
   */
  showResults(results, filterSet, backendResult) {
    const self = this;

    // Use the CSV string directly from the backend instead of generating our own
    const csvStr = backendResult.getCsvString();
    const encodedValue = encodeURI("data:text/csv;charset=utf-8," + csvStr);
    const exportLink = document.getElementById("export-button");
    exportLink.href = encodedValue;
  }
}

/**
 * Presenter for scorecard metrics display.
 *
 * Presenter for scorecard metrics display that, in addition to optionally
 * showing a high level metric, also allows for changing filter values.
 */
class ScorecardPresenter {
  /**
   * Create a new ScorecardPresenter.
   *
   * @param {HTMLElement} root - Root DOM element.
   * @param {Function} onUpdateFilterSet - Callback for filter updates.
   */
  constructor(root, onUpdateFilterSet) {
    const self = this;
    self._root = root;
    self._filterSet = null;
    self._onUpdateFilterSet = onUpdateFilterSet;

    // Create custom metric presenters
    self._customPresenters = {};
    self._customPresenters.emissions = new CustomMetricPresenter(
      "custom-emissions-dialog",
      ["recharge", "eol", "export", "initial charge"],
      (selection) => self._onCustomMetricChanged("emissions", selection),
      ".emissions-submetric",
    );
    self._customPresenters.sales = new CustomMetricPresenter(
      "custom-sales-dialog",
      ["manufacture", "import", "export", "recycle"],
      (selection) => self._onCustomMetricChanged("sales", selection),
      ".sales-submetric",
    );

    self._registerEventListeners();
  }

  /**
   * Update scorecards with new results data.
   *
   * @param {Object} results - Results data to display.
   * @param {FilterSet} filterSet - Current filter settings.
   */
  showResults(results, filterSet) {
    const self = this;
    self._filterSet = filterSet;

    /**
     * Execute updates in the UI to show the results without checks for invalid user selections.
     */
    const showUnsafe = () => {
      const currentYear = self._filterSet.getYear();

      const emissionsScorecard = self._root.querySelector("#emissions-scorecard");
      const salesScorecard = self._root.querySelector("#sales-scorecard");
      const equipmentScorecard = self._root.querySelector("#population-scorecard");

      const emissionsValue = results.getTotalEmissions(filterSet);
      const salesValue = results.getSales(filterSet);
      const equipmentValue = results.getPopulation(filterSet);

      const roundToTenths = (x) => Math.round(x * 10) / 10;
      const emissionRounded = roundToTenths(self._safeGetValue(emissionsValue) / 1000000);
      const salesMt = roundToTenths(self._safeGetValue(salesValue) / 1000000) + " k";
      const millionEqipment = roundToTenths(self._safeGetValue(equipmentValue) / 1000000) + " M";

      const metricSelected = filterSet.getMetric();
      const emissionsSelected = metricSelected === "emissions";
      const salesSelected = metricSelected === "sales";
      const equipmentSelected = metricSelected === "population";

      const scenarios = results.getScenarios(self._filterSet.getWithScenario(null));
      const showVal = ALLOW_SCORE_DISPLAY && self._filterSet.hasSingleScenario(scenarios);
      const hideVal = !showVal;

      self._updateCard(
        emissionsScorecard,
        emissionRounded,
        currentYear,
        emissionsSelected,
        hideVal,
      );
      self._updateCard(
        salesScorecard,
        salesMt,
        currentYear,
        salesSelected,
        hideVal,
      );
      self._updateCard(
        equipmentScorecard,
        millionEqipment,
        currentYear,
        equipmentSelected,
        hideVal,
      );

      self._updateDropdownMenus(emissionsScorecard, salesScorecard, equipmentScorecard);
    };

    // Execute with a catch for invalid user selections.
    try {
      showUnsafe();
    } catch (error) {
      // Reset filter set to default when null values cause errors
      console.warn("Error in ScorecardPresenter.showResults, resetting filter set:", error);
      self._onUpdateFilterSet(new FilterSet(
        null,
        null,
        null,
        null,
        "sales:domestic:mt / year",
        "simulations",
        null,
        false,
        null,
      ));
    }
  }

  /**
   * Safely extract numeric value from potentially null EngineNumber.
   *
   * @param {EngineNumber|null} engineNumber - The engine number to extract value from.
   * @param {number} defaultValue - Default value if engineNumber is null (defaults to 0).
   * @returns {number} The numeric value or default.
   * @private
   */
  _safeGetValue(engineNumber, defaultValue = 0) {
    const self = this;
    return engineNumber !== null ? engineNumber.getValue() : defaultValue;
  }

  /**
   * Get the strategy function for updating dropdown menus based on metric family.
   *
   * @param {string} metricFamily - The metric family ('emissions', 'sales', or 'population').
   * @returns {Function} Strategy function that updates the appropriate dropdown menus.
   * @throws {Error} If metric family is unknown.
   * @private
   */
  _getDropdownMetricStrategy(metricFamily) {
    const self = this;

    const metricStrategies = {
      "emissions": (emissionsScorecard, subMetric, units) => {
        const emissionsSubmetricDropdown = emissionsScorecard.querySelector(
          ".emissions-submetric");
        const emissionsUnitsDropdown = emissionsScorecard.querySelector(".emissions-units");
        emissionsSubmetricDropdown.value = subMetric;
        emissionsUnitsDropdown.value = units;
      },
      "sales": (salesScorecard, subMetric, units) => {
        const salesSubmetricDropdown = salesScorecard.querySelector(".sales-submetric");
        const salesUnitsDropdown = salesScorecard.querySelector(".sales-units");
        salesSubmetricDropdown.value = subMetric;
        salesUnitsDropdown.value = units;
      },
      "population": (equipmentScorecard, subMetric, units) => {
        const equipmentSubmetricDropdown = equipmentScorecard.querySelector(
          ".equipment-submetric");
        const equipmentUnitsDropdown = equipmentScorecard.querySelector(".equipment-units");
        equipmentSubmetricDropdown.value = subMetric;
        equipmentUnitsDropdown.value = units;
      },
    };

    const strategy = metricStrategies[metricFamily];
    if (!strategy) {
      throw new Error(`Unknown metric family: ${metricFamily}`);
    }
    return strategy;
  }

  /**
   * Update dropdown menu values to match current FilterSet selections.
   *
   * @param {HTMLElement} emissionsScorecard - Emissions scorecard DOM element.
   * @param {HTMLElement} salesScorecard - Sales scorecard DOM element.
   * @param {HTMLElement} equipmentScorecard - Equipment scorecard DOM element.
   * @private
   */
  _updateDropdownMenus(emissionsScorecard, salesScorecard, equipmentScorecard) {
    const self = this;

    const metricFamily = self._filterSet.getMetric();
    const subMetric = self._filterSet.getSubMetric();
    const units = self._filterSet.getUnits();

    const strategy = self._getDropdownMetricStrategy(metricFamily);

    // Execute strategy with appropriate scorecard based on metric family
    if (metricFamily === "emissions") {
      strategy(emissionsScorecard, subMetric, units);
    } else if (metricFamily === "sales") {
      strategy(salesScorecard, subMetric, units);
    } else if (metricFamily === "population") {
      strategy(equipmentScorecard, subMetric, units);
    }
  }

  /**
   * Handle custom metric definition changes.
   *
   * @param {string} metricFamily - The metric family ('emissions' or 'sales').
   * @param {Array<string>} selection - Array of selected submetrics.
   * @private
   */
  _onCustomMetricChanged(metricFamily, selection) {
    const self = this;

    // Update the corresponding dropdown to "custom"
    const presenter = self._customPresenters[metricFamily];
    const dropdown = self._root.querySelector(presenter._dropdownSelector);
    dropdown.value = "custom";

    // Update custom definition in filter set
    const newFilterSet = self._filterSet.getWithCustomDefinition(metricFamily, selection);

    // Switch to custom metric if not already
    const currentMetric = newFilterSet.getMetric();
    const currentUnits = newFilterSet.getUnits();

    if (currentMetric === metricFamily) {
      const customMetric = `${metricFamily}:custom:${currentUnits}`;
      const finalFilterSet = newFilterSet.getWithMetric(customMetric);
      self._onUpdateFilterSet(finalFilterSet);
    } else {
      self._onUpdateFilterSet(newFilterSet);
    }
  }

  /**
   * Update an individual scorecard.
   *
   * @param {HTMLElement} scorecard - Scorecard DOM element.
   * @param {string|number} value - Value to display.
   * @param {number} currentYear - Current year.
   * @param {boolean} selected - Flag indicating if the card is selected.
   * @param {boolean} hideVal - Flag indicating if the value display should be
   *     hidden. True if a value should be displayed and false otherwise which
   *     may be appropriate if a single summary value cannot be generated.
   * @private
   */
  _updateCard(scorecard, value, currentYear, selected, hideVal) {
    const self = this;
    self._setText(scorecard.querySelector(".value"), value);

    if (hideVal) {
      scorecard.querySelector(".value").style.display = "none";
    } else {
      scorecard.querySelector(".value").style.display = "block";
    }

    if (selected) {
      scorecard.classList.remove("inactive");
    } else {
      scorecard.classList.add("inactive");
    }

    d3.select(scorecard).select(".metric-radio").property("checked", selected);
  }

  /**
   * Set text content of an element.
   *
   * @param {HTMLElement} selection - Element to update.
   * @param {string} value - New text value.
   * @private
   */
  _setText(selection, value) {
    const self = this;
    selection.innerHTML = "";
    const newTextNode = document.createTextNode(value);
    selection.appendChild(newTextNode);
  }

  /**
   * Register event listeners for scorecard interactions.
   *
   * @private
   */
  _registerEventListeners() {
    const self = this;

    const emissionsScorecard = self._root.querySelector("#emissions-scorecard");
    const salesScorecard = self._root.querySelector("#sales-scorecard");
    const equipmentScorecard = self._root.querySelector("#population-scorecard");

    const registerListener = (scorecard, family) => {
      const subMetricDropdown = scorecard.querySelector(".submetric-input");
      const unitsDropdown = scorecard.querySelector(".units-input");
      const customConfigLink = scorecard.querySelector(".configure-custom-link");

      const callback = () => {
        const subMetric = subMetricDropdown.value;
        const units = unitsDropdown.value;

        // Handle custom metric selection
        if (subMetric === "custom") {
          const customPresenter = self._customPresenters[family];
          const currentDef = customPresenter.getCurrentDefinition();

          // Set the current definition based on FilterSet
          const filterSetDef = self._filterSet.getCustomDefinition(family);
          if (filterSetDef) {
            customPresenter.setCurrentDefinition(filterSetDef);
          }

          if (!currentDef || currentDef.length === 0) {
            customPresenter.showDialog();
            return; // Don't update filter until dialog is completed
          }
        }

        const fullName = family + ":" + subMetric + ":" + units;
        const newFilterSet = self._filterSet.getWithMetric(fullName);
        self._onUpdateFilterSet(newFilterSet);
      };

      const radio = scorecard.querySelector(".metric-radio");
      radio.addEventListener("click", callback);

      subMetricDropdown.addEventListener("change", callback);
      unitsDropdown.addEventListener("change", callback);

      // Custom configuration link listener
      if (customConfigLink) {
        customConfigLink.addEventListener("click", (event) => {
          event.preventDefault();
          const customPresenter = self._customPresenters[family];

          // Set current definition before showing dialog
          const filterSetDef = self._filterSet.getCustomDefinition(family);
          if (filterSetDef) {
            customPresenter.setCurrentDefinition(filterSetDef);
          }

          customPresenter.showDialog();
        });
      }
    };

    registerListener(emissionsScorecard, "emissions");
    registerListener(salesScorecard, "sales");
    registerListener(equipmentScorecard, "population");
  }
}

/**
 * Presenter for dimension cards display.
 *
 * Presenter which shows the dimensions selectors with embedded bar charts that
 * allow for changing of filters.
 */
class DimensionCardPresenter {
  /**
   * Create a new DimensionCardPresenter.
   *
   * @param {HTMLElement} root - Root DOM element.
   * @param {Function} onUpdateFilterSet - Callback for filter updates.
   */
  constructor(root, onUpdateFilterSet) {
    const self = this;
    self._root = root;
    self._onUpdateFilterSet = onUpdateFilterSet;
    self._filterSet = null;
    self._registerEventListeners();
  }

  /**
   * Update dimension cards with new results
   * @param {Object} results - Results data to display
   * @param {FilterSet} filterSet - Current filter settings
   */
  showResults(results, filterSet) {
    const self = this;
    self._filterSet = filterSet;

    const metricSelected = self._filterSet.getMetric();
    const metricUnits = self._filterSet.getUnits();

    const currentYear = self._filterSet.getYear();
    const scenarios = results.getScenarios(self._filterSet.getWithScenario(null));

    const allTickUnits = Array.of(...self._root.querySelectorAll(".units-tick"));
    allTickUnits.forEach((x) => (x.innerHTML = metricUnits));

    const allTickYears = Array.of(...self._root.querySelectorAll(".years-tick"));
    if (self._filterSet.hasSingleScenario(scenarios)) {
      allTickYears.forEach((x) => (x.innerHTML = "in year " + currentYear));
    } else {
      allTickYears.forEach((x) => (x.innerHTML = ""));
    }

    const simulationsCard = self._root.querySelector("#simulations-dimension");
    const applicationsCard = self._root.querySelector("#applications-dimension");
    const substancesCard = self._root.querySelector("#substances-dimension");

    const dimensionSelected = self._filterSet.getDimension();
    const simulationsSelected = dimensionSelected === "simulations";
    const applicationsSelected = dimensionSelected === "applications";
    const substancesSelected = dimensionSelected === "substances";

    const interpret = (x) => (x === null ? null : x.getValue());

    self._updateCard(
      "sim",
      simulationsCard,
      results.getScenarios(self._filterSet.getWithScenario(null)),
      simulationsSelected,
      self._filterSet.getScenario(),
      (x) => self._filterSet.getWithScenario(x),
      true,
      (value) => interpret(results.getMetric(self._filterSet.getWithScenario(value))),
      metricUnits,
      scenarios,
    );

    self._updateCard(
      "app",
      applicationsCard,
      getWithMetaEquipment(
        getWithMetaApplications(results.getApplications(self._filterSet.getWithApplication(null))),
      ),
      applicationsSelected,
      self._filterSet.getApplication(),
      (x) => self._filterSet.getWithApplication(x),
      true,
      (value) => interpret(results.getMetric(self._filterSet.getWithApplication(value))),
      metricUnits,
      scenarios,
    );

    self._updateCard(
      "sub",
      substancesCard,
      getWithMetaSubstanceEquipment(results.getSubstances(self._filterSet.getWithSubstance(null))),
      substancesSelected,
      self._filterSet.getSubstance(),
      (x) => self._filterSet.getWithSubstance(x),
      true,
      (value) => interpret(results.getMetric(self._filterSet.getWithSubstance(value))),
      metricUnits,
      scenarios,
    );
  }

  /**
   * Update an individual dimension card.
   *
   * @param {string} label - Card identifier.
   * @param {HTMLElement} card - Card DOM element.
   * @param {Set<string>} identifiers - Set of dimension values.
   * @param {boolean} selected - Whether card is selected.
   * @param {string} subSelection - Currently selected value.
   * @param {Function} subFilterSetBuilder - Filter builder function.
   * @param {boolean} addAll - Whether to add "All" option. True if should be
   *     added and false otherwise.
   * @param {Function} valueGetter - Function to get display value.
   * @param {string} suffix - Value suffix like for units.
   * @param {Set<string>} scenarios - Available scenarios.
   * @private
   */
  _updateCard(
    label,
    card,
    identifiers,
    selected,
    subSelection,
    subFilterSetBuilder,
    addAll,
    valueGetter,
    suffix,
    scenarios,
  ) {
    const self = this;

    if (selected) {
      card.classList.remove("inactive");
    } else {
      card.classList.add("inactive");
    }

    d3.select(card).select(".dimension-radio").property("checked", selected);

    const identifiersArray = Array.of(...identifiers);
    identifiersArray.sort();

    const values = identifiersArray.map(valueGetter);
    // Filter out null values and handle empty arrays gracefully
    const validValues = values.filter((v) => v !== null && v !== undefined && !isNaN(v));
    const maxValue = validValues.length > 0 ? Math.max(...validValues) : 0;
    d3.select(card.querySelector(".right-tick")).text(Math.round(maxValue));

    const hasSingleScenario = self._filterSet.hasSingleScenario(scenarios);
    const isOnlyValue = identifiersArray.length == 1;

    const allNeeded = ALLOW_REDUNDANT_ALL ? addAll : addAll && !isOnlyValue;
    if (allNeeded) {
      identifiersArray.unshift("All");
    }

    const listSelection = d3.select(card).select(".list");
    listSelection.html("");

    const itemDivs = listSelection
      .selectAll(".item")
      .data(identifiersArray)
      .enter()
      .append("div")
      .classed("item", true);

    const itemLabels = itemDivs.append("label");

    itemLabels
      .append("input")
      .attr("type", "radio")
      .classed(label + "-radio", true)
      .attr("name", label + "-viz")
      .style("height", "13px")
      .style("width", "13px")
      .property("checked", (x) => {
        const valuesMatch = x === subSelection;
        const isAllAndSelected = subSelection === null && x === "All";
        return valuesMatch || isAllAndSelected || isOnlyValue;
      });

    itemLabels.append("span").text((x) => x);

    itemLabels.on("click", (event, x) => {
      const newFilterSet = subFilterSetBuilder(x === "All" ? null : x);
      self._onUpdateFilterSet(newFilterSet);
    });

    if (hasSingleScenario || label === "sim") {
      const offset = allNeeded ? 1 : 0;

      // Only use base name color coordination if there are more than 5 items
      const useBaseNameColors = identifiersArray.length > 5;

      // Build base names for color assignment
      const nonAllItems = identifiersArray.filter((x) => x !== "All");
      const baseNames = Array.from(new Set(nonAllItems.map((x) => getBaseName(x))));
      baseNames.sort();

      const getColorIndex = (x, i) => {
        if (useBaseNameColors) {
          const baseName = getBaseName(x);
          return baseNames.indexOf(baseName);
        } else {
          return i - offset;
        }
      };

      const lineHolders = itemDivs.append("div").classed("list-line-holder", true);

      const lines = lineHolders
        .append("div")
        .classed("list-line", true)
        .style("width", "100%")
        .style("height", (x, i) => {
          if (x === "All") {
            return "0px";
          }
          return "1px";
        })
        .style("background-color", (x, i) => {
          if (!selected) {
            return "#C0C0C0";
          }
          if (x === "All") {
            return "transparent";
          }
          return getColor(getColorIndex(x, i));
        });

      lines
        .append("div")
        .classed("list-bar", true)
        .style("height", (x, i) => {
          if (x === "All") {
            return "0px";
          }
          return "5px";
        })
        .style("background-color", (x, i) => {
          if (!selected) {
            return "#C0C0C0";
          }
          if (x === "All") {
            return "transparent";
          }
          return getColor(getColorIndex(x, i));
        })
        .style("width", (x) => {
          if (x === "All") {
            return "0%";
          } else {
            const value = valueGetter(x);
            // Handle null values gracefully
            if (value === null || value === undefined || isNaN(value) || maxValue === 0) {
              return "0%";
            }
            const percent = value / maxValue;
            return Math.round(percent * 100) + "%";
          }
        });

      d3.select(card).select(".axis").style("display", "grid");
    } else {
      d3.select(card).select(".axis").style("display", "none");
    }
  }

  /**
   * Register event listeners for dimension card interactions.
   *
   * @private
   */
  _registerEventListeners() {
    const self = this;

    const simulationsCard = self._root.querySelector("#simulations-dimension");
    const applicationsCard = self._root.querySelector("#applications-dimension");
    const substancesCard = self._root.querySelector("#substances-dimension");

    const registerListener = (scorecard, value) => {
      const radio = scorecard.querySelector(".dimension-radio");
      radio.addEventListener("click", () => {
        const newFilterSet = self._filterSet.getWithDimension(value);
        self._onUpdateFilterSet(newFilterSet);
      });
    };

    registerListener(simulationsCard, "simulations");
    registerListener(applicationsCard, "applications");
    registerListener(substancesCard, "substances");
  }

  /**
   * Set the label text for a specific dimension.
   *
   * @param {string} dimensionType - The dimension type ("simulations",
   *     "applications", "substances").
   * @param {string} labelText - The text to use for the label.
   */
  setLabel(dimensionType, labelText) {
    const self = this;
    const selector = `#${dimensionType}-dimension`;
    const dimensionCard = self._root.querySelector(selector);
    const labelSpan = dimensionCard.querySelector(".dimension-label");
    labelSpan.textContent = labelText;
  }
}

/**
 * Presenter for options checkboxes.
 *
 * Presenter for options checkboxes at the bottom of the results panel before export button which,
 * at this time, only includes a checkbox for attributing initial charge to importer (defaults to
 * unchecked / attribute to exporter).
 */
class OptionsPanelPresenter {
  /**
   * Create a new OptionsPanelPresenter.
   *
   * @param {HTMLElement} root - Root DOM element.
   * @param {Function} onUpdateFilterSet - Callback for filter updates.
   */
  constructor(root, onUpdateFilterSet) {
    const self = this;
    self._root = root;
    self._onUpdateFilterSet = onUpdateFilterSet;
    self._filterSet = null;
    self._attributeImporterCheck = self._root.querySelector("#importer-assignment-check");
    self._registerEventListeners();
  }

  /**
   * Update dimension cards with new results
   * @param {Object} results - Results data to display
   * @param {FilterSet} filterSet - Current filter settings
   */
  showResults(results, filterSet) {
    const self = this;
    self._filterSet = filterSet;
    self._attributeImporterCheck.checked = self._filterSet.getAttributeImporter();
  }


  /**
   * Register event listeners for options being changed.
   */
  _registerEventListeners() {
    const self = this;
    self._attributeImporterCheck.addEventListener("change", () => {
      const newValue = self._attributeImporterCheck.checked;

      const newFilterSet = self._filterSet.getWithAttributeImporter(newValue);
      self._onUpdateFilterSet(newFilterSet);
    });
  }
}

/**
 * Presenter for the central chart visualization.
 *
 * Presenter for the central chart visualization which is currently backed by
 * Chartjs.
 */
class CenterChartPresenter {
  /**
   * Create a new CenterChartPresenter.
   *
   * @param {HTMLElement} root - Root DOM element
   */
  constructor(root) {
    const self = this;
    self._root = root;
    self._chart = null;
  }

  /**
   * Update chart with new results.
   *
   * @param {Object} results - Results data to display.
   * @param {FilterSet} filterSet - Current filter settings.
   */
  showResults(results, filterSet) {
    const self = this;

    if (self._chart !== null) {
      self._chart.destroy();
    }

    const years = Array.of(...results.getYears(filterSet.getWithYear(null)));
    years.sort((a, b) => a - b);

    const getDimensionValues = (filterSet) => {
      const dimensionValuesRaw = Array.of(...results.getDimensionValues(filterSet));

      if (filterSet.getDimension() === "applications") {
        return getWithMetaEquipment(getWithMetaApplications(dimensionValuesRaw));
      } else if (filterSet.getDimension() === "substances") {
        return getWithMetaSubstanceEquipment(dimensionValuesRaw);
      } else {
        return dimensionValuesRaw;
      }
    };
    const dimensionValues = getDimensionValues(filterSet);
    dimensionValues.sort();

    const getForDimValue = (dimValue) => {
      const valsWithUnits = years.map((year) => {
        const withYear = filterSet.getWithYear(year);
        const subFilterSet = withYear.getWithDimensionValue(dimValue);

        if (filterSet.getBaseline() === null) {
          return results.getMetric(subFilterSet);
        } else {
          const absoluteVal = results.getMetric(subFilterSet);
          const baselineFilterSet = subFilterSet.getWithScenario(filterSet.getBaseline());
          const baselineVal = results.getMetric(baselineFilterSet);

          if (absoluteVal === null || baselineVal === null) {
            return null;
          }

          if (absoluteVal.getUnits() !== baselineVal.getUnits()) {
            throw "Mismanaged units in absolute vs baseline.";
          }

          return new EngineNumber(
            absoluteVal.getValue() - baselineVal.getValue(),
            absoluteVal.getUnits(),
          );
        }
      });
      const valsWithUnitsValid = valsWithUnits.filter((x) => x !== null);
      const vals = valsWithUnitsValid.map((x) => x.getValue());
      return {name: dimValue, vals: vals};
    };
    const dimensionSeries = dimensionValues.map(getForDimValue);

    const unconstrainedDimValues = getDimensionValues(filterSet.getWithDimensionValue(null));
    unconstrainedDimValues.sort();

    // Only use base name color coordination if there are more than 5 dimension values
    const useBaseNameColors = unconstrainedDimValues.length > 5;

    // Get base names for color assignment so subapplications share colors
    const baseNames = Array.from(
      new Set(unconstrainedDimValues.map((x) => getBaseName(x))),
    );
    baseNames.sort();

    const getColorIndex = (name) => {
      if (useBaseNameColors) {
        // Use base name to determine color, so all subapplications get same color
        const baseName = getBaseName(name);
        return baseNames.indexOf(baseName);
      } else {
        // Use full name for unique colors
        return unconstrainedDimValues.indexOf(name);
      }
    };

    const chartJsDatasets = dimensionSeries.map((x) => {
      const color = getColor(getColorIndex(x["name"]));
      return {
        label: x["name"],
        data: x["vals"],
        fill: false,
        borderColor: color,
        backgroundColor: color,
      };
    });

    const minVals = dimensionSeries.map((x) => {
      return Math.min(...x["vals"]);
    });
    const minVal = Math.min(...minVals);

    const chartJsData = {
      labels: years,
      datasets: chartJsDatasets,
    };

    const metricSelected = filterSet.getMetric();
    const metricUnits = filterSet.getUnits();

    const chartJsConfig = {
      type: "line",
      data: chartJsData,
      options: {
        scales: {
          y: {
            min: minVal >= 0 ? 0 : null,
            title: {text: metricUnits, display: true},
          },
          x: {
            title: {text: "Year", display: true},
          },
        },
        plugins: {
          tooltip: {
            callbacks: {
              title: (x) => "Year " + x[0]["label"],
            },
          },
          legend: {
            display: false,
          },
        },
      },
    };

    self._chart = new Chart(self._root, chartJsConfig);
  }
}

/**
 * Presenter for selector title display.
 *
 * Presenter for selector title display using a fill in the blank-like
 * approach.
 */
class SelectorTitlePresenter {
  /**
   * Create a new SelectorTitlePresenter.
   *
   * @param {HTMLElement} root - Root DOM element.
   * @param {Function} changeCallback - Callback for selection changes.
   */
  constructor(root, changeCallback) {
    const self = this;
    self._selection = root;
    self._changeCallback = changeCallback;
    self._filterSet = null;
    self._setupEventListeners();
  }

  /**
   * Update selector title with new results.
   *
   * @param {Object} results - Results data to display.
   * @param {FilterSet} filterSet - Current filter settings.
   */
  showResults(results, filterSet) {
    const self = this;
    self._filterSet = filterSet;

    const metricDropdown = self._selection.querySelector(".metric-select");
    const metricSelected = self._filterSet.getMetric();
    self._updateSimpleDropdown(metricDropdown, metricSelected);

    const dimensionDropdown = self._selection.querySelector(".dimension-select");
    const dimensionSelected = self._filterSet.getDimension();
    self._updateSimpleDropdown(dimensionDropdown, dimensionSelected);

    const scenarioDropdown = self._selection.querySelector(".scenario-select");
    const scenarioSelected = self._filterSet.getScenario();
    const scenarios = results.getScenarios(self._filterSet);
    self._updateDynamicDropdown(
      scenarioDropdown,
      scenarios,
      scenarioSelected,
      "All Simulations",
    );

    const baselineDropdown = self._selection.querySelector(".baseline-select");
    const baselineSelected = self._filterSet.getBaseline();
    self._updateDynamicDropdown(
      baselineDropdown,
      scenarios,
      baselineSelected,
      "Absolute Value",
      "Relative to ",
    );

    const applicationDropdown = self._selection.querySelector(".application-select");
    const applicationSelected = self._filterSet.getApplication();
    const applications = results.getApplications(self._filterSet.getWithApplication(null));
    self._updateDynamicDropdown(
      applicationDropdown,
      getWithMetaEquipment(getWithMetaApplications(applications)),
      applicationSelected,
      "All Applications",
    );

    const substanceDropdown = self._selection.querySelector(".substance-select");
    const substanceSelected = self._filterSet.getSubstance();
    const substances = getWithMetaSubstanceEquipment(
      results.getSubstances(self._filterSet.getWithSubstance(null)),
    );
    self._updateDynamicDropdown(
      substanceDropdown,
      substances,
      substanceSelected,
      "All Substances",
    );
  }

  /**
   * Update a simple dropdown with fixed options.
   *
   * @param {HTMLElement} selection - Dropdown element.
   * @param {string} value - Selected value.
   * @private
   */
  _updateSimpleDropdown(selection, value) {
    const self = this;
    selection.value = value;
  }

  /**
   * Update a dynamic dropdown with variable options.
   *
   * @param {HTMLElement} selection - Dropdown element.
   * @param {Set<string>} allValues - Available values.
   * @param {string} selectedValue - Currently selected value.
   * @param {string} allText - Text for "All" option.
   * @param {string} prefix - Optional string prefix to prepend to non-all text
   *     defaulting to empty string.
   * @private
   */
  _updateDynamicDropdown(selection, allValues, selectedValue, allText, prefix) {
    const self = this;

    if (prefix === undefined) {
      prefix = "";
    }

    const allValuesArray = Array.of(...allValues);
    allValuesArray.unshift(allText);
    allValuesArray.sort();

    const d3Selection = d3.select(selection);
    d3Selection.html("");
    d3Selection
      .selectAll("option")
      .data(allValuesArray)
      .enter()
      .append("option")
      .attr("value", (x) => (allText === x ? "" : x))
      .text((x) => (x === allText ? x : prefix + x))
      .property("selected", (x) => {
        const nativelySelected = x === selectedValue;
        const allSelected = x === allText && selectedValue === null;
        return nativelySelected || allSelected;
      });
  }

  /**
   * Set up event listeners for selector dropdowns.
   *
   * @private
   */
  _setupEventListeners() {
    const self = this;

    const addListener = (selection, newFilterSetGen) => {
      selection.addEventListener("change", () => {
        const value = selection.value === "" ? null : selection.value;
        const newFilterSet = newFilterSetGen(self._filterSet, value);
        self._changeCallback(newFilterSet);
      });
    };

    const metricDropdown = self._selection.querySelector(".metric-select");
    addListener(metricDropdown, (filterSet, val) => filterSet.getWithMetric(val));

    const dimensionDropdown = self._selection.querySelector(".dimension-select");
    addListener(dimensionDropdown, (filterSet, val) => filterSet.getWithDimension(val));

    const scenarioDropdown = self._selection.querySelector(".scenario-select");
    addListener(scenarioDropdown, (filterSet, val) => filterSet.getWithScenario(val));

    const applicationDropdown = self._selection.querySelector(".application-select");
    addListener(applicationDropdown, (filterSet, val) => filterSet.getWithApplication(val));

    const substanceDropdown = self._selection.querySelector(".substance-select");
    addListener(substanceDropdown, (filterSet, val) => filterSet.getWithSubstance(val));

    const baselineDropdown = self._selection.querySelector(".baseline-select");
    addListener(baselineDropdown, (filterSet, val) => filterSet.getWithBaseline(val));
  }
}

/**
 * Presenter for custom metric configuration and management.
 *
 * Handles the definition and presentation of custom metrics that combine
 * multiple submetrics based on user selection. Manages dialog interactions
 * and maintains the current custom metric definition.
 */
class CustomMetricPresenter {
  /**
   * Create a new CustomMetricPresenter.
   *
   * @param {string} dialogId - ID of the configuration dialog element.
   * @param {Array<string>} availableOptions - Array of available submetric options.
   * @param {Function} onSelectionChanged - Callback when custom definition changes.
   * @param {string} dropdownSelector - CSS selector for the submetric dropdown to update.
   */
  constructor(dialogId, availableOptions, onSelectionChanged, dropdownSelector) {
    const self = this;
    self._dialog = document.getElementById(dialogId);
    self._availableOptions = availableOptions;
    self._onSelectionChanged = onSelectionChanged;
    self._dropdownSelector = dropdownSelector;

    if (!self._dialog) {
      throw new Error(`Dialog with id '${dialogId}' not found`);
    }

    self._setupEventListeners();
  }

  /**
   * Get the current custom metric definition.
   *
   * @returns {Array<string>|null} Array of selected submetrics or null if none.
   */
  getCurrentDefinition() {
    const self = this;
    const checkboxes = self._dialog.querySelectorAll('input[type="checkbox"]:checked');
    const selected = Array.from(checkboxes).map((cb) => cb.value);
    return selected.length > 0 ? selected : null;
  }

  /**
   * Set the current custom metric definition.
   *
   * @param {Array<string>|null} definition - Array of submetrics to select.
   */
  setCurrentDefinition(definition) {
    const self = this;
    const checkboxes = self._dialog.querySelectorAll('input[type="checkbox"]');

    checkboxes.forEach((checkbox) => {
      checkbox.checked = definition && definition.includes(checkbox.value);
    });

    self._validateSelection();
  }

  /**
   * Show the custom metric configuration dialog.
   */
  showDialog() {
    const self = this;
    self._dialog.showModal();

    // Focus first checkbox
    const firstCheckbox = self._dialog.querySelector('input[type="checkbox"]');
    if (firstCheckbox) {
      firstCheckbox.focus();
    }
  }

  /**
   * Hide the custom metric configuration dialog.
   */
  hideDialog() {
    const self = this;
    self._dialog.close();
  }

  /**
   * Validate current selection and enable/disable Apply button.
   *
   * Ensures at least one checkbox is selected before enabling Apply button.
   * Updates Apply button disabled state and shows/hides validation messages.
   *
   * @private
   */
  _validateSelection() {
    const self = this;
    const selected = self.getCurrentDefinition();
    const applyButton = self._dialog.querySelector(".primary");

    if (selected && selected.length > 0) {
      applyButton.classList.remove("disabled");
      applyButton.style.pointerEvents = "auto";
      applyButton.style.opacity = "1";
    } else {
      applyButton.classList.add("disabled");
      applyButton.style.pointerEvents = "none";
      applyButton.style.opacity = "0.5";
    }
  }

  /**
   * Set up event listeners for dialog interactions.
   *
   * @private
   */
  _setupEventListeners() {
    const self = this;

    // Apply button listener
    const applyButton = self._dialog.querySelector(".primary");
    applyButton.addEventListener("click", (event) => {
      event.preventDefault();
      const selected = self.getCurrentDefinition();
      if (selected && selected.length > 0) {
        self._onSelectionChanged(selected);
        self.hideDialog();
      }
    });

    // Cancel button listener
    const cancelButton = self._dialog.querySelector(".secondary");
    cancelButton.addEventListener("click", (event) => {
      event.preventDefault();
      self.hideDialog();
    });

    // Checkbox change listeners for immediate validation
    const checkboxes = self._dialog.querySelectorAll('input[type="checkbox"]');
    checkboxes.forEach((checkbox) => {
      checkbox.addEventListener("change", () => {
        self._validateSelection();
      });
    });

    // Initialize validation state
    self._validateSelection();
  }
}

/**
 * Presenter for managing dimension labels based on filter context.
 *
 * Handles dynamic label updates for dimension radio buttons based on
 * the current metric selection (e.g., changing "Substances" to "Equipment Models"
 * when viewing equipment/population data).
 */
class DimensionPresenter {
  /**
   * Create a new DimensionPresenter.
   *
   * @param {DimensionCardPresenter} dimensionCardPresenter - The dimension
   *     card presenter to coordinate with.
   */
  constructor(dimensionCardPresenter) {
    const self = this;
    self._dimensionCardPresenter = dimensionCardPresenter;
  }

  /**
   * Set the label for the substances dimension.
   *
   * @param {string} labelText - The text to use for the substances label.
   */
  setSustancesLabel(labelText) {
    const self = this;
    self._dimensionCardPresenter.setLabel("substances", labelText);
  }

  /**
   * Update the substances label based on the current filter state.
   *
   * @param {FilterSet} filterSet - Current filter settings.
   */
  updateSubstancesLabel(filterSet) {
    const self = this;
    const isPopulation = filterSet.getMetric() === "population";
    const labelText = isPopulation ? "Equipment" : "Substances";
    self.setSustancesLabel(labelText);
  }
}

export {ResultsPresenter};