Public
Edited
Dec 4
Paused
2 forks
Importers
5 stars
Also listed in…
Observable Tricks
Insert cell
Insert cell
viewof selectedIsland = scentedCheckbox(penguins, (d) => d.island)
Insert cell
selectedIsland
Insert cell
viewof selectedSpecies = scentedCheckbox(penguins, (d) => d.species, {
showTotal: false,
label: "Species",
value: ["Gentoo", "Chinstrap"],
search: true // Use the search checkbox
})
Insert cell
viewof selectedSpeciesCustom = scentedCheckbox(penguins, (d) => d.species, {
showTotal: false,
label: "Species",
value: ["Gentoo", "Chinstrap"],
checkbox: multiAutoSelect // Use a custom checkbox
})
Insert cell
Insert cell
viewof selectedI = scentedCheckbox(
penguins.map((d, i) => ({ ...d, i })),
(d) => d.i,
{ cutoff: 10, maxHeight: 200 }
)
Insert cell
function scentedCheckbox(data, attr = (d) => d[0], _options) {
let options = {
barWidth: 50,
barFill: "#cdcdcd",
barBorder: "#bbb",
barBorderWidth: "0.5px",
showTotal: false, // Should use the max category as a reference (false) for the bars or the overall total (true)
selectAll: true, // Have all attributes selected by default
format: (d) => d,
cutoff: 5, // Any groups with less than this number will be grouped into other
othersLabel: "Others", // Label to show for others
arrayAttrib: false, // attribute values are arrays,
valueFont: `ultra-condensed 0.8em "Fira Sans", sans-serif`,
valueFmt: d3.format(",d"),
search: false,
checkbox: Inputs.checkbox,
maxHeight: null, // e.g. 200px
..._options
};

// Check if attr is array
if (options.arrayAttrib || (data.length && Array.isArray(attr(data[0])))) {
let unrolledData = [];

data = data
.map((d) => {
const attrD = attr(d);
// check if array is null or empty
return attrD
? attrD.map((attrValue) => ({ ...d, __unrolledAttr: attrValue }))
: [{ ...d, __unrolledAttr: attrD }];
})
.flat();
attr = (d) => d.__unrolledAttr;
options.arrayAttrib = true;
}

const dataGrouped = d3.group(data, attr);
let dataGroupedSorted = Array.from(dataGrouped.entries()).sort((a, b) =>
d3.descending(a[1].length, b[1].length)
);

console.log("dataGrouped", dataGrouped);

// Array with all the keys that didn't make the cutoff
let othersKeys = [];

if (options.cutoff) {
let dataGroupsSortedWithCutoff = [];
// A group for everything below the cutoff
let othersGroup = [options.othersLabel, []];
for (let [k, v] of dataGroupedSorted) {
if (v.length > options.cutoff) {
dataGroupsSortedWithCutoff.push([k, v]);
} else {
othersGroup[1].push(v);
othersKeys.push(k);
}
}
// Did we find any items for the cutoff
if (othersGroup[1].length > 0) {
dataGroupsSortedWithCutoff.push(othersGroup);
dataGroupedSorted = dataGroupsSortedWithCutoff;
dataGrouped.set(options.othersLabel, othersGroup[1]);
}
}
const keys = dataGroupedSorted.map((d) => d[0]);
const values = dataGroupedSorted.map((d) => d[1]);
const maxValue = d3.max(values, (v) => v.length);
const totalValue = d3.sum(values, (v) => v.length);
const fmtPct = d3.format(".1%");
const fmt = d3.format(",");

const x = d3
.scaleLinear()
.domain([0, options.showTotal ? totalValue : maxValue])
.range([0, options.barWidth]);

const oldFormat = options.format;
options.format = (d) => {
const dValue = dataGrouped.get(d).length;
return htl.html`${oldFormat(d)}
<span
style='
position: relative;
top: 0px;
left: 0px;
display: inline-block;
'
title='${d} ${dValue} records ${fmtPct(dValue / totalValue)}'
>
<span
style='
min-width:${x.range()[1]}px;
border: solid ${options.barBorderWidth} ${options.barBorder};
display:flex;
height: 100%;
'>&nbsp;</span>
<span
style='
min-width:${x(dValue)}px;
background:${options.barFill};
display:flex;
position: absolute;
top: ${options.barBorderWidth};
left: ${options.barBorderWidth};
height: calc(100% - 3 * ${options.barBorderWidth});
align-items: center;
font: ${options.valueFont};
'>${options.valueFmt(dValue)}</span>
</span>
`;
};

if (options.selectAll && !options.value) {
options.value = keys;
}
let checkboxes;

if (options.search) {
checkboxes = searchCheckbox(keys, options);
} else {
checkboxes = options.checkbox(keys, options);
}
const target = htl.html`<div
style="${
options.maxHeight ? `max-height:${options.maxHeight}; overflow: scroll;` : ""
}">${checkboxes}</div>`;

checkboxes.oninput = (evt) => {
setValue();
};

function setValue() {
let value = [];
for (let v of checkboxes.value) {
// If we have others in the keys, replace them with the original keys
if (v === options.othersLabel) {
value = value.concat(othersKeys);
} else {
value.push(v);
}
}
target.value = value;

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

setValue();

target.addEventListener("input", () => {
checkboxes.dispatchEvent(new Event("input", { bubbles: false }));
});

return target;
}
Insert cell
Insert cell
// Support array attributes

viewof selected2 = scentedCheckbox(
[
{ a: [1, 2, 3] },
{ a: [2, 3] },
{ a: [2] },
{ a: [4] },
{ a: [5] },
{ a: [6] },
{ a: [7] },
{ a: [8] },
{ a: [9] },
{ a: [10] }
],
(d) => d.a,
{
cutoff: 2
}
)
Insert cell
selected2
Insert cell
import {aq, op} from "@uwdata/arquero"
Insert cell
import {searchCheckbox} from "@john-guerra/search-checkbox"
Insert cell
import {multiAutoSelect} from "@john-guerra/multi-auto-select"
Insert cell
{
(viewof selectedIsland).value = ["Biscoe"];
(viewof selectedIsland).dispatchEvent(new Event("input", { bubbles: true }));
}
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more