Skip to content

Commit

Permalink
OrthoMCL - Custom tree-table for OrthoGroup page (#904)
Browse files Browse the repository at this point in the history
* made a start - WIP

* re-render sequence table with Mesa

* tweak comments

* WIP

* loosely wire in TreeTable and get shimmimg working

* add patristic and basic type support

* made a start with controlling table row height but should investigate the Mesa inline option

* added Mesa table tooltips for td contents when in 'inline' mode (basically fixed-height mode)

* use Mesa's inline option and tidy up

* Revert "added Mesa table tooltips for td contents when in 'inline' mode (basically fixed-height mode)"

This reverts commit f215762.

* remove placeholder td title

* restore whitespace in DataCell.tsx

* upgrade tidytree and use new interactive option

* table and tree order are the same

* added basic checkboxes

* WIP clustal form

* that's hopefully clustal sorted - but can't test with local ortho-site

* improve 'must select two proteins' verbiage

* de-complicate .DataTable margin-bottom override

* rename TreeResponse to GroupTreeResponse

* add PFam domain architecture column

* ugly search working as a demo on description column

* add warning banner for data issue and tweak pfam column heading

* used RealTimeSearchBox and made regexps safer and reorganised so we can useMemo for the table filtering

* more memo and remove some commented code

* add tree filtering

* tree highlighting works with filtering now, dependency issues fixed

* add core/peripheral filtering

* added pfam legend - needs wiring still

* wired up pfam checkboxes to table search

* added the row count

* conditionally render pfam legend and add heading

* Remove vert scrollbar from tidytree table

* remove code that collapsed the main section of the orthogroup page

* no longer request trees for 1 or 2 sequences; fix RowCounter props change; fix some imports

* fudge to sort table based on dodgy colon-containing full_ids

* make tree darker and make tree/table check more robust

* provide PFam descriptions in domain cartoon tooltips

* sort out row counts

* add better console logging for tree/table mismatches

* made tree-table comparison for console more robust

* add inlineUseTooltips options prop but not working fully yet

* attempted better handling of very large groups

* looks good now

* removed full_id column (from display only) and added Accession aka sequence_link column and enabled the link

* Use flex for label to allow for block children

* Use SelectList for Pfam filter

* Explicit empty PFam legend

* Use similar style for core/peripheral filter

* Add species filter to protein table

* prevent overflow in SelectTree button with many items selected; also use flex-wrap to prevent large buttons crowding the search box

* add shouldOnlyUpdateOnClose option to SelectTree

* remove some unused imports

* add MAX_SEQUENCES_FOR_TREE logic

* selected row highlighting

* tone down the row background color

* don't indent search bar and filters

* add instantUpdate option to SelectList

* revert taxon filter to instant-update

* instantUpdate for core/peripheral filter

* Pfam architectures are now scaled by protein length

* removed core-only filter for larger groups

* improved table sort and filter performance - added comments about further optimisations

* rename shouldOnlyUpdateOnClose to instantUpdate

* remove an effect

* missed a setSelected and dependency

* final tweaks to button label behaviour

* add reset button, revisit filter button layout

* placeholder wrapper for image next to group stats

* restore the buttonless phyletic distribution

* improved comment/documentation

* Adding update evalue histogram

* static histogram image now shows, with elaborate width-setting for caption below

* make caption rendering smoother - no horizontal shifting of the figure

* moved png file again

* simplified to hardcoded dimensions

* Address typescript errors

* remove taxon_abbrev column

* rename Taxon column to Clade

* rename Species filter button to Organism

* improve filter button layout, especially when buttons have expanded content

* fix Omit prop typo that wasn't detected because only consumer so far was a JSX file

* main proteins table now has fully-fledged search box with column selector

* moved styling to .scss file

* revert column changes now that model is updated OrthoMCLModel#5

* added new WDK record scope - record-collapsed

* added Dave's suggestions

* merge in npm action change

* cherry picking missed this line

* Orthogroup tree table  perf (#1233)

* Only call anchorNode.scrollIntoView() if the position has changed

* Update note to handle MIN_SEQUENCES_FOR_TREE

* useDeferredValue partially working

* onSpeciesSelected wasn't stable, is now

* replace debouncing with deferred value for CheckboxTree searchTerm

* tidy up

* new useDeferredState hook

* Use fixed dimensions for organism filter of protein table

* the very basics

* straw man ready

* forgot the reset button

* DRYed up a bit

* fixed the edge case as described in PR, improved search box responsiveness

* text search field selectors now more responsive

* added asterisk and improved filter positioning

* fixed broken logic with corePeripheral filter

* memoized rowsByAccession and mesaState and fixed some logic errors

* memoize treeProps just for tidiness

* replaced the key hack with ref approach

* Orthogroup tree table - tooltips (#1253)

* new warning added, no new component, sorry

* improved tree error warning with link to contact form

* use display names in SelectList button labels

* standardised search behaviour with help text

* fixed broken protein popover with useImperativeHandle deps array

* protein filter and paged table interaction fixed

* fixed label display issue in EDA

* Convert to function component with simple debounce logic using setTimeout

* Make callbacks stable across renders

* target _blank

* fix import path

* show table for 1 or 2 protein groups, not the error. Changes to CheckboxList/SelectList generics 1 of 2

* changes to CheckboxList/SelectList generics 2 of 2

* reset button also cancels text search

* error banner was showing instead of loading spinner for large groups, now it isn't

* use const for min seqs in tree

* Remove style override that is no longer necessary

---------

Co-authored-by: Dave Falke <[email protected]>
Co-authored-by: Richard Demko <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2024
1 parent 298457c commit f8cfb2a
Show file tree
Hide file tree
Showing 50 changed files with 1,767 additions and 586 deletions.
2 changes: 1 addition & 1 deletion packages/libs/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"react-spring": "^9.7.1",
"react-transition-group": "^4.4.1",
"shape2geohash": "^1.2.5",
"tidytree": "github:d-callan/TidyTree"
"tidytree": "https://github.com/d-callan/TidyTree.git#commit=9063e2df3d93c72743702a6d8f43169a1461e5b0"
},
"files": [
"lib",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef } from 'react';
import { CSSProperties, useEffect, useLayoutEffect, useRef } from 'react';
import { TidyTree as TidyTreeJS } from 'tidytree';

export interface HorizontalDendrogramProps {
Expand All @@ -19,6 +19,7 @@ export interface HorizontalDendrogramProps {
for now just default to all zero margins (left-most edges
*/
margin?: [number, number, number, number];
interactive?: boolean;
};

/// The remaining props are handled with a redraw: ///
Expand All @@ -31,10 +32,21 @@ export interface HorizontalDendrogramProps {
* width of tree in pixels
*/
width: number;
/**
* hopefully temporary prop that we can get rid of when we understand the
* horizontal layout behaviour of the tree (with respect to number of nodes)
* which will come with testing with more examples. Defaults to 1.0
* update: possibly wasn't needed in the end!
*/
hStretch?: number;
/**
* number of pixels height taken per leaf
*/
rowHeight: number;
/**
* CSS styles for the container div
*/
containerStyles?: CSSProperties;
/**
* which leaf nodes to highlight
*/
Expand All @@ -43,6 +55,10 @@ export interface HorizontalDendrogramProps {
* highlight whole subtrees ('monophyletic') or just leaves ('none')
*/
highlightMode?: 'monophyletic' | 'none';
/**
* highlight color (optional - default is tidytree's yellow/orange)
*/
highlightColor?: string;
}

/**
Expand All @@ -57,9 +73,12 @@ export function HorizontalDendrogram({
leafCount,
rowHeight,
width,
options: { ruler = false, margin = [0, 0, 0, 0] },
options: { ruler = false, margin = [0, 0, 0, 0], interactive = true },
highlightedNodeIds,
highlightMode,
highlightColor,
hStretch = 1.0,
containerStyles,
}: HorizontalDendrogramProps) {
const containerRef = useRef<HTMLDivElement>(null);
const tidyTreeRef = useRef<TidyTreeJS>();
Expand All @@ -80,13 +99,15 @@ export function HorizontalDendrogram({
equidistantLeaves: true,
ruler,
margin,
hStretch,
animation: 0, // it's naff and it reveals edge lengths/weights momentarily
interactive,
});
tidyTreeRef.current = instance;
return function cleanup() {
instance.destroy();
};
}, [data, ruler, margin]);
}, [data, ruler, margin, hStretch, interactive, containerRef]);

// redraw when the container size changes
// useLayoutEffect ensures that the redraw is not called for brand new TidyTreeJS objects
Expand All @@ -106,19 +127,23 @@ export function HorizontalDendrogram({
tidyTreeRef.current.setColorOptions({
nodeColorMode: 'predicate',
branchColorMode: highlightMode ?? 'none',
highlightColor: highlightColor,
leavesOnly: true,
predicate: (node) => highlightedNodeIds.includes(node.__data__.data.id),
defaultBranchColor: '#333',
});
// no redraw needed, setColorOptions does it
}
}, [highlightedNodeIds, highlightMode, tidyTreeRef]);
}, [highlightedNodeIds, highlightMode, tidyTreeRef, data]);
// `data` not used in effect but needed to trigger recoloring

const containerHeight = leafCount * rowHeight;
return (
<div
style={{
width: width + 'px',
height: containerHeight + 'px',
...containerStyles,
}}
ref={containerRef}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.TreeTable {
--tree-table-row-height: 1em;
tr {
height: var(--tree-table-row-height);
}
}
99 changes: 58 additions & 41 deletions packages/libs/components/src/components/tidytree/TreeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import {
} from '../../components/tidytree/HorizontalDendrogram';
import Mesa from '@veupathdb/coreui/lib/components/Mesa';
import { MesaStateProps } from '../../../../coreui/lib/components/Mesa/types';
import { css as classNameStyle, cx } from '@emotion/css';
import { css as globalStyle, Global } from '@emotion/react';

import './TreeTable.scss';

export interface TreeTableProps<RowType> {
/**
* number of pixels vertical space for each row of the table and tree
* (for the table this is a minimum height, so make sure table content doesn't wrap)
* required; no default; minimum seems to be 42; suggested value: 45
*/
rowHeight: number;
/**
* number of pixels max width for table columns; defaults to 200
*/
maxColumnWidth?: number;
/**
* data and options for the tree
*/
Expand All @@ -25,8 +30,18 @@ export interface TreeTableProps<RowType> {
* data and options for the table
*/
tableProps: MesaStateProps<RowType>;
/**
* hide the tree (but keep its horizontal space); default = false
*/
hideTree?: boolean;
/**
* Passed as children to the `Mesa` component
*/
children?: React.ReactNode;
}

const margin: [number, number, number, number] = [0, 10, 0, 10];

/**
* main props are
* data: string; // Newick format tree
Expand All @@ -42,50 +57,52 @@ export interface TreeTableProps<RowType> {
* - allow additional Mesa props and options to be passed
*/
export default function TreeTable<RowType>(props: TreeTableProps<RowType>) {
const { rowHeight } = props;
const { rows } = props.tableProps;

const rowStyleClassName = useMemo(
() =>
cx(
classNameStyle({
height: rowHeight + 'px',
background: 'yellow',
})
),
[rowHeight]
);
const { rowHeight, maxColumnWidth = 200, hideTree = false, children } = props;
const { rows, filteredRows } = props.tableProps;

// tableState is just the tableProps with an extra CSS class
// to make sure the height is consistent with the tree
const tableState: MesaStateProps<RowType> = {
...props.tableProps,
options: {
...props.tableProps.options,
deriveRowClassName: (_) => rowStyleClassName,
},
};

return (
<div
style={{ display: 'flex', alignItems: 'flex-end', flexDirection: 'row' }}
>
const tableState: MesaStateProps<RowType> = useMemo(() => {
const tree = hideTree ? null : (
<HorizontalDendrogram
{...props.treeProps}
rowHeight={rowHeight}
leafCount={rows.length}
options={{ margin: [0, 10, 0, 10] }}
leafCount={filteredRows?.length ?? rows.length}
options={{ margin, interactive: false }}
/>
<>
<Global
styles={globalStyle`
.DataTable {
margin-bottom: 0px !important;
}
`}
/>
<Mesa state={tableState} />
</>
</div>
);
);

return {
...props.tableProps,
options: {
...props.tableProps.options,
className: 'TreeTable',
style: {
'--tree-table-row-height': rowHeight + 'px',
} as React.CSSProperties,
inline: true,
// TO DO: explore event delegation to avoid each tooltip having handlers
// replace inline mode's inline styling with emotion classes
inlineUseTooltips: true,
inlineMaxHeight: `${rowHeight}px`,
inlineMaxWidth: `${maxColumnWidth}px`,
marginContent: tree,
},
};
}, [
filteredRows?.length,
hideTree,
maxColumnWidth,
props.tableProps,
props.treeProps,
rowHeight,
rows.length,
]);

// if `hideTree` is used more dynamically than at present
// (for example if the user sorts the table)
// then the table container styling will need
// { marginLeft: hideTree ? props.treeProps.width : 0 }
// to stop the table jumping around horizontally
return <Mesa state={tableState} children={children} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface ColorOptions {
branchColorMode: 'monophyletic' | 'none';
highlightColor?: string;
defaultNodeColor?: string;
defaultBranchColor?: string;
}

declare module 'tidytree' {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export type RadioButtonGroupProps = {
* if a Map is used, then the values are used in Tooltips to explain why each option is disabled
*/
disabledList?: string[] | Map<string, ReactNode>;
/** capitalize of the labels; default: true */
capitalizeLabels?: boolean;
};

/**
Expand All @@ -62,6 +64,7 @@ export default function RadioButtonGroup({
margins,
itemMarginRight,
disabledList,
capitalizeLabels = true,
}: RadioButtonGroupProps) {
const isDisabled = (option: string) => {
if (!disabledList) return false;
Expand Down Expand Up @@ -141,7 +144,7 @@ export default function RadioButtonGroup({
marginRight: itemMarginRight,
fontSize: '0.75em',
fontWeight: 400,
textTransform: 'capitalize',
textTransform: capitalizeLabels ? 'capitalize' : undefined,
minWidth: minWidth,
}}
/>
Expand Down
25 changes: 21 additions & 4 deletions packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import Templates from '../Templates';
import { makeClassifier } from '../Utils/Utils';

import { Tooltip } from '../../../components/info/Tooltip';

const dataCellClass = makeClassifier('DataCell');

class DataCell extends React.PureComponent {
Expand Down Expand Up @@ -47,8 +49,14 @@ class DataCell extends React.PureComponent {
}
}

setTitle(el) {
if (el == null) return;
el.title = el.scrollWidth <= el.clientWidth ? '' : el.innerText;
}

render() {
let { column, inline, options, isChildRow, childRowColSpan } = this.props;
let { column, inline, options, isChildRow, childRowColSpan, rowIndex } =
this.props;
let { style, width, className, key } = column;

let whiteSpace = !inline
Expand All @@ -64,15 +72,24 @@ class DataCell extends React.PureComponent {
width = width ? { width, maxWidth: width, minWidth: width } : {};
style = Object.assign({}, style, width, whiteSpace);
className = dataCellClass() + (className ? ' ' + className : '');
const children = this.renderContent();

const content = this.renderContent();

const props = {
style,
children,
children: content,
className,
...(isChildRow ? { colSpan: childRowColSpan } : null),
};

return column.hidden ? null : <td key={key} {...props} />;
return column.hidden ? null : (
<td
onMouseEnter={(e) => this.setTitle(e.target)}
onMouseLeave={() => this.setTitle()}
key={key + '_' + rowIndex}
{...props}
/>
);
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ class DataRow extends React.PureComponent {

expandRow() {
const { options } = this.props;
if (!options.inline) return;
if (!options.inline || options.inlineUseTooltips) return;
this.setState({ expanded: true });
}

collapseRow() {
const { options } = this.props;
if (!options.inline) return;
if (!options.inline || options.inlineUseTooltips) return;
this.setState({ expanded: false });
}

handleRowClick() {
const { row, rowIndex, options } = this.props;
const { inline, onRowClick } = options;
const { inline, onRowClick, inlineUseTooltips } = options;
if (!inline && !onRowClick) return;

if (inline) this.setState({ expanded: !this.state.expanded });
if (inline && !inlineUseTooltips)
this.setState({ expanded: !this.state.expanded });
if (typeof onRowClick === 'function') onRowClick(row, rowIndex);
}

Expand Down
Loading

0 comments on commit f8cfb2a

Please sign in to comment.