ardeaStat
Jon C. Herron
  • Data
  • Scatterplot
  • Histogram
  • Strip chart
  • Marimekko chart
  • Info
more

Filter data by searching. Results match all terms.

Select individual rows with floating checkboxes next to the first column. Sort the table by clicking column heads.

viewof search = Inputs.search(data);
function getCustomHeaders(myObject) {
  const customHeaders = {};
    for (const key in myObject) {
      const newVal = formatLabel(key);
      customHeaders[key] = newVal;
    };
    
  return customHeaders;
};
viewof filtered = Inputs.table(search, {rows: 1000, 
  // required: false, 
  header: getCustomHeaders(search[0])
 });

ardeaStat
ardea rapid data exploration & analysis


Welcome to ardeaStat

  1. Choose a built-in dataset from the menu below or upload your own CSV file.

  2. Pick a graph type from the menu bar at the top.

  3. Customize your graph and add a formal statistical analysis using the controls in the sidebar on the left. I suggest working from top to bottom.

To print or save your graph, click the expand button in the lower right corner of the graph panel. Then take a screenshot or use your browser’s print command.


html`<label>Use a built-in dataset:</label>`
viewof builtIn = html`
  <select name="biSel" style="width: 80%; margin-left: 0.25em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${setList(builtInDataSets)}
  </select>`

viewof uploaded = Inputs.file({value: null, label: "Or choose/drag a csv file:", width: "5"});
{
// When menu changes, clear upload
if (builtIn !== "") {
  set(viewof uploaded.file, null);
  }
}
{ 
// When file is dragged, clear menu
if (uploaded !== null) {
  set(viewof builtIn, "");
  }
}

Inputs.button([
  ["Reset data", () => resetDataControls()]
  ]
  );
Inputs.button([
  ["Reset controls for all graphs", () => resetAllControls()]
  ]
  );
builtInDataSets = "Default"; // "Default" or "RDA"

function setList(flag) {
  if (flag === "RDA") {
    return [
//    "<option value='empty' > </option>",
    "<option value='blackbirds' selected>Blackbirds</option>",
    "<option value='eelgrass' >Eelgrass</option>",
    "<option value='monkeyflower' >Monkeyflower</option>",
    "<option value='oysters' >Oysters</option>",
    "<option value='phytoliths' >Phytoliths</option>",
    "<option value='silver-fir' >Silver fir</option>"
      ];
    } else { 
    return [
//    "<option value='empty' > </option>",
    "<option value='palmer-penguins' selected>Palmer penguins</option>",
    "<option value='elephants' >Elephants</option>",
    "<option value='primates-and-carnivores' >Primates and carnivores</option>",
    "<option value='hurricane-lizards-one' >Hurricane lizards one</option>",
    "<option value='hurricane-lizards-two' >Hurricane lizards two</option>"
      ];
    };  
};

attached = {
 
  switch (builtIn) {
 
  case "palmer-penguins":
    return FileAttachment("datasets/palmer-penguins.csv");
    break;
  
  case "elephants":
    return FileAttachment("datasets/elephants.csv");
    break;
    
  case "primates-and-carnivores":
    return FileAttachment("datasets/primates-and-carnivores.csv");
    break;
    
  case "hurricane-lizards-one":
    return FileAttachment("datasets/hurricane-lizards-one.csv");
    break;
    
  case "hurricane-lizards-two":
    return FileAttachment("datasets/hurricane-lizards-two.csv");
    break;
    
  case "blackbirds":
    return FileAttachment("datasets/blackbirds.csv");
    break;
    
  case "eelgrass":
    return FileAttachment("datasets/eelgrass.csv");
    break;
  
  case "monkeyflower":
    return FileAttachment("datasets/monkeyflower.csv");
    break;
    
  case "oysters":
    return FileAttachment("datasets/oysters.csv");
    break;
  
  case "phytoliths":
    return FileAttachment("datasets/phytoliths.csv");
    break;
    
  case "silver-fir":
    return FileAttachment("datasets/silver-fir.csv");
    break;
  
  default:
    return (builtInDataSets == "Default") ? FileAttachment("datasets/penguins.csv") :
            FileAttachment("datasets/blackbirds.csv");

  };

};

data = {

if (uploaded !== null) {
 
  return uploaded
    .csv({ typed: true });
    
} else {

  return attached
    .csv({ typed: true });

};

};

function resetDataControls() {
  set(viewof uploaded.file, null);
  (builtInDataSets == "Default") ? set(viewof builtIn, "palmer-penguins") : 
    set(viewof builtIn, "blackbirds");
  
};

categoricalColumns = getCategoricalColumns(filtered);
numericalColumns = getNumericalColumns(filtered);
allColumns = filtered.columns;

function resetAllControls() {
  resetScatterplotControls();
  resetHistogramControls();
  resetStripControls();
  resetBarChartControls();
  
};

function ifOneThingThenAnother(oneThing, anotherThing) {
  // alert('this and that');
  return anotherThing;
};

ifOneThingThenAnother(data, resetAllControls());
Plot.plot({
  grid: false,
  marginTop: 36, // more room for facets
  marginBottom: 48,
  marks: [
    Plot.axisX({  // Axis with just the ticks in the default fontSize
      label: null,
      // ticks: (scatterFacetX !== "") ? 4 : 8,
      }),
    Plot.axisX({  // Axis with just the label in custom fontSize
      label: formatLabel(scatterX),
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    Plot.axisY({  // Axis with just the ticks in the default fontSize
      label: null,
      // ticks: (scatterFacetX !== "") ? 4 : 8,
      }),
    Plot.axisY({  // Axis with just the label in the custom fontSize
      label: formatLabel(scatterY),
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    (scatterRegr=="Line")?Plot.linearRegressionY(
        filtered, {
        x: scatterX,
        y: scatterY,
        stroke: scatterRegrFill,
        ci: 0}):[],
    (scatterRegr=="± 95% CI")?Plot.linearRegressionY(
       filtered, {
       x: scatterX,
       y: scatterY,
       stroke: scatterRegrFill,
       ci: 0.95}):[],
     Plot.dot(
       filtered, {
       x: scatterX,
       y: scatterY,
       r: 5,
       fill: scatterFill,
       opacity: scatterOpacity,
     }),
  ],
  height: scatterHeight,
  width: scatterWidth,
  marginLeft: 60
});
html
`<div id="scatterStats" autocorrect="off" spellcheck="false" style="display: ${scatterStatsDisplay}; padding-left: 1em; font-size: 80%;">
  
  <caption>
    <b>Least-squares linear regression</b>
    <p><span style="color: gray;">Best-fit line: y = </span>${myOLSModelLinearRegr.coef[1].toPrecision(3)}<span style="color: gray;">x ${(myOLSModelLinearRegr.coef[0] < 0) ? "-" : "+"}</span> ${Math.abs(myOLSModelLinearRegr.coef[0].toPrecision(3))}</p>
  </caption>
  <table style="line-height: 75%;">
      <tr>
        <td style="padding: 0.5em; color: gray;">Source</td>
        <td style="padding: 0.5em; color: gray;">Deg fr</td>
        <td style="padding: 0.5em; color: gray;">Sum Sq</td>
        <td style="padding: 0.5em; color: gray;">Mean Sq</td>
        <td style="padding: 0.5em; color: gray;"><i>F</i></td>
        <td style="padding: 0.5em; color: gray;"><i>P</i> - value</td>
      </tr>
    <tbody>
      <tr>
        <td style="padding: 0.5em; color: gray;">Regression</td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.df_model}</td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.SSE.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelLinearRegr.SSE / myOLSModelLinearRegr.df_model).toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${myOLSModelLinearRegr.f.F_statistic.toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${pValStr(myOLSModelLinearRegr.f.pvalue)}</td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Residual</td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.SSR.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelLinearRegr.SSR / myOLSModelLinearRegr.df_resid).toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Total<br><br><span style="color: steelblue;"><i>R</i><sup> 2</sup> = ${myOLSModelLinearRegr.R2.toPrecision(5)}</span></td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.df_model + myOLSModelLinearRegr.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelLinearRegr.SST.toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
    </tbody>
  </table>
</div>`
html
`<div id="scatterEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
html`<label>→ Plot as X:</label>`
viewof scatterX = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(numericalColumns, 0)}
  </select>`
html`<label>↑ Plot as Y:</label>`
viewof scatterY = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(numericalColumns, 1)}
  </select>`

//viewof scatterFill = Inputs.select(
//  [null].concat(filtered.columns),
//  {width: "5"}
//  );

// viewof scatterFill = html`
//  <select style="width: 85%; margin-top: 0.25em; margin-bottom: 0.25em; margin-left: 0.25em; height: 1.7em; background-color: white;">
//  ${populateWithOptions(myColors, 3)}
//  </select>`

scatterFill = "steelblue";
//viewof scatterOpacity = Inputs.number([0, 1], {step: 0.1, value: 0.6, width: "5"});

viewof scatterOpacity = html`<input type="range" min="0" max="1" value="0.7" step="any" style="width: 89.5%; height: 2em; margin-top: 0em; opacity: 0.6;" />`

viewof scatterRegr = Inputs.radio(["No", "Line", "± 95% CI"], {label: "Linear regression:", value: "No"});
// Created to support choice of colors
scatterRegrFill = "gray";

html`<label>Plot width:</label>`
viewof scatterWidth = html`<input type="range" min="50" max="1028" value="640" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
html`<label>Plot height:</label>`
viewof scatterHeight = html`<input type="range" min="50" max="768" value="500" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
viewof scatterAddCap = Inputs.toggle({label: "Add caption"});

Inputs.button([
  ["Reset scatterplot", () => {
    resetScatterplotControls()
  }]
]);
fillType = typeof data[0][scatterFill];

scatterQuill = new Quill('#scatterEditor', {
    modules: {
      toolbar: false, // ['bold', 'italic'],
      },
    theme: 'bubble',
  });

quillDisplay(scatterAddCap, "scatterEditor");
quillSetWidth(scatterWidth, "scatterEditor");
scatterStatsDisplay = statsDisplay( ( (scatterRegr !== "No") && (currentHash === "#scatterplot") ), "scatterStats");

function resetScatterplotControls() {
  set(viewof scatterX, numericalColumns[0]);
  set(viewof scatterY,
    (numericalColumns.length > 1) ?
     numericalColumns[1] : numericalColumns[0]);
  // set(viewof scatterFill, "steelblue");
  set(viewof scatterOpacity, 0.6);
  set(viewof scatterRegr, "No");
  // set(viewof scatterGrpRegr, "No"); // NOT NEEDED because redundant
  set(viewof scatterHeight, 500);
  set(viewof scatterWidth, 640);
  set(viewof scatterAddCap, false);
  quillResetText(scatterQuill);

};

// Experimental for statistical functions

scatDataSansMissing = filtered.filter(d => (!isNaN(d[scatterX]))
                                         && (d[scatterX] !== null)
                                         && (d[scatterX] !== "")
                                         && (!isNaN(d[scatterY]))
                                         && (d[scatterY] !== null)
                                         && (d[scatterY] !=="")
                                         );

// myRegression = new ML.SimpleLinearRegression(scatDataSansMissing.map(d => d[scatterY]), scatDataSansMissing.map(d => d[scatterX]));

// myRegrCoefficients = myRegression.coefficients;

// myCorrelation = myRegression.score(scatDataSansMissing.map(d => d[scatterY]), scatDataSansMissing.map(d => d[scatterX]));

function getArrayForLinearRegr(data, column) {
  // alert('getting array for linear regr');
  const secondCol = data.map((row) => row[column]);
  const firstCol = [];
  const twoDArr = [];
  for(let i = 0; i < secondCol.length; i++) {
    firstCol.push(1);
  };
  for(let i = 0; i < secondCol.length; i++) {
    twoDArr.push([firstCol[i], secondCol[i]]);
  };
  return twoDArr;
};

myOLSModelLinearRegr = ( (scatterRegr !== "No") && (currentHash === "#scatterplot") ) ? // Kluge to avoid calculating a complex model when not needed.
    new myJstat.models.ols(
      columnToArray(scatDataSansMissing, scatterY),
      getArrayForLinearRegr(scatDataSansMissing, scatterX)
      )
    :
    new myJstat.models.ols(
      [0, 1],
      [[1, 0], [1, 1]]
      )
    ;
myHisto = Plot.plot({
  height: histoHeight,
  width: histoWidth,
  y: {grid: true},
//   figure: true,
  facet: {
      data: filtered,
      x: histFacetX,
      y: histFacetY,
      marginRight: 80
    },
  color: {
    legend: true, 
    label: formatLabel(histoFill),
    tickFormat: formatLabel,
    },
  marginTop: 36, // more room for facets
  marginBottom: 48,
  marks: [
    Plot.axisX({  // Axis with just the ticks in the default fontSize
      label: null,
      }),
    Plot.axisX({  // Axis with just the label in custom fontSize
      label: formatLabel(histoX),
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    (histFacetX !== "") ? Plot.axisFx({  // Facet axis with just the ticks
       label: null,
       tickFormat: formatLabel,
       }) : [],
    (histFacetX !== "") ? Plot.axisFx({  // Facet axis with just the label
       label: formatLabel(histFacetX),
       fontSize: largeFontSize,
       ticks: [ ],
       }) : [],
    Plot.axisY({  // Axis with just the ticks in the default fontSize
      label: null,
      }),
    Plot.axisY({  // Axis with just the label in the custom fontSize
      label: formatLabel(histoY),
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    (histFacetY !== "") ? Plot.axisFy({  // Facet axis with just the ticks
      label: null,
      tickFormat: formatLabel,
      }) : [],
    (histFacetY !== "") ? Plot.axisFy({  // Facet axis with just the label
       label: formatLabel(histFacetY),
       labelAnchor: "top",
       fontSize: largeFontSize,
       ticks: [ ],
      }) : [],
    Plot.rectY(filtered,
      Plot.binX({y: histoY},
       {x: histoX,
       fill: (histoFill !== "") ? histoFill : "steelblue",
       opacity: histoOpacity,
       thresholds: 20}
       )
       ),
    // Mean and SE marks
    ( (histoStats == "± SE") || (histoStats == "± 95% CI") )?
      Plot.dot(filtered,
        Plot.groupY(
         {x: "mean"},
         {x: histoX, 
         y: 0,
         fill: 
           // (histoFill !== "") ? histoFill :
           "black",
         r: 5}
         )) : [],
   (histoStats == "± SE") ?
      Plot.link(filtered,
        Plot.groupY({
         x1: (filtered) => d3.mean(filtered) 
           - d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         x2: (filtered) => d3.mean(filtered) 
           + d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {x: histoX,
         y: 0, 
         stroke: 
           // (histoFill !== "") ? histoFill : 
           "black",
         strokeWidth: 3}
         )) : [],
    (histoStats == "± 95% CI") ?
      Plot.link(filtered,
        Plot.groupY({
         x1: (filtered) => d3.mean(filtered) 
           - 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         x2: (filtered) => d3.mean(filtered) 
           + 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {x: histoX,
         y: 0,
         stroke:
           // (histoFill !== "") ? histoFill : 
           "black",
         strokeWidth: 3}
         )) : [],
    // Plot.ruleY([0])
  ]
});

html
`<div id="histoStatistics" autocorrect="off" spellcheck="false" style="display: ${histoStatsDisplay}; padding-left: 1em; font-size: 80%;">

<caption>
    <b>One-way ANOVA</b>
  </caption>
  <table style="line-height: 75%;">
      <tr>
        <td style="padding: 0.5em; color: gray;">Source</td>
        <td style="padding: 0.5em; color: gray;">Deg fr</td>
        <td style="padding: 0.5em; color: gray;">Sum Sq</td>
        <td style="padding: 0.5em; color: gray;">Mean Sq</td>
        <td style="padding: 0.5em; color: gray;"><i>F</i></td>
        <td style="padding: 0.5em; color: gray;"><i>P</i> - value</td>
      </tr>
    <tbody>
      <tr>
        <td style="padding: 0.5em; color: gray;">Between grps</td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.df_model}</td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.SSE.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelHistoANOVA.SSE / myOLSModelHistoANOVA.df_model).toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${myOLSModelHistoANOVA.f.F_statistic.toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${pValStr(myOLSModelHistoANOVA.f.pvalue)}</td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Within grps</td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.SSR.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelHistoANOVA.SSR / myOLSModelHistoANOVA.df_resid).toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Total<br><br><span style="color: steelblue;"><i>R</i><sup> 2</sup> = ${myOLSModelHistoANOVA.R2.toPrecision(5)}</span></td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.df_model + myOLSModelHistoANOVA.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelHistoANOVA.SST.toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
    </tbody>
  </table>
</div>`
html
`<div id="histoEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
html`<label style="padding-top: 1em;">→ Plot as X:</label>`
viewof histoX = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.75em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(numericalColumns, 0)}
  </select>`
html`<label style="padding-top: 0em; margin-top: 0em;"><span style="font-size: 200%; vertical-align: -5%; padding-top: 0em; margin-top: 0em;">🁣 </span>Groups:</label>`
viewof histoFill = html`
  <select style="width: 80%; margin-top: 0.25em; margin-bottom: 0.25em; margin-left: 0.5em; height: 1.7em; background-color: white;">
  ${populateWithOptions(categoricalColumns, 0)}
  </select>`
// Was created to support a toggle between count versus percentage
histoY = "count";

// viewof histoOpacity = Inputs.number([0, 1], {step: 0.1, value: 0.5, width: 0});
  
viewof histoOpacity = html`<input type="range" min="0" max="1" value="0.7" step="any" style="width: 89.5%; height: 2em; margin-top: 0em; opacity: 0.6;" />`

viewof histoStats = Inputs.radio(
  ["No", "± SE", "± 95% CI" ],
  {label: "One-way ANOVA:", value: "No"}
  );

histFacetX = "";
histFacetY = histoFill;
html`<label>Plot width:</label>`
viewof histoWidth = html`<input type="range" min="50" max="1028" value="640" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
html`<label>Plot height:</label>`
viewof histoHeight = html`<input type="range" min="50" max="768" value="500" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
viewof histoAddCap = Inputs.toggle({label: "Add caption", value: false});

Inputs.button([
  ["Reset histogram", () => {
    resetHistogramControls()
  }]
]);
histoQuill = new Quill('#histoEditor', {
    modules: {
      toolbar: false, // ['bold', 'italic'],
      },
    theme: 'bubble',
  });

quillDisplay(histoAddCap, "histoEditor");
quillSetWidth(histoWidth, "histoEditor");
histoStatsDisplay = statsDisplay( ( (histoStats !== "No") && (currentHash === "#histogram") ), "histoStatistics");

function resetHistogramControls() {
  set(viewof histoX, numericalColumns[0]);
  set(viewof histoFill, categoricalColumns[0]);
  set(viewof histoOpacity, 0.7);
  // set(viewof histFacetX, "");
  // set(viewof histFacetY, "");
  set(viewof histoHeight, 500);
  set(viewof histoWidth, 640);
  set(viewof histoAddCap, false);
  set(viewof histoStats, "No");
  quillResetText(histoQuill);

};

// Experimental for statistical functions

histoANOVADataSansMissing = filtered.filter(d => (!isNaN(d[histoX])) 
                                              && (d[histoX] !== null)
                                              && (d[histoX] !== "")
                                              && (d[histoFill] !== null)
                                              && (d[histoFill] !== "")
                                              );

function getDummiesForHistoANOVARegr(data, groups) {
  
  const grpsCol = columnToArray(data, groups);
  const uniqueGrps = getUniqueVals(grpsCol).sort();
  const numDummies = uniqueGrps.length - 1;
  const dummiesArr = [];
  
  // Once per observation
  for (let i = 0; i < grpsCol.length; i++) {
    // Once per dummy needed
    const myRow = [];
    myRow.push(1);
    for (let j = 0; j < numDummies; j++) {
      if (grpsCol[i] === uniqueGrps[j]) {
        myRow.push(1);
        } else {
        myRow.push(0);
       };
    };
    
    dummiesArr.push(myRow);
    
  };

  return dummiesArr;
  
};

myOLSModelHistoANOVA = ( (histoStats !== "No") && (currentHash === "#histogram") )? // Kluge to avoid calculating a complex model when not needed.
    new myJstat.models.ols(
      columnToArray(histoANOVADataSansMissing, histoX),
      getDummiesForHistoANOVARegr(histoANOVADataSansMissing, histoFill)
      )
    :
      new myJstat.models.ols(
      [0, 1],
      [[1, 0], [1, 1]]
      )
    ;
( ((stripOrient == "Vertical ↑") && (stripX == stripDotFill)) || 
    ((stripOrient == "Horizontal →") && (stripY == stripDotFill)) ) ?

// Big IF stripDotFill is the same as the main x or y variable
// No subplots needed

Plot.plot({
  axis: null, // Will specify these in detail with axis marks
  symbol:
    {
     legend: true, 
     tickFormat: formatLabel,
     label: formatLabel(stripDotFill),
     },
  facet: {
    marginRight: (stripOrient == "Horizontal →") ? 90 : [],
    },
  height: stripHeight,
  insetTop: ((stripOrient == "Horizontal →") &&
            (dotsJitterOrDodge == "Jitter")) ? 
            10 : [],
  insetBottom: ((stripOrient == "Horizontal →") &&
               (dotsJitterOrDodge == "Jitter")) ?
               10 : [],
  insetLeft: ((stripOrient == "Vertical ↑") &&
             (dotsJitterOrDodge == "Jitter")) ? 
             10 : [],
  insetRight: ((stripOrient == "Vertical ↑") &&
              (dotsJitterOrDodge == "Jitter")) ? 
              10 : [],
  // marginTop: 36, // more room for facets
  marginBottom: 42,
  marks: [
    // Axis marks
    (stripOrient == "Vertical ↑") ? // Main axis w/o label
      Plot.axisY({
        label: null,
        }) : 
      Plot.axisX({
        label: null,
        }),
    (stripOrient == "Vertical ↑") ? // Main axis label, large font
       Plot.axisY({
         label: formatLabel(stripY),
         fontSize: largeFontSize,
         labelOffset: 36,
         ticks: [],
         }) : 
       Plot.axisX({
         label: formatLabel(stripX),
         fontSize: largeFontSize,
         labelOffset: 36,
         ticks: [],
         }),
    (stripOrient == "Vertical ↑") ? // Facet axis w/o label
      Plot.axisFx({
        anchor: "bottom",
        label: null,
        tickFormat: formatLabel,
        }) : 
      Plot.axisFy({
        label: null,
        tickFormat: formatLabel,
        }),
      (stripOrient == "Vertical ↑") ? // Facet axis label, large font
      Plot.axisFx({
        anchor: "bottom",
        label: formatLabel(stripX),
        fontSize: largeFontSize,
        ticks: [],
        }) : 
      Plot.axisFy({
        label: formatLabel(stripY),
        labelAnchor: "top",
        fontSize: largeFontSize,
        ticks: [],
        }),
    // Grid marks
    (stripOrient == "Vertical ↑") ? 
      Plot.gridY({}) :
      Plot.gridX({}),
    // Box marks
    ((stripOrient == "Vertical ↑") && (stripStats == "BoxPlot")) ? 
      Plot.boxY(filtered,
       {fx: stripX,
       y: stripY,
       fill: "gray",
       fillOpacity: 0.35,
       stroke: "gray"}) : [],
    ((stripOrient == "Horizontal →") && (stripStats == "BoxPlot")) ? 
      Plot.boxX(filtered,
       {fy: stripY,
       x: stripX,
       fill: "gray",
       fillOpacity: 0.35,
       stroke: "gray"}) : [],
    // Jitter marks
    ((stripOrient == "Vertical ↑") && (dotsJitterOrDodge == "Jitter")) ?
      Plot.dot(filtered, {
        fx: stripX,
        x: Math.random,
        y: stripY, 
        fill: stripDotFill, 
        opacity: dotsOpacity,
        symbol: stripDotFill,
        }) : [],
    ((stripOrient == "Horizontal →") && (dotsJitterOrDodge == "Jitter")) ?
      Plot.dot(filtered, {
        axis: null,
        x: stripX,
        fy: stripY,
        y: Math.random,
        fill: stripDotFill, 
        opacity: dotsOpacity,
        symbol: stripDotFill,
        }) : [],
    // Dodge marks
    ((stripOrient == "Vertical ↑") && (dotsJitterOrDodge == "Dodge")) ? 
      Plot.dot(filtered,
       Plot.dodgeX("middle", {
       fx: stripX,
       y: stripY, 
       fill: stripDotFill,
       opacity: dotsOpacity,
       symbol: stripDotFill,})) : [],
    ((stripOrient == "Horizontal →") && (dotsJitterOrDodge == "Dodge")) ? 
      Plot.dot(filtered, // Horizontal
       Plot.dodgeY("middle", {
       fy: stripY,
       x: stripX, 
       fill: stripDotFill,
       opacity: dotsOpacity,
       symbol: stripDotFill,})) : [],
    // Mean and SE marks
    ((stripOrient == "Vertical ↑") && (stripStats == "± SE")) ?
      Plot.dot(
      filtered,
      Plot.groupX(
         {y: "mean"},
         {y: stripY,
          fx: stripX,
          x: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Vertical ↑") && (stripStats == "± SE")) ?
      Plot.link(filtered,
      Plot.groupX({
         y1: (filtered) => d3.mean(filtered)
           - d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         y2: (filtered) => d3.mean(filtered)
           + d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {y: stripY, 
         fx: stripX,
         x: 0.5,
         stroke: "black",
         strokeWidth: 2}
         )) : [],
    ((stripOrient == "Horizontal →") && (stripStats == "± SE")) ?
      Plot.dot(filtered,
        Plot.groupY(
         {x: "mean"},
         {x: stripX,
          fy: stripY,
          y: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Horizontal →") && (stripStats == "± SE")) ?
      Plot.link(filtered,
        Plot.groupY({
         x1: (filtered) => d3.mean(filtered) 
           - d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         x2: (filtered) => d3.mean(filtered) 
           + d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {x: stripX,
          fy: stripY,
          y: 0.5,
          stroke: "black",
          strokeWidth: 3}
         )) : [],
    // Mean and confidence interval marks
    ((stripOrient == "Vertical ↑") && (stripStats == "± 95% CI")) ?
      Plot.dot(
      filtered,
      Plot.groupX(
         {y: "mean"},
         {y: stripY,
          fx: stripX,
          x: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Vertical ↑") && (stripStats == "± 95% CI")) ?
      Plot.link(filtered,
      Plot.groupX({
         y1: (filtered) => d3.mean(filtered)
           - 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         y2: (filtered) => d3.mean(filtered)
           + 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {y: stripY,
          fx: stripX,
          x: 0.5,
          stroke: "black",
          strokeWidth: 2}
         )) : [],
    ((stripOrient == "Horizontal →") && (stripStats == "± 95% CI")) ?
      Plot.dot(filtered,
        Plot.groupY(
         {x: "mean"},
         {x: stripX,
          fy: stripY,
          y: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Horizontal →") && (stripStats == "± 95% CI")) ?
      Plot.link(filtered,
        Plot.groupY({
         x1: (filtered) => d3.mean(filtered) 
           - 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
         x2: (filtered) => d3.mean(filtered) 
           + 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
         },
         {x: stripX,
          fy: stripY,
          y: 0.5,
          stroke: "black",
          strokeWidth: 3}
         )) : [],
    ],
  width: stripWidth
})

// Big ELSE stripDotFill is the NOT same as the main x or y variable
// Subplots needed. Subplots implemented using undocumented render
// transform, as demonstrated by the extraordinary Fil:
// https://observablehq.com/@fil/subplots-1870
// see also: https://observablehq.com/@observablehq/plot-of-plots

: 

Plot.plot({
  height: stripHeight,
  width: stripWidth,
  marginLeft: (stripOrient == "Horizontal →") ? 80 : 40,
  marginRight: (stripOrient == "Horizontal →") ? 80 : [],
  marginBottom: 36,
  marginTop: 36,
  symbol: 
    { legend: true,
    tickFormat: formatLabel,
    label: formatLabel(stripDotFill),
    },
  x: (stripOrient == "Horizontal →") ?
    { domain: [Math.min(...Plot.valueof(filtered, stripX)),
               Math.max(...Plot.valueof(filtered, stripX))],
    } : [],
  y: (stripOrient == "Vertical ↑") ? 
    { domain: [Math.min(...Plot.valueof(filtered, stripY)),
               Math.max(...Plot.valueof(filtered, stripY))],
    } : [],
  fx: (stripOrient == "Horizontal →") ?
    { axis: null } : [], // Will set with axis mark
  fy: (stripOrient == "Vertical ↑") ? 
    { axis: null } : [], // Will set with axis mark
  marks: [
    // Grid and axis marks
    (stripOrient == "Vertical ↑") ? Plot.gridY() : Plot.gridX(),
    (stripOrient == "Horizontal →") ?
      Plot.axisFy({
          label: formatLabel(stripY),
          fontSize: largeFontSize,
          labelAnchor: "top",
          ticks: [],
          }) : [],
    (stripOrient == "Vertical ↑") ?
      Plot.axisFx({
        label: formatLabel(stripX),
        fontSize: largeFontSize,
        ticks: [],
        }) : [],
    (stripOrient == "Horizontal →") ?
      Plot.axisFy({
        label: null,
        tickFormat: formatLabel,
        }) : [],
    (stripOrient == "Vertical ↑") ?
      Plot.axisFx({
        label: null,
        tickFormat: formatLabel,
        }) : [],
    (stripOrient == "Horizontal →") ?
      Plot.axisX({
        label: formatLabel(stripX),
        fontSize: largeFontSize,
        labelOffset: 36,
        ticks: [],
        }) : [],
    (stripOrient == "Vertical ↑") ?
      Plot.axisY({
        label: formatLabel(stripY),
        fontSize: largeFontSize,
        labelOffset: 36,
        ticks: [],
        }) : [],
    (stripOrient == "Horizontal →") ?
      Plot.axisX({
        label: null,
        tickFormat: formatLabel,
        }) : [],
    (stripOrient == "Vertical ↑") ?
      Plot.axisY({
        label: null,
        tickFormat: formatLabel,
        }) : [],
    // This Kluge puts symbols in the color legend
    Plot.dot(filtered, { 
      fill: stripDotFill,
      symbol: stripDotFill,
    }),
    // Text marks - Where the subplots are
    (stripOrient == "Horizontal →") ?
      Plot.text(getUniqueValsOfColumn(filtered, stripY), {
        frameAnchor: "middle",
        text: Plot.identity,
        fy: Plot.identity, // facets vertically by stripY
        render([i], { scales }, { channels }, dimensions) {
          return svg`<g>${
            Plot.plot({
              ...dimensions,
              ...scales,
              x: { ...scales.x, axis: null },
              y: { axis: null },
              fy: { axis: null },
              marks:[
                // Axis marks
                Plot.axisFy({
                  anchor: "left",
                  tickFormat: formatLabel,
                  label: formatLabel(stripDotFill),
                }),
                // Box marks
                (stripStats == "BoxPlot") ?
                  Plot.boxX(filtered, {
                   filter: (d) => d[stripY] === channels.text.value[i],
                   fy: stripDotFill,
                   x: stripX,
                   // fill: stripStatsColor,
                   fill: "gray",
                   fillOpacity: 0.35,
                   stroke: "gray"}) : [],
                // Dot marks -- Jitter
                (dotsJitterOrDodge == "Jitter") ?
                  Plot.dot(filtered, {
                    filter: (d) => d[stripY] === channels.text.value[i],
                    fy: stripDotFill,
                    y: Math.random,
                    x: stripX, 
                    fill: stripDotFill, 
                    opacity: dotsOpacity,
                    symbol: stripDotFill,
                    }) : [],
                // Dot marks -- Dodge
                (dotsJitterOrDodge == "Dodge") ?
                  Plot.dot(filtered, 
                    Plot.dodgeY("middle", {
                      filter: (d) => d[stripY] === channels.text.value[i],
                      fy: stripDotFill,
                      x: stripX, 
                      fill: stripDotFill, 
                      opacity: dotsOpacity,
                      symbol: stripDotFill,
                      })) : [],
                // Mean marks
                ((stripStats == "± SE") || (stripStats == "± 95% CI")) ?
                 Plot.dot(
                   filtered,
                   Plot.groupY(
                     {x: "mean"},
                     {
                      filter: (d) => d[stripY] === channels.text.value[i],
                      x: stripX, 
                      fy: stripDotFill,
                      y: 0.5,
                      fill: "black",
                      r: 5
                      }
                     )) : [],
                // SE marks
                (stripStats == "± SE") ? 
                  Plot.link(filtered,
                   Plot.groupY({
                     x1: (filtered) => d3.mean(filtered)
                          - d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
                     x2: (filtered) => d3.mean(filtered)
                          + d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
                    },
                  {
                   filter: (d) => d[stripY] === channels.text.value[i],
                   x: stripX,
                   fy: stripDotFill, 
                   y: 0.5, 
                   stroke: "black",
                   strokeWidth: 2}
                 )) : [],
                // 95% CI marks
                (stripStats == "± 95% CI") ? 
                  Plot.link(filtered,
                   Plot.groupY({
                     x1: (filtered) => d3.mean(filtered)
                          - 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
                     x2: (filtered) => d3.mean(filtered)
                          + 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
                    },
                  {
                   filter: (d) => d[stripY] === channels.text.value[i],
                   x: stripX,
                   fy: stripDotFill, 
                   y: 0.5, 
                   stroke: "black",
                   strokeWidth: 2}
                 )) : [],
                 
                ]
            })
          
          }`;
        }
      }) : [],
      
      // Text marks - Where the subplots are
      (stripOrient == "Vertical ↑") ? 
      Plot.text(getUniqueValsOfColumn(filtered, stripX), {
        frameAnchor: "middle",
        text: Plot.identity,
        fx: Plot.identity, // facets vertically by stripX
        render([i], { scales }, { channels }, dimensions) {
          return svg`<g>${
            Plot.plot({
              ...dimensions,
              ...scales,
              insetLeft: 4,
              insetRight: 4,
              y: { ...scales.y, axis: null },
              x: { axis: null },
              fx: { axis: null },
              marks:[
                // Axis marks
                Plot.axisFx({
                  anchor: "bottom",
                  label: formatLabel(stripDotFill),
                  tickFormat: formatLabel,
                }),
                // Box marks
                (stripStats == "BoxPlot") ?
                  Plot.boxY(filtered, {
                   filter: (d) => d[stripX] === channels.text.value[i],
                   fx: stripDotFill,
                   y: stripY,
                   fill: "gray",
                   fillOpacity: 0.35,
                   stroke: "gray"}) : [],
                // Dot marks -- Jitter
                (dotsJitterOrDodge == "Jitter") ?
                  Plot.dot(filtered, {
                    filter: (d) => d[stripX] === channels.text.value[i],
                    fx: stripDotFill,
                    x: Math.random,
                    y: stripY, 
                    fill: stripDotFill, 
                    opacity: dotsOpacity,
                    symbol: stripDotFill,
                    }) : [],
                // Dot marks -- Dodge
                (dotsJitterOrDodge == "Dodge") ?
                  Plot.dot(filtered,
                    Plot.dodgeX("middle", {
                    filter: (d) => d[stripX] === channels.text.value[i],
                    fx: stripDotFill,
                    y: stripY, 
                    fill: stripDotFill, 
                    opacity: dotsOpacity,
                    symbol: stripDotFill,
                    })) : [],
                // Mean marks
                ((stripStats == "± SE") || (stripStats == "± 95% CI")) ?
                 Plot.dot(
                   filtered,
                   Plot.groupX(
                     {y: "mean"},
                     {
                      filter: (d) => d[stripX] === channels.text.value[i],
                      y: stripY, 
                      fx: stripDotFill,
                      x: 0.5,
                      fill: "black",
                      r: 5
                      }
                     )) : [],
                // SE marks
                (stripStats == "± SE") ? 
                  Plot.link(filtered,
                   Plot.groupX({
                     y1: (filtered) => d3.mean(filtered)
                          - d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
                     y2: (filtered) => d3.mean(filtered)
                          + d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
                    },
                  {
                   filter: (d) => d[stripX] === channels.text.value[i],
                   y: stripY,
                   fx: stripDotFill, 
                   x: 0.5, 
                   stroke: "black",
                   strokeWidth: 2}
                 )) : [],
                // 95% CI marks
                (stripStats == "± 95% CI") ? 
                  Plot.link(filtered,
                   Plot.groupX({
                     y1: (filtered) => d3.mean(filtered)
                          - 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered)),
                     y2: (filtered) => d3.mean(filtered)
                          + 1.96*d3.deviation(filtered)/Math.sqrt(d3.count(filtered))
                    },
                  {
                   filter: (d) => d[stripX] === channels.text.value[i],
                   y: stripY,
                   fx: stripDotFill, 
                   x: 0.5, 
                   stroke: "black",
                   strokeWidth: 2}
                 )) : [],
                
                ]
            })
          
          }`;
        }
      }) : [],
      
  ]
})

;
html
`<div id="stripStatistics" autocorrect="off" spellcheck="false" style="display: ${stripStatsDisplay}; padding-left: 1em; font-size: 80%;">

<caption>
    <b>One-way ANOVA</b>
  </caption>
  <table style="line-height: 75%;">
      <tr>
        <td style="padding: 0.5em; color: gray;">Source</td>
        <td style="padding: 0.5em; color: gray;">Deg fr</td>
        <td style="padding: 0.5em; color: gray;">Sum Sq</td>
        <td style="padding: 0.5em; color: gray;">Mean Sq</td>
        <td style="padding: 0.5em; color: gray;"><i>F</i></td>
        <td style="padding: 0.5em; color: gray;"><i>P</i> - value</td>
      </tr>
    <tbody>
      <tr>
        <td style="padding: 0.5em; color: gray;">Between grps</td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.df_model}</td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.SSE.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelStripANOVA.SSE / myOLSModelStripANOVA.df_model).toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${myOLSModelStripANOVA.f.F_statistic.toPrecision(5)}</td>
        <td style="padding: 0.5em; color: steelblue;">${pValStr(myOLSModelStripANOVA.f.pvalue)}</td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Within grps</td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.SSR.toPrecision(5)}</td>
        <td style="padding: 0.5em;">${(myOLSModelStripANOVA.SSR / myOLSModelStripANOVA.df_resid).toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
      <tr>
        <td style="padding: 0.5em; color: gray;">Total<br><br><span style="color: steelblue;"><i>R</i><sup> 2</sup> = ${myOLSModelStripANOVA.R2.toPrecision(5)}</span></td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.df_model + myOLSModelStripANOVA.df_resid}</td>
        <td style="padding: 0.5em;">${myOLSModelStripANOVA.SST.toPrecision(5)}</td>
        <td style="padding: 0.5em;"></td>
        <td style="padding: 0.5em;"></td>
      </tr>
    </tbody>
  </table>
</div>`
html
`<div id="stripEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
// viewof stripOrient = Inputs.radio(["Vertical ↑", "Horizontal →"], {value: "Vertical ↑", label: "Orientation:"});

stripOrient = "Vertical ↑";
html`<label style="padding-top: 0em; margin-top: 0em;"><span style="font-size: 200%; vertical-align: -7%;">🀱 </span>Groups</label>`
viewof stripX = (stripOrient == "Vertical ↑") ? 
  html`
  <select style="width: 80%; margin-left: 1em; margin-top: 0em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(categoricalColumns, 0)}
  </select>` :
  html`
  <select style="width: 80%; margin-left: 1em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(numericalColumns, 0)}
  </select>`;
html`<label style = "padding-top: 0.95em; padding-bottom: 0.3em;">↑ Plot as Y:</label>`
viewof stripY = (stripOrient == "Vertical ↑") ?
  html`
  <select style="width: 80%; margin-left: 1em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(numericalColumns, 0)}
  </select>` :
  html`
  <select style="width: 80%; margin-left: 1em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(categoricalColumns, 0)}
  </select>`;
stripDotFill = stripX;

//viewof stripDotFill = Inputs.select(categoricalColumns, {width: "5"});

// viewof stripDotFill = html`
//   <select style="width: 89.5%; margin-top: 0.25em; margin-bottom: 0.25em; margin-left: 0.25em; height: 1.7em; background-color: white;">
//   ${populateWithOptions(categoricalColumns, 0)}
//   </select>`

//viewof dotsOpacity = Inputs.number([0, 1], {step: 0.1, value: 0.7, width: "5"});

viewof dotsOpacity = html`<input type="range" min="0" max="1" value="0.7" step="any" style="width: 89.5%; height: 2em; margin-top: 0em; opacity: 0.6;" />`
viewof dotsJitterOrDodge = Inputs.radio(["Jitter", "Dodge"], {value: "Jitter"});

viewof stripStats = Inputs.radio(["No", "± SE", "± 95% CI" ], {label: "One-way ANOVA:", value: "No"});

html`<label>Plot width:</label>`
viewof stripWidth = html`<input type="range" min="50" max="1028" value="640" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
html`<label>Plot height:</label>`
viewof stripHeight = html`<input type="range" min="50" max="768" value="500" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
viewof stripAddCap = Inputs.toggle({label: "Add caption"});

Inputs.button([
  ["Reset strip chart", () => {
    resetStripControls()
  }]
]);
stripQuill = new Quill('#stripEditor', {
    modules: {
      toolbar: false, // ['bold', 'italic'],
      },
    theme: 'bubble',
  });

quillDisplay(stripAddCap, "stripEditor");
quillSetWidth(stripWidth, "stripEditor");
stripStatsDisplay = statsDisplay( ( (stripStats !== "No") && (currentHash === "#strip-chart") ), "stripStatistics");

function resetStripControls() {
//  set(viewof stripOrient, "Vertical ↑");
//  set(viewof stripX, categoricalColumns[0]);  // NOT NEEDED because redundant
//  set(viewof stripY, numericalColumns[0]);  // NOT NEEDED because redundant
//  set(viewof stripDotFill, categoricalColumns[0]);
  set(viewof dotsJitterOrDodge, "Jitter");
  set(viewof dotsOpacity, 0.7);
  set(viewof stripStats , "No");
  set(viewof stripHeight, 500);
  set(viewof stripWidth, 640);
  set(viewof stripAddCap, false);
  quillResetText(stripQuill);

};

// Experimental for statistical functions

stripANOVADataSansMissing = filtered.filter(d => (!isNaN(d[stripY])) 
                                              && (d[stripY] !== null)
                                              && (d[stripY] !== "")
                                              && (d[stripDotFill] !== null)
                                              && (d[stripDotFill] !== "")
                                              );

function getDummiesForStripANOVARegr(data, groups) {
  
  const grpsCol = columnToArray(data, groups);
  const uniqueGrps = getUniqueVals(grpsCol).sort();
  const numDummies = uniqueGrps.length - 1;
  const dummiesArr = [];
  
  // Once per observation
  for (let i = 0; i < grpsCol.length; i++) {
    // Once per dummy needed
    const myRow = [];
    myRow.push(1);
    for (let j = 0; j < numDummies; j++) {
      if (grpsCol[i] === uniqueGrps[j]) {
        myRow.push(1);
        } else {
        myRow.push(0);
       };
    };
    
    dummiesArr.push(myRow);
    
  };

  return dummiesArr;
  
};

myOLSModelStripANOVA = ( (stripStats!== "No") && (currentHash === "#strip-chart") ) ? // Kluge to avoid calculating a complex model when not needed.
    new myJstat.models.ols(
      columnToArray(stripANOVADataSansMissing, stripY),
      getDummiesForStripANOVARegr(stripANOVADataSansMissing, stripDotFill)
      )
    :
    new myJstat.models.ols(
      [0, 1],
      [[1, 0], [1, 1]]
      )
    ;
chart = {

// const mData = getMarimekkoData(filtered, barXVar, barFill, barFacetX, barFacetY);

// From https://observablehq.com/@observablehq/plot-marimekko
const xy = (options) => marimekko({...options,
                        x: "mXVar",
                        y: "mYVar",
                        value: "mVal"});
                        

// THE BIG IF ELSE
// Bar chart or marimekko
return (barCountsOrPercentages == "Counts") ? // THE BIG IF - Bar chart

Plot.plot({
  //  marginLeft: 60,
  //  x: {label: barXVar},
  //  y: {label: "Count"},
  facet: {
    data: filtered,
    x: barFacetX,
    y: barFacetY,
    marginRight: 80,
    //  marginTop: 36,
    },
  fx: {
    label: formatLabel(barFacetX),
    },
  fy: {
    label: formatLabel(barFacetY),
    },
  color: {legend: true, tickFormat: formatLabel},
  marginTop: 35, // more room for facets
  marginBottom: 48,
  marks: [
    // Axis marks
    Plot.axisX({  // Axis with just the ticks in the default fontSize
      label: null,
      tickFormat: formatLabel,
      }), 
    Plot.axisX({  // Axis with just the label in custom fontSize
      label: formatLabel(barXVar),
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    (barFacetX !== "") ? Plot.axisFx({  // Facet axis with just the ticks
       label: null,
       tickFormat: formatLabel,
       }) : [],
    (barFacetX !== "") ? Plot.axisFx({  // Facet axis with just the label
       label: formatLabel(barFacetX),
       fontSize: largeFontSize,
       ticks: [ ],
       }) : [],
    Plot.axisY({  // Axis with just the ticks in the default fontSize
      label: null,
      }),
    Plot.axisY({  // Axis with just the label in the custom fontSize
      label: "Count",
      fontSize: largeFontSize,
      labelOffset: 36,
      ticks: [],
      }),
    (barFacetY !== "") ? Plot.axisFy({  // Facet axis with just the ticks
      label: null,
      tickFormat: formatLabel,
      }) : [],
    (barFacetY !== "") ? Plot.axisFy({  // Facet axis with just the label
       label: formatLabel(barFacetY),
       labelAnchor: "top",
       fontSize: largeFontSize,
       ticks: [ ],
      }) : [],
    // Bars
      Plot.barY(
        filtered,
        Plot.groupX({y: "count"},
         {x: barXVar, fill: barFill, opacity: barOpacity}
         )),
        
    ],
  height: barChartHeight,
  width: barChartWidth,
})

: // The big ELSE - marimekko

Plot.plot({
    width: barChartWidth,
    height: barChartHeight,
    label: null,
    facet: {data: mData,
            x: "mFXVar",
            y: "mFYVar",
            marginRight: 80},
    fx: {padding: 0.12},
    fy: {padding: 0.12},
    color: {legend: true, tickFormat: formatLabel},
    marginTop: 35,
    marginBottom: 48,
    x: {percent: true},
    y: {percent: true, ticks: 0, tickFormat: (d) => d === 100 ? `100%` : d},
    marks: [
      Plot.frame({opacity: 0.25}),
      // Added these
      Plot.axisX({  // Axis with just the ticks in the default fontSize
        label: null,
        ticks: (barFacetX == "") ? 10 : 5,
        tickFormat: (d) => d === 100 ? `100%` : d
        }), 
      Plot.axisX({  // Axis with just the label in custom fontSize
        label: formatLabel(barXVar),
        fontSize: largeFontSize,
        labelOffset: 36,
        ticks: [],
        }),
     (barFacetX !== "") ? Plot.axisFx({  // Facet axis with just the ticks
       label: null,
       tickFormat: formatLabel,
       }) : [],
     (barFacetX !== "") ? Plot.axisFx({  // Facet axis with just the label
       label: formatLabel(barFacetX),
       fontSize: largeFontSize,
       ticks: [ ],
       }) : [],
      (barXVar !== barFill) ?
        Plot.axisY({  // Axis with just the ticks in the default fontSize
          label: null,
          ticks: (barFacetY == "") ? 10 : 5,
          tickFormat: (d) => d === 100 ? `100%` : d
          }) : [],
      (barXVar !== barFill) ?
        Plot.axisY({  // Axis with just the label in the custom fontSize
          label: formatLabel(barFill),
          fontSize: largeFontSize,
          labelOffset: 36,
          ticks: [],
         }) : [],
      (barFacetY !== "") ? Plot.axisFy({  // Facet axis with just the ticks
       label: null,
       tickFormat: formatLabel,
       }) : [],
     (barFacetY !== "") ? Plot.axisFy({  // Facet axis with just the label
       label: formatLabel(barFacetY),
       labelAnchor: "top",
       fontSize: largeFontSize,
       ticks: [ ],
       }) : [],
      Plot.rect(mData, xy({fill: "mYVar", fillOpacity: barOpacity})),
      // Added 'formatLabel' ×3; made conditional on d.mVal > 0; conditional on barFill ≠ barXVar
      Plot.text(mData, xy({text: d => (d.mVal > 0) ?
                                       [formatLabel(d.mVal.toLocaleString("en")),
                                       (barFill !== barXVar) ?
                                       "y: " + formatLabel(d.mYVar) : formatLabel(d.mYVar),
                                       (barFill !== barXVar) ? 
                                       "x: " + formatLabel(d.mXVar)
                                       : 
                                       ""
                                       ].join("\n") : "" })),
      // Plot.text(mData, Plot.selectMaxX(xy({z: "mYVar", text: "mYVar", anchor: "right", textAnchor: "middle", lineAnchor: "bottom", rotate: 90, dx: 6}))),
      // Made conditional on mData.mYVar !== mData.mXVar -- but it's redunant, because axis label is handled elsewhere.
      // (mData.mYVar !== mData.mXVar) ? Plot.text(mData, Plot.selectMaxY(xy({z: "mXVar", text: "mXVar", anchor: "top", lineAnchor: "bottom", dy: -6}))) : [],
    ]
  });

};

html
`<div id="marimekkoStatistics" autocorrect="off" spellcheck="false" style="display: ${marimekkoStatsDisplay}; padding-left: 1em; font-size: 80%;">

<caption>
    <b>χ<sup>2</sup> test</b>
</caption>

<p style="padding-top: 1em;">
${chiSq}
</p>

</div>`
html
`<div id="barEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
html`<label>🀱 Plot as X:</label>`
viewof barXVar = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(categoricalColumns, 0)}
  </select>`
html`<label>🁣 Plot as Y:</label>`
viewof barFill = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions(categoricalColumns, 0)}
  </select>`

//viewof barOpacity = Inputs.number([0, 1], {step: 0.1, value: 0.8, width: "5"}); 
  
viewof barOpacity = html`<input type="range" min="0" max="1" value="0.7" step="any" style="width: 80%; height: 2em; margin-top: 0em; opacity: 0.6;" />`
// viewof barCountsOrPercentages = Inputs.radio(["Counts", "Percentages"], {value: "Counts"});

barCountsOrPercentages ="Percentages";
//viewof barFacetX = Inputs.select(
//  [null].concat(categoricalColumns),
//  {label: "🀱 Split by:",
//  width: "5"}); 

//html`<label><span style="font-size: 200%; vertical-align: -5%;">🀱 </span> Split by:</label>`

//viewof barFacetX = html`
//  <select style="width: 80%; margin-left: 1em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
//  ${populateWithOptions([""].concat(categoricalColumns), 0)}
//  </select>`

barFacetX = "";
//viewof barFacetY = Inputs.select(
//  [null].concat(categoricalColumns),
//  {label: "🁣 Split by:",
//  width: "5"});

// html`<label><span style="font-size: 200%; vertical-align: -5%;">🁣 </span> Split by:</label>`

// viewof barFacetY = html`
//  <select style="width: 80%; margin-left: 1em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
//  ${populateWithOptions([""].concat(categoricalColumns), 0)}
//   </select>`

barFacetY = "";

χ2 test:

viewof showChiSq = Inputs.radio(
  ["No", "Yes"]
   ,
  {value: "No"}
  );

html`<label>Plot width:</label>`
viewof barChartWidth = html`<input type="range" min="50" max="1028" value="640" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
html`<label>Plot height:</label>`
viewof barChartHeight = html`<input type="range" min="50" max="768" value="500" step="any" style="margin-left: 0.5em; width: 85%; height: 2em; opacity: 0.6;" />`
viewof barAddCap = Inputs.toggle({label: "Add caption"});

Inputs.button([
  ["Reset bar chart", () => {
    resetBarChartControls()
  }]
]);
barQuill = new Quill('#barEditor', {
    modules: {
      toolbar: false, // ['bold', 'italic'],
      },
    theme: 'bubble',
  });

quillDisplay(barAddCap, "barEditor");
quillSetWidth(barChartWidth, "barEditor");
marimekkoStatsDisplay = statsDisplay( ( (showChiSq === "Yes") && (currentHash === "#marimekko-chart") ), "marimekkoStatistics");

// Extract data for marimekko plot - see marimekko function
function getMarimekkoData(data, xVar, yVar, facetXVar, facetYVar) {

  const uniqueXVals = getUniqueValsOfColumn(data, xVar).sort();
  const uniqueYVals = getUniqueValsOfColumn(data, yVar).sort();
  const uniqueFacetXVals = getUniqueValsOfColumn(data, facetXVar).sort();
  const uniqueFacetYVals = getUniqueValsOfColumn(data, facetYVar).sort();
  
  function marimekkoLine(mXVar, mYVar, mVal, mFXVar, mFYVar) {
    this.mXVar = mXVar;
    this.mYVar = mYVar;
    this.mVal = mVal;
    this.mFXVar = mFXVar;
    this.mFYVar = mFYVar;
    
  }
  
  let marimekkoData = [];
  
  for (let i = 0; i < uniqueXVals.length; i++) {
    for (let j = 0; j < uniqueYVals.length; j++) {
      for (let k = 0; k < uniqueFacetXVals.length; k++) {
        for (let l = 0; l < uniqueFacetYVals.length; l++) {
      
          const mySubset = data.filter( (datum) => (datum[xVar] === uniqueXVals[i]) && (datum[yVar] === uniqueYVals[j]) && (datum[facetXVar] === uniqueFacetXVals[k]) && (datum[facetYVar] === uniqueFacetYVals[l]) );
      
          const myMarimekkoLine = new marimekkoLine(uniqueXVals[i], uniqueYVals[j], mySubset.length, uniqueFacetXVals[k], uniqueFacetYVals[l],);
      
          marimekkoData.push(myMarimekkoLine);
      
        };
      };
    };
  };
  
  return marimekkoData;
  
};

// From https://observablehq.com/@observablehq/plot-marimekko-facets-plus-order
// See also: https://observablehq.com/@observablehq/plot-marimekko
function marimekko({x, y, z, value = z, anchor = "middle", inset = 0.5, ...options} = {}) {
  const stackX = /\bleft$/i.test(anchor) ? Plot.stackX1 : /\bright$/i.test(anchor) ? Plot.stackX2 : Plot.stackX;
  const stackY = /^top\b/i.test(anchor) ? Plot.stackY2 : /^bottom\b/i.test(anchor) ? Plot.stackY1 : Plot.stackY;
  const [X, setX] = Plot.column(x);
  const [Y, setY] = Plot.column(y);
  const [Xv, setXv] = Plot.column(value);
  const {x: Xs, x1, x2, transform: tx} = 
          stackX({offset: "expand",
                  y: Y,
                  x: Xv,
                  z: X,
                  // order: "appearance"
                  });
  const {y: Ys, y1, y2, transform: ty} = 
          stackY({offset: "expand",
                  x,
                  y: value,
                  z: Y,
                  // order: "appearance"
                  });
  return Plot.transform({x: Xs, x1, x2, y: Ys, y1, y2, z, inset, frameAnchor: anchor, ...options}, (data, facets) => {
    const X = setX(Plot.valueof(data, x));
    setY(Plot.valueof(data, y));
    const Xv = setXv(new Float64Array(data.length));
    const Z = Plot.valueof(data, value);
    for (const I of facets) {
      const sum = d3.rollup(
        I,
        (J) => d3.sum(J, (i) => Z[i]),
        (i) => X[i]
      );
      for (const i of I) Xv[i] = sum.get(X[i]);
    }
    tx(data, facets);
    ty(data, facets);
    return {data, facets};
  });
}

function resetBarChartControls() {
  set(viewof barXVar, categoricalColumns[0]);
  set(viewof barFill, categoricalColumns[0]);
  set(viewof barOpacity, 0.8);
  // set(viewof barCountsOrPercentages, "Counts");
  // set(viewof barFacetX, "");
  // set(viewof barFacetY, "");
  set(viewof showChiSq, "No");
  set(viewof barChartHeight, 500);
  set(viewof barChartWidth, 640);
  set(viewof barAddCap, false);
  quillResetText(barQuill);

};

// Experimental χ2 test

function getChiSq(myMData) {

  if (barXVar === barFill) {
  
    return "Please choose different variables for X versus Y.";
  
  } else {

  const cols = getUniqueValsOfColumn(myMData, "mXVar");
  const rows = getUniqueValsOfColumn(myMData, "mYVar");
  const df = (cols.length - 1) * (rows.length - 1);
  const totalObs = myMData.reduce((a, c) => {return a + c.mVal;}, 0);

  let chisq = 0;

  for (let i = 0; i < cols.length; i++) {
    for (let j = 0; j < rows.length; j++) {
      const observed = myMData.filter( (d) => ( (d.mXVar == cols[i]) && (d.mYVar == rows[j]) ) )[0].mVal;
      const col = myMData.filter( (d) => (d.mXVar == cols[i]) );
      const colSum = col.reduce((a, c) => {return a + c.mVal;}, 0);
      const row = myMData.filter( (d) => (d.mYVar == rows[j]) );
      const rowSum = row.reduce((a, c) => {return a + c.mVal;}, 0);
      const expected = colSum * rowSum / totalObs;
      chisq = chisq + ( (observed - expected) * (observed - expected) / expected );
    
    };
    
  };
  
  const pval = 1 - myJstat.chisquare.cdf( chisq, df );
  
  return "χ<sup>2</sup> = " + chisq.toPrecision(5) + "<br> Degrees of freedom = " + df + "<br> <i>N</i> = " + totalObs + "<br> <i>P</i> = " + pValStr(pval);
  
  };

};

// Experimental filter out rows with missing data for x or y. ardeaPlot leaves them in.
mariDataSansMissing = filtered.filter(d => (d[barXVar] !== null)
                                           && (d[barXVar] !== "")
                                           && (d[barFill] !== null)
                                           && (d[barFill] !== "")
                                           );

mData = getMarimekkoData(mariDataSansMissing, barXVar, barFill, barFacetX, barFacetY);

chiSq = getChiSq(mData);

ardeaStat is inspired by SimBio’s GraphSmarts Assessements and by easyPlot. It is made with:

Quarto
  • Quarto dashboards
  • Quarto’s built-in Observable js
Observable js
  • Observable Plot

jStat and Quill

Many thanks to the creators of these tools!

Sources for built-in datasets:

  • Palmer Penguins — observations with missing data (NA) have been removed.

  • Elephants: Mduduzi Ndlovu et al. 2018

  • Primates and carnivores: Daniel L. Bowling et al. 2020

  • Hurricane lizards one: Colin M. Donihue et al. 2018

  • Hurricane lizards two: Colin M. Donihue et al. 2020

d3 = require("d3@7"); // Used for basic functionality

Quill = require("https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"); // Used for captions

// ML = require("https://www.lactame.com/lib/ml/6.0.0/ml.min.js"); 
// Used for linear models. See: 
// https://observablehq.com/@observablehq/simple-linear-regression-in-observable
// Might be useful for simulations, b/c
// https://mljs.github.io/matrix/classes/CholeskyDecomposition.html

myJstat = require("https://cdn.jsdelivr.net/npm/jstat@latest/dist/jstat.min.js");

// Global variables

largeFontSize = 13;

myColors = ["tomato", "forestgreen", "steelblue", "slateblue", "black"];

// Functions used across pages

function formatLabel(myString) {

  if ((typeof myString !== "string") || (myString === "pH")){
    return myString;
  } else if ( (myString == null) || (myString.length === 0) ) {
    return "";
  } else {
    myString = myString.replaceAll("_", " ");
    return myString.charAt(0).toUpperCase() + myString.slice(1);
  };
  
};

function getCategoricalColumns(data) {

  if (!data || data.length === 0) {
    return [];
  };

  const categoricalColumns = [];
  const dataKeys = Object.keys(data[0]);

  for (let i = 0; i < dataKeys.length; i++) {
    let j = 0;
    let myType = undefined;
    while ( (j < data.length) && (myType !== 'string' ) && (myType !== 'number') ) {
      myType = typeof data[j][dataKeys[i]];
      j++;
    };
  
    if (myType === 'string') {
      categoricalColumns.push(dataKeys[i]);
    };
    
  };

  return categoricalColumns;
  
};

function getNumericalColumns(data) {

  if (!data || data.length === 0) {
    return [];
  };

  const numericalColumns = [];
  const dataKeys = Object.keys(data[0]);

  for (let i = 0; i < dataKeys.length; i++) {
    let j = 0;
    let myType = undefined;
    while ( (j < data.length) && (myType !== 'string' ) && (myType !== 'number') ) {
      myType = typeof data[j][dataKeys[i]];
      j++;
    };
  
    if (myType === 'number') {
      numericalColumns.push(dataKeys[i]);
    };
    
  };

  return numericalColumns;
  
};

// May replicate Plot.valueof (see: https://observablehq.com/plot/features/transforms)
function columnToArray(data, column) {
  return data.map((row) => row[column]);
};

function getUniqueVals(array) {
  return array.filter((value, index, self) => self.indexOf(value) === index);
};

function getUniqueValsOfColumn(data, column) {
  return getUniqueVals(columnToArray(data, column));
};

function set(input, value) {
  input.value = value;
  input.dispatchEvent(new Event("input", {bubbles: true}));
  
};

populateWithOptions = function(myArray, defIndex) {

  const returnArray = [];

  for (let i = 0; i < myArray.length; i++) {
    if (i == defIndex) {
      returnArray.push("<option value='" + myArray[i] + "' selected>" + formatLabel(myArray[i]) + "</option>")
      } else {
      returnArray.push("<option value='" + myArray[i] + "'>" + formatLabel(myArray[i]) + "</option>")
    };
  };

  return returnArray;

};

function pValStr(pVal) {
  if (pVal < 0.000001) {
    return "< 0.000001";
  } else {
    const myStr = "";
    return myStr.concat(pVal.toPrecision(5));
  }

};

function quillDisplay(myBoolean, myElement) {

  const elem = document.getElementById(myElement);

  if (myBoolean == true) {
    elem.style.display = "block";
  } else {
    elem.style.display = "none";
  };
  
};

function statsDisplay(myBoolean,
   //myElement
   ) {

  //const elem = document.getElementById(myElement);

  if (myBoolean == true) {
  //  elem.style.display = "block";
    return "block";
  } else {
  //  elem.style.display = "none";
    return "none";
  };
  
};

function quillResetText(myQuill) {

  myQuill.setContents([
    { insert: 'Figure #. ' },
    { insert: 'Title', attributes: { bold: true } },
    { insert: '\n' },
    { insert: 'Caption.' },
    { insert: '\n' },
  ]);

};

function quillSetWidth(myNum, myElement) {

  const elem = document.getElementById(myElement);
  const myString = myNum.toString().concat("px");
  elem.style.width = myString; 

};

// Experimental - can detect clicks on window; set global flag
// Using this as a kluge to avoid caclulating complex models when they're not visible

mutable currentHash = "";

addEventListener('click', function() {
  mutable currentHash = window.location.hash;
 
});

ardeaStat
ardea rapid data exploration & analysis