How can I make my facet chart span over multiple rows and columns automatically? I also want to color my charts based on the starting and ending values like google finance charts.
Note: Question is very simple without code etc as I will answer this myself.
Below is a chart spec that combines both a facet and vconcat to enable charts that span over rows and columns. On line 46 you can test by simply changing:
"columns": 2 to "columns": 3 for example.
I have attempted to design this spec similar to Google finance as of March 2024. Had some issues aligning the text marks and rect at the top but these x,y,offset settings are easily updated. I also managed to get gradient background working with line and area layers. Area layer uses a min aggregate to avoid showing zero in the scale.
In my dashboard I have a simple JavaScript function which returns the screen width and then I can adjust the columns value dynamically. I have created a request on Github so we can provide an expression for the columns value but for now we must update this from JavaScript. For example:
function getWindowWidth() {
var currentWidth = window.innerWidth;
if (currentWidth >= 2250) {return 5;}
else if (currentWidth >= 1800) {return 4;}
else if (currentWidth >= 1350) {return 3;}
else if (currentWidth >= 900) {return 2;}
else if (currentWidth >= 450) {return 1;}
else {return 1;}
}
function handleWindowResize() {
var respWidth = getWindowWidth();
var yourV1Spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Stock prices of 5 Tech Companies over Time.",
....
"vconcat": [
{
"columns": respWidth,
....
};
vegaEmbed('#vis', yourV1Spec, {"actions": false});
}
window.addEventListener('resize', handleWindowResize, { passive: true });
handleWindowResize();
I hope this is useful for the vega-lite community!
Adam (APB Reports)
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"description": "Stock prices of 5 Tech Companies over Time.",
"title": {
"text": "Stock prices of 5 Tech Companies over Time.",
"color": "#5f5f5f",
"fontSize": 14,
"anchor": "start",
"subtitle": "This chart demonstrates how to facet chart by column and row and how to color based on start and end values.",
"subtitlePadding": 10,
"offset": 20
},
"usermeta": {
"embedOptions": {
"renderer": "svg",
"actions": false,
"config": {"view": {"stroke": "transparent"}}
}
},
"data": {"url": "data/stocks.csv"},
"transform": [
{"filter": "timeFormat(datum.date,'%Y') >= 2008"},
{
"window": [{"op": "row_number", "as": "minDate"}],
"sort": [{"field": "date", "order": "ascending"}],
"groupby": ["symbol"]
},
{
"window": [{"op": "row_number", "as": "maxDate"}],
"sort": [{"field": "date", "order": "descending"}],
"groupby": ["symbol"]
},
{"calculate": "datum.minDate == 1 ? datum.price : 0", "as": "minValTemp"},
{"calculate": "datum.maxDate == 1 ? datum.price : 0", "as": "maxValTemp"},
{
"joinaggregate": [{"op": "max", "field": "minValTemp", "as": "minVal"}],
"groupby": ["symbol"]
},
{
"joinaggregate": [{"op": "max", "field": "maxValTemp", "as": "maxVal"}],
"groupby": ["symbol"]
}
],
"vconcat": [
{
"columns": 2,
"facet": {
"field": "symbol",
"type": "nominal",
"title": "",
"header": {
"labelOrient": "top",
"labelAnchor": "start",
"labelAlign": "center",
"labelFontSize": 15,
"labelPadding": 8,
"title": null
}
},
"spacing": 30,
"spec": {
"height": 130,
"width": 400,
"encoding": {
"opacity": {"value": 1},
"x": {
"timeUnit": "yearmonth",
"field": "date",
"axis": {
"grid": false,
"domainColor": "silver",
"tickColor": "silver",
"tickCount": 5,
"labelExpr": "[timeFormat(datum.value, '%b'),timeFormat(datum.value, '%Y')]"
},
"title": null
},
"y": {
"field": "price",
"type": "quantitative",
"title": null,
"axis": {
"grid": false,
"domainColor": "silver",
"tickColor": "silver",
"tickCount": 6,
"offset": 1
}
},
"tooltip": null
},
"layer": [
{
"transform": [
{"filter": "toNumber(datum.minVal) >= toNumber(datum.maxVal)"}
],
"mark": {"type": "line", "size": 3, "color": "#ea4335"},
"encoding": {
"y": {
"field": "price",
"type": "quantitative",
"scale": {"zero": false}
}
}
},
{
"transform": [
{"filter": "toNumber(datum.minVal) >= toNumber(datum.maxVal)"}
],
"mark": {
"type": "area",
"size": 2,
"color": {
"x1": 1,
"y1": 1,
"x2": 1,
"y2": 0,
"gradient": "linear",
"stops": [
{"offset": 0, "color": "white"},
{"offset": 1, "color": "#ffe8e3"}
]
}
},
"encoding": {
"y": {
"field": "price",
"type": "quantitative",
"title": null,
"aggregate": "min"
}
}
},
{
"transform": [
{"filter": "datum.maxDate == 1"},
{"filter": "toNumber(datum.minVal) >= toNumber(datum.maxVal)"}
],
"mark": {"type": "circle", "size": 80, "color": "#ea4335"}
},
{
"transform": [
{"filter": "toNumber(datum.minVal) < toNumber(datum.maxVal)"}
],
"mark": {"type": "line", "size": 3, "color": "#34a853"},
"encoding": {
"y": {
"field": "price",
"type": "quantitative",
"scale": {"zero": false}
}
}
},
{
"transform": [
{"filter": "toNumber(datum.minVal) < toNumber(datum.maxVal)"}
],
"mark": {
"type": "area",
"color": {
"x1": 1,
"y1": 1,
"x2": 1,
"y2": 0,
"gradient": "linear",
"stops": [
{"offset": 0, "color": "white"},
{"offset": 1, "color": "#d6f0cc"}
]
}
},
"encoding": {
"y": {
"field": "price",
"type": "quantitative",
"aggregate": "min"
}
}
},
{
"transform": [
{"filter": "datum.maxDate == 1"},
{"filter": "toNumber(datum.minVal) < toNumber(datum.maxVal)"}
],
"mark": {"type": "circle", "size": 80, "color": "#34a853"}
},
{
"mark": {
"type": "line",
"color": "#808080",
"strokeWidth": 1,
"strokeDash": [3, 3]
},
"encoding": {
"x": {"field": "date", "type": "temporal"},
"y": {"field": "minVal", "type": "quantitative"}
}
},
{
"transform": [{"filter": "datum.maxDate == 1"}],
"mark": {
"type": "text",
"tooltip": true,
"align": "left",
"baseline": "middle",
"dx": {"expr": "-406"},
"dy": {"expr": "-60"},
"color": "black",
"fontSize": 18,
"fontWeight": 600
},
"encoding": {
"text": {
"field": "price",
"type": "quantitative",
"format": ".2f"
}
}
},
{
"transform": [
{"filter": "datum.maxDate == 1"},
{
"calculate": "toNumber(datum.maxVal-datum.minVal)",
"as": "diffVal"
}
],
"mark": {
"type": "rect",
"cornerRadius": 6,
"xOffset": {"expr": "-290"},
"yOffset": {"expr": "-60"},
"height": 27,
"width": 70,
"fill": {"expr": "datum.diffVal >= 0 ? '#e6f4ea' : '#fce8e6'"}
}
},
{
"transform": [
{"filter": "datum.maxDate == 1"},
{
"calculate": "((datum.maxVal-datum.minVal) / datum.minVal)* 100",
"as": "diffPerc"
},
{
"calculate": "(datum.diffPerc > 0 ? '🠉 ' : datum.diffPerc < 0 ? '🠋 ' : '🠊 ') + (datum.diffPerc <0 ? format(datum.diffPerc*-1,',.1f')+ '%' : format(datum.diffPerc,',.1f') + '%')",
"as": "change"
}
],
"mark": {
"type": "text",
"tooltip": true,
"align": "left",
"baseline": "middle",
"dx": {"expr": "-305"},
"dy": {"expr": "-60"},
"color": {"expr": "datum.diffPerc >= 0 ? '#137333' : '#a50e0e'"}
},
"encoding": {"text": {"field": "change"}}
},
{
"transform": [
{"filter": "datum.maxDate == 1"},
{
"calculate": "toNumber(datum.maxVal-datum.minVal)",
"as": "diffVal"
},
{
"calculate": "((datum.diffVal > 0 ? '+ ' : datum.diffVal < 0 ? '- ' : ' ') + (datum.diffVal <0 ? format(datum.diffVal*-1,',.0f') : format(datum.diffVal,',.0f'))) + ' (Change last 26M)'",
"as": "change"
}
],
"mark": {
"type": "text",
"align": "left",
"baseline": "middle",
"dx": {"expr": "-230"},
"dy": {"expr": "-60"},
"color": {"expr": "datum.diffVal >= 0 ? '#137333' : '#a50e0e'"}
},
"encoding": {"text": {"field": "change"}}
},
{
"transform": [{"filter": {"param": "label", "empty": false}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {"type": "temporal", "field": "date", "aggregate": "min"}
}
},
{
"encoding": {
"text": {
"type": "quantitative",
"field": "price",
"format": ".2f"
},
"x": {"type": "temporal", "field": "date"},
"y": {"type": "quantitative", "field": "price"}
},
"layer": [
{
"mark": {
"type": "text",
"stroke": "white",
"strokeWidth": 3,
"align": {
"expr": "datum.minDate <= 2 ? 'left': datum.maxDate <= 2 ? 'right': 'center'"
},
"dx": 0,
"dy": -13
}
},
{
"mark": {
"type": "text",
"align": {
"expr": "datum.minDate <= 2 ? 'left': datum.maxDate <= 2 ? 'right': 'center'"
},
"dx": 0,
"dy": -13,
"color": "gray"
}
}
]
},
{
"encoding": {
"text": {
"type": "temporal",
"field": "date",
"timeUnit": "yearmonth"
},
"x": {"type": "temporal", "field": "date"},
"y": {"type": "quantitative", "field": "price"}
},
"layer": [
{
"mark": {
"type": "text",
"stroke": "white",
"strokeWidth": 3,
"align": {
"expr": "datum.minDate <= 2 ? 'left': datum.maxDate <= 2 ? 'right': 'center'"
},
"dx": 0,
"dy": -29
}
},
{
"mark": {
"type": "text",
"align": {
"expr": "datum.minDate <= 2 ? 'left': datum.maxDate <= 2 ? 'right': 'center'"
},
"dx": 0,
"dy": -29,
"color": "gray"
}
}
]
}
]
},
{
"params": [
{
"name": "label",
"select": {
"type": "point",
"encodings": ["x"],
"nearest": true,
"on": "pointerover"
}
}
],
"mark": {"type": "circle", "size": 80, "color": "gray"},
"encoding": {
"opacity": {
"condition": {"param": "label", "empty": false, "value": 1},
"value": 0
}
}
}
]
},
"resolve": {"scale": {"y": "independent", "x": "independent"}}
}
],
"config": {"view": {"stroke": "transparent"}}
}