protocol-viz / index.html
ucalyptus's picture
Update index.html
43f0e42 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Playable In-Ear Device Protocols (B&W)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* Custom styles */
body {
font-family: 'Inter', sans-serif;
background-color: #f9fafb; /* Light gray background */
}
.protocol-container {
margin-bottom: 1.5rem; /* Space between protocols */
padding: 1.5rem;
background-color: white;
border-radius: 0.5rem; /* Rounded corners */
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* Subtle shadow */
position: relative; /* Needed for absolute positioning of progress line */
}
.protocol-title {
font-size: 1.1rem; /* Slightly smaller title */
font-weight: 600;
margin-bottom: 1rem;
color: #1f2937; /* Dark gray text */
}
svg {
display: block;
margin: auto;
overflow: visible;
}
.bar {
stroke: #6b7280; /* Medium Gray border for bars */
stroke-width: 0.5;
transition: fill 0.1s ease-in-out, stroke-width 0.1s ease-in-out; /* Smooth transition for highlight */
}
.bar.active {
fill: #000000 !important; /* Black for active */
stroke-width: 1.5;
stroke: #000000;
}
.tooltip {
position: absolute;
text-align: center;
padding: 6px 10px;
font-size: 12px;
background: #374151; /* Darker gray background */
color: white;
border-radius: 4px;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
white-space: nowrap;
z-index: 10; /* Ensure tooltip is above elements */
}
.axis path,
.axis line {
fill: none;
stroke: #9ca3af; /* Lighter gray axis */
shape-rendering: crispEdges;
}
.axis text {
font-size: 10px;
fill: #6b7280; /* Medium gray text for axis */
}
.progress-line {
stroke: #ef4444; /* Red color for progress */
stroke-width: 1.5;
pointer-events: none; /* Prevent line from interfering */
}
/* Control Buttons Styling */
.control-button {
background-color: #3b82f6; /* Blue */
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem; /* Rounded */
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
margin: 0 0.25rem; /* Spacing between buttons */
}
.control-button:hover {
background-color: #2563eb; /* Darker Blue */
}
.control-button:disabled {
background-color: #9ca3af; /* Gray when disabled */
cursor: not-allowed;
}
.time-display {
font-weight: 600;
color: #1f2937;
min-width: 100px; /* Ensure space for time */
text-align: right;
}
</style>
</head>
<body class="p-6 bg-gray-50">
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">In-Ear Recording Protocols Player</h1>
<div class="flex justify-center items-center mb-6 p-4 bg-white rounded-lg shadow">
<button id="play-pause-button" class="control-button">Play</button>
<button id="reset-button" class="control-button">Reset</button>
<div class="ml-4 time-display">
Time: <span id="current-time">0.0</span>s / <span id="total-duration">0.0</span>s
</div>
<input type="range" id="speed-slider" min="0.1" max="5" step="0.1" value="1" class="ml-4 w-32 cursor-pointer">
<label for="speed-slider" class="ml-2 text-sm text-gray-600">Speed: <span id="speed-value">1.0</span>x</label>
</div>
<div id="jaw-protocol" class="protocol-container">
<h2 class="protocol-title">Jaw Movements Protocol</h2>
<svg id="jaw-chart"></svg>
</div>
<div id="tongue-protocol" class="protocol-container">
<h2 class="protocol-title">Tongue Movements Protocol</h2>
<svg id="tongue-chart"></svg>
</div>
<div id="spoken-aloud-protocol" class="protocol-container">
<h2 class="protocol-title">Spoken Words Protocol (Aloud)</h2>
<svg id="spoken-aloud-chart"></svg>
</div>
<div id="spoken-whisper-protocol" class="protocol-container">
<h2 class="protocol-title">Spoken Words Protocol (Whisper)</h2>
<svg id="spoken-whisper-chart"></svg>
</div>
<div id="spoken-closed-protocol" class="protocol-container">
<h2 class="protocol-title">Spoken Words Protocol (Mouth Closed)</h2>
<svg id="spoken-closed-chart"></svg>
</div>
<div id="eyes-protocol" class="protocol-container">
<h2 class="protocol-title">Eyes Open/Closed Protocol</h2>
<svg id="eyes-chart"></svg>
</div>
<div id="clench-protocol" class="protocol-container">
<h2 class="protocol-title">Clench Jaw Protocol</h2>
<svg id="clench-chart"></svg>
</div>
<div id="tooltip" class="tooltip"></div>
<script>
// --- Protocol Data (Keep as is) ---
const JAW_PROTOCOL = [
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Post-Repetition Rest", "duration": 15, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Post-Repetition Rest", "duration": 15, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Open jaw", "duration": 2, "color": "#FF9999"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Close jaw", "duration": 2, "color": "#99FF99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift to right", "duration": 2, "color": "#9999FF"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"},
{"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}, {"name": "Shift left", "duration": 2, "color": "#FFCC99"}, {"name": "Rest", "duration": 1, "color": "#CCCCCC"}
];
const TONGUE_PROTOCOL = [
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Right", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Tongue Left", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Upper Teeth", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Push Lower Teeth", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Retraction", "duration": 6, "color": "#CC99FF"}
];
const SPOKEN_ALOUD_PROTOCOL = [
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Say Aloud: 'Later'", "duration": 6, "color": "#99CCFF"}
];
const SPOKEN_WHISPER_PROTOCOL = [
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Whisper: 'Later'", "duration": 6, "color": "#99CCFF"}
];
const SPOKEN_CLOSED_PROTOCOL = [
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"}, {"name": "Post-Repetition Rest", "duration": 30, "color": "#CCCCCC"},
{"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Yes'", "duration": 6, "color": "#FF9999"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'No'", "duration": 6, "color": "#99FF99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Reply'", "duration": 6, "color": "#9999FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Call'", "duration": 6, "color": "#FFCC99"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'You'", "duration": 6, "color": "#FF99FF"}, {"name": "Rest", "duration": 3, "color": "#CCCCCC"}, {"name": "Mouth closed: 'Later'", "duration": 6, "color": "#99CCFF"}
];
const EYES_PROTOCOL = [
{"name": "Rest", "duration": 5, "color": "#CCCCCC"},
{"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"},
{"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"},
{"name": "Eyes Closed", "duration": 30, "color": "#FF9999"}, {"name": "Eyes Open", "duration": 30, "color": "#99FF99"}
];
const CLENCH_PROTOCOL = [
{"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"},
{"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"},
{"name": "Rest", "duration": 15, "color": "#99FF99"}, {"name": "Clench", "duration": 15, "color": "#FF9999"}
];
const ALL_PROTOCOLS = {
"jaw-chart": JAW_PROTOCOL,
"tongue-chart": TONGUE_PROTOCOL,
"spoken-aloud-chart": SPOKEN_ALOUD_PROTOCOL,
"spoken-whisper-chart": SPOKEN_WHISPER_PROTOCOL,
"spoken-closed-chart": SPOKEN_CLOSED_PROTOCOL,
"eyes-chart": EYES_PROTOCOL,
"clench-chart": CLENCH_PROTOCOL
};
// --- Global State ---
let timerInterval = null;
let currentTime = 0; // Current time in seconds
let isPlaying = false;
let playbackSpeed = 1.0;
const timeStep = 0.1; // Update interval in seconds (100ms)
let maxDuration = 0; // Maximum duration across all protocols
const scales = {}; // Store scales for each chart
const processedDataStore = {}; // Store processed data for each chart
// --- DOM Elements ---
const playPauseButton = document.getElementById('play-pause-button');
const resetButton = document.getElementById('reset-button');
const currentTimeDisplay = document.getElementById('current-time');
const totalDurationDisplay = document.getElementById('total-duration');
const tooltip = d3.select("#tooltip");
const speedSlider = document.getElementById('speed-slider');
const speedValueDisplay = document.getElementById('speed-value');
// --- Helper Functions ---
// Function to calculate cumulative duration and add start/end times
function processProtocolData(protocolData) {
let cumulativeDuration = 0;
return protocolData.map(d => {
const start = cumulativeDuration;
cumulativeDuration += d.duration;
return { ...d, start: start, end: cumulativeDuration };
});
}
// Function to calculate total duration of a processed protocol
function calculateTotalDuration(processedData) {
return processedData.length > 0 ? processedData[processedData.length - 1].end : 0;
}
// --- Visualization Function ---
function createTimeline(chartId, processedData, globalMaxDuration) {
const container = d3.select(`#${chartId}`);
if (container.empty()) {
console.error(`Container element #${chartId} not found.`);
return;
}
const containerWidth = container.node().getBoundingClientRect().width;
const margin = { top: 5, right: 15, bottom: 25, left: 15 };
const width = containerWidth - margin.left - margin.right;
const height = 60 - margin.top - margin.bottom; // Reduced height
const barHeight = 25; // Reduced bar height
// --- Scales ---
// Use the global max duration for the domain
const xScale = d3.scaleLinear()
.domain([0, globalMaxDuration])
.range([0, width]);
scales[chartId] = xScale; // Store the scale
// --- SVG Setup ---
container.selectAll("*").remove(); // Clear previous chart
const svg = container
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// --- Draw Bars ---
svg.selectAll(".bar")
.data(processedData)
.enter()
.append("rect")
.attr("class", "bar")
.attr("data-chart-id", chartId) // Add chart ID for easier selection later
.attr("x", d => xScale(d.start))
.attr("y", (height - barHeight) / 2)
.attr("width", d => Math.max(0, xScale(d.end) - xScale(d.start))) // Ensure width is non-negative
.attr("height", barHeight)
.attr("fill", d => d.name.toLowerCase().includes("rest") ? "#D3D3D3" : "#696969") // Initial colors
.on("mouseover", function(event, d) {
tooltip.style("opacity", 1);
})
.on("mousemove", function(event, d) {
tooltip.html(`${d.name}<br>Duration: ${d.duration}s<br>Time: ${d.start.toFixed(1)}s - ${d.end.toFixed(1)}s`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function() {
tooltip.style("opacity", 0);
});
// --- Add Time Axis ---
const xAxis = d3.axisBottom(xScale)
.ticks(Math.min(10, Math.ceil(globalMaxDuration / 15))) // Adjust ticks
.tickFormat(d => `${d}s`);
svg.append("g")
.attr("class", "axis x-axis")
.attr("transform", `translate(0,${height})`)
.call(xAxis);
// --- Add Progress Line ---
svg.append("line")
.attr("class", "progress-line")
.attr("x1", xScale(0))
.attr("x2", xScale(0))
.attr("y1", -margin.top / 2) // Position slightly above the bars
.attr("y2", height + margin.bottom / 2) // Extend slightly below axis
.style("opacity", 0); // Initially hidden
}
// --- Update Highlighting and Progress ---
function updateVisuals(time) {
currentTimeDisplay.textContent = time.toFixed(1);
// Update highlights for all bars
d3.selectAll(".bar").each(function(d) {
const bar = d3.select(this);
const isActive = time >= d.start && time < d.end;
bar.classed("active", isActive)
// Reset fill if not active (needed because !important overrides direct fill)
.style("fill", isActive ? null : (d.name.toLowerCase().includes("rest") ? "#D3D3D3" : "#696969"));
});
// Update progress lines for all charts
d3.selectAll(".progress-line").each(function() {
const line = d3.select(this);
const chartId = d3.select(this.parentNode).select('.bar').attr('data-chart-id'); // Get chart ID from sibling bar
if (chartId && scales[chartId]) {
const xScale = scales[chartId];
line.attr("x1", xScale(time))
.attr("x2", xScale(time))
.style("opacity", 1); // Make visible
}
});
}
// --- Playback Control Functions ---
function play() {
if (isPlaying) return;
isPlaying = true;
playPauseButton.textContent = 'Pause';
resetButton.disabled = true; // Disable reset while playing
// Clear existing interval if any
if (timerInterval) clearInterval(timerInterval);
let lastTimestamp = performance.now();
function tick(currentTimestamp) {
if (!isPlaying) return; // Stop if paused
const elapsedRealTime = (currentTimestamp - lastTimestamp) / 1000; // Time since last frame in seconds
lastTimestamp = currentTimestamp;
currentTime += elapsedRealTime * playbackSpeed; // Increment time based on speed
if (currentTime >= maxDuration) {
currentTime = maxDuration; // Clamp to end
pause(); // Automatically pause at the end
resetButton.disabled = false; // Re-enable reset
}
updateVisuals(currentTime);
// Continue the loop only if playing
if (isPlaying) {
requestAnimationFrame(tick);
}
}
requestAnimationFrame(tick); // Start the loop
}
function pause() {
if (!isPlaying) return;
isPlaying = false;
playPauseButton.textContent = 'Play';
resetButton.disabled = false; // Re-enable reset
// No need to clear interval with requestAnimationFrame
}
function reset() {
pause(); // Ensure it's paused
currentTime = 0;
updateVisuals(currentTime);
d3.selectAll(".progress-line").style("opacity", 0); // Hide progress line on reset
}
// --- Initialization ---
function initialize() {
// Process all data and find max duration
let currentMax = 0;
for (const chartId in ALL_PROTOCOLS) {
const processedData = processProtocolData(ALL_PROTOCOLS[chartId]);
processedDataStore[chartId] = processedData; // Store processed data
const duration = calculateTotalDuration(processedData);
if (duration > currentMax) {
currentMax = duration;
}
}
maxDuration = currentMax;
totalDurationDisplay.textContent = maxDuration.toFixed(1);
// Create all timelines using the global max duration
for (const chartId in processedDataStore) {
createTimeline(chartId, processedDataStore[chartId], maxDuration);
}
// Set initial visuals
updateVisuals(0);
d3.selectAll(".progress-line").style("opacity", 0); // Ensure lines are hidden initially
// Setup Speed Slider
speedSlider.addEventListener('input', (event) => {
playbackSpeed = parseFloat(event.target.value);
speedValueDisplay.textContent = playbackSpeed.toFixed(1);
});
speedValueDisplay.textContent = playbackSpeed.toFixed(1); // Initial display
// Add event listeners
playPauseButton.addEventListener('click', () => {
if (isPlaying) {
pause();
} else {
if (currentTime >= maxDuration) { // If at end, reset before playing
reset();
}
play();
}
});
resetButton.addEventListener('click', reset);
}
// --- Run Initialization on Load ---
window.addEventListener('load', initialize);
// --- Redraw charts on resize ---
// Debounce resize event for performance
let resizeTimeout;
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
// Re-initialize everything on resize to recalculate widths/scales
// Store current state
const wasPlaying = isPlaying;
const timeBeforeResize = currentTime;
// Pause before re-initializing
pause();
// Re-initialize
initialize();
// Restore state
currentTime = timeBeforeResize;
updateVisuals(currentTime);
if (wasPlaying) {
play(); // Resume playing if it was playing before
}
}, 250); // Wait 250ms after last resize event
});
</script>
</body>
</html>