Public
Edited
Apr 25, 2023
Insert cell
Insert cell
chart = likertscales()
Insert cell
function likertscales() {
const numCols = dimensions.numCols
//enable tooltips as seen in https://observablehq.com/@clhenrick/tooltip-component
const container = html`<div class="tooltip-demo">
<style>
@font-face {
font-family: 'Raleway';
font-style: regular;
font-weight: 400;
src: url(${fontData}) format('truetype');
}
div.tooltip-demo > div.tooltip {
position: fixed;
display: none;
padding: 12px 6px;
background: #fff;
border: 1px solid #333;
pointer-events: none;
font-family: 'Raleway';
}
</style>
</div>`;
const rowScale = d3.scaleBand()
.domain(d3.range(sub_data.length))
.range([0, dimensions.visHeight])
.padding(0.05)

// create scales for each likert chart
const y = d3.scaleLinear()
.domain([0,1])
.range([(rowScale.bandwidth()+dimensions.margins.top)/2, rowScale.bandwidth()-dimensions.margins.top]);
const y2 = d3.scaleLinear()
.domain([0,1])
.range([(rowScale.bandwidth()+dimensions.margins.top)/2, dimensions.margins.top*2]);
const x = d3.scaleLinear()
.domain([0, maximum_X_axis])
.range([dimensions.margins.left, colScale.bandwidth()-dimensions.margins.right]);

// create axes for the likerts
const xAxis1 = d3.axisTop(x).ticks(maximum_X_axis/dimensions.thickTicksforEvery_x_Entries); //this gets us our bold ticks
const xAxis2 = d3.axisTop(x).ticks(maximum_X_axis/dimensions.thinTicksforEvery_x_Entries).tickFormat("").tickSize(0); //this gets us our thin ticks
const yAxis = d3.axisLeft(y);

// create the svg
const svg = d3
.select(container)
.append("svg")
.attr("viewBox", `0 0 ${dimensions.visWidth} ${dimensions.visHeight + dimensions.margins.top + dimensions.margins.bottom}`);

//enable tooltips as seen in https://observablehq.com/@clhenrick/tooltip-component
const div = d3
.select(container)
.append("div")
.classed("tooltip", true);
const tooltip = new Tooltip(div);
function tooltipContents(datum) {
const unit = (datum[1] == 1 ? design.xAxisUnit.slice(0,design.xAxisUnit.length-1) : design.xAxisUnit)
return `${datum[0]}: ${datum[1] + " " + unit} `;
}

const defs = html`
<defs>
<style>
@font-face {
font-family: 'Raleway';
font-style: regular;
font-weight: 400;
src: url(${fontData}) format('truetype');
}
<\style>
<\defs>`;

svg.append(() => defs);

//add background for title
svg.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr("width", "100%")
.attr("height", "100%")
.attr("fill", design.backgroundColor);

//add overall title
svg.append('text')
.attr('y', dimensions.margins.top)
.attr('x', dimensions.visWidth/2)
.attr("text-anchor", 'middle')
.attr("font-family", "Raleway")
.attr("fill", "black")
.attr("font-size", design.titleFontSize)
.attr("font-weight", 900)
.text(design.title)
//append each sub-group that represents 1 likert
const g = svg.append('g')
.attr('transform', `translate(0, ${dimensions.margins.top})`);

const likerts = g.selectAll('g')
.data(sub_data)
.join('g')
.attr('transform', (d, i) => {
const r = Math.floor(i / numCols);
const c = i % numCols ;
return `translate(${colScale(c)}, ${rowScale(r)})`;
});

//append background rectangle for each likert
likerts.append('rect')
.attr('width', "100%")
.attr('height', "100%")
.attr('fill', design.backgroundColor)
//append border
likerts.append('rect')
.attr('width', dimensions.visWidth - dimensions.margins.left/1.3)
.attr('height', rowScale.bandwidth() - dimensions.margins.top)
.attr("x", dimensions.margins.left/2)
.attr("y", dimensions.margins.top-design.questionFontSize/3)
.attr('fill', "none")
.attr('stroke', "#d5d1ab")
.attr("stroke-width", design.gridStrokeWidth*3)

//add rectangles over each border to leave space to write text
likerts.append("rect")
.attr("x", dimensions.margins.left-dimensions.labelMargins.left)
.attr("y", dimensions.margins.top-dimensions.labelMargins.top - design.questionFontSize)
.attr("width", d => label_lengths_post
.filter(e => e.info == d.question)[0].width
+ 2*dimensions.labelMargins.left)
.attr("height", d => label_lengths_post
.filter(e => e.info == d.question)[0].height
+ 2*dimensions.labelMargins.top)
.attr("fill", design.backgroundColor)
.attr("stroke", "none")

//add question titles for each likert
likerts.append('text')
.data(sub_data)
.join('text')
.attr("x", dimensions.margins.left)
.attr("y", dimensions.margins.top)
.attr("text-anchor", "start")
.attr("font-family", "Raleway")
.attr("font-size", design.questionFontSize)
.text(d => d.question);

// add axis for each likert
likerts.append("g")
.attr("transform", `translate(0, ${dimensions.margins.top+30})`)
.attr("color", design.gridColor)
.attr("stroke-width", 1.5)
.call(xAxis1)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll('.tick line') // add grid lines à la https://observablehq.com/@d3/connected-scatterplot
.clone()
.attr('stroke', design.gridColor)
.attr('stroke-width', design.gridStrokeWidth)
.attr('stroke-dasharray', '2,2')
.attr('y1', 0)
.attr('y2', (dimensions.visHeight/sub_data.length)-dimensions.margins.bottom-dimensions.margins.top-30))
.append("text")
.attr("x", dimensions.visWidth-dimensions.margins.right+8)
.attr("y", -8)
.attr("fill", design.gridColor)
.attr("text-anchor", "start")
.attr("font-family", 'Raleway')
.attr("font-size", design.labelFontSize * 0.7)
.attr("font-weight", 900)
.text(design.xAxisUnit);
likerts.append("g")
.attr("transform", `translate(0, ${dimensions.margins.top+30})`)
.call(xAxis2)
.call(g => g.select(".domain").remove())
.call(g => g.selectAll('.tick line') // add grid lines à la https://observablehq.com/@d3/connected-scatterplot
.clone()
.attr('stroke', design.gridColor)
.attr('stroke-width', design.gridStrokeWidth)
.attr('stroke-dasharray', '2,10')
.attr('y1', 0)
.attr('y2', (dimensions.visHeight/sub_data.length)-dimensions.margins.bottom-dimensions.margins.top-30))
.append("text")
.attr("x", dimensions.margins.left+6)
.attr("y", -8)
.attr("fill", design.gridColor)
.attr("text-anchor", "start")
.attr("font-family", 'Raleway')
.attr("font-size", design.labelFontSize * 0.7)
.attr("font-weight", 900)
.text(design.xAxisUnit);

//append sub-small multiples to draw each bubble color
likerts.append('g')
.attr('class', 'hi')
.attr("width", "100%")
.attr("height", "100%")
const bubbles = likerts.selectAll('.hi')
.data(d => d.construction)
.join('g')
.attr('transform', d => `translate (${colScale2(d.last_amt)+dimensions.margins.left}, 0)`)

//create scale and line function to generate bubbles
const xs = d3.scaleLinear()
.domain([0, maximum_X_axis])
.range([0, colScale.bandwidth()-dimensions.margins.left-dimensions.margins.right]);
const line1 = d3.line() //bottom half of the bubble
.x(d => xs(d["amt"]))
.y(d => y(d["ht"]))
const line2 = d3.line() //top half of the bubble
.x(d => xs(d["amt"]))
.y(d => y2(d["ht"]))

for(var k = design.desiredLayers-1; k >= 0; k--){
bubbles.append("path")
.attr("fill", d => d.color)
.attr("stroke", "black")
.attr("stroke-width", design.bubbleStrokeWidth)
.attr("d", d => line1.curve(d3.curveBundle.beta(1/design.desiredLayers*k))(d.pts))
.on("mouseover", (event, d) => tooltip.display([d.type, d.amt], tooltipContents))
.on("mouseout", () => tooltip.hide())
.on("mousemove", event => tooltip.move(event))

bubbles.append("path")
.attr("fill", d => d.color)
.attr("stroke", "black")
.attr("stroke-width", design.bubbleStrokeWidth)
.attr("d", d => line2.curve(d3.curveBundle.beta(1/design.desiredLayers*k))(d.pts))
.on("mouseover", (event, d) => tooltip.display([d.type, d.amt], tooltipContents))
.on("mouseout", () => tooltip.hide())
.on("mousemove", event => tooltip.move(event))
}


//add legend callouts
//this makes the rectangles
bubbles.filter(d => d.show_legend =="true" || d.show_legend =="TRUE").append("rect")
.attr("x", d => xs(d.pts[2].amt) - dimensions.margins.left/5)
.attr("y", (d, i) => (i%2 == 0 ? y(1.2)
- label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
:
y2(1.2)
-label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
))
.attr("width", d => label_lengths_post.filter(e => e.info == d.type)[0].width
+ 2*dimensions.labelMargins.left)
.attr("height", d => label_lengths_post.filter(e => e.info == d.type)[0].height
+ 2*dimensions.labelMargins.top)
.attr("fill", "white")
.attr("stroke", d => d.color)
.attr("stroke-width", design.labelStrokeWidth)

//this makes the text
bubbles.filter(d => d.show_legend =="true" || d.show_legend =="TRUE").append("text")
.attr("x", d => xs(d.pts[2].amt))
.attr("y", (d, i) => (i%2 == 0 ? y(1.2) : y2(1.2)))
.attr("fill", "black")
.attr("text-anchor", "start")
.attr("font-family", "Raleway")
.attr("font-size", design.labelFontSize)
.text(d => d.type)


//this makes the pointies
bubbles.filter(d => d.show_legend =="true" || d.show_legend =="TRUE").append("path")
.attr("fill", "white")
.attr("stroke", d => d.color)
.attr("d", (d, i)=> `M ${xs(d.pts[2].amt) - 5}
${(i%2 == 0 ? y(1.2)
- label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
+ 1.5
:
y2(1.2)
-label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
+ label_lengths_post.filter(e => e.info == d.type)[0].height
+ 2*dimensions.labelMargins.top)
-0.6 }
L ${xs(d.pts[2].amt)}
${(i%2 == 0 ? y(0.3) : y2(0.3))}
L ${xs(d.pts[2].amt) + 5}
${(i%2 == 0 ? y(1.2)
- label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
+ 1.5
:
y2(1.2)
-label_lengths_post.filter(e => e.info == d.type)[0].height
- dimensions.labelMargins.top/2
+ label_lengths_post.filter(e => e.info == d.type)[0].height
+ 2*dimensions.labelMargins.top)
-0.6 }`)
.attr("stroke-width", design.labelStrokeWidth)


//add credit blurb
if(design.showCredit){
svg.append('text')
.attr('y', dimensions.visHeight + dimensions.margins.bottom)
.attr('x', 7*dimensions.visWidth/10)
.attr("text-anchor", 'start')
.attr("font-family", "Raleway")
.attr("fill", "grey")
.attr("font-size", 10)
.text("made using observablehq.com/@racquellevia/stylized-likert-scales")
}


return container;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colScale.bandwidth(70)
Insert cell
Insert cell
a = colScale2(1)
Insert cell
b = colScale2(4)
Insert cell
c2 = 59+53
Insert cell
longest_subdata_construction = d3.max(sub_data, d => (d.construction).length)
Insert cell
Insert cell
Insert cell
fontData = toDataURL(await FileAttachment("Raleway-v4020-Regular.otf").url())
Insert cell
Insert cell
get_label_size = {
const svg = d3.create('svg')
.attr('width', width)
.attr('height', 130)

const defs = html`
<defs>
<style>
@font-face {
font-family: 'Raleway';
font-style: regular;
font-weight: 400;
src: url(${fontData}) format('truetype');
}
<\style>
<\defs>`;
svg.append(() => defs);
const labels = svg.selectAll('text')
.data(label_lengths_pre)
.join('text')
.attr("id", d =>d.info.replace(/[^a-z]+/gi, ''))
.text(d => d.info)
.attr("font-family", "Raleway")
.attr("font-size", d => ((d.type == "label") ? design.labelFontSize: design.questionFontSize))
.attr("fill", "black")
.attr("y", 50)
.attr('x', 10);

svg.append("text")
.attr("class", "blah")
.attr("y", 20)
.attr("x", 10)
.attr("font-size", 12)
.attr("fill", "black")
.text("This svg is used to calculate the lengths of chart text so that bounding boxes can be added behind them")

svg.append("text")
.attr("class", "blah")
.attr("y", 70)
.attr("x", 10)
.attr("font-size", 12)
.attr("fill", "black")
.text("We have to call bounding boxes outside of our chart to be compatable with Firefox")

svg.append("text")
.attr("class", "blah")
.attr("y", 80)
.attr("x", 10)
.attr("dy", "1em")
.attr("font-size", 12)
.attr("fill", "black")
.text("as per https://observablehq.com/@mbostock/wheres-getbbox")
svg.append("text")
.attr("class", "blah")
.attr("y", 100)
.attr("x", 10)
.attr("dy", "1em")
.attr("font-size", 12)
.attr("fill", "black")
.text(design.questionFontSize)

return svg.node()
}
Insert cell
fontLoaded = {
get_label_size;
return await document.fonts.ready;
}
Insert cell
label_lengths_post = {
fontLoaded;
var retval = JSON.parse(JSON.stringify(label_lengths_pre));
for(var i = 0; i < retval.length; i++){
var id = retval[i].info.replace(/[^a-z]+/gi, '')
retval[i].width = d3.select(get_label_size).select("#"+id).node().getBBox().width
retval[i].height = d3.select(get_label_size).select("#"+id).node().getBBox().height
}
return retval
}
Insert cell
Insert cell
Insert cell
Insert cell
import {toDataURL} from "@mootari/embedding-fonts-into-an-svg"
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