{# Particle explorer scatter: volume animate sync, discrete/continuous colour filters #} function traceColorNumericValue(ti) { if (!gd || !gd.data || !gd.data[0]) return null; var trace = gd.data[0]; var markerColors = trace.marker ? trace.marker.color : null; if (isPlotlyArray(markerColors)) { var mv = Number(PLOTLY.valueAt(markerColors, ti)); if (isFinite(mv)) return mv; } var cd = trace.customdata; var cdLen = customdataLength(cd); if (cd && ti < cdLen) { var row = plotlyDataRowAsArray(PLOTLY.rowAt(cd, ti)); if (row.length > 2) { var dv = Number(row[2]); if (isFinite(dv)) return dv; } } return null; } function currentColorValueItems() { if (!gd || !gd.data || !gd.data[0] || !gd.data[0].customdata) return []; if (!sc.value || sc.value === "none") return []; var cd = gd.data[0].customdata; var out = []; var len = customdataLength(cd); for (var i = 0; i < len; i++) { var row = PLOTLY.rowValue(cd, i, 1); var val = traceColorNumericValue(i); if (row != null && val != null) out.push({ti: i, row: row, value: val}); } return out; } function syncParticleExplorerRowExpandForColorHist() { window.requestAnimationFrame(function() { syncScatterControlsAlignment(); scheduleSyncMontageGridRegionLayout(); try { if (gd && gd.data && gd.data.length) Plotly.Plots.resize(gd); } catch (e) {} if (colorHistPanel && !colorHistPanel.hidden && colorHistDiv) { try { Plotly.Plots.resize(colorHistDiv); } catch (e2) {} } equalizeMontageActionButtonWidths(); if (explorerColorLegend) { if (typeof explorerColorLegend.fitDiscreteSwitchColumnWidths === "function") { explorerColorLegend.fitDiscreteSwitchColumnWidths(); } if (typeof explorerColorLegend.fitDiscreteLegendScrollRegion === "function") { explorerColorLegend.fitDiscreteLegendScrollRegion(); } if (typeof explorerColorLegend.measureDiscreteInvertAsideColumn === "function") { explorerColorLegend.measureDiscreteInvertAsideColumn(); } } }); } function hideColorHistogram() { if (explorerColorLegend) { explorerColorLegend.hide(); } if (colorHistPanel) { colorHistPanel.hidden = true; colorHistPanel.setAttribute("aria-hidden", "true"); } if (colorThresholdStatus) colorThresholdStatus.textContent = ""; if (colorContinuousWrap) colorContinuousWrap.hidden = false; if (colorDiscreteWrap) { colorDiscreteWrap.hidden = true; colorDiscreteWrap.classList.remove("cryo-color-discrete-wrap--show"); colorDiscreteWrap.classList.remove("cryo-cc-discrete-wrap--show"); } if (colorDiscreteSwitches) colorDiscreteSwitches.innerHTML = ""; if (colorDiscreteWrap) colorDiscreteWrap.classList.remove("cryo-discrete-legend--collapsed"); syncDiscreteInvertSelectionButton(); syncParticleExplorerRowExpandForColorHist(); } function discreteCategoryKeyFromCustomRow(cdrow) { var row = plotlyDataRowAsArray(cdrow); if (row.length < 3) return "__na__"; var v = row[2]; if (v == null || v === "") return "__na__"; if (typeof v === "number" && !isFinite(v)) return "__na__"; return String(v); } function syncDiscreteInvertSelectionButton() { if (!btnDiscreteInvertSelection || !colorDiscreteSwitches) return; var inputs = colorDiscreteSwitches.querySelectorAll("input[type=\"checkbox\"]"); var anyChecked = false; for (var sx = 0; sx < inputs.length; sx++) { if (inputs[sx].checked) { anyChecked = true; break; } } if (!anyChecked) { btnDiscreteInvertSelection.disabled = true; btnDiscreteInvertSelection.textContent = "no selection made!"; return; } btnDiscreteInvertSelection.disabled = false; btnDiscreteInvertSelection.textContent = "Invert selection"; } function explorerApplyDiscreteMarkerColoursFromOverrides() { if (!gd || !gd.data || !gd.data[0] || scatterTraceColorMode() !== "discrete") return; var tr = gd.data[0]; var cd = tr.customdata || []; var mcSrc = tr.marker && tr.marker.color ? tr.marker.color : null; var cdLen = customdataLength(cd); if (!cdLen || !isPlotlyArray(mcSrc) || plotlyArrayLength(mcSrc) !== cdLen) return; var mc = PLOTLY.slice(mcSrc); var changed = false; for (var pi = 0; pi < cdLen; pi++) { var pk = discreteCategoryKeyFromCustomRow(PLOTLY.rowAt(cd, pi)); var cand = explorerDiscreteLabelColors[String(pk)]; var hx = cand ? CryoColorCovariateLegend.normalizeDiscreteLegendHex(String(cand)) : ""; if (hx && mc[pi] !== hx) { mc[pi] = hx; changed = true; } } if (!changed) return; try { Plotly.restyle(gd, { "marker.color": [mc] }, [0]); } catch (re0) {} explorerMontageReloadAfterDiscretePaletteEdit(); } function explorerMontageReloadAfterDiscretePaletteEdit() { if (!lastMontageRows || !lastMontageRows.length) return; var items = []; for (var mm = 0; mm < lastMontageRows.length; mm++) { var mw = montageItemForRow(lastMontageRows[mm]); if (mw) items.push(mw); } if (items.length) updateMontage(items); } function explorerLegendContextData() { if (!gd || !gd.data || !gd.data[0] || !sc.value || sc.value === "none") return {}; if (scatterTraceColorMode() === "discrete") { var meta = gd.layout && gd.layout.meta; var datasetCounts = (meta && meta.cdrgn_discrete_category_counts) || {}; var cd = gd.data[0].customdata || []; var markerColors = (gd.data[0].marker && gd.data[0].marker.color) || null; var colorByKey = {}; var tracedCountByKey = {}; var len = customdataLength(cd); for (var i = 0; i < len; i++) { var fk = discreteCategoryKeyFromCustomRow(PLOTLY.rowAt(cd, i)); tracedCountByKey[fk] = (tracedCountByKey[fk] || 0) + 1; if (!colorByKey[fk] && isPlotlyArray(markerColors)) { var cval = PLOTLY.valueAt(markerColors, i); if (typeof cval === "string" && /^#/.test(cval)) colorByKey[fk] = cval; } } var keyUnion = {}; var kk; for (kk in datasetCounts) { if (Object.prototype.hasOwnProperty.call(datasetCounts, kk)) keyUnion[kk] = true; } for (kk in tracedCountByKey) { if (Object.prototype.hasOwnProperty.call(tracedCountByKey, kk)) keyUnion[kk] = true; } var allKeys = Object.keys(keyUnion); var sortFn = CryoColorCovariateLegend.sortDiscreteKeys || function (keys) { return keys.slice().sort(); }; var sortedKeys = sortFn(allKeys); var cats = []; for (var j = 0; j < sortedKeys.length; j++) { var k = sortedKeys[j]; var cnt = datasetCounts[k] != null ? Number(datasetCounts[k]) : tracedCountByKey[k] || 0; var colHx = colorByKey[k]; var ovh = explorerDiscreteLabelColors[String(k)]; if (ovh) colHx = String(ovh); cats.push({ key: k, color: colHx || "#aab4bf", count: cnt }); } return {mode: "discrete", categories: cats}; } return { mode: "continuous", values: currentColorValueItems().map(function(item) { return item.value; }) }; } function setExplorerLegendDiscreteKeysForSelection() { if (!explorerColorLegend || !gd || !gd.data || !gd.data[0] || scatterTraceColorMode() !== "discrete") return; var cd = gd.data[0].customdata || []; var sel = new Set(saveSelectionRows || []); var keyToRows = new Map(); var len = customdataLength(cd); for (var i = 0; i < len; i++) { var k = discreteCategoryKeyFromCustomRow(PLOTLY.rowAt(cd, i)); var row = PLOTLY.rowValue(cd, i, 1); if (!keyToRows.has(k)) keyToRows.set(k, []); keyToRows.get(k).push(row); } var checkedKeys = []; keyToRows.forEach(function(rows, key) { if (rows.length && rows.every(function(r) { return sel.has(r); })) checkedKeys.push(key); }); explorerColorLegend.setDiscreteCheckedKeys(checkedKeys); } function applyExplorerDiscreteFilter(filter) { if (!gd || !gd.data || !gd.data[0] || scatterTraceColorMode() !== "discrete") return; var keys = filter && Array.isArray(filter.keys) ? filter.keys : []; var keySet = new Set(keys.map(function(k) { return String(k); })); clearScatterGeometricSelectionSuppressed(); colorThresholdLevel = null; colorThresholdRange = null; colorThresholdColorCol = null; if (colorThresholdStatus) colorThresholdStatus.textContent = ""; if (!keySet.size) { applyRowsSelection([], "Discrete"); return; } var cd = gd.data[0].customdata || []; var rows = []; var len = customdataLength(cd); for (var i = 0; i < len; i++) { var k = discreteCategoryKeyFromCustomRow(PLOTLY.rowAt(cd, i)); if (keySet.has(k)) rows.push(PLOTLY.rowValue(cd, i, 1)); } applyRowsSelection(rows, "Discrete"); setExplorerLegendDiscreteKeysForSelection(); } function applyExplorerContinuousFilter(filter) { var items = currentColorValueItems(); if (!filter || !items.length) return; cancelLassoSelectionDebounce(); if (explorerColorLegend) explorerColorLegend.setDiscreteCheckedKeys([]); clearScatterGeometricSelectionSuppressed(); var body = {column: sc.value}; var fallbackRows; if (filter.kind === "threshold") { var level = Number(filter.level); if (!isFinite(level)) return; colorThresholdLevel = level; colorThresholdRange = null; colorThresholdColorCol = sc.value; body.level = level; body.use_max = !!filter.use_max; fallbackRows = function() { return rowsMatchingColorThresholdFromTrace(items, level, !!filter.use_max); }; } else if (filter.kind === "range") { var lo = Number(filter.range_min); var hi = Number(filter.range_max); if (!isFinite(lo) || !isFinite(hi)) return; var loN = Math.min(lo, hi); var hiN = Math.max(lo, hi); colorThresholdLevel = null; colorThresholdRange = {lo: loN, hi: hiN}; colorThresholdColorCol = sc.value; body.range_min = loN; body.range_max = hiN; body.invert_range = !!filter.invert_range; fallbackRows = function() { return rowsMatchingColorRangeFromTrace(items, loN, hiN, !!filter.invert_range); }; } else { return; } syncColorHistModeCheckboxCaption(); fetch("{{ url_for('api_covariate_threshold_rows') }}", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body) }) .then(function(r) { return r.json().then(function(j) { return {ok: r.ok, j: j}; }); }) .then(function(res) { var rows = (res.ok && res.j && Array.isArray(res.j.rows)) ? res.j.rows : fallbackRows(); applyRowsSelection(rows, "Color threshold"); }) .catch(function() { applyRowsSelection(fallbackRows(), "Color threshold"); }); }