ardeaPlot
Jon C. Herron
  • Data
  • Scatterplot
  • Histogram
  • Strip chart
  • Bar/Marimekko chart
  • Info
more

Filter data by searching. Results match all terms.

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

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

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

};

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

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

and

Quill

Many thanks to the creators of these tools!

Sources for built-in datasets:

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

  • Elephants: Mduduzi Ndlovu et al. 2018

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

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

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

d3 = require("d3@7");
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