Public
Edited
Jun 27, 2024
Insert cell
Insert cell
viewof patient = Inputs.select(adsl.map(d => d.USUBJID))
Insert cell
{
let data = adlb.filter(d => d.USUBJID === patient)
.filter(d => d.LBNRIND !== "NORMAL")
// accessors
const xAccessor = d => d.ADY
const radiusAccessor = d => d.AVAL

// canvas
const svg = d3.create("svg")
.attr("width", dimensions.width)
.attr("height", dimensions.height)

const bounds = svg.append("g")
.style("transform", `translate(${
dimensions.margin.left
}px, ${
dimensions.margin.top
}px)`
)

// scales
const colorScale = d3.scaleOrdinal()
.domain(["LOW", "NORMAL", "HIGH"])
.range(['#fdd26e', '#86cac5', '#ff8041'])

// marks
const dots = bounds.selectAll("circle")
.data(beeswarmForce(data, 'ADY', 'AVAL', 'LBNRIND', xScale))
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r)
.attr("fill", d => colorScale(d.fill))
.on('mousemove', nodeMouseOver)
.on('mouseout', nodeMouseOut );

// extras
const xAxisGenerator = d3.axisBottom()
.scale(xScale)

const xAxis = bounds.append("g")
.call(xAxisGenerator)
.style("transform", `translateY(${dimensions.boundedHeight}px)`)
.style("stroke", "#004f5b")


// tooltip

// const listenerRect = bounds.append('rect')
// .attr('class', 'listener-rect')
// .attr('width', dimensions.boundedWidth)
// .attr('height', dimensions.boundedHeight)
// .on("mousemove", onMouseMo)
// .on("mouseleave", onMouseLeave)
// const tooltip = bounds.append("div")
const tooltip = d3.select("body").append('div')
// const tooltip = wrapper.append('div')
.attr('class', 'tooltip')
.html(
`<span class='tooltip-param'></span>
<span class='tooltip-aval'></span>
<span class='tooltip-range'></span>
<span class='tooltip-day'></span>`
)
// .append('span')
// .attr('class', 'tooltip-param')
// .apppend('span')
// .attr('class', 'tooltip-aval')
// .append('span')
// .attr('class', 'tooltip-range')


function onMouseEnter(event, d) {
const mousePosition = d3.pointer(event)
const xPosition = mousePosition[0]
const yPosition = mousePosition[1]

// console.log(mousePosition)

tooltip.select('.tooltip-param')
.text(`PARAM: ${d.PARAM}`)
// .style('left', `${dimensions.boundedWidth - xPosition - 20}px`)
// .style('top', `${dimensions.boundedHeight + yPosition + 20}px`)
// .style('left', '79px')
// .style('right', '260px')

// tooltip.style('transform', `translate(calc(-50% + ${xPosition}px), calc(-100% + ${yPosition}px))`)
// .style('opacity', 1)

tooltip
.style('width', '100px')
.style('height', '100px')
.style('left', xPosition + 'px')
.style('top', yPosition + 'px')
// .style('transform', `translate(${xPosition}px, ${yPosition}px)`)
.style('opacity', 1)
.style('background', 'red')

// console.log(dimensions.boundedWidth - xPosition - 20)
// console.log(dimensions.boundedHeight + yPosition + 20)
// const hoveredDay = xScale.invert(mousePosition[0])
// // need to find closet point in our data
// const getDistanceFromHoveredDay = d => {
// return Math.abs(xAccessor(d) - hoveredDay)
// }

// const closestIndex = d3.leastIndex(
// data,
// (a, b) => { // kind of like native JS sort
// return getDistanceFromHoveredDay(a) - getDistanceFromHoveredDay(b)
// }
// )
// const closestDataPoint = data[closestIndex]
// console.log(closestDataPoint)
// // at this point, we're at the 'd' we usually work with
// const formatDate = d3.timeFormat("%B %A %-d, %Y")
// tooltip.select("#date")
// .text(formatDate(xAccessor(closestDataPoint)))
// const formatTemperature = d => `${d3.format(".1f")(d)}°F`
// tooltip.select("#temperature")
// .text(formatTemperature(yAccessor(closestDataPoint)))
// // ended up not using this but it's nice to know there's an inverse
// //const hoveredTemp = yScale.invert(mousePosition[1])
// // move tooltip to correct position
// const x = xScale(xAccessor(closestDataPoint)) + dimensions.margin.left
// const y = yScale(yAccessor(closestDataPoint)) + dimensions.margin.top
// tooltip.style('transform', `translate(calc(-50% + ${x}px), calc(-100% + ${y}px))`)
// tooltip.style('opacity', 1)

}

function onMouseLeave(event, d) {
tooltip.style('opacity', 0)

}

return svg.node()
}
Insert cell
function nodeMouseOver(event, d){
// Get the toolTip, update the position, and append the inner html depending on your content
// I tend to use template literal to more easily output variables.
toolTip.style("left", event.pageX + 18 + "px")
.style("top", event.pageY + 18 + "px")
.style("display", "block")
.html(
`
Tooltip for <strong>${d.PARAM}</strong>
`
);
// Optional cursor change on target
d3.select(event.target).style("cursor", "pointer");
// Optional highlight effects on target
d3.select(event.target)
.transition()
.attr('stroke', '#A8234E')
.attr('stroke-width', 3);
}
Insert cell
function nodeMouseOut(event, d){
// Hide tooltip on mouse out
toolTip.style("display", "none"); // Hide toolTip
// Optional cursor change removed
d3.select(event.target).style("cursor", "default");
// Optional highlight removed
d3.select(event.target)
.transition()
.attr('stroke', '#fff')
.attr('stroke-width', 0);
}
Insert cell
toolTip = d3.select("body").append("div").attr("class", "toolTip")
Insert cell
Insert cell
<style>
.ae-x-axis-label {
fill: #004f5b;
font-size: 1.1rem;
}

.toolTip {
position: absolute;
display: none;
min-width: 30px;
max-width: 240px;
border-radius: 4px;
height: auto;
background: rgba(250,250,250, 0.9);
border: 1px solid #DDD;
padding: 4px 8px;
font-size: .85rem;
text-align: left;
z-index: 1000;
}

</style>
Insert cell
Insert cell
xScale = d3.scaleLinear()
.domain([d3.min(adae.map(d => d.AESTDY)), d3.max(adae.map(d => d.AEENDY))])
.range([0, dimensions.boundedWidth])
Insert cell
Insert cell
Insert cell
beeswarmForce = function(data, xName, radiusName, fillName, xScale){
// adapted from
// https://observablehq.com/@harrystevens/force-directed-beeswarm
const ticks = 300
let forceData = data.map(d => {
return {
x: d[xName],
y: dimensions.boundedHeight / 2,
r: 8,
fill: d[fillName],
// below is for tooltip
PARAM: d.PARAM,
AVAL: d.AVAL,
ANRLO: d.ANRLO,
ANRHI: d.ANRHI,
ADY: d[xName]
}
});

const simulation = d3.forceSimulation(forceData)
.force("x", d3.forceX(d => xScale(d.x)))
.force("y", d3.forceY(d => d.y))
.force("collide", d3.forceCollide(d => d.r))

for (let i = 0; i < ticks; i++) simulation.tick();
return forceData;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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