Salary Ridgelines · A Distribution Survey
Nine overlapping salary distributions rendered as an antique cyanotype survey plate, built with Muze.
What you’re looking at
A ridgeline plot, also known as a joyplot, stacks partially overlapping density curves along a shared quantitative axis. The form is beloved for its compact, rhythmic overview of how many distributions shift and change shape. Its overlap and normalization can make exact comparisons harder, so it is less common in everyday business dashboards—but feels right at home in curated data stories.
Here, nine salary distributions overlap roughly in order of median salary. Each crest is a kernel-density estimate; the small baseline tick marks that role’s median and the full-height dotted rule marks the pooled median. Manager and Research Director are picked out in warm copper tones to draw the eye toward their skewed, high-salary distributions.
Each density is independently normalized. Ridge height describes the shape of a category’s distribution, not its number of employees. Hover a ridge for its sample size, median, quartiles, and densest salary band.
How it was built
The source’s fixed role order broadly follows median salary, while the reversed paint order preserves the intended overlap. Every ridge uses the same 200-dollar salary grid, role-tuned KDE bandwidths, and an edge taper before being independently normalized. Muze’s ranged-area setup uses Operators.share("Ridge Top", "Baseline"), with separate line, tick, and text layers; SVG patterns add the cyanotype hatching after render.
A custom physical polygon action uses each layer’s getDataBoundary hit test, maps into Muze’s built-in highlight behaviour, and drives the readout through a GenericSideEffect. Default hover, click, drag, and touch mappings are dissociated.
To point the same salary/role semantics at renamed Worksheet columns, change only ROLE_FIELD andSALARY_FIELD at the top of the JavaScript. Role values, ordering, and density tuning intentionally remain fixed to this example.
Take it with you
Paste these complete artifacts into Muze Studio, then load the CSV into a Worksheet. Change ROLE_FIELD and SALARY_FIELD only when the source columns are renamed.
JavaScript
Self-contained chart, KDE preparation, interaction, readout, and hatch injection.
Preview
const { muze, getDataFromSearchQuery, events } = viz;
const data = getDataFromSearchQuery();
const ROLE_FIELD = "Role";
const SALARY_FIELD = "Salary";
const RIDGE_READOUT_SIDE_EFFECT = "ridge-readout";
const RIDGE_HOVER_PHYSICAL_ACTION = "ridge-polygon-hover";
const MUTED_PATTERN_BASE_DEFAULT = "#062541";
const ROLE_ORDER = [
"Sales Representative",
"Laboratory Technician",
"Research Scientist",
"Human Resources",
"Sales Executive",
"Manufacturing Director",
"Healthcare Representative",
"Research Director",
"Manager",
];
const PAINT_ORDER = [...ROLE_ORDER].reverse();
const LABEL_X = 0;
const SALARY_MIN = 1000;
const SALARY_MAX = 20000;
const SALARY_STEP = 200;
const RIDGE_HEIGHT = 1.38;
const BASELINE_GAP = 0.74;
const ROLE_MEDIAN_TICK_HALF_PIXELS = 4;
const X_DOMAIN = [-3000, SALARY_MAX];
const Y_DOMAIN = [
0,
(ROLE_ORDER.length - 1) * BASELINE_GAP + RIDGE_HEIGHT + 0.95,
];
const X_TICKS = [5000, 10000, 15000, 20000];
const ROW_DETAIL_FIELDS = [
"Role",
"Role Label",
"Sample",
"Pooled Median Label",
];
const SMALL_CAPS_FONT = '"Cormorant SC", Georgia, serif';
const SERIF_FONT = '"Cormorant Garamond", Georgia, serif';
const ridgelineTheme = {
name: "ridgeline-plot-theme",
className: "ridgeline-plot-theme",
font: {
fontSize: "12px",
fontFamily: SERIF_FONT,
fontWeight: "400",
fontStyle: "normal",
components: {
axis: {
ticks: {
fontSize: "18px",
fontFamily: SMALL_CAPS_FONT,
fontWeight: "600",
fontStyle: "normal",
},
fields: {
Salary: {
ticks: {
fontSize: "18px",
fontFamily: SMALL_CAPS_FONT,
fontWeight: "600",
fontStyle: "normal",
},
},
},
},
textLayer: {
fontSize: "18px",
fontFamily: SMALL_CAPS_FONT,
fontWeight: "700",
fontStyle: "normal",
fields: {
"Role Label": {
fontSize: "18px",
fontFamily: SMALL_CAPS_FONT,
fontWeight: "700",
fontStyle: "normal",
},
"Pooled Median Label": {
fontSize: "20px",
fontFamily: SERIF_FONT,
fontWeight: "500",
fontStyle: "italic",
},
},
},
},
},
loadCSS: () => {},
};
const mutedRoleFill = "#66859b";
const mutedRoleInk = "#adc5d3";
const roleFill = {
"Sales Representative": mutedRoleFill,
"Laboratory Technician": mutedRoleFill,
"Research Scientist": mutedRoleFill,
"Human Resources": mutedRoleFill,
"Sales Executive": mutedRoleFill,
"Manufacturing Director": mutedRoleFill,
"Healthcare Representative": mutedRoleFill,
"Research Director": "#8d6144",
Manager: "#b0734a",
};
const roleInk = {
"Sales Representative": mutedRoleInk,
"Laboratory Technician": mutedRoleInk,
"Research Scientist": mutedRoleInk,
"Human Resources": mutedRoleInk,
"Sales Executive": mutedRoleInk,
"Manufacturing Director": mutedRoleInk,
"Healthcare Representative": mutedRoleInk,
"Research Director": "#e7ba9d",
Manager: "#f1d4c1",
};
const roleLabel = {
"Sales Representative": "Sales Rep.",
"Laboratory Technician": "Lab Tech.",
"Research Scientist": "R&D Scientist",
"Human Resources": "HR",
"Sales Executive": "Sales Exec.",
"Manufacturing Director": "Mfg. Director",
"Healthcare Representative": "Health Rep.",
"Research Director": "R&D Director",
Manager: "Manager",
};
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
const compactCurrency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 0,
});
const bandCurrency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
notation: "compact",
maximumFractionDigits: 1,
});
const ridgeSchema = [
{ name: "Role", type: "dimension", subtype: "categorical" },
{ name: "Role Label", type: "dimension", subtype: "categorical" },
{ name: "Sample", type: "dimension", subtype: "categorical" },
{ name: "Pooled Median Label", type: "dimension", subtype: "categorical" },
{
name: "Salary",
type: "measure",
subtype: "continuous",
defAggFn: "avg",
},
{ name: "Density", type: "measure", subtype: "continuous", defAggFn: "avg" },
{ name: "Baseline", type: "measure", subtype: "continuous", defAggFn: "avg" },
{ name: "Ridge Top", type: "measure", subtype: "continuous", defAggFn: "avg" },
{ name: "Role Median X", type: "measure", subtype: "continuous", defAggFn: "avg" },
{ name: "Pooled Median X", type: "measure", subtype: "continuous", defAggFn: "avg" },
];
async function buildViz(muze, data, mountId) {
const mount = document.getElementById(mountId);
if (!mount) {
throw new Error(`Salary ridgelines mount #${mountId} was not found.`);
}
await (document.fonts?.ready ?? Promise.resolve());
mount.innerHTML = `<main class="viz-panel salary-ridgelines" aria-label="Ridgeline plot example"><div class="ridge-readout salary-ridgelines__readout" aria-live="polite"></div><div class="chart-frame salary-ridgelines__frame"><div id="${mountId}-canvas" class="salary-ridgelines__canvas"></div></div></main>`;
const readout = mount.querySelector(".ridge-readout");
const chart = mount.querySelector(".salary-ridgelines__canvas");
const { DataModel } = muze;
const { GenericSideEffect } = muze.SideEffects.standards;
const salaryRows = rowsFromDataModel(data);
const groupedSalaries = groupSalariesByRole(salaryRows);
const pooledMedian = median(salaryRows.map((row) => row.salary));
const roleMedians = buildRoleMedians(groupedSalaries);
const ridgeRows = buildRidgelineRows(groupedSalaries, roleMedians, pooledMedian);
const roleSummaries = buildRoleSummaries(groupedSalaries, roleMedians, ridgeRows);
const ridgeData = new DataModel(DataModel.loadDataSync(ridgeRows, ridgeSchema));
const { share } = muze.Operators;
let activeRole = null;
const ridgeLayers = PAINT_ORDER.flatMap((role) => [
ridgeAreaLayer(role),
ridgeLineLayer(role),
roleMedianTickLayer(role),
]);
class RidgeReadoutSideEffect extends GenericSideEffect {
static formalName() {
return RIDGE_READOUT_SIDE_EFFECT;
}
static target() {
return "visual-unit";
}
apply(selectionSet, payload) {
setActiveRole(roleFromInteractionPayload(payload, selectionSet));
return this;
}
}
const ridgePolygonHover = (firebolt) => (targetEl) => {
const resetHover = () => {
firebolt.triggerPhysicalAction(RIDGE_HOVER_PHYSICAL_ACTION, {
criteria: null,
ridgeRole: null,
layerId: null,
metaData: {},
});
};
const dispatchHover = (...args) => {
const event = eventFromInteractionArgs(args);
if (!event) {
return;
}
const pos = muze.utils.getClientPoint(
firebolt.context.getDrawingContext().svgContainer,
event,
);
const boundary = ridgeBoundaryFromPosition(firebolt.context, pos.x, pos.y);
const role = roleFromAreaBoundary(boundary?.points);
const criteria = roleHighlightCriteria(role);
firebolt.triggerPhysicalAction(RIDGE_HOVER_PHYSICAL_ACTION, {
criteria,
target: criteria?.dimensions || null,
targetData: null,
ridgeRole: role,
position: pos,
layerId: boundary?.layerId || null,
metaData: boundary
? {
borderPoints: boundary.points,
borderId: boundary.id,
layerId: boundary.layerId,
}
: {},
});
};
targetEl
.on(`mouseover.${RIDGE_HOVER_PHYSICAL_ACTION}`, dispatchHover)
.on(`mousemove.${RIDGE_HOVER_PHYSICAL_ACTION}`, dispatchHover)
.on(`mouseout.${RIDGE_HOVER_PHYSICAL_ACTION}`, resetHover)
.on(`mouseleave.${RIDGE_HOVER_PHYSICAL_ACTION}`, resetHover);
};
function eventFromInteractionArgs(args) {
const currentWindowEvent = globalThis.event;
return (
args.find((arg) => Number.isFinite(arg?.clientX) && Number.isFinite(arg?.clientY)) ||
(Number.isFinite(currentWindowEvent?.clientX) &&
Number.isFinite(currentWindowEvent?.clientY)
? currentWindowEvent
: null) ||
muze.utils.getEvent()
);
}
renderRoleReadout(null);
const env = typeof muze === "function" ? muze() : muze;
const canvas = env
.canvas()
.data(ridgeData)
.rows([[share("Ridge Top", "Baseline")]])
.columns([{ field: "Salary", as: "continuous" }])
.layers([
...ridgeLayers,
{
mark: "tick",
className: "pooled-median-tick",
transform: { type: "identity" },
encodingTransform: keepVisibleTicks,
interactive: false,
encoding: {
x: { field: "Salary", as: "continuous", value: pooledMedianXPosition },
x0: { field: "Salary", as: "continuous", value: pooledMedianXPosition },
y: { field: "Baseline", value: plotTop },
y0: { field: "Baseline", value: plotBottom },
detail: ROW_DETAIL_FIELDS,
color: { value: () => "rgba(236, 244, 250, 0.72)" },
opacity: { value: pooledMedianOpacity },
},
transition: { disabled: true },
},
{
mark: "text",
className: "role-label-text",
transform: { type: "identity" },
encodingTransform: keepVisibleText,
interactive: false,
encoding: {
x: { field: "Salary", as: "continuous", value: roleLabelXPosition },
y: { field: "Baseline", value: roleLabelYPosition },
text: {
field: "Role Label",
textAnchor: "end",
alignmentBaseline: "baseline",
},
detail: ROW_DETAIL_FIELDS,
color: { value: roleLabelColor },
opacity: { value: roleLabelOpacity },
},
transition: { disabled: true },
},
{
mark: "text",
className: "pooled-median-label",
transform: { type: "identity" },
encodingTransform: keepVisibleText,
interactive: false,
encoding: {
x: { field: "Salary", as: "continuous", value: pooledMedianLabelXPosition },
y: { field: "Baseline", value: pooledMedianLabelYPosition },
text: {
field: "Pooled Median Label",
textAnchor: "start",
alignmentBaseline: "baseline",
},
detail: ROW_DETAIL_FIELDS,
color: { value: () => "#b9ccd9" },
opacity: { value: pooledMedianLabelOpacity },
},
transition: { disabled: true },
},
])
.config({
theme: ridgelineTheme,
legend: {
show: false,
color: {
fields: {
Role: {
ordering: { type: "custom", values: PAINT_ORDER },
},
},
},
},
border: { width: 0 },
gridLines: {
show: true,
x: { show: true },
y: { show: false },
color: "rgba(86, 119, 145, 0.28)",
zeroLineColor: "transparent",
transition: { disabled: true },
},
axes: {
x: {
domain: X_DOMAIN,
tickValues: X_TICKS,
nice: false,
showAxisName: false,
showAxisLine: false,
tickSize: 0,
tickFormat: ({ rawValue }) => {
const value = Array.isArray(rawValue) ? rawValue[0] : rawValue;
return compactCurrency.format(Number(value));
},
labels: { rotation: 0 },
ticks: {
labels: {
style: {
fill: "#a8bdcb",
},
},
},
},
y: {
domain: Y_DOMAIN,
nice: false,
show: false,
showAxisName: false,
showAxisLine: false,
tickSize: 0,
},
},
interaction: {
sideEffects: {
tooltip: { common: false },
},
highlight: {
sideEffects: {
tooltip: { enabled: false },
crossline: { enabled: false },
"axis-label-highlighter": { enabled: false },
[RIDGE_READOUT_SIDE_EFFECT]: {},
},
},
select: {
sideEffects: {
tooltip: { enabled: false },
},
},
},
});
muze.ActionModel.for(canvas)
.registerPhysicalActions({
[RIDGE_HOVER_PHYSICAL_ACTION]: ridgePolygonHover,
})
.registerSideEffects(RidgeReadoutSideEffect)
.registerPhysicalBehaviouralMap({
[RIDGE_HOVER_PHYSICAL_ACTION]: {
target: ".muze-tracker-group",
behaviours: ["highlight"],
},
})
.dissociateBehaviour(
["highlight", "hover"],
["select", "click"],
["select", "longtouch"],
["brush", "drag"],
["brush", "touchdrag"],
);
const mounted = canvas.mount(`#${mountId}-canvas`);
if (typeof canvas.on === "function") {
canvas.on("afterRendered", () => {
refreshSvgDecorations();
});
}
requestAnimationFrame(refreshSvgDecorations);
setTimeout(refreshSvgDecorations, 100);
function ridgeAreaLayer(role) {
return {
mark: "area",
className: `ridge-area ${roleClassName(role)}`,
interpolate: "catmullRom",
connectNullData: true,
transform: { type: "group", groupBy: ["Role"] },
encodingTransform: keepAreaForRole(role),
interaction: {
highlight: {
sideEffects: {
"area-anchors": { enabled: false },
"area-borders": { enabled: false },
},
},
},
encoding: {
x: { field: "Salary", as: "continuous" },
y: "Ridge Top",
y0: "Baseline",
detail: ROW_DETAIL_FIELDS,
color: { field: "Role", value: roleColor },
opacity: { value: 1 },
},
transition: { disabled: true },
};
}
function ridgeLineLayer(role) {
return {
mark: "line",
className: `ridge-line ${roleClassName(role)}`,
interpolate: "catmullRom",
connectNullData: true,
transform: { type: "group", groupBy: ["Role"] },
encodingTransform: keepAreaForRole(role),
interactive: false,
encoding: {
x: { field: "Salary", as: "continuous" },
y: "Ridge Top",
detail: ROW_DETAIL_FIELDS,
color: { value: () => roleInk[role] || "#dce6ee" },
opacity: { value: 1 },
},
transition: { disabled: true },
};
}
function roleMedianTickLayer(role) {
return {
mark: "tick",
className: `role-median-ticks ${roleClassName(role)}`,
transform: { type: "identity" },
encodingTransform: keepVisibleTicks,
interactive: false,
encoding: {
x: { field: "Salary", as: "continuous", value: roleMedianXPosition },
x0: { field: "Salary", as: "continuous", value: roleMedianXPosition },
y: { field: "Baseline", value: tickBottomFromBaseline },
y0: { field: "Baseline", value: tickTopFromBaseline },
detail: ROW_DETAIL_FIELDS,
color: { value: () => "rgba(242, 248, 252, 0.96)" },
opacity: { value: roleMedianOpacityFor(role) },
},
transition: { disabled: true },
};
}
function keepAreaForRole(role) {
return (points) => (points[0]?.data?.Role === role ? points : []);
}
function roleMedianOpacityFor(role) {
return ({ datum }) =>
datum.dataObj.Role === role && Number.isFinite(datum.dataObj["Role Median X"])
? 1
: 0;
}
function roleClassName(role) {
return `role-${role.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`;
}
function rowsFromDataModel(dataModel) {
const { data, schema } = dataModel.getData();
const fields = Object.fromEntries(schema.map((field, index) => [field.name, index]));
if (!(ROLE_FIELD in fields) || !(SALARY_FIELD in fields)) {
throw new Error(
`Salary ridgelines requires configured fields "${ROLE_FIELD}" and "${SALARY_FIELD}". Available fields: ${schema.map((field) => field.name).join(", ")}`,
);
}
const rows = data
.map((row) => ({
role: row[fields[ROLE_FIELD]],
salary: Number(row[fields[SALARY_FIELD]]),
}))
.filter((row) => row.role && Number.isFinite(row.salary));
if (!rows.length) {
throw new Error(
`Salary ridgelines found no valid ${ROLE_FIELD}/${SALARY_FIELD} rows.`,
);
}
return rows;
}
function groupSalariesByRole(rows) {
const grouped = new Map(ROLE_ORDER.map((role) => [role, []]));
rows.forEach(({ role, salary }) => {
if (!grouped.has(role)) {
grouped.set(role, []);
}
grouped.get(role).push(salary);
});
grouped.forEach((values) => values.sort((a, b) => a - b));
return grouped;
}
function buildRoleMedians(grouped) {
return new Map(
ROLE_ORDER.map((role) => [role, median(grouped.get(role) || [])]),
);
}
function buildRidgelineRows(grouped, roleMedians, pooledMedianValue) {
return ROLE_ORDER.flatMap((role, roleIndex) => {
const salaries = grouped.get(role) || [];
const baseline = roleIndex * BASELINE_GAP + 0.45;
const roleMedian = roleMedians.get(role);
const xs = [
LABEL_X,
...range(SALARY_MIN, SALARY_MAX, SALARY_STEP),
];
const bandwidth = bandwidthFor(role);
const densities = xs.map((salary) =>
salary < SALARY_MIN
? 0
: kernelDensity(salaries, salary, bandwidth) * edgeTaper(salary),
);
const maxDensity = Math.max(...densities) || 1;
return xs.map((salary, index) => {
const normalizedDensity = densities[index] / maxDensity;
const isMedianMarkerRow = index === 0;
return {
Role: role,
"Role Label": isMedianMarkerRow ? roleLabel[role] : null,
Sample: `${roleIndex}-${index}`,
"Pooled Median Label":
roleIndex === 0 && isMedianMarkerRow
? `pooled median, ${currency.format(pooledMedianValue)}`
: null,
Salary: salary,
Density: normalizedDensity,
Baseline: baseline,
"Ridge Top": baseline + normalizedDensity * RIDGE_HEIGHT,
"Role Median X": isMedianMarkerRow ? roleMedian : null,
"Pooled Median X": roleIndex === 0 && isMedianMarkerRow ? pooledMedianValue : null,
};
});
});
}
function buildRoleSummaries(grouped, medians, rows) {
return new Map(
ROLE_ORDER.map((role) => {
const values = grouped.get(role) || [];
const peakRow = rows
.filter((row) => row.Role === role && row.Salary >= SALARY_MIN)
.reduce((best, row) => (row.Density > best.Density ? row : best), { Density: -1 });
const peakCenter = Number.isFinite(peakRow.Salary) ? peakRow.Salary : medians.get(role);
return [
role,
{
role,
count: values.length,
median: medians.get(role),
q1: quantile(values, 0.25),
q3: quantile(values, 0.75),
peakStart: Math.max(SALARY_MIN, peakCenter - SALARY_STEP / 2),
peakEnd: Math.min(SALARY_MAX, peakCenter + SALARY_STEP / 2),
},
];
}),
);
}
function roleMedianXPosition(dataInfo, contextInfo) {
return markerXPosition("Role Median X", dataInfo, contextInfo);
}
function pooledMedianXPosition(dataInfo, contextInfo) {
return markerXPosition("Pooled Median X", dataInfo, contextInfo);
}
function markerXPosition(field, { translatedValue, datum }, { context }) {
const markerX = datum.dataObj[field];
return Number.isFinite(markerX)
? context.axes().x.getScaleValue(markerX)
: translatedValue;
}
function pooledMedianOpacity({ datum }) {
return Number.isFinite(datum.dataObj["Pooled Median X"]) ? 1 : 0;
}
function keepVisibleTicks(points) {
return points.filter((point) => Number(point.style.opacity) > 0);
}
function keepVisibleText(points) {
return points.filter((point) => Number(point.opacity) > 0 && hasText(point.text));
}
function hasText(value) {
return typeof value === "string" && value.trim() !== "" && value !== "_invalid" && value !== "Null";
}
function plotTop() {
return 0;
}
function plotBottom(_, { context }) {
return context.measurement().height;
}
function tickBottomFromBaseline({ translatedValue }) {
return translatedValue + ROLE_MEDIAN_TICK_HALF_PIXELS;
}
function tickTopFromBaseline({ translatedValue }) {
return translatedValue - ROLE_MEDIAN_TICK_HALF_PIXELS;
}
function roleLabelXPosition({ translatedValue }) {
return translatedValue - 14;
}
function roleLabelYPosition({ translatedValue }) {
return translatedValue - 5;
}
function roleLabelOpacity({ datum }) {
return hasText(datum.dataObj["Role Label"]) ? 0.96 : 0;
}
function roleLabelColor({ datum }) {
return roleInk[datum.dataObj.Role] || "#dce6ee";
}
function pooledMedianLabelXPosition(dataInfo, contextInfo) {
return pooledMedianXPosition(dataInfo, contextInfo) + 7;
}
function pooledMedianLabelYPosition() {
return 62;
}
function pooledMedianLabelOpacity({ datum }) {
return hasText(datum.dataObj["Pooled Median Label"]) ? 1 : 0;
}
function bandwidthFor(role) {
if (role === "Manager" || role === "Research Director") {
return 560;
}
if (role.includes("Director") || role === "Healthcare Representative") {
return 620;
}
if (role === "Sales Executive") {
return 700;
}
return 470;
}
function kernelDensity(values, x, bandwidth) {
if (!values.length) {
return 0;
}
const sum = values.reduce((total, value) => {
const z = (x - value) / bandwidth;
return total + Math.exp(-0.5 * z * z);
}, 0);
return sum / (values.length * bandwidth * Math.sqrt(2 * Math.PI));
}
function edgeTaper(value) {
const left = smoothStep((value - SALARY_MIN) / 900);
const right = smoothStep((SALARY_MAX - value) / 900);
return left * right;
}
function smoothStep(value) {
const t = Math.max(0, Math.min(1, value));
return t * t * (3 - 2 * t);
}
function median(values) {
const sorted = values.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
if (!sorted.length) {
return 0;
}
if (sorted.length % 2) {
return sorted[middle];
}
return (sorted[middle - 1] + sorted[middle]) / 2;
}
function quantile(values, percentile) {
if (!values.length) {
return 0;
}
const sorted = values.slice().sort((a, b) => a - b);
const index = (sorted.length - 1) * percentile;
const lower = Math.floor(index);
const upper = Math.ceil(index);
return sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower);
}
function range(min, max, step) {
const values = [];
for (let value = min; value <= max; value += step) {
values.push(value);
}
return values;
}
function roleColor(dataInfo) {
const role =
dataInfo?.pathFieldValueMap?.Role ||
dataInfo?.groupData?.[0]?.data?.Role ||
dataInfo?.groupData?.[0]?.dataObj?.Role ||
dataInfo?.datum?.data?.Role ||
dataInfo?.datum?.dataObj?.Role;
return roleFill[role] || "#8fa4b7";
}
function roleFromInteractionPayload(payload) {
if (!payload) {
return null;
}
if (Object.prototype.hasOwnProperty.call(payload, "ridgeRole")) {
return roleSummaries.has(payload.ridgeRole) ? payload.ridgeRole : null;
}
return roleFromAreaBoundary(payload.metaData?.borderPoints);
}
function roleHighlightCriteria(role) {
return roleSummaries.has(role) ? { dimensions: [["Role"], [role]] } : null;
}
function ridgeBoundaryFromPosition(context, x, y) {
const bounds = context
.layers()
.map((layer) => layer.getDataBoundary(x, y))
.filter(Boolean);
return bounds[bounds.length - 1] || null;
}
function roleFromAreaBoundary(borderPoints) {
const point = Array.isArray(borderPoints)
? borderPoints.find((value) => value?.data?.Role || value?.dataObj?.Role)
: borderPoints?.data?.find((value) => value?.data?.Role || value?.dataObj?.Role);
const role = point?.data?.Role || point?.dataObj?.Role;
return roleSummaries.has(role) ? role : null;
}
function setActiveRole(role) {
if (role === activeRole) {
return;
}
activeRole = role;
renderRoleReadout(role);
applyRoleFocus(role);
}
function renderRoleReadout(role) {
if (!readout) {
return;
}
const summary = roleSummaries.get(role);
if (!summary) {
readout.innerHTML = `<span>hover a role to inspect it · ${salaryRows.length.toLocaleString()} employees across ${ROLE_ORDER.length} roles · pooled median <span class="m">${currency.format(pooledMedian)}</span></span>`;
return;
}
readout.innerHTML = `
<span>
<b>${escapeHtml(summary.role)}</b> · n = ${summary.count.toLocaleString()} ·
median <span class="m">${currency.format(summary.median)}</span> ·
quartiles <span class="p">${currency.format(summary.q1)}–${currency.format(summary.q3)}</span> ·
densest band ${bandCurrency.format(summary.peakStart)}–${bandCurrency.format(summary.peakEnd)}
</span>
`;
}
function applyRoleFocus(role) {
const activeClass = role ? roleClassName(role) : null;
chart.classList.toggle("is-reading-role", Boolean(activeClass));
ROLE_ORDER.forEach((roleName) => {
const className = roleClassName(roleName);
const isActive = className === activeClass;
chart
.querySelectorAll(
`.ridge-area.${className}, .ridge-line.${className}, .role-median-ticks.${className}`,
)
.forEach((element) => setFocusClasses(element, activeClass, isActive));
});
const labelRoleByText = new Map(
ROLE_ORDER.map((roleName) => [roleLabel[roleName], roleClassName(roleName)]),
);
chart.querySelectorAll(".role-label-text text").forEach((element) => {
const labelRoleClass = labelRoleByText.get(element.textContent.trim());
setFocusClasses(element, activeClass, labelRoleClass === activeClass);
});
}
function setFocusClasses(element, activeClass, isActive) {
element.classList.toggle("is-active", Boolean(activeClass) && isActive);
element.classList.toggle("is-dimmed", Boolean(activeClass) && !isActive);
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function refreshSvgDecorations() {
installRidgePatterns(chart);
applyRidgeColorFills(chart);
applyRoleFocus(activeRole);
}
function installRidgePatterns(chart) {
const svg = chart.querySelector("svg");
if (!svg) {
return;
}
const svgNamespace = "http://www.w3.org/2000/svg";
let defs = svg.querySelector("defs.ridge-pattern-defs");
if (!defs) {
defs = document.createElementNS(svgNamespace, "defs");
defs.setAttribute("class", "ridge-pattern-defs");
svg.prepend(defs);
}
ROLE_ORDER.forEach((role) => {
const id = ridgePatternId(role);
let pattern = defs.querySelector(`#${id}`);
if (!pattern) {
pattern = document.createElementNS(svgNamespace, "pattern");
pattern.setAttribute("id", id);
pattern.setAttribute("patternUnits", "userSpaceOnUse");
pattern.setAttribute("width", "4.5");
pattern.setAttribute("height", "4.5");
const base = document.createElementNS(svgNamespace, "rect");
base.setAttribute("class", "ridge-pattern-base");
base.setAttribute("width", "4.5");
base.setAttribute("height", "4.5");
base.setAttribute("opacity", "0.98");
const shadow = document.createElementNS(svgNamespace, "rect");
shadow.setAttribute("width", "4.5");
shadow.setAttribute("height", "4.5");
shadow.setAttribute("fill", "#0b2238");
shadow.setAttribute("opacity", "0.04");
const hatch = document.createElementNS(svgNamespace, "path");
hatch.setAttribute("class", "ridge-pattern-hatch");
hatch.setAttribute("d", "M 0 4.5 L 4.5 0");
hatch.setAttribute("stroke-width", "0.78");
hatch.setAttribute("opacity", "0.5");
pattern.append(base, shadow, hatch);
defs.append(pattern);
}
const patternBaseFill = ["Research Director", "Manager"].includes(role)
? roleFill[role]
: MUTED_PATTERN_BASE_DEFAULT;
pattern.querySelector(".ridge-pattern-base")?.setAttribute("fill", patternBaseFill);
pattern
.querySelector(".ridge-pattern-hatch")
?.setAttribute("stroke", roleInk[role]);
});
}
function applyRidgeColorFills(chart) {
ROLE_ORDER.forEach((role) => {
const layer = chart.querySelector(`.ridge-area.${roleClassName(role)}`);
const paths = Array.from(layer?.querySelectorAll("path") || []);
if (!paths.length) {
return;
}
paths.forEach((path) => {
path.style.setProperty("fill", `url(#${ridgePatternId(role)})`, "important");
path.style.setProperty("stroke", "none", "important");
path.style.setProperty("stroke-width", "0", "important");
});
});
}
function ridgePatternId(role) {
return `ridge-hatch-${roleClassName(role)}`;
}
return {
dispose() {
try {
mounted.dispose();
} catch (_) {}
mount.replaceChildren();
},
};
}
buildViz(muze, data, "chart").then(() => events.emitRenderCompletedEvent());
CSS
Complete, #chart-scoped cyanotype survey-plate styling.
Preview
@import url("https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Cormorant+SC:wght@500;600;700&display=swap");
html, body { margin: 0; width: 100%; height: 100%; }
#chart { width: 100%; height: 100%; min-width: 900px; }
#chart {
--bg: #0b2238;
--ink: #d9e5ef;
--muted: #a8bdcb;
--ridge: #66859b;
--grid: rgba(86, 119, 145, 0.28);
background: var(--bg);
}
#chart .salary-ridgelines {
box-sizing: border-box;
height: 100%;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
padding: 24px 32px 28px;
overflow: visible;
background: radial-gradient(circle at 76% 18%, rgba(45, 92, 128, 0.2), transparent 30%), linear-gradient(180deg, #081b2e 0%, var(--bg) 100%);
color: var(--ink);
font-family: "Cormorant Garamond", Georgia, "Times New Roman", serif;
}
#chart .ridge-readout {
min-height: 34px;
margin: 0 0 12px 2px;
color: var(--muted);
font-size: 22px;
font-style: italic;
line-height: 1.45;
}
#chart .ridge-readout b {
color: var(--ink);
font-style: normal;
font-weight: 600;
letter-spacing: 0.02em;
}
#chart .ridge-readout .m {
color: #d9e5ef;
font-style: normal;
}
#chart .ridge-readout .p {
color: #c2d2dd;
font-style: normal;
}
#chart .chart-frame {
position: relative;
box-sizing: content-box;
width: auto;
height: auto;
min-height: 0;
padding: 25px 15px 15px;
overflow: visible;
}
#chart .chart-frame::before,
#chart .chart-frame::after {
content: "";
position: absolute;
pointer-events: none;
z-index: 10;
}
#chart .chart-frame::before {
inset: 0;
border: 3.2px solid rgba(220, 233, 242, 0.28);
}
#chart .chart-frame::after {
inset: 8px;
border: 0.5px solid rgba(220, 233, 242, 0.28);
}
#chart .salary-ridgelines__canvas {
width: 100%;
height: 100%;
min-height: 0;
overflow: visible;
}
#chart .salary-ridgelines__canvas svg {
overflow: visible;
}
#chart .salary-ridgelines__canvas .muze-layer-area.ridge-area path {
fill-opacity: 1 !important;
stroke: none !important;
stroke-width: 0 !important;
}
#chart .salary-ridgelines__canvas .muze-layer-line.ridge-line {
pointer-events: none;
}
#chart .salary-ridgelines__canvas .muze-layer-line.ridge-line path {
fill: none !important;
fill-opacity: 0 !important;
stroke-width: 1.7px !important;
stroke-linecap: round !important;
stroke-linejoin: round !important;
}
#chart .salary-ridgelines__canvas .muze-layer-area.ridge-area,
#chart .salary-ridgelines__canvas .muze-layer-line.ridge-line,
#chart .salary-ridgelines__canvas .muze-layer-tick.role-median-ticks,
#chart .salary-ridgelines__canvas .role-label-text text {
transition: opacity 160ms ease;
}
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-area.ridge-area.is-dimmed,
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-line.ridge-line.is-dimmed,
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-tick.role-median-ticks.is-dimmed {
opacity: 0.34 !important;
}
#chart .salary-ridgelines__canvas.is-reading-role .role-label-text text.is-dimmed {
opacity: 0.46 !important;
}
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-area.ridge-area.is-active,
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-line.ridge-line.is-active,
#chart .salary-ridgelines__canvas.is-reading-role .muze-layer-tick.role-median-ticks.is-active,
#chart .salary-ridgelines__canvas.is-reading-role .role-label-text text.is-active {
opacity: 1 !important;
}
#chart .salary-ridgelines__canvas .muze-layer-area.ridge-area path.area-borders {
fill: none !important;
}
#chart .salary-ridgelines__canvas .muze-layer-tick.role-median-ticks,
#chart .salary-ridgelines__canvas .muze-layer-tick.pooled-median-tick {
pointer-events: none;
}
#chart .salary-ridgelines__canvas .muze-layer-tick.role-median-ticks path {
stroke: rgba(242, 248, 252, 0.96) !important;
stroke-width: 1.35px !important;
stroke-linecap: round !important;
}
#chart .salary-ridgelines__canvas .muze-layer-tick.pooled-median-tick path {
stroke: rgba(236, 244, 250, 0.72) !important;
stroke-width: 1.2px !important;
stroke-dasharray: 1 5 !important;
stroke-linecap: round !important;
}
#chart .salary-ridgelines__canvas .muze-layer-text {
pointer-events: none;
}
#chart .salary-ridgelines__canvas .muze-axis-name {
display: none !important;
}
#chart .salary-ridgelines__canvas .muze-ticks-Salary {
fill: #a8bdcb !important;
}
#chart .salary-ridgelines__canvas .muze-tick-lines {
stroke: transparent !important;
}
#chart .salary-ridgelines__canvas .muze-axis-grid-lines-x path {
stroke: var(--grid) !important;
stroke-width: 1px !important;
stroke-dasharray: 1 6 !important;
stroke-linecap: round !important;
}
#chart .salary-ridgelines__canvas .muze-axis path,
#chart .salary-ridgelines__canvas .muze-axis line {
stroke: rgba(138, 160, 179, 0.35) !important;
}
#chart .salary-ridgelines__canvas .muze-grid-lines line,
#chart .salary-ridgelines__canvas .muze-grid-line,
#chart .salary-ridgelines__canvas [class*="grid-line"] line {
stroke: var(--grid) !important;
stroke-dasharray: 1 6 !important;
}
#chart,
#chart .salary-ridgelines {
min-width: 1150px;
}
#chart .salary-ridgelines__canvas {
min-width: 1050px;
}
#chart .salary-ridgelines__canvas .role-label-text text {
font-family: "Cormorant SC", Georgia, serif !important;
font-size: 18px !important;
font-style: normal !important;
font-weight: 700 !important;
}
#chart .salary-ridgelines__canvas .pooled-median-label text {
font-family: "Cormorant Garamond", Georgia, serif !important;
font-size: 20px !important;
font-style: italic !important;
font-weight: 500 !important;
}
HTML
The Muze Studio mount element.
Preview
<div id="chart"></div>