Hello everyone, I'm struggling with a bug I found when implementing a custom visualization using Vega. I'm reposting this from Stackoverflow (Where the Vega folks suggest posting) because nobody is awnsering, so I'm trying my luck here.
I've been working on a line chart with some functionality that pushed me to switch from vega-lite to vega, more especificaly I needed to bind two signals (which, as far as I know is not possible yet in vega-lite) in order to reset the legend values when I change the group.
There's this specific scenario where if you have legend values selected, move around the chart and then change the group value, the legendSelection signal is not cleared, so the lines in the new group are not visible (Because the signal is still holding the previous group values)
Why would the legendSelection (and all other related signals) be affected by the xZoom and yZoom signals?
Steps to recreate:
- Select a single or multiple values in the legend
- Pan the chart around
- Change the
groupvalue

{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "Google's stock price over time.",
"background": "white",
"padding": 5,
"width": 200,
"height": 200,
"style": "cell",
"data": [
{"name": "xZoom_store"},
{"name": "yZoom_store"},
{"name": "legendSelection_store"},
{
"name": "source_0",
"values": [
{
"dimention1": 1,
"dimention2": "C",
"data": [
{"x": 1, "y": 1},
{"x": 2, "y": 1},
{"x": 3, "y": 1},
{"x": 4, "y": 1},
{"x": 5, "y": 1}
]
},
{
"dimention1": 2,
"dimention2": "B",
"data": [
{"x": 1, "y": 2},
{"x": 2, "y": 2},
{"x": 3, "y": 2},
{"x": 4, "y": 2},
{"x": 5, "y": 2}
]
},
{
"dimention1": 3,
"dimention2": "A",
"data": [
{"x": 1, "y": 3},
{"x": 2, "y": 3},
{"x": 3, "y": 3},
{"x": 4, "y": 3},
{"x": 5, "y": 3}
]
}
]
},
{
"name": "data_0",
"source": "source_0",
"transform": [
{"type": "flatten", "fields": ["data"], "as": ["data"]},
{
"type": "formula",
"expr": "datum[\"data\"] && datum[\"data\"][\"x\"]",
"as": "data.x"
},
{
"type": "formula",
"expr": "datum[\"data\"] && datum[\"data\"][\"y\"]",
"as": "data.y"
},
{
"type": "fold",
"fields": ["dimention1", "dimention2"],
"as": ["key", "value"]
},
{"type": "filter", "expr": "group == datum.key"}
]
}
],
"signals": [
{
"name": "unit",
"value": {},
"on": [
{"events": "mousemove", "update": "isTuple(group()) ? group() : unit"}
]
},
{
"name": "legendSelection_value_legend",
"value": null,
"on": [
{
"events": [
{
"source": "view",
"type": "click",
"markname": "value_legend_symbols"
},
{
"source": "view",
"type": "click",
"markname": "value_legend_labels"
},
{
"source": "view",
"type": "click",
"markname": "value_legend_entries"
}
],
"update": "isDefined(datum.value) ? datum.value : item().items[0].items[0].datum.value",
"force": true
},
{
"events": [{"source": "view", "type": "click"}],
"update": "!event.item || !datum ? null : legendSelection_value_legend",
"force": true
},
{
"events": [
{"signal": "group"},
{"source": "view", "type": "dblclick"}
],
"update": "null",
"force": true
}
]
},
{
"name": "xZoom",
"update": "vlSelectionResolve(\"xZoom_store\", \"union\")"
},
{
"name": "yZoom",
"update": "vlSelectionResolve(\"yZoom_store\", \"union\")"
},
{
"name": "legendSelection",
"update": "vlSelectionResolve(\"legendSelection_store\", \"union\", true, true)"
},
{
"name": "group",
"value": "dimention1",
"bind": {"input": "select", "options": ["dimention1", "dimention2"]}
},
{
"name": "xZoom_data_x",
"on": [
{"events": [{"source": "view", "type": "dblclick"}], "update": "null"},
{
"events": {"signal": "xZoom_translate_delta"},
"update": "panLinear(xZoom_translate_anchor.extent_x, -xZoom_translate_delta.x / width)"
},
{
"events": {"signal": "xZoom_zoom_delta"},
"update": "zoomLinear(domain(\"x\"), xZoom_zoom_anchor.x, xZoom_zoom_delta)"
}
]
},
{
"name": "xZoom_tuple",
"on": [
{
"events": [{"signal": "xZoom_data_x"}],
"update": "xZoom_data_x ? {unit: \"\", fields: xZoom_tuple_fields, values: [xZoom_data_x]} : null"
}
]
},
{
"name": "xZoom_tuple_fields",
"value": [{"field": "data\\.x", "channel": "x", "type": "R"}]
},
{
"name": "xZoom_translate_anchor",
"value": {},
"on": [
{
"events": [{"source": "scope", "type": "mousedown"}],
"update": "{x: x(unit), y: y(unit), extent_x: domain(\"x\")}"
}
]
},
{
"name": "xZoom_translate_delta",
"value": {},
"on": [
{
"events": [
{
"source": "window",
"type": "mousemove",
"consume": true,
"between": [
{"source": "scope", "type": "mousedown"},
{"source": "window", "type": "mouseup"}
]
}
],
"update": "{x: xZoom_translate_anchor.x - x(unit), y: xZoom_translate_anchor.y - y(unit)}"
}
]
},
{
"name": "xZoom_zoom_anchor",
"on": [
{
"events": [
{
"source": "scope",
"type": "wheel",
"consume": true,
"filter": ["event.shiftKey"]
}
],
"update": "{x: invert(\"x\", x(unit)), y: invert(\"y\", y(unit))}"
}
]
},
{
"name": "xZoom_zoom_delta",
"on": [
{
"events": [
{
"source": "scope",
"type": "wheel",
"consume": true,
"filter": ["event.shiftKey"]
}
],
"force": true,
"update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
}
]
},
{
"name": "xZoom_modify",
"on": [
{
"events": {"signal": "xZoom_tuple"},
"update": "modify(\"xZoom_store\", xZoom_tuple, true)"
}
]
},
{
"name": "yZoom_data_y",
"on": [
{"events": [{"source": "view", "type": "dblclick"}], "update": "null"},
{
"events": {"signal": "yZoom_translate_delta"},
"update": "panLinear(yZoom_translate_anchor.extent_y, yZoom_translate_delta.y / height)"
},
{
"events": {"signal": "yZoom_zoom_delta"},
"update": "zoomLinear(domain(\"y\"), yZoom_zoom_anchor.y, yZoom_zoom_delta)"
}
]
},
{
"name": "yZoom_tuple",
"on": [
{
"events": [{"signal": "yZoom_data_y"}],
"update": "yZoom_data_y ? {unit: \"\", fields: yZoom_tuple_fields, values: [yZoom_data_y]} : null"
}
]
},
{
"name": "yZoom_tuple_fields",
"value": [{"field": "data\\.y", "channel": "y", "type": "R"}]
},
{
"name": "yZoom_translate_anchor",
"value": {},
"on": [
{
"events": [{"source": "scope", "type": "mousedown"}],
"update": "{x: x(unit), y: y(unit), extent_y: domain(\"y\")}"
}
]
},
{
"name": "yZoom_translate_delta",
"value": {},
"on": [
{
"events": [
{
"source": "window",
"type": "mousemove",
"consume": true,
"between": [
{"source": "scope", "type": "mousedown"},
{"source": "window", "type": "mouseup"}
]
}
],
"update": "{x: yZoom_translate_anchor.x - x(unit), y: yZoom_translate_anchor.y - y(unit)}"
}
]
},
{
"name": "yZoom_zoom_anchor",
"on": [
{
"events": [
{
"source": "scope",
"type": "wheel",
"consume": true,
"filter": ["!event.shiftKey"]
}
],
"update": "{x: invert(\"x\", x(unit)), y: invert(\"y\", y(unit))}"
}
]
},
{
"name": "yZoom_zoom_delta",
"on": [
{
"events": [
{
"source": "scope",
"type": "wheel",
"consume": true,
"filter": ["!event.shiftKey"]
}
],
"force": true,
"update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
}
]
},
{
"name": "yZoom_modify",
"on": [
{
"events": {"signal": "yZoom_tuple"},
"update": "modify(\"yZoom_store\", yZoom_tuple, true)"
}
]
},
{
"name": "legendSelection_tuple",
"update": "legendSelection_value_legend !== null ? {fields: legendSelection_tuple_fields, values: [legendSelection_value_legend]} : null"
},
{
"name": "legendSelection_tuple_fields",
"value": [{"type": "E", "field": "value"}]
},
{
"name": "legendSelection_toggle",
"value": false,
"on": [
{
"events": {"merge": [{"source": "view", "type": "click"}]},
"update": "true"
},
{
"events": [
{"signal": "group"},
{"source": "view", "type": "dblclick"}
],
"update": "null",
"force": true
}
]
},
{
"name": "legendSelection_modify",
"on": [
{
"events": {"signal": "legendSelection_tuple"},
"update": "modify(\"legendSelection_store\", legendSelection_toggle ? null : legendSelection_tuple, legendSelection_toggle ? null : true, legendSelection_toggle ? legendSelection_tuple : null)"
}
]
}
],
"marks": [
{
"name": "pathgroup",
"type": "group",
"from": {
"facet": {
"name": "faceted_path_main",
"data": "data_0",
"groupby": ["value"]
}
},
"encode": {
"update": {
"width": {"field": {"group": "width"}},
"height": {"field": {"group": "height"}}
}
},
"marks": [
{
"name": "marks",
"type": "line",
"clip": true,
"style": ["line"],
"sort": {"field": "datum[\"data.x\"]"},
"interactive": true,
"from": {"data": "faceted_path_main"},
"encode": {
"update": {
"stroke": {"scale": "color", "field": "value"},
"opacity": [
{
"test": "!length(data(\"legendSelection_store\")) || vlSelectionTest(\"legendSelection_store\", datum)",
"value": 1
},
{"value": 0}
],
"description": {
"signal": "\"data.x: \" + (format(datum[\"data.x\"], \"\")) + \"; data.y: \" + (format(datum[\"data.y\"], \"\")) + \"; value: \" + (isValid(datum[\"value\"]) ? datum[\"value\"] : \"\"+datum[\"value\"])"
},
"x": {"scale": "x", "field": "data\\.x"},
"y": {"scale": "y", "field": "data\\.y"},
"defined": {
"signal": "isValid(datum[\"data.x\"]) && isFinite(+datum[\"data.x\"]) && isValid(datum[\"data.y\"]) && isFinite(+datum[\"data.y\"])"
}
}
}
}
]
}
],
"scales": [
{
"name": "x",
"type": "linear",
"domain": {"data": "data_0", "field": "data\\.x"},
"domainRaw": {"signal": "xZoom[\"data\\\\.x\"]"},
"range": [0, {"signal": "width"}],
"nice": true,
"zero": false
},
{
"name": "y",
"type": "linear",
"domain": {"data": "data_0", "field": "data\\.y"},
"domainRaw": {"signal": "yZoom[\"data\\\\.y\"]"},
"range": [{"signal": "height"}, 0],
"nice": true,
"zero": true
},
{
"name": "color",
"type": "ordinal",
"domain": {"data": "data_0", "field": "value", "sort": true},
"range": "category"
}
],
"axes": [
{
"scale": "x",
"orient": "bottom",
"gridScale": "y",
"grid": true,
"tickCount": {"signal": "ceil(width/40)"},
"domain": false,
"labels": false,
"aria": false,
"maxExtent": 0,
"minExtent": 0,
"ticks": false,
"zindex": 0
},
{
"scale": "y",
"orient": "left",
"gridScale": "x",
"grid": true,
"tickCount": {"signal": "ceil(height/40)"},
"domain": false,
"labels": false,
"aria": false,
"maxExtent": 0,
"minExtent": 0,
"ticks": false,
"zindex": 0
},
{
"scale": "x",
"orient": "bottom",
"grid": false,
"title": "data.x",
"labelFlush": true,
"labelOverlap": true,
"tickCount": {"signal": "ceil(width/40)"},
"zindex": 0
},
{
"scale": "y",
"orient": "left",
"grid": false,
"title": "data.y",
"labelOverlap": true,
"tickCount": {"signal": "ceil(height/40)"},
"zindex": 0
}
],
"legends": [
{
"stroke": "color",
"symbolType": "stroke",
"title": "value",
"encode": {
"labels": {
"name": "value_legend_labels",
"interactive": true,
"update": {
"opacity": [
{
"test": "(!length(data(\"legendSelection_store\")) || (legendSelection[\"value\"] && indexof(legendSelection[\"value\"], datum.value) >= 0))",
"value": 1
},
{"value": 0.35}
]
}
},
"symbols": {
"name": "value_legend_symbols",
"interactive": true,
"update": {
"opacity": [
{
"test": "(!length(data(\"legendSelection_store\")) || (legendSelection[\"value\"] && indexof(legendSelection[\"value\"], datum.value) >= 0))",
"value": 1
},
{"value": 0.35}
]
}
},
"entries": {
"name": "value_legend_entries",
"interactive": true,
"update": {"fill": {"value": "transparent"}}
}
}
}
]
}
Using the Record signal changes in the Online Vega Editor I can see that the legendSelection signal is cleared in every other scenario except this one.
The legendSelection signal is updated using the vlSelectionResolve function, which is not called in this particular case.
Is this a known bug? Am I missing something?
I would really appreciate your help