Created
October 28, 2025 13:41
-
-
Save anthonywong/fe1b2007ff4c90dc428c0bc269ed1b58 to your computer and use it in GitHub Desktop.
Timeline chart on Ubuntu Kernel SRU tracking bug
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
| // ==UserScript== | |
| // @name Timeline chart on Ubuntu Kernel SRU tracking bug | |
| // @namespace http://anthonywong.net/ | |
| // @version 3.0 | |
| // @description Display a timeline chart on Ubuntu Kernel SRU tracking bug | |
| // @match https://bugs.launchpad.net/kernel-sru-workflow/+bug/* | |
| // @grant none | |
| // @require https://cdn.plot.ly/plotly-2.20.0.min.js | |
| // @license GPLv2 | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // Define a color mapping for states | |
| const stateColors = { | |
| 'N': '#1f77b4', // New - Blue | |
| 'T': '#ff7f0e', // Triaged - Orange | |
| 'IP': '#2ca02c', // In Progress - Green | |
| 'FC': '#d62728', // Fix Committed - Red | |
| 'FR': '#9467bd', // Fix Released - Purple | |
| 'I': '#8c564b' // Invalid - Brown | |
| }; | |
| // Function to fetch the activity log page | |
| async function fetchActivityLog() { | |
| const activityLogUrl = location.href + '/+activity'; | |
| const response = await fetch(activityLogUrl); | |
| const html = await response.text(); | |
| const parser = new DOMParser(); | |
| return parser.parseFromString(html, 'text/html'); | |
| } | |
| // Function to parse transitions from the activity log | |
| function parseTransitions(activityLogDoc) { | |
| const transitions = []; | |
| const rows = activityLogDoc.querySelectorAll("table.listing tbody tr"); | |
| rows.forEach(row => { | |
| const cells = row.querySelectorAll("td"); | |
| if (cells.length >= 5) { | |
| const timestamp = cells[0].textContent.trim(); | |
| const description = cells[2].textContent.trim(); | |
| const oldValue = cells[3].textContent.trim(); | |
| const newValue = cells[4].textContent.trim(); | |
| // Check for "status" changes for any items | |
| const statusMatch = description.match(/kernel-sru-workflow\/([\w-]+): status/i); | |
| if (statusMatch) { | |
| transitions.push({ | |
| item: statusMatch[1], | |
| timestamp: timestamp, | |
| oldState: oldValue, | |
| newState: newValue | |
| }); | |
| } | |
| } | |
| }); | |
| return transitions; | |
| } | |
| // Function to convert states to short forms | |
| function shortenState(state) { | |
| if (!state) return ''; | |
| return state.split(' ').map(word => word[0].toUpperCase()).join(''); | |
| } | |
| // Function to group transitions by item | |
| function groupTransitions(transitions) { | |
| const grouped = {}; | |
| transitions.forEach(transition => { | |
| if (!grouped[transition.item]) { | |
| grouped[transition.item] = []; | |
| } | |
| grouped[transition.item].push({ | |
| timestamp: new Date(transition.timestamp), | |
| state: shortenState(transition.newState) | |
| }); | |
| }); | |
| return grouped; | |
| } | |
| // Function to draw the timeline chart using Plotly | |
| function drawTimeline(groupedTransitions) { | |
| const items = Object.keys(groupedTransitions); | |
| const traces = []; | |
| items.forEach((item, index) => { | |
| const transitions = groupedTransitions[item]; | |
| const x = transitions.map(t => t.timestamp); | |
| const y = Array(x.length).fill(item); | |
| const text = transitions.map(t => t.state); | |
| const colors = transitions.map(t => stateColors[t.state] || '#000000'); // Default to black if state not in mapping | |
| traces.push({ | |
| x: x, | |
| y: y, | |
| mode: 'lines+markers+text', | |
| text: text, | |
| textposition: 'top center', | |
| line: { shape: 'hv', color: colors[0] }, // Line color is based on the first state | |
| marker: { size: 10, color: colors }, // Marker colors based on state | |
| name: item | |
| }); | |
| }); | |
| const layout = { | |
| title: 'Bug State Transitions Timeline', | |
| xaxis: { | |
| title: 'Time', | |
| type: 'date' | |
| }, | |
| yaxis: { | |
| title: '', // No Y-axis label | |
| automargin: true, // Ensure labels fit within the canvas | |
| autorange: 'reversed' // Reverse the Y-axis order | |
| }, | |
| showlegend: false, // Disable the legend | |
| height: 800, | |
| width: 1000, // Constrain width to avoid overlap with content | |
| margin: { l: 150, r: 50, t: 50, b: 50 } // Increase left margin for Y-axis labels | |
| }; | |
| // Create a container for the chart | |
| const container = document.createElement('div'); | |
| container.id = 'timeline-chart'; | |
| container.style.margin = '20px auto'; // Center horizontally | |
| container.style.maxWidth = '1000px'; // Constrain width | |
| container.style.padding = '20px'; | |
| container.style.border = '1px solid #ddd'; | |
| container.style.borderRadius = '8px'; | |
| container.style.backgroundColor = '#ffffff'; | |
| // Insert the container before the <div id="maincontentsub"> | |
| const mainContentSub = document.getElementById('maincontentsub'); | |
| if (mainContentSub) { | |
| mainContentSub.parentNode.insertBefore(container, mainContentSub); | |
| } | |
| Plotly.newPlot('timeline-chart', traces, layout); | |
| } | |
| // Main execution | |
| async function main() { | |
| const activityLogDoc = await fetchActivityLog(); | |
| const transitions = parseTransitions(activityLogDoc); | |
| if (transitions.length > 0) { | |
| const groupedTransitions = groupTransitions(transitions); | |
| drawTimeline(groupedTransitions); | |
| } else { | |
| console.warn('No transitions found.'); | |
| } | |
| } | |
| main(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment