Search code examples
vega-liteclip

Can you clip marks to a region based on other marks in Vega-Lite?


I would like to create a table with circle marks representing the table values, that are clipped to the underlying cell region. Is it possible to implement this behavior in Vega-Lite?

The closest behavior I have found is the "clip" property, which is able to clip the mark to the group, but it is not clear how to specify the group of an individual mark as the underlying table cell.

I have created an example specification (link), based on the Heatmap with Labels example. Here is the code:

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
  "data": {"url": "data/cars.json"},
  "transform": [
    {
      "aggregate": [{"op": "count", "as": "num_cars"}],
      "groupby": ["Origin", "Cylinders"]
    }
  ],
  "encoding": {
    "y": {"field": "Origin", "type": "ordinal"},
    "x": {"field": "Cylinders", "type": "ordinal"}
  },
  "layer": [
    {
      "width": {"step": 120},
      "mark": {"type": "rect", "fill": "white", "stroke": "gray"}
    },
    {
      "mark": {"type": "circle", "clip": true},
      "encoding": {
        "size": {
          "field": "num_cars",
          "type": "quantitative",
          "title": "Count of Records",
          "scale": {"rangeMax": 4000},
          "legend": {"direction": "horizontal"}
        }
      }
    },
    {
      "mark": {"type": "text", "align": "right", "dx": 55},
      "encoding": {
        "text": {"field": "num_cars", "type": "quantitative"}
      }
    }
  ],
  "config": {
    "axis": {"grid": false, "tickBand": "extent"}
  }
}

Table created with Vega-Lite with circle marks representing the cell values.

While the "clip" property is able to truncate the circles as desired around the edge of the visualization region, it is not clear how to group the marks in order to clip based on the cell region.


Solution

  • It is possible to get the desired result using Vega (instead of Vega-Lite) by using a "group" mark. Here is a link to the Vega specification in the online editor. However, it would still be nice to know if this is possible directly in Vega-Lite.

    enter image description here

    The key approach for Vega is to create a group mark the specifies the cell region, with nested marks for the symbols and text, as shown below. To divide the data amongst the cells, you will need to specify the data for the group using a facet, and then draw the data for the text and symbol from that facet (as illustrated).

    {
      "name": "cellGroup",
      "type": "group",
      "from": {"facet": {"name": "cells", "data": "source_0", "groupby": ["Cylinders", "Origin", "num_cars"]}},
      "encode": {
        "update": {
          "stroke": {"value": "lightgray"},
          "x": {"scale": "x", "field": "Cylinders"},
          "width": {"signal": "max(0.25, bandwidth('x'))"},
          "y": {"scale": "y", "field": "Origin"},
          "height": {"signal": "max(0.25, bandwidth('y'))"},
          "tooltip": {"field": "num_cars"}
        }
      },
      "marks": [
        {
          "name": "layer_1_marks",
          "type": "symbol",
          "from": {"data": "cells"},
          "clip": true,
          "style": ["circle"],
          "encode": {
            "update": {
              "opacity": {"value": 0.7},
              "fill": {"value": "#4c78a8"},
              "ariaRoleDescription": {"value": "circle"},
              "description": {
                "signal": "\"Cylinders: \" + (isValid(datum[\"Cylinders\"]) ? datum[\"Cylinders\"] : \"\"+datum[\"Cylinders\"]) + \"; Origin: \" + (isValid(datum[\"Origin\"]) ? datum[\"Origin\"] : \"\"+datum[\"Origin\"]) + \"; Count of Records: \" + (format(datum[\"num_cars\"], \"\"))"
              },
              "x": {"value": 60},
              "y": {"value": 12},
              "size": {"scale": "size", "field": "num_cars"},
              "shape": {"value": "circle"},
              "tooltip": {"signal": "datum"}
            }
          }
        },
        ...
      ]
    }