Public
Edited
Jul 19, 2021
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
propertyArray = [
{
"name": "integerBound",
"category": "1. Numeric",
"type": "integer",
"shape": "scalar",
"domain": [1,10],
"value": 5,
"readOnly": false,
"hidden": false
},
{
"name": "integerBoundReadOnlyWithLongParamName",
"category": "1. Numeric",
"type": "integer",
"shape": "scalar",
"domain": [1,10],
"value": 2,
"readOnly": true,
"hidden": false
},
{
"name": "selectionType",
"category": "2. Strings",
"type": "char",
"shape": "scalar",
"domain": ["a","set","of","selectable","strings"],
"value": ["of"],
"readOnly": false,
"hidden": false
},
{
"name": "dateOnlySelection",
"category": "3. Dates",
"type": "datestr",
"shape": "row",
"domain": [],
"value": "2020-04-20",
"readOnly": false,
"hidden": false
},
{
"name": "dateTimeSelection",
"category": "3. Dates",
"type": "datetime",
"shape": "row",
"domain": [],
"value": "2014-11-16T15:25:33",
"readOnly": false,
"hidden": false
},
{
"name": "doubleType",
"category": "1. Numeric",
"type": "double",
"shape": "scalar",
"domain": [],
"value": 0.01,
"readOnly": false,
"hidden": false
},
{
"name": "Logical",
"category": "4. Logical",
"type": "logical",
"shape": "scalar",
"domain": [],
"value": true,
"readOnly": false,
"hidden": false
},
{
"name": "LogicalReadOnly",
"category": "4. Logical",
"type": "logical",
"shape": "scalar",
"domain": [],
"value": true,
"readOnly": true,
"hidden": false
},
{
"name": "LogicalArray",
"category": "4. Logical",
"type": "logical",
"shape": "row",
"domain": ["aaaaaa","bbb","ccccccc"],
"value": [true,true,false],
"readOnly": false,
"hidden": false
},
{
"name": "SuggestSelection",
"category": "2. Strings",
"type": "char",
"shape": "scalar",
"domain": ["a","set", "with","optional","custom","input","..."],
"value": "a",
"readOnly": false,
"hidden": false
},
{
"name": "textEntry",
"category": "2. Strings",
"type": "char",
"shape": "row",
"domain": [],
"value": "some text",
"readOnly": false,
"hidden": false
},
{
"name": "multipleSelection",
"category": "2. Strings",
"type": "char",
"shape": "row",
"domain": ["a","multi","set","to","select"],
"value": ["a", "set"],
"readOnly": false,
"hidden": false
},
{
"name": "treeSelectorFromMap",
"category":"5. Map Selection",
"type": "map",
"shape":"row",
"domain": {"":"","a":5,"b":"20","c":{"a":5,"b":"c"}},
"value": "",
"readOnly": false,
"hidden": false
},
{
"name": "fileInputWithSelector",
"category":"6. Files",
"type": "file",
"shape":"scalar",
"domain": ["File1","File2","..."],
"value": "",
"readOnly": false,
"hidden": false
}
]
Insert cell
validateInput = (element) => {
let val = parseFloat(element.value);
let domain = [parseFloat(element.min),parseFloat(element.max)];
val = (val < domain[0]) ? domain[0] : val;
val = (val > domain[1]) ? domain[1] : val;
let precision = countDecimals(parseFloat(element.step)) + 1;
// set the value to the correct length
element.value = val.toFixed(precision);
};
Insert cell
humanize = str => {
const regex = /([A-Z])(?=[A-Z][a-z])|([a-z])(?=[A-Z])/g;
const first = /(^[a-z])/g;
return str.replace(regex,'$& ').replace(first,(m)=>m.toUpperCase());
}
Insert cell
theStyle = {
let s = `
table#proptable {
border-collapse: collapse;
table-layout: auto;
width: 100%;
font-family: "Times New Roman", "Source Serif Pro", "Iowan Old Style", "Apple Garamond", "Palatino Linotype","Droid Serif", Times, serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 15px;
font-weight: normal;
line-height: 1.35;
}

table#proptable tbody tr td,
table#proptable tbody tr th {
overflow: hidden;
white-space: nowrap;
padding:0px;
margin:0px;
}

table#proptable input,
table#proptable textarea,
table#proptable select {
border: none;
width: 100%;
}

table#proptable input::focus,
table#proptable textarea::focus,
table#proptable select::focus {
outline: 1px solid rgba(0, 0, 0, 0.15);
}

table#proptable input {
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}

table#proptable input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
appearance: none;
}

table#proptable input[type=number].spin::-webkit-inner-spin-button {
opacity: 1;
}

table#proptable .read-only input[type=number]::-webkit-inner-spin-button {
-webkit-appearance: none;
appearance: none;
}

input[type=number].typed::-webkit-inner-spin-button,
input[type=number].typed::-webkit-outer-spin-button {
-webkit-appearance: none;
opacity: 0;
}

table#proptable input[type=text]::-webkit-calendar-picker-indicator {
opacity: 1;
}

table#proptable input[type=datetime-local]::-webkit-calendar-picker-indicator {
background: url(https://systemuicons.com/images/icons/calendar_day.svg) no-repeat;
}

table#proptable input[type=date]::-webkit-calendar-picker-indicator {
background: url(https://systemuicons.com/images/icons/calendar_date.svg) no-repeat;
opacity: 1
}

table#proptable input[type=datetime-local]::-webkit-calendar-picker-indicator:hover,
table#proptable input[type=date]::-webkit-calendar-picker-indicator:hover {
background-size:100.5%;
}

.left {
width: 1px;
padding-right: 20px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
}

.right {
width: 100%;
padding-right: 2px;
}

.tooltip {
display: none;
position: absolute;
z-index: 100;
border: 1px;
background-color: white;
border: 1px solid black;
padding: 3px;
color: black;
top: 20px;
left: 20px;
}

.left:hover span.tooltip {
display: block;
}

div.checkboxes {
display:inline-block;
}

div.checkboxes input, div.checkboxes label {
padding:0px 10px 0px 0px;
margin: 0px;
display:inline-block;
}

table#proptable tr td div.checkboxes input[type=checkbox] {
width:fit-content;
margin-right:2px;
}

.static {
cursor: default;
}

.hide {
display: none;
}

.category {
background-color: rgb(70, 130, 180);
color: rgb(240, 240, 240);
font-size: 0.95em;
border: 1px solid rgba(0, 0, 0, 0.25);
}

.category.open:before {
content: "\\2BC6\\a0";
}

.category.closed:before {
content: "\\2BC8\\a0";
}

.no-select {
user-select: none;
}

.read-only * {
pointer-events: none !important;
color: rgba(0, 0, 0, 0.35);
}


`;
return `<style>${s}</style>`;
}
Insert cell
html`
<style>
div#table-wrap {
width:${width}px;
}
</style>
`
Insert cell
countDecimals = (value) => {
if(Math.floor(value) === value) return 0;
return value.toString().split(".")[1].length-1 || 0;
}
Insert cell
valueType = (obj) => {
var elem;
if (obj.readOnly){
elem = (str) => {return `<td class="right read-only">${str}</td>`};
} else {
elem = (str) => {return `<td class="right">${str}</td>`};
}
// determine type
switch (obj.type) {
case "integer":
if (!obj.domain.length) {
return elem(`<input class="spin" type="number" value="${obj.value}">`);
} else {
return elem(`<input class="spin" type="number" min=${obj.domain[0]} max=${obj.domain[1]} value=${obj.value}>`);
}
case "char":
// check domain and determine if is a selection
if (obj.domain.length > 0) {
// is select box
// determine if input is select or text
var io;
let boolMask = obj.domain.map( (e) => e === "..." );
const isTextEntry = boolMask.some((e) => e);
if (isTextEntry) {
let domain = obj.domain.filter( (e,i) => !boolMask[i] );
let options = domain.map( (e) => `<option value="${e}"></option>` );
let olist = `<datalist id="${obj.name}">${options.join("\n")}</datalist>`;
io = `<input type="text" list="${obj.name}" value="" placeholder="${obj.value}">${olist}`
} else {
let olist = obj.domain.map( (e) => `<option value="${e}"${e === obj.value?" selected":""}>${humanize(e)}</option>` );
io = `<select>${olist.join("\n")}</select>`
}
return elem(io);
} else {
return elem(`<input type="text" value="${obj.value}">`);
}
case "datestr":
if (!obj.domain.length) {
return elem(`<input type="date" name=${obj.name} value=${obj.value}>`);
} else {
return elem(`<input type="date" name=${obj.name} min=${obj.domain[0]} max=${obj.domain[1]} value=${obj.value}>`);
}
case "datetime":
if (!obj.domain.length) {
return elem(`<input type="datetime-local" name=${obj.name} value=${obj.value}>`);
} else {
return elem(`<input type="datetime-local" name=${obj.name} min=${obj.domain[0]} max=${obj.domain[1]} value=${obj.value}>`);
}
case "double":
let val = obj.value;
const ndec = countDecimals(val);
let stepVal = 1/10**(ndec+1);
if (!obj.domain.length) {
return elem(`<input class="typed" type="number" value="${val.toPrecision(ndec+1)}" step="${stepVal}" onblur="validateInput(this)">`);
} else {
return elem(`<input class="typed" type="number" min=${obj.domain[0]} max=${obj.domain[1]} value="${val.toPrecision(ndec+1)}" step="${stepVal}" onblur="validateInput(this)">`);
}
case "logical":
if (obj.shape == "scalar") {
return elem(`<input id="${obj.name}" type="checkbox"${obj.value ? " checked" : ""}${obj.readOnly?" disabled":""} /><span class="check-icon"></span>`);
} else {
let items = obj.domain.map( (d,i) => {
return `<label for="${d}"><input type="checkbox" id="${d}"${obj.value[i]?" checked":""}${obj.readOnly?" disabled":""} /><span class="check-icon"></span>${d}</label>`;
} ).join('\r\n');
return elem(`<div class="checkboxes">${items}</div>`);
}
default:
return elem(`${obj.value}`)
}
}
Insert cell
Insert cell
categorize = (pArr) => {
// Determine the category names and sort the propertyArray accordingly
// for observable, we need to return a new array
// gather categories and names
let categories = pArr.map( obj => ["category"].reduce((a,c) => a + obj[c],""));
let names = pArr.map( obj => ["name"].reduce((a,c) => a + obj[c],""));
// get unique categories and sort them so that undefined categories get sent to the end
let uniques = categories.filter((cat,idx,arr) => arr.indexOf(cat) === idx).sort((a,b)=> a===""?1:b===""?-1:0);
// collect indices from the sorted list
const indexOfAll = (arr, val) => arr.reduce((acc, el, i) => (el === val ? [...acc, i] : acc), []);
let catInds = uniques.map( cat => indexOfAll(categories,cat));
// get sort order by property "name" for each category.
let groupedSortOrder = catInds.map( locs => {
// gather names for sorting
let theseNames = names.filter( (n, idx) => locs.includes(idx) );
let nameMap = theseNames.map( (name,idx) => {return {index: idx, value: name.toLowerCase()}} );
nameMap.sort( (a,b) => a.value < b.value ? -1 : (a.value > b.value ? 1 : 0) );
return nameMap.map( (obj) =>locs[obj.index] );
} );
// reorder the propertyArray
return groupedSortOrder.map( (propInds,catNum) => {
return {category: uniques[catNum], properties: propInds.map( i => propertyArray[i] )};
} );
}
Insert cell
categorize(propertyArray)
Insert cell
Insert cell
Insert cell
Insert cell
{
const data = {
"name": "treeSelectorFromMap",
"category":"5. Map Selection",
"type": "map",
"shape":"row",
"domain": {"":"","a":5,"b":"20","c":{"a":5,"b":"c"}},
"value": "",
"readOnly": false,
"hidden": false
};
return data
}
Insert cell
testMultiSelect = {
const Data = {
"name": "multipleSelection",
"category": "2. Strings",
"type": "char",
"shape": "row",
"domain": ["a","multi","set","to","select","EvenIfAnEntryIsVeryLong","and","theTotal","length","isCrazy!"],
"value": ["a", "multi"],
"readOnly": false,
"hidden": false
};
const Data2 = {
"name": "multipleSelection2",
"category": "2. Strings",
"type": "char",
"shape": "row",
"domain": ["a","multi","set","to","select","EvenIfAnEntryIsVeryLong","and","theTotal","length","isCrazy!"],
"value": ["a", "set"],
"readOnly": false,
"hidden": false
};
/* TODO:
* Make the dropdown box "float" over everything like the builtin dropdown boxes.
It is currently clipped by the viewport. Maybe this isnt needed, but I may need to change
the position depending on where the cell is relative to the html boundary.
* Hook up search filter (onInput method) to only show the entries containing the text
*/
class MSel {
constructor({container,data,id}={}) {
this._uniqueId = id || 'sel';
this._parent = container || DOM.element('div');
this._parent.id = "multiselect-parent";
this._shadow = this._parent.attachShadow({mode:"open"});
this._shadow.innerHTML = `<style>${innerStyle}</style>`;
// set data
this._selectionCount = 0;
this._data = data || {};
this.parse();
// construct the ui
this.createUi();
this.bind();
// set the initial view
this.update();
// initialize list showing status
this._isListShowing = false;
}
// Render
render() {
return this._parent;
}
// Parser
parse() {
if (!this.isReady) return;
var value = this._data.value;
this._optionCount = this._data.domain.length;
// Items are ordered as they are presented.
this._items = this._data.domain.map((name,idx) => {
if (~Array.isArray(value)) value = Array.from(value);
const isChecked = value.length ? value.some((d) => d === name ) : false;
let item = {
id: name,
index: idx,
text: humanize(name),
element: MSel.createItem("li",name,humanize(name),isChecked),
get selected() {
let input = this.element.input;
return input.checked;
},
set selected(bool) {
// allow setting selected to be the mechanism for checking/unchecking
// the checkbox
this.element.input.checked = bool;
}
};
return item;
});
}
// Create UI
createUi() {
if (!this.isReady) return;
// create the input field
const wrap = DOM.element("div");
wrap.classList.add("multiselect-input-div");
const input = DOM.element("input");
input.id = this.InputFieldId;
input.classList.add("multiselect-input");
input.setAttribute("autocomplete","off");
input.setAttribute("type","text");
const counter = DOM.element("label");
counter.id = this.InputBadgeId;
counter.classList.add("multiselect-count");
counter.setAttribute("for",this.InputFieldId);
counter.innerHTML = 0;
const arrow = DOM.element("label");
arrow.classList.add("multiselect-dropdown-arrow");
arrow.setAttribute("for",this.InputFieldId);
wrap.appendChild(input);
wrap.appendChild(counter);
wrap.appendChild(arrow);
// store the input handles
this._input = {wrap:wrap,text:input,counter:counter,arrow:arrow};
// create the itemslist
const itemwrap = DOM.element("div");
itemwrap.id = this.ItemListId;
itemwrap.classList.add("multiselect-list");
var list = DOM.element("ul");
this._items.forEach( (item) => {
list.appendChild(item.element.wrap);
});
let selectall = MSel.createItem("span",-1,"Select All",false);
itemwrap.appendChild(selectall.wrap);
let hrule = DOM.element("hr");
itemwrap.appendChild(hrule);
itemwrap.appendChild(list);
// store the items list handles
this._itemList = {wrap:itemwrap,elems:list,rule:hrule,all:selectall};
}
// Bind elements and listeners
bind() {
if (!this.isReady) return;
const self = this;
// add listener to open the dropdown.
this._input.wrap.addEventListener("click", (evt) => {evt.stopPropagation});
// bind listeners to the input text field
/* OPEN UI */
//this._input.text.addEventListener("focus", (evt) => self.onInputFocus(evt));
this._input.text.addEventListener("click", (evt) => self.onInputFocus(evt));
this._input.text.addEventListener("dblclick", (evt) => self.onHideList(evt));
/* Capture keystrokes */
this._input.text.addEventListener("input", (evt) => self.onInput(evt));
// bind listeners to the itemlist container div
//this._itemList.wrap.addEventListener("mouseover",(evt) => self.onInputFocus(evt));
// add listener to the checkboxes
this._itemList.all.input.addEventListener("change", (evt) => {
self.onSelectAllChanged(evt);
});
this._items.forEach( (item) => {
let box = item.element.input;
box.addEventListener("change", (evt) => {
self.onSelectionChanged(item,evt);
});
});
// bind elements to the wrapper
this._shadow.appendChild(this._input.wrap);
this._shadow.appendChild(this._itemList.wrap);
// Capture window onclick to blur the dropdown
/*
window.onclick = (event) => {
let p = self._parent;
let obj = self;
(event.target !== p && !p.contains(event.target)) ? obj.onHideList(event) : null;
};
*/
}
/* ---------------- DOM Manipulation ----------------*/
// show the dropdown list
showList() {
this._itemList.wrap.classList.add('active');
this._input.arrow.classList.add('up');
this._isListShowing = true;
}
// hide the dropdown list, unfilter text
hideList() {
this._itemList.wrap.classList.remove('active');
this._input.arrow.classList.remove('up');
// make sure on next click all filters are removed
this.show(this._itemList.all.wrap);
this.show(this._itemList.rule);
this._itemList.elems.childNodes.forEach(this.show);
// update the text field from selected
this.update();
this._isListShowing = false;
}
update() {
// update the text in the input box and the counter label
let Ids = this.selectedIds;
let idCount = Ids.length;
this._selectionCount = idCount;
// text
this._input.text.value = idCount === this._optionCount ?
"All Selected" :
Ids.join(",");
// update the counter
this._input.counter.innerHTML = idCount;
// update data selection
this._data.value = Ids;
}
// Helper functions
show(elem) {
elem.style.display = "block";
}
hide(elem) {
elem.style.display = "none";
}
/* -------------------- Callbacks --------------------*/
onHideList(event) {
this.hideList();
if (event) event.stopPropagation();
}
onInputFocus(event) {
// show the item list
console.log("event:",event)
console.log("arrow:",this._input.arrow.offsetLeft)
if (!this._isListShowing) {
this.showList();
// erase the current text string
this._input.text.value = "";
} else if(event.offsetX >= this._input.arrow.offsetLeft) {
this.hideList();
}
}
onSelectionChanged(source,event) {
// synchronize the selection and update the text
if (event) event.stopPropagation();
this.update();
}
onSelectAllChanged(event) {
// toggle each item to to the new status of the selectAll
const status = event.target.checked;
this._items.forEach( (item) => MSel.setStatus(item,status) );
if (event) event.stopPropagation();
this.update();
}
onInput(event) {
// filter selections as matches to typed text. When typing, drop the selectAll option.
let currentText = this._input.text.value;
console.log(currentText);
}
/* -------------------- SET/GET --------------------*/
get isReady() {
let obj = this._data;
return !(!!obj && Object.keys(obj).length === 0 && obj.constructor === Object);
}
get Id() {
return `${this._uniqueId}_multiselect`;
}
get InputFieldId() {
return `${this._uniqueId}_input`;
}
get ItemListId() {
return `${this._uniqueId}_itemList`;
}
get InputBadgeId() {
return `${this._uniqueId}_inputCount`;
}
get itemIds() {
if (!this.isReady) return [];
return this._items.map( (v) => v.id );
}
get itemNames() {
if (!this.isReady) return [];
return this._items.map( (v) => humanize(v.id) );
}
get selectedIndices() {
if (!this.isReady) return [];
return this._items.reduce( (inds,elem,loc) => {
if(elem.selected) inds.push(loc);
return inds;
}, []);
}
get selectedIds() {
if (!this.isReady) return [];
return this._items.reduce( (ids,elem) => {
if(elem.selected) ids.push(elem.id);
return ids
}, []);
}
/* -------------------- Static --------------------*/
static createItem(type,value,text,isSelected) {
// create a checkbox and wrap it in a element of type
let box = DOM.element("input");
box.type = "checkbox";
box.classList.add("multiselect-checkbox");
box.dataset.val = value;
// set checked status
box.checked = isSelected;
let textBox = DOM.element("span");
textBox.classList.add("multiselect-text");
textBox.innerHTML = text;
let wrap = DOM.element(type);
let label = DOM.element("label");
label.appendChild(box);
label.appendChild(textBox);
wrap.appendChild(label);
return {wrap:wrap,input:box};
}
static setStatus(item,status) {
item.selected = status;
}
}
//--- SETUP ---//
const m = new MSel({data:Data});
const m2 = new MSel({data:Data2});
const tab = DOM.element('table');
tab.id = "testOuter";
const body = DOM.element('tbody');
body.innerHTML = `<tr><td class="left">left</td><td class="right">right</td></tr>`;
const row = DOM.element('tr');
row.appendChild(html`<td class="left">propname</td>`);
const cell = DOM.element('td');
cell.classList.add("right","show-overflow");
cell.appendChild(m.render());
row.appendChild(cell);

const row2 = DOM.element('tr');
row2.appendChild(html`<td class="left">propname2</td>`);
const cell2 = DOM.element('td');
cell2.classList.add("right","show-overflow");
cell2.appendChild(m2.render());
row2.appendChild(cell2);

body.appendChild(row);
body.appendChild(row2);
tab.appendChild(body);
const out = DOM.element('div');
out.id = "table-wrap";
out.style.height = "500px";
out.appendChild(tab);
/*
const but = DOM.element('input');
but.type = "button";
but.onclick = (evt) => {
//let vals = m._items.map()
};
*/
return out;
}
Insert cell
Insert cell
innerStyle = `
:host(#multiselect-parent) {
width: 100%;
display: inline-table;
white-space: nowrap;
position:relative;
}

.multiselect-input {
width:100%;
}

label {
display: block;
font-family: "Source Serif Pro", Iowan Old Style, Apple Garamond, Palatino Linotype, Times New Roman, "Droid Serif", Times, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
font-size: 15px;
font-weight: normal;
user-select: none;
}

.multiselect-input-div {
height: 100%;
width: 100%;
box-sizing: border-box;
border-collapse: collapse;
display: table-row;
}

.multiselect-input-div input {
border: none;
outline: none;
background: #fff;
margin: 0px;
padding: 3px 40px 3px 0px;
box-sizing: border-box;
text-overflow: ellipsis;
display: table-cell;
font-family: "Source Serif Pro", Iowan Old Style, Apple Garamond, Palatino Linotype, Times New Roman, "Droid Serif", Times, serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
font-size: 15px;
}

.multiselect-input-div input:focus {
outline: 1px solid rgba(0, 0, 0, 0.15);
}

.multiselect-input-div .multiselect-count {
position: absolute;
text-align: center;
border-radius: 5px;
background-color: lightblue;
display: table-cell;
overflow: visible;
font-size: 12px;
width: 20px;
height: 100%;
line-height: 200%;
margin-top: 2px;
bottom: 0px;
right: 20px;
}

.multiselect-dropdown-arrow {
width: 20px;
height: 100%;
position: absolute;
text-align: center;
display: table-cell;
right: 0px;
bottom: 0px;
background: url(https://systemuicons.com/images/icons/chevron_down_double.svg) no-repeat;
}

.multiselect-dropdown-arrow.up {
background: url(https://systemuicons.com/images/icons/chevron_up_double.svg) no-repeat;
}

.multiselect-list {
z-index: 1;
position: absolute;
display: block;
background-color: white;
outline: 1px solid rgba(0,0,0,0.25);
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
/* margin-top: 0px; */
padding: 5px;
min-width: 200px;
left: 0px;
right: 0px;
transition: opacity 210ms;
visibility: hidden;
opacity: 0;
}

.multiselect-list.active {
visibility: visible;
display: block;
opacity: 1;
}

.multiselect-list > span {
font-weight: normal;
}

.multiselect-list .multiselect-checkbox {
margin-right: 2px;
}

.multiselect-list > span,
.multiselect-list li {
cursor: default;
}

ul {
list-style: none;
display: block;
position: relative;
padding: 0px;
margin: 0px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
}

ul li {
padding-right: 20px;
display: block;
}

ul li.active {
background-color: rgba(0, 102, 255, 0.5);
color: white;
}

ul li:hover {
background-color: rgba(0, 102, 255, 0.7);
color: white;
}

.disabled .multiselect-dropdown-arrow {
border-top: 5px solid lightgray;
}

.disabled .multiselect-count {
background-color: lightgray;
}
`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more