jsonchartsvisualizationvega-litevega

Vega-lite Gradient: Specify Where the Color Changes Relative to the Axis Scale


I have a metric and want to show a gradient color in an area chart corresponding to good (0 to .1 = green), ok (>.1 to .3 = yellow), bad (> .3 = red) for that metric at various time points. How an I make the color cut points relative to the actual scale on the y axis?

I thought I could specify the color breaks with offset and it looked correct with my first pass:

{
  "width": 600,
  "data": {
    "values": [
      {
        "__row__": 0,
        "calendar_month_year": "Dec-23",
        "concentration_risk_monthly": 0.51
      },
      {
        "__row__": 1,
        "calendar_month_year": "Jan-24",
        "concentration_risk_monthly": 0.5
      },
      {
        "__row__": 2,
        "calendar_month_year": "Feb-24",
        "concentration_risk_monthly": 0.27
      },
      {
        "__row__": 3,
        "calendar_month_year": "Mar-24",
        "concentration_risk_monthly": 0.22
      },
      {
        "__row__": 4,
        "calendar_month_year": "Apr-24",
        "concentration_risk_monthly": 0.25
      },
      {
        "__row__": 5,
        "calendar_month_year": "May-24",
        "concentration_risk_monthly": 0.22
      }
    ]
  },
  "mark": {
    "type": "area",
    "color": {
      "x1": 1,
      "y1": 1,
      "x2": 1,
      "y2": 0,
      "gradient": "linear",
      "stops": [
        {"offset": 0, "color": "#00800166"},
        {"offset": 0.3, "color": "#F7B50066"},
        {"offset": 1, "color": "#A9281F66"}
      ]
    }
  },
  "encoding": {
    "x": {
      "field": "calendar_month_year",
      "type": "ordinal",
      "sort": {"field": "__row__"},
      "axis": {"title": null},
      "scale": {"padding": 0}
    },
    "y": {
      "field": "concentration_risk_monthly",
      "type": "quantitative",
      "axis": {"title": null}
    }
  }
}

Gives: enter image description here

But if I change the data to be below .05 for that metric (well below the .1 for green):

{
  "width": 600,
  "data": {
    "values": [
      {
        "__row__": 0,
        "calendar_month_year": "Dec-23",
        "concentration_risk_monthly": 0.051
      },
      {
        "__row__": 1,
        "calendar_month_year": "Jan-24",
        "concentration_risk_monthly": 0.05
      },
      {
        "__row__": 2,
        "calendar_month_year": "Feb-24",
        "concentration_risk_monthly": 0.027
      },
      {
        "__row__": 3,
        "calendar_month_year": "Mar-24",
        "concentration_risk_monthly": 0.022
      },
      {
        "__row__": 4,
        "calendar_month_year": "Apr-24",
        "concentration_risk_monthly": 0.025
      },
      {
        "__row__": 5,
        "calendar_month_year": "May-24",
        "concentration_risk_monthly": 0.022
      }
    ]
  },
  "mark": {
    "type": "area",
    "color": {
      "x1": 1,
      "y1": 1,
      "x2": 1,
      "y2": 0,
      "gradient": "linear",
      "stops": [
        {"offset": 0.0, "color": "#00800166"},
        {"offset": 0.3, "color": "#F7B50066"},
        {"offset": 1.0, "color": "#A9281F66"}
      ]
    }
  },
  "encoding": {
    "x": {
      "field": "calendar_month_year",
      "type": "ordinal",
      "sort": {"field": "__row__"},
      "axis": {"title": null},
      "scale": {"padding": 0}
    },
    "y": {
      "field": "concentration_risk_monthly",
      "type": "quantitative",
      "axis": {"title": null}
    }
  }
}

I'd expect the entire area to be green but it seems that the offsets are relative to the overall area and now there are still red areas even though all metrics are below .1: enter image description here


Solution

  • Just a very minor tweak to Davide's answer.

    {
      "width": 600,
      "data": {
        "values": [
          {
            "__row__": 0,
            "calendar_month_year": "Dec-23",
            "concentration_risk_monthly": 0.8
          },
          {
            "__row__": 1,
            "calendar_month_year": "Jan-24",
            "concentration_risk_monthly": 0.7
          },
          {
            "__row__": 2,
            "calendar_month_year": "Feb-24",
            "concentration_risk_monthly": 0.7
          },
          {
            "__row__": 3,
            "calendar_month_year": "Mar-24",
            "concentration_risk_monthly": 0.8
          },
          {
            "__row__": 4,
            "calendar_month_year": "Apr-24",
            "concentration_risk_monthly": 0.8
          },
          {
            "__row__": 5,
            "calendar_month_year": "May-24",
            "concentration_risk_monthly": 0.8
          }
        ]
      },
      "transform": [{"extent": "concentration_risk_monthly", "param": "myExtent"}],
      "mark": {
        "type": "area",
       "fill": {
          "expr": "{'gradient': 'linear', 'x1':0,'y1':1,'x2':0,'y2':0,  'stops': [{'offset': 0, 'color': scale('stroke',  0)},{'offset': 0.3, 'color':  scale('stroke', myExtent[1]>1?1:myExtent[1]>0.3?0.3:0)},{'offset': 1, 'color':  scale('stroke', myExtent[1]>=1?1:myExtent[1]>0.3?1:0.3)}]}"
        },
        "strokeOpacity": 0
      },
      "encoding": {
        "stroke": {
          "field": "1",
          "legend": null,
          "scale": {
            "type": "linear",
            "range": ["#00800166", "#F7B50066", "#A9281F66"],
            "domain": [0, 0.3, 1]
          }
        },
        "x": {
          "field": "calendar_month_year",
          "type": "ordinal",
          "sort": {"field": "__row__"},
          "axis": {"title": null},
          "scale": {"padding": 0}
        },
        "y": {
          "field": "concentration_risk_monthly",
          "type": "quantitative",
          "axis": {"title": null}
        }
      }
    }
    

    And just for fun here is a version with colored bands instead of gradient. Now it is much more clear where boundaries are.

    enter image description here

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
      "width": 600,
      "data": {
        "values": [
          {
            "__row__": 0,
            "calendar_month_year": "Dec-23",
            "concentration_risk_monthly": 0.2
          },
          {
            "__row__": 1,
            "calendar_month_year": "Jan-24",
            "concentration_risk_monthly": 0.1
          },
          {
            "__row__": 2,
            "calendar_month_year": "Feb-24",
            "concentration_risk_monthly": 0.5
          },
          {
            "__row__": 3,
            "calendar_month_year": "Mar-24",
            "concentration_risk_monthly": 0.04
          },
          {
            "__row__": 4,
            "calendar_month_year": "Apr-24",
            "concentration_risk_monthly": 0.5
          },
          {
            "__row__": 5,
            "calendar_month_year": "May-24",
            "concentration_risk_monthly": 0.2
          }
        ]
      },
      "transform": [
        {"extent": "concentration_risk_monthly", "param": "myExtent"},
        {
          "joinaggregate": [
            {"op": "max", "field": "concentration_risk_monthly", "as": "maxVal"}
          ]
        }
      ],
      "encoding": {
        "x": {
          "field": "calendar_month_year",
          "type": "ordinal",
          "sort": {"field": "__row__"},
          "axis": {
            "title": null,
            "domain": false,
            "labelAngle": 0,
            "labelFontSize": 12
          },
          "scale": {"padding": 0}
        },
        "y": {
          "field": "concentration_risk_monthly",
          "type": "quantitative",
          "stack": null,
          "axis": {
            "title": null,
            "ticks": false,
            "labels": false,
            "domain": false,
            "grid": false
          }
        }
      },
      "layer": [
        {
          "transform": [
            {
              "calculate": "datum.maxVal < 0.3 ? 0.3 : datum.maxVal >= 20 ? 1 : datum.maxVal",
              "as": "redStart"
            },
            {"calculate": "0.3", "as": "redEnd"}
          ],
          "mark": {"type": "area", "color": "#A9281F66", "clip": true},
          "encoding": {
            "y": {"type": "quantitative", "field": "redStart"},
            "y2": {"field": "redEnd"}
          }
        },
        {
          "transform": [
            {
              "calculate": "datum.maxVal < 0.3 ? 0.3 : datum.maxVal >= 0.3 ? 0.3 : datum.maxVal",
              "as": "yellowStart"
            },
            {"calculate": "0.1", "as": "yellowEnd"}
          ],
          "mark": {"type": "area", "color": "#F7B50066", "clip": true},
          "encoding": {
            "y": {"type": "quantitative", "field": "yellowStart"},
            "y2": {"field": "yellowEnd"}
          }
        },
        {
          "transform": [
            {"calculate": "0.1", "as": "greenStart"},
            {"calculate": "0", "as": "greenEnd"}
          ],
          "mark": {
            "type": "area",
            "color": "#00800166",
            "opacity": 1,
            "clip": true
          },
          "encoding": {
            "y": {"type": "quantitative", "field": "greenStart"},
            "y2": {"field": "greenEnd"}
          }
        },
        {
          "mark": {"type": "line", "strokeOpacity": 1},
          "encoding": {"color": {"value": "black"}}
        },
        {
          "mark": {"type": "area"},
          "encoding": {"color": {"value": "white"}, "y2": {"value": 0}}
        },
        {
          "mark": {
            "type": "circle",
            "size": 70,
            "stroke": "black",
            "strokeWidth": 1,
            "fill": {
              "expr": "datum.concentration_risk_monthly <= 0.1 ? '#38B510' : datum.concentration_risk_monthly <= 0.3 ? '#F3CF26' : '#EC3B15'"
            }
          },
          "encoding": {"opacity": {"value": 1}}
        },
        {
          "mark": {
            "type": "rect",
            "cornerRadius": 6,
            "xOffset": {"expr": "-0"},
            "yOffset": {"expr": "-20"},
            "height": 20,
            "width": 35,
            "fill": {
              "expr": "datum.concentration_risk_monthly <= 0.1 ? '#38B510' : datum.concentration_risk_monthly <= 0.3 ? '#F3CF26' : '#EC3B15'"
            }
          }
        },
        {
          "mark": {
            "type": "text",
            "align": "center",
            "baseline": "middle",
            "xOffset": {"expr": "1"},
            "dy": -19,
            "dx": -1
          },
          "encoding": {
            "text": {
              "field": "concentration_risk_monthly",
              "type": "quantitative",
              "format": ".2f"
            },
            "color": {
              "condition": {
                "test": "datum.concentration_risk_monthly > 0.1 && datum.concentration_risk_monthly <= 0.3",
                "value": "black"
              },
              "value": "white"
            }
          }
        }
      ],
      "config": {"view": {"stroke": "transparent"}}
    }
    

    And one more version with smooth lines and 3 separate gradients:)

    enter image description here

    {
      "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
      "usermeta": {"embedOptions": {"renderer": "svg"}},
      "width": 600,
      "data": {
        "values": [
          {
            "__row__": 0,
            "calendar_month_year": "Dec-23",
            "concentration_risk_monthly": 0.2
          },
          {
            "__row__": 1,
            "calendar_month_year": "Jan-24",
            "concentration_risk_monthly": 0.1
          },
          {
            "__row__": 2,
            "calendar_month_year": "Feb-24",
            "concentration_risk_monthly": 0.5
          },
          {
            "__row__": 3,
            "calendar_month_year": "Mar-24",
            "concentration_risk_monthly": 0.04
          },
          {
            "__row__": 4,
            "calendar_month_year": "Apr-24",
            "concentration_risk_monthly": 0.5
          },
          {
            "__row__": 5,
            "calendar_month_year": "May-24",
            "concentration_risk_monthly": 0.2
          }
        ]
      },
      "transform": [
        {"extent": "concentration_risk_monthly", "param": "myExtent"},
        {
          "joinaggregate": [
            {"op": "max", "field": "concentration_risk_monthly", "as": "maxVal"}
          ]
        }
      ],
      "encoding": {
        "x": {
          "field": "calendar_month_year",
          "type": "ordinal",
          "sort": {"field": "__row__"},
          "axis": {
            "title": null,
            "domain": false,
            "labelAngle": 0,
            "labelFontSize": 10
          },
          "scale": {"padding": 0}
        },
        "y": {
          "field": "concentration_risk_monthly",
          "type": "quantitative",
          "stack": null,
          "axis": {
            "title": null,
            "ticks": false,
            "labels": false,
            "domain": false,
            "grid": false,
            "labelPadding": 5,
            "labelFontSize": 10
          }
        }
      },
      "layer": [
        {
          "transform": [
            {
              "calculate": "datum.maxVal < 0.3 ? 0.3 : datum.maxVal >= 20 ? 1 : datum.maxVal",
              "as": "redStart"
            },
            {"calculate": "0.3", "as": "redEnd"}
          ],
          "mark": {
            "type": "area",
            "interpolate": "monotone",
            "clip": true,
            "color": {
              "x1": 1,
              "y1": 1,
              "x2": 1,
              "y2": 0.3,
              "gradient": "linear",
              "stops": [
                {"offset": 0, "color": "#FFC600"},
                {"offset": 1, "color": "#FE3800"}
              ]
            }
          },
          "encoding": {
            "y": {"type": "quantitative", "field": "redStart"},
            "y2": {"field": "redEnd"}
          }
        },
        {
          "transform": [
            {
              "calculate": "datum.maxVal < 0.3 ? 0.3 : datum.maxVal >= 0.3 ? 0.3 : datum.maxVal",
              "as": "yellowStart"
            },
            {"calculate": "0.1", "as": "yellowEnd"}
          ],
          "mark": {
            "type": "area",
            "interpolate": "monotone",
            "clip": true,
            "color": {
              "x1": 1,
              "y1": 1,
              "x2": 1,
              "y2": 0.1,
              "gradient": "linear",
              "stops": [
                {"offset": 0, "color": "#C9F047"},
                {"offset": 1, "color": "#FFC600"}
              ]
            }
          },
          "encoding": {
            "y": {"type": "quantitative", "field": "yellowStart"},
            "y2": {"field": "yellowEnd"}
          }
        },
        {
          "transform": [
            {"calculate": "0.1", "as": "greenStart"},
            {"calculate": "0", "as": "greenEnd"}
          ],
          "mark": {
            "type": "area",
            "interpolate": "monotone",
            "opacity": 1,
            "clip": true,
            "color": {
              "x1": 1,
              "y1": 1,
              "x2": 1,
              "y2": 0.1,
              "gradient": "linear",
              "stops": [
                {"offset": 0, "color": "#1AA60C"},
                {"offset": 1, "color": "#C9F047"}
              ]
            }
          },
          "encoding": {
            "y": {"type": "quantitative", "field": "greenStart"},
            "y2": {"field": "greenEnd"}
          }
        },
        {
          "transform": [{"calculate": "0.1", "as": "yellowBorder"}],
          "mark": {
            "type": "line",
            "strokeWidth": 1,
            "strokeDash": [2, 2],
            "color": "black"
          },
          "encoding": {"y": {"field": "yellowBorder"}}
        },
        {
          "transform": [{"calculate": "0.3", "as": "redBorder"}],
          "mark": {
            "type": "line",
            "strokeWidth": 1,
            "strokeDash": [2, 2],
            "color": "black"
          },
          "encoding": {"y": {"field": "redBorder"}}
        },
        {
          "mark": {
            "type": "area",
            "interpolate": "monotone",
            "stroke": "white",
            "strokeWidth": 1
          },
          "encoding": {"color": {"value": "white"}, "y2": {"value": 0}}
        },
        {
          "mark": {"type": "line", "strokeWidth": 1, "interpolate": "monotone"},
          "encoding": {"color": {"value": "black"}}
        },
        {
          "mark": {
            "type": "circle",
            "size": 70,
            "stroke": "black",
            "strokeWidth": 1,
            "fill": {
              "expr": "datum.concentration_risk_monthly <= 0.1 ? '#38B510' : datum.concentration_risk_monthly <= 0.3 ? '#F3CF26' : '#EC3B15'"
            }
          },
          "encoding": {"opacity": {"value": 1}}
        },
        {
          "mark": {
            "type": "text",
            "align": "left",
            "baseline": "middle",
            "xOffset": {"expr": "1"},
            "dy": 0,
            "dx": 12,
            "color": "black",
            "angle": 270
          },
          "encoding": {
            "text": {
              "field": "concentration_risk_monthly",
              "type": "quantitative",
              "format": ".2f"
            }
          }
        }
      ],
      "config": {"view": {"stroke": "white", "strokeWidth": 3}}
    }