Published
Edited
Oct 2, 2020
Insert cell
Insert cell
{
const m = DOM.svg(width, height);
const svg = d3.select(m).append('g');
let zoomArea = svg.append("g").attr("style", "cursor:move;pointer-events:all;");
let base = zoomArea.append("g");
let dataCanvas = zoomArea.append("g");
let gAxis = zoomArea.append("g");
base.selectAll("*").remove()
base
.append("rect")
.attr("id", "labels-rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "#f4f7f9");

base
.append("rect")
.attr("id", "base-rect")
.attr("y", 0)
.attr("width", width)
.attr("height", 56)
.attr("fill", "#f4f7f9");
let xAxis = d3.axisBottom(xScale);
let zoomed = function(event) {
let eventType = event.sourceEvent.type;
let xz = event.transform.rescaleX(xScale);
gAxis
.transition()
.duration(eventType === "wheel" ? 100 : 0)
.ease(d3.easeLinear)
.call(
xAxis.scale(xz)
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M"))
.ticks(5)
)
updateChartCollisions(xz, dataCanvas);
}
let zoom = d3.zoom()
.scaleExtent([1, 500])
.translateExtent([[-20, 0], [width, 0]])
.extent([[-20, 0], [width, 0]])
.on("zoom", zoomed);

zoomArea.call(zoom);
gAxis.call(xAxis
.tickFormat(d3.timeFormat("%Y-%m-%d %H:%M"))
.ticks(5)
);
gAxis.style("font-size", "12px");
let enterCanvas = dataCanvas
.selectAll(".alert-point")
.data(data)
.enter();
enterCanvas
.append("path")
.attr("class", "alert-icon")
.attr("d", feedbackIcon.d)
.attr("fill", "red");

enterCanvas
.append("text")
.attr("class", (d,i) => `alert-label alert-label${i}`)
.attr("id", (d,i) => `label${i}`)
.style("font-size", "15px");
updateChartCollisions(xScale, dataCanvas)

return m;
}
Insert cell
updateChartCollisions = (scale, dataCanvas) => {
let points = [];
dataCanvas.selectAll(".alert-icon").each((d, i, nodes) => {
let currentCircle = d3.select(nodes[i]);
let newPos = scale(new Date(d.date)) - (width / 2);
points.unshift({ x: newPos * 1, _id: i });
currentCircle
.transition().duration(0).ease(d3.easeLinear)
.attr("transform",`translate(${newPos},${logPosY})`)
});

let clusters = [];
if (points.length <= 1) {
clusters = [points]
} else {
clusters = flatten(cluster(points.sort((a, b) => a.x - b.x), 30));
}
dataCanvas.selectAll(".alert-label").text("");

for (let i = 0; i < clusters.length; i++) {
let group = clusters[i];
let label = d3.select(`.alert-label${group[0]._id}`);

if (group.length === 1) {
label
.transition().duration(10).ease(d3.easeLinear)
.attr("text-anchor", "end")
.attr("transform", d => `translate(${(scale(new Date(d.date)) - (width / 2))},${margin.top + 70}) rotate(-30)`)
.text(d => d.label);
} else {
label
.transition().duration(10).ease(d3.easeLinear)
.attr("text-anchor", "middle")
.attr("transform", `translate(${center(group)},${logPosY + (radius * 3)})`)
.text(group.length);
}
}
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date).map(d => new Date(d)))
.range([margin.left, width - margin.right])
.nice();
Insert cell
logPosY = margin.top + (radius * 3)
Insert cell
center = l => {
if (l.length == 0) {
return 0;
} else if (l.length == 1) {
return l[0].x;
}

let sum = l[0].x + l[l.length - 1].x;
return sum / 2;
};
Insert cell
computeMedianIndex = l => {
let c = center(l);
for (let i = 0; i < l.length; i++) {
if (l[i].x > c) {
return i;
}
}
return 1;
};
Insert cell
function cluster (l, vitalMargin) {
if (l.length <= 1) {
return l;
}
let medianIndex = computeMedianIndex(l);
let left = l.slice(0, medianIndex);
let right = l.slice(medianIndex);
let leftCenter = center(left);
let rightCenter = center(right);
if (rightCenter - leftCenter < vitalMargin) {
return [l];
}
return [cluster(left, vitalMargin), cluster(right, vitalMargin)];
};
Insert cell
function flatten (l) {
let f = [];
for (let i = 0; i < l.length; i++) {
if (l[i][0].x !== undefined) {
f.push(l[i]);
} else {
let aux = flatten(l[i]);
for (let j = 0; j < aux.length; j++) {
f.push(aux[j]);
}
}
}
return f;
};
Insert cell
Insert cell
feedbackIcon = ({d:"M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"})
Insert cell
radius = 15
Insert cell
t = d3.transition().duration(500).ease(d3.easeLinear);
Insert cell
margin = ({top: 0, bottom: 30, left: 30, right: 30 });
Insert cell
width = 800
Insert cell
height = 155
Insert cell
d3 = require("d3")
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