Public
Edited
Jun 26, 2024
1 fork
Insert cell
Insert cell
viewof patient = Inputs.select(adsl.map(d => d.USUBJID))
Insert cell
Inputs.table(
adae.filter(d => d.USUBJID === patient)
.filter(d => typeof d.AESTDY !== 'undefined' & typeof d.AEENDY !== 'undefined')
)
Insert cell
{
const data = adae.filter(d => d.USUBJID === patient)
.filter(d => typeof d.AESTDY !== 'undefined' & typeof d.AEENDY !== 'undefined')
.map((d) => {
if (d.AESTDY === d.AEENDY) { // need end - start > 0 to have a line
return {
...d,
AEENDY: nudge(d.AEENDY)
}
}
return d
})

// accessors
const startDayAccessor = d => d.AESTDY
const endDayAccessor = d => d.AEENDY
// const yAccessor = d => d.AEDECOD
const yAccessor = d => d.AESEQ
const colorAccessor = d => d.AESEV

// canvas
// const wrapper = d3.select("#wrapper")
// .append("svg")
// .attr("width", dimensions.width)
// .attr("height", dimensions.height)

// const bounds = wrapper.append("g")
// .style("transform", `translate(${
// dimensions.margin.left
// }px, ${
// dimensions.margin.top
// }px)`
// )
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 sortedAECodes = data.sort((a, b) => d3.ascending(a.AESEQ, b.AESEQ)).map(d => d.AEDECOD)
// const uniqueAECodes = unique(sortedAECodes)
// const yScale = d3.scaleBand()
// .domain(uniqueAECodes)
// .range([dimensions.boundedHeight, 0])
const yScale = d3.scaleLinear()
// .domain(d3.extent(data, yAccessor))
.domain([0, d3.max(data, yAccessor)])
.range([dimensions.boundedHeight, 0])

const colorScale = d3.scaleOrdinal()
.domain(["MILD", "MODERATE", "SEVERE"])
.range(['#fdd26e', '#ff8041', '#cc66ff'])

// marks
const lines = bounds.selectAll("line")
.data(data)
.join("line")
.attr("x1", d => xScale(startDayAccessor(d)))
.attr("x2", d => xScale(endDayAccessor(d)))
.attr("y1", d => yScale(yAccessor(d)))
.attr("y2", d => yScale(yAccessor(d)))
.attr("stroke", "#004f5b")
.attr("stroke-width", "1px")

const startDots = bounds.selectAll("startCircle")
.data(data)
.join("circle")
.attr("cx", d => xScale(startDayAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 4)
.attr('fill', d => colorScale(colorAccessor(d)))

const endDots = bounds.selectAll("endCircle")
.data(data)
.join("circle")
.attr("cx", d => xScale(endDayAccessor(d)))
.attr("cy", d => yScale(yAccessor(d)))
.attr("r", 4)
.attr("fill", d => colorScale(colorAccessor(d)))

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

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

const xAxisLabel = xAxis.append("text")
.attr("class", "ae-x-axis-label")
.attr("x", dimensions.boundedWidth / 2)
.attr("y", dimensions.margin.bottom - 5)
.html("Study Day")

const yAxisGenerator = d3.axisLeft()
.scale(yScale)
// .tickFormat(d => d.AEDECOD)
const yAxis = bounds.append("g")
.call(yAxisGenerator)

return svg.node()
}
Insert cell
viewof param = Inputs.select(unique(adlb.map(d => d.PARAM)))
Insert cell
{
let data = adlb.filter(d => d.USUBJID === patient)
.filter(d => d.PARAM === param)
.filter(d => d.LBNRIND !== "NORMAL")
// accessors
const xAccessor = d => d.ADY
const colorAccessor = d => d.LBNRIND
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'])

const radiusScale = d3.scaleSqrt()
.domain(d3.extent(data, radiusAccessor))
.range([8, Math.sqrt(dimensions.width * dimensions.height) / 30])

// marks
const dots = bounds.selectAll("circle")
.data(beeswarmForce(data, xAccessor, xScale, radiusScale))
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => radiusScale(d.r))
.attr("fill", d => colorScale(colorAccessor(d)))

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

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

const xAxisLabel = xAxis.append("text")
.attr("class", "ae-x-axis-label")
.attr("x", dimensions.boundedWidth / 2)
.attr("y", dimensions.margin.bottom - 5)
.html("Study Day")

return svg.node()
}
Insert cell
viewof paramVitals = Inputs.select(unique(advs.map(d => d.PARAM)))
Insert cell
{
let data = advs.filter(d => d.USUBJID === patient)
.filter(d => d.PARAM === paramVitals)
// accessors
const xAccessor = d => d.ADY
const colorAccessor = d => d.ANRIND
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'])

const radiusScale = d3.scaleSqrt()
.domain(d3.extent(data, radiusAccessor))
.range([8, Math.sqrt(dimensions.width * dimensions.height) / 30])

// marks
const dots = bounds.selectAll("circle")
.data(beeswarmForce(data, xAccessor, xScale, radiusScale))
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => radiusScale(d.r))
.attr("fill", d => colorScale(colorAccessor(d)))

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

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

const xAxisLabel = xAxis.append("text")
.attr("class", "ae-x-axis-label")
.attr("x", dimensions.boundedWidth / 2)
.attr("y", dimensions.margin.bottom - 5)
.html("Study Day")

return svg.node()
}
Insert cell
<style>
.ae-x-axis-label {
fill: #004f5b;
font-size: 1.1rem;
}
</style>
Insert cell
Insert cell
xScale = d3.scaleLinear()
.domain([d3.min(adae.map(d => d.AESTDY)), 1.05 * d3.max(adae.map(d => d.AEENDY))]) // 1.05 = extra wiggle room for dot plots
.range([0, dimensions.boundedWidth])
Insert cell
// for AEs, the start and end day can be the same
// so we need to nudge one of the days a bit if we want a line
nudge = function(x) {
const random = d3.randomInt(4, 6)
return x + random()
}
Insert cell
Insert cell
beeswarmForce = function(data, xAccessor, xScale, radiusScale){
// adapted from
// https://observablehq.com/@harrystevens/force-directed-beeswarm
const ticks = 300
let forceData = data.map(d => {
return {
x: d.ADY,
y: dimensions.boundedHeight / 2,
r: d.AVAL,
LBNRIND: d.LBNRIND
}
});

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 => radiusScale(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