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

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