surveyHistogram = (questionData, {
domain,
subset,
interactive = true,
multiselect,
width,
limit = 10,
rowHeight = 14,
barHeight = 14,
rowPadding = 4,
answerWidth = 60,
colorPrimary = histoGreen,
colorSecondary = histoGrey,
title = true
} = {}) => {
if(!width) width = document.body.clientWidth
const surveyData = questionData.data
const question = questionData.question;
if(!multiselect && !multiselect === false) multiselect = questionData.type === "multi";
const full = questionData.getCounts(surveyData, multiselect)
full.forEach(d => d.subset = { count: 0, pct: 0})
if(subset && subset.length) {
const sub = questionData.getCounts(subset, multiselect)
sub.forEach(s => {
let d = full.find(f => f.answer == s.answer)
d.subset.count = s.count
d.subset.pct = s.pct
})
} else {
subset = []
}
let data1 = full.slice(0, limit)
let other = d3.sum(full.slice(limit), d=> d.count)
let otherSubset = d3.sum(full.slice(limit), d => d.subset.count)
if(other) {
data1 = data1.concat([{
question: question,
answer: "Other",
count: other,
pct: other / surveyData.length,
subset: {
count: otherSubset,
pct: otherSubset / subset.length
}
}])
}
if(domain === undefined) {
domain = data1.map(d => d.answer)
} else {
// re-order the data by the given domain
data1 = d3.sort(data1, function(a,b) {
return domain.indexOf(a.answer) - domain.indexOf(b.answer)
})
}
const gap = 20
const h = Math.ceil((limit + 0.2) * rowHeight) + margin.top + margin.bottom
const w = width / 2 - gap
const x = d3.scaleLinear()
.domain([0, d3.max(data1, d => d.pct)])
.range([0, 100])
const y = d3.scaleBand()
.domain(d3.range(limit))
.rangeRound([margin.top, h - margin.bottom])
.padding(0.2)
const chart = d3.create("div")
.style("padding-bottom", "16px")
.style("max-width", `${width}px`)
const root = chart.node()
root.value = { selected: subset, hovered: []}
if(title) {
chart.selectAll(".headline")
.data([question])
.join("div")
.attr("class", "headline")
.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")
.text(d => d)
}
const questionContainer = chart.append("div")
.style("font-family", "sans-serif")
.style("font-size", "13px")
.style("line-height", "1.2em")
.attr("class", "question")
const row = questionContainer.selectAll(".row")
.data(data1)
.join("div")
.attr("class", "row")
.style("align-items", "center")
.style("height", `${rowHeight}px`)
.style("padding-top", `${rowPadding}px`)
.style("margin-top", `${rowPadding}px`)
.style("border-top", (d, i) => i === 0 ? "" : "dotted 1px rgba(0, 0, 0, 0.1)")
.style("display", "flex")
.style("gap", "10px")
if(interactive) {
row
.on("mouseover", function(event, hover) {
let data = (subset.length ? subset : surveyData)
.filter(d => filterRow(hover.answer, d, questionData, domain))
root.value.hovered = data
root.dispatchEvent(new CustomEvent("input"));
})
.on("mouseout", function(event, hover) {
root.value.hovered = []
root.dispatchEvent(new CustomEvent("input"));
})
.on("click", function(event, selected) {
let data = (subset.length ? subset : surveyData)
.filter(d => filterRow(selected.answer, d, questionData, domain))
if(root.value.selectedAnswer == selected.answer) {
root.value.selected = []
root.value.selectedAnswer = null
if(subset.length) {
questionContainer.selectAll(".bar.subset").style("background", colorPrimary)
} else {
questionContainer.selectAll(".bar.full").style("background", colorPrimary)
}
} else {
root.value.selectedAnswer = selected.answer
root.value.selected = data
if(subset.length) {
questionContainer.selectAll(".bar.subset").style("background", histoGreyLight)
.filter(d => d == selected)
.style("background", colorPrimary)
} else {
questionContainer.selectAll(".bar.full").style("background", colorSecondary)
.filter(d => d == selected)
.style("background", colorPrimary)
}
}
root.dispatchEvent(new CustomEvent("input"));
})
}
row.append("div")
.text(d => d.answer ? d.answer : "No response")
.style("color", d => d.answer ? "" : "grey")
.style("width", `${answerWidth}px`)
.style("flex-grow", "1")
.style("overflow-x", "hidden")
.style("white-space", "nowrap")
.style("text-overflow", "ellipsis")
row.append("div")
.text(d => {
if(subset.length) {
return `${commaFormat(d.subset.count)} / ${commaFormat(subset.length)}`
}
return commaFormat(d.count)
})
.style("min-width", "40px")
.style("text-align", "right")
row.append("div")
.text(d => pctFormat(subset.length ? d.subset.pct : d.pct))
.style("width", "60px")
.style("text-align", "right")
const barContainer = row.append("div")
.style("width", "50%")
.style("height", `${barHeight}px`)
barContainer.append("div")
.classed("bar", true)
.classed("full", true)
.style("border-radius", "2px")
.style("background", d => {
if(subset.length) {
return colorSecondary
}
return colorPrimary
})
.style("height", `${barHeight}px`)
.style("width", d => x(d.pct) + "%")
.style("cursor", interactive ? "pointer" : "default")
if(subset.length) {
barContainer.append("div")
.style("position", "relative")
.classed("bar", true)
.classed("subset", true)
.style("border-radius", "2px")
.style("background", d => {
return colorPrimary
})
.style("top", `${-barHeight / 2 - barHeight/5}px`)
.style("height", `${barHeight / 2.5}px`)
.style("width", d => x(d.subset.pct) + "%")
.style("pointer-events", "none")
}
return root
}