From f2d2f8aa98523bb88b202520cd78eb06f878c73c Mon Sep 17 00:00:00 2001 From: Alex Pickering Date: Sat, 23 May 2026 05:55:08 -0400 Subject: [PATCH 1/3] adjust data-processing marker defaults for large datasets Signed-off-by: Alex Pickering --- .../ConfigureEmbedding/ConfigureEmbedding.jsx | 47 +++++++++-- .../DataIntegration/DataIntegration.jsx | 41 +++++++++- src/components/data-processing/PlotLayout.jsx | 2 + .../plotConfig/getEmbeddingInitialConfig.js | 78 +++++++++++++++++++ 4 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/utils/plotConfig/getEmbeddingInitialConfig.js diff --git a/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx b/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx index 8400df407..7f2a2d3fe 100644 --- a/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx +++ b/src/components/data-processing/ConfigureEmbedding/ConfigureEmbedding.jsx @@ -26,6 +26,7 @@ import loadCellMeta from 'redux/actions/cellMeta'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; import Loader from 'components/Loader'; import { getCellSets } from 'redux/selectors'; +import { getEmbeddingInitialConfig } from 'utils/plotConfig/getEmbeddingInitialConfig'; import CalculationConfig from 'components/data-processing/ConfigureEmbedding/CalculationConfig'; import PlotLegendAlert, { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert'; import EmptyPlot from 'components/plots/helpers/EmptyPlot'; @@ -408,6 +409,35 @@ const ConfigureEmbedding = (props) => { if (showAlert) updatePlotWithChanges({ legend: { showAlert, enabled: !showAlert } }); }, [!selectedConfig, activePlotType, cellSets.accessible]); + // Apply cell-count-aware marker defaults for large datasets in embedding plots + // Only applies once when config is first loaded and hasn't been customized yet + useEffect(() => { + if (!selectedConfig || !cellSets.accessible || plotType !== 'embedding' || !currentPlot) return; + + const initialConfig = getEmbeddingInitialConfig(currentPlot.plotType, cellSets); + + // Check if we should apply large dataset defaults + if (initialConfig.defaultValues?.largeDatasetDefaults) { + // Get the standard (non-adjusted) initial config to check if marker was already customized + const standardConfig = initialPlotConfigStates[currentPlot.plotType]; + + // Only apply if marker config currently matches standard defaults (not yet customized) + const isUsingStandardDefaults = selectedConfig.marker.outline === standardConfig.marker.outline + && selectedConfig.marker.size === standardConfig.marker.size; + + if (isUsingStandardDefaults) { + dispatch(updatePlotConfig(activePlotUuid, { + marker: { + ...selectedConfig.marker, + outline: false, + size: 1, + }, + })); + debounceSave(activePlotUuid); + } + } + }, [activePlotUuid, currentPlot?.plotType, cellSets?.accessible]); + useEffect(() => { // if we change a plot and the config is not saved yet if (outstandingChanges) { @@ -431,8 +461,8 @@ const ConfigureEmbedding = (props) => { embeddingPlotUuids.forEach((plotUuid) => { if (plotUuid !== activePlotUuid) { const otherPlotConfig = plotConfigs[plotUuid]; - if (otherPlotConfig && - JSON.stringify(otherPlotConfig.marker) !== JSON.stringify(selectedConfig.marker)) { + if (otherPlotConfig && + JSON.stringify(otherPlotConfig.marker) !== JSON.stringify(selectedConfig.marker)) { dispatch(updatePlotConfig(plotUuid, { marker: selectedConfig.marker })); } } @@ -447,7 +477,7 @@ const ConfigureEmbedding = (props) => { const plotActions = { export: true, }; - + if (cellSets.accessible && selectedConfig) { setPlot(currentPlot.plot(selectedConfig, plotActions)); } @@ -486,6 +516,8 @@ const ConfigureEmbedding = (props) => { const isEqual = Object.keys(initialConfig).every((key) => { // By pass plot data because we want to compare settings not data if (key === 'plotData') return true; + // Skip defaultValues as it's metadata about defaults, not actual config + if (key === 'defaultValues') return true; if (initialConfig.keepValuesOnReset?.includes(key)) return true; if (currentConfig[key] && typeof currentConfig[key] === 'object' && initialConfig[key] && typeof initialConfig[key] === 'object') { // For nested objects, exclude defaultValues from comparison as it's metadata about defaults @@ -503,12 +535,15 @@ const ConfigureEmbedding = (props) => { useEffect(() => { if (!selectedConfig || !currentPlot) return; - const initialConfig = initialPlotConfigStates[currentPlot.plotType]; + const initialConfig = getEmbeddingInitialConfig(currentPlot.plotType, cellSets); setIsResetDisabled(isConfigEqual(selectedConfig, initialConfig)); - }, [selectedConfig]); + }, [selectedConfig, cellSets]); const onClickReset = () => { - dispatch(resetPlotConfig(experimentId, currentPlot.plotUuid, currentPlot.plotType)); + // For embedding preview plots, use cell-count-aware defaults + const initialConfig = getEmbeddingInitialConfig(currentPlot.plotType, cellSets); + dispatch(updatePlotConfig(currentPlot.plotUuid, initialConfig)); + debounceSave(currentPlot.plotUuid); }; const renderExtraControlPanels = () => ( diff --git a/src/components/data-processing/DataIntegration/DataIntegration.jsx b/src/components/data-processing/DataIntegration/DataIntegration.jsx index 4ab1edaff..ba168c3a8 100644 --- a/src/components/data-processing/DataIntegration/DataIntegration.jsx +++ b/src/components/data-processing/DataIntegration/DataIntegration.jsx @@ -25,6 +25,7 @@ import FrequencyPlot from 'components/plots/FrequencyPlot'; import ElbowPlot from 'components/plots/ElbowPlot'; import { generateDataProcessingPlotUuid } from 'utils/generateCustomPlotUuid'; import EmptyPlot from 'components/plots/helpers/EmptyPlot'; +import { getEmbeddingInitialConfig } from 'utils/plotConfig/getEmbeddingInitialConfig'; import PlotStyling from 'components/plots/styling/PlotStyling'; import { getIsUnisample } from 'utils/experimentPredicates'; import PlotLegendAlert, { MAX_LEGEND_ITEMS } from 'components/plots/helpers/PlotLegendAlert'; @@ -246,6 +247,8 @@ const DataIntegration = (props) => { const isEqual = Object.keys(initialConfig).every((key) => { // By pass plot data because we want to compare settings not data if (key === 'plotData') return true; + // Skip defaultValues as it's metadata about defaults, not actual config + if (key === 'defaultValues') return true; if (initialConfig.keepValuesOnReset?.includes(key)) return true; if (currentConfig[key] && typeof currentConfig[key] === 'object' && initialConfig[key] && typeof initialConfig[key] === 'object') { // For nested objects, exclude defaultValues from comparison as it's metadata about defaults @@ -263,12 +266,15 @@ const DataIntegration = (props) => { useEffect(() => { if (!selectedConfig || !plots[selectedPlot]) return; - const initialConfig = initialPlotConfigStates[activePlotType]; + const initialConfig = getEmbeddingInitialConfig(activePlotType, cellSets); setIsResetDisabled(isConfigEqual(selectedConfig, initialConfig)); - }, [selectedConfig]); + }, [selectedConfig, cellSets]); const onClickReset = () => { - dispatch(resetPlotConfig(experimentId, activePlotUuid, activePlotType)); + // For embedding plots in data integration, use cell-count-aware defaults + const initialConfig = getEmbeddingInitialConfig(activePlotType, cellSets); + dispatch(updatePlotConfig(activePlotUuid, initialConfig)); + debounceSave(activePlotUuid); }; useEffect(() => { @@ -281,6 +287,35 @@ const DataIntegration = (props) => { if (showAlert) updatePlotWithChanges({ legend: { showAlert, enabled: !showAlert } }); }, [!selectedConfig, activePlotType, cellSets.accessible]); + // Apply cell-count-aware marker defaults for large datasets in embedding plots + // Only applies once when config is first loaded and hasn't been customized yet + useEffect(() => { + if (!selectedConfig || !cellSets.accessible || activePlotType !== 'dataIntegrationEmbedding') return; + + const initialConfig = getEmbeddingInitialConfig(activePlotType, cellSets); + + // Check if we should apply large dataset defaults + if (initialConfig.defaultValues?.largeDatasetDefaults) { + // Get the standard (non-adjusted) initial config to check if marker was already customized + const standardConfig = initialPlotConfigStates[activePlotType]; + + // Only apply if marker config currently matches standard defaults (not yet customized) + const isUsingStandardDefaults = selectedConfig.marker.outline === standardConfig.marker.outline + && selectedConfig.marker.size === standardConfig.marker.size; + + if (isUsingStandardDefaults) { + dispatch(updatePlotConfig(activePlotUuid, { + marker: { + ...selectedConfig.marker, + outline: false, + size: 1, + }, + })); + debounceSave(activePlotUuid); + } + } + }, [activePlotUuid, activePlotType, cellSets?.accessible]); + const completedSteps = useSelector(getBackendStatus(experimentId)) .status?.pipeline?.completedSteps; diff --git a/src/components/data-processing/PlotLayout.jsx b/src/components/data-processing/PlotLayout.jsx index 5f30bf5d3..787ee70fe 100644 --- a/src/components/data-processing/PlotLayout.jsx +++ b/src/components/data-processing/PlotLayout.jsx @@ -76,6 +76,8 @@ const PlotLayout = ({ const isEqual = Object.keys(initialConfig).every((key) => { // By pass plot data because we want to compare settings not data if (key === 'plotData') return true; + // Skip defaultValues as it's metadata about defaults, not actual config + if (key === 'defaultValues') return true; if (initialConfig.keepValuesOnReset?.includes(key)) return true; if (currentConfig[key] && typeof currentConfig[key] === 'object' && initialConfig[key] && typeof initialConfig[key] === 'object') { // For nested objects, exclude defaultValues from comparison as it's metadata about defaults diff --git a/src/utils/plotConfig/getEmbeddingInitialConfig.js b/src/utils/plotConfig/getEmbeddingInitialConfig.js new file mode 100644 index 000000000..17151fb03 --- /dev/null +++ b/src/utils/plotConfig/getEmbeddingInitialConfig.js @@ -0,0 +1,78 @@ +import { initialPlotConfigStates } from 'redux/reducers/componentConfig/initialState'; + +/** + * Gets the total cell count from cellSets + * @param {Object} cellSets - The cellSets object from redux + * @returns {number} Total number of cells + */ +const getTotalCellCount = (cellSets) => { + if (!cellSets?.properties || !cellSets?.hierarchy) { + return 0; + } + + const sampleNode = cellSets.hierarchy.find((node) => node.key === 'sample'); + const totalCells = sampleNode?.children?.reduce((sum, child) => { + const cellIds = cellSets.properties[child.key]?.cellIds; + return sum + (cellIds?.size || 0); + }, 0) || 0; + + return totalCells; +}; + +/** + * Determines if a plot is an embedding preview plot used in data-processing + * @param {string} plotType - The plot type + * @returns {boolean} + */ +const isEmbeddingPreviewPlot = (plotType) => { + const embeddingPreviewPlots = [ + 'embeddingPreviewBySample', + 'embeddingPreviewByCellSets', + 'embeddingPreviewMitochondrialContent', + 'embeddingPreviewDoubletScore', + 'embeddingPreviewNumOfGenes', + 'embeddingPreviewNumOfUmis', + 'dataIntegrationEmbedding', + ]; + return embeddingPreviewPlots.includes(plotType); +}; + +/** + * Gets the initial configuration for an embedding plot, adjusted based on cell count. + * For data-processing embedding plots with >100k cells: + * - Sets marker.outline to false + * - Sets marker.size to 1 + * + * @param {string} plotType - The plot type + * @param {Object} cellSets - The cellSets object from redux (optional) + * @returns {Object} The initial plot configuration + */ +const getEmbeddingInitialConfig = (plotType, cellSets = null) => { + const baseConfig = { ...initialPlotConfigStates[plotType] }; + + // Apply conditional defaults for embedding preview plots with large datasets + if (isEmbeddingPreviewPlot(plotType) && cellSets) { + const totalCells = getTotalCellCount(cellSets); + + if (totalCells > 100000) { + return { + ...baseConfig, + marker: { + ...baseConfig.marker, + outline: false, + size: 1, + }, + // Store the conditional defaults info for use in reset logic + defaultValues: { + ...baseConfig.defaultValues, + largeDatasetDefaults: true, + cellCount: totalCells, + }, + }; + } + } + + return baseConfig; +}; + +export { getEmbeddingInitialConfig, getTotalCellCount, isEmbeddingPreviewPlot }; From 1a042279c18450e36645241598a9e1aa9785691b Mon Sep 17 00:00:00 2001 From: Alex Pickering Date: Sat, 23 May 2026 06:20:36 -0400 Subject: [PATCH 2/3] adjust plots-and-tables markers for large data Signed-off-by: Alex Pickering --- src/components/plots/PlotContainer.jsx | 59 ++++++++++++++++++- .../plotConfig/getEmbeddingInitialConfig.js | 21 ++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/components/plots/PlotContainer.jsx b/src/components/plots/PlotContainer.jsx index 51bedb75d..4af95c4b5 100644 --- a/src/components/plots/PlotContainer.jsx +++ b/src/components/plots/PlotContainer.jsx @@ -13,9 +13,11 @@ import { resetPlotConfig, savePlotConfig, } from 'redux/actions/componentConfig'; +import { getCellSets } from 'redux/selectors'; import _ from 'lodash'; import PlotStyling from 'components/plots/styling/PlotStyling'; import MultiTileContainer from 'components/MultiTileContainer'; +import { getEmbeddingInitialConfig, isEmbeddingPlotType } from 'utils/plotConfig/getEmbeddingInitialConfig'; const PLOT = 'Plot'; const CONTROLS = 'Controls'; @@ -39,6 +41,7 @@ const PlotContainer = (props) => { const [tileDirection, setTileDirection] = useState(DEFAULT_ORIENTATION); const { config } = useSelector((state) => state.componentConfig[plotUuid] || {}); + const cellSets = useSelector(getCellSets()); const debounceSave = useCallback( _.debounce(() => dispatch(savePlotConfig(experimentId, plotUuid)), saveDebounceTime), [plotUuid], ); @@ -57,6 +60,8 @@ const PlotContainer = (props) => { const isEqual = Object.keys(initialConfig).every((key) => { // By pass plot data because we want to compare settings not data if (key === 'plotData') return true; + // Skip defaultValues as it's metadata about defaults, not actual config + if (key === 'defaultValues') return true; if (initialConfig.keepValuesOnReset?.includes(key)) return true; if (currentConfig[key] && typeof currentConfig[key] === 'object' && initialConfig[key] && typeof initialConfig[key] === 'object') { // For nested objects, exclude defaultValues from comparison as it's metadata about defaults @@ -87,15 +92,63 @@ const PlotContainer = (props) => { debounceSave(); + // For embedding plots with cellSets available, use large-dataset-aware comparison + let initialConfig; + if (isEmbeddingPlotType(plotType) && cellSets?.properties && cellSets?.hierarchy) { + const embeddingConfig = getEmbeddingInitialConfig(plotType, cellSets); + initialConfig = embeddingConfig || initialPlotConfigStates[plotType]; + } else { + initialConfig = initialPlotConfigStates[plotType]; + } + setIsResetDisabled( - isConfigEqual(config, initialPlotConfigStates[plotType]), + isConfigEqual(config, initialConfig), ); - }, [config]); + }, [config, cellSets, plotType]); + + // Auto-apply large-dataset defaults for embedding plots when config first loads with cellSets + useEffect(() => { + if (!isEmbeddingPlotType(plotType) || !config || !cellSets?.properties || !cellSets?.hierarchy) return; + if (config.defaultValues?.largeDatasetDefaults) return; // Already applied + + const cellCount = cellSets.hierarchy?.find((node) => node.key === 'sample') + ?.children?.reduce((sum, child) => { + const cellIds = cellSets.properties[child.key]?.cellIds; + return sum + (cellIds?.size || 0); + }, 0); + + if (cellCount > 100000) { + const largeDatasetConfig = getEmbeddingInitialConfig(plotType, cellSets); + if (largeDatasetConfig && largeDatasetConfig !== initialPlotConfigStates[plotType]) { + dispatch(updatePlotConfig(plotUuid, largeDatasetConfig)); + debounceSave(); + } + } + }, [config, cellSets, plotType, plotUuid, experimentId]); const onClickReset = () => { + // For embedding plots with large datasets, use optimized defaults + if (isEmbeddingPlotType(plotType)) { + const initialConfig = getEmbeddingInitialConfig(plotType, cellSets); + if (initialConfig && initialConfig !== initialPlotConfigStates[plotType]) { + // Preserve fields marked with keepValuesOnReset + const keysToPreserve = initialConfig.keepValuesOnReset || []; + const resetConfig = keysToPreserve.reduce((acc, key) => { + if (config?.[key] !== undefined) { + acc[key] = config[key]; + } + return acc; + }, initialConfig); + + dispatch(updatePlotConfig(plotUuid, resetConfig)); + debounceSave(); + onPlotReset(); + return; + } + } + dispatch(resetPlotConfig(experimentId, plotUuid, plotType)); onPlotReset(); - setIsResetDisabled(true); }; const renderPlotToolbarControls = () => ( diff --git a/src/utils/plotConfig/getEmbeddingInitialConfig.js b/src/utils/plotConfig/getEmbeddingInitialConfig.js index 17151fb03..8782d6cc5 100644 --- a/src/utils/plotConfig/getEmbeddingInitialConfig.js +++ b/src/utils/plotConfig/getEmbeddingInitialConfig.js @@ -20,12 +20,14 @@ const getTotalCellCount = (cellSets) => { }; /** - * Determines if a plot is an embedding preview plot used in data-processing + * Determines if a plot is an embedding plot that should get large-dataset defaults + * Includes both data-processing embedding preview plots and Plots-Tables embedding plots * @param {string} plotType - The plot type * @returns {boolean} */ -const isEmbeddingPreviewPlot = (plotType) => { - const embeddingPreviewPlots = [ +const isEmbeddingPlotType = (plotType) => { + const embeddingPlots = [ + // Data-processing embedding preview plots 'embeddingPreviewBySample', 'embeddingPreviewByCellSets', 'embeddingPreviewMitochondrialContent', @@ -33,13 +35,16 @@ const isEmbeddingPreviewPlot = (plotType) => { 'embeddingPreviewNumOfGenes', 'embeddingPreviewNumOfUmis', 'dataIntegrationEmbedding', + // Plots and Tables embedding plots + 'embeddingContinuous', + 'embeddingCategorical', ]; - return embeddingPreviewPlots.includes(plotType); + return embeddingPlots.includes(plotType); }; /** * Gets the initial configuration for an embedding plot, adjusted based on cell count. - * For data-processing embedding plots with >100k cells: + * For embedding plots with >100k cells: * - Sets marker.outline to false * - Sets marker.size to 1 * @@ -50,8 +55,8 @@ const isEmbeddingPreviewPlot = (plotType) => { const getEmbeddingInitialConfig = (plotType, cellSets = null) => { const baseConfig = { ...initialPlotConfigStates[plotType] }; - // Apply conditional defaults for embedding preview plots with large datasets - if (isEmbeddingPreviewPlot(plotType) && cellSets) { + // Apply conditional defaults for embedding plots with large datasets + if (isEmbeddingPlotType(plotType) && cellSets) { const totalCells = getTotalCellCount(cellSets); if (totalCells > 100000) { @@ -75,4 +80,4 @@ const getEmbeddingInitialConfig = (plotType, cellSets = null) => { return baseConfig; }; -export { getEmbeddingInitialConfig, getTotalCellCount, isEmbeddingPreviewPlot }; +export { getEmbeddingInitialConfig, getTotalCellCount, isEmbeddingPlotType }; From ab4a9673c3fb8de5930c75659436a96f686e1e28 Mon Sep 17 00:00:00 2001 From: Alex Pickering Date: Sat, 23 May 2026 06:21:08 -0400 Subject: [PATCH 3/3] use spectral for qc plots Signed-off-by: Alex Pickering --- .../reducers/componentConfig/initialState.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/redux/reducers/componentConfig/initialState.js b/src/redux/reducers/componentConfig/initialState.js index 44485fa9e..65f10e2d8 100644 --- a/src/redux/reducers/componentConfig/initialState.js +++ b/src/redux/reducers/componentConfig/initialState.js @@ -527,7 +527,10 @@ const embeddingPreviewMitochondrialContentInitialConfig = { fontSize: 20, }, fontStyle: fontStyleBaseState, - colour: colourBaseState, + colour: { + ...colourBaseState, + gradient: 'spectral', + }, marker: markerBaseState, labels: labelBaseState, shownGene: null, @@ -559,7 +562,10 @@ const embeddingPreviewDoubletScoreInitialConfig = { fontSize: 20, }, fontStyle: fontStyleBaseState, - colour: colourBaseState, + colour: { + ...colourBaseState, + gradient: 'spectral', + }, marker: markerBaseState, labels: labelBaseState, selectedSample: 'All', @@ -589,7 +595,10 @@ const embeddingPreviewNumOfGenesInitialConfig = { fontSize: 20, }, fontStyle: fontStyleBaseState, - colour: colourBaseState, + colour: { + ...colourBaseState, + gradient: 'spectral', + }, marker: markerBaseState, labels: labelBaseState, selectedSample: 'All', @@ -618,7 +627,10 @@ const embeddingPreviewNumOfUmisInitialConfig = { fontSize: 20, }, fontStyle: fontStyleBaseState, - colour: colourBaseState, + colour: { + ...colourBaseState, + gradient: 'spectral', + }, marker: markerBaseState, labels: labelBaseState, selectedSample: 'All',