Published
Edited
Apr 23, 2021
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Table(selected.filter)
Insert cell
Insert cell
Table(selected.data)
Insert cell
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]);
// console.log(attribs, 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") // Numerical/Dates get an interval
: 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;
}
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"
Insert cell
import { Table } from "@observablehq/inputs"
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