Public
Edited
Mar 2, 2023
1 star
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const tooltipDiv = d3.select(tooltipContainer);
const container = d3.create("div").style("min-height", "150px");
container.append(() => tooltipDiv.node());
const svg = container.append("svg").attr("viewBox", [0, 0, width, height]);

const bg = svg
.append("rect")
.classed("background", true)
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "white");

const labelCollapsed = svg
.append("text")
.attr("x", 0)
.attr("y", 12)
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.attr("opacity", 0)
.text("All Events");

const g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);

const xAxisGroup = g.append("g").call(xAxis);

const xAxis2Group = g.append("g").call(xAxis2);

const yAxisGroup = g.append("g").call(yAxis);

const eventsGroup = g.append("g").classed("events", true);

eventsGroup
.selectAll("rect")
.data(parsed)
.join("rect")
.attr("x", d => xScale(d.startDate))
.attr("y", d => yScale(d.id))
.attr("width", d => {
const w = Math.round(xScale(d.endDate) - xScale(d.startDate));
return w < rectProps.minWidth || isNaN(w) ? rectProps.minWidth : w;
})
.attr("height", 16)
.attr("fill", "cornflowerblue")
.attr("opacity", 0.7)
.style("mix-blend-mode", "multiply")
.each(function() {
d3.select(this).call(tooltip, tooltipDiv);
});

function collapse() {
eventsGroup
.selectAll("rect")
.transition()
.call(transition)
.attr("y", 0);
xAxisGroup
.transition()
.call(transition)
.attr("transform", `translate(0, ${rectProps.height})`);
xAxis2Group
.transition()
.call(transition)
.attr("opacity", 0);
yAxisGroup
.transition()
.call(transition)
.attr("opacity", 0);
svg
.transition()
.call(transition)
.attr("viewBox", [
0,
0,
width,
rectProps.height + margin.top + margin.bottom
]);
labelCollapsed
.transition()
.call(transition)
.attr("opacity", 1);
}

function expand() {
labelCollapsed.attr("opacity", 0);
eventsGroup
.selectAll("rect")
.transition()
.call(transition)
.attr("y", d => yScale(d.id));
xAxis2Group
.transition()
.call(transition)
.attr("opacity", 1);
xAxisGroup
.transition()
.call(transition)
.attr("transform", `translate(0, ${height - margin.bottom})`);
yAxisGroup
.transition()
.call(transition)
.attr("opacity", 1);
svg
.transition()
.call(transition)
.attr("viewBox", [0, 0, width, height]);
}

function toggle(event) {
if (event.target.value) {
collapse();
} else {
expand();
}
}

// thanks to Steven Tan (@chienleng) for the suggestion of adding an event listener to a view
viewof collapsed.addEventListener("input", toggle);

return container.node();
}
Insert cell
Insert cell
introDelayMs = 1500
Insert cell
mutable introPlayed = false
Insert cell
playIntro = {
if (!introPlayed) {
await Promises.delay(introDelayMs);
set(viewof collapsed, true);
mutable introPlayed = true;
}
}
Insert cell
Insert cell
viewof collapsed = Input(false)
Insert cell
// utility function for updating the collapsed Input value
function set(input, value) {
input.value = value;
input.dispatchEvent(new Event("input"));
}
Insert cell
Insert cell
tooltipContainer = html`<div class="gantt-tooltip">
<style>
div.gantt-tooltip {
position: absolute;
display: none;
top: 0;
left: -100000000px;
padding: 12px;
font-family: sans-serif;
font-size: 12px;
color: #333;
background-color: #fff;
border: 1px solid #333;
border-radius: 4px;
pointer-events: none;
}
div.gantt-tooltip p {
margin: 0;
}
</style>
<div class="gantt-tooltp--contents"></div>
</div>`
Insert cell
tooltip = (selection, tt) => {
selection
.on("mouseover", handleMouseover)
.on("mousemove", handleMousemove)
.on("mouseleave", handleMouseleave);

function handleMouseover() {
if (!viewof collapsed.value) {
darkenSel();
showTooltip();
setContents();
}
}

function handleMousemove(event) {
const [mouseX, mouseY] = d3.pointer(event, this);
setMoseoverDate(mouseX);
setFilteredData(mouseX);
if (!viewof collapsed.value) {
moveTooltip(mouseX, mouseY);
}
}

function handleMouseleave() {
if (!viewof collapsed.value) {
lightenSel();
hideTooltip();
}
}

function darkenSel() {
selection.attr("opacity", 1).style("mix-blend-mode", null);
}

function lightenSel() {
selection
.attr("opacity", rectProps.opacity)
.style("mix-blend-mode", "multiply");
}

function showTooltip() {
tt.style("display", "block");
}

function hideTooltip() {
tt.style("display", "none");
}

function moveTooltip(mouseX, mouseY) {
tt.style("top", yPos(mouseY)).style("left", xPos(mouseX));
}

function xPos(mouseX) {
return mouseX > width / 2
? `calc(${mouseX}px - ${tt.style("width")})`
: `${mouseX + 24}px`;
}

function yPos(mouseY) {
return mouseY > height / 2
? `calc(${mouseY - 24}px - ${tt.style("height")})`
: `${mouseY + 12}px`;
}

function setContents() {
if (!viewof collapsed.value) {
tt.selectAll("p")
.data(Object.entries(selection.datum()))
.join("p")
.html(
([key, value]) =>
`<strong>${key}</strong>: ${
typeof value === 'object' ? value.toLocaleString("en-US") : value
}`
);
} else {
tt.selectAll("p").remove();
}
}

function setMoseoverDate(mouseX) {
mutable mouseoverDate = dateXPos(mouseX).toLocaleString("en-US");
}

function setFilteredData(mouseX) {
mutable filteredData = dataAroundDate(dateXPos(mouseX));
}

function dateXPos(mouseX) {
return xScale.invert(mouseX);
}

function dataAroundDate(date) {
return parsed.filter(d => +d.startDate <= +date && +d.endDate >= +date);
}
}
Insert cell
Insert cell
transition = t => t.duration(1200).ease(d3.easeLinear)
Insert cell
xScale = d3
.scaleTime()
.domain(dateExtent)
.range([margin.left, width - margin.right - margin.left])
Insert cell
yScale = d3
.scaleBand()
.domain(ids)
.range([height - margin.bottom, margin.top])
Insert cell
xAxis = g =>
g
.classed("x-axis", true)
.call(d3.axisBottom(xScale).tickSizeInner(0))
.call(g => g.attr("transform", `translate(0, ${height - margin.bottom})`))
.call(g => g.select(".domain").remove())
Insert cell
xAxis2 = g =>
g
.classed("x-axis-2", true)
.attr("opacity", 1)
.attr("isolation", "isolate")
.call(
d3.axisBottom(xScale).tickSizeInner(height - margin.bottom - margin.top)
)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick text").remove())
.call(g => g.selectAll(".tick line").attr("stroke", "#ccc"))
Insert cell
yAxis = g =>
g
.classed("y-axis", true)
.attr("opacity", 1)
.call(d3.axisLeft(yScale).tickSizeInner(0))
.call(g => g.attr("transform", `translate(${margin.left}, 0)`))
.call(g => g.select(".domain").remove())
Insert cell
margin = ({ top: 0, left: 24, bottom: 24, right: 12 })
Insert cell
rectProps = ({ height: 16, padding: 2, minWidth: 2, opacity: 0.7 })
Insert cell
height = parsed.length * (rectProps.height + rectProps.padding) +
margin.top +
margin.bottom
Insert cell
Insert cell
mutable mouseoverDate = null
Insert cell
mutable filteredData = []
Insert cell
ids = parsed.map(d => d.id)
Insert cell
dateExtent = d3.extent(
d3.merge([parsed.map(d => d.startDate), parsed.map(d => d.endDate)])
)
Insert cell
parsed = data
.map(({ startDate, endDate, ...d }) => ({
...d,
startDate: new Date(startDate),
endDate: new Date(endDate)
}))
.sort((a, b) => b.startDate - a.startDate)
Insert cell
data
Insert cell
requestCategories = ["Residential Building Request"]
Insert cell
requestLimit = 30
Insert cell
Insert cell
import { data } with {
requestCategories as categories,
requestLimit as limit
} from "@clhenrick/hello-soda-api"
Insert cell
import { html } from "@observablehq/htl"
Insert cell
import { Button, Input } from "@observablehq/inputs"
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