• Home
  • MSE papers
    • Guide to the papers
    • SC14 questions and tasks
    • Full MSE report
    • Technical paper
    • Response to review
  • Candidate MPs
    • CMP registry
    • MP43 focal hockeystick
    • MP44 focal power ramp
    • MP29 sensitivity
    • MP32 sensitivity
    • Earlier tuning controls
    • Parts of a CMP
    • How to specify a CMP
  • Annual use
    • Annual calculation
    • Input data
    • Worked example
  • Results & background
    • Results explorer
    • Scorecard
    • Scorecard: best-relative weights
    • SCW17 background and results
    • How MSE fits
    • Sources & changes
    • Agreement checks
  1. Results and background
  2. Interactive CMP scorecard explorer
  • SPRFMO Jack Mackerel Candidate Management Procedures
  • MSE papers
    • Guide to the Jack Mackerel MSE papers for SC14
    • Jack mackerel MSE: questions and tasks for SC14
    • Jack mackerel management strategy evaluation for SC14
    • Jack mackerel MSE: technical information and work remaining
    • Response to the independent SC14 MSE desk review
  • Candidate management procedures
    • CMP registry
    • MP43 focal hockeystick case
    • MP44 focal power-ramp case
    • MP29 hockeystick candidate
    • MP32 powerramp candidate
    • Earlier trial and robustness controls
    • Management-procedure components
    • CMP specification template
  • Annual use
    • Annual CMP application
    • Illustrative 2027 TAC calculation
    • Annual input data
  • Results and background
    • Load candidates into Slick
    • Interactive CMP scorecard explorer
    • Interactive CMP scorecard: best-relative weighting
    • CMP results and development history
    • How the MSE work fits together
    • Sources and record of changes
    • Checks of agreement among the SC14 files
  1. Results and background
  2. Interactive CMP scorecard explorer

Interactive CMP scorecard explorer

Static browser application using the current reference-OM performance metrics

This browser-based explorer reproduces the relative-preference scorecard without requiring R or a Shiny server. Select CMPs and metrics, choose a weighting scheme, or enter weights manually. Scores are recalculated relative to the selected CMP set and are not absolute acceptability scores or agreed management preferences.

Weighting scheme

CMPs

Performance metrics

CMP relative trade-off scores

Selected metric weights

Performance quilt

Purple indicates lower and light lavender higher relative preference within each selected metric.

Load candidates into Slick
Interactive CMP scorecard: best-relative weighting
Source Code
---
title: "Interactive CMP scorecard explorer"
subtitle: "Static browser application using the current reference-OM performance metrics"
toc: false
page-layout: full
---

This browser-based explorer reproduces the relative-preference scorecard
without requiring R or a Shiny server. Select CMPs and metrics, choose a
weighting scheme, or enter weights manually. Scores are recalculated relative
to the selected CMP set and are **not absolute acceptability scores or agreed
management preferences**.

<style>
.scorecard-app { --panel-border:#d9dde3; --muted:#5d6670; }
.scorecard-controls { display:grid; grid-template-columns:repeat(3,minmax(240px,1fr)); gap:1rem; margin:1rem 0; }
.scorecard-panel { border:1px solid var(--panel-border); border-radius:.45rem; padding:1rem; background:#fff; }
.scorecard-panel h3 { margin-top:0; font-size:1.05rem; }
.scorecard-options { max-height:260px; overflow:auto; display:grid; gap:.3rem; }
.scorecard-options label { display:flex; gap:.45rem; align-items:center; font-weight:400; }
.scorecard-custom { display:none; margin-top:.8rem; }
.scorecard-custom.active { display:grid; gap:.35rem; }
.scorecard-custom label { display:grid; grid-template-columns:1fr 90px; gap:.5rem; align-items:center; }
.scorecard-custom input { width:100%; }
.scorecard-output { margin-top:1rem; }
#score-plot { width:100%; min-height:430px; }
.scorecard-table-wrap { overflow-x:auto; }
.scorecard-app table { width:100%; border-collapse:collapse; margin-top:.75rem; }
.scorecard-app th,.scorecard-app td { border-bottom:1px solid #e3e6ea; padding:.45rem .55rem; text-align:right; }
.scorecard-app th:first-child,.scorecard-app td:first-child { text-align:left; }
.quilt-cell { text-align:center!important; min-width:95px; color:#18141d; }
.scorecard-note { color:var(--muted); font-size:.92rem; }
.scorecard-error { color:#a12622; font-weight:600; min-height:1.5rem; }
@media (max-width:900px) { .scorecard-controls { grid-template-columns:1fr; } }
</style>

```{=html}
<div class="scorecard-app">
  <div class="scorecard-controls">
    <section class="scorecard-panel">
      <h3>Weighting scheme</h3>
      <select id="weight-scheme" class="form-select">
        <option value="equal">Equal weights</option>
        <option value="balanced">Balanced fishing and stock condition</option>
        <option value="dispersion" selected>Dispersion weighted (square-root CV)</option>
        <option value="custom">Specify weights manually</option>
      </select>
      <div id="custom-weights" class="scorecard-custom"></div>
    </section>
    <section class="scorecard-panel">
      <h3>CMPs</h3>
      <div id="cmp-options" class="scorecard-options"></div>
    </section>
    <section class="scorecard-panel">
      <h3>Performance metrics</h3>
      <div id="metric-options" class="scorecard-options"></div>
    </section>
  </div>

  <div id="scorecard-error" class="scorecard-error"></div>

  <section class="scorecard-panel scorecard-output">
    <h3>CMP relative trade-off scores</h3>
    <svg id="score-plot" role="img" aria-label="CMP score bar chart"></svg>
    <div id="score-table" class="scorecard-table-wrap"></div>
  </section>

  <section class="scorecard-panel scorecard-output">
    <h3>Selected metric weights</h3>
    <div id="weight-table" class="scorecard-table-wrap"></div>
  </section>

  <section class="scorecard-panel scorecard-output">
    <h3>Performance quilt</h3>
    <p class="scorecard-note">Purple indicates lower and light lavender higher relative preference within each selected metric.</p>
    <div id="quilt-table" class="scorecard-table-wrap"></div>
  </section>
</div>
```

<script>
(async function () {
  const dataUrl = "../data/candidates/candidate_scorecard_input_reference.json";
  const colors = {
    "HS+20 (MP29)":"#C87A8A", "HS-20 (MP43)":"#B6875B",
    "HSsym (MP45)":"#909646", "HS-30 (MP47)":"#55A067",
    "PR+20 (MP32)":"#00A396", "PR-20 (MP44)":"#409BBB",
    "PRsym (MP46)":"#9189C7", "PR-30 (MP48)":"#BE7AB4"
  };
  const stockMetrics = new Set([
    "SB / SB[MSY]", "F / F[MSY]", "VB / VB[2025]", "VB / VB[MSY]",
    "P(Kobe red)"
  ]);
  const fishingMetrics = new Set(["Catch", "IACC", "Mean catch reduction"]);
  const unique = xs => [...new Set(xs)];
  const mean = xs => xs.reduce((a,b) => a+b, 0) / xs.length;
  const sd = xs => {
    if (xs.length < 2) return 0;
    const m = mean(xs);
    return Math.sqrt(xs.reduce((s,x) => s+(x-m)*(x-m), 0)/(xs.length-1));
  };
  const escapeHtml = value => String(value)
    .replaceAll("&","&amp;").replaceAll("<","&lt;")
    .replaceAll(">","&gt;").replaceAll('"',"&quot;");

  let rows;
  try {
    const response = await fetch(dataUrl);
    if (!response.ok) throw new Error(`Data request failed (${response.status})`);
    rows = await response.json();
  } catch (error) {
    document.getElementById("scorecard-error").textContent =
      `Could not load scorecard data: ${error.message}`;
    return;
  }

  const cmps = unique(rows.map(d => d.mp));
  const metrics = unique(rows.map(d => d.metric));
  const defaultMetrics = unique(rows.filter(d => d.include).map(d => d.metric));

  function optionList(containerId, values, selected, prefix) {
    document.getElementById(containerId).innerHTML = values.map((value, i) =>
      `<label><input type="checkbox" id="${prefix}-${i}" value="${escapeHtml(value)}" ${selected.includes(value)?"checked":""}> ${escapeHtml(value)}</label>`
    ).join("");
  }
  optionList("cmp-options", cmps, cmps, "cmp");
  optionList("metric-options", metrics, defaultMetrics, "metric");

  function selectedValues(containerId) {
    return [...document.querySelectorAll(`#${containerId} input:checked`)]
      .map(input => input.value);
  }

  function normalize(selectedRows, selectedMetrics) {
    const output = selectedRows.map(d => ({...d}));
    selectedMetrics.forEach(metric => {
      const subset = output.filter(d => d.metric === metric);
      const values = subset.map(d => Number(d.raw_value));
      const lo = Math.min(...values), hi = Math.max(...values), span = hi-lo;
      subset.forEach(d => {
        if (span === 0) d.preference = 1;
        else if (d.preferred_direction === "higher is better")
          d.preference = (Number(d.raw_value)-lo)/span;
        else d.preference = (hi-Number(d.raw_value))/span;
      });
    });
    return output;
  }

  function renderCustomWeights(selectedMetrics) {
    const container = document.getElementById("custom-weights");
    const existing = Object.fromEntries([...container.querySelectorAll("input")]
      .map(input => [input.dataset.metric, input.value]));
    container.innerHTML = selectedMetrics.map((metric, i) =>
      `<label>${escapeHtml(metric)}<input type="number" min="0" step="0.1" value="${existing[metric] ?? 1}" data-metric="${escapeHtml(metric)}" id="custom-${i}"></label>`
    ).join("");
    container.classList.toggle("active",
      document.getElementById("weight-scheme").value === "custom");
    container.querySelectorAll("input").forEach(input =>
      input.addEventListener("input", update));
  }

  function calculateWeights(normalizedRows, selectedMetrics, scheme) {
    let raw = {};
    if (scheme === "equal") selectedMetrics.forEach(m => raw[m] = 1);
    if (scheme === "balanced") {
      const stock = selectedMetrics.filter(m => stockMetrics.has(m));
      const fishing = selectedMetrics.filter(m => fishingMetrics.has(m));
      const other = selectedMetrics.filter(m => !stockMetrics.has(m) && !fishingMetrics.has(m));
      stock.forEach(m => raw[m] = 0.5/stock.length);
      fishing.forEach(m => raw[m] = 0.5/fishing.length);
      other.forEach(m => raw[m] = 1/other.length);
    }
    if (scheme === "dispersion") selectedMetrics.forEach(metric => {
      const values = normalizedRows.filter(d => d.metric === metric)
        .map(d => Number(d.raw_value));
      const cv = Math.abs(mean(values)) > 0 ? sd(values)/Math.abs(mean(values)) : 0;
      raw[metric] = Math.sqrt(Number.isFinite(cv) ? cv : 0);
    });
    if (scheme === "custom") selectedMetrics.forEach(metric => {
      const input = [...document.querySelectorAll("#custom-weights input")]
        .find(el => el.dataset.metric === metric);
      raw[metric] = Math.max(0, Number(input?.value ?? 0));
    });
    const total = Object.values(raw).reduce((a,b) => a+b, 0);
    if (!(total > 0)) throw new Error("At least one metric weight must be positive.");
    return Object.fromEntries(Object.entries(raw).map(([k,v]) => [k,v/total]));
  }

  function drawBars(scores) {
    const svg = document.getElementById("score-plot");
    const width=960, height=470, left=70, right=20, top=30, bottom=120;
    const innerW=width-left-right, innerH=height-top-bottom;
    const band=innerW/scores.length, barW=band*0.66;
    let markup = `<rect width="${width}" height="${height}" fill="white"/>`;
    [0,25,50,75,100].forEach(tick => {
      const y=top+innerH-(tick/100)*innerH;
      markup += `<line x1="${left}" x2="${width-right}" y1="${y}" y2="${y}" stroke="#dfe2e6"/><text x="${left-10}" y="${y+5}" text-anchor="end" font-size="13">${tick}</text>`;
    });
    scores.forEach((d,i) => {
      const x=left+i*band+(band-barW)/2, h=(d.score/100)*innerH, y=top+innerH-h;
      markup += `<rect x="${x}" y="${y}" width="${barW}" height="${h}" fill="${colors[d.mp]||"#777"}"/>`;
      markup += `<text x="${x+barW/2}" y="${y-7}" text-anchor="middle" font-size="14">${d.score.toFixed(1)}</text>`;
      markup += `<text transform="translate(${x+barW/2},${top+innerH+18}) rotate(38)" text-anchor="start" font-size="12">${escapeHtml(d.mp)}</text>`;
    });
    markup += `<text transform="translate(18,${top+innerH/2}) rotate(-90)" text-anchor="middle" font-size="14">Relative trade-off score (0–100)</text>`;
    markup += `<text x="${left+innerW/2}" y="${height-8}" text-anchor="middle" font-size="14">CMP (ordered from highest score)</text>`;
    svg.setAttribute("viewBox",`0 0 ${width} ${height}`);
    svg.innerHTML=markup;
  }

  function tableHtml(headers, bodyRows, classes="") {
    return `<table class="${classes}"><thead><tr>${headers.map(h=>`<th>${escapeHtml(h)}</th>`).join("")}</tr></thead><tbody>${bodyRows.map(row=>`<tr>${row.map((cell,i)=>`<td${i?"":""}>${cell}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
  }

  function quiltColor(preference) {
    const low=[121,82,168], high=[244,239,248];
    return `rgb(${low.map((v,i)=>Math.round(v+(high[i]-v)*preference)).join(",")})`;
  }

  function update() {
    const selectedCmps=selectedValues("cmp-options");
    const selectedMetrics=selectedValues("metric-options");
    const error=document.getElementById("scorecard-error"); error.textContent="";
    renderCustomWeights(selectedMetrics);
    if (selectedCmps.length<2) { error.textContent="Select at least two CMPs."; return; }
    if (!selectedMetrics.length) { error.textContent="Select at least one metric."; return; }
    try {
      const filtered=rows.filter(d=>selectedCmps.includes(d.mp)&&selectedMetrics.includes(d.metric));
      const normalized=normalize(filtered,selectedMetrics);
      const weights=calculateWeights(normalized,selectedMetrics,
        document.getElementById("weight-scheme").value);
      const scores=selectedCmps.map(mp=>({mp,score:normalized.filter(d=>d.mp===mp)
        .reduce((sum,d)=>sum+100*d.preference*weights[d.metric],0)}))
        .sort((a,b)=>b.score-a.score || a.mp.localeCompare(b.mp));
      drawBars(scores);
      document.getElementById("score-table").innerHTML=tableHtml(
        ["Rank","CMP","Score"], scores.map((d,i)=>[i+1,escapeHtml(d.mp),d.score.toFixed(2)]));
      document.getElementById("weight-table").innerHTML=tableHtml(
        ["Metric","Weight (%)"], selectedMetrics.map(m=>[escapeHtml(m),(100*weights[m]).toFixed(2)]));
      const quiltRows=scores.map(s=>s.mp);
      const quiltBody=quiltRows.map(mp=>[escapeHtml(mp),...selectedMetrics.map(metric=>{
        const d=normalized.find(x=>x.mp===mp&&x.metric===metric);
        let label=Number(d.raw_value).toFixed(2);
        if (d.statistic==="C") label=Number(d.raw_value).toFixed(0);
        if (["SB0red","PC270"].includes(d.statistic)) label=(100*Number(d.raw_value)).toFixed(1)+"%";
        return `<span>${label}</span>`;
      })]);
      let quilt=tableHtml(["CMP",...selectedMetrics],quiltBody,"quilt");
      document.getElementById("quilt-table").innerHTML=quilt;
      const cells=document.querySelectorAll("#quilt-table tbody tr");
      cells.forEach((tr,rowIndex)=>selectedMetrics.forEach((metric,colIndex)=>{
        const d=normalized.find(x=>x.mp===quiltRows[rowIndex]&&x.metric===metric);
        const td=tr.children[colIndex+1]; td.classList.add("quilt-cell");
        td.style.backgroundColor=quiltColor(d.preference);
      }));
    } catch (e) { error.textContent=e.message; }
  }

  document.getElementById("weight-scheme").addEventListener("change",update);
  document.querySelectorAll("#cmp-options input,#metric-options input")
    .forEach(input=>input.addEventListener("change",update));
  update();
})();
</script>

SPRFMO Jack Mackerel Working Group

 
  • View source
  • Report an issue

Specifications and results are version controlled.