prevent colors from being too similar

This commit is contained in:
2026-07-14 23:53:58 +02:00
parent 79088f405f
commit 3ac6d32b28
+12 -2
View File
@@ -1,7 +1,17 @@
const $=s=>document.querySelector(s), fmt=n=>new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1}).format(n||0), fmt2=n=>new Intl.NumberFormat('en',{notation:'compact',minimumFractionDigits:2,maximumFractionDigits:2}).format(n||0), money=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:n<10?2:0}).format(n||0), pct=n=>`${(n||0).toFixed(1)}%`; const $=s=>document.querySelector(s), fmt=n=>new Intl.NumberFormat('en',{notation:'compact',maximumFractionDigits:1}).format(n||0), fmt2=n=>new Intl.NumberFormat('en',{notation:'compact',minimumFractionDigits:2,maximumFractionDigits:2}).format(n||0), money=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:n<10?2:0}).format(n||0), pct=n=>`${(n||0).toFixed(1)}%`;
let data, chartMode='models'; let data, chartMode='models';
const palette=['#7357ff','#20a4a8','#ff805d','#e4b63f','#4b8cff','#d45bba','#72a33a','#9b6b43','#32343a','#a5a2ff','#19b875','#e85d5d']; const palette=['#7357ff','#20a4a8','#ff805d','#e4b63f','#4b8cff','#d45bba','#72a33a','#9b6b43','#32343a','#a5a2ff','#19b875','#e85d5d'];
const modelColor=name=>{let hash=2166136261;for(const char of String(name))hash=Math.imul(hash^char.codePointAt(0),16777619);return palette[(hash>>>0)%palette.length]}; const modelColors=new Map(),colorRgb=color=>color.match(/\w\w/g).map(x=>parseInt(x,16)),colorDistance=(a,b)=>Math.hypot(...colorRgb(a).map((x,i)=>x-colorRgb(b)[i]));
function colorHash(name){let hash=2166136261;for(const char of String(name))hash=Math.imul(hash^char.codePointAt(0),16777619);return hash>>>0}
function syncModelColors(names){
for(const name of [...new Set(names)].sort()){
if(modelColors.has(name))continue;
const used=[...modelColors.values()],start=colorHash(name)%palette.length,candidates=palette.map((_,i)=>palette[(start+i)%palette.length]).filter(color=>!used.includes(color));
const distance=color=>used.length?Math.min(...used.map(other=>colorDistance(color,other))):Infinity;
modelColors.set(name,candidates.find(color=>distance(color)>=90)||candidates.sort((a,b)=>distance(b)-distance(a))[0]||palette[start]);
}
}
const modelColor=name=>{syncModelColors([name]);return modelColors.get(name)};
async function load(){ async function load(){
const q=new URLSearchParams(); for(const id of ['from','to','project','model','source']) if($('#'+id).value) q.set(id,$('#'+id).value); const q=new URLSearchParams(); for(const id of ['from','to','project','model','source']) if($('#'+id).value) q.set(id,$('#'+id).value);
$('#subtitle').textContent='Reading local session history…'; $('#subtitle').textContent='Reading local session history…';
@@ -10,7 +20,7 @@ async function load(){
function options(el,values,label){const old=el.value;el.innerHTML=`<option value="">All ${label}</option>`+values.map(x=>`<option>${esc(x)}</option>`).join('');el.value=old} function options(el,values,label){const old=el.value;el.innerHTML=`<option value="">All ${label}</option>`+values.map(x=>`<option>${esc(x)}</option>`).join('');el.value=old}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))} function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]))}
function render(){ function render(){
const {total:t,meta}=data; options($('#project'),data.filters.projects,'projects');options($('#model'),data.filters.models,'models');options($('#source'),data.filters.sources||[],'sources'); const {total:t,meta}=data;syncModelColors(data.filters.models);options($('#project'),data.filters.projects,'projects');options($('#model'),data.filters.models,'models');options($('#source'),data.filters.sources||[],'sources');
$('#subtitle').textContent=`${meta.files} sessions · ${meta.events.toLocaleString()} model calls · scanned ${new Date(meta.scannedAt).toLocaleTimeString()}`; $('#subtitle').textContent=`${meta.files} sessions · ${meta.events.toLocaleString()} model calls · scanned ${new Date(meta.scannedAt).toLocaleTimeString()}`;
$('#cost').textContent=money(t.cost);const coverage=t.total?t.priced/t.total*100:0;$('#coverage').textContent=`${pct(coverage)} token pricing coverage · standard API rates`; $('#cost').textContent=money(t.cost);const coverage=t.total?t.priced/t.total*100:0;$('#coverage').textContent=`${pct(coverage)} token pricing coverage · standard API rates`;
const uncached=Math.max(0,t.input-t.cached); const uncached=Math.max(0,t.input-t.cached);