From 7ac2d3e6283501a43d697df6e3d8e2c714892101 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 1 Mar 2024 21:50:06 +0000 Subject: [PATCH 01/92] made a start - WIP --- .../js/client/records/Sequences.jsx | 64 ------------------- .../js/client/records/Sequences.tsx | 31 +++++++++ 2 files changed, 31 insertions(+), 64 deletions(-) delete mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.jsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.jsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.jsx deleted file mode 100644 index 65f7b3d092..0000000000 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.jsx +++ /dev/null @@ -1,64 +0,0 @@ -import lodash from 'lodash'; -import React, { useMemo, useRef } from 'react'; - -// function RecordTable_TaxonCounts({ value }: WrappedComponentProps) { -export function RecordTable_Sequences(props) { - const formRef = useRef(null); - - function toggleAll(checked) { - const node = formRef.current; - if (node == null) return; - for (const input of node.querySelectorAll('input[name="msa_full_ids"]')) { - input.checked = checked; - } - } - - const sortedValue = useSortedValue(props.value); - - return ( -
- - - - toggleAll(true)} - /> - toggleAll(false)} - /> -
-

- Please note: selecting a large number of proteins will take several - minutes to align. -

-
-

- Output format:   - -

- -
- - ); -} - -function useSortedValue(value) { - return useMemo(() => lodash.sortBy(value, 'sort_key'), [value]); -} diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx new file mode 100644 index 0000000000..a99a7af205 --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { RecordTableProps, WrappedComponentProps } from './Types'; + +export function RecordTable_Sequences( + props: WrappedComponentProps +) { + const ogGroupName = props.record.id.find( + ({ name }) => name === 'group_name' + )?.value; + + const tables = Object.keys(props.record.tables).join(' + '); + // EcNumber + Sequences + TaxonCounts + ProteinPFams + PFams + Statistics + + const sequences = props.record.tables['Sequences']; + const firstRow = sequences[0]; + + const sequenceT = props.recordClass.tables.find( + (table) => table.name === 'Sequences' + ); + const attributes = sequenceT?.attributes; + return ( +
    +
  • displayName: {props.record.displayName}
  • +
  • group_name id: {ogGroupName}
  • +
  • tables keys: {tables}
  • +
  • attributes of Sequences: {JSON.stringify(attributes)}
  • +
  • first row of Sequences: {JSON.stringify(firstRow)}
  • +
+ ); +} From 2f4bc1f48970813f3530b7d8093067e4d1c95948 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Mon, 4 Mar 2024 15:55:40 +0000 Subject: [PATCH 02/92] re-render sequence table with Mesa --- .../js/client/records/Sequences.tsx | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index a99a7af205..35412d2586 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import Mesa from '@veupathdb/coreui/lib/components/Mesa'; import { RecordTableProps, WrappedComponentProps } from './Types'; export function RecordTable_Sequences( @@ -9,23 +10,31 @@ export function RecordTable_Sequences( ({ name }) => name === 'group_name' )?.value; - const tables = Object.keys(props.record.tables).join(' + '); - // EcNumber + Sequences + TaxonCounts + ProteinPFams + PFams + Statistics + const mesaColumns = props.table.attributes + .map( + ({ name, displayName }) => ({ + key: name, + name: displayName, + }) + // and remove some a raw HTML checkbox field and an object-laden 'sequence_link' field + ) + .filter(({ key }) => key !== 'clustalInput' && key !== 'sequence_link'); - const sequences = props.record.tables['Sequences']; - const firstRow = sequences[0]; + const mesaRows = props.value; + + const mesaState = { + options: {}, + rows: mesaRows, + columns: mesaColumns, + }; - const sequenceT = props.recordClass.tables.find( - (table) => table.name === 'Sequences' - ); - const attributes = sequenceT?.attributes; return ( -
    -
  • displayName: {props.record.displayName}
  • -
  • group_name id: {ogGroupName}
  • -
  • tables keys: {tables}
  • -
  • attributes of Sequences: {JSON.stringify(attributes)}
  • -
  • first row of Sequences: {JSON.stringify(firstRow)}
  • -
+ <> +
    +
  • displayName: {props.record.displayName}
  • +
  • group_name id: {ogGroupName}
  • +
+ + ); } From 3498348f42c674ed73a35f931f58cee680b01ba1 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Mon, 4 Mar 2024 18:12:28 +0000 Subject: [PATCH 03/92] tweak comments --- .../js/client/records/Sequences.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 35412d2586..85180b3d54 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -11,13 +11,12 @@ export function RecordTable_Sequences( )?.value; const mesaColumns = props.table.attributes - .map( - ({ name, displayName }) => ({ - key: name, - name: displayName, - }) - // and remove some a raw HTML checkbox field and an object-laden 'sequence_link' field - ) + .map(({ name, displayName }) => ({ + key: name, + name: displayName, + })) + // and remove a raw HTML checkbox field - we'll use Mesa's built-in checkboxes for this + // and an object-laden 'sequence_link' field - the ID seems to be replicated in the full_id field .filter(({ key }) => key !== 'clustalInput' && key !== 'sequence_link'); const mesaRows = props.value; From dc1731f3251b5c5c313f4e31245945e563e6b2dd Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Mon, 4 Mar 2024 19:32:46 +0000 Subject: [PATCH 04/92] WIP --- .../js/client/records/Sequences.tsx | 14 ++++++++++++-- .../webapp/wdkCustomization/js/client/services.tsx | 9 +++++++++ .../wdkCustomization/js/client/utils/tree.ts | 9 +++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 85180b3d54..e5a7f35770 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -2,14 +2,24 @@ import React from 'react'; import Mesa from '@veupathdb/coreui/lib/components/Mesa'; import { RecordTableProps, WrappedComponentProps } from './Types'; +import { useOrthoService } from 'ortho-client/hooks/orthoService'; export function RecordTable_Sequences( props: WrappedComponentProps ) { - const ogGroupName = props.record.id.find( + const groupName = props.record.id.find( ({ name }) => name === 'group_name' )?.value; + if (!groupName) { + throw new Error('groupName is required but was not found in the record.'); + } + + const treeResponse = useOrthoService( + (orthoService) => orthoService.getGroupTree(groupName), + [groupName] + ); + const mesaColumns = props.table.attributes .map(({ name, displayName }) => ({ key: name, @@ -31,7 +41,7 @@ export function RecordTable_Sequences( <>
  • displayName: {props.record.displayName}
  • -
  • group_name id: {ogGroupName}
  • +
  • group_name id: {groupName}
diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx index 8f04799f42..714f90cc4b 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx @@ -9,11 +9,13 @@ import { groupLayoutResponseDecoder, } from 'ortho-client/utils/groupLayout'; import { TaxonEntries, taxonEntriesDecoder } from 'ortho-client/utils/taxons'; +import { TreeResponse, treeResponseDecoder } from 'ortho-client/utils/tree'; export function wrapWdkService(wdkService: WdkService): OrthoService { return { ...wdkService, getGroupLayout: orthoServiceWrappers.getGroupLayout(wdkService), + getGroupTree: orthoServiceWrappers.getGroupTree(wdkService), getProteomeSummary: orthoServiceWrappers.getProteomeSummary(wdkService), getTaxons: orthoServiceWrappers.getTaxons(wdkService), }; @@ -26,6 +28,12 @@ const orthoServiceWrappers = { method: 'get', path: `/group/${groupName}/layout`, }), + getGroupTree: (wdkService: WdkService) => (groupName: string) => + wdkService.sendRequest(treeResponseDecoder, { + useCache: true, + method: 'get', + path: `/newick-protein-tree/${groupName}`, + }), getProteomeSummary: (wdkService: WdkService) => () => wdkService.sendRequest(proteomeSummaryRowsDecoder, { useCache: true, @@ -42,6 +50,7 @@ const orthoServiceWrappers = { export interface OrthoService extends WdkService { getGroupLayout: (groupName: string) => Promise; + getGroupTree: (groupName: string) => Promise; getProteomeSummary: () => Promise; getTaxons: () => Promise; } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts new file mode 100644 index 0000000000..5549b886f3 --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts @@ -0,0 +1,9 @@ +import { Decoder, record, string } from '@veupathdb/wdk-client/lib/Utils/Json'; + +export interface TreeResponse { + newick: string; +} + +export const treeResponseDecoder: Decoder = record({ + newick: string, +}); From 785a9ed58b305a60756414ea558ca5be946f326b Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Mar 2024 12:58:01 +0000 Subject: [PATCH 05/92] loosely wire in TreeTable and get shimmimg working --- .../js/client/records/Sequences.tsx | 14 +++++++++++--- packages/sites/ortho-site/webpack.config.js | 16 ++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index e5a7f35770..ced31da926 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -1,8 +1,8 @@ import React from 'react'; - -import Mesa from '@veupathdb/coreui/lib/components/Mesa'; +import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; +import { Loading } from '../../../../../../../libs/wdk-client/lib/Components'; export function RecordTable_Sequences( props: WrappedComponentProps @@ -20,6 +20,8 @@ export function RecordTable_Sequences( [groupName] ); + if (treeResponse == null) return ; + const mesaColumns = props.table.attributes .map(({ name, displayName }) => ({ key: name, @@ -37,13 +39,19 @@ export function RecordTable_Sequences( columns: mesaColumns, }; + const treeProps = { + data: treeResponse.newick, + width: 200, + highlightMode: 'monophyletic' as const, + }; + return ( <>
  • displayName: {props.record.displayName}
  • group_name id: {groupName}
- + ); } diff --git a/packages/sites/ortho-site/webpack.config.js b/packages/sites/ortho-site/webpack.config.js index 0f6ef873d5..7f4d03aa29 100644 --- a/packages/sites/ortho-site/webpack.config.js +++ b/packages/sites/ortho-site/webpack.config.js @@ -1,8 +1,9 @@ var configure = require('@veupathdb/site-webpack-config'); +const { addD3Shimming } = require('@veupathdb/components/webpack-shimming'); var additionalConfig = { entry: { - 'site-client': __dirname + '/webapp/wdkCustomization/js/client/main.js' + 'site-client': __dirname + '/webapp/wdkCustomization/js/client/main.js', }, module: { rules: [ @@ -12,18 +13,21 @@ var additionalConfig = { test: /\.jsx?$/, include: /node_modules\/@?react-leaflet/, use: [ - { loader: 'babel-loader', options: { configFile: './.babelrc' } } - ] + { loader: 'babel-loader', options: { configFile: './.babelrc' } }, + ], }, ], }, resolve: { alias: { 'ortho-client': __dirname + '/webapp/wdkCustomization/js/client', - 'ortho-images': __dirname + '/webapp/wdkCustomization/images' - } - } + 'ortho-images': __dirname + '/webapp/wdkCustomization/images', + }, + }, }; +// shimming of a specific version of d3 for CRC's tidytree JS library +addD3Shimming(additionalConfig.module.rules); + module.exports = configure(additionalConfig); module.exports.additionalConfig = additionalConfig; From 8c3d3305e77f143840532b5ec506cc426e854a70 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 5 Mar 2024 18:13:16 +0000 Subject: [PATCH 06/92] add patristic and basic type support --- packages/sites/ortho-site/package.json | 5 ++++- .../js/client/records/Sequences.tsx | 16 ++++++++++++-- .../js/client/types/patristic.d.ts | 22 +++++++++++++++++++ yarn.lock | 8 +++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts diff --git a/packages/sites/ortho-site/package.json b/packages/sites/ortho-site/package.json index ad20423e94..1ec27a5055 100644 --- a/packages/sites/ortho-site/package.json +++ b/packages/sites/ortho-site/package.json @@ -89,5 +89,8 @@ "files": [ "dist", "webapp" - ] + ], + "dependencies": { + "patristic": "^0.6.0" + } } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index ced31da926..641ec057d3 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -2,7 +2,8 @@ import React from 'react'; import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; -import { Loading } from '../../../../../../../libs/wdk-client/lib/Components'; +import { Loading } from '@veupathdb/wdk-client/lib/Components'; +import { parseNewick } from 'patristic'; export function RecordTable_Sequences( props: WrappedComponentProps @@ -33,6 +34,17 @@ export function RecordTable_Sequences( const mesaRows = props.value; + // do some validation on the tree w.r.t. the table + + // should this be async? it's potentially expensive + const tree = parseNewick(treeResponse.newick); + const leaves = tree.getLeaves(); + + const numLeaves = leaves.length; + const numSequences = mesaRows.length; + + console.log({ numLeaves, numSequences }); + const mesaState = { options: {}, rows: mesaRows, @@ -41,7 +53,7 @@ export function RecordTable_Sequences( const treeProps = { data: treeResponse.newick, - width: 200, + width: 400, highlightMode: 'monophyletic' as const, }; diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts new file mode 100644 index 0000000000..391468a394 --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts @@ -0,0 +1,22 @@ +declare module 'patristic' { + interface BranchData { + id: string; + parent?: Branch | null; + length?: number; + children?: Branch[]; + value?: number; + depth?: number; + height?: number; + } + + export class Branch { + constructor(data: BranchData, children?: (data: any) => Branch[]); + addChild(data: Branch | BranchData): Branch; + addParent(data: Branch | BranchData, siblings?: Branch[]): Branch; + ancestors(): Branch[]; + clone(): Branch; + getLeaves(): Branch[]; + } + + export function parseNewick(newickStr: string): Branch; +} diff --git a/yarn.lock b/yarn.lock index 6c4aa82f82..20606fde45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8755,6 +8755,7 @@ __metadata: mini-css-extract-plugin: ^2.7.6 notistack: ^1.0.10 npm-run-all: ^4.1.5 + patristic: ^0.6.0 popper.js: ^1.16.1 react: 17 react-cytoscapejs: ^2.0.0 @@ -28440,6 +28441,13 @@ __metadata: languageName: node linkType: hard +"patristic@npm:^0.6.0": + version: 0.6.0 + resolution: "patristic@npm:0.6.0" + checksum: b2253b1dcc9ca85e8a4c50f3d496d50bff45f8378f6a5b2e4ec03f69b2332c51d374bcd5e90b4387222ec98c8fb6b26b4788784a6df8234530557dac9e4b0b6f + languageName: node + linkType: hard + "pbf@npm:^3.2.1": version: 3.2.1 resolution: "pbf@npm:3.2.1" From adb464a2197be219b537a55a18a4e39e687fd4ed Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Mar 2024 00:09:21 +0000 Subject: [PATCH 07/92] made a start with controlling table row height but should investigate the Mesa inline option --- .../tidytree/HorizontalDendrogram.tsx | 17 +++++++++- .../src/components/tidytree/TreeTable.tsx | 31 +++++++++++++++---- .../src/components/Mesa/Ui/DataCell.jsx | 8 ++++- .../js/client/records/Sequences.tsx | 10 ++++-- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx index 54ccefc3fc..8b2ee777f4 100644 --- a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx +++ b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx @@ -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 { @@ -31,10 +31,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 */ @@ -60,6 +71,8 @@ export function HorizontalDendrogram({ options: { ruler = false, margin = [0, 0, 0, 0] }, highlightedNodeIds, highlightMode, + hStretch = 1.0, + containerStyles, }: HorizontalDendrogramProps) { const containerRef = useRef(null); const tidyTreeRef = useRef(); @@ -80,6 +93,7 @@ export function HorizontalDendrogram({ equidistantLeaves: true, ruler, margin, + hStretch, animation: 0, // it's naff and it reveals edge lengths/weights momentarily }); tidyTreeRef.current = instance; @@ -119,6 +133,7 @@ export function HorizontalDendrogram({ style={{ width: width + 'px', height: containerHeight + 'px', + ...containerStyles, }} ref={containerRef} /> diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 35eb2fc55f..81258bb33a 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -48,10 +48,23 @@ export default function TreeTable(props: TreeTableProps) { const rowStyleClassName = useMemo( () => cx( - classNameStyle({ - height: rowHeight + 'px', - background: 'yellow', - }) + classNameStyle` + height: ${rowHeight}px; + + & td { + max-width: 0; + max-height: ${rowHeight}px; /* Match the row height for consistency */ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + /* Add a basic tooltip to encourage hover to reveal the 'title' attribute */ + &:hover { + cursor: pointer; + position: relative; + } + } + ` ), [rowHeight] ); @@ -76,16 +89,22 @@ export default function TreeTable(props: TreeTableProps) { leafCount={rows.length} options={{ margin: [0, 10, 0, 10] }} /> - <> +
- +
); } diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index 06c3a78c11..ef18a98406 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -61,7 +61,13 @@ class DataCell extends React.PureComponent { style = Object.assign({}, style, width, whiteSpace); className = dataCellClass() + (className ? ' ' + className : ''); const children = this.renderContent(); - const props = { style, children, key, className }; + const props = { + style, + children, + key, + className, + title: 'possibly an option to use the title attribute', + }; return column.hidden ? null : ; } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 641ec057d3..8f979c2749 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -53,17 +53,23 @@ export function RecordTable_Sequences( const treeProps = { data: treeResponse.newick, - width: 400, + width: 200, highlightMode: 'monophyletic' as const, }; + const rowHeight = 45; + return ( <>
  • displayName: {props.record.displayName}
  • group_name id: {groupName}
- + ); } From f215762e18da269ff8a08539c90d16488d61c87f Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Mar 2024 13:18:48 +0000 Subject: [PATCH 08/92] added Mesa table tooltips for td contents when in 'inline' mode (basically fixed-height mode) --- .../coreui/src/components/Mesa/Templates.jsx | 59 +++++++++++++------ .../src/components/Mesa/Ui/DataCell.jsx | 37 +++++++++++- 2 files changed, 76 insertions(+), 20 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Templates.jsx b/packages/libs/coreui/src/components/Mesa/Templates.jsx index 5923c57954..1aed26f712 100644 --- a/packages/libs/coreui/src/components/Mesa/Templates.jsx +++ b/packages/libs/coreui/src/components/Mesa/Templates.jsx @@ -6,10 +6,14 @@ import TruncatedText from './Components/TruncatedText'; import { stringValue } from './Utils/Utils'; const Templates = { + textText({ value }) { + return stringValue(value); + }, + textCell({ key, value, row, rowIndex, column }) { const { truncated } = column; const className = 'Cell Cell-' + key; - const text = stringValue(value); + const text = Templates.textText({ value }); return truncated ? ( {display}; }, + wdkLinkText({ value, href }) { + const { displayText, url } = value; + return displayText.length ? value.displayText : href; + }, + wdkLinkCell({ key, value, row, rowIndex, column }) { const className = 'Cell wdkLinkCell Cell-' + key; - let { displayText, url } = value; - let href = url ? url : '#'; - let text = displayText.length ? value.displayText : href; - text =
; - let target = '_blank'; - + const { url } = value; + const href = url ? url : '#'; + const text = Templates.wdkLinkText({ value, href }); + const div =
; + const target = '_blank'; const props = { href, target, className }; - return {text}; + return {div}; + }, + + linkText({ value }) { + const { text } = getLinkDetails(value); + return text; }, linkCell({ key, value, row, rowIndex, column }) { const className = 'Cell LinkCell Cell-' + key; - const defaults = { href: null, target: '_blank', text: '' }; - let { href, target, text } = typeof value === 'object' ? value : defaults; - href = href ? href : typeof value === 'string' ? value : '#'; - text = text.length ? text : href; - + const { href, text, target } = getLinkDetails(value); const props = { href, target, className, name: text }; - return {text}; }, @@ -78,4 +90,17 @@ const Templates = { }, }; +function getLinkDetails(value) { + const defaults = { href: '#', text: '', target: '_blank' }; + if (typeof value === 'string') { + // If the value is a string, it's used for both href and text, with default target + return { ...defaults, href: value, text: value }; + } else if (typeof value === 'object' && value != null) { + // If the value is an object, extract href, text, and target, applying defaults as necessary + const { href = '#', text = '', target = '_blank' } = value; + return { href, text: text.length > 0 ? text : href, target }; + } + return defaults; +} + export default Templates; diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index ef18a98406..1449037de7 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -23,8 +23,7 @@ class DataCell extends React.PureComponent { return column.renderCell(cellProps); } - if (!column.type) return Templates.textCell(cellProps); - if (!cellProps.value) return Templates.textCell(cellProps); + if (!column.type || !cellProps.value) return Templates.textCell(cellProps); switch (column.type.toLowerCase()) { case 'wdklink': @@ -43,6 +42,33 @@ class DataCell extends React.PureComponent { } } + getTooltipText() { + const { row, column, rowIndex, columnIndex, inline } = this.props; + const { key, getValue } = column; + const value = + typeof getValue === 'function' ? getValue({ row, key }) : row[key]; + const cellProps = { key, value, row, column, rowIndex, columnIndex }; + + // ignores optional renderCell wrapper function + + if (!column.type || !cellProps.value) return Templates.textText(cellProps); + + switch (column.type.toLowerCase()) { + case 'wdklink': + return Templates.wdkLinkText(cellProps); + case 'link': + return Templates.linkText(cellProps); + case 'number': + return Templates.numberText(cellProps); + case 'html': { + return undefined; // no title attribute/tooltip for HTML cells + } + case 'text': + default: + return Templates.textText(cellProps); + } + } + render() { let { column, inline, options } = this.props; let { style, width, className, key } = column; @@ -60,13 +86,18 @@ class DataCell extends React.PureComponent { width = width ? { width, maxWidth: width, minWidth: width } : {}; style = Object.assign({}, style, width, whiteSpace); className = dataCellClass() + (className ? ' ' + className : ''); + + // provide basic mouse-over support for inline tables where + // text is likely to be truncated + const title = inline ? this.getTooltipText() : undefined; + const children = this.renderContent(); const props = { style, children, key, className, - title: 'possibly an option to use the title attribute', + title, }; return column.hidden ? null : ; From 1abea4b75441dce3cebfded2be5d8e79e3c627a3 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Mar 2024 13:30:19 +0000 Subject: [PATCH 09/92] use Mesa's inline option and tidy up --- .../src/components/tidytree/TreeTable.tsx | 20 +++++++++++-------- .../js/client/records/Sequences.tsx | 5 +---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 81258bb33a..667ebb83e0 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -12,8 +12,13 @@ export interface TreeTableProps { /** * 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 */ @@ -42,27 +47,23 @@ export interface TreeTableProps { * - allow additional Mesa props and options to be passed */ export default function TreeTable(props: TreeTableProps) { - const { rowHeight } = props; + const { rowHeight, maxColumnWidth = 200 } = props; const { rows } = props.tableProps; const rowStyleClassName = useMemo( () => cx( + // minimum height for table rows classNameStyle` height: ${rowHeight}px; & td { - max-width: 0; - max-height: ${rowHeight}px; /* Match the row height for consistency */ - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - /* Add a basic tooltip to encourage hover to reveal the 'title' attribute */ + &:hover { cursor: pointer; position: relative; } + } ` ), @@ -76,6 +77,9 @@ export default function TreeTable(props: TreeTableProps) { options: { ...props.tableProps.options, deriveRowClassName: (_) => rowStyleClassName, + inline: true, + inlineMaxHeight: `${rowHeight}px`, + inlineMaxWidth: `${maxColumnWidth}px`, }, }; diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 8f979c2749..3097bfd79e 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -61,10 +61,7 @@ export function RecordTable_Sequences( return ( <> -
    -
  • displayName: {props.record.displayName}
  • -
  • group_name id: {groupName}
  • -
+
Ignore the help text above for now!
Date: Fri, 8 Mar 2024 15:15:30 +0000 Subject: [PATCH 10/92] Revert "added Mesa table tooltips for td contents when in 'inline' mode (basically fixed-height mode)" This reverts commit f215762e18da269ff8a08539c90d16488d61c87f. --- .../coreui/src/components/Mesa/Templates.jsx | 59 ++++++------------- .../src/components/Mesa/Ui/DataCell.jsx | 37 +----------- 2 files changed, 20 insertions(+), 76 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Templates.jsx b/packages/libs/coreui/src/components/Mesa/Templates.jsx index 1aed26f712..5923c57954 100644 --- a/packages/libs/coreui/src/components/Mesa/Templates.jsx +++ b/packages/libs/coreui/src/components/Mesa/Templates.jsx @@ -6,14 +6,10 @@ import TruncatedText from './Components/TruncatedText'; import { stringValue } from './Utils/Utils'; const Templates = { - textText({ value }) { - return stringValue(value); - }, - textCell({ key, value, row, rowIndex, column }) { const { truncated } = column; const className = 'Cell Cell-' + key; - const text = Templates.textText({ value }); + const text = stringValue(value); return truncated ? ( {display}
; - }, + const display = + typeof value === 'number' ? value.toLocaleString() : stringValue(value); - wdkLinkText({ value, href }) { - const { displayText, url } = value; - return displayText.length ? value.displayText : href; + return
{display}
; }, wdkLinkCell({ key, value, row, rowIndex, column }) { const className = 'Cell wdkLinkCell Cell-' + key; - const { url } = value; - const href = url ? url : '#'; - const text = Templates.wdkLinkText({ value, href }); - const div =
; - const target = '_blank'; - const props = { href, target, className }; + let { displayText, url } = value; + let href = url ? url : '#'; + let text = displayText.length ? value.displayText : href; + text =
; + let target = '_blank'; - return {div}; - }, + const props = { href, target, className }; - linkText({ value }) { - const { text } = getLinkDetails(value); - return text; + return {text}; }, linkCell({ key, value, row, rowIndex, column }) { const className = 'Cell LinkCell Cell-' + key; - const { href, text, target } = getLinkDetails(value); + const defaults = { href: null, target: '_blank', text: '' }; + let { href, target, text } = typeof value === 'object' ? value : defaults; + href = href ? href : typeof value === 'string' ? value : '#'; + text = text.length ? text : href; + const props = { href, target, className, name: text }; + return {text}; }, @@ -90,17 +78,4 @@ const Templates = { }, }; -function getLinkDetails(value) { - const defaults = { href: '#', text: '', target: '_blank' }; - if (typeof value === 'string') { - // If the value is a string, it's used for both href and text, with default target - return { ...defaults, href: value, text: value }; - } else if (typeof value === 'object' && value != null) { - // If the value is an object, extract href, text, and target, applying defaults as necessary - const { href = '#', text = '', target = '_blank' } = value; - return { href, text: text.length > 0 ? text : href, target }; - } - return defaults; -} - export default Templates; diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index 1449037de7..ef18a98406 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -23,7 +23,8 @@ class DataCell extends React.PureComponent { return column.renderCell(cellProps); } - if (!column.type || !cellProps.value) return Templates.textCell(cellProps); + if (!column.type) return Templates.textCell(cellProps); + if (!cellProps.value) return Templates.textCell(cellProps); switch (column.type.toLowerCase()) { case 'wdklink': @@ -42,33 +43,6 @@ class DataCell extends React.PureComponent { } } - getTooltipText() { - const { row, column, rowIndex, columnIndex, inline } = this.props; - const { key, getValue } = column; - const value = - typeof getValue === 'function' ? getValue({ row, key }) : row[key]; - const cellProps = { key, value, row, column, rowIndex, columnIndex }; - - // ignores optional renderCell wrapper function - - if (!column.type || !cellProps.value) return Templates.textText(cellProps); - - switch (column.type.toLowerCase()) { - case 'wdklink': - return Templates.wdkLinkText(cellProps); - case 'link': - return Templates.linkText(cellProps); - case 'number': - return Templates.numberText(cellProps); - case 'html': { - return undefined; // no title attribute/tooltip for HTML cells - } - case 'text': - default: - return Templates.textText(cellProps); - } - } - render() { let { column, inline, options } = this.props; let { style, width, className, key } = column; @@ -86,18 +60,13 @@ class DataCell extends React.PureComponent { width = width ? { width, maxWidth: width, minWidth: width } : {}; style = Object.assign({}, style, width, whiteSpace); className = dataCellClass() + (className ? ' ' + className : ''); - - // provide basic mouse-over support for inline tables where - // text is likely to be truncated - const title = inline ? this.getTooltipText() : undefined; - const children = this.renderContent(); const props = { style, children, key, className, - title, + title: 'possibly an option to use the title attribute', }; return column.hidden ? null : ; From a7c0e1be1c447ed933bd188d863f8f6f63114421 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Mar 2024 15:17:55 +0000 Subject: [PATCH 11/92] remove placeholder td title --- packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index ef18a98406..dc26a3bc56 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -66,7 +66,6 @@ class DataCell extends React.PureComponent { children, key, className, - title: 'possibly an option to use the title attribute', }; return column.hidden ? null : ; From a19369eb0a95c286ce8ddbf2033320ccb62763fc Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 8 Mar 2024 15:19:14 +0000 Subject: [PATCH 12/92] restore whitespace in DataCell.tsx --- packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index dc26a3bc56..06c3a78c11 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -61,12 +61,7 @@ class DataCell extends React.PureComponent { style = Object.assign({}, style, width, whiteSpace); className = dataCellClass() + (className ? ' ' + className : ''); const children = this.renderContent(); - const props = { - style, - children, - key, - className, - }; + const props = { style, children, key, className }; return column.hidden ? null : ; } From 5f908bd6d5bf96ae33ec758c877298c3e768e30b Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 15 Mar 2024 19:36:43 +0000 Subject: [PATCH 13/92] upgrade tidytree and use new interactive option --- packages/libs/components/package.json | 2 +- .../src/components/tidytree/HorizontalDendrogram.tsx | 4 +++- .../components/src/components/tidytree/TreeTable.tsx | 2 +- yarn.lock | 10 +++++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/libs/components/package.json b/packages/libs/components/package.json index 7d6ea14969..575c2ca577 100755 --- a/packages/libs/components/package.json +++ b/packages/libs/components/package.json @@ -45,7 +45,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", diff --git a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx index 8b2ee777f4..1b80c21a10 100644 --- a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx +++ b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx @@ -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: /// @@ -68,7 +69,7 @@ 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, hStretch = 1.0, @@ -95,6 +96,7 @@ export function HorizontalDendrogram({ margin, hStretch, animation: 0, // it's naff and it reveals edge lengths/weights momentarily + interactive, }); tidyTreeRef.current = instance; return function cleanup() { diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 667ebb83e0..675094961d 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -91,7 +91,7 @@ export default function TreeTable(props: TreeTableProps) { {...props.treeProps} rowHeight={rowHeight} leafCount={rows.length} - options={{ margin: [0, 10, 0, 10] }} + options={{ margin: [0, 10, 0, 10], interactive: false }} />
Date: Fri, 15 Mar 2024 23:33:10 +0000 Subject: [PATCH 14/92] table and tree order are the same --- .../js/client/records/Sequences.tsx | 14 ++++++++++---- .../js/client/types/patristic.d.ts | 10 ++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 3097bfd79e..54d8de9230 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -4,6 +4,7 @@ import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; import { Loading } from '@veupathdb/wdk-client/lib/Components'; import { parseNewick } from 'patristic'; +import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; export function RecordTable_Sequences( props: WrappedComponentProps @@ -40,14 +41,19 @@ export function RecordTable_Sequences( const tree = parseNewick(treeResponse.newick); const leaves = tree.getLeaves(); - const numLeaves = leaves.length; - const numSequences = mesaRows.length; + // sort the table in the same order as the tree's leaves + const sortedRows = leaves + .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) + .filter((row): row is Record => row != null); - console.log({ numLeaves, numSequences }); + if (leaves.length !== sortedRows.length) + return ( +
Tree and protein list mismatch, please contact the helpdesk
+ ); const mesaState = { options: {}, - rows: mesaRows, + rows: sortedRows, columns: mesaColumns, }; diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts index 391468a394..4b7b06bb10 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts @@ -1,5 +1,5 @@ declare module 'patristic' { - interface BranchData { + export class Branch { id: string; parent?: Branch | null; length?: number; @@ -7,12 +7,10 @@ declare module 'patristic' { value?: number; depth?: number; height?: number; - } - export class Branch { - constructor(data: BranchData, children?: (data: any) => Branch[]); - addChild(data: Branch | BranchData): Branch; - addParent(data: Branch | BranchData, siblings?: Branch[]): Branch; + constructor(data: Branch, children?: (data: any) => Branch[]); + addChild(data: Branch): Branch; + addParent(data: Branch, siblings?: Branch[]): Branch; ancestors(): Branch[]; clone(): Branch; getLeaves(): Branch[]; From 76bfe0567965c31b2ace1904ad587b9d04537132 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 15 Mar 2024 23:49:01 +0000 Subject: [PATCH 15/92] added basic checkboxes --- .../js/client/records/Sequences.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 54d8de9230..e122c59204 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; @@ -6,6 +6,8 @@ import { Loading } from '@veupathdb/wdk-client/lib/Components'; import { parseNewick } from 'patristic'; import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; +type RowType = Record; + export function RecordTable_Sequences( props: WrappedComponentProps ) { @@ -22,6 +24,8 @@ export function RecordTable_Sequences( [groupName] ); + const [highlightedNodes, setHighlightedNodes] = useState([]); + if (treeResponse == null) return ; const mesaColumns = props.table.attributes @@ -44,7 +48,7 @@ export function RecordTable_Sequences( // sort the table in the same order as the tree's leaves const sortedRows = leaves .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) - .filter((row): row is Record => row != null); + .filter((row): row is RowType => row != null); if (leaves.length !== sortedRows.length) return ( @@ -52,15 +56,25 @@ export function RecordTable_Sequences( ); const mesaState = { - options: {}, + options: { + isRowSelected: (row: RowType) => + highlightedNodes.includes(row.full_id as string), + }, rows: sortedRows, columns: mesaColumns, + eventHandlers: { + onRowSelect: (row: RowType) => + setHighlightedNodes((prev) => [...prev, row.full_id as string]), + onRowDeselect: (row: RowType) => + setHighlightedNodes((prev) => prev.filter((id) => id !== row.full_id)), + }, }; const treeProps = { data: treeResponse.newick, width: 200, highlightMode: 'monophyletic' as const, + highlightedNodeIds: highlightedNodes, }; const rowHeight = 45; From cdc9d49d2a7a13b37bbe833c90dd3c8843ba355f Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 19 Mar 2024 22:41:56 +0000 Subject: [PATCH 16/92] WIP clustal form --- .../js/client/records/Sequences.tsx | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index e122c59204..1a2057d18f 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -81,12 +81,37 @@ export function RecordTable_Sequences( return ( <> -
Ignore the help text above for now!
+
+ + +

+ Please note: selecting a large number of proteins will take several + minutes to align. +

+
+

+ Output format:   + +

+ +
+
); } From 1495c072b642260098809ab3a95f74259ab3d2c9 Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 19 Mar 2024 23:00:38 +0000 Subject: [PATCH 17/92] that's hopefully clustal sorted - but can't test with local ortho-site --- .../wdkCustomization/js/client/records/Sequences.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 1a2057d18f..ca540374d6 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -88,10 +88,12 @@ export function RecordTable_Sequences( />
- + {highlightedNodes.map((id) => ( + + ))}

Please note: selecting a large number of proteins will take several - minutes to align. + minutes to align. You must select at least two proteins.

@@ -106,7 +108,7 @@ export function RecordTable_Sequences(

From 88316951fea5e3b549e5a3de2ae0498a8082eee3 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 20 Mar 2024 10:15:00 +0000 Subject: [PATCH 18/92] improve 'must select two proteins' verbiage --- .../js/client/records/Sequences.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index ca540374d6..c7be380af7 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -78,6 +78,8 @@ export function RecordTable_Sequences( }; const rowHeight = 45; + const clustalDisabled = + highlightedNodes == null || highlightedNodes.length < 2; return ( <> @@ -93,7 +95,7 @@ export function RecordTable_Sequences( ))}

Please note: selecting a large number of proteins will take several - minutes to align. You must select at least two proteins. + minutes to align.

@@ -106,12 +108,14 @@ export function RecordTable_Sequences(

- +
+ + {clustalDisabled && ( + (You must select at least two proteins.) + )} +
From 5c896a0152eacb19703484cce39ce670734fc8c8 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 20 Mar 2024 10:21:36 +0000 Subject: [PATCH 19/92] de-complicate .DataTable margin-bottom override --- .../src/components/tidytree/TreeTable.tsx | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 675094961d..52f6f1e522 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -5,8 +5,7 @@ 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 { css, cx } from '@emotion/css'; export interface TreeTableProps { /** @@ -54,18 +53,16 @@ export default function TreeTable(props: TreeTableProps) { () => cx( // minimum height for table rows - classNameStyle` + css` height: ${rowHeight}px; & td { - &:hover { cursor: pointer; position: relative; } - } - ` + ` ), [rowHeight] ); @@ -94,19 +91,15 @@ export default function TreeTable(props: TreeTableProps) { options={{ margin: [0, 10, 0, 10], interactive: false }} />
-
From fd446f87e06b32b937a17ca637acf2ff4fd1180f Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 20 Mar 2024 13:02:32 +0000 Subject: [PATCH 20/92] rename TreeResponse to GroupTreeResponse --- .../webapp/wdkCustomization/js/client/services.tsx | 9 ++++++--- .../webapp/wdkCustomization/js/client/utils/tree.ts | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx index 714f90cc4b..ebb26772e1 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/services.tsx @@ -9,7 +9,10 @@ import { groupLayoutResponseDecoder, } from 'ortho-client/utils/groupLayout'; import { TaxonEntries, taxonEntriesDecoder } from 'ortho-client/utils/taxons'; -import { TreeResponse, treeResponseDecoder } from 'ortho-client/utils/tree'; +import { + GroupTreeResponse, + groupTreeResponseDecoder, +} from 'ortho-client/utils/tree'; export function wrapWdkService(wdkService: WdkService): OrthoService { return { @@ -29,7 +32,7 @@ const orthoServiceWrappers = { path: `/group/${groupName}/layout`, }), getGroupTree: (wdkService: WdkService) => (groupName: string) => - wdkService.sendRequest(treeResponseDecoder, { + wdkService.sendRequest(groupTreeResponseDecoder, { useCache: true, method: 'get', path: `/newick-protein-tree/${groupName}`, @@ -50,7 +53,7 @@ const orthoServiceWrappers = { export interface OrthoService extends WdkService { getGroupLayout: (groupName: string) => Promise; - getGroupTree: (groupName: string) => Promise; + getGroupTree: (groupName: string) => Promise; getProteomeSummary: () => Promise; getTaxons: () => Promise; } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts index 5549b886f3..89cf84fede 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/utils/tree.ts @@ -1,9 +1,9 @@ import { Decoder, record, string } from '@veupathdb/wdk-client/lib/Utils/Json'; -export interface TreeResponse { +export interface GroupTreeResponse { newick: string; } -export const treeResponseDecoder: Decoder = record({ +export const groupTreeResponseDecoder: Decoder = record({ newick: string, }); From a36c6addb9a04a3e056905144464edaedd962ae8 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 20 Mar 2024 18:36:02 +0000 Subject: [PATCH 21/92] add PFam domain architecture column --- .../js/client/records/Sequences.tsx | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index c7be380af7..2dc0d70604 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -5,6 +5,10 @@ import { useOrthoService } from 'ortho-client/hooks/orthoService'; import { Loading } from '@veupathdb/wdk-client/lib/Components'; import { parseNewick } from 'patristic'; import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; +import { MesaColumn } from '../../../../../../../libs/coreui/lib/components/Mesa/types'; +import { groupBy } from 'lodash'; +import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; +import { extractPfamDomain } from 'ortho-client/records/utils'; type RowType = Record; @@ -26,9 +30,7 @@ export function RecordTable_Sequences( const [highlightedNodes, setHighlightedNodes] = useState([]); - if (treeResponse == null) return ; - - const mesaColumns = props.table.attributes + const mesaColumns: MesaColumn[] = props.table.attributes .map(({ name, displayName }) => ({ key: name, name: displayName, @@ -39,6 +41,44 @@ export function RecordTable_Sequences( const mesaRows = props.value; + // deal with Pfam domain architectures + const proteinPfams = props.record.tables['ProteinPFams']; + const rowsByAccession = groupBy(proteinPfams, 'full_id'); + + // const maxLength = useMemo( + // () => mesaRows.reduce( + // (max, current) => { + // const length = Number(current['protein_length']); + // return length > max ? length : max; + // }, 0) + // , [ mesaRows ]); + + mesaColumns.unshift({ + key: 'pfamArchitecture', + name: 'Domain architecture (all drawn to 100% length)', + renderCell: (cellProps) => { + const proteinId = cellProps.row.full_id as string; + const flatPfamData = rowsByAccession[proteinId]; + if (flatPfamData && flatPfamData.length > 0) { + const pfamDomains = flatPfamData.flatMap(extractPfamDomain); + const proteinLength = Number( + flatPfamData[0]['protein_length'] as string + ); + return ( + + ); + } else { + return no PFAM domains; + } + }, + }); + + if (treeResponse == null) return ; + // do some validation on the tree w.r.t. the table // should this be async? it's potentially expensive From f4890662da136d53243596b2cbab5602fc2ad1c8 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 20 Mar 2024 20:30:13 +0000 Subject: [PATCH 22/92] ugly search working as a demo on description column --- .../src/components/tidytree/TreeTable.tsx | 21 ++++++++++++------- .../src/components/Mesa/Ui/TableSearch.jsx | 2 +- .../src/components/Mesa/Ui/TableToolbar.jsx | 10 +++++++-- .../js/client/records/Sequences.tsx | 20 ++++++++++++++++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 52f6f1e522..0a05edef43 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -29,6 +29,10 @@ export interface TreeTableProps { * data and options for the table */ tableProps: MesaStateProps; + /** + * hide the tree (but keep its horizontal space); default = false + */ + hideTree?: boolean; } /** @@ -46,7 +50,7 @@ export interface TreeTableProps { * - allow additional Mesa props and options to be passed */ export default function TreeTable(props: TreeTableProps) { - const { rowHeight, maxColumnWidth = 200 } = props; + const { rowHeight, maxColumnWidth = 200, hideTree = false } = props; const { rows } = props.tableProps; const rowStyleClassName = useMemo( @@ -84,14 +88,17 @@ export default function TreeTable(props: TreeTableProps) {
- + {!hideTree && ( + + )}
; + return ( + + ); } renderCounter() { diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 2dc0d70604..01659edd49 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -15,6 +15,8 @@ type RowType = Record; export function RecordTable_Sequences( props: WrappedComponentProps ) { + const [searchQuery, setSearchQuery] = useState(''); + const groupName = props.record.id.find( ({ name }) => name === 'group_name' )?.value; @@ -90,6 +92,13 @@ export function RecordTable_Sequences( .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) .filter((row): row is RowType => row != null); + const filteredRows = + searchQuery !== '' + ? sortedRows.filter((row) => + (row.description as string).match(new RegExp(searchQuery, 'i')) + ) + : undefined; + if (leaves.length !== sortedRows.length) return (
Tree and protein list mismatch, please contact the helpdesk
@@ -99,14 +108,24 @@ export function RecordTable_Sequences( options: { isRowSelected: (row: RowType) => highlightedNodes.includes(row.full_id as string), + toolbar: true, + searchPlaceholder: + 'Type to filter the table. (Description column only at the moment!!) The tree will not be shown while filtering.', + }, + uiState: { + searchQuery, }, rows: sortedRows, + filteredRows, columns: mesaColumns, eventHandlers: { onRowSelect: (row: RowType) => setHighlightedNodes((prev) => [...prev, row.full_id as string]), onRowDeselect: (row: RowType) => setHighlightedNodes((prev) => prev.filter((id) => id !== row.full_id)), + onSearch: (query: string) => { + setSearchQuery(query); + }, }, }; @@ -127,6 +146,7 @@ export function RecordTable_Sequences( rowHeight={rowHeight} treeProps={treeProps} tableProps={mesaState} + hideTree={!!filteredRows} />
From c06178d1988f737e7f7c0371ee679a4a7c95c8f6 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 12:28:30 +0000 Subject: [PATCH 23/92] add warning banner for data issue and tweak pfam column heading --- .../wdkCustomization/js/client/records/Sequences.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 01659edd49..86b72e8eff 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -9,6 +9,7 @@ import { MesaColumn } from '../../../../../../../libs/coreui/lib/components/Mesa import { groupBy } from 'lodash'; import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { extractPfamDomain } from 'ortho-client/records/utils'; +import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; type RowType = Record; @@ -57,7 +58,7 @@ export function RecordTable_Sequences( mesaColumns.unshift({ key: 'pfamArchitecture', - name: 'Domain architecture (all drawn to 100% length)', + name: 'Domain architecture (all drawn to same length)', renderCell: (cellProps) => { const proteinId = cellProps.row.full_id as string; const flatPfamData = rowsByAccession[proteinId]; @@ -101,7 +102,13 @@ export function RecordTable_Sequences( if (leaves.length !== sortedRows.length) return ( -
Tree and protein list mismatch, please contact the helpdesk
+ ); const mesaState = { From 2599155cd72121c19c123f1a9b29b5d997a4b5ae Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 16:11:36 +0000 Subject: [PATCH 24/92] used RealTimeSearchBox and made regexps safer and reorganised so we can useMemo for the table filtering --- .../js/client/records/Sequences.tsx | 92 +++++++++++++------ 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 86b72e8eff..45fa2c0362 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -1,8 +1,11 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; -import { Loading } from '@veupathdb/wdk-client/lib/Components'; +import { + Loading, + RealTimeSearchBox, +} from '@veupathdb/wdk-client/lib/Components'; import { parseNewick } from 'patristic'; import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; import { MesaColumn } from '../../../../../../../libs/coreui/lib/components/Mesa/types'; @@ -17,6 +20,10 @@ export function RecordTable_Sequences( props: WrappedComponentProps ) { const [searchQuery, setSearchQuery] = useState(''); + const safeSearchRegexp = useMemo( + () => createSafeSearchRegExp(searchQuery), + [searchQuery] + ); const groupName = props.record.id.find( ({ name }) => name === 'group_name' @@ -80,27 +87,34 @@ export function RecordTable_Sequences( }, }); - if (treeResponse == null) return ; - // do some validation on the tree w.r.t. the table // should this be async? it's potentially expensive - const tree = parseNewick(treeResponse.newick); - const leaves = tree.getLeaves(); + const tree = treeResponse && parseNewick(treeResponse.newick); + const leaves = tree?.getLeaves(); // sort the table in the same order as the tree's leaves - const sortedRows = leaves - .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) - .filter((row): row is RowType => row != null); - - const filteredRows = - searchQuery !== '' - ? sortedRows.filter((row) => - (row.description as string).match(new RegExp(searchQuery, 'i')) - ) - : undefined; + const sortedRows = useMemo( + () => + leaves + ?.map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) + .filter((row): row is RowType => row != null), + [leaves, mesaRows] + ); - if (leaves.length !== sortedRows.length) + // can't memoize this easily after the early return for null treeResponse above :-( + const filteredRows = useMemo( + () => + searchQuery !== '' + ? sortedRows?.filter((row) => rowMatch(row, safeSearchRegexp)) + : undefined, + [searchQuery, safeSearchRegexp, sortedRows] + ); + + if (treeResponse == null || leaves == null || sortedRows == null) + return ; + + if (leaves?.length !== sortedRows?.length) return ( highlightedNodes.includes(row.full_id as string), - toolbar: true, - searchPlaceholder: - 'Type to filter the table. (Description column only at the moment!!) The tree will not be shown while filtering.', - }, - uiState: { - searchQuery, }, rows: sortedRows, filteredRows, @@ -130,9 +138,6 @@ export function RecordTable_Sequences( setHighlightedNodes((prev) => [...prev, row.full_id as string]), onRowDeselect: (row: RowType) => setHighlightedNodes((prev) => prev.filter((id) => id !== row.full_id)), - onSearch: (query: string) => { - setSearchQuery(query); - }, }, }; @@ -149,6 +154,13 @@ export function RecordTable_Sequences( return ( <> + ); } + +function rowMatch(row: RowType, query: RegExp): boolean { + return ( + Object.values(row).find((value) => { + if (value != null) { + if (typeof value === 'string') return value.match(query); + else if ( + typeof value === 'object' && + 'displayText' in value && + typeof value.displayText === 'string' + ) + return value.displayText.match(query); + } + return false; + }) !== undefined + ); +} + +function createSafeSearchRegExp(input: string): RegExp { + try { + // Attempt to create a RegExp from the user input directly + return new RegExp(input, 'i'); + } catch (error) { + // If an error occurs (e.g., invalid RegExp), escape the input and create a literal search RegExp + const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return new RegExp(escapedInput, 'i'); + } +} From 76680851ae1d9ccae6fd5b3b0090af9b6bfaa4a1 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 16:47:38 +0000 Subject: [PATCH 25/92] more memo and remove some commented code --- .../js/client/records/Sequences.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 45fa2c0362..0e3b7414f9 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -55,14 +55,6 @@ export function RecordTable_Sequences( const proteinPfams = props.record.tables['ProteinPFams']; const rowsByAccession = groupBy(proteinPfams, 'full_id'); - // const maxLength = useMemo( - // () => mesaRows.reduce( - // (max, current) => { - // const length = Number(current['protein_length']); - // return length > max ? length : max; - // }, 0) - // , [ mesaRows ]); - mesaColumns.unshift({ key: 'pfamArchitecture', name: 'Domain architecture (all drawn to same length)', @@ -90,8 +82,11 @@ export function RecordTable_Sequences( // do some validation on the tree w.r.t. the table // should this be async? it's potentially expensive - const tree = treeResponse && parseNewick(treeResponse.newick); - const leaves = tree?.getLeaves(); + const tree = useMemo( + () => treeResponse && parseNewick(treeResponse.newick), + [treeResponse] + ); + const leaves = useMemo(() => tree?.getLeaves(), [tree]); // sort the table in the same order as the tree's leaves const sortedRows = useMemo( From 95ac689c7ebca466352bbe6fd1057dffe1d3ca2c Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 18:09:29 +0000 Subject: [PATCH 26/92] add tree filtering --- .../src/components/tidytree/TreeTable.tsx | 4 +- .../js/client/records/Sequences.tsx | 42 ++++++++++++++++++- .../js/client/types/patristic.d.ts | 2 + 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 0a05edef43..9e174a1edb 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -51,7 +51,7 @@ export interface TreeTableProps { */ export default function TreeTable(props: TreeTableProps) { const { rowHeight, maxColumnWidth = 200, hideTree = false } = props; - const { rows } = props.tableProps; + const { rows, filteredRows } = props.tableProps; const rowStyleClassName = useMemo( () => @@ -92,7 +92,7 @@ export default function TreeTable(props: TreeTableProps) { )} diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 0e3b7414f9..cfc907f6b5 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -106,6 +106,45 @@ export function RecordTable_Sequences( [searchQuery, safeSearchRegexp, sortedRows] ); + // now filter the tree if needed. + const filteredTree = useMemo(() => { + if (leaves == null || tree == null || filteredRows?.length === 0) return; + + if (filteredRows != null && filteredRows.length < leaves.length) { + // must work on a copy of the tree because it's destructive + const treeCopy = tree.clone(); + let leavesRemoved = false; + + do { + const leavesCopy = treeCopy.getLeaves(); + leavesRemoved = false; // Reset flag for each iteration + + leavesCopy.forEach((leaf) => { + if (!filteredRows.find(({ full_id }) => full_id === leaf.id)) { + leaf.remove(true); // remove leaf and remove any dangling ancestors + leavesRemoved = true; // A leaf was removed, so set flag to true + } + }); + } while (leavesRemoved); // Continue looping if any leaf was removed + + return treeCopy; + } + return tree; + }, [tree, leaves, filteredRows]); + + // make a newick string from the filtered tree if needed + const finalNewick = useMemo(() => { + if (filteredTree === tree && treeResponse != null) { + return treeResponse.newick; // no filtering so return what we read from the back end + } else if ( + filteredTree != null && + filteredRows != null && + filteredRows.length > 0 + ) { + return filteredTree.toNewick(); // make new newick data from the filtered tree + } else return; + }, [filteredTree, treeResponse, tree, filteredRows]); + if (treeResponse == null || leaves == null || sortedRows == null) return ; @@ -137,7 +176,7 @@ export function RecordTable_Sequences( }; const treeProps = { - data: treeResponse.newick, + data: finalNewick, width: 200, highlightMode: 'monophyletic' as const, highlightedNodeIds: highlightedNodes, @@ -160,7 +199,6 @@ export function RecordTable_Sequences( rowHeight={rowHeight} treeProps={treeProps} tableProps={mesaState} - hideTree={!!filteredRows} /> diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts index 4b7b06bb10..c6a6e56d57 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/patristic.d.ts @@ -14,6 +14,8 @@ declare module 'patristic' { ancestors(): Branch[]; clone(): Branch; getLeaves(): Branch[]; + remove(pruneAncestors: boolean): Branch[]; + toNewick(): string; } export function parseNewick(newickStr: string): Branch; From 0a15ccbd25420d01410f1277041dccdbca631e39 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 20:18:23 +0000 Subject: [PATCH 27/92] tree highlighting works with filtering now, dependency issues fixed --- .../tidytree/HorizontalDendrogram.tsx | 5 +++-- .../src/components/tidytree/TreeTable.tsx | 4 +++- .../js/client/records/Sequences.tsx | 20 +++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx index 1b80c21a10..14243d6dd0 100644 --- a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx +++ b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx @@ -102,7 +102,7 @@ export function HorizontalDendrogram({ 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 @@ -127,7 +127,8 @@ export function HorizontalDendrogram({ }); // 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 ( diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 9e174a1edb..1683b7486a 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -35,6 +35,8 @@ export interface TreeTableProps { hideTree?: boolean; } +const margin: [number, number, number, number] = [0, 10, 0, 10]; + /** * main props are * data: string; // Newick format tree @@ -93,7 +95,7 @@ export default function TreeTable(props: TreeTableProps) { {...props.treeProps} rowHeight={rowHeight} leafCount={filteredRows?.length ?? rows.length} - options={{ margin: [0, 10, 0, 10], interactive: false }} + options={{ margin, interactive: false }} /> )}
; +const treeWidth = 200; export function RecordTable_Sequences( props: WrappedComponentProps @@ -177,7 +178,7 @@ export function RecordTable_Sequences( const treeProps = { data: finalNewick, - width: 200, + width: treeWidth, highlightMode: 'monophyletic' as const, highlightedNodeIds: highlightedNodes, }; @@ -188,17 +189,20 @@ export function RecordTable_Sequences( return ( <> - +
+ +
From 0c8639b6b17236bc9bf5ee135fc931ecac58f585 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 25 Mar 2024 21:55:13 +0000 Subject: [PATCH 28/92] add core/peripheral filtering --- .../src/components/tidytree/TreeTable.tsx | 1 - .../components/widgets/RadioButtonGroup.tsx | 5 +- .../js/client/records/Sequences.tsx | 50 +++++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 1683b7486a..b19be1a5a7 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -105,7 +105,6 @@ export default function TreeTable(props: TreeTableProps) { width: 1 /* arbitrary non-zero width seems necessary for flex */, '.DataTable': { marginBottom: '0px !important', - width: '80%', }, }} > diff --git a/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx b/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx index dd39fd1654..015bce73b2 100755 --- a/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx +++ b/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx @@ -34,6 +34,8 @@ export type RadioButtonGroupProps = { itemMarginRight?: number | string; /** disabled list to disable radio button item(s): grayed out */ disabledList?: string[]; + /** capitalize of the labels; default: true */ + capitalizeLabels?: boolean; }; /** @@ -54,6 +56,7 @@ export default function RadioButtonGroup({ margins, itemMarginRight, disabledList, + capitalizeLabels = true, }: RadioButtonGroupProps) { // perhaps not using focused? // const [focused, setFocused] = useState(false); @@ -127,7 +130,7 @@ export default function RadioButtonGroup({ marginRight: itemMarginRight, fontSize: '0.75em', fontWeight: 400, - textTransform: 'capitalize', + textTransform: capitalizeLabels ? 'capitalize' : undefined, minWidth: minWidth, }} /> diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index ab0a0da741..e7554c2736 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -13,8 +13,21 @@ import { groupBy } from 'lodash'; import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; +import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; type RowType = Record; +const CorePeripheralFilterStates = ['both', 'core', 'peripheral'] as const; +type CorePeripheralFilterState = typeof CorePeripheralFilterStates[number]; + +const CorePeripheralFilterStateLabels: Record< + CorePeripheralFilterState, + string +> = { + both: 'Core & Peripheral', + core: 'Core only', + peripheral: 'Peripheral only', +}; + const treeWidth = 200; export function RecordTable_Sequences( @@ -26,6 +39,9 @@ export function RecordTable_Sequences( [searchQuery] ); + const [corePeripheralFilterState, setCorePeripheralFilterState] = + useState('both'); + const groupName = props.record.id.find( ({ name }) => name === 'group_name' )?.value; @@ -101,10 +117,16 @@ export function RecordTable_Sequences( // can't memoize this easily after the early return for null treeResponse above :-( const filteredRows = useMemo( () => - searchQuery !== '' - ? sortedRows?.filter((row) => rowMatch(row, safeSearchRegexp)) + searchQuery !== '' || corePeripheralFilterState !== 'both' + ? sortedRows?.filter( + (row) => + (searchQuery === '' || rowMatch(row, safeSearchRegexp)) && + (corePeripheralFilterState === 'both' || + ((row['core_peripheral'] as string) ?? '').toLowerCase() === + (corePeripheralFilterState as string)) + ) : undefined, - [searchQuery, safeSearchRegexp, sortedRows] + [searchQuery, safeSearchRegexp, sortedRows, corePeripheralFilterState] ); // now filter the tree if needed. @@ -189,7 +211,16 @@ export function RecordTable_Sequences( return ( <> -
+
+ CorePeripheralFilterStateLabels[s] + )} + selectedOption={corePeripheralFilterState} + onOptionSelected={(newOption: string) => + setCorePeripheralFilterState(newOption as CorePeripheralFilterState) + } + capitalizeLabels={false} + />
Date: Mon, 25 Mar 2024 23:04:34 +0000 Subject: [PATCH 29/92] added pfam legend - needs wiring still --- .../js/client/records/Sequences.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index e7554c2736..7d787e143c 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -14,6 +14,8 @@ import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/Pfa import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; +import Mesa from '../../../../../../../libs/coreui/lib/components/Mesa'; +import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; type RowType = Record; const CorePeripheralFilterStates = ['both', 'core', 'peripheral'] as const; @@ -42,6 +44,8 @@ export function RecordTable_Sequences( const [corePeripheralFilterState, setCorePeripheralFilterState] = useState('both'); + const [pfamFilterIds, setPfamFilterIds] = useState([]); + const groupName = props.record.id.find( ({ name }) => name === 'group_name' )?.value; @@ -72,6 +76,17 @@ export function RecordTable_Sequences( const proteinPfams = props.record.tables['ProteinPFams']; const rowsByAccession = groupBy(proteinPfams, 'full_id'); + const accessionToPfamIds = useMemo( + () => + proteinPfams.reduce((map, row) => { + const full_id = row['full_id'] as string; + if (!map.has(full_id)) map.set(full_id, new Set()); + map.set(full_id, map.get(full_id)!.add(row['accession'] as string)); + return map; + }, new Map>()), + [proteinPfams] + ); + mesaColumns.unshift({ key: 'pfamArchitecture', name: 'Domain architecture (all drawn to same length)', @@ -209,8 +224,59 @@ export function RecordTable_Sequences( const clustalDisabled = highlightedNodes == null || highlightedNodes.length < 2; + const pfamMesaState = { + options: { + isRowSelected: (row: RowType) => + pfamFilterIds.includes(row.accession as string), + }, + rows: props.record.tables['PFams'], + columns: [ + { + key: 'accession', + name: 'PFam accession', + }, + { + key: 'symbol', + name: 'Symbol', + }, + { + key: 'description', + name: 'Description', + }, + { + key: 'num_proteins', + name: 'Count', + helpText: 'Number of proteins that contain this domain', + }, + { + key: 'legend', + name: 'Legend', + renderCell: (cellProps: { row: RowType }) => { + const pfamId = cellProps.row.accession as string; + const symbol = cellProps.row.symbol as string; + return ; + }, + }, + ], + eventHandlers: { + onRowSelect: (row: RowType) => + setPfamFilterIds((prev) => [...prev, row.accession as string]), + onRowDeselect: (row: RowType) => + setPfamFilterIds((prev) => prev.filter((id) => id !== row.accession)), + }, + }; + return ( <> +
+ +
Date: Mon, 25 Mar 2024 23:30:01 +0000 Subject: [PATCH 30/92] wired up pfam checkboxes to table search --- .../js/client/records/Sequences.tsx | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 7d787e143c..b85982270a 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -129,20 +129,44 @@ export function RecordTable_Sequences( [leaves, mesaRows] ); - // can't memoize this easily after the early return for null treeResponse above :-( - const filteredRows = useMemo( - () => - searchQuery !== '' || corePeripheralFilterState !== 'both' - ? sortedRows?.filter( - (row) => - (searchQuery === '' || rowMatch(row, safeSearchRegexp)) && - (corePeripheralFilterState === 'both' || - ((row['core_peripheral'] as string) ?? '').toLowerCase() === - (corePeripheralFilterState as string)) - ) - : undefined, - [searchQuery, safeSearchRegexp, sortedRows, corePeripheralFilterState] - ); + // filter the rows of the table based on + // 1. user-entered text search + // 2. core-peripheral radio button + // 3. checked boxes in the Pfam legend + const filteredRows = useMemo(() => { + if ( + searchQuery !== '' || + corePeripheralFilterState !== 'both' || + pfamFilterIds.length > 0 + ) { + return sortedRows?.filter((row) => { + const rowCorePeripheral = ( + (row['core_peripheral'] as string) ?? '' + ).toLowerCase(); + const rowFullId = row['full_id'] as string; + const rowPfamIdsSet = accessionToPfamIds.get(rowFullId); + + const searchMatch = + searchQuery === '' || rowMatch(row, safeSearchRegexp); + const corePeripheralMatch = + corePeripheralFilterState === 'both' || + rowCorePeripheral === corePeripheralFilterState; + const pfamIdMatch = + pfamFilterIds.length === 0 || + pfamFilterIds.some((pfamId) => rowPfamIdsSet?.has(pfamId)); + + return searchMatch && corePeripheralMatch && pfamIdMatch; + }); + } + return undefined; + }, [ + searchQuery, + safeSearchRegexp, + sortedRows, + corePeripheralFilterState, + accessionToPfamIds, + pfamFilterIds, + ]); // now filter the tree if needed. const filteredTree = useMemo(() => { From 0599d1b0c2b23183496cb6a9c8482b664016e60b Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 26 Mar 2024 22:57:50 +0000 Subject: [PATCH 31/92] added the row count --- .../js/client/records/Sequences.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index b85982270a..259b6cd534 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -14,7 +14,9 @@ import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/Pfa import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; -import Mesa from '../../../../../../../libs/coreui/lib/components/Mesa'; +import Mesa, { + RowCounter, +} from '../../../../../../../libs/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; type RowType = Record; @@ -226,6 +228,7 @@ export function RecordTable_Sequences( isRowSelected: (row: RowType) => highlightedNodes.includes(row.full_id as string), }, + uiState: {}, rows: sortedRows, filteredRows, columns: mesaColumns, @@ -290,6 +293,8 @@ export function RecordTable_Sequences( }, }; + const rowCount = (filteredRows ?? sortedRows).length; + return ( <>
+
+
+ +
+
Date: Tue, 26 Mar 2024 23:09:05 +0000 Subject: [PATCH 32/92] conditionally render pfam legend and add heading --- .../js/client/records/Sequences.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 259b6cd534..904c22e1c2 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -251,12 +251,13 @@ export function RecordTable_Sequences( const clustalDisabled = highlightedNodes == null || highlightedNodes.length < 2; + const pfamRows = props.record.tables['PFams']; const pfamMesaState = { options: { isRowSelected: (row: RowType) => pfamFilterIds.includes(row.accession as string), }, - rows: props.record.tables['PFams'], + rows: pfamRows, columns: [ { key: 'accession', @@ -276,8 +277,8 @@ export function RecordTable_Sequences( helpText: 'Number of proteins that contain this domain', }, { - key: 'legend', - name: 'Legend', + key: 'graphic', + name: 'Graphic', renderCell: (cellProps: { row: RowType }) => { const pfamId = cellProps.row.accession as string; const symbol = cellProps.row.symbol as string; @@ -297,15 +298,20 @@ export function RecordTable_Sequences( return ( <> -
- -
+ {pfamRows.length > 0 && ( +
+
+

PFam legend

+ +
+
+ )}
Date: Wed, 27 Mar 2024 14:03:15 -0400 Subject: [PATCH 33/92] Remove vert scrollbar from tidytree table --- .../client/records/GroupRecordClasses.GroupRecordClass.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.scss b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.scss index 5761e1de28..77e3475077 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.scss +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.scss @@ -16,4 +16,10 @@ min-width: 0.41666666666em; } } + + #Sequences .MesaComponent > .DataTable { + max-width: unset; + width: unset; + overflow-x: unset; + } } From c0cb043b58115a5d9dd50818454586e6723aa480 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 29 May 2024 14:43:45 +0100 Subject: [PATCH 34/92] remove code that collapsed the main section of the orthogroup page --- .../client/store-modules/RecordStoreModule.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/store-modules/RecordStoreModule.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/store-modules/RecordStoreModule.ts index b5599ce329..11431ea122 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/store-modules/RecordStoreModule.ts +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/store-modules/RecordStoreModule.ts @@ -9,30 +9,6 @@ import { export const getAllFields = RecordStoreModule.getAllFields; -export function reduce( - state = {} as RecordStoreModule.State, - action: Action -): RecordStoreModule.State { - const nextState = RecordStoreModule.reduce(state, action); - - switch (action.type) { - case RecordActions.RECORD_RECEIVED: - return action.payload.recordClass.urlSegment === 'group' - ? { - ...nextState, - collapsedSections: RecordStoreModule.getAllFields(nextState).filter( - (name) => - name === SEQUENCES_TABLE_NAME || - name === PROTEIN_PFAMS_TABLE_NAME - ), - } - : nextState; - - default: - return nextState; - } -} - const { observeNavigationVisibilityPreference, observeNavigationVisibilityState, From 3e47e2cfd94770ac9f67ed545754fb8ec0657472 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 29 May 2024 23:32:47 +0100 Subject: [PATCH 35/92] no longer request trees for 1 or 2 sequences; fix RowCounter props change; fix some imports --- .../js/client/records/Sequences.tsx | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 904c22e1c2..fe5b7fcd6e 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -8,15 +8,16 @@ import { } from '@veupathdb/wdk-client/lib/Components'; import { parseNewick } from 'patristic'; import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; -import { MesaColumn } from '../../../../../../../libs/coreui/lib/components/Mesa/types'; +import { + MesaColumn, + MesaStateProps, +} from '@veupathdb/coreui/lib/components/Mesa/types'; import { groupBy } from 'lodash'; import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; -import Mesa, { - RowCounter, -} from '../../../../../../../libs/coreui/lib/components/Mesa'; +import Mesa, { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; type RowType = Record; @@ -33,6 +34,7 @@ const CorePeripheralFilterStateLabels: Record< }; const treeWidth = 200; +const MIN_SEQUENCES_FOR_TREE = 3; export function RecordTable_Sequences( props: WrappedComponentProps @@ -56,11 +58,6 @@ export function RecordTable_Sequences( throw new Error('groupName is required but was not found in the record.'); } - const treeResponse = useOrthoService( - (orthoService) => orthoService.getGroupTree(groupName), - [groupName] - ); - const [highlightedNodes, setHighlightedNodes] = useState([]); const mesaColumns: MesaColumn[] = props.table.attributes @@ -74,6 +71,14 @@ export function RecordTable_Sequences( const mesaRows = props.value; + const numSequences = mesaRows.length; + const treeResponse = useOrthoService( + numSequences >= MIN_SEQUENCES_FOR_TREE + ? (orthoService) => orthoService.getGroupTree(groupName) + : () => Promise.resolve(undefined), // avoid making a request we know will fail and cause a "We're sorry, something went wrong." modal + [groupName, numSequences] + ); + // deal with Pfam domain architectures const proteinPfams = props.record.tables['ProteinPFams']; const rowsByAccession = groupBy(proteinPfams, 'full_id'); @@ -122,12 +127,14 @@ export function RecordTable_Sequences( ); const leaves = useMemo(() => tree?.getLeaves(), [tree]); - // sort the table in the same order as the tree's leaves + // sort the table in the same order as the tree's leaves (if we have a tree) const sortedRows = useMemo( () => - leaves - ?.map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) - .filter((row): row is RowType => row != null), + leaves != null + ? leaves + .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) + .filter((row): row is RowType => row != null) + : mesaRows, [leaves, mesaRows] ); @@ -209,10 +216,10 @@ export function RecordTable_Sequences( } else return; }, [filteredTree, treeResponse, tree, filteredRows]); - if (treeResponse == null || leaves == null || sortedRows == null) + if (numSequences >= MIN_SEQUENCES_FOR_TREE && treeResponse == null) return ; - if (leaves?.length !== sortedRows?.length) + if (leaves != null && leaves.length !== sortedRows?.length) return ( ); - const mesaState = { + const mesaState: MesaStateProps = { options: { isRowSelected: (row: RowType) => highlightedNodes.includes(row.full_id as string), @@ -252,11 +259,12 @@ export function RecordTable_Sequences( highlightedNodes == null || highlightedNodes.length < 2; const pfamRows = props.record.tables['PFams']; - const pfamMesaState = { + const pfamMesaState: MesaStateProps = { options: { isRowSelected: (row: RowType) => pfamFilterIds.includes(row.accession as string), }, + uiState: {}, rows: pfamRows, columns: [ { @@ -331,10 +339,7 @@ export function RecordTable_Sequences( />
- +
Date: Tue, 4 Jun 2024 21:36:58 +0100 Subject: [PATCH 36/92] fudge to sort table based on dodgy colon-containing full_ids --- .../wdkCustomization/js/client/records/Sequences.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index fe5b7fcd6e..20ae3be297 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -132,7 +132,17 @@ export function RecordTable_Sequences( () => leaves != null ? leaves - .map(({ id }) => mesaRows.find(({ full_id }) => full_id === id)) + .map(({ id }) => + mesaRows.find(({ full_id }) => { + // Some full_ids end in :RNA + // However, the Newick files seem to be omitting the colon and everything following it. + // (Colons are part of Newick format.) + // So we remove anything after a ':' and hope it works! + // This is the only place where we use the IDs from the tree file. + const truncated_id = (full_id as string).split(':')[0]; + return truncated_id === id; + }) + ) .filter((row): row is RowType => row != null) : mesaRows, [leaves, mesaRows] From 8ff24e2cfb0c20a72d15bcffe4c47fd195202dbf Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 6 Jun 2024 11:18:46 +0100 Subject: [PATCH 37/92] make tree darker and make tree/table check more robust --- .../src/components/tidytree/HorizontalDendrogram.tsx | 1 + .../libs/components/src/components/tidytree/tidytree.d.ts | 1 + .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx index 14243d6dd0..ecf7398e61 100644 --- a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx +++ b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx @@ -124,6 +124,7 @@ export function HorizontalDendrogram({ branchColorMode: highlightMode ?? 'none', leavesOnly: true, predicate: (node) => highlightedNodeIds.includes(node.__data__.data.id), + defaultBranchColor: '#333', }); // no redraw needed, setColorOptions does it } diff --git a/packages/libs/components/src/components/tidytree/tidytree.d.ts b/packages/libs/components/src/components/tidytree/tidytree.d.ts index 254f32e428..c2be3d971f 100644 --- a/packages/libs/components/src/components/tidytree/tidytree.d.ts +++ b/packages/libs/components/src/components/tidytree/tidytree.d.ts @@ -15,6 +15,7 @@ interface ColorOptions { branchColorMode: 'monophyletic' | 'none'; highlightColor?: string; defaultNodeColor?: string; + defaultBranchColor?: string; } declare module 'tidytree' { diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 20ae3be297..702e311752 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -229,13 +229,13 @@ export function RecordTable_Sequences( if (numSequences >= MIN_SEQUENCES_FOR_TREE && treeResponse == null) return ; - if (leaves != null && leaves.length !== sortedRows?.length) + if (mesaRows?.length !== sortedRows?.length) return ( ); From fcdddbd390cad304dde83be6764eaf0b76af08d3 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 6 Jun 2024 12:28:06 +0100 Subject: [PATCH 38/92] provide PFam descriptions in domain cartoon tooltips --- .../pfam-domains/PfamDomainArchitecture.tsx | 19 +++++++++++++++---- .../js/client/records/Sequences.tsx | 13 ++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/pfam-domains/PfamDomainArchitecture.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/pfam-domains/PfamDomainArchitecture.tsx index bc99fcc5c0..a9ed32ae1d 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/pfam-domains/PfamDomainArchitecture.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/pfam-domains/PfamDomainArchitecture.tsx @@ -7,6 +7,7 @@ import './PfamDomainArchitecture.scss'; interface Props { length: number; domains: { start: number; end: number; pfamId: string }[]; + pfamDescriptions?: Map; style?: React.CSSProperties; } @@ -16,7 +17,12 @@ export interface Domain { pfamId: string; } -export function PfamDomainArchitecture({ length, domains, style }: Props) { +export function PfamDomainArchitecture({ + length, + domains, + style, + pfamDescriptions, +}: Props) { return (
@@ -24,7 +30,7 @@ export function PfamDomainArchitecture({ length, domains, style }: Props) { ))} @@ -32,8 +38,13 @@ export function PfamDomainArchitecture({ length, domains, style }: Props) { ); } -function makeDomainTitle({ start, end, pfamId }: Domain) { - return `${pfamId} (location: [${start} - ${end}])`; +function makeDomainTitle( + { start, end, pfamId }: Domain, + pfamDescriptions?: Map +) { + if (pfamDescriptions != null) + return `${pfamId} (${pfamDescriptions.get(pfamId)}) [${start} - ${end}]`; + else return `${pfamId} (location: [${start} - ${end}])`; } function makeDomainPositionStyling( diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 702e311752..44645d6d8a 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -70,6 +70,7 @@ export function RecordTable_Sequences( .filter(({ key }) => key !== 'clustalInput' && key !== 'sequence_link'); const mesaRows = props.value; + const pfamRows = props.record.tables['PFams']; const numSequences = mesaRows.length; const treeResponse = useOrthoService( @@ -94,6 +95,16 @@ export function RecordTable_Sequences( [proteinPfams] ); + const pfamIdToDescription = useMemo( + () => + pfamRows.reduce((map, row) => { + const pfamId = row.accession as string; + const description = row.description as string; + return map.set(pfamId, description); + }, new Map()), + [pfamRows] + ); + mesaColumns.unshift({ key: 'pfamArchitecture', name: 'Domain architecture (all drawn to same length)', @@ -110,6 +121,7 @@ export function RecordTable_Sequences( style={{ width: '150px', top: '10px' }} length={proteinLength} domains={pfamDomains} + pfamDescriptions={pfamIdToDescription} /> ); } else { @@ -268,7 +280,6 @@ export function RecordTable_Sequences( const clustalDisabled = highlightedNodes == null || highlightedNodes.length < 2; - const pfamRows = props.record.tables['PFams']; const pfamMesaState: MesaStateProps = { options: { isRowSelected: (row: RowType) => From 032c479e0a0100915d7eb2540f9b8a7dbea055a3 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 6 Jun 2024 14:38:15 +0100 Subject: [PATCH 39/92] sort out row counts --- .../wdkCustomization/js/client/records/Sequences.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 44645d6d8a..47886623e6 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -360,7 +360,13 @@ export function RecordTable_Sequences( />
- +
Date: Tue, 11 Jun 2024 15:47:06 +0100 Subject: [PATCH 40/92] add better console logging for tree/table mismatches --- .../js/client/records/Sequences.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 47886623e6..0b9b8f0b01 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -12,7 +12,7 @@ import { MesaColumn, MesaStateProps, } from '@veupathdb/coreui/lib/components/Mesa/types'; -import { groupBy } from 'lodash'; +import { groupBy, difference } from 'lodash'; import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; @@ -241,7 +241,14 @@ export function RecordTable_Sequences( if (numSequences >= MIN_SEQUENCES_FOR_TREE && treeResponse == null) return ; - if (mesaRows?.length !== sortedRows?.length) + if (mesaRows?.length !== sortedRows?.length) { + console.log( + 'Tree and protein list mismatch. A=Tree, B=Table. Summary below:' + ); + summarizeIDMismatch( + (leaves ?? []).map((leaf) => leaf.id), + mesaRows.map((row) => row.full_id as string) + ); return ( ); + } const mesaState: MesaStateProps = { options: { @@ -448,3 +456,21 @@ function createSafeSearchRegExp(input: string): RegExp { return new RegExp(escapedInput, 'i'); } } + +function summarizeIDMismatch(A: string[], B: string[]) { + const inAButNotB = difference(A, B); + const inBButNotA = difference(B, A); + + console.log(`Total unique IDs in A: ${new Set(A).size}`); + console.log(`Total unique IDs in B: ${new Set(B).size}`); + + console.log(`Number of IDs in A but not in B: ${inAButNotB.length}`); + console.log( + `First few IDs in A but not in B: ${inAButNotB.slice(0, 5).join(', ')}` + ); + + console.log(`Number of IDs in B but not in A: ${inBButNotA.length}`); + console.log( + `First few IDs in B but not in A: ${inBButNotA.slice(0, 5).join(', ')}` + ); +} From e6904a137269cf9c1bd3c501e65a3b9b213027fd Mon Sep 17 00:00:00 2001 From: Bob Date: Tue, 11 Jun 2024 16:05:40 +0100 Subject: [PATCH 41/92] made tree-table comparison for console more robust --- .../js/client/records/Sequences.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 0b9b8f0b01..6bfb4f623d 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -151,8 +151,9 @@ export function RecordTable_Sequences( // (Colons are part of Newick format.) // So we remove anything after a ':' and hope it works! // This is the only place where we use the IDs from the tree file. - const truncated_id = (full_id as string).split(':')[0]; - return truncated_id === id; + return ( + truncate_full_id_for_tree_comparison(full_id as string) === id + ); }) ) .filter((row): row is RowType => row != null) @@ -247,7 +248,9 @@ export function RecordTable_Sequences( ); summarizeIDMismatch( (leaves ?? []).map((leaf) => leaf.id), - mesaRows.map((row) => row.full_id as string) + mesaRows.map((row) => + truncate_full_id_for_tree_comparison(row.full_id as string) + ) ); return ( Date: Wed, 19 Jun 2024 15:53:38 +0100 Subject: [PATCH 42/92] add inlineUseTooltips options prop but not working fully yet --- .../components/src/components/tidytree/TreeTable.tsx | 1 + .../libs/coreui/src/components/Mesa/Ui/DataRow.jsx | 11 ++++++----- packages/libs/coreui/src/components/Mesa/types.ts | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index b19be1a5a7..98046114c8 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -81,6 +81,7 @@ export default function TreeTable(props: TreeTableProps) { ...props.tableProps.options, deriveRowClassName: (_) => rowStyleClassName, inline: true, + inlineUseTooltips: true, inlineMaxHeight: `${rowHeight}px`, inlineMaxWidth: `${maxColumnWidth}px`, }, diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx index d53ec90a5d..09b3886d80 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx @@ -30,22 +30,23 @@ 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 }); + console.log({ options }); + if (inline && !inlineUseTooltips) + this.setState({ expanded: !this.state.expanded }); if (typeof onRowClick === 'function') onRowClick(row, rowIndex); } diff --git a/packages/libs/coreui/src/components/Mesa/types.ts b/packages/libs/coreui/src/components/Mesa/types.ts index 54e792ae5a..d8290c7870 100644 --- a/packages/libs/coreui/src/components/Mesa/types.ts +++ b/packages/libs/coreui/src/components/Mesa/types.ts @@ -36,6 +36,7 @@ export interface MesaStateProps< inline?: boolean; inlineMaxWidth?: string; inlineMaxHeight?: string; + inlineUseTooltips?: boolean; // on clicking on a row, don't expand the row height-wise to show the full contents, use a tooltip instead className?: string; errOnOverflow?: boolean; editableColumns?: boolean; From 9ecec3d61286d4051c424d01c2746bafa1f23a34 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 19 Jun 2024 21:21:28 +0100 Subject: [PATCH 43/92] attempted better handling of very large groups --- .../js/client/records/Sequences.tsx | 143 +++++++++++++----- 1 file changed, 104 insertions(+), 39 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 6bfb4f623d..c64ba21466 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; @@ -6,8 +6,11 @@ import { Loading, RealTimeSearchBox, } from '@veupathdb/wdk-client/lib/Components'; -import { parseNewick } from 'patristic'; -import { AttributeValue } from '@veupathdb/wdk-client/lib/Utils/WdkModel'; +import { Branch, parseNewick } from 'patristic'; +import { + AttributeValue, + TableValue, +} from '@veupathdb/wdk-client/lib/Utils/WdkModel'; import { MesaColumn, MesaStateProps, @@ -35,6 +38,7 @@ const CorePeripheralFilterStateLabels: Record< const treeWidth = 200; const MIN_SEQUENCES_FOR_TREE = 3; +const MAX_SEQUENCES_TO_SHOW_ALL = 2000; export function RecordTable_Sequences( props: WrappedComponentProps @@ -45,9 +49,6 @@ export function RecordTable_Sequences( [searchQuery] ); - const [corePeripheralFilterState, setCorePeripheralFilterState] = - useState('both'); - const [pfamFilterIds, setPfamFilterIds] = useState([]); const groupName = props.record.id.find( @@ -73,6 +74,13 @@ export function RecordTable_Sequences( const pfamRows = props.record.tables['PFams']; const numSequences = mesaRows.length; + + // show only core as default for large groups + const [corePeripheralFilterState, setCorePeripheralFilterState] = + useState( + numSequences > MAX_SEQUENCES_TO_SHOW_ALL ? 'core' : 'both' + ); + const treeResponse = useOrthoService( numSequences >= MIN_SEQUENCES_FOR_TREE ? (orthoService) => orthoService.getGroupTree(groupName) @@ -130,36 +138,39 @@ export function RecordTable_Sequences( }, }); - // do some validation on the tree w.r.t. the table + // parse the tree and other expensive processing asynchronously + const [tree, setTree] = useState(); + const [leaves, setLeaves] = useState(); + const [sortedRows, setSortedRows] = useState(); + + useEffect(() => { + if (!treeResponse) return; + + let isMounted = true; + + const fetchTree = async () => { + try { + const parsedTree = await parseNewickAsync(treeResponse.newick); + const leaves = await getLeavesAsync(parsedTree); + const sortedRows = await sortRowsAsync(leaves, mesaRows); + if (isMounted) { + setTree(parsedTree); + setLeaves(leaves); + setSortedRows(sortedRows); + } + } catch (error) { + console.error('Error parsing Newick:', error); + } + }; - // should this be async? it's potentially expensive - const tree = useMemo( - () => treeResponse && parseNewick(treeResponse.newick), - [treeResponse] - ); - const leaves = useMemo(() => tree?.getLeaves(), [tree]); + fetchTree(); - // sort the table in the same order as the tree's leaves (if we have a tree) - const sortedRows = useMemo( - () => - leaves != null - ? leaves - .map(({ id }) => - mesaRows.find(({ full_id }) => { - // Some full_ids end in :RNA - // However, the Newick files seem to be omitting the colon and everything following it. - // (Colons are part of Newick format.) - // So we remove anything after a ':' and hope it works! - // This is the only place where we use the IDs from the tree file. - return ( - truncate_full_id_for_tree_comparison(full_id as string) === id - ); - }) - ) - .filter((row): row is RowType => row != null) - : mesaRows, - [leaves, mesaRows] - ); + return () => { + isMounted = false; + }; + }, [treeResponse, mesaRows]); + + // do some validation on the tree w.r.t. the table // filter the rows of the table based on // 1. user-entered text search @@ -200,7 +211,7 @@ export function RecordTable_Sequences( pfamFilterIds, ]); - // now filter the tree if needed. + // now filter the tree if needed - takes a couple of seconds for large trees const filteredTree = useMemo(() => { if (leaves == null || tree == null || filteredRows?.length === 0) return; @@ -208,7 +219,6 @@ export function RecordTable_Sequences( // must work on a copy of the tree because it's destructive const treeCopy = tree.clone(); let leavesRemoved = false; - do { const leavesCopy = treeCopy.getLeaves(); leavesRemoved = false; // Reset flag for each iteration @@ -239,10 +249,28 @@ export function RecordTable_Sequences( } else return; }, [filteredTree, treeResponse, tree, filteredRows]); - if (numSequences >= MIN_SEQUENCES_FOR_TREE && treeResponse == null) - return ; + const largeGroupWarning = + numSequences > MAX_SEQUENCES_TO_SHOW_ALL + ? '(note: this is a large group, only the core sequences will be shown by default)' + : ''; + if ( + !sortedRows || + (numSequences >= MIN_SEQUENCES_FOR_TREE && + (tree == null || treeResponse == null)) + ) { + return ( + <> +
Loading... {largeGroupWarning}
+ + + ); // The loading spinner does not show :-( + } - if (mesaRows?.length !== sortedRows?.length) { + if ( + mesaRows != null && + sortedRows != null && + mesaRows.length !== sortedRows.length + ) { console.log( 'Tree and protein list mismatch. A=Tree, B=Table. Summary below:' ); @@ -482,3 +510,40 @@ function truncate_full_id_for_tree_comparison(full_id: string): string { const truncated_id = (full_id as string).split(':')[0]; return truncated_id; } + +async function parseNewickAsync(treeResponse: string): Promise { + return new Promise((resolve) => { + const result = parseNewick(treeResponse); + resolve(result); + }); +} + +async function getLeavesAsync(tree: Branch): Promise { + return new Promise((resolve) => { + const result = tree.getLeaves(); + resolve(result); + }); +} + +async function sortRowsAsync( + leaves: Branch[], + mesaRows: TableValue +): Promise { + if (leaves == null) return mesaRows; + + return new Promise((resolve) => { + const result = leaves + .map(({ id }) => + mesaRows.find(({ full_id }) => { + // Some full_ids end in :RNA + // However, the Newick files seem to be omitting the colon and everything following it. + // (Colons are part of Newick format.) + // So we remove anything after a ':' and hope it works! + // This is the only place where we use the IDs from the tree file. + return truncate_full_id_for_tree_comparison(full_id as string) === id; + }) + ) + .filter((row): row is RowType => row != null); + resolve(result); + }); +} From 30da70a6b385f7466a075a42d2ecaebd6ad1e092 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 21 Jun 2024 11:50:50 +0100 Subject: [PATCH 44/92] looks good now --- .../src/components/Mesa/Ui/DataCell.jsx | 26 ++++++++++++++++++- .../coreui/src/components/Mesa/Ui/DataRow.jsx | 1 - .../libs/coreui/src/components/Mesa/types.ts | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx index 468227edc0..f8136527aa 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataCell.jsx @@ -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 { @@ -64,7 +66,29 @@ 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 columnName = column.name ?? column.key; + + // Ideally the tooltip would also be conditional on there + // being actual content, but this is not trivial without + // copy-pasting the getValue logic from this.renderContent(). + // Verdict: not worth it + const children = options.inlineUseTooltips ? ( + + {columnName && {columnName}:} + {content} + + } + > + {content} + + ) : ( + content + ); + const props = { style, children, diff --git a/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx b/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx index 09b3886d80..6382798461 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/DataRow.jsx @@ -44,7 +44,6 @@ class DataRow extends React.PureComponent { const { row, rowIndex, options } = this.props; const { inline, onRowClick, inlineUseTooltips } = options; if (!inline && !onRowClick) return; - console.log({ options }); if (inline && !inlineUseTooltips) this.setState({ expanded: !this.state.expanded }); if (typeof onRowClick === 'function') onRowClick(row, rowIndex); diff --git a/packages/libs/coreui/src/components/Mesa/types.ts b/packages/libs/coreui/src/components/Mesa/types.ts index d8290c7870..b7bb7fc266 100644 --- a/packages/libs/coreui/src/components/Mesa/types.ts +++ b/packages/libs/coreui/src/components/Mesa/types.ts @@ -36,7 +36,7 @@ export interface MesaStateProps< inline?: boolean; inlineMaxWidth?: string; inlineMaxHeight?: string; - inlineUseTooltips?: boolean; // on clicking on a row, don't expand the row height-wise to show the full contents, use a tooltip instead + inlineUseTooltips?: boolean; // don't use onClick to show the full contents, use an onMouseOver tooltip instead className?: string; errOnOverflow?: boolean; editableColumns?: boolean; From 443528ca47c9fd546087876686416fff5471b004 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 24 Jun 2024 23:11:47 +0100 Subject: [PATCH 45/92] removed full_id column (from display only) and added Accession aka sequence_link column and enabled the link --- .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index c64ba21466..a782ad51a8 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -62,13 +62,14 @@ export function RecordTable_Sequences( const [highlightedNodes, setHighlightedNodes] = useState([]); const mesaColumns: MesaColumn[] = props.table.attributes - .map(({ name, displayName }) => ({ + .map(({ name, displayName, type }) => ({ key: name, name: displayName, + type: type === 'link' ? 'wdkLink' : type, })) // and remove a raw HTML checkbox field - we'll use Mesa's built-in checkboxes for this // and an object-laden 'sequence_link' field - the ID seems to be replicated in the full_id field - .filter(({ key }) => key !== 'clustalInput' && key !== 'sequence_link'); + .filter(({ key }) => key !== 'clustalInput' && key !== 'full_id'); const mesaRows = props.value; const pfamRows = props.record.tables['PFams']; From 5de59a6b35c833672c484a852dde7d329f1046ec Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 28 Jun 2024 13:28:51 -0400 Subject: [PATCH 46/92] Use flex for label to allow for block children --- .../coreui/src/components/inputs/checkboxes/CheckboxList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxList.tsx b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxList.tsx index 9dc1c29a4a..a945e10340 100644 --- a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxList.tsx +++ b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxList.tsx @@ -205,7 +205,7 @@ export default function CheckboxList({ ) : ( -
+ {pfamDomains} Date: Tue, 2 Jul 2024 14:43:40 +0100 Subject: [PATCH 48/92] Explicit empty PFam legend --- .../js/client/records/Sequences.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index a782ad51a8..0e3d5e9899 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -367,20 +367,30 @@ export function RecordTable_Sequences( return ( <> - {pfamRows.length > 0 && ( +
-
-

PFam legend

+

PFam legend

+ {pfamRows.length > 0 ? ( -
+ ) : ( + + No data available + + )}
- )} +
Date: Fri, 12 Jul 2024 10:39:19 -0400 Subject: [PATCH 49/92] Use similar style for core/peripheral filter --- .../js/client/records/Sequences.tsx | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 34f953a19b..16b8364f11 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -78,10 +78,9 @@ export function RecordTable_Sequences( const numSequences = mesaRows.length; // show only core as default for large groups - const [corePeripheralFilterState, setCorePeripheralFilterState] = - useState( - numSequences > MAX_SEQUENCES_TO_SHOW_ALL ? 'core' : 'both' - ); + const [corePeripheralFilterValue, setCorePeripheralFilterValue] = useState< + ('core' | 'peripheral')[] + >(numSequences > MAX_SEQUENCES_TO_SHOW_ALL ? ['core'] : []); const treeResponse = useOrthoService( numSequences >= MIN_SEQUENCES_FOR_TREE @@ -181,7 +180,7 @@ export function RecordTable_Sequences( const filteredRows = useMemo(() => { if ( searchQuery !== '' || - corePeripheralFilterState !== 'both' || + corePeripheralFilterValue != null || pfamFilterIds.length > 0 ) { return sortedRows?.filter((row) => { @@ -194,8 +193,10 @@ export function RecordTable_Sequences( const searchMatch = searchQuery === '' || rowMatch(row, safeSearchRegexp); const corePeripheralMatch = - corePeripheralFilterState === 'both' || - rowCorePeripheral === corePeripheralFilterState; + corePeripheralFilterValue.length === 0 || + corePeripheralFilterValue.includes( + rowCorePeripheral.toLowerCase() as any + ); const pfamIdMatch = pfamFilterIds.length === 0 || pfamFilterIds.some((pfamId) => rowPfamIdsSet?.has(pfamId)); @@ -208,7 +209,7 @@ export function RecordTable_Sequences( searchQuery, safeSearchRegexp, sortedRows, - corePeripheralFilterState, + corePeripheralFilterValue, accessionToPfamIds, pfamFilterIds, ]); @@ -323,7 +324,7 @@ export function RecordTable_Sequences( const rowCount = (filteredRows ?? sortedRows).length; - const pfamDomains = pfamRows.length > 0 && ( + const pfamFilter = pfamRows.length > 0 && ( ({ @@ -356,6 +357,24 @@ export function RecordTable_Sequences( /> ); + const corePeripheralFilter = ( + + ); + return ( <>
- {pfamDomains} - CorePeripheralFilterStateLabels[s] - )} - selectedOption={corePeripheralFilterState} - onOptionSelected={(newOption: string) => - setCorePeripheralFilterState(newOption as CorePeripheralFilterState) - } - capitalizeLabels={false} - /> +
+ Filters: + {pfamFilter} + {corePeripheralFilter} +
Date: Tue, 16 Jul 2024 11:24:13 -0400 Subject: [PATCH 50/92] Add species filter to protein table --- .../inputs/SelectTree/SelectTree.tsx | 4 +- .../PhyleticDistributionCheckbox.tsx | 72 ++++++++++--------- .../RecordTable_TaxonCounts_Filter.tsx | 45 ++++++++++++ .../js/client/records/Sequences.tsx | 28 +++++++- 4 files changed, 112 insertions(+), 37 deletions(-) create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 19cc9c7d6e..1833b7999b 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -84,7 +84,9 @@ function SelectTree(props: SelectTreeProps) { onClose={onClose} isDisabled={props.isDisabled} > - {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree} +
+ {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree} +
); } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index e7daaac55b..bd725c6499 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -25,6 +25,7 @@ import { } from 'ortho-client/utils/taxons'; import './PhyleticDistributionCheckbox.scss'; +import { SelectTree } from '@veupathdb/coreui'; const cx = makeClassNameHelper('PhyleticDistributionCheckbox'); @@ -41,6 +42,7 @@ type SelectionConfig = | { selectable: true; onSpeciesSelected: (selection: string[]) => void; + selectedSpecies: string[]; }; export function PhyleticDistributionCheckbox({ @@ -71,40 +73,42 @@ export function PhyleticDistributionCheckbox({ ); return ( -
- - -   Hide zero counts - , - ]} - /> -
+ + +   Hide zero counts + , + ]} + /> ); } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx new file mode 100644 index 0000000000..96794e8f76 --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from 'react'; +import { RecordTableProps, WrappedComponentProps } from './Types'; +import { Loading } from '@veupathdb/wdk-client/lib/Components'; +import PopoverButton from '@veupathdb/coreui/lib/components/buttons/PopoverButton/PopoverButton'; +import { PhyleticDistributionCheckbox } from 'ortho-client/components/phyletic-distribution/PhyleticDistributionCheckbox'; +import { taxonCountsTableValueToMap } from './utils'; +import { useTaxonUiMetadata } from 'ortho-client/hooks/taxons'; + +export interface Props extends WrappedComponentProps { + selectedSpecies: string[]; + onSpeciesSelected: (taxons: string[]) => void; +} + +export function RecordTable_TaxonCounts_Filter({ + value, + selectedSpecies, + onSpeciesSelected, +}: Props) { + const selectionConfig = useMemo( + () => + ({ + selectable: true, + onSpeciesSelected, + selectedSpecies, + } as const), + [onSpeciesSelected, selectedSpecies] + ); + + const speciesCounts = useMemo( + () => taxonCountsTableValueToMap(value), + [value] + ); + + const taxonUiMetadata = useTaxonUiMetadata(); + + return taxonUiMetadata == null ? ( + + ) : ( + + ); +} diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 16b8364f11..cfd4b1a825 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -23,6 +23,7 @@ import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/Radio import Mesa, { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; import { SelectList } from '@veupathdb/coreui'; +import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; type RowType = Record; const CorePeripheralFilterStates = ['both', 'core', 'peripheral'] as const; @@ -50,6 +51,8 @@ export function RecordTable_Sequences( [searchQuery] ); + const [selectedSpecies, setSelectedSpecies] = useState([]); + const [pfamFilterIds, setPfamFilterIds] = useState([]); const groupName = props.record.id.find( @@ -181,7 +184,8 @@ export function RecordTable_Sequences( if ( searchQuery !== '' || corePeripheralFilterValue != null || - pfamFilterIds.length > 0 + pfamFilterIds.length > 0 || + selectedSpecies.length > 0 ) { return sortedRows?.filter((row) => { const rowCorePeripheral = ( @@ -200,8 +204,13 @@ export function RecordTable_Sequences( const pfamIdMatch = pfamFilterIds.length === 0 || pfamFilterIds.some((pfamId) => rowPfamIdsSet?.has(pfamId)); + const speciesMatch = + selectedSpecies.length === 0 || + selectedSpecies.some((specie) => row.taxon_abbrev === specie); - return searchMatch && corePeripheralMatch && pfamIdMatch; + return ( + searchMatch && corePeripheralMatch && pfamIdMatch && speciesMatch + ); }); } return undefined; @@ -212,6 +221,7 @@ export function RecordTable_Sequences( corePeripheralFilterValue, accessionToPfamIds, pfamFilterIds, + selectedSpecies, ]); // now filter the tree if needed - takes a couple of seconds for large trees @@ -375,6 +385,19 @@ export function RecordTable_Sequences( /> ); + const taxonFilter = + props.record.tables.TaxonCounts?.length > 0 ? ( + + ) : null; + return ( <>
Filters: {pfamFilter} {corePeripheralFilter} + {taxonFilter}
Date: Wed, 17 Jul 2024 11:18:14 +0100 Subject: [PATCH 51/92] prevent overflow in SelectTree button with many items selected; also use flex-wrap to prevent large buttons crowding the search box --- .../inputs/SelectTree/SelectTree.tsx | 20 ++++++++++++++++++- .../js/client/records/Sequences.tsx | 2 ++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 1833b7999b..b975512f4d 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -29,9 +29,27 @@ function SelectTree(props: SelectTreeProps) { onClose(); }, [shouldCloseOnSelection, selectedList]); + function truncatedButtonContent(selectedList: string[]) { + return ( + + {selectedList.join(', ')} + + ); + } + const onClose = () => { setButtonDisplayContent( - selectedList.length ? selectedList.join(', ') : props.buttonDisplayContent + selectedList.length + ? truncatedButtonContent(selectedList) + : props.buttonDisplayContent ); }; diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index cfd4b1a825..0c851186a2 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -435,6 +435,8 @@ export function RecordTable_Sequences( gap: '1em', alignItems: 'center', marginLeft: 'auto', + flexWrap: 'wrap', + justifyContent: 'flex-end', }} > Filters: From c50a85eeeec9ffa33a794ded8f5c5823b27924e9 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 17 Jul 2024 13:17:23 +0100 Subject: [PATCH 52/92] add shouldOnlyUpdateOnClose option to SelectTree --- .../inputs/SelectTree/SelectTree.tsx | 32 +++++++++++++++---- .../PhyleticDistributionCheckbox.tsx | 1 + .../RecordTable_TaxonCounts_Filter.tsx | 1 - 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index b975512f4d..3cb92b20a1 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -10,6 +10,8 @@ export interface SelectTreeProps extends CheckboxTreeProps { shouldCloseOnSelection?: boolean; wrapPopover?: (checkboxTree: ReactNode) => ReactNode; isDisabled?: boolean; + /** only update `selectedList` state when the popover closes */ + shouldOnlyUpdateOnClose?: boolean; } function SelectTree(props: SelectTreeProps) { @@ -18,7 +20,18 @@ function SelectTree(props: SelectTreeProps) { ? props.currentList.join(', ') : props.buttonDisplayContent ); - const { selectedList, shouldCloseOnSelection, wrapPopover } = props; + const { + selectedList, + onSelectionChange, + shouldCloseOnSelection, + shouldOnlyUpdateOnClose, + wrapPopover, + } = props; + + // This local state is updated whenever a checkbox is clicked in the species tree. + // When `shouldOnlyUpdateOnClose` is true, pass the final value to `onSelectionChange` when the popover closes. + // When it is false we call `onSelectionChange` whenever `localSelectedList` changes + const [localSelectedList, setLocalSelectedList] = useState(selectedList); /** Used as a hack to "auto close" the popover when shouldCloseOnSelection is true */ const [key, setKey] = useState(''); @@ -27,7 +40,13 @@ function SelectTree(props: SelectTreeProps) { if (!shouldCloseOnSelection) return; setKey(selectedList.join(', ')); onClose(); - }, [shouldCloseOnSelection, selectedList]); + }, [shouldCloseOnSelection, localSelectedList]); + + // live updates to caller when needed + useEffect(() => { + if (shouldOnlyUpdateOnClose) return; + onSelectionChange(localSelectedList); + }, [onSelectionChange, localSelectedList]); function truncatedButtonContent(selectedList: string[]) { return ( @@ -47,10 +66,11 @@ function SelectTree(props: SelectTreeProps) { const onClose = () => { setButtonDisplayContent( - selectedList.length - ? truncatedButtonContent(selectedList) + localSelectedList.length + ? truncatedButtonContent(localSelectedList) : props.buttonDisplayContent ); + if (shouldOnlyUpdateOnClose) onSelectionChange(localSelectedList); }; const checkboxTree = ( @@ -67,12 +87,12 @@ function SelectTree(props: SelectTreeProps) { renderNode={props.renderNode} expandedList={props.expandedList} isSelectable={props.isSelectable} - selectedList={selectedList} + selectedList={localSelectedList} filteredList={props.filteredList} customCheckboxes={props.customCheckboxes} isMultiPick={props.isMultiPick} name={props.name} - onSelectionChange={props.onSelectionChange} + onSelectionChange={setLocalSelectedList} currentList={props.currentList} defaultList={props.defaultList} isSearchable={props.isSearchable} diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index bd725c6499..6c71c7254b 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -89,6 +89,7 @@ export function PhyleticDistributionCheckbox({ ? selectionConfig.onSpeciesSelected : undefined } + shouldOnlyUpdateOnClose={true} selectedList={ selectionConfig.selectable ? selectionConfig.selectedSpecies : undefined } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx index 96794e8f76..55bf462d14 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/RecordTable_TaxonCounts_Filter.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { Loading } from '@veupathdb/wdk-client/lib/Components'; -import PopoverButton from '@veupathdb/coreui/lib/components/buttons/PopoverButton/PopoverButton'; import { PhyleticDistributionCheckbox } from 'ortho-client/components/phyletic-distribution/PhyleticDistributionCheckbox'; import { taxonCountsTableValueToMap } from './utils'; import { useTaxonUiMetadata } from 'ortho-client/hooks/taxons'; From 96056a2989e6efcb6f6629081a587e78716ccc77 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 17 Jul 2024 18:38:49 +0100 Subject: [PATCH 53/92] remove some unused imports --- .../PhyleticDistributionCheckbox.tsx | 4 +--- .../js/client/records/Sequences.tsx | 14 +------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index 6c71c7254b..f0b7c1b6e1 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -3,9 +3,7 @@ import React, { useMemo, useState } from 'react'; import { orderBy } from 'lodash'; import { Checkbox } from '@veupathdb/wdk-client/lib/Components'; -import CheckboxTree, { - LinksPosition, -} from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxTree/CheckboxTree'; +import { LinksPosition } from '@veupathdb/coreui/lib/components/inputs/checkboxes/CheckboxTree/CheckboxTree'; import { makeClassNameHelper } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; import { makeSearchHelpText } from '@veupathdb/wdk-client/lib/Utils/SearchUtils'; import { diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 0c851186a2..c0b4bd7215 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -19,24 +19,12 @@ import { groupBy, difference } from 'lodash'; import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; -import RadioButtonGroup from '@veupathdb/components/lib/components/widgets/RadioButtonGroup'; -import Mesa, { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; +import { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; import { SelectList } from '@veupathdb/coreui'; import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; type RowType = Record; -const CorePeripheralFilterStates = ['both', 'core', 'peripheral'] as const; -type CorePeripheralFilterState = typeof CorePeripheralFilterStates[number]; - -const CorePeripheralFilterStateLabels: Record< - CorePeripheralFilterState, - string -> = { - both: 'Core & Peripheral', - core: 'Core only', - peripheral: 'Peripheral only', -}; const treeWidth = 200; const MIN_SEQUENCES_FOR_TREE = 3; From 8c05b0c5a3bd33e6cf7a60c348a3dd5e23479ca1 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 17 Jul 2024 19:09:55 +0100 Subject: [PATCH 54/92] add MAX_SEQUENCES_FOR_TREE logic --- .../src/components/tidytree/TreeTable.tsx | 6 +++++- .../js/client/records/Sequences.tsx | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 98046114c8..7bf3e6097f 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -87,6 +87,11 @@ export default function TreeTable(props: TreeTableProps) { }, }; + // 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 (
(props: TreeTableProps) { )}
; const treeWidth = 200; const MIN_SEQUENCES_FOR_TREE = 3; +const MAX_SEQUENCES_FOR_TREE = 9999; const MAX_SEQUENCES_TO_SHOW_ALL = 2000; export function RecordTable_Sequences( @@ -74,7 +75,8 @@ export function RecordTable_Sequences( >(numSequences > MAX_SEQUENCES_TO_SHOW_ALL ? ['core'] : []); const treeResponse = useOrthoService( - numSequences >= MIN_SEQUENCES_FOR_TREE + numSequences >= MIN_SEQUENCES_FOR_TREE && + numSequences <= MAX_SEQUENCES_FOR_TREE ? (orthoService) => orthoService.getGroupTree(groupName) : () => Promise.resolve(undefined), // avoid making a request we know will fail and cause a "We're sorry, something went wrong." modal [groupName, numSequences] @@ -133,7 +135,8 @@ export function RecordTable_Sequences( // parse the tree and other expensive processing asynchronously const [tree, setTree] = useState(); const [leaves, setLeaves] = useState(); - const [sortedRows, setSortedRows] = useState(); + // default unsorted `sortedRows`! + const [sortedRows, setSortedRows] = useState(mesaRows); useEffect(() => { if (!treeResponse) return; @@ -257,6 +260,7 @@ export function RecordTable_Sequences( if ( !sortedRows || (numSequences >= MIN_SEQUENCES_FOR_TREE && + numSequences <= MAX_SEQUENCES_FOR_TREE && (tree == null || treeResponse == null)) ) { return ( @@ -388,9 +392,17 @@ export function RecordTable_Sequences( return ( <> + {numSequences > MAX_SEQUENCES_FOR_TREE && ( +
+ + Note: no phylogenetic tree is displayed because this group has more + than {MAX_SEQUENCES_FOR_TREE.toLocaleString()} sequences. + +
+ )}
Date: Wed, 17 Jul 2024 21:26:35 +0100 Subject: [PATCH 55/92] selected row highlighting --- .../tidytree/HorizontalDendrogram.tsx | 6 ++++++ .../src/components/tidytree/TreeTable.tsx | 18 +++++++++++++++++- .../js/client/records/Sequences.tsx | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx index ecf7398e61..f5ffb2e646 100644 --- a/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx +++ b/packages/libs/components/src/components/tidytree/HorizontalDendrogram.tsx @@ -55,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; } /** @@ -72,6 +76,7 @@ export function HorizontalDendrogram({ options: { ruler = false, margin = [0, 0, 0, 0], interactive = true }, highlightedNodeIds, highlightMode, + highlightColor, hStretch = 1.0, containerStyles, }: HorizontalDendrogramProps) { @@ -122,6 +127,7 @@ 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', diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index 7bf3e6097f..fb99287a5a 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -79,7 +79,10 @@ export default function TreeTable(props: TreeTableProps) { ...props.tableProps, options: { ...props.tableProps.options, - deriveRowClassName: (_) => rowStyleClassName, + deriveRowClassName: mergeDeriveRowClassName( + props.tableProps.options?.deriveRowClassName, + (_) => rowStyleClassName + ), inline: true, inlineUseTooltips: true, inlineMaxHeight: `${rowHeight}px`, @@ -118,3 +121,16 @@ export default function TreeTable(props: TreeTableProps) {
); } + +function mergeDeriveRowClassName( + func1: ((row: RowType) => string | undefined) | undefined, + func2: ((row: RowType) => string | undefined) | undefined +): ((row: RowType) => string | undefined) | undefined { + if (func1 == null && func2 == null) return undefined; + return (row: RowType) => { + const className1 = func1 && func1(row); + const className2 = func2 && func2(row); + // Combine the class names that are defined + return [className1, className2].filter(Boolean).join(' ') || undefined; + }; +} diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index b21ab55b28..f146206c35 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -23,6 +23,7 @@ import { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; import { SelectList } from '@veupathdb/coreui'; import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; +import { css, cx } from '@emotion/css'; type RowType = Record; @@ -31,6 +32,8 @@ const MIN_SEQUENCES_FOR_TREE = 3; const MAX_SEQUENCES_FOR_TREE = 9999; const MAX_SEQUENCES_TO_SHOW_ALL = 2000; +const highlightColor = '#feb640'; + export function RecordTable_Sequences( props: WrappedComponentProps ) { @@ -296,10 +299,22 @@ export function RecordTable_Sequences( ); } + const highlightedRowClassName = cx( + css` + & td { + background-color: ${highlightColor} !important; + } + ` + ); + const mesaState: MesaStateProps = { options: { isRowSelected: (row: RowType) => highlightedNodes.includes(row.full_id as string), + deriveRowClassName: (row: RowType) => + highlightedNodes.includes(row.full_id as string) + ? highlightedRowClassName + : undefined, }, uiState: {}, rows: sortedRows, @@ -317,6 +332,7 @@ export function RecordTable_Sequences( data: finalNewick, width: treeWidth, highlightMode: 'monophyletic' as const, + highlightColor, highlightedNodeIds: highlightedNodes, }; From 871583e6deb705102eccd1f49be035c0ca75a5a0 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 17 Jul 2024 21:35:44 +0100 Subject: [PATCH 56/92] tone down the row background color --- .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index f146206c35..54f66af18e 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -33,6 +33,7 @@ const MAX_SEQUENCES_FOR_TREE = 9999; const MAX_SEQUENCES_TO_SHOW_ALL = 2000; const highlightColor = '#feb640'; +const highlightColor50 = highlightColor + '7f'; export function RecordTable_Sequences( props: WrappedComponentProps @@ -302,7 +303,7 @@ export function RecordTable_Sequences( const highlightedRowClassName = cx( css` & td { - background-color: ${highlightColor} !important; + background-color: ${highlightColor50} !important; } ` ); From 11deb342867a8ebe66a442741730b2ef324ead37 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jul 2024 15:55:23 +0100 Subject: [PATCH 57/92] don't indent search bar and filters --- .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 54f66af18e..a3ef5a03b2 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -419,7 +419,6 @@ export function RecordTable_Sequences( )}
Date: Fri, 19 Jul 2024 15:56:35 +0100 Subject: [PATCH 58/92] add instantUpdate option to SelectList --- .../coreui/src/components/inputs/SelectList.tsx | 16 ++++++++++++++++ .../js/client/records/Sequences.tsx | 1 + 2 files changed, 17 insertions(+) diff --git a/packages/libs/coreui/src/components/inputs/SelectList.tsx b/packages/libs/coreui/src/components/inputs/SelectList.tsx index 1de1366795..f32639cdbc 100644 --- a/packages/libs/coreui/src/components/inputs/SelectList.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectList.tsx @@ -9,6 +9,10 @@ export interface SelectListProps extends CheckboxListProps { isDisabled?: boolean; /** Are contents loading? */ isLoading?: boolean; + /** If true, don't wait for component to close before calling `onChange` + * with latest selection. + */ + instantUpdate?: boolean; } export default function SelectList({ @@ -21,6 +25,7 @@ export default function SelectList({ defaultButtonDisplayContent, isDisabled = false, isLoading = false, + instantUpdate = false, ...props }: SelectListProps) { const [selected, setSelected] = useState['value']>(value); @@ -29,12 +34,23 @@ export default function SelectList({ ); const onClose = () => { + if (instantUpdate) return; // the next two effects take care of the following when constantly updating + onChange(selected); setButtonDisplayContent( selected.length ? selected.join(', ') : defaultButtonDisplayContent ); }; + /** + * Keep caller up to date with any selection changes + */ + useEffect(() => { + if (instantUpdate) { + onChange(selected); + } + }, [onChange, selected, instantUpdate]); + /** * Need to ensure that the state syncs with parent component in the event of an external * clearSelection button, as is the case in EDA's line plot controls diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 54f66af18e..018014c35f 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -373,6 +373,7 @@ export function RecordTable_Sequences( }))} value={pfamFilterIds} onChange={setPfamFilterIds} + instantUpdate={true} /> ); From 9c5ff46297363bf4861dde138947c343243142b9 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jul 2024 15:59:25 +0100 Subject: [PATCH 59/92] revert taxon filter to instant-update --- .../phyletic-distribution/PhyleticDistributionCheckbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index f0b7c1b6e1..c66a585088 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -87,7 +87,7 @@ export function PhyleticDistributionCheckbox({ ? selectionConfig.onSpeciesSelected : undefined } - shouldOnlyUpdateOnClose={true} + shouldOnlyUpdateOnClose={false} selectedList={ selectionConfig.selectable ? selectionConfig.selectedSpecies : undefined } From de9d761402f9228f3cee6170171f2b09684709cb Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jul 2024 16:01:08 +0100 Subject: [PATCH 60/92] instantUpdate for core/peripheral filter --- .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 08a2888761..ce8cb1257b 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -392,6 +392,7 @@ export function RecordTable_Sequences( ]} value={corePeripheralFilterValue} onChange={setCorePeripheralFilterValue} + instantUpdate={true} /> ); From 9281471c6a5aa0549bdfbcd5e88c909387146b12 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jul 2024 18:29:13 +0100 Subject: [PATCH 61/92] Pfam architectures are now scaled by protein length --- .../js/client/records/Sequences.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index a3ef5a03b2..07336a0838 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -28,6 +28,8 @@ import { css, cx } from '@emotion/css'; type RowType = Record; const treeWidth = 200; +const maxColumnWidth = 200; +const maxArchitectureLength = maxColumnWidth - 10 - 10 - 1; // 10px padding each side plus a 1px border const MIN_SEQUENCES_FOR_TREE = 3; const MAX_SEQUENCES_FOR_TREE = 9999; const MAX_SEQUENCES_TO_SHOW_ALL = 2000; @@ -111,9 +113,19 @@ export function RecordTable_Sequences( [pfamRows] ); + const maxProteinLength = useMemo( + () => + mesaRows.reduce((max, row) => { + const length = Number(row['length'] || ('0' as string)); + return length > max ? length : max; + }, 0), + [mesaRows] + ); + console.log({ maxProteinLength }); + mesaColumns.unshift({ key: 'pfamArchitecture', - name: 'Domain architecture (all drawn to same length)', + name: 'Domain architecture', renderCell: (cellProps) => { const proteinId = cellProps.row.full_id as string; const flatPfamData = rowsByAccession[proteinId]; @@ -122,9 +134,12 @@ export function RecordTable_Sequences( const proteinLength = Number( flatPfamData[0]['protein_length'] as string ); + const architectureLength = Math.floor( + (maxArchitectureLength * proteinLength) / maxProteinLength + ); return ( From 789629bf664eb65873004dd92909412b7cee4e55 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 19 Jul 2024 18:31:17 +0100 Subject: [PATCH 62/92] removed core-only filter for larger groups --- .../wdkCustomization/js/client/records/Sequences.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 07336a0838..c9d7896a0d 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -32,7 +32,6 @@ const maxColumnWidth = 200; const maxArchitectureLength = maxColumnWidth - 10 - 10 - 1; // 10px padding each side plus a 1px border const MIN_SEQUENCES_FOR_TREE = 3; const MAX_SEQUENCES_FOR_TREE = 9999; -const MAX_SEQUENCES_TO_SHOW_ALL = 2000; const highlightColor = '#feb640'; const highlightColor50 = highlightColor + '7f'; @@ -78,7 +77,7 @@ export function RecordTable_Sequences( // show only core as default for large groups const [corePeripheralFilterValue, setCorePeripheralFilterValue] = useState< ('core' | 'peripheral')[] - >(numSequences > MAX_SEQUENCES_TO_SHOW_ALL ? ['core'] : []); + >([]); const treeResponse = useOrthoService( numSequences >= MIN_SEQUENCES_FOR_TREE && @@ -121,7 +120,6 @@ export function RecordTable_Sequences( }, 0), [mesaRows] ); - console.log({ maxProteinLength }); mesaColumns.unshift({ key: 'pfamArchitecture', @@ -272,10 +270,6 @@ export function RecordTable_Sequences( } else return; }, [filteredTree, treeResponse, tree, filteredRows]); - const largeGroupWarning = - numSequences > MAX_SEQUENCES_TO_SHOW_ALL - ? '(note: this is a large group, only the core sequences will be shown by default)' - : ''; if ( !sortedRows || (numSequences >= MIN_SEQUENCES_FOR_TREE && @@ -284,7 +278,7 @@ export function RecordTable_Sequences( ) { return ( <> -
Loading... {largeGroupWarning}
+
Loading...
); // The loading spinner does not show :-( From 209fd02a9f1c3e59522ed32592dcff1a56e640d8 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 1 Aug 2024 18:53:22 +0100 Subject: [PATCH 63/92] improved table sort and filter performance - added comments about further optimisations --- .../src/components/tidytree/TreeTable.tsx | 2 + .../js/client/records/Sequences.tsx | 39 ++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index fb99287a5a..ea22835553 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -84,6 +84,8 @@ export default function TreeTable(props: TreeTableProps) { (_) => rowStyleClassName ), 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`, diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index c9d7896a0d..4ddde90ba4 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -162,6 +162,7 @@ export function RecordTable_Sequences( const fetchTree = async () => { try { + // TO DO: these do not actually "do stuff in the background" const parsedTree = await parseNewickAsync(treeResponse.newick); const leaves = await getLeavesAsync(parsedTree); const sortedRows = await sortRowsAsync(leaves, mesaRows); @@ -237,6 +238,10 @@ export function RecordTable_Sequences( if (leaves == null || tree == null || filteredRows?.length === 0) return; if (filteredRows != null && filteredRows.length < leaves.length) { + const filteredRowIds = new Set( + filteredRows.map(({ full_id }) => full_id as string) + ); + // must work on a copy of the tree because it's destructive const treeCopy = tree.clone(); let leavesRemoved = false; @@ -245,15 +250,15 @@ export function RecordTable_Sequences( leavesRemoved = false; // Reset flag for each iteration leavesCopy.forEach((leaf) => { - if (!filteredRows.find(({ full_id }) => full_id === leaf.id)) { + if (!filteredRowIds.has(leaf.id)) { leaf.remove(true); // remove leaf and remove any dangling ancestors leavesRemoved = true; // A leaf was removed, so set flag to true } }); } while (leavesRemoved); // Continue looping if any leaf was removed - return treeCopy; } + return tree; }, [tree, leaves, filteredRows]); @@ -292,7 +297,7 @@ export function RecordTable_Sequences( console.log( 'Tree and protein list mismatch. A=Tree, B=Table. Summary below:' ); - summarizeIDMismatch( + logIdMismatches( (leaves ?? []).map((leaf) => leaf.id), mesaRows.map((row) => truncate_full_id_for_tree_comparison(row.full_id as string) @@ -539,7 +544,7 @@ function createSafeSearchRegExp(input: string): RegExp { } } -function summarizeIDMismatch(A: string[], B: string[]) { +function logIdMismatches(A: string[], B: string[]) { const inAButNotB = difference(A, B); const inBButNotA = difference(B, A); @@ -583,18 +588,24 @@ async function sortRowsAsync( if (leaves == null) return mesaRows; return new Promise((resolve) => { + // Some full_ids end in :RNA + // However, the Newick files seem to be omitting the colon and everything following it. + // (Colons are part of Newick format.) + // So we remove anything after a ':' and hope it works! + // This is the only place where we use the IDs from the tree file. + + // make a map for performance + const rowMap = new Map( + mesaRows.map((row) => [ + truncate_full_id_for_tree_comparison(row.full_id as string), + row, + ]) + ); + const result = leaves - .map(({ id }) => - mesaRows.find(({ full_id }) => { - // Some full_ids end in :RNA - // However, the Newick files seem to be omitting the colon and everything following it. - // (Colons are part of Newick format.) - // So we remove anything after a ':' and hope it works! - // This is the only place where we use the IDs from the tree file. - return truncate_full_id_for_tree_comparison(full_id as string) === id; - }) - ) + .map(({ id }) => rowMap.get(id)) .filter((row): row is RowType => row != null); + resolve(result); }); } From 3874c5a9c139508308756833b3b87b21be9eb21d Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Fri, 2 Aug 2024 11:23:06 +0100 Subject: [PATCH 64/92] rename shouldOnlyUpdateOnClose to instantUpdate --- .../components/inputs/SelectTree/SelectTree.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 3cb92b20a1..8d29badf36 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -10,8 +10,8 @@ export interface SelectTreeProps extends CheckboxTreeProps { shouldCloseOnSelection?: boolean; wrapPopover?: (checkboxTree: ReactNode) => ReactNode; isDisabled?: boolean; - /** only update `selectedList` state when the popover closes */ - shouldOnlyUpdateOnClose?: boolean; + /** update `selectedList` state instantly when a selection is made (default: true) */ + instantUpdate?: boolean; } function SelectTree(props: SelectTreeProps) { @@ -24,13 +24,13 @@ function SelectTree(props: SelectTreeProps) { selectedList, onSelectionChange, shouldCloseOnSelection, - shouldOnlyUpdateOnClose, + instantUpdate = true, wrapPopover, } = props; // This local state is updated whenever a checkbox is clicked in the species tree. - // When `shouldOnlyUpdateOnClose` is true, pass the final value to `onSelectionChange` when the popover closes. - // When it is false we call `onSelectionChange` whenever `localSelectedList` changes + // When `instantUpdate` is false, pass the final value to `onSelectionChange` when the popover closes. + // When it is true we call `onSelectionChange` whenever `localSelectedList` changes const [localSelectedList, setLocalSelectedList] = useState(selectedList); /** Used as a hack to "auto close" the popover when shouldCloseOnSelection is true */ @@ -44,7 +44,7 @@ function SelectTree(props: SelectTreeProps) { // live updates to caller when needed useEffect(() => { - if (shouldOnlyUpdateOnClose) return; + if (!instantUpdate) return; onSelectionChange(localSelectedList); }, [onSelectionChange, localSelectedList]); @@ -70,7 +70,7 @@ function SelectTree(props: SelectTreeProps) { ? truncatedButtonContent(localSelectedList) : props.buttonDisplayContent ); - if (shouldOnlyUpdateOnClose) onSelectionChange(localSelectedList); + if (!instantUpdate) onSelectionChange(localSelectedList); }; const checkboxTree = ( @@ -146,6 +146,7 @@ const defaultProps = { searchPredicate: () => true, linksPosition: LinksPosition.Both, isDisabled: false, + instantUpdate: true, // Set default value to true }; SelectTree.defaultProps = defaultProps; From d45f9756ec749308796e85aa46cf38cfea5f8777 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Fri, 2 Aug 2024 11:43:35 +0100 Subject: [PATCH 65/92] remove an effect --- .../src/components/inputs/SelectList.tsx | 19 +++++++++++-------- .../PhyleticDistributionCheckbox.tsx | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectList.tsx b/packages/libs/coreui/src/components/inputs/SelectList.tsx index f32639cdbc..d5e53f59d6 100644 --- a/packages/libs/coreui/src/components/inputs/SelectList.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectList.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useState } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import PopoverButton from '../buttons/PopoverButton/PopoverButton'; import CheckboxList, { CheckboxListProps } from './checkboxes/CheckboxList'; @@ -43,13 +43,16 @@ export default function SelectList({ }; /** - * Keep caller up to date with any selection changes + * Keep caller up to date with any selection changes, if required by `instantUpdate` */ - useEffect(() => { - if (instantUpdate) { - onChange(selected); - } - }, [onChange, selected, instantUpdate]); + const handleCheckboxListUpdate = useCallback( + (newSelection: SelectListProps['value']) => { + if (instantUpdate) { + onChange(newSelection); + } + }, + [instantUpdate, setSelected] + ); /** * Need to ensure that the state syncs with parent component in the event of an external @@ -91,7 +94,7 @@ export default function SelectList({ name={name} items={items} value={selected} - onChange={setSelected} + onChange={handleCheckboxListUpdate} linksPosition={linksPosition} {...props} /> diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index c66a585088..dc88a330f1 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -87,7 +87,7 @@ export function PhyleticDistributionCheckbox({ ? selectionConfig.onSpeciesSelected : undefined } - shouldOnlyUpdateOnClose={false} + instantUpdate={true} selectedList={ selectionConfig.selectable ? selectionConfig.selectedSpecies : undefined } From eb238e533bb6aacc74b37b12ac427c178eeab8c3 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Fri, 2 Aug 2024 14:57:16 +0100 Subject: [PATCH 66/92] missed a setSelected and dependency --- packages/libs/coreui/src/components/inputs/SelectList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectList.tsx b/packages/libs/coreui/src/components/inputs/SelectList.tsx index d5e53f59d6..0cc7a27fbf 100644 --- a/packages/libs/coreui/src/components/inputs/SelectList.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectList.tsx @@ -47,11 +47,12 @@ export default function SelectList({ */ const handleCheckboxListUpdate = useCallback( (newSelection: SelectListProps['value']) => { + setSelected(newSelection); if (instantUpdate) { onChange(newSelection); } }, - [instantUpdate, setSelected] + [instantUpdate, setSelected, onChange] ); /** From 66e13f26c85b86d753097ee373d6b561f5da34bf Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Fri, 2 Aug 2024 15:54:29 +0100 Subject: [PATCH 67/92] final tweaks to button label behaviour --- packages/libs/coreui/src/components/inputs/SelectList.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectList.tsx b/packages/libs/coreui/src/components/inputs/SelectList.tsx index 0cc7a27fbf..ce495361d3 100644 --- a/packages/libs/coreui/src/components/inputs/SelectList.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectList.tsx @@ -34,8 +34,6 @@ export default function SelectList({ ); const onClose = () => { - if (instantUpdate) return; // the next two effects take care of the following when constantly updating - onChange(selected); setButtonDisplayContent( selected.length ? selected.join(', ') : defaultButtonDisplayContent @@ -61,6 +59,7 @@ export default function SelectList({ */ useEffect(() => { setSelected(value); + if (instantUpdate) return; // we don't want the button text changing on every click setButtonDisplayContent( value.length ? value.join(', ') : defaultButtonDisplayContent ); From febb3128c1054b8388c18fe0469f257830a7b628 Mon Sep 17 00:00:00 2001 From: Bob MacCallum Date: Sat, 3 Aug 2024 15:21:07 +0100 Subject: [PATCH 68/92] add reset button, revisit filter button layout --- .../js/client/records/Sequences.tsx | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 5ec78206dd..a858a210e1 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -21,7 +21,7 @@ import { extractPfamDomain } from 'ortho-client/records/utils'; import Banner from '@veupathdb/coreui/lib/components/banners/Banner'; import { RowCounter } from '@veupathdb/coreui/lib/components/Mesa'; import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; -import { SelectList } from '@veupathdb/coreui'; +import { FloatingButton, SelectList, Undo } from '@veupathdb/coreui'; import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; import { css, cx } from '@emotion/css'; @@ -45,9 +45,12 @@ export function RecordTable_Sequences( [searchQuery] ); + const [resetCounter, setResetCounter] = useState(0); // used for forcing re-render of filter buttons const [selectedSpecies, setSelectedSpecies] = useState([]); - const [pfamFilterIds, setPfamFilterIds] = useState([]); + const [corePeripheralFilterValue, setCorePeripheralFilterValue] = useState< + ('core' | 'peripheral')[] + >([]); const groupName = props.record.id.find( ({ name }) => name === 'group_name' @@ -74,11 +77,6 @@ export function RecordTable_Sequences( const numSequences = mesaRows.length; - // show only core as default for large groups - const [corePeripheralFilterValue, setCorePeripheralFilterValue] = useState< - ('core' | 'peripheral')[] - >([]); - const treeResponse = useOrthoService( numSequences >= MIN_SEQUENCES_FOR_TREE && numSequences <= MAX_SEQUENCES_FOR_TREE @@ -359,6 +357,7 @@ export function RecordTable_Sequences( const pfamFilter = pfamRows.length > 0 && ( ({ display: ( @@ -393,6 +392,7 @@ export function RecordTable_Sequences( const corePeripheralFilter = ( 0 ? ( ) : null; + const resetButton = ( + { + setPfamFilterIds([]); + setCorePeripheralFilterValue([]); + setSelectedSpecies([]); + setResetCounter((prev) => prev + 1); + }} + /> + ); + return ( <> {numSequences > MAX_SEQUENCES_FOR_TREE && ( @@ -466,15 +490,15 @@ export function RecordTable_Sequences( flexDirection: 'row', gap: '1em', alignItems: 'center', - marginLeft: 'auto', flexWrap: 'wrap', - justifyContent: 'flex-end', + justifyContent: 'flex-start', }} > Filters: {pfamFilter} {corePeripheralFilter} {taxonFilter} + {resetButton}
Date: Fri, 9 Aug 2024 11:38:07 +0100 Subject: [PATCH 69/92] placeholder wrapper for image next to group stats --- .../GroupRecordClasses.GroupRecordClass.tsx | 3 ++ .../js/client/records/GroupStats.tsx | 42 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.tsx index 0e9c402600..28d8e3037b 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupRecordClasses.GroupRecordClass.tsx @@ -19,6 +19,7 @@ import { PhyleticDistributionCheckbox } from 'ortho-client/components/phyletic-d import { PfamDomainArchitecture } from 'ortho-client/components/pfam-domains/PfamDomainArchitecture'; import { RecordTable_Sequences } from 'ortho-client/records/Sequences'; +import { RecordTable_GroupStats } from './GroupStats'; import { RecordAttributeProps, @@ -50,6 +51,7 @@ const MSA_ATTRIBUTE_NAME = 'msa'; const PFAMS_TABLE_NAME = 'PFams'; const PROTEIN_PFAMS_TABLE_NAME = 'ProteinPFams'; const SEQUENCES_TABLE_NAME = 'Sequences'; +const GROUP_STATS_TABLE_NAME = 'GroupStat'; const CORE_PERIPHERAL_ATTRIBUTE_NAME = 'core_peripheral'; const PROTEIN_LENGTH_ATTRIBUTE_NAME = 'protein_length'; @@ -364,4 +366,5 @@ const recordTableWrappers: Record< [PROTEIN_PFAMS_TABLE_NAME]: RecordTable_ProteinDomainArchitectures, [TAXON_COUNTS_TABLE_NAME]: RecordTable_TaxonCounts, [SEQUENCES_TABLE_NAME]: RecordTable_Sequences, + [GROUP_STATS_TABLE_NAME]: RecordTable_GroupStats, }; diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx new file mode 100644 index 0000000000..54d9b30890 --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { RecordTable } from './SequenceRecordClasses.SequenceRecordClass'; +import { RecordTableProps, WrappedComponentProps } from './Types'; + +export function RecordTable_GroupStats( + props: WrappedComponentProps +) { + const regularRecordTable = RecordTable(props); + + return ( +
+
+

This could be an image.

+

+ Though I don't know how we will programmatically obtain the image{' '} + src. +

+

+ I don't know where images/eval-hist.png is or what it + looks like. +

+
+ {regularRecordTable} +
+ ); +} From 5e310a1aca25bd8c1a3a9dadf323581c18ff69f0 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 9 Aug 2024 13:53:11 +0100 Subject: [PATCH 70/92] restore the buttonless phyletic distribution --- .../coreui/src/components/inputs/SelectTree/SelectTree.tsx | 6 +++++- .../phyletic-distribution/PhyleticDistributionCheckbox.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 8d29badf36..6b221a3ab4 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -8,6 +8,7 @@ import CheckboxTree, { export interface SelectTreeProps extends CheckboxTreeProps { buttonDisplayContent: ReactNode; shouldCloseOnSelection?: boolean; + hasPopoverButton?: boolean; wrapPopover?: (checkboxTree: ReactNode) => ReactNode; isDisabled?: boolean; /** update `selectedList` state instantly when a selection is made (default: true) */ @@ -24,6 +25,7 @@ function SelectTree(props: SelectTreeProps) { selectedList, onSelectionChange, shouldCloseOnSelection, + hasPopoverButton = true, instantUpdate = true, wrapPopover, } = props; @@ -115,7 +117,7 @@ function SelectTree(props: SelectTreeProps) { /> ); - return ( + return hasPopoverButton ? ( (props: SelectTreeProps) { {wrapPopover ? wrapPopover(checkboxTree) : checkboxTree}
+ ) : ( + <>{wrapPopover ? wrapPopover(checkboxTree) : checkboxTree} ); } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index dc88a330f1..59a04e8790 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -72,6 +72,7 @@ export function PhyleticDistributionCheckbox({ return ( Date: Fri, 9 Aug 2024 13:58:20 +0100 Subject: [PATCH 71/92] improved comment/documentation --- .../libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx index 6b221a3ab4..437abe7b3a 100644 --- a/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx +++ b/packages/libs/coreui/src/components/inputs/SelectTree/SelectTree.tsx @@ -8,7 +8,7 @@ import CheckboxTree, { export interface SelectTreeProps extends CheckboxTreeProps { buttonDisplayContent: ReactNode; shouldCloseOnSelection?: boolean; - hasPopoverButton?: boolean; + hasPopoverButton?: boolean; // default=true wrapPopover?: (checkboxTree: ReactNode) => ReactNode; isDisabled?: boolean; /** update `selectedList` state instantly when a selection is made (default: true) */ From 8c8f3a3f999ec1b5d20d896197157a656d87eadd Mon Sep 17 00:00:00 2001 From: Richard Demko Date: Thu, 15 Aug 2024 12:18:17 -0400 Subject: [PATCH 72/92] Adding update evalue histogram --- .../ortho-site/webapp/images/eval-hist.png | Bin 0 -> 24039 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/sites/ortho-site/webapp/images/eval-hist.png diff --git a/packages/sites/ortho-site/webapp/images/eval-hist.png b/packages/sites/ortho-site/webapp/images/eval-hist.png new file mode 100644 index 0000000000000000000000000000000000000000..082fec2e9d4734b0a670be177ec40d127f811bbd GIT binary patch literal 24039 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 Date: Fri, 16 Aug 2024 13:07:23 +0100 Subject: [PATCH 73/92] static histogram image now shows, with elaborate width-setting for caption below --- .../images/eval-hist.png | Bin .../js/client/records/GroupStats.tsx | 45 ++++++++++-------- .../js/client/types/images.d.ts | 4 ++ 3 files changed, 29 insertions(+), 20 deletions(-) rename packages/sites/ortho-site/webapp/{ => wdkCustomization}/images/eval-hist.png (100%) create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/images.d.ts diff --git a/packages/sites/ortho-site/webapp/images/eval-hist.png b/packages/sites/ortho-site/webapp/wdkCustomization/images/eval-hist.png similarity index 100% rename from packages/sites/ortho-site/webapp/images/eval-hist.png rename to packages/sites/ortho-site/webapp/wdkCustomization/images/eval-hist.png diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx index 54d9b30890..f2aff7d083 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx @@ -1,12 +1,21 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { RecordTable } from './SequenceRecordClasses.SequenceRecordClass'; import { RecordTableProps, WrappedComponentProps } from './Types'; +import eval_hist_img from '../../../images/eval-hist.png'; + export function RecordTable_GroupStats( props: WrappedComponentProps ) { const regularRecordTable = RecordTable(props); + const imgRef = useRef(null); + const [imgWidth, setImgWidth] = useState(0); + + useEffect(() => { + setImgWidth(imgRef.current?.offsetWidth ?? 0); + }, [imgRef]); + return (
-
-

This could be an image.

-

- Though I don't know how we will programmatically obtain the image{' '} - src. -

-

- I don't know where images/eval-hist.png is or what it - looks like. -

-
- {regularRecordTable} + Histogram of median inter-group e-values for both core-only and core+peripheral proteins. The distributions of both peak at around 1e-20 to 1e-60 with a substantial tail out to e-values of 1e-300. setImgWidth(imgRef.current?.offsetWidth ?? 0)} + /> +
+ This histogram is provided to aid the interpretation of E-values in + the adjoining table. E-values have been transformed using a negative + logarithm, so higher significance is represented further to the right. +
+
); } diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/images.d.ts b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/images.d.ts new file mode 100644 index 0000000000..1c5923252c --- /dev/null +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/types/images.d.ts @@ -0,0 +1,4 @@ +declare module '*.png' { + const value: string; + export default value; +} From eff237e964628258af917bbd273e0190c490ba33 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 16 Aug 2024 13:19:20 +0100 Subject: [PATCH 74/92] make caption rendering smoother - no horizontal shifting of the figure --- .../js/client/records/GroupStats.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx index f2aff7d083..6cf8a0e120 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx @@ -27,7 +27,7 @@ export function RecordTable_GroupStats( {regularRecordTable}
setImgWidth(imgRef.current?.offsetWidth ?? 0)} /> -
- This histogram is provided to aid the interpretation of E-values in - the adjoining table. E-values have been transformed using a negative - logarithm, so higher significance is represented further to the right. -
+ {imgWidth ? ( // only render the caption after the image is fully loaded so it doesn't shape-shift +
+ This histogram is provided to aid the interpretation of E-values in + the adjoining table. E-values have been transformed using a negative + logarithm, so higher significance is represented further to the + right. +
+ ) : null}
); From 144c838276982da20a9291e74111c6150b0e8928 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 16 Aug 2024 13:43:20 +0100 Subject: [PATCH 75/92] moved png file again --- .../js/client/records/GroupStats.tsx | 2 +- .../{images => js/client/records}/eval-hist.png | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/sites/ortho-site/webapp/wdkCustomization/{images => js/client/records}/eval-hist.png (100%) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx index 6cf8a0e120..e6b6597d62 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { RecordTable } from './SequenceRecordClasses.SequenceRecordClass'; import { RecordTableProps, WrappedComponentProps } from './Types'; -import eval_hist_img from '../../../images/eval-hist.png'; +import eval_hist_img from './eval-hist.png'; export function RecordTable_GroupStats( props: WrappedComponentProps diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/images/eval-hist.png b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/eval-hist.png similarity index 100% rename from packages/sites/ortho-site/webapp/wdkCustomization/images/eval-hist.png rename to packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/eval-hist.png From 65b065dd8ae65d85c2e56507be8c8804f44b4833 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 16 Aug 2024 15:56:39 +0100 Subject: [PATCH 76/92] simplified to hardcoded dimensions --- .../js/client/records/GroupStats.tsx | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx index e6b6597d62..75e3551e55 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/GroupStats.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React from 'react'; import { RecordTable } from './SequenceRecordClasses.SequenceRecordClass'; import { RecordTableProps, WrappedComponentProps } from './Types'; @@ -9,13 +9,6 @@ export function RecordTable_GroupStats( ) { const regularRecordTable = RecordTable(props); - const imgRef = useRef(null); - const [imgWidth, setImgWidth] = useState(0); - - useEffect(() => { - setImgWidth(imgRef.current?.offsetWidth ?? 0); - }, [imgRef]); - return (
Histogram of median inter-group e-values for both core-only and core+peripheral proteins. The distributions of both peak at around 1e-20 to 1e-60 with a substantial tail out to e-values of 1e-300. setImgWidth(imgRef.current?.offsetWidth ?? 0)} /> - {imgWidth ? ( // only render the caption after the image is fully loaded so it doesn't shape-shift -
- This histogram is provided to aid the interpretation of E-values in - the adjoining table. E-values have been transformed using a negative - logarithm, so higher significance is represented further to the - right. -
- ) : null} +
+ This histogram is provided to aid the interpretation of E-values in + the adjoining table. E-values have been transformed using a negative + logarithm, so higher significance is represented further to the right. +
); From 9e6d9399c89c4c64eed44b19156f02aaf3e3340e Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Wed, 21 Aug 2024 14:09:50 -0400 Subject: [PATCH 77/92] Address typescript errors --- .../wdkCustomization/js/client/records/Sequences.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index a858a210e1..5699e26e73 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -24,6 +24,7 @@ import { PfamDomain } from 'ortho-client/components/pfam-domains/PfamDomain'; import { FloatingButton, SelectList, Undo } from '@veupathdb/coreui'; import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; import { css, cx } from '@emotion/css'; +import { formatAttributeValue } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; type RowType = Record; @@ -375,14 +376,14 @@ export function RecordTable_Sequences( style={{ width: 100 }} pfamId={row.accession as string} /> -
{row.accession}
-
{row.description}
+
{formatAttributeValue(row.accession)}
+
{formatAttributeValue(row.description)}
- {row.num_proteins} proteins + {formatAttributeValue(row.num_proteins)} proteins
), - value: row.accession as string, + value: formatAttributeValue(row.accession), }))} value={pfamFilterIds} onChange={setPfamFilterIds} From 7508ad78a2c0a7891fda8682b1357bd88c473adf Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 28 Aug 2024 19:10:13 +0100 Subject: [PATCH 78/92] remove taxon_abbrev column --- .../webapp/wdkCustomization/js/client/records/Sequences.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 5699e26e73..a89e39c932 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -71,7 +71,10 @@ export function RecordTable_Sequences( })) // and remove a raw HTML checkbox field - we'll use Mesa's built-in checkboxes for this // and an object-laden 'sequence_link' field - the ID seems to be replicated in the full_id field - .filter(({ key }) => key !== 'clustalInput' && key !== 'full_id'); + .filter( + ({ key }) => + key !== 'clustalInput' && key !== 'full_id' && key !== 'taxon_abbrev' + ); const mesaRows = props.value; const pfamRows = props.record.tables['PFams']; From 7f758f495e87ab9bd9b3a860e1afbe6001e5467b Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 28 Aug 2024 19:21:14 +0100 Subject: [PATCH 79/92] rename Taxon column to Clade --- .../wdkCustomization/js/client/records/Sequences.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index a89e39c932..f6c4033c67 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -74,7 +74,12 @@ export function RecordTable_Sequences( .filter( ({ key }) => key !== 'clustalInput' && key !== 'full_id' && key !== 'taxon_abbrev' - ); + ) + // rename some headings (shouldn't this be done on the back end?) + .map((column) => ({ + ...column, + name: column.name === 'Taxon' ? 'Clade' : column.name, + })); const mesaRows = props.value; const pfamRows = props.record.tables['PFams']; From 0eb1a700cc503bc16e70306149c3a92979d323ee Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 28 Aug 2024 19:24:14 +0100 Subject: [PATCH 80/92] rename Species filter button to Organism --- .../phyletic-distribution/PhyleticDistributionCheckbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx index 59a04e8790..5d4e8c124d 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/components/phyletic-distribution/PhyleticDistributionCheckbox.tsx @@ -73,7 +73,7 @@ export function PhyleticDistributionCheckbox({ return ( Date: Wed, 28 Aug 2024 19:49:13 +0100 Subject: [PATCH 81/92] improve filter button layout, especially when buttons have expanded content --- .../libs/coreui/src/components/Mesa/Ui/RowCounter.jsx | 11 ++++++++++- .../wdkCustomization/js/client/records/Sequences.tsx | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx b/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx index ce66c5789d..cc79aeadcf 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx @@ -54,7 +54,16 @@ class RowCounter extends React.PureComponent { } return ( -
+
{countString} {filterString}
diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index f6c4033c67..06859d9138 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -499,8 +499,7 @@ export function RecordTable_Sequences( flexDirection: 'row', gap: '1em', alignItems: 'center', - flexWrap: 'wrap', - justifyContent: 'flex-start', + justifyContent: 'flex-end', }} > Filters: From dd0c40a0c2e372715334f07defb5c05e9bc6f07d Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 29 Aug 2024 11:18:46 +0100 Subject: [PATCH 82/92] fix Omit prop typo that wasn't detected because only consumer so far was a JSX file --- .../wdk-client/src/Views/Records/RecordTable/RecordFilter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/libs/wdk-client/src/Views/Records/RecordTable/RecordFilter.tsx b/packages/libs/wdk-client/src/Views/Records/RecordTable/RecordFilter.tsx index 96fa96db92..20b7f439c5 100644 --- a/packages/libs/wdk-client/src/Views/Records/RecordTable/RecordFilter.tsx +++ b/packages/libs/wdk-client/src/Views/Records/RecordTable/RecordFilter.tsx @@ -24,7 +24,7 @@ type RecordFilterSelectorProps = { interface RecordFilterProps extends Omit< RecordFilterSelectorProps, - 'toggleFilterFieldSelect' | 'containerClassName' + 'toggleFilterFieldSelector' | 'containerClassName' > { searchTerm: string; onSearchTermChange: (searchTerm: string) => void; From d3f421b8161d9085fbc5ea516c0ec85cce3dc36f Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 29 Aug 2024 12:34:30 +0100 Subject: [PATCH 83/92] main proteins table now has fully-fledged search box with column selector --- .../js/client/records/Sequences.tsx | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index 06859d9138..e91f99fd2f 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -2,10 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import TreeTable from '@veupathdb/components/lib/components/tidytree/TreeTable'; import { RecordTableProps, WrappedComponentProps } from './Types'; import { useOrthoService } from 'ortho-client/hooks/orthoService'; -import { - Loading, - RealTimeSearchBox, -} from '@veupathdb/wdk-client/lib/Components'; +import { Loading } from '@veupathdb/wdk-client/lib/Components'; import { Branch, parseNewick } from 'patristic'; import { AttributeValue, @@ -25,6 +22,7 @@ import { FloatingButton, SelectList, Undo } from '@veupathdb/coreui'; import { RecordTable_TaxonCounts_Filter } from './RecordTable_TaxonCounts_Filter'; import { css, cx } from '@emotion/css'; import { formatAttributeValue } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { RecordFilter } from '@veupathdb/wdk-client/lib/Views/Records/RecordTable/RecordFilter'; type RowType = Record; @@ -34,6 +32,8 @@ const maxArchitectureLength = maxColumnWidth - 10 - 10 - 1; // 10px padding each const MIN_SEQUENCES_FOR_TREE = 3; const MAX_SEQUENCES_FOR_TREE = 9999; +const PFAM_ARCH_COLUMN_KEY = 'pfamArchitecture'; + const highlightColor = '#feb640'; const highlightColor50 = highlightColor + '7f'; @@ -129,7 +129,7 @@ export function RecordTable_Sequences( ); mesaColumns.unshift({ - key: 'pfamArchitecture', + key: PFAM_ARCH_COLUMN_KEY, name: 'Domain architecture', renderCell: (cellProps) => { const proteinId = cellProps.row.full_id as string; @@ -196,6 +196,11 @@ export function RecordTable_Sequences( // 1. user-entered text search // 2. core-peripheral radio button // 3. checked boxes in the Pfam legend + + const [selectedColumnFilters, setSelectedColumnFilters] = useState( + [] + ); + const filteredRows = useMemo(() => { if ( searchQuery !== '' || @@ -211,7 +216,8 @@ export function RecordTable_Sequences( const rowPfamIdsSet = accessionToPfamIds.get(rowFullId); const searchMatch = - searchQuery === '' || rowMatch(row, safeSearchRegexp); + searchQuery === '' || + rowMatch(row, safeSearchRegexp, selectedColumnFilters); const corePeripheralMatch = corePeripheralFilterValue.length === 0 || corePeripheralFilterValue.includes( @@ -232,6 +238,7 @@ export function RecordTable_Sequences( return undefined; }, [ searchQuery, + selectedColumnFilters, safeSearchRegexp, sortedRows, corePeripheralFilterValue, @@ -296,6 +303,11 @@ export function RecordTable_Sequences( ); // The loading spinner does not show :-( } + // list of column keys and display names to show in the checkbox dropdown in the table text search box (RecordFilter) + const filterAttributes = mesaColumns + .map(({ key, name }) => ({ value: key, display: name ?? 'Unknown column' })) + .filter(({ value }) => value !== PFAM_ARCH_COLUMN_KEY); + if ( mesaRows != null && sortedRows != null && @@ -475,12 +487,13 @@ export function RecordTable_Sequences( justifyContent: 'space-between', }} > - setSelectedColumnFilters(keys)} />
@@ -550,9 +563,13 @@ export function RecordTable_Sequences( ); } -function rowMatch(row: RowType, query: RegExp): boolean { +function rowMatch(row: RowType, query: RegExp, keys?: string[]): boolean { + // Get the values to search in based on the optionally provided keys + const valuesToSearch = + keys && keys.length > 0 ? keys.map((key) => row[key]) : Object.values(row); + return ( - Object.values(row).find((value) => { + valuesToSearch.find((value) => { if (value != null) { if (typeof value === 'string') return value.match(query); else if ( From 6f0cad609fe635f361933eb1390dd018c5daa71b Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 29 Aug 2024 16:30:48 +0100 Subject: [PATCH 84/92] moved styling to .scss file --- .../libs/coreui/src/components/Mesa/Ui/RowCounter.jsx | 11 +---------- .../src/components/Mesa/style/Ui/TableToolbar.scss | 8 ++++++++ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx b/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx index cc79aeadcf..ce66c5789d 100644 --- a/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx +++ b/packages/libs/coreui/src/components/Mesa/Ui/RowCounter.jsx @@ -54,16 +54,7 @@ class RowCounter extends React.PureComponent { } return ( -
+
{countString} {filterString}
diff --git a/packages/libs/coreui/src/components/Mesa/style/Ui/TableToolbar.scss b/packages/libs/coreui/src/components/Mesa/style/Ui/TableToolbar.scss index f2a8283559..67cc35d9bd 100644 --- a/packages/libs/coreui/src/components/Mesa/style/Ui/TableToolbar.scss +++ b/packages/libs/coreui/src/components/Mesa/style/Ui/TableToolbar.scss @@ -18,6 +18,14 @@ .TableToolbar-Info { font-size: 80%; padding: 0px 20px 0px 10px; + + .RowCounter { + display: flex; + justify-content: flex-start; + flex-direction: row; + flex-wrap: wrap; + column-gap: 1em; + } } .TableToolbar-Children { From 0e0996274f27c836520bc6400db239bf1a7c8772 Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 30 Aug 2024 12:18:41 +0100 Subject: [PATCH 85/92] revert column changes now that model is updated OrthoMCLModel#5 --- .../wdkCustomization/js/client/records/Sequences.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx index e91f99fd2f..35038d712a 100644 --- a/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx +++ b/packages/sites/ortho-site/webapp/wdkCustomization/js/client/records/Sequences.tsx @@ -64,21 +64,11 @@ export function RecordTable_Sequences( const [highlightedNodes, setHighlightedNodes] = useState([]); const mesaColumns: MesaColumn[] = props.table.attributes + .filter(({ isDisplayable }) => isDisplayable) .map(({ name, displayName, type }) => ({ key: name, name: displayName, type: type === 'link' ? 'wdkLink' : type, - })) - // and remove a raw HTML checkbox field - we'll use Mesa's built-in checkboxes for this - // and an object-laden 'sequence_link' field - the ID seems to be replicated in the full_id field - .filter( - ({ key }) => - key !== 'clustalInput' && key !== 'full_id' && key !== 'taxon_abbrev' - ) - // rename some headings (shouldn't this be done on the back end?) - .map((column) => ({ - ...column, - name: column.name === 'Taxon' ? 'Clade' : column.name, })); const mesaRows = props.value; From c02d237d2116a452a8195a51ad570b10e6ccdb2e Mon Sep 17 00:00:00 2001 From: Bob Date: Sat, 7 Sep 2024 02:16:30 +0100 Subject: [PATCH 86/92] added new WDK record scope - record-collapsed --- .../src/Controllers/RecordController.tsx | 2 +- .../src/StoreModules/RecordStoreModule.ts | 15 +++++++++++++-- .../libs/wdk-client/src/Utils/CategoryUtils.tsx | 1 + .../wdk-client/src/Views/Records/RecordUtils.ts | 9 ++++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/libs/wdk-client/src/Controllers/RecordController.tsx b/packages/libs/wdk-client/src/Controllers/RecordController.tsx index a2535021d9..7a1bab5cfb 100644 --- a/packages/libs/wdk-client/src/Controllers/RecordController.tsx +++ b/packages/libs/wdk-client/src/Controllers/RecordController.tsx @@ -111,7 +111,7 @@ class RecordController extends PageController { return ( attributes?.includes(id) || tables?.includes(id) || - scopes.includes('record-internal') + scopes.includes('record-internal') // do we need to handle 'record-collapsed'? ); }) .toArray(); diff --git a/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts b/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts index 0e8d41c859..998cfbf16b 100644 --- a/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts +++ b/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts @@ -42,7 +42,7 @@ import { getValue, preferences, setValue } from '../Preferences'; import { ServiceError } from '../Service/ServiceError'; import { CategoryTreeNode, getId, getTargetType } from '../Utils/CategoryUtils'; import { stateEffect } from '../Utils/ObserverUtils'; -import { filterNodes } from '../Utils/TreeUtils'; +import { filterNodes, getLeaves } from '../Utils/TreeUtils'; import { RecordClass, RecordInstance } from '../Utils/WdkModel'; export const key = 'record'; @@ -84,11 +84,22 @@ export function reduce(state: State = {} as State, action: Action): State { case RECORD_RECEIVED: { if (action.id !== state.requestId) return state; let { record, recordClass, categoryTree } = action.payload; + + const collapsedSections = getLeaves(categoryTree, (node) => node.children) + .filter( + ( + node + ): node is CategoryTreeNode & { properties: { name: string[] } } => + !!node.properties.scope?.includes('record-collapsed') && + !!node.properties.name + ) + .map((node) => node.properties.name[0]); + return { ...state, record, recordClass, - collapsedSections: [], + collapsedSections, navigationCategoriesExpanded: state.navigationCategoriesExpanded || [], isLoading: false, categoryTree, diff --git a/packages/libs/wdk-client/src/Utils/CategoryUtils.tsx b/packages/libs/wdk-client/src/Utils/CategoryUtils.tsx index b98bbe156a..bfa0a28599 100644 --- a/packages/libs/wdk-client/src/Utils/CategoryUtils.tsx +++ b/packages/libs/wdk-client/src/Utils/CategoryUtils.tsx @@ -25,6 +25,7 @@ export type TargetType = 'search' | 'attribute' | 'table'; export type Scope = | 'record' | 'record-internal' + | 'record-collapsed' | 'results' | 'results-internal' | 'download' diff --git a/packages/libs/wdk-client/src/Views/Records/RecordUtils.ts b/packages/libs/wdk-client/src/Views/Records/RecordUtils.ts index 19e0c4749f..840705a4be 100644 --- a/packages/libs/wdk-client/src/Views/Records/RecordUtils.ts +++ b/packages/libs/wdk-client/src/Views/Records/RecordUtils.ts @@ -85,7 +85,14 @@ export const isInternalNode = partial( 'scope', 'record-internal' ); -export const isNotInternalNode = partial(nodeHasProperty, 'scope', 'record'); +const isRecordNode = partial(nodeHasProperty, 'scope', 'record'); +const isRecordCollapsedNode = partial( + nodeHasProperty, + 'scope', + 'record-collapsed' +); +export const isNotInternalNode = (node: CategoryTreeNode) => + isRecordNode(node) || isRecordCollapsedNode(node); export const isAttributeNode = partial( nodeHasProperty, 'targetType', From 51216546830f7da5ae0d1c4f646ddf29469a7da6 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 25 Sep 2024 16:54:57 +0100 Subject: [PATCH 87/92] added Dave's suggestions --- .../libs/wdk-client/src/Controllers/RecordController.tsx | 2 +- .../wdk-client/src/StoreModules/RecordStoreModule.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/libs/wdk-client/src/Controllers/RecordController.tsx b/packages/libs/wdk-client/src/Controllers/RecordController.tsx index 7a1bab5cfb..a2535021d9 100644 --- a/packages/libs/wdk-client/src/Controllers/RecordController.tsx +++ b/packages/libs/wdk-client/src/Controllers/RecordController.tsx @@ -111,7 +111,7 @@ class RecordController extends PageController { return ( attributes?.includes(id) || tables?.includes(id) || - scopes.includes('record-internal') // do we need to handle 'record-collapsed'? + scopes.includes('record-internal') ); }) .toArray(); diff --git a/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts b/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts index 998cfbf16b..7f16fd6fa7 100644 --- a/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts +++ b/packages/libs/wdk-client/src/StoreModules/RecordStoreModule.ts @@ -40,7 +40,12 @@ import { RootState } from '../Core/State/Types'; import { EpicDependencies, ModuleEpic } from '../Core/Store'; import { getValue, preferences, setValue } from '../Preferences'; import { ServiceError } from '../Service/ServiceError'; -import { CategoryTreeNode, getId, getTargetType } from '../Utils/CategoryUtils'; +import { + CategoryTreeNode, + getId, + getRefName, + getTargetType, +} from '../Utils/CategoryUtils'; import { stateEffect } from '../Utils/ObserverUtils'; import { filterNodes, getLeaves } from '../Utils/TreeUtils'; import { RecordClass, RecordInstance } from '../Utils/WdkModel'; @@ -93,7 +98,7 @@ export function reduce(state: State = {} as State, action: Action): State { !!node.properties.scope?.includes('record-collapsed') && !!node.properties.name ) - .map((node) => node.properties.name[0]); + .map((node) => getRefName(node)); return { ...state, From 506dd666c343d645fb8fa1704fdfe96f94c74ae9 Mon Sep 17 00:00:00 2001 From: Bob Date: Wed, 25 Sep 2024 21:30:51 +0100 Subject: [PATCH 88/92] merge in npm action change --- .github/workflows/npm-publish-sites.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/npm-publish-sites.yml b/.github/workflows/npm-publish-sites.yml index 67092afc65..01dcf84c9f 100644 --- a/.github/workflows/npm-publish-sites.yml +++ b/.github/workflows/npm-publish-sites.yml @@ -3,9 +3,9 @@ name: NPM Publish Sites on: - release: - types: - - published + push: + tags: + - v* jobs: # Gather the names of the sites directories and store it as an output variable @@ -51,9 +51,6 @@ jobs: elif [[ "$git_tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+-([a-zA-Z0-9]+)(\.[0-9]+)*$ ]]; then npm_tag="${BASH_REMATCH[1]}" echo "npm_tag=$npm_tag" >> $GITHUB_ENV - # if prerelease is checked, use "prerelease" for the npm tag - elif ${{ github.event.release.prerelease }}; then - echo "npm_tag=prerelease" >> $GITHUB_ENV # or fall back to 'latest' just in case else echo "npm_tag=latest" >> $GITHUB_ENV From 07934e8d608150d86afb7d7e0b200bcb328a6375 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 26 Sep 2024 11:15:52 +0100 Subject: [PATCH 89/92] cherry picking missed this line --- .github/workflows/npm-publish-sites.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/npm-publish-sites.yml b/.github/workflows/npm-publish-sites.yml index 01dcf84c9f..8ac153eba8 100644 --- a/.github/workflows/npm-publish-sites.yml +++ b/.github/workflows/npm-publish-sites.yml @@ -56,7 +56,6 @@ jobs: echo "npm_tag=latest" >> $GITHUB_ENV fi - uses: JS-DevTools/npm-publish@v2 - if: ${{ startsWith(github.event.release.tag_name, 'v') }} with: token: ${{ secrets.NPM_TOKEN }} access: public From 85dc60ee7385182ba36500177dbb5f6be7e34082 Mon Sep 17 00:00:00 2001 From: Dave Falke Date: Fri, 18 Oct 2024 13:56:51 -0400 Subject: [PATCH 90/92] Orthogroup tree table perf (#1233) --- .gitignore | 3 + .../plotControls/SelectedRangeControl.tsx | 5 + .../src/components/tidytree/TreeTable.tsx | 42 +- .../widgets/NumberAndDateRangeInputs.tsx | 20 + .../src/components/Mesa/Ui/DataCell.jsx | 38 +- .../src/components/Mesa/Ui/DataTable.jsx | 92 ++--- .../src/components/Mesa/Ui/RowCounter.jsx | 7 +- .../components/Mesa/style/Ui/DataTable.scss | 51 ++- .../libs/coreui/src/components/Mesa/types.ts | 6 + .../src/components/containers/Modal.tsx | 7 +- .../core/__test__/hooks/useAnalysis.test.tsx | 3 +- .../lib/core/components/FilterChipList.tsx | 6 +- .../components/filter/HistogramFilter.tsx | 111 +++-- .../libs/eda/src/lib/core/hooks/promise.ts | 12 +- packages/libs/eda/src/lib/core/hooks/state.ts | 12 + .../libs/eda/src/lib/map/MapVeuContainer.tsx | 6 +- .../eda/src/lib/map/analysis/MapAnalysis.tsx | 4 +- .../eda/src/lib/workspace/AllAnalyses.tsx | 18 +- .../src/lib/workspace/PublicAnalysesRoute.tsx | 12 +- .../src/lib/workspace/Subsetting/index.tsx | 35 +- packages/libs/multi-blast/config-overrides.js | 41 +- packages/libs/multi-blast/package.json | 7 +- packages/libs/multi-blast/src/index.tsx | 27 +- .../src/lib/components/BlastForm.scss | 6 +- .../src/lib/components/BlastForm.tsx | 175 +++++--- .../src/lib/components/BlastRequestError.tsx | 11 +- .../src/lib/components/BlastWorkspace.tsx | 42 +- .../src/lib/components/BlastWorkspaceAll.scss | 6 + .../src/lib/components/BlastWorkspaceAll.tsx | 1 + .../src/lib/components/BlastWorkspaceHelp.tsx | 20 +- .../src/lib/components/BlastWorkspaceNew.tsx | 13 +- .../lib/components/BlastWorkspaceResult.scss | 9 + .../lib/components/BlastWorkspaceResult.tsx | 176 +++++--- .../lib/components/DiamondResultContainer.tsx | 171 ++++++++ .../src/lib/components/ExpiredDiamondJob.tsx | 22 + .../lib/controllers/BlastWorkspaceRouter.tsx | 36 +- .../multi-blast/src/lib/hooks/allJobs.tsx | 18 +- .../src/lib/hooks/blastAlgorithms.tsx | 147 ++++--- .../src/lib/hooks/combinedResults.tsx | 6 +- .../src/lib/hooks/individualResult.tsx | 6 +- .../libs/multi-blast/src/lib/hooks/params.ts | 18 +- .../multi-blast/src/lib/utils/ServiceTypes.ts | 70 +++- .../libs/multi-blast/src/lib/utils/api.ts | 27 +- .../libs/multi-blast/src/lib/utils/params.ts | 71 +++- .../multi-blast/src/lib/utils/targetTypes.ts | 12 +- .../libs/preferred-organisms/package.json | 2 +- .../src/lib/utils/preferredOrganisms.ts | 2 +- .../UserDatasetDetailController.tsx | 16 +- .../src/lib/Hooks/wdkServiceWithVdi.ts | 18 - .../wdk-client/src/Components/Layout/Page.tsx | 35 +- .../src/Controllers/UserProfileController.tsx | 15 +- packages/libs/wdk-client/src/Core/Root.tsx | 184 +++++++-- .../wdk-client/src/Core/Style/wdk-Button.scss | 31 ++ packages/libs/wdk-client/src/Core/routes.tsx | 7 +- .../libs/wdk-client/src/Hooks/PromiseHook.ts | 12 +- .../wdk-client/src/Service/ServiceBase.ts | 46 ++- .../StoreModules/UnhandledErrorStoreModule.ts | 11 +- .../wdk-client/src/Views/Answer/Answer.jsx | 2 +- .../RecordTable/RecordTableDescription.jsx | 60 ++- .../src/Views/User/Profile/UserProfile.css | 78 ---- .../src/Views/User/Profile/UserProfile.jsx | 4 +- .../src/Views/User/Profile/UserProfile.scss | 95 +++++ .../Views/User/Profile/UserRegistration.jsx | 2 - .../src/Views/User/UserAccountForm.jsx | 11 + .../src/Views/User/UserFormContainer.jsx | 105 +---- .../src/Views/User/UserIdentity.jsx | 44 +- .../web-common/src/App/Header/Header.scss | 4 - packages/libs/web-common/src/bootstrap.js | 18 +- .../ApiApplicationSpecificProperties.jsx | 2 +- .../web-common/src/components/GalaxyTerms.jsx | 72 +--- .../src/components/JbrowseIframe.tsx | 2 +- .../src/components/ProfileModal.tsx | 76 ++++ .../src/components/homepage/Footer.tsx | 7 - ...atasetRecordClasses.DatasetRecordClass.jsx | 66 +-- packages/libs/web-common/src/routes.jsx | 2 + .../libs/web-common/src/styles/AllSites.scss | 3 - .../libs/web-common/src/styles/client.scss | 13 + packages/sites/genomics-site/.env.sample | 5 + packages/sites/genomics-site/package.json | 2 +- .../js/client/blastRoutes.tsx | 3 +- .../js/client/componentWrappers.jsx | 9 +- .../js/client/components/Jbrowse.tsx | 3 +- .../components/homepage/PageDescription.tsx | 25 +- .../components/homepage/VEuPathDBHomePage.tsx | 20 +- .../controllers/GenomicsIndexController.tsx | 13 + .../wdkCustomization/js/client/routes.jsx | 13 - .../genomics-site/webpack.config.local.mjs | 1 + packages/sites/ortho-site/.env.sample | 3 + packages/sites/ortho-site/.eslintrc | 2 +- packages/sites/ortho-site/package.json | 1 + .../js/client/blastRoutes.tsx | 47 +++ .../client/components/layout/OrthoMCLPage.tsx | 10 +- .../controllers/BlastWorkspaceResult.tsx | 2 + .../controllers/BlastWorkspaceRouter.tsx | 2 + .../js/client/pluginConfig.tsx | 25 ++ .../js/client/plugins/BlastForm.tsx | 2 + .../plugins/BlastQuestionController.tsx | 2 + .../js/client/proteinMappingRoutes.tsx | 36 ++ .../GroupRecordClasses.GroupRecordClass.scss | 30 +- .../js/client/records/Sequences.tsx | 387 ++++++++++-------- .../wdkCustomization/js/client/routes.tsx | 5 + .../wdkCustomization/js/client/services.tsx | 24 +- .../wdkCustomization/js/client/utils/tree.ts | 10 +- .../sites/ortho-site/webpack.config.local.mjs | 1 + yarn.lock | 17 +- 105 files changed, 2230 insertions(+), 1143 deletions(-) create mode 100644 packages/libs/eda/src/lib/core/hooks/state.ts create mode 100644 packages/libs/multi-blast/src/lib/components/DiamondResultContainer.tsx create mode 100644 packages/libs/multi-blast/src/lib/components/ExpiredDiamondJob.tsx delete mode 100644 packages/libs/user-datasets/src/lib/Hooks/wdkServiceWithVdi.ts create mode 100644 packages/libs/wdk-client/src/Core/Style/wdk-Button.scss delete mode 100644 packages/libs/wdk-client/src/Views/User/Profile/UserProfile.css create mode 100644 packages/libs/wdk-client/src/Views/User/Profile/UserProfile.scss create mode 100644 packages/libs/web-common/src/components/ProfileModal.tsx create mode 100644 packages/sites/genomics-site/webapp/wdkCustomization/js/client/controllers/GenomicsIndexController.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/blastRoutes.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/controllers/BlastWorkspaceResult.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/controllers/BlastWorkspaceRouter.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/plugins/BlastForm.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/plugins/BlastQuestionController.tsx create mode 100644 packages/sites/ortho-site/webapp/wdkCustomization/js/client/proteinMappingRoutes.tsx diff --git a/.gitignore b/.gitignore index daba46ee92..5fd88e0228 100644 --- a/.gitignore +++ b/.gitignore @@ -73,8 +73,11 @@ typings/ # dotenv environment variables file .env +.env~ .env.test +.env.test~ .env.local +.env.local~ # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/packages/libs/components/src/components/plotControls/SelectedRangeControl.tsx b/packages/libs/components/src/components/plotControls/SelectedRangeControl.tsx index cb59fa2059..d62ba0e616 100644 --- a/packages/libs/components/src/components/plotControls/SelectedRangeControl.tsx +++ b/packages/libs/components/src/components/plotControls/SelectedRangeControl.tsx @@ -22,6 +22,8 @@ export interface SelectedRangeControlProps enforceBounds?: boolean; /** show a clear button, optional, default is true */ showClearButton?: boolean; + /** display 'inclusive' after the from/to inputs */ + inclusive?: boolean; } export default function SelectedRangeControl({ @@ -33,6 +35,7 @@ export default function SelectedRangeControl({ enforceBounds = false, showClearButton = true, containerStyles, + inclusive = false, }: SelectedRangeControlProps) { const validator = enforceBounds ? undefined @@ -67,6 +70,7 @@ export default function SelectedRangeControl({ showClearButton={showClearButton} containerStyles={containerStyles} validator={validator} + inclusive={inclusive} /> ) : ( )} diff --git a/packages/libs/components/src/components/tidytree/TreeTable.tsx b/packages/libs/components/src/components/tidytree/TreeTable.tsx index ea22835553..480449ee80 100644 --- a/packages/libs/components/src/components/tidytree/TreeTable.tsx +++ b/packages/libs/components/src/components/tidytree/TreeTable.tsx @@ -33,6 +33,10 @@ export interface TreeTableProps { * 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]; @@ -52,7 +56,7 @@ const margin: [number, number, number, number] = [0, 10, 0, 10]; * - allow additional Mesa props and options to be passed */ export default function TreeTable(props: TreeTableProps) { - const { rowHeight, maxColumnWidth = 200, hideTree = false } = props; + const { rowHeight, maxColumnWidth = 200, hideTree = false, children } = props; const { rows, filteredRows } = props.tableProps; const rowStyleClassName = useMemo( @@ -73,6 +77,15 @@ export default function TreeTable(props: TreeTableProps) { [rowHeight] ); + const tree = hideTree ? null : ( + + ); + // tableState is just the tableProps with an extra CSS class // to make sure the height is consistent with the tree const tableState: MesaStateProps = { @@ -89,6 +102,7 @@ export default function TreeTable(props: TreeTableProps) { inlineUseTooltips: true, inlineMaxHeight: `${rowHeight}px`, inlineMaxWidth: `${maxColumnWidth}px`, + marginContent: tree, }, }; @@ -97,31 +111,7 @@ export default function TreeTable(props: TreeTableProps) { // then the table container styling will need // { marginLeft: hideTree ? props.treeProps.width : 0 } // to stop the table jumping around horizontally - return ( -
- {!hideTree && ( - - )} -
- -
-
- ); + return ; } function mergeDeriveRowClassName( diff --git a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx index d1d6159b0e..2f1e919622 100755 --- a/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx +++ b/packages/libs/components/src/components/widgets/NumberAndDateRangeInputs.tsx @@ -45,6 +45,8 @@ export type BaseProps = { disabled?: boolean; /** specify the height of the input element */ inputHeight?: number; + /** add 'inclusive' text after the second range box */ + inclusive?: boolean; }; export type NumberRangeInputProps = BaseProps & { step?: number }; @@ -89,6 +91,7 @@ function BaseInput({ // add disabled prop to disable input fields disabled = false, inputHeight, + inclusive = false, ...props }: BaseInputProps) { if (validator && required) @@ -268,6 +271,23 @@ function BaseInput({ inputHeight={inputHeight} /> )} + {inclusive && ( +
+ {/* change margin */} +
+ + inclusive + +
+
+ )} {showClearButton && (