Skip to content

Instantly share code, notes, and snippets.

@alber70g
Last active May 13, 2025 08:47
Show Gist options
  • Save alber70g/e0bdad727189fd7ad66f61b609377686 to your computer and use it in GitHub Desktop.
Save alber70g/e0bdad727189fd7ad66f61b609377686 to your computer and use it in GitHub Desktop.
Popularity color indicator for Show HN for TamperMonkey

Visual Popularity Indicator with Colors based on Vote Count

How to install

Vote Popularity

Changes

0.8

  • fix with new HN elements
  • adds more logging

0.7

  • fix when multiple tables side by side

0.6

  • only select the first table when using it with Hacker UX Chrome extension (somehow it didn't get fixed in 0.4)

0.5

  • ability to use it for multiple websites (of course they need to add a @match in the UserScript metadata)

0.4

  • select first table (to be compatible Hacker News UX Chrome extension)

0.3

  • changed colors to be more defined and only using 5 colors in total
  • changed skipcount to 10% to allow more differentiation between outliers

0.2

  • also show on /shownew

0.1

  • initial release
// ==UserScript==
// @name Show HN vote color identification
// @namespace gist.github.com/Alber70g
// @version 0.8
// @description HN votes colors (logarithmic)
// @author Alber70g
// @match https://news.ycombinator.com/*
// @require https://unpkg.com/[email protected]/chroma.js
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require https://gist.github.com/raw/2625891/waitForKeyElements.js
// @grant none
// @updateURL https://gist.github.com/alber70g/e0bdad727189fd7ad66f61b609377686/raw/HNPopularityColors.user.js
// ==/UserScript==
(function(){
function createLogger(name, level = 4, logger = console) {
const levels = {
error: 0,
warning: 1,
success: 2,
info: 3,
start: 4,
debug: 5,
trace: 6
};
const formatMessage = (emoji, levelName, message) => `${emoji} [${name}] ${levelName}: ${message}`;
const shouldLog = (messageLevel) => levels[messageLevel] <= level;
return {
start: (message) => {
if (shouldLog('start')) logger.log(formatMessage('🚀', 'START', message));
},
info: (message) => {
if (shouldLog('info')) logger.info(formatMessage('ℹ️', 'INFO', message));
},
success: (message) => {
if (shouldLog('success')) logger.log(formatMessage('✅', 'SUCCESS', message));
},
warning: (message) => {
if (shouldLog('warning')) logger.warn(formatMessage('⚠️', 'WARNING', message));
},
error: (message) => {
if (shouldLog('error')) logger.error(formatMessage('❌', 'ERROR', message));
},
debug: (message) => {
if (shouldLog('debug')) logger.debug(formatMessage('🐞', 'DEBUG', message));
},
trace: (message) => {
if (shouldLog('trace')) logger.trace(formatMessage('🔬', 'TRACE', message));
}
};
}
const { start, info, success, warning, error, debug, trace } = createLogger('HN Votes');
function colorPopularity(href) {
start('Initializing HN Vote Colors script');
if (!href.match(/news\.ycombinator\.com\.*/)) {
info('Not on news.ycombinator.com, skipping execution');
return;
}
debug('URL matched, proceeding with hackerNews');
hackerNews();
}
function hackerNews() {
info('Waiting for table with score elements');
let timeoutId = setTimeout(() => {
error('No table with score elements found on page');
}, 5000);
waitForKeyElements('table .score', () => {
clearTimeout(timeoutId);
const table = Array.from(document.querySelectorAll('table')).find(t => t.querySelector('.score'));
if (!table) {
error('No table with score elements found on page');
return;
}
const elements = Array.from(table.querySelectorAll('.score')).map(x => x.parentElement.parentElement);
if (elements.length === 0) {
warning('No score elements found in table');
return;
}
debug(`Found ${elements.length} score elements`);
const scoreFn = (el) => {
const scoreText = el.querySelector('.score')?.innerText;
if (!scoreText) {
warning('Score text missing for element');
return 0;
}
const score = parseInt(scoreText.split(' ')[0], 10);
if (isNaN(score)) {
warning('Invalid score format for element');
return 0;
}
return score;
};
const styleFn = (el, color) => {
if (!el.children[0]) {
warning('No child element to style');
return;
}
el.children[0].style = `background: linear-gradient(0deg, #f6f6ef 50%, ${color} 50%);`;
trace(`Applied color ${color} to element`);
};
colorVoting(elements, scoreFn, styleFn);
success(`Processed ${elements.length} elements with vote colors`);
}, false);
}
function colorVoting(elements, scoreFn, styleFn) {
if (!elements || elements.length === 0) {
error('No elements provided to colorVoting');
return;
}
info(`Coloring ${elements.length} voting elements`);
const arrayElements = Array.from(elements);
const allScores = arrayElements.map((x, i) => {
const score = scoreFn(x);
trace(`Element ${i} score: ${score}`);
return score;
});
const [high, low] = getHighLow(allScores);
debug(`Score range: low=${low}, high=${high}`);
if (high === low) {
warning('All scores are identical, using single color');
}
/**
* Color palette being used
* http://gka.github.io/palettes/#diverging|c0=lightgrey,lime,yellow|c1=yellow,Red|steps=12|bez0=0|bez1=0|coL0=1|coL1=1
*/
const getColor = chroma.scale(['lightgrey', 'lime', 'orange', 'Red']).domain([low, high], 5);
arrayElements.forEach((el, i) => {
const score = scoreFn(el);
const color = getColor(score);
styleFn(el.parentElement, color);
trace(`Element ${i} assigned color ${color} for score ${score}`);
});
}
function getHighLow(allVotes) {
if (!allVotes || allVotes.length === 0) {
error('No votes provided to getHighLow');
return [0, 0];
}
const sortedVotes = allVotes.sort((a, b) => a - b);
const skipCount = allVotes.length / 100 * 5;
const high = sortedVotes[Math.round(allVotes.length - skipCount)] || 0;
const low = sortedVotes[Math.round(skipCount)] || 0;
debug(`Calculated high=${high}, low=${low} from ${allVotes.length} votes`);
return [high, low];
}
colorPopularity(window.location.href);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment