Public
Edited
May 5
Insert cell
Insert cell
svgNode = importSVGnode(humanBodySVG)

Insert cell
body=d3.select(svgNode)
Insert cell
///body.select("#knee_l").style("fill","red")
Insert cell
rawData = FileAttachment("injury_data@3.csv").csv({typed: true});
Insert cell
rawData.forEach(d => {
let path = body.select("#" + d["Body Part"].toLowerCase() + "_l");
let valueField = "Womens Avg";
path.transition().duration(1000).style("fill", colorScale(d[valueField]));
})

Insert cell
rawData.forEach(d => { // for each row in rawdata
let path = body.select("#" + d["Body Part"].toLowerCase() + "_r");
// using id, append left
let valueField = "Mens Avg";
path.transition().duration(1000).style("fill", colorScale(d[valueField]));
})
Insert cell
function calculateAverages(data) {
if (!data || data.length === 0) return [];

const womenCols = ["Womens Basketball", "Womens Track", "Womens Swimming", "Womens Soccer", "Womens Tennis", "Womens Water Polo", "Womens Cross Country"];
const menCols = ["Mens Basketball", "Mens Track", "Mens Swimming", "Mens Soccer", "Mens Tennis", "Mens Water Polo", "Mens Cross Country"];

return data.map(d => {
const womenSum = womenCols.reduce((sum, col) => sum + (d[col] || 0), 0);
const menSum = menCols.reduce((sum, col) => sum + (d[col] || 0), 0);

const womenAvg = womenCols.length > 0 ? womenSum / womenCols.length : 0;
const menAvg = menCols.length > 0 ? menSum / menCols.length : 0;

const label = d["Body Part"] ? d["Body Part"].toLowerCase().replace(/[ \/]/g, "_") : null;

return { label, womenAvg, menAvg };
}).filter(d => d.label && d.label !== 'total');
}
Insert cell
averagedData = calculateAverages(rawData)
Insert cell
dataLabelToSvgBase = ({
"ankle": "ankle",
"arm": "arm",
"back": "back",
"face": "face",
"foot": "foot",
"hand": "hand",
"hip": "hip",
"knee": "knee",
"calf": "calf",
"shoulder": "shoulder",
"thigh": "thigh",
"other": "other",
})
Insert cell
transformedData = averagedData.flatMap(d => {
const svgBaseName = dataLabelToSvgBase[d.label];
if (!svgBaseName) {
return [];
}

const results = [];
if (d.menAvg !== undefined && !isNaN(d.menAvg)) {
results.push({ label: `${svgBaseName}_r`, value: d.menAvg });
}
if (d.womenAvg !== undefined && !isNaN(d.womenAvg)) {
results.push({ label: `${svgBaseName}_l`, value: d.womenAvg });
}
return results;
}).filter(d => d.label !== "other_l" && d.label !== "other_r");
Insert cell
maxVal = d3.max(transformedData, d => d.value) || 1
Insert cell
// Cell 12: Create the color scale
colorScale = d3.scaleLinear()
.domain([0, maxVal])
.range(["lightblue", "red"])
Insert cell
humanBodySVG = FileAttachment("body2@1.svg").text()
Insert cell
import {importSVGnode} from "@emfielduva/dvlib"
Insert cell
import { Inputs } from "@observablehq/inputs"

Insert cell
// Cell: Simple Tooltip Container Div
tooltipContainerSimple = html`<div
class="simple-svg-tooltip"
style="position: absolute;
visibility: hidden;
background: white;
border: 1px solid #ccc;
padding: 5px 8px;
border-radius: 4px;
font-size: 10px;
font-family: sans-serif;
pointer-events: none; /* Important */
white-space: nowrap;
z-index: 999;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);"
>Tooltip</div>`
Insert cell
mutable selectedView = "overview"
Insert cell
viewDefinitions = ({
overview: [0, 0, 145.52, 261.94],
left_ankle: [58, 220, 20, 20],
right_knee: [70, 180, 25, 25],
head_area: [60, 5, 25, 35],
left_arm: [30, 50, 30, 80],
torso: [55, 35, 35, 80]

})
Insert cell
svgFinalOutput = {
const svgSelection = d3.select(svgNode);

const tooltip = d3.select(tooltipContainerSimple);
if (tooltip.empty()){
console.error("Simple tooltip container selection failed!");
return svgNode;
}

let minX = 0, minY = 0, svgWidth = 550, svgHeight = 990;
const viewBoxAttr = svgNode.getAttribute("viewBox");
if (viewBoxAttr) {
try {
const parts = viewBoxAttr.split(" ").map(Number);
if (parts.length === 4 && parts.every(n => !isNaN(n))) {
[minX, minY, svgWidth, svgHeight] = parts;
} else throw new Error("Invalid viewBox value format");
} catch (e) {
console.error("Error parsing viewBox:", e, "Using fallback dimensions.");
}
} else {
console.warn("SVG viewBox attribute missing, using fallback dimensions.");
}
const centerX = minX + svgWidth / 2;

svgSelection.append("line")
.attr("class", "split-line")
.attr("x1", centerX - 1)
.attr("y1", minY)
.attr("x2", centerX - 1)
.attr("y2", minY + svgHeight)
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");

const labelYPosition = minY + 15;
const labelFontSize = "12px";


svgSelection.append("text")
.attr("class", "side-label female-label")
.attr("x", minX + 15)
.attr("y", labelYPosition)
.attr("text-anchor", "start")
.attr("dominant-baseline", "hanging")
.style("font-size", labelFontSize)
.style("font-weight", "bold")
.text("Female");

svgSelection.append("text")
.attr("class", "side-label male-label")
.attr("x", minX + svgWidth - 15)
.attr("y", labelYPosition)
.attr("text-anchor", "end")
.attr("dominant-baseline", "hanging")
.style("font-size", labelFontSize)
.style("font-weight", "bold")
.text("Male");

svgSelection.selectAll("path[id]")
.style("fill", "#d3d3d3").style("cursor", "default")
.on("pointerover", null).on("pointermove", null).on("pointerout", null);
transformedData.forEach(d => {
const selector = `#${d.label}`;
const pathElement = svgSelection.select(selector);
if (!pathElement.empty()) {
pathElement.style("fill", colorScale(d.value)).style("cursor", "pointer");
pathElement
.on("pointerover", function(event) { tooltip.html(`${d.label}: ${d.value.toFixed(2)}`).style("left", (event.pageX + 10) + "px").style("top", (event.pageY - 15) + "px").style("visibility", "visible"); d3.select(this).style("stroke", "black").style("stroke-width", "2px").raise(); })
.on("pointermove", function(event) { tooltip.style("left", (event.pageX + 10) + "px").style("top", (event.pageY - 15) + "px"); })
.on("pointerout", function() { tooltip.style("visibility", "hidden"); d3.select(this).style("stroke", null).style("stroke-width", null); });
}
});

const currentViewKey = mutable selectedView;
const targetViewBoxParams = viewDefinitions[currentViewKey];

if (targetViewBoxParams && targetViewBoxParams.length === 4) {
svgSelection
.transition("zoom_transition")
.duration(750)
.attr("viewBox", targetViewBoxParams.join(" "));
} else {
console.warn(`Zoom: Invalid view definition for "${currentViewKey}" in svgFinalOutput cell.`);

}
}
return svgNode;
}
Insert cell

<div style="position: relative; width: 900px;">
<style>
#narrativeTextBox {
position: absolute;
width: 300px;
top: 30px;
left: 550px;
font-size: 14px;
}
a {
color: blue;
font-weight: bold;
}
a:hover {
cursor: pointer;
color: orange;
}
</style>

<div id="svgContainer"></div>

<div id="narrativeTextBox">
<h3>Women get injured more than men ?</h3>
<p>
Explore how <strong>male</strong> and <strong>female</strong> injury counts compare across the body. The body is divided into
<a id="upperBody">upper body</a>,
<a id="middleBody">middle body</a>, and
<a id="lowerBody">lower body</a> regions.
The closer to pure red on each body part, the more injuries were reported in that area.
You can click <a id="highlight">highlight</a> and <a id="resetHighlight">reset highlight</a> to emphasize the most different areas between male and female injury rates.
Use these controls to interactively explore where injuries concentrate and how they differ across genders. Other controls:
<a id="fullview">reset</a>,
<a id="turnOn">on</a>,
<a id="turnOff">off</a>
</p>
</div>
</div>


Insert cell
d3.select(narrativeBox.querySelector("#svgContainer"))
.node()
.appendChild(svgNode);


Insert cell
function zoomToRegion(selectors) {
const nodes = selectors
.map(sel => d3.select(svgNode).select(sel).node())
.filter(n => n);

if (nodes.length === 0) return;

const bboxes = nodes.map(n => n.getBBox());

const x0 = d3.min(bboxes, b => b.x);
const y0 = d3.min(bboxes, b => b.y);
const x1 = d3.max(bboxes, b => b.x + b.width);
const y1 = d3.max(bboxes, b => b.y + b.height);

const width = x1 - x0;
const height = y1 - y0;
const padding = 10;

d3.select(svgNode)
.transition()
.duration(750)
.attr(
"viewBox",
`${x0 - padding} ${y0 - padding} ${width + padding * 2} ${height + padding * 2}`
);
}

Insert cell
{d3.select("#upperBody").on("click", () =>
zoomToRegion(["#shoulder_r", "#shoulder_l", "#arm_r", "#arm_l", "#hand_r", "#hand_l", "#face_r", "#face_l", "#back_r", "#back_l"])
);

d3.select("#middleBody").on("click", () =>
zoomToRegion(["#hip_r", "#hip_l", "#thigh_r", "#thigh_l"])
);

d3.select("#lowerBody").on("click", () =>
zoomToRegion(["#knee_r", "#knee_l", "#ankle_r", "#ankle_l", "#foot_r", "#foot_l", "#calf_r", "#calf_l"])
);

d3.select("#fullview").on("click", resetView);

d3.select("#highlight").on("click", highlight);
d3.select("#resetHighlight").on("click", () => {
d3.select(svgNode).selectAll("path")
.style("stroke", null)
.style("stroke-width", null);
});
d3.select("#turnOn").on("click", showAll);
d3.select("#turnOff").on("click", dimAll);
}
Insert cell
function resetView() {
const defaultViewBox = "0 0 145.52 261.94";
d3.select(svgNode)
.transition()
.duration(750)
.attr("viewBox", defaultViewBox);
}

Insert cell
function highlight() {
d3.select(svgNode).selectAll("path")
.style("stroke", "orange")
.style("stroke-width", 2);
}

Insert cell
function dimAll() {
d3.select(svgNode).selectAll("path")
.style("opacity", 0.2);
}

Insert cell
function showAll() {
d3.select(svgNode).selectAll("path")
.style("opacity", 1);
}
Insert cell
tooltip = d3.select("body").append("div")
.style("position", "absolute")
.style("visibility", "hidden")
.style("padding", "6px")
.style("background", "rgba(0,0,0,0.7)")
.style("color", "white")
.style("border-radius", "4px")
.style("font-size", "12px");
Insert cell
d3.select(svgNode)
.selectAll("path[id]")
.on("mouseover", function(event) {
const id = d3.select(this).attr("id");
const baseLabel = id.replace("_r", "").replace("_l", "");

const femaleData = transformedData.find(d => d.label === `${baseLabel}_l`);
const maleData = transformedData.find(d => d.label === `${baseLabel}_r`);

const femaleValue = femaleData ? femaleData.value.toFixed(2) : "N/A";
const maleValue = maleData ? maleData.value.toFixed(2) : "N/A";

d3.select(this).style("stroke", "blue");

tooltip.html(`<b>${baseLabel.toUpperCase()}</b><br/>
Female (right): ${femaleValue}<br/>
Male (left): ${maleValue}`)
.style("visibility", "visible");
})
.on("mousemove", function(event) {
tooltip.style("top", (event.pageY + 10) + "px")
.style("left", (event.pageX + 10) + "px");
})
.on("mouseout", function() {
d3.select(this).style("stroke", "black");
tooltip.style("visibility", "hidden");
});

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