surveyCompareMatrix = (question1, question2, {
domain,
subset = [],
interactive = true,
limit = 8,
width,
height,
margin = ({top: 80, right: 20, bottom: 2, left: 150}),
rowHeight = 30,
colorInterpolator = d3.interpolateGreens,
title = true
} = {}) => {
if(!width) width = document.body.clientWidth
const surveyData = (subset.length ? subset : question1.data)
let q2 = getCountsLimit(question2, surveyData, limit)
let domain2 = q2.data.map(d => d.answer)
const compareData = domain2.flatMap(a2 => {
let filteredData = surveyData.filter(d => filterRow(a2, d, question2, domain2))
const compared = getCountsLimit(question1, filteredData, limit).data
.map(d => {
return {
a1: d.answer,
a2,
count: d.count,
columnPct: d.pct,
pct: d.count / surveyData.length,
q1: question1.question,
q2: question2.question,
}
})
return compared
})
// the available
let domain1 = Array.from(new Set(compareData.map(d => d.a1)))
// be a little responsive for the axis to have some more space if they need it
margin.left = d3.min([200, d3.max(domain1.map(d => d ? d.length : 0)) * 5 + 20])
margin.top = d3.min([100, d3.max(domain2.map(d => d ? d.length : 0)) * 3 + 20])
if(!height) height = domain1.length * rowHeight + margin.bottom + margin.top
let x = d3
.scaleBand()
.domain(domain2)
.range([margin.left, width - margin.right])
.paddingInner(0.2)
.align(0.5)
let y = d3
.scaleBand()
.domain(domain1.reverse())
.range([height - margin.bottom, margin.top])
.paddingInner(0.2)
.align(0.5)
let color = d3.scaleSequential(
// d3.extent(processedData, d => d.count),
d3.extent(compareData, d => d.count),
colorInterpolator
)
const container = d3.create("div")
.style("padding-bottom", "16px")
// .style("max-width", `${textWidth}px`)
const root = container.node()
root.value = { selected: subset, hovered: []}
if(title) {
container.append("div").classed("headline", true)
.style("font-style", "italic")
.style("line-height", "1.3em")
// .style("border-top", "solid 1px rgba(0, 0, 0, 0.1)")
.style("padding-top", "16px")
.style("padding-bottom", "8px")
.html(`${question1.question} <br> vs. <br> ${question2.question}`)
}
let svg = container.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("shape-rendering", "crispEdges")
// Ensure that custom font makes it into the downloaded SVG or PNG
// https://observablehq.com/@mootari/embedding-fonts-into-an-svg
svg.append("style")
.text(`
@font-face {
font-family: 'Roboto Condensed';
font-style: normal;
font-weight: 400;
src: url(${fontDataURL}) format('woff2');
}
.cell text {
font-variant-numeric: tabular-nums;
}
`);
// X Axis
let xaxis = svg.append("g")
.attr("transform", `translate(0,${margin.top})`)
.call(
d3
.axisTop(x)
.ticks(width / 80)
.tickSizeOuter(0)
.tickSize(-height + margin.bottom + margin.top + 12, 0, 0)
)
.attr("font-family", "Roboto Condensed")
xaxis.call(g => g.select(".domain").remove())
xaxis.selectAll(".tick line").style("stroke", "#efefef")
xaxis.selectAll("text")
.attr("transform", `translate(5, -18)rotate(${-45})`)
.attr("y", 0)
.attr("x", -10)
.attr("dy", ".2em")
.style("text-anchor", "start")
// Y Axis
let yaxis = svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y)
.ticks(height / 40)
.tickSize(-width + margin.left + margin.right + 10, 0, 0))
.attr("font-family", "Roboto Condensed")
yaxis.call(g => g.select(".domain").remove())
yaxis.selectAll(".tick line").style("stroke", "#efefef")
// the Rectangles
svg
.append("g").classed("rects", true)
.selectAll("rect")
// .data(processedData)
.data(compareData)
.join("rect")
.attr("x", d => x(d.a2))
.attr("width", d => x.bandwidth())
.attr("y", d => y(d.a1))
.attr("height", d => y.bandwidth())
.attr("fill", d => color(d.count))
.style("stroke", "#a9a9a9aa")
.attr("stroke-width", .5)
.style("cursor", "pointer")
.on("click", function(evt, selected) {
// clicking will select this rectangle and pin it until it or another one is clicked
let data = filterSelected(selected)
if(root.value.selectedAnswer == selected) {
root.value.selected = []
root.value.selectedAnswer = null
d3.select(this).style("stroke", "#a9a9a9aa")
.style("stroke-width", 1)
} else {
root.value.selectedAnswer = selected//selected.a1 + gseparator + selected.a2
root.value.selected = data
svg.selectAll("g.rects rect")
.style("stroke", "#a9a9a9aa")
.style("stroke-width", 1)
d3.select(this).style("stroke", "#111")
.style("stroke-width", 2)
}
root.dispatchEvent(new CustomEvent("input"));
})
.on("mouseover", function(evt, d) {
// if we have a selected rect, don't do hover behavior
if(root.value.selected && root.value.selected.length) return;
d3.select(this).style("stroke", "#111")
.style("stroke-width", 2)
let data = filterSelected(d)
root.value.hoveredAnswer = d//d.a1 + gseparator + d.a2
root.value.hovered = data
root.dispatchEvent(new CustomEvent("input"));
})
.on("mouseout", function(evt, d) {
if(root.value.selected && root.value.selected.length) return;
d3.select(this).style("stroke", "#a9a9a9aa")
.style("stroke-width", 1)
})
function filterSelected(selected) {
let data = surveyData.filter(d => {
return filterRow(selected.a1, d, question1, domain1)
&& filterRow(selected.a2, d, question2, domain2)
})
return data
}
let pct = d3.format(".0%")
// The counts
svg
.append("g").classed("cell", true)
.selectAll("text")
// .data(processedData)
.data(compareData)
.join("text")
.attr("x", d => x(d.a2) + x.bandwidth() / 2)
.attr("y", d => y(d.a1) + y.bandwidth() / 2)
.attr("dy", ".35em")
.attr("fill", d => d3.lab(color(d.count)).l < 55 ? "white" : "black")
.style("pointer-events", "none")
.attr("font-size", "12px")
.attr("font-family", "Roboto Condensed")
.attr("text-anchor", "middle")
// .text(d => d.count)
.text(d => d.count + " (" + pct(d.pct) + ")")
return root
}