Skip to content

Instantly share code, notes, and snippets.

@anthonywong
Created October 28, 2025 13:41
Show Gist options
  • Select an option

  • Save anthonywong/fe1b2007ff4c90dc428c0bc269ed1b58 to your computer and use it in GitHub Desktop.

Select an option

Save anthonywong/fe1b2007ff4c90dc428c0bc269ed1b58 to your computer and use it in GitHub Desktop.
Timeline chart on Ubuntu Kernel SRU tracking bug
// ==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