Public
Edited
Mar 31
1 star
Insert cell
Insert cell
Insert cell
Insert cell
viewof file = Inputs.file({label: "Test result data"})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
charts = {
const circleRadius = 20;
const numberOfChannels = pluslifeTestData.testResult.numberOfChannels;
if (numberOfChannels !== 7) {
throw new Error("as far as I know, test cards always have 7 channels 🤔");
}

// data setup
const values = samples.map(s => s.value);
const times = samples.map(s => s.time);
const colors = d3.quantize(d3.interpolateRainbow, numberOfChannels + 2);
const timeScale = d3.scaleLinear().domain(d3.extent(samples, d => d.time));

const samplesByTimeByChannel = d3.group(samples, d => d.time, d => d.channel);
const samplesByChannel = d3.group(samples, d => d.channel);

const container = d3.create("div")
.style("display", "flex");
const layout = {
card: { width: 240, height: 320 },
graph: {
width: 560,
height: 340,
margin: { top: 30, right: 0, bottom: 50, left: 60 }
}
};

// card setup
const card = cardSetup(container, layout.card, colors);

const scaleRadius = d3.scaleLinear().domain(d3.extent(values)).range([2, circleRadius]);
const channelPositions = d3.range(7).map((index) => (
getPointOnCircle(120, 200, outerChannelPlacementRadius, outerChannelAngles[index])
));

// graph setup
const { graph, line } = graphSetup(container, layout.graph, samples);

const dasharrayByChannel = d3.range(7).map((index) => {
const lineLength = htl.svg`<path d="${line(samplesByChannel.get(index + 1))}">`.getTotalLength();
return d3.interpolate(`0,${lineLength}`, `${lineLength},${lineLength}`);
});

// draw card fluorescence at time t
const circles = card.append("g")
.selectAll("circle")
.data(samplesByTimeByChannel.get(t))
.join("circle")
.attr("cx", ([channel]) => channelPositions[channel - 1].x)
.attr("cy", ([channel]) => channelPositions[channel - 1].y)
.attr("r", ([channel, d]) => scaleRadius(d[0].value))
.attr("fill", ([channel]) => colors[channel - 1])
.attr("opacity", 0.5);

// draw graph at time t
const lines = graph.append("g")
.selectAll("path")
.data(samplesByChannel)
.join("path")
.attr("fill", "none")
.attr("stroke", ([channel]) => colors[channel - 1])
.attr("d", ([channel, d]) => line(d))
.attr("stroke-dasharray", ([channel]) => dasharrayByChannel[channel - 1](timeScale(t)))
.on("mouseover", function (d, i) {
lines.attr("opacity", "0.25");
d3.select(this)
.attr("stroke-width", "2")
.attr("opacity", "1");
})
.on("mouseout", function (d, i) {
lines.attr("stroke-width", "1").attr("opacity", "1");
});


return container.node();

}
Insert cell
outerChannelPlacementRadius = 50
Insert cell
outerChannelAngles = [-140, -182, -225, 90, 45, 2, -40];
Insert cell
<svg viewBox="0 0 240 320" width="240">
<defs>
<style type="text/css">
.card text {
font-size: 12px;
font-family: var(--monospace);
fill: currentColor;
stroke: none;
}
.card .channel-label {
font-size: 10px;
opacity: 0.5;
}
</style>
</defs>
<g stroke="currentColor" fill="none" class="card">
<g>
${[-120, -170, -220, 90, 40, -10, -60].map((angle, index) => {
const {x, y} = getPointOnCircle(120, 200, 23, angle);
return htl.svg`<circle cx="${x}" cy="${y}" r="5" fill="none" id="inner-channel-${index + 1}" />`;
})}
</g>
<g>
${outerChannelAngles.map((angle, index) => {
const {x, y} = getPointOnCircle(120, 200, outerChannelPlacementRadius, angle);
const textPos = (index - 3) !== 0 ? Math.min(Math.max(index - 3, -1), 1) : 0;
return htl.svg`
<circle cx="${x}" cy="${y}" r="8" fill="none" id="outer-channel-${index + 1}" />
<text x="${x - 3 + (textPos * 16)}" y="${y + 4 + (textPos === 0 ? 18 : 0)}" class="channel-label">${index + 1}</text>
`;
})}
</g>
<circle cx="120" cy="200" r="35" />
<rect x="10" y="10" width="220" height="300" rx="15" />
<rect x="30" y="90" width="180" height="200" rx="10" />
<text x="30" y="40" fill="currentColor" stroke="none" style="">${pluslifeTestData.testType}</text>
<g transform="translate(30, 70)" id="control-legend">
<rect x="119" y="-12" width="15" height="15" fill="none" stroke="none" opacity="0.5" />
<text>Control channel: <tspan>${controlChannel}</tspan></text>
</g>
<path d="M 105,90 l 0,10 l 10,0 l 0,10 l 10,0 l 0,-10 l 10,0 l 0,-10" />
<path d="M 120,110
a 5 5 0 0 1 -5,5
l -10,0
a 5 5 0 0 0 -5,5
l 0, 10
a 5 5 0 0 0 5,5
l 25,0
a 5 5 0 0 1 5,5
l 0,10
a 5 5 0 0 1 -5,5
l -5,0
a 5 5 0 0 0 -5,5
l 0,5" />
</g>
</svg>
Insert cell
Insert cell
Insert cell
Promise.all(Object.values(attachments))
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// unfortunately this is 98% copied and pasted from the Google AI Overview suggestion. feels gross tbh!
function getPointOnCircle(centerX, centerY, radius, angle) {
const radians = angle * Math.PI / 180;
const x = centerX + radius * Math.cos(radians);
const y = centerY + radius * Math.sin(radians);
return { x, y };
}
Insert cell
import {Scrubber} from "@mbostock/scrubber"
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