Published
Edited
Mar 13, 2021
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
events = {
const deltasByStatus = {
Active: 1,
Terminated: -1
};
return d3.sort(
d3.csvParse(manuallyCleanedCsv).map(d => ({
...d,
Date: new Date(d.Date),
delta: deltasByStatus[d["Activity Status"]] || 0
})),
d => d.Date
);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
netCumSum = d3.cumsum(events, d => d.delta)
Insert cell
Insert cell
eventsWithNet = events.map((d, i) => ({ ...d, Net: netCumSum[i] }))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dates = d3.timeDays(new Date("04-Aug-20"), new Date("31-Jan-21"))
Insert cell
Insert cell
eventsByDate = d3.group(
eventsWithNet,
({ Date: d }) => new Date(d.getFullYear(), d.getMonth(), d.getDate())
)
Insert cell
Insert cell
Insert cell
interpolatedData = {
const bisector = d3.bisector(d => d.Date).right;
return dates.map(date => {
const i = bisector(events, date);
const eventIndex = i - 1;
const eventsAtDate = eventsByDate.get(date) || [];
const gain = d3.sum(eventsAtDate, d => Math.max(d.delta, 0));
const loss = -d3.sum(eventsAtDate, d => Math.min(d.delta, 0));
return {
date,
net: netCumSum[eventIndex],
change: gain - loss,
gain,
loss,
events: eventsAtDate.length
};
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
computeLambda = date =>
(0.9 * (1 + 3 * (0.5 + 0.5 * Math.sin(5e-10 * date.valueOf())))) /
millisPerDay
Insert cell
Insert cell
Insert cell
Insert cell
computeTerminationProbability = date =>
0.1 + 0.48 * (0.5 + 0.5 * Math.sin(3e-10 * date.valueOf() + 5050))
Insert cell
Insert cell
Insert cell
generatedEvents = {
const random = d3.randomLcg(0);

let t = dates[0].valueOf();
const tMax = dates[dates.length - 1].valueOf();
const events = [];
const maxEventCount = 10000;
let userCount = 0;
while (events.length < maxEventCount && t < tMax) {
const terminationProbability = computeTerminationProbability(t);
const dt = d3.randomExponential.source(random)(computeLambda(t))();
t += dt;

// If there are no accounts, there can be no termination events
const delta = random() < terminationProbability && userCount > 0 ? -1 : 1;
userCount += delta;
events.push({
date: new Date(t),
delta
});
}
return events;
}
Insert cell
Insert cell
Insert cell
binnedGeneratedData = d3
.bin()
.value(d => d.date)
.thresholds(dates)(generatedEvents)
.map((d, i) => Object.assign(d, { date: dates[i] }))
Insert cell
interpolatedGeneratedData = {
let net = 0;
return binnedGeneratedData.map((a, i) => {
const gain = d3.sum(a, d => Math.max(d.delta, 0));
const loss = -d3.sum(a, d => Math.min(d.delta, 0));
const change = gain - loss;
net += change;
return {
net,
gain,
loss,
change,
date: a.date
};
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createFinalChart(interpolatedData) {
const netMax = d3.max(interpolatedData, d => d.net);
const changeDomainMin = -d3.max(interpolatedData, d => d.loss);
const changeDomainMax = d3.max(interpolatedData, d => d.gain);
const augmentedData = interpolatedData.map(d => ({
...d,
changeDomainMin,
changeDomainMax,
netMax
}));
const xDateEncoding = vl
.x()
.fieldT("date")
.title("Date");

const net = vl.layer(
vl
.markLine({ tooltip: true, interpolate: "step-after" })
.data(augmentedData)
.encode(
vl.x().fieldT("date"),
vl
.y()
.fieldQ("net")
.title("Net")
),
vl
.markBar({ fillOpacity: 0 })
.data(augmentedData)
.encode(
xDateEncoding,
vl.y().fieldQ("netMax"),
vl.tooltip(tooltipSpecification)
)
);

const deltas = vl.layer(
vl
.markBar({ fill: "green" })
.data(augmentedData)
.encode(xDateEncoding, vl.y().fieldQ("gain")),
vl
.markBar({ fill: "crimson" })
.data(augmentedData)
.transform({ calculate: "-datum.loss", as: "negativeLoss" })
.encode(xDateEncoding, vl.y().fieldQ("negativeLoss")),
vl
.markPoint({
stroke: "black",
fill: "white",
opacity: 1
})
.data(
augmentedData.filter(
d => d.gain !== 0 && d.loss !== 0 && d.change === 0
)
)
.encode(xDateEncoding, vl.y().fieldQ("change")),
vl
.markPoint({
stroke: "black",
fill: "green",
opacity: 1
})
.data(augmentedData.filter(d => d.change > 0))
.encode(xDateEncoding, vl.y().fieldQ("change")),
vl
.markPoint({
stroke: "black",
fill: "crimson",
opacity: 1
})
.data(augmentedData.filter(d => d.change < 0))
.encode(xDateEncoding, vl.y().fieldQ("change")),
vl
.markBar({ fillOpacity: 0 })
.data(augmentedData)
.encode(
xDateEncoding,
vl
.y()
.fieldQ("changeDomainMin")
.title("Gain, Loss, Change"),
vl.y2().fieldQ("changeDomainMax"),
vl.tooltip(tooltipSpecification)
)
);

return vl
.vconcat([net, deltas].map(d => d.width(width).height(150)))
.autosize({ type: 'fit-x', contains: 'padding' })
.render();
}
Insert cell
Insert cell
millisPerDay = 24 * 60 * 60 * 1000
Insert cell
Insert cell
Insert cell
Insert cell
import { Table } from "@observablehq/inputs"
Insert cell
import { vl } from '@vega/vega-lite-api'
Insert cell
d3 = require("d3@6")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more