Published
Edited
Dec 10, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viz = {
// ------ settings ------
const height = 500;
const tablePosX = 400;
const tablePosY = 0;
const tableColWidth = 80;
const tableRowHeight = 20;
const tableFontSize = 14;
const columns = ({
likes: {
label: "Likes",
colorScale: d3.scaleSequential().domain([-50,100]).interpolator(d3.interpolateBlues),
},
dislikes: {
label: "Dislikes",
colorScale: d3.scaleSequential().domain([-50,100]).interpolator(d3.interpolateReds),
},
newAndDiff: {
label: "New/Different",
colorScale: d3.scaleSequential().domain([-50,100]).interpolator(d3.interpolatePurples),
},
});

// ------ helper functions ------
const line = d3.line(d => d.x, d => d.y);
const zoneLineGenerator = (d, i) => {
return line([
{
x: d.x + d.w / 2,
y: d.y + d.h / 2,
},
{
x: tablePosX - 4,
y: tablePosY + (tableRowHeight + 4) * i + tableRowHeight / 2,
},
]);
}
// ------ create base SVG elements ------
const svg = d3.create("svg").attr("id", "hz")
.attr("width", width)
.attr("height", height)
.attr("viewBox", `0 0 ${width} ${height}`);

const main = svg.append("g")
.attr("transform", "translate(20, 60)");
const packImage = main.append("g");
const packZones = main.append("g")
.attr("fill", "none")
.attr("stroke", "gray")
.attr("stroke-width", "0.1px")
.attr("stroke-opacity", 1);
const tableHeaders = main.append("g")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "baseline")
.attr("font-family", "Arial, sans-serif")
.attr("font-size", tableFontSize * 0.8)
.attr("text-decoration", "underline");
const tableBody = main.append("g");
const tableCellRects = main.append("g");
const tableCellText = main.append("g");
const zoneLines = main.append("g")
.attr("fill", "none")
.attr("stroke", "gray")
.attr("stroke-width", 1.5);

// ------ update SVG elements with data ------
function update(packObject) {

// --- count enter/update/exit zone selections ---
const zones = packZones
.selectAll("rect")
.data(packObject.zones, d => d.name);
// ------ create transitions ------
const totalTime = 1000;
const exitTime = zones.exit().size() > 0
? totalTime * 0.25
: 0;
const updateTime = zones.size() > 0
? totalTime
: 0;
const enterTime = zones.enter().size() > 0
? totalTime * 0.75
: 0;

const t_exit = svg.transition().delay(0).duration(exitTime);
const t_update = svg.transition().delay(0).duration(updateTime);
const t_revealZones = svg.transition().delay(totalTime - enterTime + enterTime * 0.00).duration(enterTime * 0.25);
const t_revealZoneLines = svg.transition().delay(totalTime - enterTime + enterTime * 0.00).duration(enterTime * 0.75);
const t_revealZoneRow = svg.transition().delay(totalTime - enterTime + enterTime * 0.50).duration(enterTime * 0.50);
const t1 = svg.transition().duration(updateTime);
const t2 = svg.transition().delay(updateTime/2).duration(enterTime);


// --- object permanence transition ---
zones
.join(
enter => enter.append("rect")
.attr("class", "zone")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.w)
.attr("height", d => d.h)
.attr("opacity", 0)
.transition(t_revealZones)
.attr("opacity", 1),
update => update
.transition(t_update)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.w)
.attr("height", d => d.h),
exit => exit
.transition(t_exit)
.attr("opacity", 0)
.remove(),
);

zoneLines
.selectAll("path")
.data(packObject.zones, d => d.name)
.join(
enter => enter.append("path")
.attr("d", zoneLineGenerator)
.call(revealPath, t_revealZoneLines),
update => update
.transition(t_update)
.attr("d", zoneLineGenerator),
exit => exit
.transition()
.attr("opacity", 0)
.remove(),
);


// --- todo: carousel transition ---
packImage
.selectAll("image")
.data([packObject])
.join("image")
.attr("x", 0)
.attr("y", 0)
.attr("width", 400)
.attr("height", 400)
.attr("href", d => d.imageUrl);

// --- no transition ---
tableHeaders
.selectAll("text")
.data(Object.keys(columns), key => key)
.join(
enter => enter.append("text")
.attr("x", (d, i) => tablePosX + (tableColWidth + 4) * i)
.attr("y", tablePosY)
.attr("dx", (tableColWidth - 4) / 2)
.attr("dy", -8)
.attr("fill", k => columns[k].colorScale(100))
.text(k => columns[k].label),
);
tableBody
.selectAll("g")
.data(packObject.zones, zone => zone.name)
.join(
enter => enter
.append("g")
.attr("opacity", 0)
.attr("transform", (d, i) => `translate(${tablePosX}, ${tablePosY + (tableRowHeight + 4) * i})`)
.each(createCells)
.transition(t_revealZoneRow)
.attr("opacity", 1)
.each(updateCells),
update => update
.transition(t_update)
.attr("transform", (d, i) => `translate(${tablePosX}, ${tablePosY + (tableRowHeight + 4) * i})`)
.each(updateCells),
exit => exit
.transition(t_exit)
.attr("opacity", 0)
.remove(),
);
}

function createCells(zone) {
const g = d3.select(this);
const keys = Object.keys(columns);
g.selectAll("rect")
.data(keys, k => k)
.join("rect")
.attr("x", (d, i) => (tableColWidth + 4) * i)
.attr("y", 0)
.attr("width", tableColWidth)
.attr("height", tableRowHeight)
.attr("fill", "lightgray");
g.selectAll("text")
.data(keys, k => k)
.join("text")
.attr("x", (d, i) => (tableColWidth + 4) * i + tableColWidth - 10)
.attr("y", tableRowHeight + 4)
.attr("dx", 0)
.attr("dy", -8)
.attr("text-anchor", "end")
.attr("dominant-baseline", "baseline")
.attr("font-family", "Arial, sans-serif")
.attr("font-weight", "bold")
.attr("font-size", tableFontSize)
.attr("fill", "white")
.text("-");
}

function updateCells(zone) {
const g = d3.select(this);
const keys = Object.keys(columns);
g.selectAll("rect")
.data(keys, k => k)
.join("rect")
.attr("fill", k => columns[k].colorScale(zone.results[k]));
g.selectAll("text")
.data(keys, k => k)
.join("text")
.text(k => zone.results[k] + "%");
}

return Object.assign(svg.node(), { update });
}
Insert cell
Insert cell
revealPath = (pathSelection, t) => {
if (!t) t = pathSelection.transition().duration(1000);
// store previous state
const dashArray = pathSelection.attr("stroke-dasharray");
const dashOffset = pathSelection.attr("stroke-dashoffset");

let lengths = [];
pathSelection
.style("visibility", "hidden")
.transition(t)
.on("start", function(d, i) {
lengths[i] = this.getTotalLength();
d3.select(this)
.attr("stroke-dasharray", `${lengths[i]} ${lengths[i]}`)
.attr("stroke-dashoffset", lengths[i])
.style("visibility", "visible");
})
.on("end", function() {
d3.select(this)
.attr("stroke-dasharray", dashArray)
.attr("stroke-dashoffset", dashOffset);
})
.attrTween("stroke-dashoffset", function(d, i) {
return d3.interpolateNumber(lengths[i], 0);
});
}
Insert cell
Insert cell
updateRects = (rectSelection, t) => {
if (!t) t = rectSelection.transition().duration(1000);
// store previous state
const dashArray = pathSelection.attr("stroke-dasharray");
const dashOffset = pathSelection.attr("stroke-dashoffset");


}
Insert cell
{
const svg = d3.create("svg")
.attr("width", width);
svg.selectAll("rect")
.data([
{x: 20, y: 20, w: 50, h: 50},
{x: 20, y: 20, w: 75, h: 75},
{x: 20, y: 20, w: 100, h: 100},
])
.join("rect")
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.w)
.attr("height", d => d.h)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 2)
.data([
{x: 360, y: 20, w: 100, h: 100},
{x: 240, y: 20, w: 75, h: 75},
{x: 120, y: 20, w: 50, h: 50},
])
.transition().duration(2000)
.attr("x", d => d.x)
.attr("y", d => d.y)
.attr("width", d => d.w)
.attr("height", d => d.h);
return svg.node();
}
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