UNPKG

vitessce

Version:

Vitessce app and React component library

290 lines (266 loc) 10.7 kB
/* eslint-disable no-underscore-dangle */ import React, { useState, useMemo } from 'react'; import isEqual from 'lodash/isEqual'; import Tree from './Tree'; import TreeNode from './TreeNode'; import { PlusButton, SetOperationButtons } from './SetsManagerButtons'; import { nodeToRenderProps } from './cell-set-utils'; import { getDefaultColor } from '../utils'; import { pathToKey } from './utils'; function processNode(node, prevPath, setColor, theme) { const nodePath = [...prevPath, node.name]; return { ...node, ...(node.children ? ({ children: node.children .map(c => processNode(c, nodePath, setColor)), }) : {}), color: setColor?.find(d => isEqual(d.path, nodePath))?.color || getDefaultColor(theme), }; } function processSets(sets, setColor, theme) { return { ...sets, tree: sets ? sets.tree.map(lzn => processNode(lzn, [], setColor, theme)) : [], }; } function getAllKeys(node, path = []) { if (!node) { return null; } const newPath = [...path, node.name]; if (node.children) { return [pathToKey(newPath), ...node.children.flatMap(v => getAllKeys(v, newPath))]; } return pathToKey(newPath); } /** * A generic hierarchical set manager component. * @prop {object} tree An object representing set hierarchies. * @prop {string} datatype The data type for sets (e.g. "cell") * @prop {function} clearPleaseWait A callback to signal that loading is complete. * @prop {boolean} draggable Whether tree nodes can be rearranged via drag-and-drop. * By default, true. * @prop {boolean} checkable Whether to show the "Check" menu button * and checkboxes for selecting multiple sets. By default, true. * @prop {boolean} editable Whether to show rename, delete, color, or create options. * By default, true. * @prop {boolean} expandable Whether to allow hierarchies to be expanded * to show the list or tree of sets contained. By default, true. * @prop {boolean} operatable Whether to enable union, intersection, * and complement operations on checked sets. By default, true. * @prop {boolean} exportable Whether to enable exporting hierarchies and sets to files. * By default, true. * @prop {boolean} importable Whether to enable importing hierarchies from files. * By default, true. * @prop {function} onError Function to call with error messages (failed import validation, etc). * @prop {function} onCheckNode Function to call when a single node has been checked or un-checked. * @prop {function} onExpandNode Function to call when a node has been expanded. * @prop {function} onDropNode Function to call when a node has been dragged-and-dropped. * @prop {function} onCheckLevel Function to call when an entire hierarchy level has been selected, * via the "Color by cluster" and "Color by subcluster" buttons below collapsed level zero nodes. * @prop {function} onNodeSetColor Function to call when a new node color has been selected. * @prop {function} onNodeSetName Function to call when a node has been renamed. * @prop {function} onNodeRemove Function to call when the user clicks the "Delete" menu button * to remove a node. * @prop {function} onNodeView Function to call when the user wants to view the set associated * with a particular node. * @prop {function} onImportTree Function to call when a tree has been imported * using the "plus" button. * @prop {function} onCreateLevelZeroNode Function to call when a user clicks the "Create hierarchy" * menu option using the "plus" button. * @prop {function} onExportLevelZeroNode Function to call when a user wants to * export an entire hierarchy via the "Export hierarchy" menu button for a * particular level zero node. * @prop {function} onExportSet Function to call when a user wants to export a set associated with * a particular node via the "Export set" menu button. * @prop {function} onUnion Function to call when a user wants to create a new set from the union * of the sets associated with the currently-checked nodes. * @prop {function} onIntersection Function to call when a user wants to create a new set from the * intersection of the sets associated with the currently-checked nodes. * @prop {function} onComplement Function to call when a user wants to create a new set from the * complement of the (union of the) sets associated with the currently-checked nodes. * @prop {function} onView Function to call when a user wants to view the sets * associated with the currently-checked nodes. * @prop {string} theme "light" or "dark" for the vitessce theme */ export default function SetsManager(props) { const { theme, sets, additionalSets, setColor, // TODO: use this levelSelection: checkedLevel, setSelection, setExpansion, hasColorEncoding, datatype, draggable = true, checkable = true, editable = true, expandable = true, operatable = true, exportable = true, importable = true, onError, onCheckNode, onExpandNode, onDropNode, onCheckLevel, onNodeSetColor, onNodeSetName, onNodeCheckNewName, onNodeRemove, onNodeView, onImportTree, onCreateLevelZeroNode, onExportLevelZeroNodeJSON, onExportLevelZeroNodeTabular, onExportSetJSON, onUnion, onIntersection, onComplement, hasCheckedSetsToUnion, hasCheckedSetsToIntersect, hasCheckedSetsToComplement, } = props; const isChecking = true; const autoExpandParent = true; const [isDragging, setIsDragging] = useState(false); const [isEditingNodeName, setIsEditingNodeName] = useState(null); const processedSets = useMemo(() => processSets( sets, setColor, theme, ), [sets, setColor, theme]); const processedAdditionalSets = useMemo(() => processSets( additionalSets, setColor, theme, ), [additionalSets, setColor, theme]); const additionalSetKeys = (processedAdditionalSets ? processedAdditionalSets.tree.flatMap(v => getAllKeys(v, [])) : [] ); const allSetSelectionKeys = (setSelection || []).map(pathToKey); const allSetExpansionKeys = (setExpansion || []).map(pathToKey); const setSelectionKeys = allSetSelectionKeys.filter(k => !additionalSetKeys.includes(k)); const setExpansionKeys = allSetExpansionKeys.filter(k => !additionalSetKeys.includes(k)); const additionalSetSelectionKeys = allSetSelectionKeys.filter(k => additionalSetKeys.includes(k)); const additionalSetExpansionKeys = allSetExpansionKeys.filter(k => additionalSetKeys.includes(k)); /** * Recursively render TreeNode components. * @param {object[]} nodes An array of node objects. * @returns {TreeNode[]|null} Array of TreeNode components or null. */ function renderTreeNodes(nodes, readOnly, currPath) { if (!nodes) { return null; } return nodes.map((node) => { const newPath = [...currPath, node.name]; return ( <TreeNode theme={theme} key={pathToKey(newPath)} {...nodeToRenderProps(node, newPath, setColor)} isEditing={isEqual(isEditingNodeName, newPath)} datatype={datatype} draggable={draggable && !readOnly} editable={editable && !readOnly} checkable={checkable} expandable={expandable} exportable={exportable} hasColorEncoding={hasColorEncoding} isChecking={isChecking} checkedLevelPath={checkedLevel ? checkedLevel.levelZeroPath : null} checkedLevelIndex={checkedLevel ? checkedLevel.levelIndex : null} onCheckNode={onCheckNode} onCheckLevel={onCheckLevel} onNodeView={onNodeView} onNodeSetColor={onNodeSetColor} onNodeSetName={(targetPath, name) => { onNodeSetName(targetPath, name); setIsEditingNodeName(null); }} onNodeCheckNewName={onNodeCheckNewName} onNodeSetIsEditing={setIsEditingNodeName} onNodeRemove={onNodeRemove} onExportLevelZeroNodeJSON={onExportLevelZeroNodeJSON} onExportLevelZeroNodeTabular={onExportLevelZeroNodeTabular} onExportSetJSON={onExportSetJSON} disableTooltip={isDragging} onDragStart={() => setIsDragging(true)} onDragEnd={() => setIsDragging(false)} > {renderTreeNodes(node.children, readOnly, newPath, theme)} </TreeNode> ); }); } return ( <div className="sets-manager"> <div className="sets-manager-tree"> <Tree draggable={false} checkable={checkable} checkedKeys={setSelectionKeys} expandedKeys={setExpansionKeys} autoExpandParent={autoExpandParent} onCheck={(checkedKeys, info) => onCheckNode( info.node.props.nodeKey, info.checked, )} onExpand={(expandedKeys, info) => onExpandNode( expandedKeys, info.node.props.nodeKey, info.expanded, )} > {renderTreeNodes(processedSets.tree, true, [], theme)} </Tree> <Tree draggable /* TODO */ checkable={checkable} checkedKeys={additionalSetSelectionKeys} expandedKeys={additionalSetExpansionKeys} autoExpandParent={autoExpandParent} onCheck={(checkedKeys, info) => onCheckNode( info.node.props.nodeKey, info.checked, )} onExpand={(expandedKeys, info) => onExpandNode( expandedKeys, info.node.props.nodeKey, info.expanded, )} onDrop={(info) => { const { eventKey: dropKey } = info.node.props; const { eventKey: dragKey } = info.dragNode.props; const { dropToGap, dropPosition } = info; onDropNode(dropKey, dragKey, dropPosition, dropToGap); }} > {renderTreeNodes(processedAdditionalSets.tree, false, [], theme)} </Tree> <PlusButton datatype={datatype} onError={onError} onImportTree={onImportTree} onCreateLevelZeroNode={onCreateLevelZeroNode} importable={importable} editable={editable} /> </div> {isChecking ? ( <div className="set-operation-buttons"> <SetOperationButtons onUnion={onUnion} onIntersection={onIntersection} onComplement={onComplement} operatable={operatable} hasCheckedSetsToUnion={hasCheckedSetsToUnion} hasCheckedSetsToIntersect={hasCheckedSetsToIntersect} hasCheckedSetsToComplement={hasCheckedSetsToComplement} /> </div> ) : null} </div> ); }