UNPKG

vitessce

Version:

Vitessce app and React component library

212 lines (195 loc) 7.77 kB
/* eslint-disable no-plusplus */ import packageJson from '../../package.json'; import { getNextScope } from '../utils'; import { COORDINATION_TYPES, DEFAULT_COORDINATION_VALUES, COMPONENT_COORDINATION_TYPES, AUTO_INDEPENDENT_COORDINATION_TYPES, } from './state/coordination'; import { SCHEMA_HANDLERS } from './view-config-versions'; /** * Get a list of all unique scope names for a * particular coordination type, which exist in * a particular view config. * @param {object} config A view config object. * @param {string} coordinationType A coordination type, * for example 'spatialZoom' or 'dataset'. * @returns {string[]} Array of existing coordination scope names. */ export function getExistingScopesForCoordinationType(config, coordinationType) { const spaceScopes = Object.keys(config?.coordinationSpace?.[coordinationType] || {}); const componentScopes = config.layout.map(c => c.coordinationScopes?.[coordinationType]); return Array.from(new Set([...spaceScopes, ...componentScopes])); } /** * Give each component the same scope name for this coordination type. * @param {object} config A view config object. * @param {string} coordinationType A coordination type, * for example 'spatialZoom' or 'dataset'. * @param {*} scopeValue The initial value for the coordination scope, * to set in the coordination space. * @returns {object} The new view config. */ function coordinateComponentsTogether(config, coordinationType, scopeValue) { const scopeName = getNextScope(getExistingScopesForCoordinationType(config, coordinationType)); const newConfig = { ...config, coordinationSpace: { ...config.coordinationSpace, [coordinationType]: { ...config?.coordinationSpace?.[coordinationType], // Add the new scope name and value to the coordination space. [scopeName]: scopeValue, }, }, layout: config.layout.map(component => ({ ...component, coordinationScopes: { ...component.coordinationScopes, // Only set the coordination scope if this component uses this coordination type, // and the component is missing a coordination scope for this coordination type. ...(( COMPONENT_COORDINATION_TYPES[component.component].includes(coordinationType) && !component.coordinationScopes?.[coordinationType] ) ? { // Only set the new scope name if the scope name // for this component and coordination type is currently undefined. [coordinationType]: scopeName, } : {}), }, })), }; return newConfig; } /** * Give each component a different scope name for this coordination type. * @param {object} config A view config object. * @param {string} coordinationType A coordination type, * for example 'spatialZoom' or 'dataset'. * @param {*} scopeValue The initial value for the coordination scope, * to set in the coordination space. * @returns {object} The new view config. */ function coordinateComponentsIndependent(config, coordinationType, scopeValue) { const newConfig = { ...config, layout: [...config.layout], }; const newScopes = {}; newConfig.layout.forEach((component, i) => { // Only set the coordination scope if this component uses this coordination type, // and the component is missing a coordination scope for this coordination type. if (COMPONENT_COORDINATION_TYPES[component.component].includes(coordinationType) && !component.coordinationScopes?.[coordinationType] ) { const scopeName = getNextScope([ ...getExistingScopesForCoordinationType(config, coordinationType), ...Object.keys(newScopes), ]); newScopes[scopeName] = scopeValue; newConfig.layout[i] = { ...component, coordinationScopes: { ...component.coordinationScopes, [coordinationType]: scopeName, }, }; } }); newConfig.coordinationSpace = { ...newConfig.coordinationSpace, [coordinationType]: { ...newConfig.coordinationSpace[coordinationType], // Add the new scope name and value to the coordination space. ...newScopes, }, }; return newConfig; } function initializeAuto(config) { let newConfig = config; const { layout, datasets } = newConfig; // For each coordination type, check whether it requires initialization. Object.values(COORDINATION_TYPES).forEach((coordinationType) => { // A coordination type requires coordination if at least one component is missing // a (coordination type, coordination scope) tuple. // Components may only use a subset of all coordination types. const requiresCoordination = !layout .every(c => ( !COMPONENT_COORDINATION_TYPES[c.component].includes(coordinationType) || c.coordinationScopes?.[coordinationType] )); if (requiresCoordination) { // Note that the default value may be undefined. let defaultValue = DEFAULT_COORDINATION_VALUES[coordinationType]; // Check whether this is the special 'dataset' coordination type. if (coordinationType === 'dataset' && datasets.length >= 1) { // Use the first dataset ID as the default // if there is at least one dataset. defaultValue = datasets[0].uid; } // Use the list of "independent" coordination types // to determine whether a particular coordination type // should be initialized to // a unique scope for every component ("independent") // vs. the same scope for every component ("together"). if (AUTO_INDEPENDENT_COORDINATION_TYPES.includes(coordinationType)) { newConfig = coordinateComponentsIndependent(newConfig, coordinationType, defaultValue); } else { newConfig = coordinateComponentsTogether(newConfig, coordinationType, defaultValue); } } }); return newConfig; } /** * Initialize the view config: * - Fill in missing coordination objects with default values. * - Fill in missing component coordination scope mappings. * based on the `initStrategy` specified in the view config. * Should be "stable": if run on the same view config twice, the return value the second * time should be identical to the return value the first time. * @param {object} config The view config prop. */ export function initialize(config) { if (config.initStrategy === 'auto') { return initializeAuto(config); } return config; } export function upgradeAndValidate(oldConfig) { // oldConfig object must have a `version` property. let nextConfig = oldConfig; let fromVersion; let upgradeFunction; let validateFunction; do { fromVersion = nextConfig.version; if (!Object.keys(SCHEMA_HANDLERS).includes(fromVersion)) { return [{ title: 'Config validation failed', preformatted: 'Unknown config version.', }, false]; } [validateFunction, upgradeFunction] = SCHEMA_HANDLERS[fromVersion]; // Validate under the legacy schema before upgrading. const validLegacy = validateFunction(nextConfig); if (!validLegacy) { const failureReason = JSON.stringify(validateFunction.errors, null, 2); return [{ title: 'Config validation failed', preformatted: failureReason, }, false]; } if (upgradeFunction) { nextConfig = upgradeFunction(nextConfig); } } while (upgradeFunction); // NOTE: Remove when a view config viewer/editor is available in UI. console.groupCollapsed(`🚄 Vitessce (${packageJson.version}) view configuration`); console.info(`data:,${JSON.stringify(nextConfig)}`); console.info(JSON.stringify(nextConfig, null, 2)); console.groupEnd(); return [nextConfig, true]; }