-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #813 from VEuPathDB/tidytree-component
TreeTable React component
- Loading branch information
Showing
14 changed files
with
1,204 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,19 @@ | ||
import '@storybook/addon-console'; | ||
import React from 'react'; | ||
import { QueryClient, QueryClientProvider } from 'react-query'; | ||
|
||
// storybook v6 seems to have default margin so remove it - https://github.com/storybookjs/storybook/issues/12109 | ||
export const parameters = { | ||
controls: { expanded: true }, | ||
layout: 'fullscreen', | ||
}; | ||
|
||
// wrap all stories in react-query (for useQuery hook) | ||
const queryClient = new QueryClient(); | ||
export const decorators = [ | ||
(Story) => ( | ||
<QueryClientProvider client={queryClient}> | ||
<Story /> | ||
</QueryClientProvider> | ||
), | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
126 changes: 126 additions & 0 deletions
126
packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import { useEffect, useLayoutEffect, useRef } from 'react'; | ||
import { TidyTree as TidyTreeJS } from 'tidytree'; | ||
|
||
export interface HorizontalDendrogramProps { | ||
/// The first set of props are expected to be fairly constant /// | ||
/// and when changed, a whole new TidyTreeJS instance will be created /// | ||
/** | ||
* tree data in Newick format | ||
*/ | ||
data: string | undefined; | ||
/** | ||
* TO DO: add width prop and nail down most of the options to | ||
* horizontal dendrograms | ||
*/ | ||
options: { | ||
ruler?: boolean; | ||
/** | ||
* supposedly [ top, right, bottom, left ] but in practice not at all | ||
for now just default to all zero margins (left-most edges | ||
*/ | ||
margin?: [number, number, number, number]; | ||
}; | ||
|
||
/// The remaining props are handled with a redraw: /// | ||
/** | ||
* how many leaf nodes are in the data string | ||
* (maybe we can calculate this from the Newick string in future?) | ||
*/ | ||
leafCount: number; | ||
/** | ||
* width of tree in pixels | ||
*/ | ||
width: number; | ||
/** | ||
* number of pixels height taken per leaf | ||
*/ | ||
rowHeight: number; | ||
/** | ||
* which leaf nodes to highlight | ||
*/ | ||
highlightedNodeIds?: string[]; | ||
/** | ||
* highlight whole subtrees ('monophyletic') or just leaves ('none') | ||
*/ | ||
highlightMode?: 'monophyletic' | 'none'; | ||
} | ||
|
||
/** | ||
* This is hardwired to produce a horizontal, equally spaced, square cornered dendrogram. | ||
* A more general purpose wrapping of TidyTreeJS will have to come later. It's not trivial | ||
* due to horizontal/vertical orientation affecting heights and widths. Given that users expect | ||
* to scroll up/down in a large tree rather than left/right, and because we will be aligning | ||
* it with a table that also works in up/down space, this seems reasonable. | ||
*/ | ||
export function HorizontalDendrogram({ | ||
data, | ||
leafCount, | ||
rowHeight, | ||
width, | ||
options: { ruler = false, margin = [0, 0, 0, 0] }, | ||
highlightedNodeIds, | ||
highlightMode, | ||
}: HorizontalDendrogramProps) { | ||
const containerRef = useRef<HTMLDivElement>(null); | ||
const tidyTreeRef = useRef<TidyTreeJS>(); | ||
|
||
useEffect(() => { | ||
if (containerRef.current == null || data == null) { | ||
// If props.data is nullish and containerRef.current exists, clear its content | ||
if (containerRef.current) { | ||
containerRef.current.innerHTML = ''; // Clear the container for blank rendering | ||
} | ||
return; | ||
} | ||
const instance = new TidyTreeJS(data, { | ||
parent: containerRef.current, | ||
layout: 'horizontal', | ||
type: 'dendrogram', | ||
mode: 'square', | ||
equidistantLeaves: true, | ||
ruler, | ||
margin, | ||
animation: 0, // it's naff and it reveals edge lengths/weights momentarily | ||
}); | ||
tidyTreeRef.current = instance; | ||
return function cleanup() { | ||
instance.destroy(); | ||
}; | ||
}, [data, ruler, margin]); | ||
|
||
// redraw when the container size changes | ||
// useLayoutEffect ensures that the redraw is not called for brand new TidyTreeJS objects | ||
// look out for potential performance issues (the effect is run synchronously) | ||
useLayoutEffect(() => { | ||
if (tidyTreeRef.current) { | ||
tidyTreeRef.current.redraw(); | ||
} | ||
}, [leafCount, width, rowHeight, tidyTreeRef]); | ||
|
||
// now handle changes to props that act via tidytree methods | ||
// which also usually trigger a redraw | ||
|
||
// highlightedNodeIds | ||
useEffect(() => { | ||
if (tidyTreeRef.current && highlightedNodeIds) { | ||
tidyTreeRef.current.setColorOptions({ | ||
nodeColorMode: 'predicate', | ||
branchColorMode: highlightMode ?? 'none', | ||
leavesOnly: true, | ||
predicate: (node) => highlightedNodeIds.includes(node.__data__.data.id), | ||
}); | ||
// no redraw needed, setColorOptions does it | ||
} | ||
}, [highlightedNodeIds, highlightMode, tidyTreeRef]); | ||
|
||
const containerHeight = leafCount * rowHeight; | ||
return ( | ||
<div | ||
style={{ | ||
width: width + 'px', | ||
height: containerHeight + 'px', | ||
}} | ||
ref={containerRef} | ||
/> | ||
); | ||
} |
91 changes: 91 additions & 0 deletions
91
packages/libs/components/src/components/tidytree/TreeTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import React, { useMemo } from 'react'; // import React seems to be needed | ||
import { | ||
HorizontalDendrogram, | ||
HorizontalDendrogramProps, | ||
} 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'; | ||
|
||
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) | ||
*/ | ||
rowHeight: number; | ||
/** | ||
* data and options for the tree | ||
*/ | ||
treeProps: Omit< | ||
HorizontalDendrogramProps, | ||
'leafCount' | 'options' | 'rowHeight' | ||
>; | ||
/** | ||
* data and options for the table | ||
*/ | ||
tableProps: MesaStateProps<RowType>; | ||
} | ||
|
||
/** | ||
* main props are | ||
* data: string; // Newick format tree | ||
* rows: RowType[]; // array of row objects | ||
* columns: MesaColumn[]; // column configurations (see Storybook story) | ||
* width: number; // width of the tree | ||
* rowHeight: number; // height of rows in table and leaves in tree | ||
* | ||
* The tree should have the same number of leaf nodes as rows.length! | ||
* This is not currently validated by the component. | ||
* | ||
* Probably TO DO: | ||
* - 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] | ||
); | ||
|
||
// 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' }} | ||
> | ||
<HorizontalDendrogram | ||
{...props.treeProps} | ||
rowHeight={rowHeight} | ||
leafCount={rows.length} | ||
options={{ margin: [0, 10, 0, 10] }} | ||
/> | ||
<> | ||
<Global | ||
styles={globalStyle` | ||
.DataTable { | ||
margin-bottom: 0px !important; | ||
} | ||
`} | ||
/> | ||
<Mesa state={tableState} /> | ||
</> | ||
</div> | ||
); | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/libs/components/src/components/tidytree/tidytree.d.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// this is just the subset of props we are dealing with | ||
interface Node { | ||
__data__: { | ||
data: { | ||
id: string; // the display label provided in the Newick file | ||
length: number; | ||
}; | ||
}; | ||
} | ||
|
||
interface ColorOptions { | ||
predicate: (node: Node) => boolean; | ||
leavesOnly: boolean; | ||
nodeColorMode: 'predicate' | 'none'; | ||
branchColorMode: 'monophyletic' | 'none'; | ||
highlightColor?: string; | ||
defaultNodeColor?: string; | ||
} | ||
|
||
declare module 'tidytree' { | ||
export declare class TidyTree { | ||
constructor(data: any, options: any); | ||
destroy(): void; | ||
redraw(): void; | ||
setColorOptions(newColorOptions: ColorOptions): void; | ||
} | ||
} |
Oops, something went wrong.