async function vegaCrossfilter(data, attribs, config = {}) {
let {
colorBase = "#ddd",
colorSelected = "steelblue",
columns = 4,
facetWidth = 100,
facetHeight = 100,
binStep = 30
} = config;
if (!Array.isArray(data) || data.length === 0) {
throw "data is not an array with elements";
}
columns = config.columns || columns;
facetWidth = config.facetWidth || width / columns - 100;
facetHeight = config.facetHeight || facetWidth;
const makeHist = (base, filter, crossfilter) => {
return vl
.layer(
base.select(filter).encode(vl.color().value(colorBase)),
base
.encode(vl.color().value(colorSelected))
.transform(vl.filter(crossfilter))
)
.height(facetHeight)
.width(facetWidth);
};
attribs = attribs || Object.keys(data[0]);
const filters = attribs.map((attrib) => {
return typeof data[0][attrib] === "number" ||
(data[0][attrib] && typeof data[0][attrib].getMonth === "function")
? vl.selectInterval(attrib).encodings("x")
: vl.selectMulti(attrib).encodings("x"); // Categorical data
});
const crossfilter = vl.and.apply(null, filters);
const histograms = attribs.map((attrib, i) =>
makeHist(
vl
.markBar()
.encode(
typeof data[0][attrib] === "number"
? vl.x().fieldQ(attrib).bin({ maxbins: binStep })
: data[0][attrib] && typeof data[0][attrib].getMonth === "function"
? vl.x().fieldT(attrib)
: vl.x().fieldO(attrib),
vl.y().count()
),
filters[i],
crossfilter
)
);
const chart = await vl.concat
.apply(null, histograms)
.data(data)
.columns(columns)
.render();
// Save the vega view so we can use the value attribute
const view = chart.value;
const attribsSelected = {};
// Returns the filter data corresponding to the selected attributes
// Seems silly to have to filter the data manually again, given
// that vega is already filtering it, but I couldn't figure out
// how to get the data
const filterData = () =>
data.filter((d) => {
const filtered = Object.keys(attribsSelected).map((key) => {
if (
typeof data[0][key] === "number" ||
typeof data[0][key].getMonth === "function"
) {
return (
attribsSelected[key][key][0] <= d[key] &&
d[key] < attribsSelected[key][key][1]
);
} else {
return attribsSelected[key][key].indexOf(d[key]) !== -1;
}
});
return filtered.reduce((p, n) => p && n, true);
});
// Listen to events on the vega-lite view
const signaled = (name, value) => {
console.log("signal", name, value);
if (Object.keys(value).length === 0) {
//empty object
delete attribsSelected[name];
} else {
attribsSelected[name] = value;
}
// convert format of filter (attribsSelected) to iterable
const filterAttribs = Object.keys(attribsSelected).map((d) => ({
attribute: d,
filter: attribsSelected[d][d]
}));
chart.value = { filter: filterAttribs, data: filterData() };
// console.log("attribs selected", attribsSelected);
chart.dispatchEvent(new CustomEvent("input"));
};
attribs.map((attrib) => {
view.addSignalListener(attrib, signaled);
});
invalidation.then(() =>
attribs.map((attrib) => view.removeSignalListener(attrib, signaled))
);
chart.value = { filter: {}, data: data };
return chart;
}