2003809b18f5b9267e19693568edbccede0e6ab2 max Tue Sep 15 05:48:44 2026 -0700 hgc scatterPlot: bounds by walking the points, and no duplicate legend entries, refs #38353 niceBounds() collected every coordinate into an array and passed it to Math.min(...xs). A spread is one argument per point, and this module is meant for clouds of tens of thousands: measured, 120,000 points are fine and 200,000 throw RangeError, which the promise's catch turns into "Plot data could not be loaded". Nothing shipped is that big yet. Walked instead, which needs no arrays either. A file that supplies a labels list to fix the legend order and then gives its points in object form had every category appended a second time, so each one appeared twice in the legend. The indexer is seeded from the labels already present. diff --git src/hg/js/hgc.scatterPlot.js src/hg/js/hgc.scatterPlot.js index b3658be2a08..12c0bd238a8 100644 --- src/hg/js/hgc.scatterPlot.js +++ src/hg/js/hgc.scatterPlot.js @@ -169,39 +169,48 @@ // fall through: a mislabelled or extensionless TSV still works } } return parseTsv(text); }); cloudCache.set(key, p); return p; } function makeCloud() { return {points: [], labels: [], hasLabels: false}; } function catIndexer(cloud) { // Maps a category name to its index in cloud.labels, adding it on first sight. + // Seeded from the labels the cloud already has: a file is allowed to supply a + // labels list to fix the legend order and then give its points in object form, + // and without the seeding every one of those names would be appended a second + // time and show up twice in the legend. let seen = new Map(); + cloud.labels.forEach((name, i) => { + if (!seen.has(name)) + seen.set(name, i); + }); return function (name) { if (!name) return -1; - if (!seen.has(name)) { - seen.set(name, cloud.labels.length); - cloud.labels.push(String(name)); + let key = String(name); + if (!seen.has(key)) { + seen.set(key, cloud.labels.length); + cloud.labels.push(key); } - return seen.get(name); + return seen.get(key); }; } function addPoint(cloud, x, y, cat, label) { if (!isFinite(x) || !isFinite(y)) return; let lab = (label === undefined || label === null || label === "") ? null : String(label); if (lab !== null) cloud.hasLabels = true; cloud.points.push([x, y, cat, lab]); } function normalizePoints(data) { // Returns {points: [[x,y,catIndex,label|null], ...], labels: [...], hasLabels} let raw = Array.isArray(data) ? data : (data && data.points); @@ -275,43 +284,48 @@ let x = Number(xm[1]), y = Number(ym[1]); return (isFinite(x) && isFinite(y)) ? {x: x, y: y} : null; } return null; } function labelColor(i, n) { // Hues stepped in legend order, which the data file puts in cluster order, so // categories that sit together in the plot also sit together in hue. if (n <= 0 || i < 0) return CLOUD_COLOR; return "hsl(" + Math.round((360 * i) / n) + ", 62%, 47%)"; } function niceBounds(points, highlights) { - let xs = [], ys = []; - for (let p of points) { - xs.push(p[0]); - ys.push(p[1]); - } - for (let h of highlights) { - xs.push(h.x); - ys.push(h.y); - } - if (xs.length === 0) + // Walked rather than collected and passed to Math.min(...xs): a spread is one + // argument per point, and this module is meant for clouds of tens of thousands, + // where that overflows the call stack (a RangeError somewhere above 120,000 on + // current V8) and the plot silently degrades to "data could not be loaded". + let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity, n = 0; + let see = (x, y) => { + if (x < x0) x0 = x; + if (x > x1) x1 = x; + if (y < y0) y0 = y; + if (y > y1) y1 = y; + n++; + }; + for (let p of points) + see(p[0], p[1]); + for (let h of highlights) + see(h.x, h.y); + if (n === 0) return null; - let x0 = Math.min(...xs), x1 = Math.max(...xs); - let y0 = Math.min(...ys), y1 = Math.max(...ys); // A degenerate range would divide by zero; give it some width. if (x1 - x0 < 1e-9) { x0 -= 0.5; x1 += 0.5; } if (y1 - y0 < 1e-9) { y0 -= 0.5; y1 += 0.5; } let padX = (x1 - x0) * 0.05, padY = (y1 - y0) * 0.05; return {x0: x0 - padX, x1: x1 + padX, y0: y0 - padY, y1: y1 + padY}; } function render(host, cloud, highlights, cfg) { let points = cloud.points, labels = cloud.labels; let b = niceBounds(points, highlights); if (!b) { host.textContent = "No data"; return; }