|
// ==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); |
|
})(); |