Brushing Scatter Plots Example

Brushing and linking is an interaction technique that highlights points based on linked selections across multiple views. Brushing and linking can be particularly useful for exploring relationships in multi-dimensional data. This example uses an interactive scatter plot matrix of penguin measurements, and echoes the original Brushing Scatterplots paper by Becker & Cleveland. Click and drag on the visualization to initiate a linked selection.

Vega JSON Specification <>

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "A scatter plot matrix of penguin data with interactive linked selections.",
  "padding": 10,
  "config": {
    "axis": {
      "tickColor": "#ccc"
    }
  },

  "signals": [
    { "name": "chartSize", "value": 120 },
    { "name": "chartPad", "value": 20 },
    { "name": "chartStep", "update": "chartSize + chartPad" },
    { "name": "width", "update": "chartStep * 4" },
    { "name": "height", "update": "chartStep * 4" },
    {
      "name": "cell", "value": null,
      "on": [
        {
          "events": "@cell:pointerdown", "update": "group()"
        },
        {
          "events": "@cell:pointerup",
          "update": "!span(brushX) && !span(brushY) ? null : cell"
        }
      ]
    },
    {
      "name": "brushX", "value": 0,
      "on": [
        {
          "events": "@cell:pointerdown",
          "update": "[x(cell), x(cell)]"
        },
        {
          "events": "[@cell:pointerdown, window:pointerup] > window:pointermove",
          "update": "[brushX[0], clamp(x(cell), 0, chartSize)]"
        },
        {
          "events": {"signal": "delta"},
          "update": "clampRange([anchorX[0] + delta[0], anchorX[1] + delta[0]], 0, chartSize)"
        }
      ]
    },
    {
      "name": "brushY", "value": 0,
      "on": [
        {
          "events": "@cell:pointerdown",
          "update": "[y(cell), y(cell)]"
        },
        {
          "events": "[@cell:pointerdown, window:pointerup] > window:pointermove",
          "update": "[brushY[0], clamp(y(cell), 0, chartSize)]"
        },
        {
          "events": {"signal": "delta"},
          "update": "clampRange([anchorY[0] + delta[1], anchorY[1] + delta[1]], 0, chartSize)"
        }
      ]
    },
    {
      "name": "down", "value": [0, 0],
      "on": [{"events": "@brush:pointerdown", "update": "[x(cell), y(cell)]"}]
    },
    {
      "name": "anchorX", "value": null,
      "on": [{"events": "@brush:pointerdown", "update": "slice(brushX)"}]
    },
    {
      "name": "anchorY", "value": null,
      "on": [{"events": "@brush:pointerdown", "update": "slice(brushY)"}]
    },
    {
      "name": "delta", "value": [0, 0],
      "on": [
        {
          "events": "[@brush:pointerdown, window:pointerup] > window:pointermove",
          "update": "[x(cell) - down[0], y(cell) - down[1]]"
        }
      ]
    },
    {
      "name": "rangeX", "value": [0, 0],
      "on": [
        {
          "events": {"signal": "brushX"},
          "update": "invert(cell.datum.x.data + 'X', brushX)"
        }
      ]
    },
    {
      "name": "rangeY", "value": [0, 0],
      "on": [
        {
          "events": {"signal": "brushY"},
          "update": "invert(cell.datum.y.data + 'Y', brushY)"
        }
      ]
    },
    {
      "name": "cursor", "value": "'default'",
      "on": [
        {
          "events": "[@cell:pointerdown, window:pointerup] > window:pointermove!",
          "update": "'nwse-resize'"
        },
        {
          "events": "@brush:pointermove, [@brush:pointerdown, window:pointerup] > window:pointermove!",
          "update": "'move'"
        },
        {
          "events": "@brush:pointerout, window:pointerup",
          "update": "'default'"
        }
      ]
    }
  ],

  "data": [
    {
      "name": "penguins",
      "url": "data/penguins.json",
      "transform": [
        {"type": "filter", "expr": "datum['Beak Length (mm)'] != null"}
      ]
    },
    {
      "name": "fields",
      "values": [
        "Beak Length (mm)",
        "Beak Depth (mm)",
        "Flipper Length (mm)",
        "Body Mass (g)"
      ]
    },
    {
      "name": "cross",
      "source": "fields",
      "transform": [
        { "type": "cross", "as": ["x", "y"] },
        { "type": "formula", "as": "xscale", "expr": "datum.x.data + 'X'" },
        { "type": "formula", "as": "yscale", "expr": "datum.y.data + 'Y'" }
      ]
    }
  ],

  "scales": [
    {
      "name": "groupx",
      "type": "band",
      "range": "width",
      "domain": {"data": "fields", "field": "data"}
    },
    {
      "name": "groupy",
      "type": "band",
      "range": [{"signal": "height"}, 0],
      "domain": {"data": "fields", "field": "data"}
    },
    {
      "name": "color",
      "type": "ordinal",
      "domain": {"data": "penguins", "field": "Species"},
      "range": "category"
    },

    {
      "name": "Beak Length (mm)X", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Beak Length (mm)"},
      "range": [0, {"signal": "chartSize"}]
    },
    {
      "name": "Beak Depth (mm)X", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Beak Depth (mm)"},
      "range": [0, {"signal": "chartSize"}]
    },
    {
      "name": "Flipper Length (mm)X", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Flipper Length (mm)"},
      "range": [0, {"signal": "chartSize"}]
    },
    {
      "name": "Body Mass (g)X", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Body Mass (g)"},
      "range": [0, {"signal": "chartSize"}]
    },

    {
      "name": "Beak Length (mm)Y", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Beak Length (mm)"},
      "range": [{"signal": "chartSize"}, 0]
    },
    {
      "name": "Beak Depth (mm)Y", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Beak Depth (mm)"},
      "range": [{"signal": "chartSize"}, 0]
    },
    {
      "name": "Flipper Length (mm)Y", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Flipper Length (mm)"},
      "range": [{"signal": "chartSize"}, 0]
    },
    {
      "name": "Body Mass (g)Y", "zero": false, "nice": true,
      "domain": {"data": "penguins", "field": "Body Mass (g)"},
      "range": [{"signal": "chartSize"}, 0]
    }
  ],

  "axes": [
    {
      "orient": "left", "scale": "Beak Length (mm)Y", "minExtent": 25,
      "title": "Beak Length (mm)", "tickCount": 5, "domain": false,
      "position": {"signal": "3 * chartStep"}
    },
    {
      "orient": "left", "scale": "Beak Depth (mm)Y", "minExtent": 25,
      "title": "Beak Depth (mm)", "tickCount": 5, "domain": false,
      "position": {"signal": "2 * chartStep"}
    },
    {
      "orient": "left", "scale": "Flipper Length (mm)Y", "minExtent": 25,
      "title": "Flipper Length (mm)", "tickCount": 5, "domain": false,
      "position": {"signal": "1 * chartStep"}
    },
    {
      "orient": "left", "scale": "Body Mass (g)Y", "minExtent": 25,
      "title": "Body Mass (g)", "tickCount": 5, "domain": false
    },
    {
      "orient": "bottom", "scale": "Beak Length (mm)X",
      "title": "Beak Length (mm)", "tickCount": 5, "domain": false,
      "offset": {"signal": "-chartPad"}
    },
    {
      "orient": "bottom", "scale": "Beak Depth (mm)X",
      "title": "Beak Depth (mm)", "tickCount": 5, "domain": false,
      "offset": {"signal": "-chartPad"}, "position": {"signal": "1 * chartStep"}
    },
    {
      "orient": "bottom", "scale": "Flipper Length (mm)X",
      "title": "Flipper Length (mm)", "tickCount": 5, "domain": false,
      "offset": {"signal": "-chartPad"}, "position": {"signal": "2 * chartStep"}
    },
    {
      "orient": "bottom", "scale": "Body Mass (g)X",
      "title": "Body Mass (g)", "tickCount": 5, "domain": false,
      "offset": {"signal": "-chartPad"}, "position": {"signal": "3 * chartStep"}
    }
  ],

  "legends": [
    {
      "fill": "color",
      "title": "Species",
      "offset": 0,
      "encode": {
        "symbols": {
          "update": {
            "fillOpacity": {"value": 0.5},
            "stroke": {"value": "transparent"}
          }
        }
      }
    }
  ],

  "marks": [
    {
      "type": "rect",
      "encode": {
        "enter": {
          "fill": {"value": "#eee"}
        },
        "update": {
          "opacity": {"signal": "cell ? 1 : 0"},
          "x": {"signal": "cell ? cell.x + brushX[0] : 0"},
          "x2": {"signal": "cell ? cell.x + brushX[1] : 0"},
          "y": {"signal": "cell ? cell.y + brushY[0] : 0"},
          "y2": {"signal": "cell ? cell.y + brushY[1] : 0"}
        }
      }
    },
    {
      "name": "cell",
      "type": "group",
      "from": {"data": "cross"},

      "encode": {
        "enter": {
          "x": {"scale": "groupx", "field": "x.data"},
          "y": {"scale": "groupy", "field": "y.data"},
          "width": {"signal": "chartSize"},
          "height": {"signal": "chartSize"},
          "fill": {"value": "transparent"},
          "stroke": {"value": "#ddd"}
        }
      },

      "marks": [
        {
          "type": "symbol",
          "from": {"data": "penguins"},
          "interactive": false,
          "encode": {
            "enter": {
              "x": {
                "scale": {"parent": "xscale"},
                "field": {"datum": {"parent": "x.data"}}
              },
              "y": {
                "scale": {"parent": "yscale"},
                "field": {"datum": {"parent": "y.data"}}
              },
              "fillOpacity": {"value": 0.5},
              "size": {"value": 36}
            },
            "update": {
              "fill": [
                {
                  "test": "!cell || inrange(datum[cell.datum.x.data], rangeX) && inrange(datum[cell.datum.y.data], rangeY)",
                  "scale": "color", "field": "Species"
                },
                {"value": "#ddd"}
              ]
            }
          }
        }
      ]
    },
    {
      "type": "rect",
      "name": "brush",
      "encode": {
        "enter": {
          "fill": {"value": "transparent"}
        },
        "update": {
          "x": {"signal": "cell ? cell.x + brushX[0] : 0"},
          "x2": {"signal": "cell ? cell.x + brushX[1] : 0"},
          "y": {"signal": "cell ? cell.y + brushY[0] : 0"},
          "y2": {"signal": "cell ? cell.y + brushY[1] : 0"}
        }
      }
    }
  ]
}