• Overview
  • Scores
  • Items
ns = Inputs.text().classList[0]

// custom css to override some ojs defaults for inputs
html`<style>

  .${ns} {
    --label-width: 80px;
  }

  form.${ns} {
    flex-wrap: wrap;
  }
  
  .plot-inputs form.${ns} {
    flex-direction: column;
  }
  
  .${ns} div label {
    background-color: #f4f4f4;
    padding: 0.25rem 0.5rem;
    border-radius: 0.5rem;
    margin-right: 0.25rem;
    margin-bottom: 0.25rem;
    width: auto;
  }
  
  .${ns} div label:hover,
  .${ns} div label:active,
  .${ns} div label:focus {
    background-color: #fbe4b4;
  }
  
  .${ns} input[type="checkbox"] {
    accent-color: black;
    margin-bottom: 0;
  }

  .${ns} div input[type="number"] {
    background-color: #f4f4f4;
    padding: 0.25rem 0.5rem;
    border-radius: 0.5rem;
    flex-shrink: 3;
    border: none;
  }
  
  .${ns} select {
    background-color: #f4f4f4;
    border: none;
    border-radius: 0.5rem;
    padding: 0.25rem 0.5rem;
    //width: auto;
  }
  
  .${ns} .hist {
    width: 100%;
    display: flex;
    flex-direction: column;
    row-gap: 0em;
  }

</style>`
tol = ({
    QualBright: ['#4477AA', '#EE6677', '#228833', '#CCBB44', '#66CCEE','#AA3377'],
    QualHighContrast: ['#004488', '#DDAA33', '#BB5566'],
    QualVibrant: ['#EE7733', '#0077BB', '#33BBEE', '#EE3377', '#CC3311', '#009988'],
    QualMuted: ['#CC6677', '#332288', '#DDCC77', '#117733', '#88CCEE','#882255', '#44AA99', '#999933', '#AA4499'],
    QualLight: ['#77AADD', '#EE8866', '#EEDD88', '#FFAABB', '#99DDFF', '#44BB99', '#BBCC33', '#AAAA00'],
    Sunset: ['#364B9A', '#4A7BB7', '#6EA6CD', '#98CAE1', '#C2E4EF', '#EAECCC', '#FEDA8B', '#FDB366', '#F67E4B', '#DD3D2D', '#A50026'],
    BuRd: ['#2166AC', '#4393C3', '#92C5DE', '#D1E5F0', '#F7F7F7', '#FDDBC7', '#F4A582', '#D6604D', '#B2182B'],
    PRGn: ['#762A83', '#9970AB', '#C2A5CF', '#E7D4E8', '#F7F7F7', '#D9F0D3', '#ACD39E', '#5AAE61', '#1B7837'],
    YlOrBr: ['#FFFFE5', '#FFF7BC', '#FEE391', '#FEC44F', '#FB9A29',
                '#EC7014', '#CC4C02', '#993404', '#662506'],
    Iridescent: ['#FEFBE9', '#FCF7D5', '#F5F3C1', '#EAF0B5', '#DDECBF',
                '#D0E7CA', '#C2E3D2', '#B5DDD8', '#A8D8DC', '#9BD2E1',
                '#8DCBE4', '#81C4E7', '#7BBCE7', '#7EB2E4', '#88A5DD',
                '#9398D2', '#9B8AC4', '#9D7DB2', '#9A709E', '#906388',
                '#805770', '#684957', '#46353A'],
    RainbowPuRd: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
                '#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
                '#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
                '#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
                '#DF4828', '#DA2222'],
    RainbowPuBr: ['#6F4C9B', '#6059A9', '#5568B8', '#4E79C5', '#4D8AC6',
                '#4E96BC', '#549EB3', '#59A5A9', '#60AB9E', '#69B190',
                '#77B77D', '#8CBC68', '#A6BE54', '#BEBC48', '#D1B541',
                '#DDAA3C', '#E49C39', '#E78C35', '#E67932', '#E4632D',
                '#DF4828', '#DA2222', '#B8221E', '#95211B', '#721E17',
                '#521A13'],
    RainbowWhRd: ['#E8ECFB', '#DDD8EF', '#D1C1E1', '#C3A8D1', '#B58FC2',
                '#A778B4', '#9B62A7', '#8C4E99', '#6F4C9B', '#6059A9',
                '#5568B8', '#4E79C5', '#4D8AC6', '#4E96BC', '#549EB3',
                '#59A5A9', '#60AB9E', '#69B190', '#77B77D', '#8CBC68',
                '#A6BE54', '#BEBC48', '#D1B541', '#DDAA3C', '#E49C39',
                '#E78C35', '#E67932', '#E4632D', '#DF4828', '#DA2222'],
    RainbowDiscrete: ['#E8ECFB', '#D9CCE3', '#D1BBD7', '#CAACCB', '#BA8DB4',
                '#AE76A3', '#AA6F9E', '#994F88', '#882E72', '#1965B0',
                '#437DBF', '#5289C7', '#6195CF', '#7BAFDE', '#4EB265',
                '#90C987', '#CAE0AB', '#F7F056', '#F7CB45', '#F6C141',
                '#F4A736', '#F1932D', '#EE8026', '#E8601C', '#E65518',
                '#DC050C', '#A5170E', '#72190E', '#42150A']
})
function dropdownInput(config = {}) {

  //set up configuration
  let {
    inputId, 
    inputLabel,
    placeholderText,
    options, 
    selected,
  } = config;


  const width = '250px'

  const maxHeight = '250px'

  //dropdown  button
  const dropdownButton = Inputs.button(html`<div class='button-inner' style='display:flex;justify-content:space-between;height:100%;' id='dropdown-${inputId}'><div class='filter-selected' style='text-align:left;'>${placeholderText}</div><span style='font-size:10px;padding-left:5px;display:flex;align-items:center;'><i class="fas fa-chevron-down"></i></span></div>`);
  dropdownButton.classList.add("dropdown-button");

  const  optionValues = options.map(option => option.value)

  //selectInput
  const selectInput = Inputs.select(optionValues, {multiple:true, value:selected});
  selectInput.classList.add("dropdown-select");
  selectInput.style.display="none";

  //select and deselect buttons
  const selectDeselectButtons = Inputs.button([["Select All", value=> "Select All"],["Deselect All", value => "Deselect All"]]);
  selectDeselectButtons.classList.add("dropdown-action-buttons")
  
  let selectInputValue = selected.slice(); // Create a copy of selected for selectInputValue

  let optionListItems = options.map(option => {
    const isSelected = selectInputValue.includes(option.value);
    const listItem = html`<li id='${option.value}'><a style='cursor:pointer;'><span class='text'>${option.label}</span><span class='checkmark'><i class="fas fa-check"></i></span></a></li>`;

    
    // Event listener for the list item
    listItem.addEventListener('click', (event) => {
      event.preventDefault();
      
      const clickedListItem = event.currentTarget;
      const clickedItemId = clickedListItem.id;
      const index = selectInputValue.indexOf(clickedItemId);

      if (clickedListItem.classList.contains('selected')) {
        clickedListItem.classList.remove('selected');
        
        if (index !== -1) {
          selectInputValue.splice(index, 1);
        }
      } else {

        clickedListItem.classList.add('selected');
        selectInputValue.push(option.value);
      }

      // Update selectInput value
      selectInput.value = selectInputValue;
      
      // Update form value with selectInputValue
      form.value = selectInputValue;
      form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
    });

    return listItem;
  });

  const optionList = html`<ul class='dropdown-list'>${optionListItems}</ul>`

  const innerDropdown = html`<div class='dropdown-action-buttons-container'>${selectDeselectButtons}</div>${selectInput} <div class='dropdown-list-container' style='max-height:${maxHeight};overflow-y:scroll;'>${optionList}</div>`
  
  innerDropdown.classList.add("dropdown-inner")
  innerDropdown.style.display = "none";
  innerDropdown.style.width = width;

   // Function to toggle dropdown visibility
  const toggleDropdown = () => {
    innerDropdown.style.display = innerDropdown.style.display === "none" ? "block" : "none";
  };

  // Toggle the visibility of the select input on button click
  dropdownButton.onclick = (event) => {
    toggleDropdown();
  };

    optionListItems.forEach((t, index) => {
  const option = options[index].value;
  if (selectInputValue.includes(option)) {
    t.classList.add('selected');
  }
});
  
selectDeselectButtons.onclick = () => {
  const buttonText = selectDeselectButtons.value;
  if (buttonText === "Select All") {
    selectInput.value = options.map(option => option.value);
    selectInputValue = options.map(option => option.value);
    form.value = selectInputValue;

    optionListItems.forEach(t => {
        t.classList.add('selected');
    });
  } else if (buttonText === "Deselect All") {
    selectInput.value = [];
    selectInputValue = [];
    form.value = [];

    optionListItems.forEach(t => {
        t.classList.remove('selected');
    });
  }

  form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};

//modified from inputs formula by https://observablehq.com/@jashkenas/inputs
selectInput.onchange = e => {
  e && e.preventDefault();
  const value = selectInput.value; 
  if (form.output) {
    const out = value;
    if (out instanceof window.Element) {
      while (form.output.hasChildNodes()) {
        form.output.removeChild(form.output.lastChild);
      }
      form.output.append(out);
    } else {
      form.output.value = out;
    }
  }
  // Update form.value with the selectInput value
  form.value = value;
  form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
  
  const form = html`<label style='font-weight:bold;font-size:14px;'>${inputLabel}</label><form style='width:250px' id=${inputId}>${dropdownButton} ${innerDropdown}</form>`

  form.classList.add("dropdown-form");

  form.value = selected;

  // clicking anywhere outside the dropdown closes it
  const closeOnClickOutside = (event) => {
    if (!form.isConnected) {
      // form was discarded by a re-render of the cell, so drop the stale listener
      document.removeEventListener("click", closeOnClickOutside);
    } else if (!form.contains(event.target)) {
      innerDropdown.style.display = "none";
    }
  };

  document.addEventListener("click", closeOnClickOutside);



const updatePlaceholderText = () => {
  const selectedValues = form.value;
  if (selectedValues.length > 0) {
    const filteredLabels = options
      .filter(option => selectedValues.includes(option.value))
      .map(filteredOption => {
        // Check if the label is an HTML element
        if (typeof filteredOption.label === 'object' && filteredOption.label instanceof HTMLElement) {
          return filteredOption.label.outerHTML;
        } else {
          return filteredOption.label; // Assuming label is a string if not HTML
        }
      });
      
    const concatenatedText = filteredLabels.join(', ');
    dropdownButton.querySelector('.filter-selected').innerHTML = `${concatenatedText}`;
  } else {
    dropdownButton.querySelector('.filter-selected').innerHTML = placeholderText;
  }
};

  // ... (existing code)

  // Listen for changes in form.value
  form.addEventListener('input', updatePlaceholderText);


  // Initial update of placeholder text
  updatePlaceholderText();
  
  return form;
}

html`
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">

<style>

  .filter-selected {
    text-wrap:nowrap;
    overflow:hidden;
    display:flex;
    align-items:center;
  }

  .dropdown-action-buttons-container {
    padding:10px;
  }

    .dropdown-action-buttons {
    width:100%;

  }

    .dropdown-action-buttons button {
    width:49%;
    font-size:14px;
    height:30px;
    background-color:#E9E9E9;
  }

    .dropdown-list a {
    width:100%;
    display:flex;
    align-items:center;
    min-height:14px!important;
  }
  
  .dropdown-list .text {
    width:90%;
  }

    .dropdown-inner {
    border:1px #ccc solid;
    border-radius:4px;
    background-color:white;
    margin-top:5px;
    position:absolute;
    z-index:2000;
  }


  .dropdown-list .checkmark {
    width:10%:
  }

  .dropdown-list {
    padding:0px;
    margin:0px;
    line-height:0px;
    list-style: none;
  }

  .dropdown-list li {
    display:flex;
    align-content:center;
    min-height:35px;
    font-size:13px;
  }

    li .checkmark {
    display:none;
    float:right;
    padding-right:5px;
  }

    li.selected .checkmark {
    display:flex!important;
  }

   .dropdown-list li:hover {
      background-color:whitesmoke!important;
      cursor:pointer;
  }
    .dropdown-list>li>a {
    padding: 10px 20px!important;
    white-space: nowrap;
}

  .dropdown-form button {
    background-color:white!important;
    color: #333!important;
    border:1px #ccc solid!important;
    border-radius:4px!important;
    cursor:pointer;
  }

    .dropdown-button button {
    width:100%!important;
    height:35px!important;
  }
</style>`
Plot = import("https://esm.sh/@observablehq/plot@0.6.17")

overview = transpose(overview_)

task_info = ({
      "hearts-and-flowers": "Hearts & Flowers",
"same-different-selection": "Same & Different",
             "memory-game": "Memory",
               "egma-math": "Math",
        "matrix-reasoning": "Pattern Matching",
         "mental-rotation": "Shape Rotation",
                    "trog": "Sentence Understanding",
                   "vocab": "Vocabulary",
          "theory-of-mind": "Stories",
                      "pa": "Language Sounds",
                     "sre": "Sentence Reading",
                     "swr": "Word Reading",
})

ov = overview.map(obj => ({
  ...obj,
  task_label: task_info[obj.task_id],
  date: d3.utcParse("%Y-%m-%d")(obj.date),
  score: obj.scored ? obj.run_id_hash : null
}))
displayScheme = [
  [204, 102, 119], // red
  [221, 204, 119], // yellow
  //[153, 153,  51], // yellow-green 17,119,51
  [ 17, 119,  51], // green
  [ 68, 170, 153], // teal
  [136, 204, 238], // light blue
  [ 51,  34, 136], // dark blue
  [170,  68, 153], // magenta
]

//['#CC6677', '#332288', '#DDCC77', '#117733', '#88CCEE','#882255', '#44AA99', '#999933', '#AA4499']

displayConfig = ([
  { index: 0, property: "dataset",      label: "Datasets",     icon: "archive"        },
  { index: 1, property: "team",         label: "Teams",        icon: "geo"            },
  { index: 2, property: "language",     label: "Languages",    icon: "translate"      },
  { index: 3, property: "task_id",      label: "Tasks",        icon: "ui-checks-grid" },
  { index: 4, property: "user_id_hash", label: "Participants", icon: "person-arms-up" },
  { index: 5, property: "score",        label: "Scores",       icon: "bullseye" },
  { index: 6, property: "run_id_hash",  label: "Assessments",  icon: "clipboard-data" },
])

displayBox = ({ index, property, label, icon }) => {
  const values = new Set(ov.map(d => d[property]))
  const color = displayScheme[index]
  const color_str = `${color[0]}, ${color[1]}, ${color[2]}`
  const borderColor = `rgb(${color_str})`
  const backgroundColor = `rgba(${color_str}, 0.4)`
  return html`<div class="callout callout-style-default callout-note no-icon callout-empty-content callout-titled callout-display" style="border-left-color: ${borderColor};">
    <div class="callout-header d-flex align-content-center" style="background-color: ${backgroundColor};">
      <div class="callout-title-container flex-fill">
        <span class="display-box">
          <span class="display-icon"><i class="bi-${icon}" role="img" aria-hidden="true"></i></span>
          <span class="display-content">
            <span class="display-var">${label}</span>
            <span class="display-val">${values.size}</span>
          </span>
        </span>
      </div>
    </div>
  </div>`
}

boxes = displayConfig.map(displayBox)
html`<div class="display-box-container">${boxes}</div>`
min_date = (days) => d3.utcDay.offset(d3.utcDay(new Date()), 1 - days)

// overview data filtered by the recency dropdown, shared by both plots
ovf = recency === null ? ov : ov.filter(d => d.date >= min_date(recency))

// distinct count of a variable over some rows, skipping rows where it is
// missing -- score is null for unscored runs, which would otherwise count as a
// value of its own and add one to every group
n_distinct = (rows, v) => new Set(rows.filter(d => d[v] !== null).map(d => d[v])).size

// display label of the currently selected value of a dropdown
opt_label = (opts, value) => [...opts].find(([label, v]) => v === value)[0]

scheme = tol.QualMuted // color scheme
default_color = scheme[1] // default color when there's no grouping variable
plt_color = group_var || default_color // plot color variable

// bootstrap icon for each radio option, reusing the display box icons above.
// labels are unique across the inputs, except dataset/team/language/task which
// mean the same thing in both places
opt_icons = ({
  "Assessments": "clipboard-data",
  "Scores": "bullseye",
  "Participants": "person-arms-up",
  "Dataset": "archive",
  "Team": "geo",
  "Language": "translate",
  "Task": "ui-checks-grid",
  //"None": "slash-circle",
  "Age": "people",
  "Date administered": "calendar-event",
  //"7 days": "calendar3",
  //"30 days": "calendar3",
  //"90 days": "calendar3",
  //"365 days": "calendar3",
  //"Anytime": "infinity"
})

// radio option rendered with its icon, falling back to text alone
icon_label = ([label]) => opt_icons[label]
  ? html`<span><i class="bi bi-${opt_icons[label]} opt-icon"></i>${label}</span>`
  : html`<span>${label}</span>`

// shown in place of a plot when the filters exclude everything
no_data = () => html`<p class="filter-error"><i class="bi bi-exclamation-triangle"></i> No data selected</p>`
count_opts = new Map([["Assessments", "run_id_hash"], ["Scores", "score"], ["Participants", "user_id_hash"]])
viewof count_var = Inputs.radio(count_opts, { label: "Count of", value: "run_id_hash", format: icon_label })
group_opts = new Map([["Dataset", "dataset"], ["Team", "team"], ["Language", "language"], ["Task", "task_label"], ["None", null]])
viewof group_var = Inputs.radio(group_opts, { label: "Grouped by", value: "language", format: icon_label })
recency_opts = new Map([["7 days", 7], ["30 days", 30], ["90 days", 90], ["365 days", 365], ["Anytime", null]])
viewof recency = Inputs.radio(recency_opts, { label: "Collected within past", value: null, format: icon_label })
y_var_opts = new Map([["Dataset", "dataset"], ["Team", "team"], ["Language", "language"], ["Task", "task_label"]])
viewof y_var = Inputs.radio(y_var_opts, { label: "For each", value: "task_label", format: icon_label })
marginLeft = 152

ovf.length === 0 ? no_data() : Plot.plot({
  style: { fontFamily: "var(--sans-serif)", fontSize: 12 },
  width: 0.43 * width,
  marginLeft: marginLeft,
  marginTop: 10,
  marginBottom: 40,
  x: { label: `${opt_label(count_opts, count_var)} count`, axis: "bottom", line: true },
  y: { label: opt_label(y_var_opts, y_var), labelAnchor: "top", tickSize: 0, tickPadding: 2 },
  color: {
    range: scheme,
    legend: group_var !== null,
    marginLeft: marginLeft,
    className: "color-legend"
  },
  marks: [
    Plot.barX(ovf, Plot.groupY(
      { x: g => n_distinct(g, count_var) },
      { y: y_var, fill: plt_color, sort: { y: "x", reverse: true, }, inset: 1 }
    )),
    Plot.textX(ovf, Plot.stackX(Plot.groupY(
      { x: g => n_distinct(g, count_var), text: g => n_distinct(g, count_var) },
      { y: y_var, z: plt_color, fill: "white" }
    ))),
  ]
})
hist_bin_opts = new Map([["Age", "age"], ["Date administered", "date"]])
viewof hist_bin = Inputs.radio(hist_bin_opts, { label: "Binned by", value: "age", format: icon_label })
// a participant has one age and date per run, so there is no single value to
// bin them by -- say so rather than plot something misleading
count_var === "user_id_hash"
  ? html`<p class="filter-error"><i class="bi bi-info-circle"></i> Participants can't be binned by age or date, since each participant has multiple assessments</p>`
  : ovf.length === 0 ? no_data() : Plot.plot({
      style: { fontFamily: "var(--sans-serif)", fontSize: 12 },
      width: 0.43 * width,
      height: 300,
      marginTop: 20,
      marginBottom: 50,
      x: { label: opt_label(hist_bin_opts, hist_bin), line: true },
      y: { label: "Count" },
      color: {
        legend: group_var !== null,
        range: scheme,
        className: "color-legend"
      },
      marks: [
        Plot.rectY(ovf, Plot.binX(
          { y: g => n_distinct(g, count_var) },
          { fill: plt_color, x: { value: hist_bin } }
        )),
      ]
    })