Public
Edited
Jun 27, 2024
1 fork
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

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