async function vegaCrossfilter(data, attribs, config = {}) {
let {
colorBase = "#ddd",
colorSelected = "steelblue",
columns = 4,
facetWidth = 100,
facetHeight = 100,
binStep = 10,
width = 900
} = 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 removeSpecialChars = (a) => a.replace(/[^a-zA-Z0-9_]/g, "");
let dicAttribsCleaned = new Map(
attribs.map((a) => {
const cleanA = removeSpecialChars(a);
return [[a, cleanA],[cleanA, a]];
}).flat()
);
console.log("attribs", attribs, data[0], dicAttribsCleaned);
const filters = attribs.map((attrib) => {
const cleanAttrib = dicAttribsCleaned.get(attrib);
return typeof data[0][attrib] === "number" ||
(data[0][attrib] && typeof data[0][attrib].getMonth === "function")
? vl.selectInterval(cleanAttrib).encodings("y") // Numerical/Dates get an interval
: vl.selectMulti(cleanAttrib).encodings("y"); // Categorical data
});
const crossfilter = vl.and(...filters);
const histograms = attribs.map((attrib, i) =>
makeHist(
vl
.markBar()
.encode(
typeof data[0][attrib] === "number"
? vl.y().fieldQ(attrib).bin(true)
: data[0][attrib] && typeof data[0][attrib].getMonth === "function"
? vl.y().fieldT(attrib)
: vl.y().fieldO(attrib),
vl.x().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 = (nameCleaned, value) => {
const name = dicAttribsCleaned.get(nameCleaned);
console.log("signal", nameCleaned, name, value);
if (Object.keys(value).length === 0) {
//empty object
delete attribsSelected[name];
} else {
attribsSelected[name] = value;
}
chart.value = filterData();
console.log("attribs selected", attribsSelected);
chart.dispatchEvent(new CustomEvent("input"));
};
attribs.map((attrib) => {
view.addSignalListener(dicAttribsCleaned.get(attrib), signaled);
});
invalidation.then(() =>
attribs.map((attrib) =>
view.removeSignalListener(dicAttribsCleaned.get(attrib), signaled)
)
);
chart.value = data;
return chart;
}