Public
Edited
May 12, 2023
2 stars
Insert cell
Insert cell
Insert cell
chart = {
let width = 0.98 * window.innerWidth;
let height = 0.66 * width;
let gap = 30;
let statusCircleGap = 0.5 * gap;
let machineTitlesGap = 1.4 * gap;
let timelineGap = 3 * gap;
let timelineWidth = width - 2 * gap;
let now = new Date(Date.parse("2023-01-29"));
let past = new Date(Date.parse("2020-01-01"));
let svg = d3.create("svg").attr("width", width).attr("height", height).style("background", "white");

let markerPath = `M5.5-0.0176c-1.7866,0-3.8711,1.0918-3.8711,3.8711
C1.6289,5.7393,4.6067,9.9082,5.5,11c0.7941-1.0918,3.871-5.1614,3.871-7.1466C9.371,1.0742,7.2866-0.0176,5.5-0.0176z`;
let defs = svg.append("defs");
// add the glow filter
let glowFilter = defs.append("filter").attr("id", "glow").attr("x", "-5000%").attr("y", "-5000%").attr("width", "10000%").attr("height", "10000%");
glowFilter.append("feComposite").attr("in", "flood").attr("result", "mask").attr("in2", "SourceGraphic").attr("operator", "in");
glowFilter.append("feMorphology").attr("in", "mask").attr("result", "dilated").attr("radius", "2").attr("operator", "dilate");
glowFilter.append("feGaussianBlur").attr("in", "dilated").attr("stdDeviation", 5).attr("result", "blurred");
let feMerge = glowFilter.append("feMerge");
feMerge.append("feMergeNode").attr("in", "blurred");
feMerge.append("feMergeNode").attr("in", "SourceGraphic");
let xAxis = svg.append("g").attr("class", "xAxis").attr("transform", `translate(${[timelineGap, height - gap]})`);

let legendItems = [{type: "UP", color: "#469E65"}, {type: "USD", color: "#B65460"}, {type: "SD", color: "#D0895E"}];
let eventItems = [{type: "TESTING", color: "#4581B1"}, {type: "IDLE", color: "#BDBAA5"}, {type: "MX", color: "#BC84C1"}];
let legendScale = d3.scalePoint().domain(legendItems).range([0.76 * width, width - gap]).padding(0.6);
let eventItemsScale = d3.scalePoint().domain(eventItems).range([timelineGap, 0.26 * width]).padding(0.2);
let legendCircles = svg.selectAll(".legend-circle").data(legendItems, (d) => d.type);

legendCircles
.join("circle")
.attr("class", "legend-circle")
.attr("transform", (d,i) => `translate(${[legendScale(d), 0.66 * gap]})`)
.attr("r", 0.2 * gap)
.attr("fill", (d,i) => d.color)
.attr("stroke", (d,i) => d3.rgb(d.color).darker());
let legendTexts = svg.selectAll(".legend-text").data(legendItems, (d) => d.type);

legendTexts
.join("text")
.attr("class", "legend-text")
.attr("transform", (d,i) => `translate(${[legendScale(d) + 0.5 * gap, 0.66 * gap]})`)
.attr("fill", "#333")
.attr("dy", 0.15 * gap)
.style("font", "12px/1.5 var(--sans-serif)")
.text((d,i) => d.type);

let eventIcons = svg.selectAll(".event-icon").data(eventItems, (d) => d.type);

eventIcons
.join("path")
.attr("class", "event-icon")
.attr("transform", (d,i) => `translate(${[eventItemsScale(d), 0.66 * gap - 5.5]})`)
.attr("d", markerPath)
.attr("fill", (d,i) => d.color)
.attr("stroke", (d,i) => d3.rgb(d.color).darker());
let eventTexts = svg.selectAll(".event-text").data(eventItems, (d) => d.type);

eventTexts
.join("text")
.attr("class", "event-text")
.attr("transform", (d,i) => `translate(${[eventItemsScale(d) + 0.5 * gap, 0.66 * gap]})`)
.attr("fill", "#333")
.attr("dy", 0.15 * gap)
.style("font", "12px/1.5 var(--sans-serif)")
.text((d,i) => d.type);

let machineTitlesContainer = svg.append("g").attr("class", "machine-titles-container").attr("transform", `translate(${[machineTitlesGap, 0]})`);

let intervalsUpTimeContainer = svg.append("g").attr("class", "intervals-container").attr("transform", `translate(${[timelineGap, 0]})`);

let eventsContainer = svg.append("g").attr("class", "events-container").attr("transform", `translate(${[timelineGap, 0]})`);

let statusCirclesContainer = svg.append("g").attr("class", "status-circles-container").attr("transform", `translate(${[statusCircleGap, 0]})`);
return Object.assign(svg.node(), {
update(input, visibleEvents) {
let yScale = d3.scalePoint().domain(input.map(d => d.machine_id)).range([gap, height - gap]).padding(0.6);
let xScale = d3.scaleTime().domain([now, now - 7.5 * 24 * 60 * 60 * 1000]).range([0, timelineWidth]);
let xAxisScale = d3.scaleLinear().domain([0, 7.5]).range([0, timelineWidth]);

// add the semi-circle filter
let semiCircleClip = defs.selectAll("clipPath").data(input, d => d.machine_id);

semiCircleClip
.join("clipPath")
.attr("id", (d,i) => `semi-circle-${d.machine_id}`)
.selectAll("g.path-container")
.data(d => [d])
.join("g")
.attr("class", "path-container")
.attr("transform", (d,i) => `translate(${[3 * timelineGap, yScale(d.machine_id) - yScale.step() / 3]})`)
.selectAll("path.clip-arc")
.data(d => [d])
.join("path")
.attr("class", "clip-arc")
.attr("d", d3.arc().innerRadius(0).outerRadius(10).startAngle(0 * Math.PI).endAngle(2 * Math.PI));

semiCircleClip
.exit()
.remove();
let machineTitles = machineTitlesContainer.selectAll(".machine-text").data(input, d => d.machine_id);

machineTitles
.enter()
.append("text")
.attr("class", "machine-text")
.attr("opacity", 0)
.attr("transform", (d,i) => `translate(${[0, yScale(d.machine_id)]})`)
.transition()
.duration((d,i) => i * 30)
.delay((d,i) => i * 30)
.attr("fill", "#333")
.attr("opacity", 1)
.attr("dy", -1)
.style("font", "14px/1.5 var(--sans-serif)")
.text((d,i) => d.machine_id);

machineTitles
.transition()
.duration((d,i) => i * 30)
.delay((d,i) => i * 30)
.text((d,i) => d.machine_id);

machineTitles
.exit()
.transition()
.duration((d,i) => i * 30)
.delay((d,i) => i * 30)
.attr("opacity", 0)
.remove();
let currentStatus = [];
let intervals = [];
let events = [];
input.forEach((d,index) => {
let hasSD = d.SD.states.find(e => e.end_date === null && e.end_date_override === null);
let hasUSD = d.USD.states.find(e => e.end_date === null && e.end_date_override === null);
currentStatus.push({machine_id: d.machine_id, type: (hasUSD ? "USD" : (hasSD ? "SD" : "UP")) });

let downtimes = [];
let uptimes = [];
d.SD.states.forEach((e,j) => {
let dt = {
id: `${d.machine_id}-SD-${j}`,
machine_index: index,
machine_id: d.machine_id,
type: "SD",
duration: e.down_duration,
period_type: e.down_type,
stillActive: e.end_date_override === null && e.end_date === null,
start_date: new Date(e.start_date),
end_date: (e.end_date_override ? new Date(e.end_date_override) : (e.end_date ? new Date(e.end_date) : now))
};
downtimes.push(dt);
events.push({
id: `${d.machine_id}-TESTING-event-${j}`,
event_order: j,
machine_id: d.machine_id,
machine_index: index,
type: "TESTING",
happened_at: dt.start_date,
finished_at: new Date((dt.start_date.getTime() + dt.end_date.getTime())/ 2)
});
events.push({
id: `${d.machine_id}-MX-event-${j}`,
event_order: j + 1,
machine_id: d.machine_id,
machine_index: index,
type: "MX",
happened_at: new Date((dt.start_date.getTime() + dt.end_date.getTime())/ 2),
finished_at: dt.end_date
});
});
d.USD.states.forEach((e,j) => {
let dt = {
id: `${d.machine_id}-USD-${j}`,
machine_index: index,
machine_id: d.machine_id,
type: "USD",
duration: e.down_duration,
period_type: e.down_type,
stillActive: e.end_date_override === null && e.end_date === null,
start_date: new Date(e.start_date),
end_date: (e.end_date_override ? new Date(e.end_date_override) : (e.end_date ? new Date(e.end_date) : now))
}
downtimes.push(dt);
events.push({
id: `${d.machine_id}-TESTING-USD-event-${j}`,
event_order: j,
machine_id: d.machine_id,
machine_index: index,
type: "TESTING",
happened_at: dt.start_date,
finished_at: dt.end_date.getTime() === now.getTime() ? new Date((dt.start_date.getTime() + dt.end_date.getTime())/ 2) : dt.end_date
});
dt.end_date.getTime() === now.getTime() && events.push({
id: `${d.machine_id}-IDLE-event-${j}`,
event_order: j + 1,
machine_id: d.machine_id,
machine_index: index,
type: "IDLE",
happened_at: new Date((dt.start_date.getTime() + dt.end_date.getTime())/ 2),
finished_at: dt.end_date
});
});
downtimes = downtimes.sort((a,b) => d3.descending(a.start_date, b.start_date));
let pivot = now;
downtimes.forEach((e,j) => {
uptimes.push({
id: `${d.machine_id}-UP-${j}`,
machine_index: index,
machine_id: d.machine_id,
type: "UP",
duration: Math.floor((pivot - e.end_date) / (60 * 1000)),
period_type: "UP",
stillActive: j === 0,
start_date: e.end_date,
end_date: pivot,
});
pivot = e.start_date;
});
uptimes.push({
id: `${d.machine_id}-UP-${uptimes.length}`,
machine_index: index,
machine_id: d.machine_id,
type: "UP",
duration: Math.floor((downtimes[downtimes.length - 1].start_date - past) / (60 * 1000)),
period_type: "UP",
stillActive: false,
start_date: past,
end_date: downtimes[downtimes.length - 1].start_date,
});
intervals.push([...uptimes, ...downtimes]);
});
let timeIntervals = intervals.flat().filter(d => d.duration > 0);
let intervalPaths = intervalsUpTimeContainer.selectAll("path.interval").data(timeIntervals, (d) => d.id);

intervalPaths
.join("path")
.attr("class", "interval")
.attr("fill", (d,i) => d3.rgb(legendItems.find(e => e.type === d.type).color).brighter(0.3))
.attr("stroke", (d,i) => d3.rgb(legendItems.find(e => e.type === d.type).color).darker())
.attr("stroke-width", 0.5)
.attr("opacity", 0)
.attr("d", (d,i) => `M${xScale(d.end_date)} ${yScale(d.machine_id) - yScale.step() / 3} h ${xScale(d.start_date) - xScale(d.end_date)} v ${yScale.step() / 3} h ${-(xScale(d.start_date) - xScale(d.end_date))} A${d.stillActive ? yScale.step() / 6 : 0},${d.stillActive ? yScale.step() / 6 : 0} 0 0 1 ${xScale(d.end_date)},${yScale(d.machine_id) - yScale.step() / 3}`)
.on("mouseover", function(e,d) {
var sel = d3.select(this);
sel.moveToFront();
sel.attr("stroke", "#ff7f0e").attr("stroke-width", 1.5);
})
.on("mouseout", function(e,d) {
d3.select(this).attr("stroke", (d,i) => d3.rgb(legendItems.find(e => e.type === d.type).color).darker())
.attr("stroke-width", 0.5);
})
.transition()
.duration((d,i) => i * d.machine_index)
.attr("opacity", 1);

intervalPaths
.exit()
.transition()
.duration((d,i) => i * d.machine_index)
.delay((d,i) => i * 20)
.attr("opacity", 0)
.remove();


let eventMarkers = eventsContainer.selectAll("path.event").data(events.filter(d => visibleEvents), (d) => d.id);

eventMarkers
.join("path")
.attr("class", "event")
.attr("transform", (d,i) => `translate(${[xScale(d.happened_at) - 6.5, yScale(d.machine_id) - 2 * yScale.step() / 3 - 22]})`)
.attr("d", markerPath)
.attr("fill", (d,i) => d3.rgb(eventItems.find(e => e.type === d.type).color))
.attr("stroke", (d,i) => d3.rgb(eventItems.find(e => e.type === d.type).color).darker())
.attr("stroke-width", 0.5)
.attr("opacity", 0)
.transition()
.duration((d,i) => i * d.machine_index)
.delay((d,i) => i * d.event_order)
.attr("opacity", 1)
.attr("transform", (d,i) => `translate(${[xScale(d.happened_at) - 5.5, yScale(d.machine_id) - 2 * yScale.step() / 3 - 4]})`);

eventMarkers
.exit()
.transition()
.duration((d,i) => i * d.machine_index)
.delay((d,i) => i * 20)
.attr("opacity", 0)
.remove();

let eventLines = eventsContainer.selectAll("line.event").data(events.filter(d => visibleEvents), (d) => d.id);

eventLines
.join("line")
.attr("class", "event")
.attr("transform", (d,i) => `translate(${[xScale(d.happened_at), yScale(d.machine_id) - 2 * yScale.step() / 3 - 11]})`)
.attr("stroke", (d,i) => eventItems.find(e => e.type === d.type).color)
.attr("stroke-width", 1.5)
.attr("opacity", 0)
.transition()
.duration((d,i) => i * d.machine_index)
.delay((d,i) => i * d.event_order)
.attr("opacity", 1)
.attr("transform", (d,i) => `translate(${[xScale(d.happened_at), yScale(d.machine_id) - 2 * yScale.step() / 3 + 9]})`)
.attr("x2", (d,i) => xScale(d.finished_at) - xScale(d.happened_at) + 1)
eventLines
.exit()
.transition()
.duration((d,i) => i * d.machine_index)
.delay((d,i) => i * 20)
.attr("opacity", 0)
.remove();
let statusCircles = statusCirclesContainer.selectAll(".status-circle").data(currentStatus, (d) => d.machine_id);

statusCircles
.join("circle")
.attr("id", (d,i) => `status-circle-${d.machine_id}`)
.attr("class", "status-circle")
.attr("transform", (d,i) => `translate(${[yScale.step() / 6, yScale(d.machine_id) - yScale.step() / 6]})`)
.attr("fill", (d,i) => legendItems.find(e => e.type === d.type).color)
.attr("stroke", (d,i) => d3.rgb(legendItems.find(e => e.type === d.type).color).darker())
.attr("stroke-width", 1)
.style("filter", "url(#glow)")
.attr("opacity", 0)
.attr("r", yScale.step() / 6)
.attr("opacity", 1)

currentStatus.forEach(function(d,i) {
function pulse(circle) {
(function repeat() {
circle
.transition()
.duration(500)
.attr("stroke-width", 0)
.attr('stroke-opacity', 0)
.transition()
.duration(500)
.attr("stroke-width", 0)
.attr('stroke-opacity', 0.5)
.transition()
.duration(1000)
.attr("stroke-width", 15)
.attr('stroke-opacity', 0)
.ease(d3.easeSin)
.on("end", repeat);
})();
}
pulse(d3.select(`#status-circle-${d.machine_id}`));
});

statusCircles
.exit()
.transition()
.duration((d,i) => i * 30)
.delay((d,i) => i * 30)
.attr("r", 0)
.remove();
xAxis.transition().call(d3.axisBottom(xAxisScale).ticks(8).tickFormat(x => x === 0 ? `Now` : `${x} day${x < 2 ? "" : "s"} ago`));
}
})
}
Insert cell
Insert cell
chart.update(inputData.data.filter(d => d.USD.total_duration < 2000).slice(settings[0] * 20,settings[0] * 20 + 20), settings[1].length > 0)
Insert cell
settings
Insert cell
library = import("https://cdn.skypack.dev/d3@7")
Insert cell
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};

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