Public
Edited
May 4
Fork of Simple SVG
1 fork
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
Insert cell
rawData.forEach(d => { // for each row in rawdata
let path = body.select("#" + d["Body Part"].toLowerCase() + "_l"); // using id, append left
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
// Cell: viewDefinitions (Keep as before - Ensure keys match link IDs)
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]
// ... etc ...
})
Insert cell
svgFinalOutput = {
const svgSelection = d3.select(svgNode);

// 2. Select the tooltip container (make sure tooltipContainerSimple is the actual div node)
const tooltip = d3.select(tooltipContainerSimple);
if (tooltip.empty()){
console.error("Simple tooltip container selection failed!");
// Return the clone without further modification if tooltip is broken
return svgNode;
}

// 3. Get SVG dimensions from viewBox (with error handling)
let minX = 0, minY = 0, svgWidth = 550, svgHeight = 990; // Sensible fallbacks
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;

// === Add Static Elements (ONCE per execution, to the clone) ===

// 1. Dividing Line
svgSelection.append("line")
.attr("class", "split-line") // Add class for potential external styling
.attr("x1", centerX - 1)
.attr("y1", minY) // Start at viewBox top
.attr("x2", centerX - 1)
.attr("y2", minY + svgHeight) // End at viewBox bottom
.attr("stroke", "black")
.attr("stroke-width", 1) // Thin line
.attr("stroke-dasharray", "3,3"); // Dashed pattern

// --- Text Labels AT THE TOP ---
const labelYPosition = minY + 15; // Y position near the top (adjust padding as needed)
const labelFontSize = "12px"; // Smaller font size might fit better at top

// 2. Female Label (Left Top)
svgSelection.append("text")
.attr("class", "side-label female-label")
.attr("x", minX + 15) // Pad from left edge
.attr("y", labelYPosition) // Set Y position
.attr("text-anchor", "start") // Align text start to X
.attr("dominant-baseline", "hanging") // Align TOP of text to Y position
.style("font-size", labelFontSize)
.style("font-weight", "bold")
.text("Female");

// 3. Male Label (Right Top)
svgSelection.append("text")
.attr("class", "side-label male-label")
.attr("x", minX + svgWidth - 15) // Pad from right edge
.attr("y", labelYPosition) // Set Y position
.attr("text-anchor", "end") // Align text end to X
.attr("dominant-baseline", "hanging") // Align TOP of text to Y position
.style("font-size", labelFontSize)
.style("font-weight", "bold")
.text("Male");


// === Dynamic Styling and Events ===
// (This part remains the same - applying defaults then iterating through data)

// 1. Apply Default Style and Remove Old Listeners first
svgSelection.selectAll("path[id]")
.style("fill", "#d3d3d3").style("cursor", "default")
.on("pointerover", null).on("pointermove", null).on("pointerout", null);
// 2. Iterate through processed data
transformedData.forEach(d => {
const selector = `#${d.label}`;
const pathElement = svgSelection.select(selector);
if (!pathElement.empty()) {
// Apply Styles
pathElement.style("fill", colorScale(d.value)).style("cursor", "pointer");
// Attach Events
pathElement
.on("pointerover", function(event) { /* ... tooltip show ... */ 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 move ... */ tooltip.style("left", (event.pageX + 10) + "px").style("top", (event.pageY - 15) + "px"); })
.on("pointerout", function() { /* ... tooltip hide ... */ tooltip.style("visibility", "hidden"); d3.select(this).style("stroke", null).style("stroke-width", null); });
}
});

// === Apply Zoom Transition based on mutable selectedView === <<< NEW SECTION >>>
const currentViewKey = mutable selectedView; // Get the current state
const targetViewBoxParams = viewDefinitions[currentViewKey]; // Look up the parameters

if (targetViewBoxParams && targetViewBoxParams.length === 4) {
// Apply the transition TO THE CLONE (svgSelection)
svgSelection
.transition("zoom_transition") // Named transition
.duration(750) // Animation duration
.attr("viewBox", targetViewBoxParams.join(" ")); // Set target viewBox
} else {
console.warn(`Zoom: Invalid view definition for "${currentViewKey}" in svgFinalOutput cell.`);
// Optional: You could apply the default 'overview' viewBox here as a fallback
// const overviewParams = viewDefinitions['overview'];
// if (overviewParams) {
// svgSelection.attr("viewBox", overviewParams.join(" ")); // Set directly, no transition
// }
}

// === Return the finished SVG node ===
// The node is returned immediately, the transition runs on it afterwards.
return svgNode;
}
Insert cell
html`
<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"; // match your original SVG viewBox
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

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