Public
Edited
Jul 28, 2024
2 forks
Importers
1 star
Insert cell
Insert cell
viewof selected = FacetedSearch(data, {
// attribs: ["date_of_birth","nationality"], // leave blank for using all attributes
filterData: filterDataJS
})
Insert cell
selected.filters
Insert cell
selected
Insert cell
viewof data = dataInput({value: olympians, format: "csv"})
Insert cell
data[0].date_of_birth
Insert cell
d = new Date()
Insert cell
d.toLocaleString()
Insert cell
attr="year"
Insert cell
extent = d3.extent(data, (d) => +new Date(d[attr]));
Insert cell
interval(extent, {
label: attr,
format: ([s, e]) => {
console.log(s, e, typeof s);
return `${new Date(s).toLocaleString()} ... ${new Date(e).toLocaleString()}`;
}
})
Insert cell
function FacetedSearch(data, { attribs, filterData = filterDataJS } = {}) {
if (!data || data.length < 0) {
throw Error("Please provide an array of objects");
}

const filtersContainer = html`<div id="filters"></div>`;
const target = html`
${filtersContainer}
`;
const filters = new Map();
// the selected elements
target.value = data;
target.value.filters = filters;

function redraw() {
filtersContainer.innerHTML = "";
filtersContainer.appendChild(
html`<div >Selected ${target?.value.length} of ${data.length}
${Array.from(filters.values()).map(({ attr, ele }) => {
return html`
<div>${ele}</div>
`;
})}
</div>`
);
}

async function updateAndRedraw() {
let before;
if (debug) {
before = performance.now();
}

const focused = document.activeElement;
// Filter the data
target.value = await filterData(data, filters);

debug && console.log(`Finished filtering ${performance.now() - before}`);
target.value.filters = filters;
// Trigger an update
target.dispatchEvent(new Event("input", { bubbles: true }));

redraw();

// Trying to return the focus
console.log("Trying to return the focus to", focused, document.activeElement);
focused.focus && focused.focus()
}

attribs = attribs || Object.keys(data[0]);

for (let attr of attribs) {
// Assumes NaN => categorical
if (isNaN(data[0][attr])) {
// if (possibleValues.length < 20) {
addSearchCheckboxes({
attr,
filters,
data,
updateAndRedraw,
target
});
// We need a better filter for many values
// }
// else {
// addMultiAutoSelect({
// attr,
// filters,
// data,
// updateAndRedraw,
// target,
// possibleValues
// });
// }
} else {
// *** Quantitative or date
addRange({ attr, filters, data, updateAndRedraw, target });
}
}

redraw();

return target;
}
Insert cell
function addSearchCheckboxes({ attr, filters, data, updateAndRedraw, target }) {
const groupCounts = d3.group(data, (d) => d[attr]),
possibleValues = Array.from(groupCounts.keys());
const ele = conditionalShow(
searchCheckbox(possibleValues, {
value: possibleValues,
label: attr,
height: 200,
format: attr => `${attr} (${groupCounts.get(attr).length})`
}),
{ label: attr }
);

ele.addEventListener("input", (evt) => {
evt.stopPropagation();
// Update the filter
const filter = filters.get(attr);
filter.selected = ele.value;
filters.set(attr, filter);

updateAndRedraw();
});

filters.set(attr, {
attr,
ele,
type: "categorical",
selected: possibleValues,
allOptions: possibleValues
});
}
Insert cell
function addMultiAutoSelect({
attr,
filters,
data,
updateAndRedraw,
target
}) {
const possibleValues = Array.from(d3.group(data, (d) => d[attr]).keys());
const ele = conditionalShow(
multiAutoSelect( {options: possibleValues, label: attr }),
{ label: attr, value: possibleValues }
);
ele.addEventListener("input", (evt) => {
evt.stopPropagation();
// Update the filter
const filter = filters.get(attr);
filter.selected = ele.value;
filters.set(attr, filter);

updateAndRedraw();
});

filters.set(attr, {
attr,
ele,
type: "categorical",
selected: possibleValues,
allOptions: possibleValues
});
}
Insert cell
Number("1.2") === 1.2
Insert cell
d3.format(",d")(31234234)
Insert cell
function addRange({
attr,
filters,
data,
updateAndRedraw,
target,
format,
step
} = {}) {
if (data[0][attr] instanceof Date) {
const fmt = (d) => new Date(d).toLocaleString();
format = format || (([s, e]) => html`${fmt(s)} ...<br> ${fmt(e)}`);
step = 1;
} else if (+data[0][attr] % 1 === 0) {
// Integer
const fmt = d3.format(",d");
format = format || (([s, e]) => `${fmt(s)} ... ${fmt(e)}`);
step = 1;
} else {
// float
const fmt = d3.format(",.2f");
format = format || (([s, e]) => `${fmt(s)} ... ${fmt(e)}`);
step = 0.01;
}
const extent = d3.extent(data, (d) => +d[attr]);

const ele = conditionalShow(
interval(extent, {
label: attr,
format,
step
}),
{
label: attr
}
);
ele.addEventListener("input", (evt) => {
evt.stopPropagation();

// Update the filter
const filter = filters.get(attr);
filter.selected = ele.value;
filters.set(attr, filter);

updateAndRedraw();
});

filters.set(attr, {
attr,
ele,
type: "range",
selected: extent,
allOptions: extent
});
}
Insert cell
// A simple JS function to apply the filters on the data
async function filterDataJS(data, filters) {
let res = data;

for (let filter of filters.values()) {
if (filter.type === "categorical") {
res = res.filter((d) => filter.selected.includes(d[filter.attr]));
} else if (filter.type === "range") {
res = res.filter(
(d) =>
d[filter.attr] >= filter.selected[0] &&
d[filter.attr] <= filter.selected[1]
);
}
}

return res;
}
Insert cell
interval()
Insert cell
debug = false
Insert cell
import {searchCheckbox} from "@john-guerra/search-checkbox"
Insert cell
import {multiAutoSelect} from "@john-guerra/multi-auto-select"
Insert cell
import {conditionalShow} from "@john-guerra/conditional-show"
Insert cell
import {interval} from '@mootari/range-slider'
Insert cell
import { dataInput} from "@john-guerra/data-input"
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