0

The linked chart contains only a legend and the legend works as follows:

  • clicking on a fruit name toggles it on and off
  • shift-clicking on a fruit name switches it ON and switches OFF all other fruit names

Legend display is controlled by two entities:

  • data set SELECTED remembers selected items
  • signal FILTERMODE toggles the type of the filter between include and exclude

Currently, if only one fruit name is ON, then a click on it switches it OFF (so all fruit names become OFF). I would like to modify this behavior so that a click on the last enabled fruit name would switch everything ON. (In other words - it would not be possible to deselect everything.)

In order to switch everything ON I only need to change the value of signal FILTERMODE to exclude. This is where I hit a snag. I have tried the following in the signal definition:

"update": "event.shiftKey? 'include' : (length(data('selected'))? filtermode : 'exclude')",

This does not work. I am fairly sure this happens because of a race condition. When I check for the length of data('source'), it is still non-empty.

So the sequence of events is the following:

  • click
  • update signal FILTERMODE (check if the data set SELECTED is empty - it is not)
  • update data set SELECTED (only now it has become empty)

What would be the most elegant work-around?

Legend with only one item selected

az5112
  • 590
  • 2
  • 11

2 Answers2

1

Are you checking the length of the correct array? It is hard to understand precisely what the desired behaviour is but if I add the code (depending on whether filter mode is include or exclude)

length(data('selected')) == 6

or

length(data('selected')) == 0

then it seems to work. Editor

Davide Bacci
  • 16,647
  • 3
  • 10
  • 36
  • Indeed - I made an error in the question - should say `length(data('selected'))` rather than `length(data('source'))`. It is now fixed. Other than the description was accurate. – az5112 May 16 '22 at 20:00
  • The steps to reproduce using the editor link in the question would be 1) update the definition of the signal `filtermode` (copy paste from the 1-line code listing) 2) shift+click to select melon 3) click melon - datasource `selected` is now empty so the signal `filtermode` should have flipped the value to `exclude`, but it did not. – az5112 May 16 '22 at 20:07
  • Your link fixes the display of the legend (by using additional conditions to determine the opacity) - but the root cause is delayed update of signal `filtermode` (which I would like to use for filtering data in the chart - I have taken that part out to simplify the example). Not sure if I am misunderstanding something or if it is a bug in Vega. – az5112 May 16 '22 at 20:11
1

Try this instead. It is the same as your code but also checks the length of the array which your single line doesn't currently do.

You can now shift click melon and then click it normally and the filter mode will switch.

Editor

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "A scatter plot example with interactive legend and x-axis.",
  "width": 200,
  "height": 200,
  "padding": 5,
  "autosize": "pad",
  "signals": [
    {
      "name": "shift",
      "value": false,
      "on": [
        {
          "events": "@legendSymbol:click, @legendLabel:click",
          "update": "event.shiftKey",
          "force": true
        }
      ]
    },
    {
      "name": "clicked",
      "value": null,
      "on": [
        {
          "events": "@legendSymbol:click, @legendLabel:click",
          "update": "{value: datum.value}",
          "force": true
        }
      ]
    },
    {
      "name": "filtermode",
      "value": "exclude",
      "on": [
        {
          "events": "@legendSymbol:click, @legendLabel:click",
          "update": "event.shiftKey? 'include' : (length(data('selected') == 0)? filtermode : 'exclude')",
          "force": true
        }
      ]
    }
  ],
  "data": [
    {
      "name": "source",
      "values": [
        {"fruit": "apple"},
        {"fruit": "plum"},
        {"fruit": "pear"},
        {"fruit": "melon"},
        {"fruit": "grape"},
        {"fruit": "strawberry"}
      ]
    },
    {
      "name": "selected",
      "on": [
        {"trigger": "clicked && (event.shiftKey)", "remove": true},
        {"trigger": "clicked && (event.shiftKey)", "insert": "clicked"},
        {"trigger": "clicked && (!event.shiftKey)", "toggle": "clicked"}
      ]
    }
  ],
  "scales": [
    {
      "name": "color",
      "type": "ordinal",
      "range": {"scheme": "category10"},
      "domain": {"data": "source", "field": "fruit"}
    }
  ],
  "legends": [
    {
      "stroke": "color",
      "title": "Fruit",
      "encode": {
        "symbols": {
          "name": "legendSymbol",
          "interactive": true,
          "update": {
            "fill": {"value": "transparent"},
            "strokeWidth": {"value": 2},
            "opacity": [
              {
                "test": "filtermode == 'exclude' && !indata('selected', 'value', datum.value)",
                "value": 1
              },
              {
                "test": "filtermode == 'include' && indata('selected', 'value', datum.value)",
                "value": 1
              },
              {"value": 0.15}
            ],
            "size": {"value": 64}
          }
        },
        "labels": {
          "name": "legendLabel",
          "interactive": true,
          "update": {
            "opacity": [
              {
                "test": "filtermode == 'exclude' && !indata('selected', 'value', datum.value)",
                "value": 1
              },
              {
                "test": "filtermode == 'include' && indata('selected', 'value', datum.value)",
                "value": 1
              },
              {"value": 0.25}
            ]
          }
        }
      }
    }
  ]
}
Davide Bacci
  • 16,647
  • 3
  • 10
  • 36
  • 1
    Yes - that is exactly what I wanted! The only difference though between my one line listing in the question and your chart is `== 0` (where I said `(length(data('selected'))?` your chart says `(length(data('selected') == 0)?`. So I wonder why the discrepancy in behavior? Shouldn't the two be equivalent? – az5112 May 17 '22 at 18:41
  • I also realised that the shift vs click+shift behavior is a bit silly. The two should be reversed. A click should select one item and disable everything else (so that the legend symbols act as radio buttons) and shift+click should toggle one item without affecting the others. So thanks for the "it is hard to understand precisely" hint in your other answer as well. – az5112 May 17 '22 at 18:48
  • One is checking the presence of an array and the other is checking the length of the array. – Davide Bacci May 18 '22 at 06:59
  • Arghh - I missed the exclamation `!(length(data('selected'))?` – az5112 May 18 '22 at 18:54
  • In my big chart I need to do `(length(data('selected') == 1)?` so I still think there is a quirk in Vega. That is what makes it so confusing. Will start a new question. – az5112 May 18 '22 at 19:18