Public
Edited
Dec 4
Paused
2 forks
Importers
5 stars
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

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