Unlisted
Edited
May 21
Fork of Simple D3
Insert cell
Insert cell
chart = {
// Process timestamps in the data
data.forEach((d) => (d.ts = parseTime(d.EVENT_TIMESTAMP)));

// Create the horizontal scale.
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;");

// Add rectangles
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);

// Append the horizontal axis.
const gx = svg
.append("g").classed("xAxis",true)
.attr("transform", `translate(0,${height - marginBottom})`)
.call(xAxis, xScale);

// Create a group for the dimension elements
const dimensionGroup = svg.append('g')
.attr('class', 'dimension-group')
.style('display', 'none'); // Hidden initially
// When zooming, redraw the area and the x axis.
function zoomed(event) {
dimensionGroup.style('display', 'none'); // erase the dimension
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);
}

// Create the zoom behavior.
const zoom = d3
.zoom()
.scaleExtent([1, 32])
.extent([
[marginLeft, 0],
[width - marginRight, height]
])
.translateExtent([
[marginLeft, -Infinity],
[width - marginRight, Infinity]
])
.on("zoom", zoomed);

// Initial zoom.
svg
.call(zoom)
.transition()
.duration(750)
.call(zoom.scaleTo, 1, [xScale(Date.UTC(2025, 4, 10)), 0]);
//---------------------------
// tooltips
// Tooltips contain a time value formatted here
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);
});
// Add tooltips
const tooltip = svg
.append("div")
.classed("tooltip",true)
.style("opacity", 0);
//---------------------------


// Create dimension line elements
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');

// Arrow marker definitions
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');

// Drag behavior
let dragStart = null;
let dragEnd = null;

const drag = d3.drag()
.filter(event => event.ctrlKey) // Only allow drag when Ctrl is pressed
.on('start', dragStarted)
.on('drag', dragging)
.on('end', dragEnded);
svg.on('mousemove', function(event) {
if (event.ctrlKey) {
d3.select(this).style('cursor', 'crosshair'); // Change cursor when Ctrl is pressed
} 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) {
// Keep dimension visible after drag
}

function updateDimensionLines(currentX, currentY) {
const startX = xScale(dragStart);
const endX = currentX;
const centerY = 20; //currentY - 30; // Offset above the drag point

// Update vertical lines
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);

// Update horizontal dimension line with arrows
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)');

// Update dimension text
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);
}

// Helper function to format duration
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`;
}
}

// CSS styles
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;
}
`;

// Add styles to document
const styleSheet = document.createElement('style');
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
// Apply drag behavior
svg.call(drag);
return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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