Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into 1103-react-query-retr…
Browse files Browse the repository at this point in the history
…ofit
  • Loading branch information
bobular committed Nov 19, 2024
2 parents deff652 + 97d1f21 commit 0415b50
Show file tree
Hide file tree
Showing 71 changed files with 2,022 additions and 793 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
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const MesaTooltip = ({
enterDelay={showDelay}
className={(className ?? '') + (corner ? ` ${corner}` : '')}
style={finalStyle}
tabIndex={0}
>
{children}
</Tooltip>
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 0415b50

Please sign in to comment.