Data binding between views.
You can send a dataset or a manipulation of a dataset between separate views by defining data binding in the query
object of a published signal. You can do this both in the global configuration file or in the view specific configuration file:
publish:
- signal: exportData
as: updateFromView1
query: # define data binding by adding a query object
dataset: table
action: change
select:
field: category
test: ==
update:
fields:
- amount
- color
There are 5 possible actions that you can perform on a dataset:
- change fields in selected data rows of a dataset
- remove fields in selected data rows of a dataset
- add new data rows to a dataset
- replace a complete dataset
- remove a complete dataset
Below the type definition of a query object. The test
key in the select
object is only required when action
is set to “change” or “remove”.
type QueryType = {
[dataset: string]: string,
[action: string]: ActionUnionType
[select: string]?: {
[field: string]: string,
[test: string]?: TestUnionType,
},
[update: string]?: {
[fields: string]: Array<string>,
},
};
type ActionUnionType =
| 'change'
| 'remove'
| 'insert'
| 'replace_all'
| 'remove_all'
;
type TestUnionType =
| '=='
| '!='
| '<'
| '>'
;
Change fields in a selected data row
Take a look at the example below. You can click on the bars and the dots and adjust the values; as you will see the values stay in sync between the 2 views. Another thing you’ll notice is that if you drag for instance the bar of category H on the left side, the values of categories A and B change as well.
In the example above I have used a global configuration, below an excerpt of this file:
publish:
- signal: exportData
as: updateDataFromView1
query:
dataset: table
action: change
select:
field: category
test: "=="
update:
fields:
- amount
- color
The query
object behaves like a sort of SQL query where dataset
tells us in which dataset the changes will be applied.
The select
object tells us how to select the right data row in the dataset. In this example we want to select a row by category and therefor field
is set to “category” and test
is set to “==”. The value that we are going to test against will be provided by the signal, see below.
Then we have an update
object, in this object we list the field(s) in the data row that will be replaced by the new value(s). These new values will be provided by the signal as well. In this case we want to update the fields “amount” and “color”.
Now let’s have a look at the spec to see how the signal sets the necessary values:
signals:
- name: exportData
value: {}
on:
- events:
signal: changeAmount
update: "[
[selectedCategory.category, changeAmount.amount],
['A', changeAmount.amount * 0.5, changeAmount.amount > 50 ? 'red' : 'steelblue'],
['B', changeAmount.amount * 0.25, changeAmount.amount < 150 ? 'lightgreen' : 'steelblue'],
]"
As you can see the signal is an array of tuples.
The first value of every tuple is the value that will be used for the test. The second and subsequent values are the new values of the fields that will be updated. In this example we can update the fields “amount” and “color” because they are defined in the query.update.fields
key of the published signal. This doesn’t mean however that we have to provide a value for both fields. In the spec above you see that the first tuple has only 2 values, but the 2nd and 3rd tuple have 3 values. Note that the order of values should match the order of the fields: in case you only want to provide a value for “color” you must pass null
as the first value.
In the first tuple we test against the value of the selectedCategory
signal which holds the datum that is coupled to the bar or dot as soon as we start dragging it. In this datum the category
key gives us the name of the currently selected category. In the 2nd tuple we simply select category “A”, and in the 3rd tuple category “B” is selected.
In the first tuple we set the value of the currently selected category to the current value of the amount
key of the changeAmount
signal. In the 2nd and 3rd tuple we set the amount to a fraction of this value and we set a different color when this value surpasses a certain value. Note that these changes only occur in the view(s) that are subscribed to the “exportData” signal and not in the view that publishes the signal.
We can draw the following conclusion: a global or view specific configuration provides the logic and the signal of a spec provides the values that this logic acts upon.
Source files of this example:
Replace a complete dataset
The example below is a more ‘brute force’ solution for keeping the values of the categories in sync between the 2 views: every time you change a value by dragging a dot or a bar, the complete dataset is copied over to the other view.
Your vmv config would look something like this:
publish:
- signal: exportData
as: updateDataFromView3
query:
dataset: table
action: replace_all
And your then exportData
signal should hold the complete table
dataset:
signals:
- name: exportData
value: {}
on:
- events:
signal: changeAmount
update: "{data: data('table')}"
Note that the dataset is wrapped in an object; this seems useless but if we sent the plain dataset like this update: "data('table')"
, then Vega doesn’t recognize that the value of the signal has changed for some reason.
Source files of this example:
Remove fields in selected data rows
When the value of a category is less than 10, this category will be removed in the other view. Click on [reset] to restore the original data.
Excerpt of the vmv config:
publish:
- signal: exportData
as: updateFromView5
query:
dataset: table
action: remove
select:
field: category
test: ==
And the exportData
signal:
signals:
- name: exportData
value: {}
on:
- events:
signal: changeAmount
update: "changeAmount.amount < 10 ? [
[selectedCategory.category],
] : {}"
Source files of this example:
Insert new data rows
When the value of a category is 101 or 102, a new category will be added to the other view. Click on [reset] to restore the original data.
Excerpt of the vmv config:
publish:
- signal: exportData
as: updateFromView7
query:
dataset: table
action: insert
And the exportData
signal:
signals:
- name: exportData
value: {}
on:
- events:
signal: changeAmount
update: "(changeAmount.amount > 100 && changeAmount.amount < 103) ? [
{'category': selectedCategory.category + '-' + changeAmount.amount, 'amount': 30, 'color': 'yellow'},
] : {}"
Source files of this example:
Remove a complete data set
Click on [remove data] to remove the complete dataset in both views.
In the vmv config the signals of the toggle button for removing and adding data are published:
publish:
- signal: removeData
query:
dataset: table
action: remove_all
- signal: addData
query:
dataset: table
action: replace_all
And in the spec of the toggle button the signals are defined as follows:
- name: addData
on:
- events: "@add_data_button:click"
update: "buttonLabel === '[add data]' ? {data: data('table')} : {}"
- name: removeData
on:
- events: "@add_data_button:click"
update: "buttonLabel === '[remove data]' ? true : false"
- name: buttonLabel
value: '[remove data]'
on:
- events:
signal: addData
update: "buttonLabel === '[add data]' ? '[remove data]' : '[add data]'"
As you can see, the state of the toggle is based on the value of the label on the button. The signal removeData
sends a boolean and the signal addData
sends either an empty object or the complete dataset. Note that both signals change on every button click; it would have been much neater if the button only triggers one signal change per click. This can be done if we use replace_all
and send an empty dataset which effectively removes the dataset as well. In the vmv config:
publish:
- signal: toggleData
query:
dataset: table
action: replace_all
And in the toggle button spec:
- name: toggleData
on:
- events: "@add_data_button:click"
update: "buttonLabel === '[add data]' ? {data: data('table')} : {}"
- name: buttonLabel
value: '[remove data]'
on:
- events:
signal: toggleData
update: "buttonLabel === '[add data]' ? '[remove data]' : '[add data]'"
Obviously this is a better solution but of course the purpose of this example was to demonstrate the use of remove_all
.
Source files of this example: