Public
Edited
Jul 21, 2024
Insert cell
Insert cell
Insert cell
Insert cell
swimmerplot(width)
Insert cell
function unique(x) {
return x.reverse().filter(function (e, i, x) {return x.indexOf(e, i+1) === -1;}).reverse();
}
Insert cell
swimmerplot = function(width) {

let color = ({
//green: '#59A14F',
green: 'white',
yellow: '#F1CE63',
red: '#E15759',
purple: "#B07AA1",
black: "#1f1f1f"
})
let fontsize = 12
let bar_height = 7
let margin = {left: 50, right: 150, top: 20, bottom: 100}
let colorScale = d3.scaleOrdinal().domain([0,1,2,3,4]).range([color.green, color.yellow, color.red, color.purple, color.black])
var xScale = d3.scaleLinear()
.domain(d3.extent(data.map(d => d.swimmers.map(d => d.shape)).flat().flat().flat(), d => d.x))
.range([margin.left, width - margin.right])

// do we just wanna do this in R and pass it to the JS object maybe?
let unique_dates = unique(data.map(x => x.swimmers).flat().map(x => x.shape).flat().map(x => x.x)).sort(function(a, b) {return a - b });
// this one we should make in R I think to pick the labels we wanna use on the axis?
let unique_labels = ["Baseline", "Week 1", "Week 2", "Week 2.1", "Week 3", "Week5", "Week 6", "Week 7", "Week 8", "Week 10"]

let unique_conmed = unique(data.map(x => x.swimmers).flat().map(x => x.shape).flat().map(x => x.shapes).flat().map(x => x.type))

let max_dose = d3.max(data.map(x => x.swimmers).flat().map(x => x.line).flat().map(x => x.lines).flat().flat().map(x => x.dose))
let cScale = d3.scaleSequential([0, max_dose], d3.interpolateBlues)

let majorShapeScale = d3.scaleOrdinal()
.domain(unique_conmed)
.range(['circle', 'triangle', 'square'])

let minorShapeScale = d3.scaleOrdinal()
.domain(['circle', 'circle-left', 'circle-right', 'triangle', 'triangle-left', 'triangle-right', 'square', 'square-left', 'square-right'])
.range([
"M 0 -24 A 1 1 0 0 0 0 0 M 0 0 A 1 1 0 0 0 0 0 A 1 1 0 0 0 0 -24",
"M 0 -24 A 1 1 0 0 0 0 0 L 0 0",
"M 0 0 A 1 1 0 0 0 0 0 A 1 1 0 0 0 0 -24 L 0 0",

"M 0 0 L -12 0 L 0 -24 M 0 0 L 12 0 L 0 -24",
"M 0 0 L -12 0 L 0 -24 L 0 0",
"M 0 0 L 12 0 L 0 -24 L 0 0",

"M 0 0 L -12 0 L -12 -24 L 0 -24 M 0 0 L 12 0 L 12 -24 L 0 -24",
"M 0 0 L -12 0 L -12 -24 L 0 -24 L 0 0",
"M 0 0 L 12 0 L 12 -24 L 0 -24 L 0 0"
])

let leftOrRight = (type, i, n) => {

if (n === 1) {
return minorShapeScale(majorShapeScale(type))
}
if (i === 0) {
return minorShapeScale(majorShapeScale(type) + "-left")
} else if (i === 1) {
return minorShapeScale(majorShapeScale(type) + "-right")
} else {
return null
}
}

let height = 400
let xAxis = g => g
.call(d3.axisBottom(xScale)
.ticks(unique_dates.length-1)
.tickValues(unique_dates)
.tickSize(-height+margin.top+margin.bottom)
.tickFormat((d,i) => unique_labels[i])
)
.selectAll("text")
.attr('fill', 'black')
.attr('font-size', "1.5em")
.attr("x", (d,i) => i === 0 ? "2.2em" : "1em")
.attr("transform", "rotate(10)");

var container = d3.select('#container').attr('class', 'container').style('width', `${width}px`)

/* LEGENDS */
var legend = container.append('div')
.attr('class', 'legend')
var all_shape_legend = legend.append('div')
.attr('class', 'inner-legend legend')
.selectAll('shape_legend_items')
.data(unique_conmed.map(x =>
({shape: majorShapeScale(x), name: x})
))
.enter()
.append('div')
.attr('class', 'small-inner-legend legend')
all_shape_legend.append('div')
.attr('class', 'shape-container')
.append('svg')
.attr("width", 25)
.attr("height", 25)
.attr('viewBox', '0 0 30 30')
.append('path')
.attr("d", d => minorShapeScale(d.shape))
.attr("fill", 'white')
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('transform', 'translate(13, 25) scale(0.7)')

all_shape_legend.append('span')
.text(d => d.name)

var ctcae_legend = legend.append('div')
.attr('class', 'legend inner-legend')

ctcae_legend.append('div')
.text("CTCAE Grade:")
let ctcae_legend_colors = ctcae_legend.append('svg')
.attr('height', 25)
.attr('width', 25*5)

let colors = ctcae_legend_colors.selectAll('grade')
.attr('class', 'grade')
.data([
{grade: 0},
{grade: 1},
{grade: 2},
{grade: 3},
{grade: 4}
])
.join('g')
.attr('class', 'grade-g')
colors.append('rect')
.attr('width', 20)
.attr('height', 20)
.attr('stroke', 'black')
.attr('stroke-width', '2')
.attr('fill', (d,i) => colorScale(d.grade))
.attr('transform', (d,i) => `translate(${i*26 + 1},1)`)
colors.append('text')
.attr('y', 12)
.attr('x', 0)
.text(d => d.grade)
.attr('fill', d => d.grade === 0 | d.grade === 1 ? "black" : "white")
.attr('transform', (d,i) => `translate(${i*26 + 6},4)`)
.attr('text-anchor', 'center')

let color_range_legend = legend.append('div')
.attr('class', 'legend inner-legend')
color_range_legend.append('div')
.text("Dose (mg):")
let color_range = color_range_legend.append('svg')
.attr('height', 40)
.attr('width', 150)
.attr('transform', 'translate(0, -10)')

var colorRangeScale = d3.scaleLinear().domain([0, 900]).range([0,100])
let colorAxis = g => g.call(d3.axisTop(colorRangeScale).ticks(2)
.tickValues([0, 900]))
var colordata = d3.range(15).map(d=> ({color:d3.interpolateBlues(d/10), value:d}))
var colorextent = d3.extent(colordata, d => d.value);
var colorbar = color_range.append("g").attr("transform", "translate(" + 20 + ", 0)");
var defs = color_range.append("defs");
var linearGradient = defs.append("linearGradient").attr("id", "myGradient");
linearGradient.selectAll("stop")
.data(colordata)
.enter()
.append("stop")
.attr("offset", d => ((d.value - colorextent[0]) / (colorextent[1] - colorextent[0]) * 100) + "%")
.attr("stop-color", d => d.color);

colorbar.append("rect")
.attr("width", 100)
.attr("height", 20)
.attr('transform', (d,i) => `translate(${-1},20)`)
.style("fill", "url(#myGradient)");

colorbar.append("g")
.call(colorAxis)
.attr('transform', (d,i) => `translate(-1,22)`)
.select(".domain").remove()
colorbar.selectAll(".tick").selectAll('line').attr('opacity', 0)
/* PLOT */

let shape_size = 30
let line_size = 15
// circle data and annotations
let swimmer = container.selectAll("swimmer-plot")
.data(data)
.enter()
.append("div")
.attr('class', 'swimmer-lane')
.style('width', `${width}px`)
//.attr('width', width)
//.append('svg')
//.attr('width', width)
.append('div')
.attr('class', 'swimmer')
.html(d => `<div>${d.id}</div>`)
.append('div')

swimmer
.append('svg')
.attr("width", width)
.attr('height', 12)
.attr('class', 'dose-text')
.selectAll('dose-text')
.data(d => d.adex)
.enter()
.append("text")
.attr("transform", d => `translate(${xScale(d.x)}, ${fontsize})`)
.attr("font-size", fontsize)
.text(d => d.value)
.attr('text-anchor', 'middle')

let drug_lane = swimmer.selectAll("drug-lane")
.data(d => d.swimmers)
.enter()
.append('svg')
.attr('width', width)
.attr('height', d => d.line[0].lines.length*10 + 45)
.attr('class', 'drug-lane')
let shape_lane = drug_lane.selectAll('drug-lane')
.data(d => d.shape)
.enter()
.append('g')
.attr('class', 'shape_lane')
.attr('transform', (d,i) => `translate(${xScale(d.x)},30)`)
//.attr('transform', (d,i) => `translate(0,${i*10})`)

let line_lane = drug_lane.selectAll('drug-lane')
.data(d => {
console.log(d.line)
return d.line
})
.enter()
.append('g')
.attr('class', 'line-lane')
.attr('transform', (d,i) => `translate(0,${d.lines.length*10})`)

let shapes = shape_lane.selectAll('shape_lane')
.data(d => d.shapes)
.enter()
.append('path')
.attr("d", (d,i) => leftOrRight(d.type, i, d.n))
.attr("fill", d => colorScale(d.risk))
.attr('stroke', 'black')
.attr('stroke-width', 2)
let lines = line_lane.selectAll("line-lanes")
.data(d => d.lines)
.enter()
.append("g")
.attr('class', 'gradient')
.attr('transform', function(d, i) { return `translate(0,${ (i*-7) + 30})` })

let bars = lines.selectAll('gradient')
.data(d => d)
.enter()
.append('rect')
.attr('x', d => xScale(d.stdy))
.attr('y', d => d.type === 'bar' ? 0 : -5)
.attr('width', function(d) {
if (d.type == 'bar') {
// if its ongoing, cut off the bar at the end or before of the plot
return (d.ongoing ? xScale(50 - d.stdy) : xScale(d.width))
} else {
return 2
}
})
// the height of blips is the y... maybe this is bad idea
// the height of the bars needs to be the max length of the longest lines array
.attr('height', d => d.type === 'bar' ? bar_height : (bar_height + 10)*4)
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('fill', d => cScale(d.dose))
.on('mousemove', nodeMouseOver)
.on('mouseout', nodeMouseOut );

container.append('div')
.attr('class', 'swimmer')
.html(`<div></div>`)
.append('svg')
.attr('width', width)
.attr('height', 50)
.append("g")
.call(xAxis)

return container
}
Insert cell
Insert cell
Insert cell
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(
`
Supplemental Dose: <strong>${d.dose}</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
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', 'black')
.attr('stroke-width', 2);
}
Insert cell
toolTip = d3.select("body").append("div").attr("class", "toolTip")
Insert cell
style = html`
<style>
.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;
}

#container .swimmer-lane:nth-child(2n + 2) {
background: #f9f9f9;
}
.svg {
margin: 0;
padding: 0;
}

.swimmer {
display: grid;
grid-template-columns: 60px 1fr;
align-items: center;
padding-left: 10px;
}

.legend {
display: flex;
justify-content: flex-start;
align-items: center;
width: max-content;
gap: 50px;
}

.small-inner-legend {
gap: 1px;
}

.inner-legend {
gap: 7px;
}
</style>
`
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