Published
Edited
Jan 5, 2022
Importers
27 stars
Insert cell
Insert cell
viewof filtered = filters(data)
Insert cell
Inputs.table(filtered)
Insert cell
Insert cell
filters = (data, facets = auto(data), { style = {}, reset = true } = {}) => {
let fields = facets.map((f) => f(data));
let value = fields.map((f) => f.value);
value = fields.map((f) => f.value);
const resetButton = reset
? htl.html`<button onClick=${() => {
fields.forEach((f) => f.resetFilters());
form.dispatchEvent(new Event("input", { bubbles: true }));
}} disabled>Reset all filters</button>`
: "";
const form = htl.html`<div style=${style}>
${resetButton}
${fields}
</div>`;

form.addEventListener("input", () => {
fields.forEach((f, i) => {
let cf = data.filter((d) => value.every((f, j) => i === j || f(d)));
f.crossfilteredData = cf;

resetButton.disabled = cf.length == data.length;
});
});
return Object.defineProperty(form, "value", {
get() {
return data.filter((d) => value.every((f) => f(d)));
}
});
}
Insert cell
Insert cell
// This is a heuristic for auto to distinguish meaningfull categorical dimensions
categoryLimit = 10
Insert cell
auto = (dataset) => {
if (dataset.columns) {
return dataset.columns.flatMap((column) => {
switch (typeof dataset[0][column]) {
case "string":
const categories = _.countBy(dataset, column);
if (
_.keys(categories).length <
Math.min(categoryLimit, dataset.length / 4)
) {
return [facetCheckboxes({ facet: column, label: column })];
}
case "number":
return [facetRange({ facet: column, label: column })];
default:
return [];
}
});
}
return [];
}
Insert cell
facetRange = ({
facet,
label,
buildFilter = (extent) => (datum) => {
const val = _.property(facet)(datum);
return (
val == null || isNaN(val) || (val >= extent.min && val <= extent.max)
);
}
}) => (data) => {
const [w, h] = [300, 70];
const padding = 25;
const form = htl.html`<form class="facet-form">
<legend>${label}</legend>
<svg width=${w} height=${h}></svg>
</form>`;
const [min, max] = d3.extent(data, _.property(facet));
let extent = { min, max };
const bin = d3
.bin()
.value(_.property(facet))
.domain([min, max])
.thresholds((values, mi, ma) => {
let vals = [
extent.min,
extent.max,
...d3.ticks(mi, ma, d3.thresholdSturges(values) - 2)
];
vals.sort();
return vals;
});
const histogram = bin(data);
const x = d3
.scaleLinear()
.domain([min, max]) // data space
.range([padding, w]); // display space
const y = d3
.scaleLinear()
.domain([0, d3.max(histogram, _.property("length"))])
.range([h - padding, 0]);
const svg = d3.select(form).select("svg");
const g = svg.append("g");
const dataContainer = g.append("g");
const render = (data) => {
const histogram = bin(data);
// draw histogram values
dataContainer
.selectAll("rect")
.data(histogram)
.join("rect")
// .transition()
// .duration(80)
.attr("x", (d) => x(d.x0))
.attr("y", (d) => y(d.length || 0))
.attr("width", (d) => x(d.x1) - x(d.x0))
.attr("height", (d) => h - padding - y(d.length || 0))
.style("fill", (d) => {
if (d.x0 >= extent.min && d.x1 <= extent.max) {
return "steelblue";
}
return "#ccc";
});
};
render(data);

var brush = d3
.brushX()
.extent([
[padding, 0],
[w, h - padding]
])
.on("brush", function (e) {
if (e.selection) {
let inverted = e.selection.map((v) => x.invert(v));
extent.min = Math.min(inverted[0], inverted[1]);
extent.max = Math.max(inverted[0], inverted[1]);
} else {
extent.min = min;
extent.max = max;
}
form.dispatchEvent(new Event("input", { bubbles: true }));
})
.on("end", function (e) {
if (e.selection == null) {
extent.min = min;
extent.max = max;
form.dispatchEvent(new Event("input", { bubbles: true }));
}
});

g.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${h - padding})`)
.call(d3.axisBottom(x));
g.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${padding},0)`)
.call(d3.axisLeft(y).ticks(2));
var gBrush = g.append("g").attr("class", "brush").call(brush);

form.resetFilters = function () {
extent.min = min;
extent.max = max;
brush.clear(g.select(".brush"));
};
return Object.defineProperties(form, {
value: {
get() {
return buildFilter(extent);
}
},
crossfilteredData: {
set(cf) {
render(cf);
}
}
});
}
Insert cell
facetCheckboxes = ({
facet,
label,
filter = (v) => true,
counter = _.countBy,
buildFilter = (val) => (datum) => val.has(_.property(facet)(datum))
}) => (data) => {
const total = data.length;
const counts = counter(data, facet);
const defaults = _.mapValues(counts, () => 0);
const allSelected = new Set(_.keys(counts).filter(filter));
let formValue = new Set(_.keys(counts).filter(filter));
const onInput = (value) => (event) => {
if (formValue.has(value)) {
formValue.delete(value);
} else {
formValue.add(value);
}
event.currentTarget.form.dispatchEvent(
new Event("input", { bubbles: true })
);
};
const form = htl.html`<form class="facet-form">
<style>
.facet-form, .facet-form .fields {
display: flex;
flex-direction: column;
font: 13px/1.2 var(--sans-serif);
}
.facet-form legend {
font-weight: bold;
margin-top: 6px;
}
.facet-form label {
position: relative;
}
.facet-form .bar {
position: absolute;
height: 1em;
background: #ccc;
opacity: 0.3;
left: 20px;
top: 4px;
transition: width 0.2s;
}
.facet-form input:checked + .bar {
background: steelblue;
}
.facet-form .label, .facet-form .count {
z-index: 20;
display: inline-block;
opacity: 0.999;
}
.facet-form .count {
margin-left: 0.6rem;
}
</style>
<legend>${label}</legend>
<div class="fields">${_.map(
_.entries(counts).filter(([v, c]) => filter(v)),
([value, count]) =>
htl.html`<label><input type="checkbox" checked=${formValue.has(
value
)} oninput=${onInput(value)} /><span class="bar" style=${{
width: (count / total) * 100 + "%"
}}></span>
<span class="label">${value}</span> <small class="count">${count}</small></label>`
)}</div>
</form>`;
const update = (data) => {
const counts = _.merge({}, defaults, counter(data, facet));
const target = form.getElementsByClassName("fields")[0];
_.forEach(
_.entries(counts).filter(([v, c]) => filter(v)),
([value, count], index) => {
target.children[index].getElementsByClassName("bar")[0].style.width =
(count / total) * 100 + "%";
target.children[index].getElementsByClassName(
"count"
)[0].textContent = count;
}
);
};

form.resetFilters = function () {
allSelected.forEach((v) => formValue.add(v));
_.forEach(form.querySelectorAll("input[type=checkbox]"), (el) => {
el.checked = true;
});
};
return Object.defineProperties(form, {
value: {
get() {
const f = buildFilter(formValue);
return (d) => {
if ([...allSelected].every((s) => formValue.has(s))) {
return true;
}
return f(d);
};
}
},
crossfilteredData: {
set(cf) {
update(cf);
}
}
});
}
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