ardeaPlot
  • Data
  • Scatterplot
  • Histogram
  • Strip chart
  • Bar/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])
 });

ardeaPlot
ardea rapid data exploration & analysis


Welcome to ardeaPlot

  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 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) {
  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({
  facet: {
    data: filtered,
    x: scatterFacetX,
    y: scatterFacetY,
    marginRight: 80
    },
  grid: false,
  symbol: ((scatterFill !== "") && (fillType === 'string')) ?
    {
    legend: true,
    tickFormat: formatLabel,
    label: formatLabel(scatterFill),
    } : [],
  color: ((scatterFill !== "") && (fillType === 'number')) ?
    {
    legend: true,
    tickFormat: formatLabel,
    label: formatLabel(scatterFill),
    } : [],
  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: [],
      }),
    (scatterFacetX !== "") ? Plot.axisFx({  // Facet axis with just the ticks
       label: null,
       tickFormat: formatLabel,
       }) : [],
    (scatterFacetX !== "") ? Plot.axisFx({  // Facet axis with just the label
       label: formatLabel(scatterFacetX),
       fontSize: largeFontSize,
       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: [],
      }),
    (scatterFacetY !== "") ? Plot.axisFy({  // Facet axis with just the ticks
      label: null,
      tickFormat: formatLabel,
      }) : [],
    (scatterFacetY !== "") ? Plot.axisFy({  // Facet axis with just the label
       label: formatLabel(scatterFacetY),
       labelAnchor: "top",
       fontSize: largeFontSize,
       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}):[],
    (scatterGrpRegr=="Line")?Plot.linearRegressionY(
       filtered, {
       x: scatterX,
       y: scatterY,
       stroke: scatterFill,
       ci: 0}):[],
    (scatterGrpRegr=="± 95% CI")?Plot.linearRegressionY(
       filtered, {
       x: scatterX,
       y: scatterY,
       stroke: scatterFill,
       ci: 0.95}):[],
     Plot.dot(
       filtered, {
       x: scatterX,
       y: scatterY,
       fill: (scatterFill !== "") ? scatterFill : "steelblue",
       opacity: scatterOpacity,
       symbol: ((scatterFill !== "") && (fillType === 'string')) ? scatterFill : "steelblue"}),
     (scatterFacetX !== "" || scatterFacetY !== "") ?
       Plot.frame() : null,
  ],
  height: scatterHeight,
  width: scatterWidth,
  marginLeft: 60
});
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 = html`
  <select style="width: 85%; margin-top: 0.1em; margin-bottom: 0.25em; margin-left: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(allColumns), 0)}
  </select>`

//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: 0.7em; opacity: 0.6;" />`

viewof scatterRegr = Inputs.radio(["No", "Line", "± 95% CI"], {label: "Overall trend:", value: "No"});
// Created to support choice of colors
scatterRegrFill = "gray";
viewof scatterGrpRegr = Inputs.radio(
((scatterFill !== "") && (fillType === 'string')) ? ["No", "Line", "± 95% CI"] : ["No"],
{label: "Group trends:", value: "No"});

html`<label><span style="font-size: 200%; vertical-align: -5%;">🀱 </span> Split by:</label>`
viewof scatterFacetX = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`
html`<label><span style="font-size: 200%; vertical-align: -5%;">🁣 </span> Split by:</label>`
viewof scatterFacetY = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`

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

};
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="histoEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
html`<label>→ Variable to plot:</label>`
viewof histoX = 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>`
// Was created to support a toggle between count versus percentage
histoY = "count";

viewof histoFill = html`
  <select style="width: 80%; margin-top: 0.1em; margin-bottom: 0.25em; margin-left: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(filtered.columns), 0)}
  </select>`
  
// 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: 80%; height: 2em; margin-top: 0.7em; opacity: 0.6;" />`

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

html`<label><span style="font-size: 200%; vertical-align: -5%;">🀱 </span> Split by:</label>`
viewof histFacetX = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`
html`<label><span style="font-size: 200%; vertical-align: -5%;">🁣 </span> Split by:</label>`
viewof histFacetY = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`

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");
function resetHistogramControls() {
  set(viewof histoX, numericalColumns[0]);
  set(viewof histoFill, "");
  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);

};
( ((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 == "Mean ± SE")) ?
      Plot.dot(
      filtered,
      Plot.groupX(
         {y: "mean"},
         {y: stripY,
          fx: stripX,
          x: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Vertical ┋") && (stripStats == "Mean ± 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 == "Mean ± SE")) ?
      Plot.dot(filtered,
        Plot.groupY(
         {x: "mean"},
         {x: stripX,
          fy: stripY,
          y: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Horizontal ┉") && (stripStats == "Mean ± 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 == "Mean ± 95% CI")) ?
      Plot.dot(
      filtered,
      Plot.groupX(
         {y: "mean"},
         {y: stripY,
          fx: stripX,
          x: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Vertical ┋") && (stripStats == "Mean ± 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 == "Mean ± 95% CI")) ?
      Plot.dot(filtered,
        Plot.groupY(
         {x: "mean"},
         {x: stripX,
          fy: stripY,
          y: 0.5,
          fill: "black",
          r: 5}
         )) : [],
    ((stripOrient == "Horizontal ┉") && (stripStats == "Mean ± 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 == "Mean ± SE") || (stripStats == "Mean ± 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 == "Mean ± 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 == "Mean ± 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 == "Mean ± SE") || (stripStats == "Mean ± 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 == "Mean ± 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 == "Mean ± 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="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><span style="font-size: 200%; vertical-align: -5%;">🀱 </span>Groups:</label>`
  :
  html`<label><span style="font-size: 200%; vertical-align: -5%;"></span>→ Plot as X:</label>`
  ;
viewof stripX = (stripOrient == "Vertical ┋") ? 
  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`
  <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>`;
(stripOrient === "Vertical ┋") ?
  html`<label><span style="font-size: 200%; vertical-align: -5%;"></span>↑ Plot as Y:</label>`
  :
  html`<label><span style="font-size: 200%; vertical-align: -5%;">🁣 </span>Groups:</label>`
  ;
viewof stripY = (stripOrient == "Vertical ┋") ?
  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`
  <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>`;
(stripOrient == "Vertical ┋") ?
  html`<label style="margin-top: 0.2em;" >🀱 Color by:</label>`
  :
  html`<label style="margin-top: 0.2em;" >🁣 Color by:</label>`
  ;

viewof stripDotFill = html`
  <select style="width: 89.5%; margin-top: 0.05em; 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: 0.55em; opacity: 0.6;" />`
viewof dotsJitterOrDodge = Inputs.radio(["Jitter", "Dodge"], {value: "Jitter"});

viewof stripStats = Inputs.radio(["None", "BoxPlot", "Mean ± SE", "Mean ± 95% CI" ], {label: "Show stats:", value: "None"});

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");
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 , "None");
  set(viewof stripHeight, 500);
  set(viewof stripWidth, 640);
  set(viewof stripAddCap, false);
  quillResetText(stripQuill);

};
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 == "Bar") ? // 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="barEditor" autocorrect="off" spellcheck="false">
  <p>Figure #. <b>Title.</b></p><p>Caption.</p>
</div>`
viewof barCountsOrPercentages = Inputs.radio(["Bar", "Marimekko"], {label: "Chart type:", value: "Bar"});

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>`
(barCountsOrPercentages === "Bar") ?
  html`<label>🁣 Color by:</label>`
  :
  html`<label>🁣 Plot as Y:</label>`
  ;
viewof barFill = 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>`

//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: 0; opacity: 0.6;" />`

html`<label><span style="font-size: 200%; vertical-align: -5%;">🀱 </span> Split by:</label>`
viewof barFacetX = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`
html`<label><span style="font-size: 200%; vertical-align: -5%;">🁣 </span> Split by:</label>`
viewof barFacetY = html`
  <select style="width: 80%; margin-left: 0.5em; margin-top: 0.25em; margin-bottom: 0.25em; height: 1.7em; background-color: white;">
  ${populateWithOptions([""].concat(categoricalColumns), 0)}
  </select>`

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");
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, "Bar");
  set(viewof barFacetX, "");
  set(viewof barFacetY, "");
  set(viewof barChartHeight, 500);
  set(viewof barChartWidth, 640);
  set(viewof barAddCap, false);
  quillResetText(barQuill);

};
Motivation

I wrote ardeaPlot 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 common types of graphs. My hope is that once students learn a bit about what graphical analyses can do for them, they’ll be motivated to master more general-purpose and complex software tools.

My work on ardeaPlot 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 ardeaPlot, I’d love to get an email about your experience.

Thanks,

Jon C. Herron

ardeaPlot is made with

Quarto, including:

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

Observable js, especially:

  • Observable Plot

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");
Quill = require("https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js");

// Global variables

largeFontSize = 13;

myColors = ["black", "gray", "lightgray", "steelblue", "darkseagreen"];

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

/* Old versions of getCategoricalColumns and getNumericalColumns

function getCategoricalColumns(data) {

  const categoricalColumns = [];
  
  if (data.length > 0) {
  
    const firstRow = data[0];
    
    for (const columnName in firstRow) {
    
      if (typeof firstRow[columnName] === 'string') {
        categoricalColumns.push(columnName);
        
      }
    }
  }
  
  return categoricalColumns;
  
};

function getNumericalColumns(data) {

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

  const numericalColumns = [];
  const firstRow = data[0];

  for (const key in firstRow) {
    if (typeof firstRow[key] === 'number') {
      numericalColumns.push(key);
    }
  }

  return numericalColumns;
  
};

*/

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
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 quillDisplay(myBoolean, myElement) {

  const elem = document.getElementById(myElement);

  if (myBoolean == true) {
    elem.style.display = "block";
  } else {
    elem.style.display = "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; 

};

ardeaPlot
ardea rapid data exploration & analysis