I having been thinking about this issue and have come up with another approach.
Instead of trying to fix the chart we just need to fix the data and introduce some fake rows of data where the values cross the threshold point. In my case this is 200.
So if my dataset is:
{"date": "2022-01-01T00:00:00", "price": 100},
{"date": "2022-01-02T00:00:00", "price": 150},
{"date": "2022-01-03T00:00:00", "price": 180},
{"date": "2022-01-04T00:00:00", "price": 270},
{"date": "2022-01-05T00:00:00", "price": 80}
We need to add some additional "fake" rows where the price crosses the threshold between the two data points:
{"date": "2022-01-03T00:00:00", "price": 200},
{"date": "2022-01-04T00:00:00", "price": 200},
To get the lines to work perfectly we need to calculate at approx what time did the line cross the threshold. We can find this out my looking at the range of start and end. If midnight was 180 and 24 hrs later it was 270 then we know it moved 90 during 24 hrs. So we just need to know how long did id take to move 20 (180 to 200). And that is easy with javascript. You could also apply the same logic with a view in SQL.
{"date": "2022-01-03T00:00:00", "price": 180},
{"date": "2022-01-04T00:00:00", "price": 270},
This can be done with Javascript like this:
if ((d1.price < threshold && d2.price >= threshold) || (d1.price >= threshold && d2.price < threshold)) {
// Calculate the interpolated point where the line crosses the threshold
const t = (threshold - d1.price) / (d2.price - d1.price);
const interpolatedPrice = threshold;
const interpolatedTimestamp = new Date(new Date(d1.date).getTime() + t * (new Date(d2.date).getTime() - new Date(d1.date).getTime()));
interpolatedTimestamp.setSeconds(0); // Round to the nearest minute
const interpolatedDate = interpolatedTimestamp.toISOString();

I also found it was necessary to add 2 fake rows each time. One for each color otherwise vega-lite would show gaps in the chart.
Please use the javascript below to show a multi-color line chart the crosses a threshold.
Happy charting!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vega-Lite Example</title>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>
<body>
<div id="vis"></div>
<script>
// Load the data
const data = [
{"date": "2022-01-10T00:00:00", "price": 202},
{"date": "2022-01-11T00:00:00", "price": 198},
{"date": "2022-01-12T00:00:00", "price": 100},
{"date": "2022-01-13T00:00:00", "price": 200},
{"date": "2022-01-14T00:00:00", "price": 200},
{"date": "2022-01-15T00:00:00", "price": 150},
{"date": "2022-01-16T00:00:00", "price": 180},
{"date": "2022-01-17T00:00:00", "price": 270},
{"date": "2022-01-18T00:00:00", "price": 170},
{"date": "2022-01-19T00:00:00", "price": 220},
{"date": "2022-01-20T00:00:00", "price": 221},
{"date": "2022-01-21T00:00:00", "price": 190},
{"date": "2022-01-22T00:00:00", "price": 185},
{"date": "2022-01-23T00:00:00", "price": 202},
{"date": "2022-01-24T00:00:00", "price": 270},
{"date": "2022-01-25T00:00:00", "price": 160},
{"date": "2022-01-26T00:00:00", "price": 220},
{"date": "2022-01-27T00:00:00", "price": 221},
{"date": "2022-01-28T00:00:00", "price": 190},
{"date": "2022-01-29T00:00:00", "price": 185},
{"date": "2022-01-30T00:00:00", "price": 202},
{"date": "2022-01-31T00:00:00", "price": 202},
{"date": "2022-02-01T00:00:00", "price": 300},
{"date": "2022-02-02T00:00:00", "price": 250},
{"date": "2022-02-03T00:00:00", "price": 280},
{"date": "2022-02-04T00:00:00", "price": 270},
{"date": "2022-02-05T00:00:00", "price": 180},
{"date": "2022-02-06T00:00:00", "price": 120},
{"date": "2022-02-07T00:00:00", "price": 171},
{"date": "2022-02-08T00:00:00", "price": 190},
{"date": "2022-02-09T00:00:00", "price": 185},
{"date": "2022-02-10T00:00:00", "price": 202},
{"date": "2022-02-11T00:00:00", "price": 230}
];
const threshold = 180;
// Iterate through the data and add fake rows for values that cross the threshold
const newData = [];
for (let i = 0; i < data.length - 1; i++) {
const d1 = data[i];
const d2 = data[i + 1];
// Check if the price crosses the threshold between these two data points
if ((d1.price < threshold && d2.price >= threshold) || (d1.price >= threshold && d2.price < threshold)) {
// Calculate the interpolated point where the line crosses the threshold
const t = (threshold - d1.price) / (d2.price - d1.price);
const interpolatedPrice = threshold;
const interpolatedTimestamp = new Date(new Date(d1.date).getTime() + t * (new Date(d2.date).getTime() - new Date(d1.date).getTime()));
interpolatedTimestamp.setSeconds(0); // Round to the nearest minute
const interpolatedDate = interpolatedTimestamp.toISOString();
// Add a fake data point for the interpolated value
newData.push({
date: d1.date,
price: d1.price,
lastDay: 0,
color: d1.price < threshold ? 'red' : 'blue'
});
newData.push({
date: interpolatedDate,
price: interpolatedPrice,
lastDay: 0,
color: d1.price < threshold ? 'blue' : 'red'
});
newData.push({
date: interpolatedDate,
price: interpolatedPrice,
lastDay: 0,
color: d1.price < threshold ? 'red' : 'blue'
});
newData.push({
date: d2.date,
price: d2.price,
lastDay: 0,
color: d2.price < threshold ? 'red' : 'blue'
});
} else {
// No interpolation needed, just copy the original data point
newData.push({
date: d1.date,
price: d1.price,
lastDay: 0,
color: d1.price < threshold ? 'red' : 'blue'
});
}
}
// Add the last data point with the color and lastDay properties
const lastDataPoint = data[data.length - 1];
newData.push({
date: lastDataPoint.date,
price: lastDataPoint.price,
lastDay: 1,
color: lastDataPoint.price < threshold ? 'red' : 'blue'
});
const processedData = newData.map(d => {
return {
date: new Date(d.date),
price: d.price,
lastDay: d.lastDay,
color: d.color
};
});
// Test out new data source
console.log(processedData);
var thresholdX = threshold.toString();
var thresholdZ = threshold.toString();
console.log(thresholdX);
// Define the Vega-Lite specification
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {"values": processedData},
"height": 400,
"width": 400,
"view": {"stroke": null},
"encoding": {
"x": {"field": "date", "type": "temporal","title": null,
"axis": {
"tickCount": 10,
"labelAlign": "left",
"labelExpr": "[timeFormat(datum.value, '%d'), timeFormat(datum.value, '%d') == '01' ? timeFormat(datum.value, '%b') : '']",
"labelOffset": 4,
"labelPadding": -24,
"tickSize": 30,
"gridDash": {
"condition": {"test": "timeFormat(datum.value, '%d') == '01'", "value": []},
"value": [2,2]
},
"tickDash": {
"condition": {"test": "timeFormat(datum.value, '%d') == '01'", "value": []},
"value": [2,2]
}
}
},
"y": {"field": "price", "type": "quantitative", "impute": {"value": null},"title": null,
"scale": {"zero": false}
},
"color": {
"field": "color",
"type": "nominal",
"scale": {"domain": ["red", "blue"], "range": ["#EC685C", "#2A84EC"]},
"legend": null
},
"tooltip": [
{"field": "date", "type": "temporal"},
{"field": "price", "type": "quantitative"}
]
},
"layer": [
// layer for horizontal rule
{
"transform": [
{"calculate": thresholdX, "as": "threshold2"}
],
"mark": {
"type": "line",
"strokeDash": [2, 2],
"strokeWidth": 1
},
"encoding": {
"y": {"field": "threshold2", "type": "quantitative","title": null,
"scale": {"zero": false}
},
"tooltip": {"field": "threshold2", "type": "quantitative"},
"color": {"value": "black"}
}
},
// + fill
{
"transform": [
{filter: "datum.price >= " + thresholdX},
{"calculate": thresholdX, "as": "threshold2"}
],
"mark": {
"type": "area"
},
"encoding": {
"y2": {"field": "threshold2", "type": "quantitative","title": null
},
"color": {"value": "#2A84EC"},
"opacity": {"value": 0.3}
}
},
// - fill
{
"transform": [
{filter: "datum.price <= " + thresholdX},
{"calculate": thresholdX, "as": "threshold2"}
],
"mark": {
"type": "area"
},
"encoding": {
"y2": {"field": "threshold2", "type": "quantitative","title": null
},
"color": {"value": "#EC685C"},
"opacity": {"value": 0.3}
}
},
// layer for actual line
{
"mark": {
"type": "line",
"strokeWidth": 2
}
},
// layer for easy tooltip. Big hidden circles
{
"params": [
{
"name": "paintbrush",
"select": {"type": "point", "on": "mouseover", "nearest": true}
}
],
"mark": {"type": "circle", "tooltip": true},
"encoding": {
"size": {"value": 150},
"color": {"value": "transparent"}
}
},
// Layer for new text mark where lastDay equals 1
{
"transform": [{"filter": "datum.lastDay == 1"}],
"mark": {
"type": "text",
"align": "right",
"baseline": "middle",
"dx": 40,
"dy": -0,
"fontWeight": 500,
"fontSize": 16
},
"encoding": {
"x": {"field": "date", "type": "temporal", "axis": {"title": null}},
"y": {"field": "price", "type": "quantitative", "impute": {"value": null}, "title": null},
"text": {"field": "price", "type": "quantitative", "format": ".0f"}
}
},
// Layer for new text mark where lastDay equals 1
{
"transform": [{"filter": "datum.lastDay == 1"}],
"mark": {
"type": "circle"
},
"encoding": {
"size": {"value": 60}
}
}
],
"config": {
"legend": null,
"axis": {"grid": false},
"view": {"toolbar": false},
"renderer": "svg"
}
};
// Render the chart using Vega-Embed
const embedOpt = {"mode": "vega-lite", "actions": false};
vegaEmbed("#vis", spec, embedOpt);
</script>
</body>
</html>