jsonvisualizationvega-litevega

Dual-axis chart with nearest point highlighting in Vega


I've created a dual-axis chart with VegaLite following this example:

{
  "data": {
    "values": [
      {"date": "2021-01-22", "var": "A", "value": 2.4},
      // ...
      {"date": "2022-07-22", "var": "A", "value": -0.45},
      {"date": "2021-01-22", "var": "B", "value": 15.971434953055383},
      // ...
      {"date": "2022-07-22", "var": "B", "value": 19.00449486480307}
    ]
  },
  "encoding": {
    "x": {"type": "temporal", "field": "date", "title": null},
    "y": {
      "type": "quantitative",
      "field": "value",
      "scale": {"nice": false, "zero": false}
    },
    "color": { "field": "var" }
  },
  "layer": [
    {
      "name": "leftAxis",
      "layer": [
        { "mark": {"type": "line", "point": false } }
      ],
      "transform": [ { "filter": "datum.var == 'A'" } ],
      "encoding": {
        "y": {
          "type": "quantitative",
          "field": "value",
          "scale": {"nice": false, "zero": false},
          "axis": { "title": "A", "tickCount": 5 }
        }
      }
    },
    {
      "name": "rightAxis",
      "layer": [
        {
          "mark": {"type": "line", "point": false }
        }
      ],
      "transform": [ { "filter": "datum.var == 'B'" } ],
      "encoding": {
        "y": {
          "type": "quantitative",
          "field": "value",
          "scale": {"nice": false, "zero": false},
          "axis": { "title": "B", "tickCount": 5 }
        }
      }
    }
  ],
  "resolve": {
    "scale": {
      "y": "independent"
    }
  }
}

(full playground) This renders roughly as I'd expect:

Dual-axis chart

Now I'd like to add a hover interaction to display a circle and tooltip for the closest point, which could be on either series (on either axis).

I can do this by adding a circle mark on left axis (full playground):

{
  "mark": {
    "type": "circle",
    "tooltip": true
  },
  "params": [
    {
      "name": "hover",
      "select": {
        "type": "point",
        "on": "pointerover",
        "clear": "pointerout",
        "nearest": true
      }
    }
  ],
  "encoding": {
    "opacity": {
        "value": 0,
        "condition": {"test": {"param": "hover", "empty": false}}
    },
    "size": { "value": 100 }
  }
}

Of course, this only selects points on series A, even if the mouse is much closer to a point on series B:

Tooltip on A series only

If I define an identical "hover" param on the right axis (playground), I get the following error:

Duplicate signal name: "hover_tuple"

If I give it a different name ("hover2"), then I'm only able to highlight points on the right axis (playground).

If I move that param to the top level (playground), I also get a duplicate signal error, which surprises me since the signal only seems to be defined in one place.

Is there any way to combine a highlight effect with dual axes in VegaLite?


Solution

  • enter image description here

    {
      "$schema": "https://vega.github.io/schema/vega/v6.json",
      "description": "A basic line chart example.",
      "width": 800,
      "height": 400,
      "padding": 5,
      "signals": [
        {
          "name": "hover",
          "value": null,
          "on": [
            {"events": "@cell:pointerover", "update": "datum.id"},
            {"events": "@cell:pointerout", "update": "null"}
          ]
        }
      ],
      "data": [
        {
          "name": "table",
          "values": [
            {"date": "2021-01-22", "var": "A", "value": 2.4},
            {"date": "2021-02-26", "var": "A", "value": 2.15},
            {"date": "2021-04-02", "var": "A", "value": 2.65},
            {"date": "2021-05-07", "var": "A", "value": 4.94},
            {"date": "2021-06-11", "var": "A", "value": 7.03},
            {"date": "2021-07-16", "var": "A", "value": 2.62},
            {"date": "2021-08-20", "var": "A", "value": 2.32},
            {"date": "2021-10-22", "var": "A", "value": 1.39},
            {"date": "2021-12-03", "var": "A", "value": 1.27},
            {"date": "2022-01-28", "var": "A", "value": 0.83},
            {"date": "2022-03-11", "var": "A", "value": 0.61},
            {"date": "2022-03-18", "var": "A", "value": 0.54},
            {"date": "2022-03-25", "var": "A", "value": 0.5},
            {"date": "2022-05-13", "var": "A", "value": -0.03},
            {"date": "2022-05-20", "var": "A", "value": -0.08},
            {"date": "2022-07-22", "var": "A", "value": -0.45},
            {"date": "2021-01-22", "var": "B", "value": 15.971434953055383},
            {"date": "2021-02-26", "var": "B", "value": 13.966284390867731},
            {"date": "2021-04-02", "var": "B", "value": 16.358341860127812},
            {"date": "2021-05-07", "var": "B", "value": 11.754610521151442},
            {"date": "2021-06-11", "var": "B", "value": 13.05470556984986},
            {"date": "2021-07-16", "var": "B", "value": 14.570909286399319},
            {"date": "2021-08-20", "var": "B", "value": 12.504173067976124},
            {"date": "2021-10-22", "var": "B", "value": 11.385928956770012},
            {"date": "2021-12-03", "var": "B", "value": 15.50068082291583},
            {"date": "2022-01-28", "var": "B", "value": 12.68456514521299},
            {"date": "2022-03-11", "var": "B", "value": 12.81006294043777},
            {"date": "2022-03-18", "var": "B", "value": 17.39343219509385},
            {"date": "2022-03-25", "var": "B", "value": 12.388043904430008},
            {"date": "2022-05-13", "var": "B", "value": 14.424112206944333},
            {"date": "2022-05-20", "var": "B", "value": 18.653838013972276},
            {"date": "2022-07-22", "var": "B", "value": 19.00449486480307}
          ],
          "transform": [
            {"type": "formula", "expr": "toDate(datum[\"date\"])", "as": "date"}
          ]
        },
        {
          "name": "data_1",
          "source": "table",
          "transform": [{"type": "filter", "expr": "datum.var == 'A'"}]
        },
        {
          "name": "data_2",
          "source": "table",
          "transform": [{"type": "filter", "expr": "datum.var == 'B'"}]
        },
        {
          "name": "data_3",
          "source": "table",
          "transform": [
            {
              "type": "formula",
              "expr": "datum.var=='A'?scale('y1', datum.value):scale('y2', datum.value)",
              "as": "y"
            },
            {"type": "formula", "expr": "scale('x', datum.date)", "as": "x"},
            {"type": "window", "ops": ["row_number"], "as": ["id"]},
            {
              "type": "voronoi",
              "x": "x",
              "y": "y",
              "size": [{"signal": "width"}, {"signal": "height"}]
            }
          ]
        }
      ],
      "scales": [
        {
          "name": "x",
          "type": "time",
          "domain": {"fields": [{"data": "table", "field": "date"}]},
          "range": [0, {"signal": "width"}]
        },
        {
          "name": "y1",
          "type": "linear",
          "range": "height",
          "nice": true,
          "domain": {"data": "data_1", "field": "value"}
        },
        {
          "name": "y2",
          "type": "linear",
          "range": "height",
          "nice": true,
          "zero": false,
          "domain": {"data": "data_2", "field": "value"}
        }
      ],
      "axes": [
        {"orient": "bottom", "scale": "x"},
        {"orient": "left", "scale": "y1"},
        {"orient": "right", "scale": "y2"}
      ],
      "marks": [
        {
          "type": "line",
          "from": {"data": "data_1"},
          "encode": {
            "enter": {
              "x": {"scale": "x", "field": "date"},
              "y": {"scale": "y1", "field": "value"},
              "stroke": {"value": "blue"},
              "strokeWidth": {"value": 2}
            },
            "update": {
              "interpolate": {"signal": "'linear'"},
              "strokeOpacity": {"value": 1}
            }
          }
        },
        {
          "type": "line",
          "from": {"data": "data_2"},
          "encode": {
            "enter": {
              "x": {"scale": "x", "field": "date"},
              "y": {"scale": "y2", "field": "value"},
              "stroke": {"value": "orange"},
              "strokeWidth": {"value": 2}
            },
            "update": {
              "interpolate": {"signal": "'linear'"},
              "strokeOpacity": {"value": 1}
            }
          }
        },
        {
          "name": "cell",
          "type": "path",
          "from": {"data": "data_3"},
          "encode": {
            "enter": {
              "stroke": {"value": "transparent"},
              "path": {"field": "path"}
            },
            "update": {"fill": {"signal": "'transparent'"}}
          }
        },
        {
          "type": "symbol",
          "from": {"data": "cell"},
          "encode": {
            "update": {
              "x": {"signal": "datum.datum.x"},
              "y": {"signal": "datum.datum.y"},
              "fill": {"value": "red"},
              "size": {"signal": "100"},
              "opacity": {"signal": "datum.datum.id==hover?1:0"}
            }
          }
        }
      ]
    }