Public
Edited
Oct 13, 2023
Importers
Insert cell
Insert cell
function drawWindow({ state, chars, showState = false }) {
const height = width * (1 / 7);
const svgWidth = width * (4 / 5);
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, svgWidth, height])
.attr("width", svgWidth);

const { start, end, label } = state;

const marginX = 5;

const xScale = d3
.scaleLinear()
.domain([-0.5, chars.length + 0.5]) // make room for the ends of the window
.range([marginX, svgWidth - 2 * marginX]);

const windowHeight = height / (7 / 3);
const windowUnit = xScale(1) - xScale(0);

const y = height / 3;

const letters = svg
.selectAll(".letters")
.data(chars)
.join("text")
.attr("class", "letters")
.attr("font-size", 36)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => xScale(i))
.attr("y", y)
.text((d) => d);

const rect = svg
.selectAll("rect.window")
.data([[start, end]])
.join("rect")
.attr("class", "window")
.attr("x", (d) => xScale(d[0] - 0.5))
.attr("y", y - windowHeight / 2)
.attr("width", ([start, end]) =>
Math.max(windowUnit * Math.max(0.125, end - start))
)
.attr("height", windowHeight)
.style("fill", "#189a18")
.attr("opacity", 0.5)
.attr("rx", 3);

if (showState) {
const variables = svg
.selectAll(".variables")
.data([[start], [end]])
.join("text")
.attr("class", "letters")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => xScale(d))
.attr(
"y",
(d, i) => y + windowHeight / 2 + 10 + (start === end ? 15 * i : 0)
)
.text((d, i) => (i === 0 ? "left" : "right"));

if (label && label.length > 0) {
const bracketHeight = 10;
const strokeWidth = 5;

const bracketGroup = svg
.selectAll(".lengthLabel")
.data([label])
.join("g")
.attr(
"transform",
([start, end]) =>
`translate(${xScale(start)}, ${y / 4 - bracketHeight})`
)
.attr("stroke", "#65a765")
.attr("opacity", 0.75)
.attr("stroke-width", strokeWidth);

bracketGroup
.append("line")
.attr("x1", 0)
.attr("x2", ([start, end]) => windowUnit * (end - start))
.attr("y1", 0)
.attr("y2", 0);

bracketGroup
.append("line")
.attr("x1", strokeWidth / 2)
.attr("x2", strokeWidth / 2)
.attr("y1", 0)
.attr("y2", bracketHeight);

bracketGroup
.append("line")
.attr(
"x1",
([start, end]) => windowUnit * (end - start) - strokeWidth / 2
)
.attr(
"x2",
([start, end]) => windowUnit * (end - start) - strokeWidth / 2
)
.attr("y1", 0)
.attr("y2", bracketHeight);
}

const stateData = extractVariables(state, ["start", "end", "label"]);
const stateSpaceMargin = 75;
const stateScale = d3
.scaleLinear()
.domain([0, stateData.length])
.range([stateSpaceMargin, width - 2 * stateSpaceMargin]);

svg
.selectAll(".state")
.data(stateData)
.join("text")
.attr("class", "state")
.attr("font-size", 16)
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => stateScale(i))
.attr("y", height * (19 / 20))
.text((d) => `${d.name}: ${toString(d.value)}`);
}


return svg.node();
}
Insert cell
function drawWindowInteractive({ state, chars, showState = false }) {
const height = width * (1 / 7);
const svgWidth = width * (4 / 5);
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, svgWidth, height])
.attr("width", svgWidth);

const { start, end, label } = state;

const marginX = 5;

const xScale = d3
.scaleLinear()
.domain([-0.5, chars.length + 0.5]) // make room for the ends of the window
.range([marginX, svgWidth - 2 * marginX]);

const windowHeight = height / (7 / 3);
const windowUnit = xScale(1) - xScale(0);

const y = height / 3;

const letters = svg
.selectAll(".letters")
.data(chars)
.join("text")
.attr("class", "letters")
.attr("font-size", 36)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => xScale(i))
.attr("y", y)
.text((d) => d);

const rect = svg
.selectAll("rect.window")
.data([[start, end]])
.join("rect")
.attr("class", "window")
.attr("x", (d) => xScale(d[0] - 0.5))
.attr("y", y - windowHeight / 2)
.attr("width", ([start, end]) =>
Math.max(windowUnit * Math.max(0.125, end - start))
)
.attr("height", windowHeight)
.style("fill", "#189a18")
.attr("opacity", 0.5)
.attr("rx", 3);

if (showState) {
const variables = svg
.selectAll(".variables")
.data([[start], [end]])
.join("text")
.attr("class", "letters")
.attr("font-size", 14)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => xScale(d))
.attr(
"y",
(d, i) => y + windowHeight / 2 + 10 + (start === end ? 15 * i : 0)
)
.text((d, i) => (i === 0 ? "left" : "right"));

if (label && label.length > 0) {
const bracketHeight = 10;
const strokeWidth = 5;

const bracketGroup = svg
.selectAll(".lengthLabel")
.data([label])
.join("g")
.attr(
"transform",
([start, end]) =>
`translate(${xScale(start)}, ${y / 4 - bracketHeight})`
)
.attr("stroke", "#65a765")
.attr("opacity", 0.75)
.attr("stroke-width", strokeWidth);

bracketGroup
.append("line")
.attr("x1", 0)
.attr("x2", ([start, end]) => windowUnit * (end - start))
.attr("y1", 0)
.attr("y2", 0);

bracketGroup
.append("line")
.attr("x1", strokeWidth / 2)
.attr("x2", strokeWidth / 2)
.attr("y1", 0)
.attr("y2", bracketHeight);

bracketGroup
.append("line")
.attr(
"x1",
([start, end]) => windowUnit * (end - start) - strokeWidth / 2
)
.attr(
"x2",
([start, end]) => windowUnit * (end - start) - strokeWidth / 2
)
.attr("y1", 0)
.attr("y2", bracketHeight);
}

const stateData = extractVariables(state, ["start", "end", "label"]);
const stateSpaceMargin = 75;
const stateScale = d3
.scaleLinear()
.domain([0, stateData.length])
.range([stateSpaceMargin, width - 2 * stateSpaceMargin]);

svg
.selectAll(".state")
.data(stateData)
.join("text")
.attr("class", "state")
.attr("font-size", 16)
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.attr("x", (d, i) => stateScale(i))
.attr("y", height * (19 / 20))
.text((d) => `${d.name}: ${toString(d.value)}`);
}

const expandable =
index < allStates.length - 1 &&
allStates[index].end === allStates[index + 1].end - 1;

const expand = svg
.selectAll("rect.expand")
.data([[start, end]])
.join("rect")
.attr("class", expandable ? "active expand" : "expand")
.attr("width", expanderWidth)
.attr("y", y - windowHeight / 2 + windowHeight / 4)
.attr(
"x",
([_, end]) =>
xScale(start - 0.5) +
Math.max(windowUnit * (end - start), expanderWidth)
)
.attr("height", windowHeight * 0.5)
.style("fill", "grey")
.attr("opacity", 0.25)
.attr("rx", 1)
.style("cursor", expandable ? "pointer" : "default");

if (expandable)
expand.call(
expanderClosure({ expanderWidth, xScale, windowUnit, selection: svg })
);

if (start !== end) {
const shrinkable =
index < allStates.length - 1 &&
allStates[index].start === allStates[index + 1].start - 1;
const shrink = svg
.selectAll("rect.shrink")
.data([[start, end]])
.join("rect")
.attr("class", shrinkable ? "active shrink" : "shrink")
.attr("width", expanderWidth)
.attr("y", y - windowHeight / 2 + windowHeight / 4)
.attr("x", ([start]) => xScale(start - 0.5) - expanderWidth)
.attr("height", windowHeight * 0.5)
.style("fill", "grey")
.attr("opacity", 0.25)
.attr("rx", 1)
.style("cursor", shrinkable ? "pointer" : "default");

if (shrinkable) {
shrink.call(
shrinkerClosure({ expanderWidth, xScale, windowUnit, selection: svg })
);
}
}

return svg.node();
}
Insert cell
function expanderClosure(config) {
const { svg, expanderWidth, xScale, windowUnit, selection } = config;

function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
selection.select("rect.window").raise().attr("stroke", "black");
}

// needs to update mutable when it ends.
function dragended(event, d) {
d3.select(this).raise().attr("stroke", null);
selection.select("rect.window").attr("stroke", null);
mutable index++;
}

function dragged(event, d) {
const dragX = d3.select(this).attr("x");

const [[start, end]] = d3.select(this).data();

if (event.x > parseFloat(dragX) + expanderWidth) {
const maxX = xScale(end + 0.5) - expanderWidth;

d3.select(this).attr("x", (d.x = Math.min(event.x, xScale(end + 0.5))));

const windowX = selection.select("rect.window").attr("x");

const maxWindowWidth = Math.min(
windowUnit * (end - start + 1),
event.x - windowX
);

selection.select("rect.window").attr("width", (d.width = maxWindowWidth));
}
}

return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
function shrinkerClosure(config) {
const { svg, expanderWidth, xScale, windowUnit, selection } = config;

function dragstarted(event, d) {
d3.select(this).raise().attr("stroke", "black");
// selection.select("rect.window").raise().attr("stroke", "black");
}

// needs to update mutable when it ends.
function dragended(event, d) {
d3.select(this).raise().attr("stroke", null);
selection.select("rect.window").attr("stroke", null);
mutable index++;
}

function dragged(event, d) {
const dragX = d3.select(this).attr("x");

const [[start, end]] = d3.select(this).data();

const newX = Math.max(dragX, Math.min(event.x, xScale(start + 1 - 0.5)));

if (event.x <= xScale(start + 1 - 0.5) && event.x > xScale(start - 0.5)) {
d3.select(this).attr("x", (d.x = newX - expanderWidth));
const window = selection.select("rect.window");
const windowX = xScale(start - 0.5);
const delta = Math.max(event.x - windowX, 0);
const windowWidth = windowUnit * (end - start);

window
.attr("x", (d.x = event.x))
.attr(
"width",
(d.width = Math.min(
windowUnit * (end - start + 1),
windowWidth - delta
))
);
}
}

return d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
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