Hi, I have a chart that, when clicked, allows me to filter an external dataset that I display in an Input.Table. The interaction works fine, however, I need the chart to be displayed inside a div. I’m using Observable Framework, and this is all the code.
function grid_rank(data, {width}={}) {
/* ---------- 1. derived sets & helpers ------------------- */
const officials = Array.from(new Set(data.map(d => d.Official)));
const statuses = Array.from(new Set(data.map(d => d.Status))); // P columns
const maxByOfficial = Object.fromEntries(
d3.groups(data, d => d.Official)
.map(([ofc, arr]) => [ofc, d3.max(arr, d => d.Order)])
);
const globalMaxOrder = d3.max(data, d => d.Order);
let clicked = null;
/* count proposals per (Official, Status) */
const counts = d3.rollup(
data,
v => v.length,
d => d.Official,
d => d.Status
); // Map<Official, Map<Status, count>>
/* avg Days per Status (across ALL officials) */
const avgDays = Object.fromEntries(
d3.groups(data, d => d.Status)
.map(([st, arr]) => [st, d3.mean(arr, d => d.Days)])
);
/* target look-up */
const targetMap = Object.fromEntries(
tobe_ans_stages.map(d => [d.statusTime, d.target])
);
const headerColour = st => {
const diff = (targetMap[st] ?? 0) - (avgDays[st] ?? 0);
if (diff >= 2) return "#006BA2"; // green
if (diff >= 0) return "#FFD700"; // yellow
return "#a81414"; // red
};
/* ---------- 2. layout & scales -------------------------- */
const margin = { top: 36, right: 30, bottom: 20, left: 160 };
const rowH = 50;
const bandW = 48; // width per status column
const gap = 28; // space between grids
const height = officials.length * rowH + margin.top + margin.bottom;
const xStatus = d3.scaleBand()
.domain(statuses)
.range([margin.left, margin.left + statuses.length * bandW])
.paddingInner(0.15);
const xOrder = d3.scaleLinear()
.domain([1, globalMaxOrder])
.range([xStatus.range()[1] + gap, width - margin.right]);
const yScale = d3.scaleBand()
.domain(officials)
.range([margin.top, height - margin.bottom])
.paddingInner(0.35);
const sym = {
"Under Review": d3.symbolCircle,
"Under Consultation": d3.symbolSquare,
"Report Prep": d3.symbolTriangle
};
/* ---------- 3. root SVG & tooltip ----------------------- */
const div = d3.create("div")
.style("overflow-x", "auto")
.style("font-variant-numeric", "tabular-nums");
const svg = div.append("svg")
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");
const tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("pointer-events", "none")
.style("visibility", "hidden")
.style("background", "#fff")
.style("border", "1px solid #ccc")
.style("border-radius", "4px")
.style("padding", "6px 8px")
.style("font-size", "12px");
const element = div.node();
element.value = null;
/* ---------- 4. grid lines ------------------------------- */
/* status-grid verticals */
statuses.forEach(st => {
const x = xStatus(st);
svg.append("line")
.attr("x1", x).attr("x2", x)
.attr("y1", margin.top).attr("y2", height - margin.bottom)
.attr("stroke", "#000").attr("stroke-dasharray", "2,2")
.attr("stroke-width", 1).attr("opacity", 0.35);
});
/* trailing boundary */
svg.append("line")
.attr("x1", xStatus.range()[1]).attr("x2", xStatus.range()[1])
.attr("y1", margin.top).attr("y2", height - margin.bottom)
.attr("stroke", "#000").attr("stroke-dasharray", "2,2")
.attr("stroke-width", 1).attr("opacity", 0.35);
/* order-grid verticals */
for (let i = 1; i <= globalMaxOrder; ++i) {
svg.append("line")
.attr("x1", xOrder(i)).attr("x2", xOrder(i))
.attr("y1", margin.top).attr("y2", height - margin.bottom)
.attr("stroke", "#000").attr("stroke-dasharray", "2,2")
.attr("stroke-width", 1).attr("opacity", 0.35);
}
/* horizontals + left label */
officials.forEach(ofc => {
const y = yScale(ofc);
svg.append("line")
.attr("x1", margin.left)
.attr("x2", xOrder(globalMaxOrder))
.attr("y1", y).attr("y2", y)
.attr("stroke", "#000").attr("stroke-dasharray", "2,2")
.attr("stroke-width", 1).attr("opacity", 0.35);
svg.append("text")
.attr("x", margin.left - 10).attr("y", y).attr("dy", "0.35em")
.style("text-anchor", "end").style("font-size", 12)
.text(`${ofc} (Pptas: ${maxByOfficial[ofc]})`);
});
/* ---------- 5. status-grid cell counts ----------------- */
officials.forEach(ofc => {
statuses.forEach(st => {
const cnt = counts.get(ofc)?.get(st) ?? 0;
svg.append("text")
.attr("x", xStatus(st) + xStatus.bandwidth() / 2)
.attr("y", yScale(ofc)).attr("dy", "0.35em")
.style("text-anchor", "middle").style("font-size", 13)
.text(cnt);
});
});
/* ---------- 6. header shapes (colour by avg vs target) -- */
statuses.forEach(st => {
svg.append("path")
.datum({ status: st })
.attr("d", d3.symbol().type(sym[st]).size(240)())
.attr("transform", `translate(${xStatus(st) + xStatus.bandwidth() / 2}, ${margin.top - 18})`)
.attr("fill", headerColour(st))
.attr("stroke", "black")
.style("cursor", "pointer") // visual cue
.on("click", click)
});
/* ---------- 7. order-grid proposal shapes -------------- */
const colourByThreshold = d => {
const diff = d.Threshold - d.Days;
if (diff >= 2) return "#006BA2";
if (diff >= 0) return "#FFD700";
return "#a81414";
};
data.forEach(d => {
const cx = xOrder(d.Order);
const cy = yScale(d.Official);
svg.append("path")
.datum(d)
.attr("d", d3.symbol().type(sym[d.Status]).size(250)())
.attr("transform", `translate(${cx}, ${cy})`)
.attr("fill", colourByThreshold(d))
.attr("stroke", "black")
.style("cursor", "pointer")
.on("mouseover", (event, d) => {
tooltip.style("visibility", "visible")
.html(`PptNo: ${d.PptNo}<br/>Banker: ${d.Banker}<br/>Status: ${d.Status}<br/>Client: ${d.Client}`);
})
.on("mousemove", event => {
tooltip.style("top", `${event.pageY + 10}px`)
.style("left", `${event.pageX + 10}px`);
})
.on("mouseout", () => tooltip.style("visibility", "hidden"))
.on("click", click)
});
function click(_, datum) {
if (datum ) {
clicked = datum;
element.value = datum;
} else {
clicked = null;
element.value = null;
}
element.dispatchEvent(new CustomEvent("input"));
}
/* ---------- 8. return ------------------------------- */
//return svg.node();
return element;
}
const sel_leader= view(Inputs.select(d3.group(data_grid, (d) => d.Banker),{width:7}));
const sel_office = view(Inputs.select(d3.group(data_grid, (d) => d.Official),{width:7}));
const selection_plot = view(grid_rank(data_grid,{width}));
display(Inputs.table( selection_plot === null ? data_grid : data_grid.filter((d) => d.Status === selection_plot.status)))
I tried to create the interaction using Mutable and d3.dispatch without any good results. My question is whether the interaction I created is correct or is there a better one, and how can I get the chart and table to display in a div?
Thanks in advance.