/* TCG Study — interactive price chart Single-series line with crosshair, tooltip, keyboard readout, range windows and a table view. Dark-surface only (the desk has no light mode). Series color: brand gold #c9962a — contrast vs panel #131a28 passes >= 3:1. Verified vs estimated closes carry a shape difference (filled vs ring), so the distinction never rests on color alone. */ (function (global) { 'use strict'; var GOLD = '#c9962a'; var SURFACE = '#131a28'; var GRID = '#232d40'; var INK_MUTED = '#8a94a6'; var INK = '#e4e0d6'; var CSS = [ '.tcgc{position:relative;width:100%}', '.tcgc svg{display:block;width:100%;height:auto;touch-action:none}', '.tcgc svg:focus{outline:1px solid ' + GOLD + ';outline-offset:3px}', '.tcgc-bar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:0 0 12px;', 'font-family:ui-monospace,Menlo,monospace;font-size:10px;letter-spacing:.16em;text-transform:uppercase}', '.tcgc-bar button{background:transparent;border:1px solid ' + GRID + ';color:' + INK_MUTED + ';', 'border-radius:4px;padding:5px 9px;cursor:pointer;font:inherit;letter-spacing:inherit}', '.tcgc-bar button:hover{color:' + GOLD + ';border-color:' + GOLD + '}', '.tcgc-bar button[aria-pressed="true"]{color:#0c111c;background:' + GOLD + ';border-color:' + GOLD + ';font-weight:700}', '.tcgc-spacer{flex:1}', '.tcgc-key{color:' + INK_MUTED + ';letter-spacing:.1em;display:flex;gap:14px;align-items:center}', '.tcgc-key i{font-style:normal}', '.tcgc-tip{position:absolute;pointer-events:none;opacity:0;transition:opacity .09s;', 'background:#0a0f18;border:1px solid ' + GRID + ';border-radius:6px;padding:9px 11px;', 'font-family:ui-monospace,Menlo,monospace;min-width:132px;max-width:250px;z-index:5;', 'box-shadow:0 6px 20px rgba(0,0,0,.55)}', '.tcgc-tip.on{opacity:1}', '.tcgc-tip .v{color:' + INK + ';font-size:16px;font-weight:700;letter-spacing:-.01em;display:flex;align-items:center;gap:7px}', '.tcgc-tip .v s{display:inline-block;width:14px;height:2px;background:' + GOLD + ';text-decoration:none;flex:none}', '.tcgc-tip .l{color:' + INK_MUTED + ';font-size:10px;letter-spacing:.16em;text-transform:uppercase;margin-top:5px}', '.tcgc-tip .n{color:' + INK_MUTED + ';font-size:11px;line-height:1.5;margin-top:7px;border-top:1px solid ' + GRID + ';padding-top:6px}', '.tcgc-table{width:100%;border-collapse:collapse;margin-top:14px;font-family:ui-monospace,Menlo,monospace;font-size:12px}', '.tcgc-table th{text-align:left;color:' + INK_MUTED + ';font-size:10px;letter-spacing:.16em;text-transform:uppercase;', 'font-weight:400;padding:7px 10px 7px 0;border-bottom:1px solid ' + GRID + '}', '.tcgc-table td{padding:7px 10px 7px 0;border-bottom:1px solid rgba(35,45,64,.55);color:' + INK + '}', '.tcgc-table td.m{color:' + INK_MUTED + '}', '.tcgc-empty{color:' + INK_MUTED + ';font-family:ui-monospace,Menlo,monospace;font-size:12px;', 'letter-spacing:.14em;text-transform:uppercase;padding:52px 0;text-align:center}' ].join(''); function injectCSS() { if (document.getElementById('tcgc-css')) return; var s = document.createElement('style'); s.id = 'tcgc-css'; s.textContent = CSS; document.head.appendChild(s); } var NS = 'http://www.w3.org/2000/svg'; function el(name, attrs) { var n = document.createElementNS(NS, name); for (var k in attrs) if (attrs.hasOwnProperty(k)) n.setAttribute(k, attrs[k]); return n; } function axisMoney(v, span) { var dec = span < 20 ? 2 : 0; return '$' + v.toLocaleString(undefined, { minimumFractionDigits: dec, maximumFractionDigits: dec }); } function money(v) { return '$' + v.toLocaleString(undefined, { minimumFractionDigits: v < 100 ? 2 : 0, maximumFractionDigits: v < 100 ? 2 : 0 }); } function TCGChart(host, opts) { if (!host) return; injectCSS(); opts = opts || {}; var all = (opts.points || []).filter(function (p) { return p && p.v > 0; }); var labels = opts.labels || []; host.className = (host.className || '') + ' tcgc'; host.textContent = ''; if (!all.length) { var e = document.createElement('div'); e.className = 'tcgc-empty'; e.textContent = 'No verified series — tracking not current'; host.appendChild(e); return; } // Labels rarely map 1:1 to points (17 axis marks against 64 closes, say). // Only claim a per-point label when the counts actually line up; otherwise // the supplied labels are axis ticks spread across the series, and each // point carries no invented date. Its note already names the source month. var exact = labels.length === all.length || labels.length === all.length - 1; if (exact) { var lab = labels.slice(); while (lab.length < all.length) lab.push('Live'); all.forEach(function (p, i) { p.label = lab[i]; }); } else { all.forEach(function (p) { p.label = ''; }); } // ---- controls ------------------------------------------------------- var bar = document.createElement('div'); bar.className = 'tcgc-bar'; var windows = []; [6, 12, 24].forEach(function (n) { if (all.length > n) windows.push(n); }); windows.push(all.length); var state = { win: all.length, idx: -1, table: false, log: false }; var winBtns = windows.map(function (n) { var b = document.createElement('button'); b.type = 'button'; b.textContent = n === all.length ? 'All' : 'Last ' + n; b.setAttribute('aria-pressed', String(n === state.win)); b.onclick = function () { state.win = n; state.idx = -1; winBtns.forEach(function (o, j) { o.setAttribute('aria-pressed', String(windows[j] === n)); }); draw(); }; bar.appendChild(b); return b; }); var spacer = document.createElement('span'); spacer.className = 'tcgc-spacer'; bar.appendChild(spacer); var key = document.createElement('span'); key.className = 'tcgc-key'; key.innerHTML = '● verified close○ estimated'; bar.appendChild(key); var vals0 = all.map(function (p) { return p.v; }); var spread = Math.max.apply(null, vals0) / Math.max(0.01, Math.min.apply(null, vals0)); if (spread >= 4) { var logBtn = document.createElement('button'); logBtn.type = 'button'; logBtn.textContent = 'Log scale'; logBtn.setAttribute('aria-pressed', 'false'); logBtn.title = 'Log scale keeps early years readable when a price has multiplied'; logBtn.onclick = function () { state.log = !state.log; logBtn.setAttribute('aria-pressed', String(state.log)); draw(); }; bar.appendChild(logBtn); } var tblBtn = document.createElement('button'); tblBtn.type = 'button'; tblBtn.textContent = 'Table'; tblBtn.setAttribute('aria-pressed', 'false'); tblBtn.onclick = function () { state.table = !state.table; tblBtn.setAttribute('aria-pressed', String(state.table)); tableWrap.style.display = state.table ? '' : 'none'; }; bar.appendChild(tblBtn); host.appendChild(bar); // ---- svg shell ------------------------------------------------------ var W = 1000, H = 380, PL = 64, PR = 20, PT = 20, PB = 36; var svg = el('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', tabindex: '0', 'aria-label': (opts.title ? opts.title + ' — ' : '') + 'price history, ' + all.length + ' points. Use arrow keys to read values.' }); host.appendChild(svg); var tip = document.createElement('div'); tip.className = 'tcgc-tip'; host.appendChild(tip); var tableWrap = document.createElement('div'); tableWrap.style.display = 'none'; host.appendChild(tableWrap); var view = [], X = null, Y = null; function draw() { svg.textContent = ''; view = all.slice(Math.max(0, all.length - state.win)); var vals = view.map(function (p) { return p.v; }); var mn = Math.min.apply(null, vals), mx = Math.max.apply(null, vals); var pad = (mx - mn) * 0.08 || (mx * 0.06) || 1; var lo = mn - pad, hi = mx + pad; if (lo < 0) lo = 0; var useLog = state.log && mn > 0 && (mx / mn) >= 4; var f = useLog ? Math.log : function (x) { return x; }; if (useLog) { lo = mn / 1.08; hi = mx * 1.08; } var flo = f(lo), fhi = f(hi), rg = (fhi - flo) || 1; X = function (i) { return view.length === 1 ? (PL + (W - PL - PR) / 2) : PL + (W - PL - PR) * i / (view.length - 1); }; Y = function (v) { return H - PB - (H - PT - PB) * ((f(v) - flo) / rg); }; // grid + y axis for (var g = 0; g <= 4; g++) { var y = PT + (H - PT - PB) * g / 4; svg.appendChild(el('line', { x1: PL, y1: y, x2: W - PR, y2: y, stroke: GRID, 'stroke-width': 1 })); var tv = useLog ? Math.exp(fhi - rg * g / 4) : (hi - rg * g / 4); var t = el('text', { x: PL - 10, y: y + 4, fill: INK_MUTED, 'text-anchor': 'end', 'font-family': 'ui-monospace,Menlo,monospace', 'font-size': 11 }); t.textContent = axisMoney(tv, hi - lo); svg.appendChild(t); } // area fill var defs = el('defs', {}); var grad = el('linearGradient', { id: 'tcgcg', x1: '0', y1: '0', x2: '0', y2: '1' }); grad.appendChild(el('stop', { offset: '0', 'stop-color': GOLD, 'stop-opacity': '.20' })); grad.appendChild(el('stop', { offset: '1', 'stop-color': GOLD, 'stop-opacity': '0' })); defs.appendChild(grad); svg.appendChild(defs); var d = view.map(function (p, i) { return (i ? 'L' : 'M') + X(i) + ' ' + Y(p.v); }).join(' '); svg.appendChild(el('path', { d: d + ' L' + X(view.length - 1) + ' ' + (H - PB) + ' L' + X(0) + ' ' + (H - PB) + ' Z', fill: 'url(#tcgcg)', stroke: 'none' })); svg.appendChild(el('path', { d: d, fill: 'none', stroke: GOLD, 'stroke-width': 2, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' })); // x axis — thinned so ticks never collide var ticks = []; if (exact) { var step = Math.max(1, Math.ceil(view.length / 7)); view.forEach(function (p, i) { if (i % step === 0 || i === view.length - 1) ticks.push({ x: X(i), text: p.label }); }); } else if (labels.length) { var want = Math.min(7, labels.length); for (var q = 0; q < want; q++) { var li = Math.round(q * (labels.length - 1) / (want - 1 || 1)); ticks.push({ x: PL + (W - PL - PR) * (li / (labels.length - 1 || 1)), text: labels[li] }); } } ticks.forEach(function (tk) { var t = el('text', { x: tk.x, y: H - 12, fill: INK_MUTED, 'text-anchor': 'middle', 'font-family': 'ui-monospace,Menlo,monospace', 'font-size': 11 }); t.textContent = tk.text; svg.appendChild(t); }); // crosshair (hidden until hover/focus) var cross = el('line', { y1: PT, y2: H - PB, stroke: GOLD, 'stroke-width': 1, 'stroke-dasharray': '3 3', opacity: '0' }); cross.setAttribute('data-cross', '1'); svg.appendChild(cross); // markers — filled = verified close, ring = estimated view.forEach(function (p, i) { var m = p.solid ? el('circle', { cx: X(i), cy: Y(p.v), r: 4, fill: GOLD, stroke: SURFACE, 'stroke-width': 2 }) : el('circle', { cx: X(i), cy: Y(p.v), r: 4, fill: SURFACE, stroke: GOLD, 'stroke-width': 2 }); m.setAttribute('data-m', i); svg.appendChild(m); }); // generous transparent hit layer var hit = el('rect', { x: 0, y: 0, width: W, height: H, fill: 'transparent' }); svg.appendChild(hit); buildTable(); hideTip(); } function nearest(clientX) { var r = svg.getBoundingClientRect(); var px = (clientX - r.left) / r.width * W; var best = 0, bd = Infinity; for (var i = 0; i < view.length; i++) { var dd = Math.abs(X(i) - px); if (dd < bd) { bd = dd; best = i; } } return best; } function showTip(i) { if (i < 0 || i >= view.length) return; state.idx = i; var p = view[i]; var cross = svg.querySelector('[data-cross]'); if (cross) { cross.setAttribute('x1', X(i)); cross.setAttribute('x2', X(i)); cross.setAttribute('opacity', '.7'); } Array.prototype.forEach.call(svg.querySelectorAll('[data-m]'), function (m) { var on = +m.getAttribute('data-m') === i; m.setAttribute('r', on ? 6 : 4); }); tip.textContent = ''; var v = document.createElement('div'); v.className = 'v'; var s = document.createElement('s'); v.appendChild(s); v.appendChild(document.createTextNode(money(p.v))); tip.appendChild(v); var l = document.createElement('div'); l.className = 'l'; l.textContent = (p.label ? p.label + ' · ' : '') + (p.solid ? 'verified close' : 'estimated'); tip.appendChild(l); if (p.tip) { var n = document.createElement('div'); n.className = 'n'; n.textContent = p.tip; tip.appendChild(n); } var r = svg.getBoundingClientRect(); var scale = r.width / W; var left = X(i) * scale; tip.classList.add('on'); var tw = tip.offsetWidth; if (left + tw + 16 > r.width) left -= tw + 14; else left += 14; tip.style.left = Math.max(0, left) + 'px'; tip.style.top = Math.max(0, Y(p.v) * scale - 12) + 'px'; } function hideTip() { state.idx = -1; tip.classList.remove('on'); var cross = svg.querySelector('[data-cross]'); if (cross) cross.setAttribute('opacity', '0'); Array.prototype.forEach.call(svg.querySelectorAll('[data-m]'), function (m) { m.setAttribute('r', 4); }); } function buildTable() { tableWrap.textContent = ''; var t = document.createElement('table'); t.className = 'tcgc-table'; var head = document.createElement('tr'); [exact ? 'Point' : '#', 'Close', 'Source', 'Note'].forEach(function (h) { var th = document.createElement('th'); th.textContent = h; head.appendChild(th); }); t.appendChild(head); view.forEach(function (p, ri) { var tr = document.createElement('tr'); [exact ? p.label : String(ri + 1), money(p.v), p.solid ? 'Verified' : 'Estimated', p.tip || '—'].forEach(function (c, ci) { var td = document.createElement('td'); if (ci > 1) td.className = 'm'; td.textContent = c; tr.appendChild(td); }); t.appendChild(tr); }); tableWrap.appendChild(t); } svg.addEventListener('pointermove', function (e) { showTip(nearest(e.clientX)); }); svg.addEventListener('pointerleave', hideTip); svg.addEventListener('blur', hideTip); svg.addEventListener('focus', function () { showTip(state.idx < 0 ? view.length - 1 : state.idx); }); svg.addEventListener('keydown', function (e) { var i = state.idx < 0 ? view.length - 1 : state.idx; if (e.key === 'ArrowRight') { showTip(Math.min(view.length - 1, i + 1)); e.preventDefault(); } else if (e.key === 'ArrowLeft') { showTip(Math.max(0, i - 1)); e.preventDefault(); } else if (e.key === 'Home') { showTip(0); e.preventDefault(); } else if (e.key === 'End') { showTip(view.length - 1); e.preventDefault(); } else if (e.key === 'Escape') { hideTip(); } }); draw(); global.addEventListener('resize', function () { if (state.idx >= 0) showTip(state.idx); }); return { redraw: draw }; } global.TCGChart = TCGChart; })(window);