ardeaStat
  • Data
  • Scatterplot
  • Histogram
  • Strip chart
  • Marimekko chart
  • Info
more

Filter data by searching. Uses spaces between terms.

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

viewof searchOptions = Inputs.radio(["all terms", "any term"], {value: "all terms", label: "Results match:"});

viewof search = (searchOptions === "all terms") ? 
  Inputs.search(data) :
  Inputs.search(data, {filter: filterFunctionCustomOr})
  ;
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])
 });

// console.log(filtered);

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 "WEEK3" 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 if (flag === "WEEK3") {
    return [
    //    "<option value='empty' > </option>",
    "<option value='palmer-penguins' selected>Palmer penguins</option>",
    "<option value='ermo-field' >Ermo Field</option>",
    "<option value='ermo-herbarium' >Ermo Herbarium</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>"
    ];
    } 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 "ermo-field":
    return FileAttachment("datasets/ermo-field.csv");
    break;
    
  case "ermo-herbarium":
    return FileAttachment("datasets/ermo-herbarium.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 === "RDA") ? FileAttachment("datasets/blackbirds.csv") :
            FileAttachment("datasets/palmer-penguins.csv");

  };

};

data = {

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

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

};

};

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

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());
function* valuesof(d) {
  for (const key in d) {
    yield d[key];
  }
};

function termFilter(term) {
  return new RegExp(`(?:^|[^\\p{L}-])${escapeRegExp(term)}`, "iu");
};

function escapeRegExp(text) {
  return text.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
};

function filterFunctionCustomOr(query) {
  const filters = `${query}`.split(/\s+/g).filter(t => t).map(termFilter);
  return d => {
    if (d == null) return false;
    
    if (typeof d === "object") {
      for (const filter of filters) {
        for (const value of valuesof(d)) {
          if (filter.test(value)) {
            return true;
          }
        }
      }
      return false;

    } else {
      for (const filter of filters) {
        if (filter.test(d)) {
          return true;
        }
      }
    return false;
    }
  };
};
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: false, // 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: "steelblue", // (histoFill !== "") ? histoFill : "steelblue",
       opacity: histoOpacity,
       // thresholds: 20,
       // insetLeft: 6, insetRight: 6, 
       })),
    // 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: "steelblue", // 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: "steelblue", // 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 = {

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

//Marimekko
return Plot.plot({
    width: barChartWidth,
    height: barChartHeight,
    label: null,
    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: 11,
        tickFormat: (d) => d === 100 ? `100%` : d
        }), 
      Plot.axisX({  // Axis with just the label in custom fontSize
        label: formatLabel(barXVar),
        fontSize: largeFontSize,
        labelOffset: 36,
        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: [],
         }) : [],
      Plot.rect(mData, xy({fill: "mYVar", fillOpacity: barOpacity})),
      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") : "" })),
    ]
  });

};

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

<div style="display: flex; flex-direction: row;">

<div style="flex: 5;">
</div>

<div style="flex: 50;">

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

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

</div>

<div style="flex: 5;">
</div>

<div style="flex: 100;">

<caption>
    <b>Expected values</b>
</caption>

<p></p>

${expChart}

</div>

</div>

<div style="flex: 5;">
</div>

</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 get expected values under null

function getExpected(myMData) {

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

  let expectedData = structuredClone(myMData);
  let arrayItem = 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;
      expectedData[arrayItem].mVal = expected;
      arrayItem++;
    
    };
  
  };
  
    return expectedData;

};

// 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);

expData = getExpected(mData);

expChart = {

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

if (barXVar === barFill) {
  
    return "<p></p>";
  
  } else {

// Marimekko
return Plot.plot({
    width: barChartWidth/2,
    height: barChartHeight/2,
    label: null,
    color: { 
            //legend: true,
            tickFormat: formatLabel
           },
    marginTop: 6,
    marginBottom: 54,
    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: 6,
        tickFormat: (d) => d === 100 ? `100%` : d
        }), 
      (barXVar !== barFill) ?
        Plot.axisY({  // Axis with just the ticks in the default fontSize
          label: null,
          ticks: 5,
          tickFormat: (d) => d === 100 ? `100%` : d
          }) : [],
      Plot.rect(expData, xy({fill: "mYVar", fillOpacity: barOpacity})),
      Plot.text(expData, xy({text: d => [formatLabel(d.mVal.toFixed(2).toLocaleString("en"))]})),
    ]
  });
 };
};
Motivation

I wrote ardeaStat for my introductory biology students. The idea is to give them a chance to explore, with interesting datasets and as frictionlessly as possible, a few relatively straightforward statistical tests that can be done with data displayed in some common types of graphs. My hope is that once students learn a bit about what formal statistical analyses can do for them, they’ll be motivated to master more general-purpose and complex software tools.

My work on ardeaStat was inspired by SimBio’s GraphSmarts assessments, by the other participants in the GraphSmarts Faculty Mentoring Network, and by easyPlot. It was partially funded by Grappling with graphs: New Tools For Improving Graphing Practices of Undergraduate Biology Students (NSF# 2111150).

If you use ardeaStat, I’d love to get an email about your experience.

Thanks,

Jon C. Herron

ardeaStat is made with

Quarto, including:

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

Observable js, especially:

  • Observable Plot

jStat and Quill

Many thanks to the creators of these libraries!

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