Public
Edited
Nov 14, 2022
3 forks
Importers
18 stars
Insert cell
Insert cell
Insert cell
viewof selected = vegaCrossfilter(
data,
[
"Miles_per_Gallon",
"Acceleration",
"Origin",
"Horsepower",
"Weight_in_lbs",
"date"
],
{ columns: 3 }
)
Insert cell
Insert cell
Table(selected)
Insert cell
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;
}
Insert cell
vegaDatasets = require("vega-datasets@2.2")
Insert cell
data = vegaDatasets["cars.json"]().then(data =>
data.map(d => ((d.date = fmt(d.Year)), d))
)
Insert cell
fmt = d3.timeParse("%Y-%m-%d")
Insert cell
d3 = require("d3-time-format@2")
Insert cell
import {vl} from "@vega/vega-lite-api-v5"
Insert cell
import { Table } from "@observablehq/inputs"
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