Public
Edited
Jan 15, 2024
Insert cell
Insert cell
Insert cell
chart_select = {
const highlightColor = "#ffd700"; // Bright yellow for highlighting
const mapWidth = 980; // Set the desired width for the map
const mapHeight = 800; // Set the desired height for the map
const svg = d3.create("svg")
.attr('viewBox', `0 0 ${mapWidth} ${mapHeight}`)
.attr('preserveAspectRatio', 'xMidYMid meet')
.style('max-width', '100%').style('height', 'auto');
// Create a mapping from class names to indexes
const classNameToIndex = {};
d3.cross([0, 1, 2], [0, 1, 2]).forEach((d, i) => {
const className = bivariateClass({ var2: d[0], var1: d[1] });
classNameToIndex[className] = i;
});

// Adjust the color scale to use this mapping
const colorScale = d3.scaleOrdinal()
.domain(Object.keys(classNameToIndex))
.range(scheme);

const index = new Map(data.map(({ county, ...rest }) => [county, rest]));
const selectedStateId = Array.from(states).find(([key, value]) => value.name === selectedState)?.[0];
const filteredCounties = topojson.feature(us, us.objects.counties).features.filter(d => d.id.startsWith(selectedStateId));
const selectedStateFeature = topojson.feature(us, us.objects.states).features.find(d => d.id === selectedStateId);

// Define the adjusted projection
const projection = d3.geoIdentity().fitSize([900, 500], selectedStateFeature); // map size
const path = d3.geoPath().projection(projection);

// Draw counties with a check for undefined values
svg.selectAll("path.county")
.data(filteredCounties)
.join("path")
.attr("class", d => {
const value = index.get(d.id);
if (!value) return "county county-no-data";
return `county ${bivariateClass(value)}`;
})
.attr("d", path)
.attr("fill", d => {
const value = index.get(d.id);
if (!value) return "#ccc"; // Default color for missing data

// Use direct color assignment based on the same logic as the legend
const { var2: a, var1: b } = value;
const indexA = isNaN(a) ? 0 : +(a > var2_thresholds[0]) + (a > var2_thresholds[1]);
const indexB = isNaN(b) ? 0 : +(b > var1_thresholds[0]) + (b > var1_thresholds[1]);
const colorIndex = indexA * 3 + indexB;

return scheme[colorIndex]; // Directly use the scheme array
})
.attr("stroke", "white")
.attr("stroke-width", 0.125)
.each(function(d) { // Use 'each' to append a <title> element to each path
const name = `${d.properties.name}, ${states.get(d.id.slice(0, 2)).name}`;
const value = index.get(d.id);
let tooltipText = `${name}\n`;
if (!value || (isNaN(value.var1) && isNaN(value.var2))) {
tooltipText += "No data";
} else {
tooltipText += `${var1_label}: ${isNaN(value.var1) ? "No Data" : value.var1}\n`;
tooltipText += `${var2_label}: ${isNaN(value.var2) ? "No Data" : value.var2}`;
}
d3.select(this).append("title").text(tooltipText); // Append <title> element with the tooltip text
})
.on("mouseover", function(event, d) {
// Optional: highlight the county
d3.select(this).attr("stroke", highlightColor).attr("stroke-width", 8);

// Get the class name for the corresponding legend item
const value = index.get(d.id);
if (value) {
const { var2: a, var1: b } = value;
const indexA = isNaN(a) ? 0 : +(a > var2_thresholds[0]) + (a > var2_thresholds[1]);
const indexB = isNaN(b) ? 0 : +(b > var1_thresholds[0]) + (b > var1_thresholds[1]);
const colorIndex = indexA * 3 + indexB;
const legendClass = `legend-color-${colorIndex}`;
// Highlight the corresponding legend square
d3.selectAll(`.legend-item.${legendClass}`)
.attr("stroke", highlightColor)
.attr("stroke-width", 5);
// Update the tooltip message
const message = getLegendMessage([indexB, indexA], var1_label, var2_label, scheme);
tooltip_message.html(message);
}
})
.on("mouseout", function(event, d) {
d3.select(this).attr("stroke", "white").attr("stroke-width", 0.125);
// Get the class name for the corresponding legend item
const value = index.get(d.id);
if (value) {
const { var2: a, var1: b } = value;
const indexA = isNaN(a) ? 0 : +(a > var2_thresholds[0]) + (a > var2_thresholds[1]);
const indexB = isNaN(b) ? 0 : +(b > var1_thresholds[0]) + (b > var1_thresholds[1]);
const colorIndex = indexA * 3 + indexB;
const legendClass = `legend-color-${colorIndex}`;
// Remove highlight from the corresponding legend square
d3.selectAll(`.legend-item.${legendClass}`)
.attr("stroke", "grey")
.attr("stroke-width", 1);
}
// Reset the tooltip message to default
tooltip_message.html("&#9432");
})
// click functionality
.on('click', function (event, d) {
d3.selectAll('.county').classed('selected', false);
d3.select(this).classed('selected', true);
mutable mapSelectValue = d.id; // 'd.id' is the county ID
});
// Draw state borders
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, (a, b) => a !== b))
.attr("fill", "none")
.attr("stroke", "white")
.attr("d", path);

// Now, append the legend. Adjust the transform to position the legend on the bottom left.
// The exact values for translation will depend on the size of your legend
const legendWidth = 100; // Width of the legend, adjust if necessary
const legendHeight = 150; // Height of the legend, adjust if necessary
const legendX = -5; // Distance from the left edge of the SVG
const legendY = mapHeight - legendHeight - 140; // Distance from the bottom edge of the SVG

// Append the legend group and translate it to the bottom left corner
svg.append("g")
.attr("transform", `translate(${legendX}, ${legendY})`)
.call(svg => svg.append(() => legend(colorScale, var1_label, var2_label, var1_definition, var2_definition)));

return svg.node();
}

Insert cell
function getLegendMessage(d, var1_label, var2_label, scheme) {
const level = ["low", "medium", "high"];
const levelColor = d => scheme[3 * d[1] + d[0]];
// Only apply color to the level text, not the entire label
const var1Level = `<span style="color: ${levelColor([d[0], 0])}; ">${level[d[0]]}</span>`;
const var2Level = `<span style="color: ${levelColor([0, d[1]])}; ">${level[d[1]]}</span>`;

// Construct the content with the variable labels in default color
const var1Content = `${var1_label} (${var1Level})`;
const var2Content = `${var2_label} (${var2Level})`;

return `<strong>${var1Content}<br>${var2Content}`;
};
Insert cell
legend = (colorScale, var1_label, var2_label, var1_definition, var2_definition) => {
const highlightColor = "#ffd700"; // Bright yellow for highlighting
const width = 300; // Width of the legend
const height = 200; // Height of the legend
const labelMarginVar1 = 100; // Extra space for Variable 1 label
const labelMarginVar2 = 30; // Reduced extra space for Variable 2 label
const svg = d3.create("svg")
.attr("width", 300 + labelMarginVar2) // Add extra space for Var2 label
.attr("height", 200 + labelMarginVar1); // Add extra space for Var1 label
// Calculate label positions
const labelOffset = 0;
const var1LabelX = width / 10;
const var1LabelY = 210 + labelOffset;
const var2LabelX = -labelOffset;
const var2LabelY = height / 4;

// Use cross to generate coordinates, with [0, 0] representing the bottom-left square
const squares = d3.cross([0, 1, 2], [0, 1, 2]);
const squareSize = 50; // Square size
const squareSpacing = 51; // Increased space between squares

svg.selectAll("rect")
.data(squares)
.join("rect")
// Adjust x and y to position squares from bottom-left to top-right
.attr("x", d => squareSpacing * d[0] + (width - squareSpacing * 3) / 2 - squareSize / 2)
.attr("y", d => height - squareSpacing * (d[1] + 1) + (height - squareSpacing * 3) / 2 - squareSize / 2)
.attr("width", squareSize)
.attr("height", squareSize)
.attr("fill", d => scheme[3 * d[1] + d[0]]) // Use the original indexing order
.attr("stroke", "grey")
.attr("stroke-width", 1)
.attr("class", d => `legend-item legend-color-${d[1] * 3 + d[0]}`)
.on("mouseover", function(event, d) {
let summary = ""; // Your existing logic for summary
// Highlight the legend box
d3.select(this)
.attr("stroke", highlightColor)
.attr("stroke-width", 5);

// Highlight corresponding counties on the map
const legendClass = `class-${d[1]}-${d[0]}`;
d3.selectAll(`.${legendClass}`)
.attr("stroke", highlightColor)
.attr("stroke-width", 8);
const color = scheme[3 * d[1] + d[0]];

const message = getLegendMessage(d, var1_label, var2_label, scheme);
tooltip_message.html(message);
})
.on("mouseout", function(event, d) {
tooltip_message.html("&#9432");
// Remove highlight from the legend box
d3.select(this)
.attr("stroke", "grey")
.attr("stroke-width", 1);

// Remove highlight from corresponding counties on the map
const legendClass = `class-${d[1]}-${d[0]}`;
d3.selectAll(`.${legendClass}`)
.attr("stroke", "white")
.attr("stroke-width", 0.125);
});

// Add Variable 1 axis label (bottom) with tooltip
svg.append("text")
.attr("x", width / 2.2)
.attr("y", height + labelMarginVar1 / 2)
.attr("text-anchor", "middle")
.text(var1_label)
.on("mouseover", function() {
tooltip_message.html(`<strong>${var1_label}</strong><br>${var1_definition}`);
})
.on("mouseout", function() {
tooltip_message.html("&#9432");
});

// Add Variable 2 axis label (side) with tooltip
svg.append("text")
.attr("transform", `translate(${labelMarginVar2 / 1.5}, ${height / 1.65}) rotate(-90)`)
.attr("text-anchor", "middle")
.text(var2_label)
.on("mouseover", function() {
tooltip_message.html(`<strong>${var2_label}</strong><br>${var2_definition}`);
})

.on("mouseout", function() {
tooltip_message.html("&#9432");
});

// Definitions for arrow markers
const arrowDefs = svg.append('defs');
arrowDefs.append('marker')
.attr('id', 'arrowhead')
.attr('markerWidth', 10)
.attr('markerHeight', 7)
.attr('refX', 10)
.attr('refY', 3.5)
.attr('orient', 'auto')
.append('polygon')
.attr('points', '0 0, 10 3.5, 0 7');

// X-axis arrow
svg.append('line')
.attr('x1', squareSize - 2)
.attr('x2', (squareSpacing + 4) * 4) // size of arrow
.attr('y1', height - 2)
.attr('y2', height - 2)
.attr('stroke-width', 2)
.attr('stroke', 'black')
.attr('marker-end', 'url(#arrowhead)');

// Y-axis arrow
svg.append('line')
.attr('x1', squareSize - 2)
.attr('x2', squareSize - 2)
.attr('y1', (squareSpacing) * 4 - 6)
.attr('y2', labelMarginVar1 / 2.5 - 12) // size of arrow
.attr('stroke-width', 2)
.attr('stroke', 'black')
.attr('marker-end', 'url(#arrowhead)');

// Threshold values along the x-axis
var1_thresholds.forEach((threshold, i) => {
svg.append('text')
.attr('x', squareSpacing * (i + 2))
.attr('y', height + labelMarginVar1 / 2 - 45) // Adjust as needed
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'hanging')
.text(threshold);
});

// X-axis ticks
var1_thresholds.forEach((threshold, i) => {
const xPos = squareSpacing * (i + 2) - 3;
svg.append('line')
.attr('x1', xPos)
.attr('y1', height - 2)
.attr('x2', xPos)
.attr('y2', height + 4) // Adjust as needed for tick length
.attr('stroke', 'black')
.attr('stroke-width', 1);
});

// Threshold values along the y-axis
var2_thresholds.forEach((threshold, i) => {
svg.append('text')
.attr('transform', `translate(${30}, ${(height - squareSpacing * (i + 1))}) rotate(-90)`) // Adjust as needed
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'hanging')
.text(threshold);
});

// Y-axis ticks
var2_thresholds.forEach((threshold, i) => {
const yPos = height - squareSpacing * (i + 1) - 2;
svg.append('line')
.attr('x1', squareSize - 2)
.attr('y1', yPos)
.attr('x2', squareSize - 6) // Adjust as needed for tick length
.attr('y2', yPos)
.attr('stroke', 'black')
.attr('stroke-width', 1);
});

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
mapSelectValue
Insert cell
mutable mapSelectValue = null;
Insert cell
// Question Container
viewof mapSelectInput = {
// Create a container
const form = html`<form>
<fieldset>
<legend style="margin-bottom: 1em;"> <strong>Select & Submit Question</strong>: <br>Click on any map county to select it, your selection will be highlighted in green.<br>Click the submit button to complete the practice task.</legend>

</div>
</fieldset>
</form>`;

// Return the value
form.oninput = event => {
form.value = form.elements.option.value;
};

// Initialize the form value
form.value = null;

return form;
}
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
// "2021-03"
var2_thresholds = [1, 2]
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
index = new Map(data.map(d => [d.county, d]));
Insert cell
dataFiltered = data.filter(d => d.state === selectedState)
Insert cell
Insert cell
us = FileAttachment("counties-albers-10m.json").json()
Insert cell
states = new Map(us.objects.states.geometries.map(d => [d.id, d.properties]))
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more