Skip to content

Instantly share code, notes, and snippets.

@shimwell
Created July 7, 2025 08:43
Show Gist options
  • Select an option

  • Save shimwell/b638594dccb10d8ba0d747b3929bf550 to your computer and use it in GitHub Desktop.

Select an option

Save shimwell/b638594dccb10d8ba0d747b3929bf550 to your computer and use it in GitHub Desktop.
plotly time series heat map with interactive click
<!DOCTYPE html>
<html>
<head>
<title>Plotly Click Interaction</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/plotly.js/2.26.0/plotly.min.js"></script>
</head>
<body>
<div style="display: flex; flex-direction: column; height: 100vh; width: 100vw; box-sizing: border-box;">
<!-- Scroll bar for days -->
<div style="width: 40vw; margin: 20px auto 0 auto; display: flex; flex-direction: column; align-items: center;">
<label for="day-slider" style="font-weight: bold; margin-bottom: 10px; display: block;">
Select Time:
</label>
<input type="range" id="day-slider" min="0" max="3" value="0"
style="width: 100%; height: 20px; margin-bottom: 10px;">
<div style="display: flex; justify-content: space-between; width: 100%; font-size: 12px; color: #666;">
<span>Day 1</span>
<span>Day 2</span>
<span>Day 3</span>
<span>Day 4</span>
</div>
</div>
<div style="display: flex; gap: 20px; width: 100vw; flex: 1 1 0; box-sizing: border-box;">
<div id="scatter-plot" style="flex: 1 1 0; height: 100%;"></div>
<div id="time-series-container" style="flex: 1 1 0; display: flex; flex-direction: column; height: 100%;">
<div style="padding: 10px 0 0 0; text-align: center;">
<label><input type="radio" name="y-scale" value="linear" checked> lin y</label>
<label style="margin-left: 20px;"><input type="radio" name="y-scale" value="log"> log y</label>
<span style="margin-left: 40px;"></span>
<label><input type="radio" name="x-scale" value="linear" checked> lin x</label>
<label style="margin-left: 20px;"><input type="radio" name="x-scale" value="log"> log x</label>
</div>
<div id="time-series" style="flex: 1 1 0; height: 100%;"></div>
</div>
</div>
</div>
<script>
// Sample data - 4x4 spatial grid
const xCoords = [0, 10, 20, 30]; // in cm
const yCoords = [0, 10, 20, 30]; // in cm
// 4 different heatmaps (days)
const heatmapData = {
'Day 1': [
[0.5, 0.7, 0.6, 0.8],
[0.8, 1.2, 0.9, 1.1],
[1.0, 1.5, 1.1, 1.3],
[1.2, 1.7, 1.4, 1.6]
],
'Day 2': [
[0.6, 0.8, 0.7, 0.9],
[0.9, 1.3, 1.0, 1.2],
[1.2, 1.7, 1.3, 1.5],
[1.4, 1.9, 1.6, 1.8]
],
'Day 3': [
[0.7, 0.9, 0.8, 1.0],
[1.0, 1.4, 1.1, 1.3],
[1.3, 1.9, 1.5, 1.7],
[1.6, 2.1, 1.8, 2.0]
],
'Day 4': [
[0.8, 1.0, 0.9, 1.1],
[1.1, 1.5, 1.2, 1.4],
[1.5, 2.1, 1.7, 1.9],
[1.8, 2.3, 2.0, 2.2]
]
};
const days = Object.keys(heatmapData);
let currentDay = 0;
// Generate region names for each spatial point
const flatPoints = [];
for (let i = 0; i < yCoords.length; i++) {
for (let j = 0; j < xCoords.length; j++) {
flatPoints.push(`(${xCoords[j]}, ${yCoords[i]})`);
}
}
// Dose time series for each spatial point (hardcoded example data)
const doseTimeSeries = {
'(0, 0)': { x: [1, 2, 3, 4], y: [0.5e1, 0.7e2, 0.8e4, 1.0e5] },
'(10, 0)': { x: [1, 2, 3, 4], y: [0.6e1, 0.8e2, 1.0e4, 1.1e5] },
'(20, 0)': { x: [1, 2, 3, 4], y: [0.7e1, 0.9e2, 1.1e4, 1.3e5] },
'(30, 0)': { x: [1, 2, 3, 4], y: [0.8e1, 1.0e2, 1.2e4, 1.4e5] },
'(0, 10)': { x: [1, 2, 3, 4], y: [0.8e1, 1.0e2, 1.2e4, 1.4e5] },
'(10, 10)': { x: [1, 2, 3, 4], y: [1.0e1, 1.1e2, 1.3e4, 1.5e5] },
'(20, 10)': { x: [1, 2, 3, 4], y: [1.1e1, 1.3e2, 1.4e4, 1.7e5] },
'(30, 10)': { x: [1, 2, 3, 4], y: [1.2e1, 1.4e2, 1.6e4, 1.8e5] },
'(0, 20)': { x: [1, 2, 3, 4], y: [1.2e1, 1.4e2, 1.5e4, 1.8e5] },
'(10, 20)': { x: [1, 2, 3, 4], y: [1.3e1, 1.5e2, 1.7e4, 2.0e5] },
'(20, 20)': { x: [1, 2, 3, 4], y: [1.4e1, 1.6e2, 1.9e4, 2.2e5] },
'(30, 20)': { x: [1, 2, 3, 4], y: [1.5e1, 1.7e2, 2.0e4, 2.3e5] },
'(0, 30)': { x: [1, 2, 3, 4], y: [1.6e1, 1.8e2, 2.1e4, 2.4e5] },
'(10, 30)': { x: [1, 2, 3, 4], y: [1.7e1, 1.9e2, 2.2e4, 2.5e5] },
'(20, 30)': { x: [1, 2, 3, 4], y: [1.8e1, 2.0e2, 2.3e4, 2.6e5] },
'(30, 30)': { x: [1, 2, 3, 4], y: [1.9e1, 2.1e2, 2.4, 2.7] }
};
// Contour data (4x4 grid, int values 0, 1, 2) with straight interfaces
const contourData = [
[0, 0, 1, 1],
[0, 0, 1, 1],
[2, 2, 1, 1],
[2, 2, 1, 1]
];
// Track the current axis scales for the scatter plot
let scatterYScale = document.querySelector('input[name="y-scale"]:checked').value;
let scatterXScale = document.querySelector('input[name="x-scale"]:checked').value;
// Function to handle heatmap clicks
function handleHeatmapClick(data) {
const pointData = data.points[0];
const selectedPoint = pointData.text;
const regionData = doseTimeSeries[selectedPoint];
const timeSeriesTrace = {
x: regionData.x,
y: regionData.y,
type: 'scatter',
mode: 'lines+markers',
name: selectedPoint,
line: {color: 'red', width: 3},
marker: {size: 8}
};
const timeSeriesLayout = {
title: `Dose Trend Over Time - ${selectedPoint}`,
xaxis: {title: 'Time [days]', type: scatterXScale},
yaxis: {title: 'Dose [Sv/hr]', type: scatterYScale}
};
Plotly.newPlot('time-series', [timeSeriesTrace], timeSeriesLayout);
}
// Function to create/update heatmap
function updateHeatmap(dayIndex) {
const dayName = days[dayIndex];
const data = heatmapData[dayName];
const heatmapTrace = {
z: data,
x: xCoords,
y: yCoords,
type: 'heatmap',
colorscale: 'Viridis',
showscale: true,
colorbar: { title: 'Dose [Sv/hr]' },
hovertemplate: '<b>Pos: %{text}</b><br>Dose: %{z} Sv/hr<extra></extra>',
text: [
flatPoints.slice(0, 4),
flatPoints.slice(4, 8),
flatPoints.slice(8, 12),
flatPoints.slice(12, 16)
]
};
const contourTrace = {
z: contourData,
x: xCoords,
y: yCoords,
type: 'contour',
showscale: false,
contours: {
coloring: 'lines',
showlabels: false,
start: 0,
end: 2,
size: 1,
values: [0, 1, 2]
},
line: {color: 'black', width: 2, smoothing: 0}, // all contours black
opacity: 1,
hoverinfo: 'skip',
visible: true,
cliponaxis: false,
// Ensures all lines are black
autocolorscale: false,
colorscale: [[0, 'black'], [1, 'black']]
};
const heatmapLayout = {
title: `Spatial Dose Map - ${dayName} (Click on a cell)`,
xaxis: {
title: 'x [cm]',
scaleanchor: 'y',
scaleratio: 1,
showline: false,
zeroline: false,
showgrid: false,
ticks: '',
showticklabels: true
},
yaxis: {
title: 'y [cm]',
showline: false,
zeroline: false,
showgrid: false,
ticks: '',
showticklabels: true
},
font: {size: 14}
};
Plotly.newPlot('scatter-plot', [heatmapTrace, contourTrace], heatmapLayout);
document.getElementById('scatter-plot').on('plotly_click', handleHeatmapClick);
}
// Listen for radio button changes (y and x)
document.querySelectorAll('input[name="y-scale"]').forEach(el => {
el.addEventListener('change', function(e) {
scatterYScale = e.target.value;
Plotly.relayout('time-series', { 'yaxis.type': scatterYScale });
});
});
document.querySelectorAll('input[name="x-scale"]').forEach(el => {
el.addEventListener('change', function(e) {
scatterXScale = e.target.value;
Plotly.relayout('time-series', { 'xaxis.type': scatterXScale });
});
});
// Initialize with first heatmap
updateHeatmap(0);
// Scroll bar event listener
document.getElementById('day-slider').addEventListener('input', function(e) {
const dayIndex = parseInt(e.target.value);
currentDay = dayIndex;
updateHeatmap(dayIndex);
});
// Initial time series plot (empty)
const initialTimeSeriesLayout = {
title: 'Dose Trend Over Time - Click a location to see data',
xaxis: {title: 'Time [days]', type: scatterXScale},
yaxis: {title: 'Dose [Sv/hr]', type: scatterYScale}
};
Plotly.newPlot('time-series', [], initialTimeSeriesLayout);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment