chart = {
data.forEach((d) => (d.ts = parseTime(d.EVENT_TIMESTAMP)));
const xScale = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.ts))
.range([marginLeft, width - marginRight]);
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");
const rectangles = svg
.append("g")
.classed("rectangles", true)
.selectAll("rect")
.data(data.slice(0, -1))
.join("rect")
.attr("y", (d) => height / 2 - 22)
.attr("height", 30)
.attr("fill", (d) => (d.EVENT_VALUE ? "#2ecc71" : "#e74c3c"))
.attr("opacity", 0.7);
const gx = svg
.append("g").classed("xAxis",true)
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis, xScale);
const dimensionGroup = svg.append('g')
.attr('class', 'dimension-group')
.style('display', 'none');
function zoomed(event) {
dimensionGroup.style('display', 'none');
const xz = event.transform.rescaleX(xScale);
rectangles
.attr("x", (d) => xz(d.ts))
.attr("width", (d, i) => xz(data[i + 1].ts) - xz(d.ts));
gx.call(xAxis, xz);
}
const zoom = d3
.zoom()
.scaleExtent([1, 32])
.extent([
[marginLeft, 0],
[width - marginRight, height]
])
.translateExtent([
[marginLeft, -Infinity],
[width - marginRight, Infinity]
])
.on("zoom", zoomed);
svg
.call(zoom)
.transition()
.duration(750)
.call(zoom.scaleTo, 1, [xScale(Date.UTC(2025, 4, 10)), 0]);
const formatTime = d3.utcFormat("%B %d, %Y %I:%M:%S %p");
svg.selectAll("rect")
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.attr("x", (event.x))
.attr("y", 28)
.style("opacity", 0.6);
tooltip.html(`Time: ${formatTime(d.ts)}<br/>State: ${d.EVENT_VALUE ? "Up" : "Down"}`)
.style("display", "block") ;
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
const tooltip = svg
.append("div")
.classed("tooltip",true)
.style("opacity", 0);
const verticalLine1 = dimensionGroup.append('line')
.attr('class', 'dimension-line vertical');
const verticalLine2 = dimensionGroup.append('line')
.attr('class', 'dimension-line vertical');
const horizontalLine = dimensionGroup.append('line')
.attr('class', 'dimension-line horizontal');
const dimensionText = dimensionGroup.append('text')
.attr('class', 'dimension-text');
svg.append('defs').selectAll('marker')
.data(['start', 'end'])
.enter()
.append('marker')
.attr('id', d => `arrow-${d}`)
.attr('viewBox', '0 -5 10 10')
.attr('refX', d => d === 'start' ? 0 : 10)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', d => d === 'start' ? 'M10,-5L0,0L10,5' : 'M0,-5L10,0L0,5')
.attr('fill', '#666');
let dragStart = null;
let dragEnd = null;
const drag = d3.drag()
.filter(event => event.ctrlKey)
.on('start', dragStarted)
.on('drag', dragging)
.on('end', dragEnded);
svg.on('mousemove', function(event) {
if (event.ctrlKey) {
d3.select(this).style('cursor', 'crosshair');
} else {
d3.select(this).style('cursor', 'default');
}
});
function dragStarted(event) {
dragStart = xScale.invert(event.x);
dimensionGroup.style('display', 'block');
}
function dragging(event) {
dragEnd = xScale.invert(event.x);
updateDimensionLines(event.x, event.y);
}
function dragEnded(event) {
}
function updateDimensionLines(currentX, currentY) {
const startX = xScale(dragStart);
const endX = currentX;
const centerY = 20;
verticalLine1
.attr('x1', startX)
.attr('y1', currentY)
.attr('x2', startX)
.attr('y2', centerY);
verticalLine2
.attr('x1', endX)
.attr('y1', currentY)
.attr('x2', endX)
.attr('y2', centerY);
horizontalLine
.attr('x1', startX)
.attr('y1', centerY)
.attr('x2', endX)
.attr('y2', centerY)
.attr('marker-start', 'url(#arrow-start)')
.attr('marker-end', 'url(#arrow-end)');
const duration = Math.abs(dragEnd - dragStart);
const formattedDuration = formatDuration(duration);
dimensionText
.attr('x', (startX + endX) / 2)
.attr('y', centerY - 5)
.attr('text-anchor', 'middle')
.text(formattedDuration);
}
function formatDuration(milliseconds) {
const seconds = milliseconds / 1000;
const minutes = seconds / 60;
const hours = minutes / 60;
if (hours >= 1) {
return `${Math.round(hours * 10) / 10}h`;
} else if (minutes >= 1) {
return `${Math.round(minutes)}m`;
} else {
return `${Math.round(seconds)}s`;
}
}
const styles = `
.dimension-line {
stroke: #666;
stroke-width: 1;
stroke-dasharray: 4,4;
}
.dimension-text {
font-size: 18px;
fill: #666;
}
.tooltip {
background-color: white;
border: none;
border-radius: 5px;
padding: 15px;
min-width: 400px;
text-align: left;
color: black;
}
`;
const styleSheet = document.createElement('style');
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
svg.call(drag);
return svg.node();
}