diff --git a/packages_rs/nextclade-web/src/components/Results/ColumnCustomNodeAttr.tsx b/packages_rs/nextclade-web/src/components/Results/ColumnCustomNodeAttr.tsx
index e55d43d3d..a42c5c9a1 100644
--- a/packages_rs/nextclade-web/src/components/Results/ColumnCustomNodeAttr.tsx
+++ b/packages_rs/nextclade-web/src/components/Results/ColumnCustomNodeAttr.tsx
@@ -1,9 +1,15 @@
-import React from 'react'
-
-import { get } from 'lodash'
+import React, { useCallback, useMemo, useState } from 'react'
+import { get, values } from 'lodash'
+import styled from 'styled-components'
import type { AnalysisResult } from 'src/types'
import { getSafeId } from 'src/helpers/getSafeId'
+import { TableSlimWithBorders } from 'src/components/Common/TableSlim'
+import { Tooltip } from './Tooltip'
+
+const Table = styled(TableSlimWithBorders)`
+ margin-top: 1rem;
+`
export interface ColumnCustomNodeAttrProps {
sequence: AnalysisResult
@@ -11,14 +17,43 @@ export interface ColumnCustomNodeAttrProps {
}
export function ColumnCustomNodeAttr({ sequence, attrKey }: ColumnCustomNodeAttrProps) {
- const { index, seqName, customNodeAttributes } = sequence
- const attrValue = get(customNodeAttributes, attrKey, '')
+ const [showTooltip, setShowTooltip] = useState(false)
+ const onMouseEnter = useCallback(() => setShowTooltip(true), [])
+ const onMouseLeave = useCallback(() => setShowTooltip(false), [])
+
+ const { id, attrValue, secondaryValues } = useMemo(() => {
+ const { index, seqName, customNodeAttributes } = sequence
+ const attr = get(customNodeAttributes, attrKey, undefined)
+
+ const secondaryValues = values(attr?.secondaryValues ?? {}).map(({ key, displayName, value }) => (
+
+ {displayName} |
+ {value} |
+
+ ))
+
+ if (attr) {
+ secondaryValues.unshift(
+
+ {attr.displayName} |
+ {attr.value} |
+
,
+ )
+ }
+
+ const id = getSafeId('col-custom-attr', { index, seqName, attrKey })
- const id = getSafeId('col-custom-attr', { index, seqName, attrKey })
+ return { id, attrValue: attr?.value, secondaryValues }
+ }, [attrKey, sequence])
return (
-
- {attrValue}
-
+ <>
+
+ {attrValue}
+
+ 0 && showTooltip} target={id} wide fullWidth>
+
+
+ >
)
}
diff --git a/packages_rs/nextclade-web/src/types.ts b/packages_rs/nextclade-web/src/types.ts
index bfed398f8..7a34a3764 100644
--- a/packages_rs/nextclade-web/src/types.ts
+++ b/packages_rs/nextclade-web/src/types.ts
@@ -380,11 +380,24 @@ export interface AnalysisResult {
privateAaMutations: Record
coverage: number
qc: QcResult
- customNodeAttributes: Record
+ customNodeAttributes: Record
warnings: PeptideWarning[]
missingGenes: string[]
}
+export interface SecondaryCustomNodeAttrValue {
+ key: string
+ displayName: string
+ value: string
+}
+
+export interface CustomNodeAttrValue {
+ key: string
+ displayName: string
+ value: string
+ secondaryValues: Record
+}
+
export interface AnalysisError {
index: number
seqName: string
diff --git a/packages_rs/nextclade/src/io/nextclade_csv.rs b/packages_rs/nextclade/src/io/nextclade_csv.rs
index 61e4d73b1..ae7803b08 100644
--- a/packages_rs/nextclade/src/io/nextclade_csv.rs
+++ b/packages_rs/nextclade/src/io/nextclade_csv.rs
@@ -182,7 +182,7 @@ impl NextcladeResultsCsvWriter {
custom_node_attributes
.clone()
.into_iter()
- .try_for_each(|(key, val)| self.add_entry(&key, &val))?;
+ .try_for_each(|(key, attr)| self.add_entry(&key, &attr.value))?;
self.add_entry("seqName", seq_name)?;
self.add_entry("clade", clade)?;
diff --git a/packages_rs/nextclade/src/run/nextclade_run_one.rs b/packages_rs/nextclade/src/run/nextclade_run_one.rs
index 40e8f8776..eb92c47bf 100644
--- a/packages_rs/nextclade/src/run/nextclade_run_one.rs
+++ b/packages_rs/nextclade/src/run/nextclade_run_one.rs
@@ -117,7 +117,7 @@ pub fn nextclade_run_one(
let clade = node.clade();
let clade_node_attr_keys = tree.clade_node_attr_descs();
- let clade_node_attrs = node.get_clade_node_attrs(clade_node_attr_keys);
+ let custom_node_attributes = node.get_clade_node_attrs(clade_node_attr_keys);
let private_nuc_mutations = find_private_nuc_mutations(
node,
@@ -203,7 +203,7 @@ pub fn nextclade_run_one(
divergence,
coverage,
qc,
- custom_node_attributes: clade_node_attrs,
+ custom_node_attributes,
nearest_node_id,
is_reverse_complement,
},
diff --git a/packages_rs/nextclade/src/tree/tree.rs b/packages_rs/nextclade/src/tree/tree.rs
index 6fad9e786..f7726f440 100644
--- a/packages_rs/nextclade/src/tree/tree.rs
+++ b/packages_rs/nextclade/src/tree/tree.rs
@@ -2,10 +2,9 @@ use crate::io::aa::Aa;
use crate::io::fs::read_file_to_string;
use crate::io::json::json_parse;
use crate::io::nuc::Nuc;
+use crate::types::outputs::{CustomNodeAttr, SecondaryCustomNodeAttrValue};
use eyre::{Report, WrapErr};
-use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
-use std::any::Any;
use std::collections::BTreeMap;
use std::path::Path;
use std::slice::Iter;
@@ -144,29 +143,75 @@ impl AuspiceTreeNode {
self.node_attrs.clade_membership.value.clone()
}
- /// Extracts clade-like node attributes, given a list of key descriptions
- pub fn get_clade_node_attrs(&self, clade_node_attr_keys: &[CladeNodeAttrKeyDesc]) -> BTreeMap {
- clade_node_attr_keys
+ fn _extract_clade_node_attr(&self, key: &str) -> Option {
+ match self.node_attrs.other.get(key) {
+ Some(attr) => attr.get("value").and_then(|value| value.as_str().map(str::to_owned)),
+ None => None,
+ }
+ }
+
+ fn _extract_clade_node_attrs(&self, meta_attr: &CladeNodeAttrKeyDesc) -> Option<(String, CustomNodeAttr)> {
+ let secondary_values: BTreeMap = meta_attr
+ .secondary_attrs
.iter()
- .filter_map(|attr| {
- let key = &attr.name;
- let attr_obj = self.node_attrs.other.get(key);
- match attr_obj {
- Some(attr) => attr.get("value"),
- None => None,
- }
- .and_then(|val| val.as_str().map(|val| (key.clone(), val.to_owned())))
+ .filter_map(|sec_attr| {
+ self._extract_clade_node_attr(&sec_attr.name).map(|value| {
+ (
+ sec_attr.name.clone(),
+ SecondaryCustomNodeAttrValue {
+ key: sec_attr.name.clone(),
+ display_name: sec_attr.display_name.clone(),
+ value,
+ },
+ )
+ })
})
+ .collect();
+
+ let key = &meta_attr.name;
+ self._extract_clade_node_attr(key).map(|value| {
+ (
+ key.clone(),
+ CustomNodeAttr {
+ key: key.clone(),
+ display_name: meta_attr.display_name.clone(),
+ value,
+ secondary_values,
+ },
+ )
+ })
+ }
+
+ /// Extracts clade-like node attribute values, given a list of key descriptions from tree meta
+ pub fn get_clade_node_attrs(
+ &self,
+ clade_node_attr_key_descs: &[CladeNodeAttrKeyDesc],
+ ) -> BTreeMap {
+ clade_node_attr_key_descs
+ .iter()
+ .filter_map(|meta_attr| self._extract_clade_node_attrs(meta_attr))
.collect()
}
}
+#[derive(Clone, Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SecondaryCustomNodeAttr {
+ name: String,
+ display_name: String,
+ description: String,
+}
+
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CladeNodeAttrKeyDesc {
pub name: String,
pub display_name: String,
pub description: String,
+
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ #[serde(default)]
+ pub secondary_attrs: Vec,
}
#[derive(Clone, Serialize, Deserialize, Validate, Debug)]
diff --git a/packages_rs/nextclade/src/types/outputs.rs b/packages_rs/nextclade/src/types/outputs.rs
index 11a9a092a..287740176 100644
--- a/packages_rs/nextclade/src/types/outputs.rs
+++ b/packages_rs/nextclade/src/types/outputs.rs
@@ -34,6 +34,23 @@ pub struct NextalignOutputs {
pub is_reverse_complement: bool,
}
+#[derive(Clone, Serialize, Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SecondaryCustomNodeAttrValue {
+ pub key: String,
+ pub display_name: String,
+ pub value: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CustomNodeAttr {
+ pub key: String,
+ pub display_name: String,
+ pub value: String,
+ pub secondary_values: BTreeMap,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NextcladeOutputs {
@@ -76,7 +93,7 @@ pub struct NextcladeOutputs {
pub divergence: f64,
pub coverage: f64,
pub qc: QcResult,
- pub custom_node_attributes: BTreeMap,
+ pub custom_node_attributes: BTreeMap,
pub nearest_node_id: usize,
pub is_reverse_complement: bool,
}