Zoomable Binned Plot Example
An interactive scatter plot with binned aggregation supporting pan and zoom. Click and drag to pan, use the scroll wheel to zoom. This example demonstrates how binning works and, when you disable stable bins, how changes to the bin alignment can affect the perceived density.
Vega JSON Specification <>
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "An interactive scatter plot example with binned aggregation supporting pan and zoom.",
"width": 500,
"height": 300,
"padding": {"top": 10, "left": 40, "bottom": 20, "right": 10},
"autosize": "none",
"config": {
"axis": {
"domain": false,
"tickSize": 3,
"tickColor": "#888",
"labelFont": "Monaco, Courier New"
}
},
"signals": [
{"name": "margin", "value": 20},
{"name": "stable", "value": true, "bind": {"input": "checkbox"}},
{
"name": "hover",
"on": [
{"events": "*:pointerover", "encode": "hover"},
{"events": "*:pointerout", "encode": "leave"},
{"events": "*:pointerdown", "encode": "select"},
{"events": "*:pointerup", "encode": "release"}
]
},
{"name": "xoffset", "update": "-(height + padding.bottom)"},
{"name": "yoffset", "update": "-(width + padding.left)"},
{"name": "xrange", "update": "[0, width]"},
{"name": "yrange", "update": "[height, 0]"},
{
"name": "down",
"value": null,
"on": [
{"events": "touchend", "update": "null"},
{"events": "pointerdown, touchstart", "update": "xy()"}
]
},
{
"name": "xcur",
"value": null,
"on": [
{"events": "pointerdown, touchstart, touchend", "update": "slice(xdom)"}
]
},
{
"name": "ycur",
"value": null,
"on": [
{"events": "pointerdown, touchstart, touchend", "update": "slice(ydom)"}
]
},
{
"name": "delta",
"value": [0, 0],
"on": [
{
"events": [
{
"source": "window",
"type": "pointermove",
"consume": true,
"between": [
{"type": "pointerdown"},
{"source": "window", "type": "pointerup"}
]
},
{
"type": "touchmove",
"consume": true,
"filter": "event.touches.length === 1"
}
],
"update": "down ? [down[0]-x(), y()-down[1]] : [0,0]"
}
]
},
{
"name": "anchor",
"value": [0, 0],
"on": [
{
"events": "wheel",
"update": "[invert('xscale', x()), invert('yscale', y())]"
},
{
"events": {
"type": "touchstart",
"filter": "event.touches.length===2"
},
"update": "[(xdom[0] + xdom[1]) / 2, (ydom[0] + ydom[1]) / 2]"
}
]
},
{
"name": "zoom",
"value": 1,
"on": [
{
"events": "wheel!",
"force": true,
"update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
},
{
"events": {"signal": "dist2"},
"force": true,
"update": "dist1 / dist2"
}
]
},
{
"name": "dist1",
"value": 0,
"on": [
{
"events": {
"type": "touchstart",
"filter": "event.touches.length===2"
},
"update": "pinchDistance(event)"
},
{"events": {"signal": "dist2"}, "update": "dist2"}
]
},
{
"name": "dist2",
"value": 0,
"on": [
{
"events": {
"type": "touchmove",
"consume": true,
"filter": "event.touches.length===2"
},
"update": "pinchDistance(event)"
}
]
},
{
"name": "xdom",
"update": "slice(xext)",
"on": [
{
"events": {"signal": "delta"},
"update": "[xcur[0] + span(xcur) * delta[0] / width, xcur[1] + span(xcur) * delta[0] / width]"
},
{
"events": {"signal": "zoom"},
"update": "[anchor[0] + (xdom[0] - anchor[0]) * zoom, anchor[0] + (xdom[1] - anchor[0]) * zoom]"
}
]
},
{
"name": "ydom",
"update": "slice(yext)",
"on": [
{
"events": {"signal": "delta"},
"update": "[ycur[0] + span(ycur) * delta[1] / height, ycur[1] + span(ycur) * delta[1] / height]"
},
{
"events": {"signal": "zoom"},
"update": "[anchor[1] + (ydom[0] - anchor[1]) * zoom, anchor[1] + (ydom[1] - anchor[1]) * zoom]"
}
]
},
{"name": "size", "update": "clamp(20 / span(xdom), 1, 1000)"}
],
"data": [
{
"name": "points",
"url": "data/normal-2d.json",
"transform": [
{"type": "extent", "field": "u", "signal": "xext"},
{"type": "extent", "field": "v", "signal": "yext"}
]
},
{
"name": "density",
"source": "points",
"transform": [
{"type": "extent", "field": "u", "signal": "xextf"},
{"type": "extent", "field": "v", "signal": "yextf"},
{
"type": "bin",
"field": "u",
"extent": {"signal": "stable ? xextf : xdom"},
"as": ["ustart", "uend"],
"maxbins": 12,
"nice": {"signal": "stable"}
},
{
"type": "bin",
"field": "v",
"extent": {"signal": "stable ? yextf : ydom"},
"as": ["vstart", "vend"],
"maxbins": 12,
"nice": {"signal": "stable"}
},
{
"type": "aggregate",
"groupby": ["ustart", "uend", "vstart", "vend"],
"as": ["count"]
}
]
}
],
"scales": [
{
"name": "xscale",
"zero": false,
"domain": {"signal": "xdom"},
"range": {"signal": "xrange"}
},
{
"name": "yscale",
"zero": false,
"domain": {"signal": "ydom"},
"range": {"signal": "yrange"}
},
{
"name": "color",
"domain": {"field": "count", "data": "density"},
"type": "linear",
"range": "heatmap",
"interpolate": "hcl",
"zero": false
}
],
"axes": [
{"scale": "xscale", "orient": "top", "offset": {"signal": "xoffset"}},
{"scale": "yscale", "orient": "right", "offset": {"signal": "yoffset"}}
],
"marks": [
{
"type": "rect",
"from": {"data": "density"},
"clip": true,
"encode": {
"enter": {
"fill": {"scale": "color", "field": "count"}
},
"update": {
"x": {"scale": "xscale", "field": "ustart"},
"x2": {"scale": "xscale", "field": "uend"},
"y": {"scale": "yscale", "field": "vstart"},
"y2": {"scale": "yscale", "field": "vend"},
"size": {"signal": "size"}
}
}
},
{
"type": "symbol",
"from": {"data": "points"},
"clip": true,
"encode": {
"enter": {
"fillOpacity": {"value": 0.6},
"fill": {"value": "black"}
},
"update": {
"x": {"scale": "xscale", "field": "u"},
"y": {"scale": "yscale", "field": "v"},
"size": {"signal": "size"}
}
}
}
]
}