Churn by Price · Diverging Histogram
A diverging binned histogram that compares retained and churned customers across monthly-charge bands.
What you’re looking at
This histogram groups 7,043 customer records by monthly charge, using pre-binned five-dollar ranges. Each horizontal bar is normalized within its churn cohort: the churned-customer distribution extends left, and the retained-customer distribution extends right.
The population is mostly retained customers — 5,174 stayed and 1,869 churned — and those two cohorts are not distributed the same way across price. The same binned counts show customers under $30/month churn at about 9%, while the $70–$100 bands are much hotter, peaking near 41% in the $70–$75 bin.
The shape makes the pricing story visible without a separate aggregation table: low-priced customers are numerous and sticky; mid-to-high priced customers carry a heavier churn mix. The diverging layout makes the two cohort distributions easy to compare because both sides use the same centred percentage scale.
How it was built
The live sample stores one row per price bin and churn value. The ChargeBinStart value is the bin start, and its DataModel schema declares it as a binned dimension using binSize: 5. Each Muze canvas passes that field to rows() as { field: "ChargeBinStart", as: "continuous" }, so the vertical axis behaves like a quantitative binned scale with low values at the bottom and high values at the top. Customers is the count of customers in that bin.
The JavaScript mirrors the standalone Muze example: it groups the source DataModel by Churn to get cohort totals, uses calculateVariable() to add a Customer Share measure, then selects Yes rows for the left histogram and No rows for the right histogram. A third Muze canvas in the middle renders the binned monthly-charge labels.
The diverging trick is the sign of that calculated measure: churned shares are negative, retained shares are positive, and the x-axis formats tick labels with absolute values so both sides read as percentages. The center spine groups the signed shares by bin and colors each bin by which cohort over-indexes there.
To rebuild this with your own customer data, copy the JavaScript, CSS, and HTML below into Muze Studio and load the CSV into a Worksheet. If your column names or bin width differ, update the constants at the top of the JavaScript.
Take it with you
Paste each piece into the matching Muze Studio panel and load the CSV into a ThoughtSpot Worksheet. The JavaScript rebuilds ChargeBinStart as a binned dimension, computes each bin’s percentage share within its churn cohort, and creates the three-canvas diverging histogram shell inside the HTML mount.
JavaScript
Field names, churn values, and bin size are hoisted to constants at the top. The snippet rebuilds the binned DataModel, computes within-cohort shares, and mounts three Muze canvases.
Preview
const { muze, getDataFromSearchQuery } = viz;
// Field names, churn values, and bin size — change these to match your dataset.
const CHARGE_BIN = "ChargeBinStart";
const CHURN = "Churn";
const CUSTOMERS = "Customers";
const CUSTOMER_SHARE = "Customer Share";
const CHURNED_VALUE = "Yes";
const RETAINED_VALUE = "No";
const BIN_SIZE = 5;
const BIN_DETAILS_EFFECT = "churn-bin-details";
const COLORS = {
churned: "#35b5c0",
churnedAccent: "#52e4ef",
retained: "#925d20",
retainedAccent: "#ffb24a",
center: "#172334",
};
const FEATURED_BINS = {
churned: {
binStart: 70,
label: "CHURNED",
className: "churn-by-price-binned__annotation--churned",
labelPlacement: { anchors: ["outside-top"] },
},
retained: {
binStart: 20,
label: "RETAINED",
className: "churn-by-price-binned__annotation--retained",
labelPlacement: { anchors: ["outside-top"] },
},
};
const CENTER_DOMINANCE_COLOR_DOMAIN = [-0.09, 0, 0.15];
const CHURNED_SHARE_HEADER = "< share of CHURNED customers";
const RETAINED_SHARE_HEADER = "share of RETAINED customers >";
const CENTER_SHARE_HEADER = "\u00A0";
const SHARE_HEADER_SPACER_HEIGHT = 38;
const SHARE_HEADER_TITLE_CONFIG = {
align: "center",
padding: 2,
maxLines: 1,
};
const DISABLED_TRANSITION_CONFIG = { disabled: true };
const ANNOTATION_FONT_FAMILY = '"Oswald", sans-serif';
const ANNOTATION_FONT_SIZE = "16px";
const ANNOTATION_FONT_WEIGHT = "600";
const ANNOTATION_FONT_STYLE = "normal";
const ANNOTATION_FONT_LOAD_TIMEOUT = 2000;
const currencyFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
const percentFormatter = new Intl.NumberFormat("en-US", {
style: "percent",
maximumFractionDigits: 0,
});
const integerFormatter = new Intl.NumberFormat("en-US");
const { DataModel } = muze;
const data = buildRootDataModel(DataModel, getDataFromSearchQuery());
buildViz(muze, data, "chart");
function waitForAnnotationFont() {
const fontLoad = document.fonts
.load(
ANNOTATION_FONT_WEIGHT + " " + ANNOTATION_FONT_SIZE + " " + ANNOTATION_FONT_FAMILY,
"CHURNEDRETAINED",
)
.catch(() => {});
return Promise.race([
fontLoad,
new Promise((resolve) =>
setTimeout(resolve, ANNOTATION_FONT_LOAD_TIMEOUT),
),
]);
}
function createAnnotationTheme(muze) {
const baseTheme = (muze.Themes && muze.Themes.MuzeLight) || {};
const baseFont = baseTheme.font || {};
const baseComponents = baseFont.components || {};
return {
...baseTheme,
name: "churn-by-price-oswald",
font: {
...baseFont,
components: {
...baseComponents,
textLayer: {
...(baseComponents.textLayer || {}),
fontFamily: ANNOTATION_FONT_FAMILY,
fontSize: ANNOTATION_FONT_SIZE,
fontWeight: ANNOTATION_FONT_WEIGHT,
fontStyle: ANNOTATION_FONT_STYLE,
},
},
},
};
}
function buildRootDataModel(DataModel, answerDataModel) {
// Muze Studio Answers expose the Worksheet as a DataModel. Rebuild just the
// three fields this visualization needs so ChargeBinStart is explicitly a
// binned dimension, matching the standalone Muze example.
const answerData = answerDataModel.getData();
const chargeBinIndex = answerData.schema.findIndex((field) => field.name === CHARGE_BIN);
const churnIndex = answerData.schema.findIndex((field) => field.name === CHURN);
const customersIndex = answerData.schema.findIndex((field) => field.name === CUSTOMERS);
const rows = answerData.data.map((row) => ({
[CHARGE_BIN]: Number(row[chargeBinIndex]),
[CHURN]: row[churnIndex],
[CUSTOMERS]: Number(row[customersIndex]),
}));
const schema = [
{
name: CHARGE_BIN,
type: "dimension",
subtype: "binned",
binSize: BIN_SIZE,
format: (value) => currencyFormatter.format(value),
},
{ name: CHURN, type: "dimension", subtype: "categorical" },
{
name: CUSTOMERS,
type: "measure",
subtype: "continuous",
defAggFn: "sum",
},
];
return new DataModel(DataModel.loadDataSync(rows, schema));
}
function renderShell(mountEl, ids) {
mountEl.innerHTML =
'<div class="churn-by-price-binned" role="img" aria-label="Diverging Muze histogram of churned and retained customer distributions by monthly charge bin">' +
'<div id="' + ids.detailId + '" class="churn-by-price-binned__interaction-detail" aria-live="polite"></div>' +
'<div id="' + ids.leftId + '" class="churn-by-price-binned__canvas churn-by-price-binned__histogram churn-by-price-binned__histogram--churned"></div>' +
'<div id="' + ids.centerId + '" class="churn-by-price-binned__canvas churn-by-price-binned__labels churn-by-price-binned__labels--center"></div>' +
'<div id="' + ids.rightId + '" class="churn-by-price-binned__canvas churn-by-price-binned__histogram churn-by-price-binned__histogram--retained"></div>' +
'</div>';
}
function getTotalsByChurn(DataModel, rootData) {
const churnTotalsData = rootData.groupBy(
[CHURN],
[{ field: CUSTOMERS, aggn: DataModel.AggregationFunctions.SUM }],
);
const churnValues = churnTotalsData.getField(CHURN).data();
const customerTotals = churnTotalsData.getField(CUSTOMERS).data();
return Object.fromEntries(
churnValues.map((churn, index) => [churn, customerTotals[index]]),
);
}
function buildCustomerShareData(DataModel, rootData, totalsByChurn) {
return rootData.calculateVariable(
{
name: CUSTOMER_SHARE,
type: "measure",
subtype: "continuous",
defAggFn: "sum",
},
[CHURN, CUSTOMERS],
(churn, customers) => {
const share = customers / totalsByChurn[churn];
return churn === CHURNED_VALUE ? -share : share;
},
);
}
function getShareDomainMax(customerShareData) {
const maxShare = Math.max(
...customerShareData.getField(CUSTOMER_SHARE).domain().map(Math.abs),
);
return Math.max(0.2, Math.ceil(maxShare * 20) / 20);
}
function buildBinDominanceData(DataModel, customerShareData) {
return customerShareData.groupBy(
[CHARGE_BIN],
[{ field: CUSTOMER_SHARE, aggn: DataModel.AggregationFunctions.SUM }],
);
}
function getDataModelFieldValue(dataModel, fieldName) {
const data = dataModel && dataModel.getData && dataModel.getData();
const row = data && data.data && data.data[0];
if (!row) {
return undefined;
}
if (!Array.isArray(row)) {
return row[fieldName];
}
const fieldIndex = data.schema.findIndex((field) => field.name === fieldName);
return fieldIndex === -1 ? undefined : row[fieldIndex];
}
function buildBinDetailsByStart(customerShareData) {
const data = customerShareData.getData();
const fieldIndexByName = Object.fromEntries(
data.schema.map((field, index) => [field.name, index]),
);
const binDetailsByStart = new Map();
data.data.forEach((row) => {
const binStart = Number(
normalizeBinValue(row[fieldIndexByName[CHARGE_BIN]]),
);
const churn = row[fieldIndexByName[CHURN]];
const customers = Number(row[fieldIndexByName[CUSTOMERS]] || 0);
const share = Math.abs(Number(row[fieldIndexByName[CUSTOMER_SHARE]] || 0));
const detail = binDetailsByStart.get(binStart) || {
binStart,
churnedCustomers: 0,
retainedCustomers: 0,
churnedShare: 0,
retainedShare: 0,
};
if (churn === CHURNED_VALUE) {
detail.churnedCustomers = customers;
detail.churnedShare = share;
} else if (churn === RETAINED_VALUE) {
detail.retainedCustomers = customers;
detail.retainedShare = share;
}
binDetailsByStart.set(binStart, detail);
});
return binDetailsByStart;
}
function normalizeBinValue(rawValue) {
return Array.isArray(rawValue) ? rawValue[0] : rawValue;
}
function formatChargeBin(rawValue) {
return currencyFormatter.format(normalizeBinValue(rawValue));
}
function formatChargeBinRange(binStart) {
return currencyFormatter.format(binStart) + "–" + currencyFormatter.format(binStart + BIN_SIZE);
}
function formatFeaturedBinLabel(rawValue, featuredBin) {
return Number(normalizeBinValue(rawValue)) === featuredBin.binStart
? featuredBin.label
: "";
}
function getRowBinStart(rowData) {
return Number(normalizeBinValue(rowData?.[CHARGE_BIN]));
}
function getDatumBinStart(datum) {
return Number(
normalizeBinValue(datum?.dataObj?.[CHARGE_BIN] ?? datum?.y ?? datum?.x),
);
}
function isFeaturedBin(rowData, featuredBin) {
return getRowBinStart(rowData) === featuredBin.binStart;
}
function getFeaturedBinClassName(datum, featuredBin) {
return getDatumBinStart(datum) === featuredBin.binStart
? "churn-by-price-binned__annotation " + featuredBin.className
: "";
}
function getLabelSpineBinClassName(datum) {
const binStart = getDatumBinStart(datum);
if (binStart === FEATURED_BINS.churned.binStart) {
return "churn-by-price-binned__label-spine-bin churn-by-price-binned__label-spine-bin--churned";
}
if (binStart === FEATURED_BINS.retained.binStart) {
return "churn-by-price-binned__label-spine-bin churn-by-price-binned__label-spine-bin--retained";
}
return "";
}
function createDetailSpan(className, text) {
const span = document.createElement("span");
span.className = "churn-by-price-binned__interaction-detail-" + className;
span.textContent = text;
return span;
}
function renderDefaultBinDetails(detailEl) {
detailEl.classList.add("churn-by-price-binned__interaction-detail--idle");
detailEl.replaceChildren(
createDetailSpan("placeholder", "Hover a bar to inspect a price bin"),
);
}
function renderBinDetails(detailEl, detail) {
if (!detail) {
renderDefaultBinDetails(detailEl);
return;
}
const totalCustomers = detail.churnedCustomers + detail.retainedCustomers;
detailEl.classList.remove("churn-by-price-binned__interaction-detail--idle");
detailEl.replaceChildren(
createDetailSpan("bin", formatChargeBinRange(detail.binStart)),
createDetailSpan("separator", "·"),
createDetailSpan(
"churned",
"CHURNED " + percentFormatter.format(detail.churnedShare) + " (" + integerFormatter.format(detail.churnedCustomers) + ")",
),
createDetailSpan("separator", "·"),
createDetailSpan(
"retained",
"RETAINED " + percentFormatter.format(detail.retainedShare) + " (" + integerFormatter.format(detail.retainedCustomers) + ")",
),
createDetailSpan("separator", "·"),
createDetailSpan("total", integerFormatter.format(totalCustomers) + " customers"),
);
}
function createBinDetailsSideEffect(muze, detailEl, binDetailsByStart) {
const GenericSideEffect = muze.SideEffects.standards.GenericSideEffect;
return class BinDetailsSideEffect extends GenericSideEffect {
static formalName() {
return BIN_DETAILS_EFFECT;
}
static target() {
return "visual-group";
}
apply(selectionSet, payload) {
if (payload && payload.criteria === null) {
renderDefaultBinDetails(detailEl);
return this;
}
const binStart = Number(
normalizeBinValue(
getDataModelFieldValue(
selectionSet && selectionSet.entrySet && selectionSet.entrySet.model,
CHARGE_BIN,
),
),
);
renderBinDetails(detailEl, binDetailsByStart.get(binStart));
return this;
}
};
}
function registerBinDetailsSideEffect(muze, canvas, BinDetailsSideEffect) {
muze.ActionModel.for(canvas).registerSideEffects(BinDetailsSideEffect);
}
function getInteractionDetailConfig() {
return {
highlight: {
sideEffects: {
tooltip: { enabled: false },
crossline: { enabled: false },
"axis-label-highlighter": { enabled: false },
[BIN_DETAILS_EFFECT]: {},
},
},
select: {
sideEffects: {
tooltip: { enabled: false },
"plot-highlighter": { enabled: false },
},
},
brush: {
sideEffects: {
selectionBox: { enabled: false },
"plot-highlighter": { enabled: false },
},
},
};
}
function getSelectionSilentBarLayerConfig() {
return {
select: {
target: {
layer: {
highlight: {
sideEffects: {
"plot-highlighter": { enabled: false },
},
},
},
},
sideEffects: {
"plot-highlighter": { enabled: false },
},
},
brush: {
target: {
layer: {
highlight: {
sideEffects: {
"plot-highlighter": { enabled: false },
},
},
},
},
sideEffects: {
"plot-highlighter": { enabled: false },
},
},
};
}
function getCenterBarLayerConfig() {
const config = getSelectionSilentBarLayerConfig();
config.highlight = {
sideEffects: {
"plot-highlighter": { enabled: false },
},
};
return config;
}
function disablePointerSelection(muze, canvas) {
canvas.once("animationEnd", () => {
muze.ActionModel.for(canvas).dissociateBehaviour(
["select", "click"],
["brush", "drag"],
["select", "longtouch"],
["brush", "touchdrag"],
);
});
}
function mountHistogram(options) {
const canvas = options.env
.canvas()
.data(options.data)
.rows([{ field: CHARGE_BIN, as: "continuous" }])
.columns([options.measure])
.layers([
{
mark: "bar",
className: options.layerClassName,
individualClassName: (datum) =>
getFeaturedBinClassName(datum, options.featuredBin),
transition: DISABLED_TRANSITION_CONFIG,
encoding: {
color: { value: () => options.fill },
text: {
field: CHARGE_BIN,
filter: ({ rowData }) =>
isFeaturedBin(rowData, options.featuredBin),
formatter: ({ rawValue }) =>
formatFeaturedBinLabel(rawValue, options.featuredBin),
labelPlacement: options.featuredBin.labelPlacement,
textAnchor: options.featuredBin.textAnchor || "middle",
removeCollision: false,
},
},
},
])
.config({
theme: options.annotationTheme,
axes: {
x: {
domain: options.domain,
showAxisName: false,
showAxisLine: false,
numberOfTicks: 6,
nice: false,
transition: DISABLED_TRANSITION_CONFIG,
tickFormat: ({ rawValue }) =>
percentFormatter.format(Math.abs(rawValue)),
},
y: { show: false, transition: DISABLED_TRANSITION_CONFIG },
},
border: { width: 0 },
gridLines: { transition: DISABLED_TRANSITION_CONFIG },
gridBands: { transition: DISABLED_TRANSITION_CONFIG },
scrollBar: {
buttons: { show: false },
thickness: 4,
},
interaction: getInteractionDetailConfig(),
})
.title(options.shareTitle, SHARE_HEADER_TITLE_CONFIG);
registerBinDetailsSideEffect(options.muze, canvas, options.BinDetailsSideEffect);
const mountedCanvas = canvas.mount(options.selector);
disablePointerSelection(options.muze, mountedCanvas);
return mountedCanvas;
}
function mountBinLabels(options) {
const dominanceColorDomain = options.colorDomain;
const dominanceColorRange = [
COLORS.churnedAccent,
COLORS.center,
COLORS.retainedAccent,
];
const canvas = options.env
.canvas()
.data(options.data)
.rows([{ field: CHARGE_BIN, as: "continuous" }])
.columns([])
.color({
field: CUSTOMER_SHARE,
as: "continuous",
domain: dominanceColorDomain,
range: dominanceColorRange,
})
.layers([
{
mark: "bar",
className: "churn-by-price-binned__label-spine-layer",
individualClassName: getLabelSpineBinClassName,
interaction: getCenterBarLayerConfig(),
transition: DISABLED_TRANSITION_CONFIG,
encoding: {
text: {
field: CHARGE_BIN,
formatter: ({ rawValue }) => formatChargeBin(rawValue),
labelPlacement: { anchors: ["center"] },
},
color: CUSTOMER_SHARE,
},
},
])
.config({
axes: {
x: {
padding: 0,
showUnivariateAxis: true,
showAxisName: false,
showAxisLine: false,
transition: DISABLED_TRANSITION_CONFIG,
},
y: { show: false, transition: DISABLED_TRANSITION_CONFIG },
},
border: { width: 0 },
gridLines: { transition: DISABLED_TRANSITION_CONFIG },
gridBands: { transition: DISABLED_TRANSITION_CONFIG },
legend: {
show: true,
position: "top",
height: SHARE_HEADER_SPACER_HEIGHT,
color: {
margin: 4,
fields: {
[CUSTOMER_SHARE]: {
domain: dominanceColorDomain,
range: dominanceColorRange,
},
},
},
},
scrollBar: {
buttons: { show: false },
thickness: 0,
},
interaction: getInteractionDetailConfig(),
})
.title(CENTER_SHARE_HEADER, SHARE_HEADER_TITLE_CONFIG);
registerBinDetailsSideEffect(options.muze, canvas, options.BinDetailsSideEffect);
const mountedCanvas = canvas.mount(options.selector);
disablePointerSelection(options.muze, mountedCanvas);
return mountedCanvas;
}
async function buildViz(muze, data, mountId) {
await waitForAnnotationFont();
const mountEl = document.getElementById(mountId);
const detailId = mountId + "-details";
const leftId = mountId + "-left";
const centerId = mountId + "-center";
const rightId = mountId + "-right";
renderShell(mountEl, { detailId, leftId, centerId, rightId });
const detailEl = document.getElementById(detailId);
const { DataModel } = muze;
const totalsByChurn = getTotalsByChurn(DataModel, data);
const customerShareData = buildCustomerShareData(DataModel, data, totalsByChurn);
const xDomainMax = getShareDomainMax(customerShareData);
const binDominanceData = buildBinDominanceData(DataModel, customerShareData);
const binDetailsByStart = buildBinDetailsByStart(customerShareData);
const dominanceColorDomain = CENTER_DOMINANCE_COLOR_DOMAIN;
const churnedData = customerShareData.select({
field: CHURN,
operator: "eq",
value: CHURNED_VALUE,
});
const retainedData = customerShareData.select({
field: CHURN,
operator: "eq",
value: RETAINED_VALUE,
});
const env = muze;
const annotationTheme = createAnnotationTheme(muze);
const BinDetailsSideEffect = createBinDetailsSideEffect(
muze,
detailEl,
binDetailsByStart,
);
renderDefaultBinDetails(detailEl);
const churnedCanvas = mountHistogram({
muze,
env,
selector: "#" + leftId,
data: churnedData,
measure: CUSTOMER_SHARE,
domain: [-xDomainMax - 0.05, 0],
fill: COLORS.churned,
layerClassName: "churn-by-price-binned__bars churn-by-price-binned__bars--churned",
featuredBin: FEATURED_BINS.churned,
shareTitle: CHURNED_SHARE_HEADER,
BinDetailsSideEffect,
annotationTheme,
});
const labelCanvas = mountBinLabels({
muze,
env,
selector: "#" + centerId,
data: binDominanceData,
colorDomain: dominanceColorDomain,
BinDetailsSideEffect,
});
const retainedCanvas = mountHistogram({
muze,
env,
selector: "#" + rightId,
data: retainedData,
measure: CUSTOMER_SHARE,
domain: [0, xDomainMax + 0.05],
fill: COLORS.retained,
layerClassName: "churn-by-price-binned__bars churn-by-price-binned__bars--retained",
featuredBin: FEATURED_BINS.retained,
shareTitle: RETAINED_SHARE_HEADER,
BinDetailsSideEffect,
annotationTheme,
});
return {
dispose() {
[churnedCanvas, labelCanvas, retainedCanvas].forEach((canvas) => {
canvas.dispose();
});
},
};
}
CSS
The same foundation layout used by the live showcase: a full-size shell split into two flexible histogram containers and a bounded center label spine.
Preview
/* The Oswald URL is the canonical docs-hosted asset for Studio copies. */
@font-face {
font-family: "Oswald";
font-style: normal;
font-weight: 600;
font-display: block;
src: url("https://cyoc-documentation-site.vercel.app/charts/fonts/showcases/churn-by-price/oswald-latin-600.woff2") format("woff2");
}
html, body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: auto;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
#chart {
display: grid;
width: 100%;
min-width: 850px;
height: 100%;
}
#chart .churn-by-price-binned {
box-sizing: border-box;
display: grid;
gap: 0;
grid-template-columns: minmax(0, 1fr) clamp(80px, 10%, 114px) minmax(0, 1fr);
grid-template-rows: max-content minmax(0, 1fr);
height: 100%;
width: 100%;
min-height: 100%;
}
.churn-by-price-binned__canvas {
grid-row: 2;
min-width: 0;
min-height: 0;
}
.churn-by-price-binned__interaction-detail {
grid-column: 1 / -1;
grid-row: 1;
justify-self: center;
color: var(--churn-viz-text);
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
Menlo,
monospace;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
line-height: 18px;
pointer-events: none;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.74);
white-space: nowrap;
}
.churn-by-price-binned__interaction-detail--idle {
color: var(--churn-viz-muted);
opacity: 0.72;
}
.churn-by-price-binned__interaction-detail-bin {
color: #dce7f7;
}
.churn-by-price-binned__interaction-detail-separator {
color: rgba(152, 166, 186, 0.7);
padding-inline: 0.42em;
}
.churn-by-price-binned__interaction-detail-churned {
color: #69f4ff;
text-shadow: 0 0 5px rgba(82, 228, 239, 0.34);
}
.churn-by-price-binned__interaction-detail-retained {
color: #ffc366;
text-shadow: 0 0 5px rgba(255, 178, 74, 0.3);
}
.churn-by-price-binned__interaction-detail-total {
color: var(--churn-viz-muted);
}
.churn-by-price-binned__labels .muze-axis .muze-ticks,
.churn-by-price-binned__labels .muze-axis path,
.churn-by-price-binned__labels .muze-axis line {
visibility: hidden;
}
.churn-by-price-binned__labels .muze-legend-container {
visibility: hidden;
pointer-events: none;
}
#chart {
background: #0d111b;
color: #dce7f7;
}
#chart .churn-by-price-binned {
--churn-viz-bg: #0d111b;
--churn-viz-grid: rgba(93, 112, 142, 0.24);
--churn-viz-text: #dce7f7;
--churn-viz-muted: #98a6ba;
--churn-viz-cyan: #35b5c0;
--churn-viz-cyan-hot: #52e4ef;
--churn-viz-orange: #925d20;
--churn-viz-orange-hot: #ffb24a;
--churn-viz-spine-edge: rgba(255, 255, 255, 0.08);
--churn-viz-spine-bleed-scale: 1.205;
--churn-viz-spine-bleed-shift: -5.55px;
background:
radial-gradient(circle at 50% 54%, rgba(38, 70, 101, 0.16), transparent 34%),
var(--churn-viz-bg);
color: var(--churn-viz-text);
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
Menlo,
monospace;
}
#chart .muze-group-container,
#chart .muze-layout-container-component,
#chart .muze-grid,
#chart .muze-grid-table,
#chart .muze-grid-td,
#chart .muze-blank-cell,
#chart .muze-geom-cell,
#chart .muze-unit {
background-color: transparent !important;
}
#chart .muze-axis-grid-lines-x path {
stroke: var(--churn-viz-grid) !important;
stroke-width: 1px !important;
shape-rendering: crispEdges;
}
#chart .muze-axis .domain,
#chart .muze-axis .muze-tick-lines {
stroke: rgba(137, 149, 170, 0.32) !important;
}
#chart .muze-axis text,
#chart .muze-ticks {
fill: var(--churn-viz-muted) !important;
color: var(--churn-viz-muted) !important;
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
Menlo,
monospace !important;
font-size: 10px !important;
letter-spacing: 0.02em;
}
#chart .churn-by-price-binned .muze-title-cell {
color: var(--churn-viz-text) !important;
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
Menlo,
monospace !important;
font-size: 11px !important;
font-weight: 700 !important;
letter-spacing: 0.04em;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
}
#chart .churn-by-price-binned__histogram--churned .muze-title-cell {
color: #69f4ff !important;
text-shadow: 0 0 5px rgba(82, 228, 239, 0.36);
}
#chart .churn-by-price-binned__histogram--retained .muze-title-cell {
color: #ffc366 !important;
text-shadow: 0 0 5px rgba(255, 178, 74, 0.32);
}
#chart .muze-layer-bar rect {
stroke: transparent !important;
shape-rendering: crispEdges;
}
#chart .churn-by-price-binned__bars {
font-family: "Oswald", sans-serif !important;
font-feature-settings: normal;
font-kerning: auto;
font-optical-sizing: auto;
font-size: 16px !important;
font-stretch: 100%;
font-style: normal;
font-variant: normal;
font-variation-settings: normal;
font-weight: 600;
letter-spacing: normal;
line-height: 24px;
}
#chart .churn-by-price-binned__bars--churned .muze-layer-bars rect {
fill: var(--churn-viz-cyan) !important;
opacity: 0.78 !important;
}
#chart .churn-by-price-binned__bars--retained .muze-layer-bars rect {
fill: var(--churn-viz-orange) !important;
opacity: 0.84 !important;
}
#chart .churn-by-price-binned__bars--churned .churn-by-price-binned__annotation--churned rect {
fill: var(--churn-viz-cyan-hot) !important;
opacity: 1 !important;
filter: drop-shadow(0 0 7px rgba(82, 228, 239, 0.7));
}
#chart .churn-by-price-binned__bars--retained .churn-by-price-binned__annotation--retained rect {
fill: var(--churn-viz-orange-hot) !important;
opacity: 1 !important;
filter: drop-shadow(0 0 8px rgba(255, 178, 74, 0.64));
}
#chart .churn-by-price-binned__labels {
overflow: visible !important;
position: relative;
z-index: 2;
}
#chart .churn-by-price-binned__labels .muze-visual-unit,
#chart .churn-by-price-binned__labels .muze-layer-group,
#chart .churn-by-price-binned__labels .muze-layer-bar,
#chart .churn-by-price-binned__labels .muze-layer-bars {
overflow: visible !important;
}
#chart .churn-by-price-binned__label-spine-layer .muze-layer-bars rect {
stroke: var(--churn-viz-spine-edge) !important;
opacity: 0.96;
filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.38));
transform: translateX(var(--churn-viz-spine-bleed-shift))
scaleX(var(--churn-viz-spine-bleed-scale));
transform-box: fill-box;
transform-origin: center;
}
#chart .churn-by-price-binned__label-spine-layer .churn-by-price-binned__label-spine-bin--churned rect {
stroke: rgba(170, 252, 255, 0.54) !important;
filter:
drop-shadow(0 0 2px rgba(82, 228, 239, 0.72))
drop-shadow(0 0 8px rgba(82, 228, 239, 0.7)) !important;
}
#chart .churn-by-price-binned__label-spine-layer .churn-by-price-binned__label-spine-bin--retained rect {
stroke: rgba(255, 214, 153, 0.48) !important;
filter:
drop-shadow(0 0 2px rgba(255, 178, 74, 0.62))
drop-shadow(0 0 8px rgba(255, 178, 74, 0.7)) !important;
}
#chart .churn-by-price-binned__labels .muze-layer-labels text {
fill: var(--churn-viz-text) !important;
color: var(--churn-viz-text) !important;
font-family:
"SFMono-Regular",
Consolas,
"Liberation Mono",
Menlo,
monospace !important;
font-size: 11px !important;
paint-order: stroke fill;
stroke: rgba(4, 8, 15, 0.74) !important;
stroke-width: 2px !important;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
transform: translateX(var(--churn-viz-spine-bleed-shift));
}
#chart .churn-by-price-binned__labels .muze-layer-bar-label-1 text {
fill: #101723 !important;
stroke: rgba(255, 224, 177, 0.68) !important;
font-weight: 700;
text-shadow: none;
}
#chart text.churn-by-price-binned__annotation {
fill: var(--churn-viz-text) !important;
color: var(--churn-viz-text) !important;
stroke: rgba(4, 8, 15, 0.62) !important;
font-family: "Oswald", sans-serif !important;
font-feature-settings: normal;
font-kerning: auto;
font-optical-sizing: auto;
font-size: 16px !important;
font-stretch: 100%;
font-style: normal;
font-variant: normal;
font-variation-settings: normal;
font-weight: 600;
letter-spacing: normal;
line-height: 24px;
paint-order: stroke fill;
stroke-width: 0.5px !important;
text-shadow: none;
text-rendering: geometricPrecision;
filter: none !important;
}
#chart text.churn-by-price-binned__annotation--churned {
fill: #69f4ff !important;
stroke: rgba(5, 19, 22, 0.38) !important;
text-shadow:
0 0 1px rgba(237, 254, 255, 0.88),
0 0 4px rgba(82, 228, 239, 0.9),
0 0 9px rgba(82, 228, 239, 0.5),
0 0 16px rgba(82, 228, 239, 0.25);
filter:
drop-shadow(0 0 2px rgba(82, 228, 239, 0.74))
drop-shadow(0 0 9px rgba(82, 228, 239, 0.38)) !important;
}
#chart text.churn-by-price-binned__annotation--retained {
fill: #ffc366 !important;
stroke: rgba(31, 17, 3, 0.38) !important;
text-shadow:
0 0 1px rgba(255, 247, 228, 0.82),
0 0 4px rgba(255, 178, 74, 0.84),
0 0 9px rgba(255, 178, 74, 0.46),
0 0 16px rgba(255, 178, 74, 0.23);
filter:
drop-shadow(0 0 2px rgba(255, 178, 74, 0.68))
drop-shadow(0 0 9px rgba(255, 178, 74, 0.32)) !important;
}
#chart .muze-tracker {
fill: transparent !important;
stroke: transparent !important;
}
HTML
The mount element the visualization renders into in Muze Studio. Same shape as the other showcases; the JavaScript creates the inner three-canvas shell.
Preview
<div id="chart"></div>
Dataset (CSV)
Pre-binned ChargeBinStart, Churn, and Customers values. Load it into a Muze Studio Worksheet and create a Muze Studio Answer with it.