Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TreeTable React component #813

Merged
merged 34 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
55c1858
setting up modules and webpack, very simple story, not quite working
bobular Jan 30, 2024
f4de5b6
Merge remote-tracking branch 'origin/main' into tidytree-component
bobular Jan 31, 2024
0fcd354
added patristic shim and it works
bobular Jan 31, 2024
7d69b5a
'fix' platform-dependent checksum issue with yarn
bobular Feb 5, 2024
cb18aa6
refactored into proper component which takes data as prop, and story …
bobular Feb 5, 2024
03d1bec
WIP
bobular Feb 5, 2024
4b06dbc
smaller trees for stories - starting to figure out vertical spacing
bobular Feb 6, 2024
e244d8e
set the scene for figuring out vertical spacing
bobular Feb 6, 2024
037e873
switch to TidyTree branch with fixed vertical spacing
bobular Feb 6, 2024
37e62b6
remove yellow, add margins, redraw on rowHeight change, comments and …
bobular Feb 6, 2024
af81a09
Merge remote-tracking branch 'origin/main' into tidytree-component
bobular Feb 7, 2024
0e24930
changed back to master branch of JS TidyTree repo
bobular Feb 7, 2024
b8d4f25
refactor JS object management and use redraw for size changes
bobular Feb 8, 2024
e772bc4
rename `ref` to `containerRef`
bobular Feb 8, 2024
e03aaa1
node highlighting
bobular Feb 8, 2024
c8171ee
remove console.log
bobular Feb 8, 2024
4dc29e0
committed to horizontal dendrograms and renamed/refactored accordingly
bobular Feb 16, 2024
8fb48bf
straight-square oopsie
bobular Feb 16, 2024
995cc24
preliminary set up of TreeTable story
bobular Feb 16, 2024
dc003af
Merge remote-tracking branch 'origin/main' into tidytree-component
bobular Feb 17, 2024
e0d2a96
got a basic table to render
bobular Feb 17, 2024
e1433e3
fix a typo that would have caused a compile error if 'filters' had be…
bobular Feb 17, 2024
0ecb5d3
configure ts and storybook/babel for emotion css tag
bobular Feb 18, 2024
1e13e4c
nasty hack to style Mesa table rows
bobular Feb 18, 2024
323ed28
get dynamic styling of table rows and override of margin-bottom worki…
bobular Feb 19, 2024
c8e9236
now as TreeTable component
bobular Feb 19, 2024
ed85047
fixed oopsie
bobular Feb 19, 2024
20e604a
for some reason, import React was needed
bobular Feb 19, 2024
dc7c924
refactored TreeTable props to keep tree and table props separate
bobular Feb 23, 2024
ee03f4c
add checkbox selection to the table
bobular Feb 23, 2024
cf16d8a
Merge remote-tracking branch 'origin/main' into tidytree-component
bobular Feb 23, 2024
f3e144d
move @emotion/css to be a regular dependency
bobular Feb 23, 2024
c62b622
factor out d3 shimming
bobular Feb 23, 2024
f68302e
Merge branch 'main' into tidytree-component
bobular Mar 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ plugins:
- path: .yarn/plugins/@husky-install/plugin-install-husky.cjs

yarnPath: .yarn/releases/yarn-3.3.1.cjs

#
# zip/checksum behaviour is platform-specific (e.g. Windows vs Linux) for github: repos
# e.g. packages/libs/components import of tidytree
# see https://github.com/yarnpkg/berry/issues/5795
#
checksumBehavior: ignore
47 changes: 47 additions & 0 deletions packages/libs/components/.storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ module.exports = {
'@storybook/addon-viewport/register',
'./redmine-addon/register.js',
],
babel: async (options) => {
return {
...options,
presets: [...options.presets, '@emotion/babel-preset-css-prop'],
// See https://stackoverflow.com/questions/70406632/typescript-parameter-properties-not-working-with-storybook-rollup-in-developm
plugins: options.plugins.filter(
(x) =>
!(typeof x === 'string' && x.includes('plugin-transform-classes'))
),
};
},
webpackFinal: async (config, { configType }) => {
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// You can change the configuration based on that.
Expand All @@ -30,6 +41,42 @@ module.exports = {
},
});

config.module.rules.push({
test: require.resolve('tidytree'),
use: [
// TidyTree expects window.d3 to be available, so we shim it with this loader
{
loader: 'imports-loader',
options: {
imports: [
{
syntax: 'namespace',
moduleName: require.resolve('d3v5'),
name: 'd3',
},
{
syntax: 'namespace',
moduleName: require.resolve('patristic'),
name: 'patristic',
},
],
},
},
// TidyTree creates a global variable, so we convert it to a named export with this laoder
{
loader: 'exports-loader',
options: {
exports: 'TidyTree',
},
},
],
});

config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
});

// Return the altered config
return config;
},
Expand Down
12 changes: 12 additions & 0 deletions packages/libs/components/.storybook/preview.js
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>
),
];
7 changes: 6 additions & 1 deletion packages/libs/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"bootstrap": "^4.5.2",
"color-math": "^1.1.3",
"d3": "^7.1.1",
"d3v5": "npm:d3@5",
"date-arithmetic": "^4.1.0",
"debounce-promise": "^3.1.2",
"dom-to-image": "^2.6.0",
Expand All @@ -42,7 +43,8 @@
"react-plotly.js": "^2.4.0",
"react-spring": "^9.7.1",
"react-transition-group": "^4.4.1",
"shape2geohash": "^1.2.5"
"shape2geohash": "^1.2.5",
"tidytree": "github:d-callan/TidyTree"
},
"files": [
"lib",
Expand Down Expand Up @@ -107,11 +109,14 @@
"babel-loader": "^8.3.0",
"eslint": "^8.29.0",
"eslint-plugin-react-hooks": "^4.6.0",
"exports-loader": "^1.1.1",
"imports-loader": "^1.1.1",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"npm-run-all": "^4.1.5",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-query": "^3.39.3",
"stats-lite": "^2.2.0",
"storybook": "^6.5.14",
"typescript": "4.3.4"
Expand Down
1 change: 1 addition & 0 deletions packages/libs/components/public/data/newick-example.json

Large diffs are not rendered by default.

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}
/>
);
}
27 changes: 27 additions & 0 deletions packages/libs/components/src/components/tidytree/tidytree.d.ts
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;
}
}
Loading
Loading