chart = {
const height = 400;
const hover = vl
.selectSingle()
.on("mouseover")
.nearest(true)
.empty("none");
const base = vl.markRule({ color: "#ccc" }).encode(
vl.x().fieldN("key"),
vl.detail().count()
);
const line = base.markLine().encode(
vl.color().fieldN("Species"),
vl.detail().fieldN("index"),
vl.opacity().if(hover, vl.value(1)).value(0.3),
vl.y().fieldQ("norm_val").axis(null),
vl.tooltip(attribs)
);
const points = line.markCircle()
.select(hover)
.encode(vl.size().if(hover, vl.value(50)).value(5));
const tick = y0 =>
vl.layer(
base.markText({ style: "label" }).encode(vl.text().max("max")),
base.markTick({ style: "tick", size: 8, color: "#ccc" })
)
.encode(vl.y().value(y0));
const ticks = Array.from({ length: numTicks })
.map((_, i) => tick((height / (numTicks - 1)) * i));
return vl
.layer(base, line, points, ...ticks)
.data(data)
.transform(
vl.filter(attribs.map(a => `datum["${a}"] != null`).join(" && ")),
vl.window(vl.count().as("index")),
vl.fold(attribs),
vl.groupby("key").joinaggregate(vl.min("value").as("min"), vl.max("value").as("max")),
vl.calculate("(datum.value - datum.min) / (datum.max - datum.min)").as("norm_val"),
vl.calculate("(datum.min + datum.max) / 2").as("mid")
)
.config({
axisX: { domain: false, labelAngle: 0, tickColor: "#ccc", title: null },
view: { stroke: null },
style: {
label: { baseline: "middle", align: "right", dx: -5 },
tick: { orient: "horizontal" }
}
})
.width(width - 100)
.height(height)
.render();
}