From 3ac6d32b280677d8abd1fea28e4c62e5c0764af8 Mon Sep 17 00:00:00 2001 From: tototomate123 Date: Tue, 14 Jul 2026 23:53:58 +0200 Subject: [PATCH] prevent colors from being too similar --- public/app.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/public/app.js b/public/app.js index 0a9c4b6..4a094a8 100644 --- a/public/app.js +++ b/public/app.js @@ -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)}%`; let data, chartMode='models'; 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(){ 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…'; @@ -10,7 +20,7 @@ async function load(){ function options(el,values,label){const old=el.value;el.innerHTML=``+values.map(x=>``).join('');el.value=old} function esc(s){return String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]))} 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()}`; $('#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);