Created
July 7, 2025 08:43
-
-
Save shimwell/b638594dccb10d8ba0d747b3929bf550 to your computer and use it in GitHub Desktop.
plotly time series heat map with interactive click
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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