Reputation: 1
I'm trying to create a discrete mapping between values and colors for a Vega visualization. I don't know beforehand exactly the values that I'll be getting (I'm pulling in data from Elasticsearch), but I do have a "base" mapping that I've built. The problem is that no matter what I do, the ordinal color scale always "rotates" through the color values if there's a repeated entry (range).
What I'd like to do is have a static, default color for un-mapped values. I've been able to define this color using a filtered dataset with a default
value:
{
"name": "mapping_filtered",
"source": "data-9794decafd64313e2c537fe6fc95bda8",
"transform": [
{
"type": "aggregate",
"groupby": [{"field": "label", "as": "domain"}, {"field": "x", "as": "x"},{"field": "y", "as": "y"}]
},
{
"type": "lookup",
"from": "mapping", "key": "domain",
"fields": ["domain"],
"values": ["range"],
"default": "#a9a9a9"
}
]
}
Here's my scale definition:
{
"name": "color",
"type": "ordinal",
"domain": {"data": "mapping_filtered", "field": "domain"},
"range": {"data": "mapping_filtered", "field": "range"}
},
The problem is that Vega seems to always rotate to the next color in the range whenever it encounters a color that's already been used (like my default value for multiple unknown inputs).
For example, consider the following, where "D" and "E" are unknown inputs that get mapped to the default:
domain | range |
---|---|
A | "#4daf4a" |
B | "#984ea3" |
C | "#ff7f00" |
D | "#a9a9a9" |
E | "#a9a9a9" |
In this case, Vega will always render "E" using a range value of "#4daf4a" even though it's theoretically mapped to "#a9a9a9".
Is there anyway to force an ordinal color scale in Vega to behave statically like my map above? Or is my only solution to generate unique range values (colors) for every domain?
Update: here's a link to a Vega editor example of what I'm trying to do: https://vega.github.io/editor/?#/gist/ee14ecd00c4265ae4d554e46b75bcb8d/spec.json
In this example, I was able to get the marks to render the way I want to because they use the colors from the data directly (not through a scale). However, if you look at the legend, you'll see that "E" has the wrong color (it should be the same gray value as "D").
Upvotes: 0
Views: 185
Reputation: 797
If scale is specified with explicit domain and range arrays of equal lengths, the result will be what you expect even with repeated color values, e.g.
{
"name": "color",
"type": "ordinal",
"domain": ["A", "B", "C", "D", "E"]
"range": ["red", green", "blue", "grey", "grey"]
},
But when scale is specified as
{
"name": "color",
"type": "ordinal",
"domain": {"data": "mapping_filtered", "field": "domain"},
"range": {"data": "mapping_filtered", "field": "range"}
},
Vega will extract unique values and so the lengths of the domain and range arrays may be unequal, e.g.
"domain": ["A", "B", "C", "D", "E"]
"range": ["red", green", "blue", "grey"]
In this case Vega will recycle the colors and so "E" is "red".
On solution to have a default color value is to use anonymous signal expressions to modify the colors in "marks" and in "legends".
Here is a complete working example. View in Vega online editor
Notes:
The scale for colors only include the pre-defined colors, not the default color.
For marks "symbol", if no match is found in color scale then color of plotted symbol is default "grey":
"stroke": {"signal": "indexof(domain('scale_color'), datum['category']) < 0 ? 'grey' : scale('scale_color', datum['category'])"},
For legends, "datum" refers to an item in the legend, and "datum.label" refers to the legend item's label ("A", "B", "C", etc in this example).
"stroke": {"signal": "indexof(domain('scale_color'), datum.label) < 0 ? 'grey' : scale('scale_color', datum.label)"},
Complete Vega spec:
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"background": "white",
"padding": 5,
"width": 400,
"height": 300,
"data": [
{
"name": "data_table",
"values": [
{"x": 1.5, "y": 0.5, "category": "E"},
{"x": 2.0, "y": 2.0, "category": "G"},
{"x": 2.5, "y": 1.5, "category": "F"},
{"x": 1.0, "y": 3.0, "category": "A"},
{"x": 1.5, "y": 2.5, "category": "G"},
{"x": 1.0, "y": 1.0, "category": "B"},
{"x": 3.0, "y": 2.0, "category": "C"},
{"x": 0.5, "y": 2.0, "category": "R"},
{"x": 1.5, "y": 1.0, "category": "G"},
{"x": 2.5, "y": 2.5, "category": "R"},
{"x": 0.5, "y": 0.5, "category": "B"},
{"x": 2.0, "y": 1.5, "category": "D"}
]
}
],
"scales": [
{
"name": "scale_x",
"type": "linear",
"domain": {"data": "data_table", "field": "x"},
"range": {"signal": "[0, width]"},
"nice": true,
"zero": true
},
{
"name": "scale_y",
"type": "linear",
"domain": {"data": "data_table", "field": "y"},
"range": {"signal": "[height, 0]"},
"nice": true,
"zero": true
},
{
"name": "scale_color",
"type": "ordinal",
"domain": ["R", "G", "B"],
"range": ["red", "green", "blue"]
},
{
"name": "scale_shape",
"type": "ordinal",
"domain": {"data": "data_table", "field": "category", "sort": true},
"range": "symbol"
}
],
"axes": [
{
"title": "X",
"scale": "scale_x",
"orient": "bottom",
"grid": true
},
{
"title": "Y",
"scale": "scale_y",
"orient": "left",
"grid": true
}
],
"marks": [
{
"name": "marks",
"type": "symbol",
"from": {"data": "data_table"},
"encode": {
"update": {
"x": {"scale": "scale_x", "field": "x"},
"y": {"scale": "scale_y", "field": "y"},
"shape": {"scale": "scale_shape", "field": "category"},
"size": {"value": 200},
"stroke": {"signal": "indexof(domain('scale_color'), datum['category']) < 0 ? 'grey' : scale('scale_color', datum['category'])"},
"strokeWidth": {"value": 2},
"fill": {"value": "transparent"},
"opacity": {"value": 0.7}
}
}
}
],
"legends": [
{
"shape": "scale_shape",
"title": "Category",
"encode": {
"symbols": {
"update": {
"stroke": {"signal": "indexof(domain('scale_color'), datum.label) < 0 ? 'grey' : scale('scale_color', datum.label)"},
"fill": {"value": "transparent"},
"opacity": {"value": 0.7}
}
}
}
}
]
}
Upvotes: 0