{
const brush = vl.selectInterval()
.encodings(['x']);
const timelineOverview = vl.markBar({opacity: 0.7})
.data(ai_jobs)
.params(brush)
.transform(
vl.timeUnit("yearmonth", "posting_date").as("date"),
vl.aggregate(vl.count().as("job_count"))
.groupby(["date"])
)
.encode(
vl.x().fieldT("date")
.title(null)
.axis({
labels: false,
}),
vl.y().fieldQ("job_count")
.title("Number of Job Postings"),
vl.color().value("#4682B4"),
vl.tooltip([
{field: "date", type: "temporal", title: "Month", format: "%B %Y"},
{field: "job_count", type: "quantitative", title: "Job Postings"}
])
)
.width(700)
.height(100);
const detailView = vl.markCircle()
.data(ai_jobs)
.transform(
vl.timeUnit("yearmonth", "posting_date").as("date"),
vl.aggregate(
vl.count().as("size_count"),
vl.mean("salary_usd").as("avg_salary")
).groupby(["date", "experience_level", "company_size"]),
vl.window([vl.rank().as("size_rank")])
.sort([{field: "size_count", order: "descending"}])
.groupby(["date", "experience_level"]),
vl.filter("datum.size_rank === 1")
)
.encode(
vl.x().fieldT("date")
.title("Date Posted")
.axis({
format: "%b %Y",
labelAngle: -45,
labelPadding: 10
}),
vl.y().fieldO("experience_level")
.title("Experience Level")
.scale({
domain: ["EN", "MI", "SE", "EX"]
})
.axis({
labelExpr: "datum.value == 'EN' ? 'EN (Entry)' : datum.value == 'MI' ? 'MI (Mid)' : datum.value == 'SE' ? 'SE (Senior)' : 'EX (Executive)'",
labelPadding: 20,
labelOffset: 5,
maxExtent: 150,
labelLimit: 120
}),
vl.size().fieldQ("avg_salary")
.title("Average Salary (USD)")
.scale({range: [50, 800]})
.legend({format: "$,.0f"}),
vl.color().fieldO("company_size")
.title("Most Frequent Company Size")
.scale({
domain: ["S", "M", "L"],
range: ["#FFB6C1", "#DDA0DD", "#98FB98"]
})
.legend({
labelExpr: "datum.value == 'S' ? 'Small (<50)' : datum.value == 'M' ? 'Medium (50-250)' : 'Large (>250)'"
}),
// Brush opasity selection
vl.opacity()
.if(brush, vl.value(0.9)) // Full opacity for selected
.value(0.15), // Low opacity for not selected
vl.stroke().value("white"),
vl.strokeWidth().value(1),
vl.tooltip([
{field: "date", type: "temporal", title: "Date Posted", format: "%B %Y"},
{field: "avg_salary", type: "quantitative", title: "Average Salary", format: "$,.0f"},
{field: "size_count", type: "quantitative", title: "Jobs with This Company Size"},
{field: "experience_level", type: "nominal", title: "Experience Level"},
{field: "company_size", type: "nominal", title: "Most Common Company Size"}
])
)
.width(700)
.height(300)
.resolve({scale: {y: "independent"}});
return vl.vconcat(timelineOverview, detailView)
.spacing(15)
.title("Interactive Job Market Analysis: Brush Timeline to Explore Details")
.render();
}