From 91751a9e431a0ae61c527d41e9c6392a163e78a7 Mon Sep 17 00:00:00 2001 From: afonso Date: Tue, 28 May 2024 17:37:31 +0100 Subject: [PATCH] ESCKAN-53 fix: Fix summary details heatmap --- src/components/Connections.tsx | 16 +- src/components/common/Heatmap.tsx | 240 +++++++++++++---------- src/components/common/HeatmapTooltip.tsx | 128 +++++------- src/components/common/Types.ts | 7 +- src/services/summaryHeatmapService.ts | 23 ++- 5 files changed, 220 insertions(+), 194 deletions(-) diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 772528d..0e9824a 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -116,12 +116,18 @@ function Connections() { const handleCellClick = (x: number, y: number, yId: string): void => { // when the heatmap cell is clicked setSelectedCell({ x, y }); - setConnectionPage(1) const row = connectionsMap.get(yId); - if (selectedConnectionSummary && Object.keys(selectedConnectionSummary.connections).length !== 0 && row) { - setShowConnectionDetails(SummaryType.DetailedSummary) - const ksMap = getKnowledgeStatementMap(row[x].ksIds, knowledgeStatements); - setUniqueKS(ksMap); + if (row) { + setConnectionPage(1) + const ksIds = Object.values(row[x]).reduce((acc, phenotypeData) => { + return new Set([...acc, ...phenotypeData.ksIds]); + }, new Set()); + + if (selectedConnectionSummary && Object.keys(selectedConnectionSummary.connections).length !== 0) { + setShowConnectionDetails(SummaryType.DetailedSummary); + const ksMap = getKnowledgeStatementMap(ksIds, knowledgeStatements); + setUniqueKS(ksMap); + } } } diff --git a/src/components/common/Heatmap.tsx b/src/components/common/Heatmap.tsx index 648c309..bbd270c 100644 --- a/src/components/common/Heatmap.tsx +++ b/src/components/common/Heatmap.tsx @@ -1,16 +1,16 @@ -import React, { FC, useCallback, useMemo, } from "react"; -import { Box, Typography } from "@mui/material"; -import { vars } from "../../theme/variables"; +import React, {FC, useCallback, useMemo,} from "react"; +import {Box, Typography} from "@mui/material"; +import {vars} from "../../theme/variables"; import CollapsibleList from "./CollapsibleList"; import HeatMap from "react-heatmap-grid"; -import HeatmapTooltip from "./HeatmapTooltip"; -import { HierarchicalItem, PhenotypeType, PhenotypeKsIdMap } from "./Types.ts"; -import { getNormalizedValueForMinMax } from "../../services/summaryHeatmapService.ts"; -import { generateYLabelsAndIds, getPhenotypeColors } from "../../services/heatmapService.ts"; -import { OTHER_PHENOTYPE_LABEL } from "../../settings.ts"; +import HeatmapTooltip, {HeatmapTooltipRow} from "./HeatmapTooltip"; +import {HierarchicalItem, PhenotypeType, PhenotypeKsIdMap} from "./Types.ts"; +import {getNormalizedValueForMinMax} from "../../services/summaryHeatmapService.ts"; +import {generateYLabelsAndIds, getPhenotypeColors} from "../../services/heatmapService.ts"; +import {OTHER_PHENOTYPE_LABEL} from "../../settings.ts"; -const { gray50, primaryPurple500, gray100A, gray500 } = vars; +const {gray50, primaryPurple500, gray100A, gray500} = vars; interface HeatmapGridProps { xAxis: string[]; @@ -27,28 +27,49 @@ interface HeatmapGridProps { const prepareSecondaryHeatmapData = (data?: PhenotypeKsIdMap[][]): number[][] => { if (!data) return []; - return data.map(row => row.map(cell => cell.ksIds.size)); + return data.map(row => row.map(cell => { + return Object.values(cell).reduce((acc, phenotype) => acc + phenotype.ksIds.size, 0); + })); } const HeatmapGrid: FC = ({ - xAxis, yAxis, setYAxis, - xAxisLabel, yAxisLabel, - onCellClick, selectedCell, heatmapData, secondaryHeatmapData, phenotypes -}) => { - const secondary = secondaryHeatmapData ? true : false; + xAxis, yAxis, setYAxis, + xAxisLabel, yAxisLabel, + onCellClick, selectedCell, heatmapData, secondaryHeatmapData, phenotypes + }) => { + + const secondary = !!secondaryHeatmapData; + const yAxisData = generateYLabelsAndIds(yAxis); const heatmapMatrixData = useMemo(() => { return secondary ? prepareSecondaryHeatmapData(secondaryHeatmapData) : heatmapData; }, [secondary, secondaryHeatmapData, heatmapData]); + const xLabelToIndex = useMemo(() => { + const lookup: { [key: string]: number } = {}; + xAxis.forEach((label, index) => { + lookup[label] = index; + }); + return lookup; + }, [xAxis]); + + const yLabelToIndex = useMemo(() => { + const lookup: { [key: string]: number } = {}; + yAxisData.labels.forEach((label, index) => { + lookup[label] = index; + }); + return lookup; + }, [yAxisData.labels]); + + const handleCollapseClick = useCallback((item: HierarchicalItem) => { const updateList = (list: HierarchicalItem[], selectedItem: HierarchicalItem): HierarchicalItem[] => { return list?.map(listItem => { if (listItem.label === selectedItem.label) { - return { ...listItem, expanded: !listItem.expanded }; + return {...listItem, expanded: !listItem.expanded}; } else if (listItem.children) { - return { ...listItem, children: updateList(listItem.children, selectedItem) }; + return {...listItem, children: updateList(listItem.children, selectedItem)}; } return listItem; }); @@ -57,9 +78,6 @@ const HeatmapGrid: FC = ({ setYAxis(updatedList); }, [yAxis, setYAxis]); - const yAxisData = generateYLabelsAndIds(yAxis); - - const handleCellClick = (x: number, y: number) => { const ids = yAxisData.ids if (onCellClick) { @@ -74,11 +92,11 @@ const HeatmapGrid: FC = ({ ) => { // Gets the color for secondary heatmap cell based on the phenotypes if (phenotypes && secondary && secondaryHeatmapData && secondaryHeatmapData[_y] && secondaryHeatmapData[_y][_x]) { - const heatmapCellPhenotypes = secondaryHeatmapData[_y][_x]?.phenotypes; - + const heatmapCellPhenotypes = secondaryHeatmapData[_y][_x]; const phenotypeColorsSet = new Set(); - heatmapCellPhenotypes.forEach(phenotype => { - const phnColor = phenotypes[phenotype]?.color + + Object.keys(heatmapCellPhenotypes).forEach(phenotype => { + const phnColor = phenotypes[phenotype]?.color; if (phnColor) { phenotypeColorsSet.add(phnColor); } else { @@ -88,12 +106,24 @@ const HeatmapGrid: FC = ({ const phenotypeColors = Array.from(phenotypeColorsSet); const phenotypeColor = getPhenotypeColors(normalizedValue, phenotypeColors); - return phenotypeColor ? phenotypeColor : `rgba(131, 0, 191, ${normalizedValue})`; } return `rgba(0, 0, 0, ${normalizedValue})` }; + const getTooltipRows = (xIndex: number, yIndex: number): HeatmapTooltipRow[] => { + if (secondary && secondaryHeatmapData && phenotypes && secondaryHeatmapData[yIndex] && secondaryHeatmapData[yIndex][xIndex]) { + const heatmapCellPhenotypes = secondaryHeatmapData[yIndex][xIndex]; + + return Object.keys(heatmapCellPhenotypes).map(phenotype => ({ + color: phenotypes[phenotype]?.color || phenotypes[OTHER_PHENOTYPE_LABEL].color, + name: phenotype, + count: heatmapCellPhenotypes[phenotype].ksIds.size + })); + } + return []; + }; + return ( @@ -123,80 +153,80 @@ const HeatmapGrid: FC = ({ )} div:first-of-type': { - '& > div:last-of-type': { - '& > div': { - '& > div': { - '&:not(:first-of-type)': { - '&:hover': { - boxShadow: '0rem 0.0625rem 0.125rem 0rem #1018280F, 0rem 0.0625rem 0.1875rem 0rem #1018281A' - }, - '& > div': { - paddingTop: '0 !important', - height: '100%', + sx={{ + '& > div:first-of-type': { + '& > div:last-of-type': { + '& > div': { + '& > div': { + '&:not(:first-of-type)': { + '&:hover': { + boxShadow: '0rem 0.0625rem 0.125rem 0rem #1018280F, 0rem 0.0625rem 0.1875rem 0rem #1018281A' + }, + '& > div': { + paddingTop: '0 !important', + height: '100%', - '& > div': { - height: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - } - } - }, - '&:first-of-type': { - width: '15.625rem', - flex: 'none !important', - '& > div': { - opacity: 0 - } - } - } - } - }, - '& > div:first-of-type': { - '& > div': { - writingMode: 'vertical-lr', - lineHeight: 1, - display: 'flex', - justifyContent: 'flex-end', - alignItems: 'center', - fontSize: '0.875rem', - fontWeight: '500', - marginLeft: '0.125rem', - padding: '0.875rem 0', - position: 'relative', - borderRadius: '0.25rem', - minWidth: '2rem !important', + '& > div': { + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + } + } + }, + '&:first-of-type': { + width: '15.625rem', + flex: 'none !important', + '& > div': { + opacity: 0 + } + } + } + } + }, + '& > div:first-of-type': { + '& > div': { + writingMode: 'vertical-lr', + lineHeight: 1, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + fontSize: '0.875rem', + fontWeight: '500', + marginLeft: '0.125rem', + padding: '0.875rem 0', + position: 'relative', + borderRadius: '0.25rem', + minWidth: '2rem !important', - '&:hover': { - background: gray50, - '&:before': { - content: '""', - width: '100%', - height: '0.0625rem', - background: primaryPurple500, - position: 'absolute', - top: '-0.25rem', - left: 0 - }, - }, + '&:hover': { + background: gray50, + '&:before': { + content: '""', + width: '100%', + height: '0.0625rem', + background: primaryPurple500, + position: 'absolute', + top: '-0.25rem', + left: 0 + }, + }, - '&:first-of-type': { - marginLeft: 0, - width: '15.625rem', - flex: 'none !important', - '&:hover': { - background: 'none', - '&:before': { - display: 'none' - } - } - } - } - } - } - }}> + '&:first-of-type': { + marginLeft: 0, + width: '15.625rem', + flex: 'none !important', + '&:hover': { + background: 'none', + '&:before': { + display: 'none' + } + } + } + } + } + } + }}> { yAxisData && heatmapMatrixData && heatmapMatrixData.length > 0 && heatmapMatrixData[0].length > 0 && ( = ({ }} - cellRender={(value: number, x: number, y: number) => ( - <> + cellRender={(value: number, xLabel: string, yLabel: string) => { + const xIndex = xLabelToIndex[xLabel]; + const yIndex = yLabelToIndex[yLabel]; + return ( 'rgba(0,0,0,0)'} + x={xLabel} + y={yLabel} + connections={value} + rows={secondary ? getTooltipRows(xIndex, yIndex) : undefined} /> - - )} + ); + }} /> )} - + diff --git a/src/components/common/HeatmapTooltip.tsx b/src/components/common/HeatmapTooltip.tsx index 71ce1e4..fd5ed6c 100644 --- a/src/components/common/HeatmapTooltip.tsx +++ b/src/components/common/HeatmapTooltip.tsx @@ -1,14 +1,19 @@ import { FC } from "react"; import { vars } from "../../theme/variables"; import { Box, Tooltip, Typography } from "@mui/material"; -const { gray25, gray300 } = vars; +const { gray25, gray300 } = vars; + +export interface HeatmapTooltipRow { + color?: string; + name?: string; + count: number; +} interface HeatmapTooltipProps { - value: number; - x: number; - y: number; - secondary?: boolean; - getCellBgColor: (value: number) => string; + x: string; + y: string; + connections: number; + rows?: HeatmapTooltipRow[]; } const commonHeadingStyles = { @@ -24,79 +29,52 @@ const commonTextStyles = { lineHeight: '1.125rem', color: gray25, } -const HeatmapTooltip: FC = ({value, x, y, secondary, getCellBgColor}) => { - let data; - if (secondary) { - data = ( - - {`${y} -> ${x}`} - - - - - - {x} - - - - {value} - - +const HeatmapTooltip: FC = ({ x, y, connections, rows }) => { + const hasRows = rows && rows.length > 0; - - - - - {y} - - - - {value} - - - - - ) - } else { - data = ( - - {`${y} -> ${x}`} - {`${value}`} connections - - ) - } return ( - - {value} - + + {`${y} -> ${x}`} + + {hasRows ? ( + rows.map((row, index) => ( + + + + + {row.name || ''} + + + + {row.count} + + + )) + ) : ( + + {`${connections} connections`} + + )} + + + } + > + {x} + ) } export default HeatmapTooltip; - \ No newline at end of file diff --git a/src/components/common/Types.ts b/src/components/common/Types.ts index 30ab41d..cf4610b 100644 --- a/src/components/common/Types.ts +++ b/src/components/common/Types.ts @@ -15,8 +15,11 @@ export type LabelIdPair = { labels: string[], ids: string[] }; export type KsMapType = Record; -export type PhenotypeKsIdMap = { phenotypes: string[], ksIds: Set }; - +export type PhenotypeKsIdMap = { + [phenotype: string]: { + ksIds: Set; + }; +}; // SummaryType - Three types of summary views - default - instruction. // When user clicks the primary heatmap, the summary view will be displayed. // When user clicks the secondary heatmap, the detailed summary view will be displayed. diff --git a/src/services/summaryHeatmapService.ts b/src/services/summaryHeatmapService.ts index 436eb81..e2dfa02 100644 --- a/src/services/summaryHeatmapService.ts +++ b/src/services/summaryHeatmapService.ts @@ -141,14 +141,18 @@ export function calculateSecondaryConnections( } const node = hierarchicalNodes[nodeId]; - const result: PhenotypeKsIdMap[] = Object.values(endorgans).map(() => ({ phenotypes: [], ksIds: new Set() })); + const result: PhenotypeKsIdMap[] = Object.values(endorgans).map(() => ({})); if (node.children && node.children.size > 0) { node.children.forEach(childId => { const childConnections = computeNodeConnections(childId); childConnections.forEach((child, index) => { - result[index].phenotypes = [...new Set([...result[index].phenotypes, ...child.phenotypes])]; - result[index].ksIds = new Set([...result[index].ksIds, ...child.ksIds]); + Object.keys(child).forEach(phenotype => { + if (!result[index][phenotype]) { + result[index][phenotype] = { ksIds: new Set() }; + } + result[index][phenotype].ksIds = new Set([...result[index][phenotype].ksIds, ...child[phenotype].ksIds]); + }); }); }); } else if (node.destinationDetails) { @@ -160,12 +164,13 @@ export function calculateSecondaryConnections( const knowledgeStatementIds = Array.from(node.destinationDetails[endOrganIRI]) .filter(ksId => ksId in knowledgeStatements); - const ksPhenotypes = knowledgeStatementIds.map(ksId => { - return knowledgeStatements[ksId].phenotype ? knowledgeStatements[ksId].phenotype : OTHER_PHENOTYPE_LABEL - }) - - result[index].phenotypes = [...new Set(ksPhenotypes)]; - result[index].ksIds = new Set([...result[index].ksIds, ...knowledgeStatementIds]); + knowledgeStatementIds.forEach(ksId => { + const phenotype = knowledgeStatements[ksId].phenotype ? knowledgeStatements[ksId].phenotype : OTHER_PHENOTYPE_LABEL; + if (!result[index][phenotype]) { + result[index][phenotype] = { ksIds: new Set() }; + } + result[index][phenotype].ksIds.add(ksId); + }); } }); }