Search code examples
vega-lite

Vega lite facet chart spread over columns and rows


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.


Solution

  • 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)

    enter image description here

    {
      "$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"}}
    }