Skip to content

Commit

Permalink
Merge pull request #265 from helxplatform/feature/dug-1.0-ui
Browse files Browse the repository at this point in the history
Dug 1.0 UI features
  • Loading branch information
YaphetKG authored Aug 15, 2023
2 parents 574ae77 + b030418 commit 63383ff
Show file tree
Hide file tree
Showing 15 changed files with 329 additions and 77 deletions.
8 changes: 8 additions & 0 deletions src/components/search/concept-modal/concept-modal.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@
margin-bottom: -16px;
}

.explanation-score-progress {
display: inline-flex !important;
align-items: center;
}
.explanation-score-progress .ant-progress-outer {
display: inline-flex;
}

@media (min-width: 600px) {
.tab-name {
display: inline;
Expand Down
16 changes: 9 additions & 7 deletions src/components/search/concept-modal/concept-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import CustomIcon, {
ExportOutlined as ExternalLinkIcon,
FullscreenOutlined as FullscreenLayoutIcon,
UnorderedListOutlined as CdesIcon,
QuestionCircleOutlined as ExplanationIcon,
ArrowLeftOutlined, InfoCircleOutlined
} from '@ant-design/icons'
import { CdesTab, OverviewTab, StudiesTab, KnowledgeGraphsTab, TranQLTab } from './tabs'
import { CdesTab, OverviewTab, StudiesTab, KnowledgeGraphsTab, TranQLTab, ExplanationTab } from './tabs'
import { useHelxSearch } from '../'
import { BouncingDots } from '../../'
import { useAnalytics, useEnvironment } from '../../../contexts'
Expand Down Expand Up @@ -92,16 +93,17 @@ export const ConceptModalBody = ({ result }) => {
)
const cdeTitle = (
<div style={{ display: "inline" }}>
CDEs { cdes ? `(${ Object.keys(cdes).length })` : <BouncingDots /> }
CDEs { !cdesLoading ? `(${ Object.keys(cdes ?? []).length })` : <BouncingDots /> }
</div>
)

const tabs = {
'overview': { title: 'Overview', icon: <OverviewIcon />, content: <OverviewTab result={ result } />, },
'studies': { title: studyTitle, icon: <StudiesIcon />, content: <StudiesTab studies={ studies } />, },
'cdes': { title: cdeTitle, icon: <CdesIcon />, content: <CdesTab cdes={ cdes } cdeRelatedConcepts={ cdeRelatedConcepts } loading={ cdesLoading } /> },
'kgs': { title: 'Knowledge Graphs', icon: <KnowledgeGraphsIcon />, content: <KnowledgeGraphsTab graphs={ graphs } />, },
'tranql': { title: 'TranQL', icon: <TranQLIcon />, content: <TranQLTab result={ result } graphs = { graphs } /> }
'overview': { title: 'Overview', icon: <OverviewIcon />, content: <OverviewTab result={ result } />, },
'studies': { title: studyTitle, icon: <StudiesIcon />, content: <StudiesTab studies={ studies } />, },
'cdes': { title: cdeTitle, icon: <CdesIcon />, content: <CdesTab cdes={ cdes } cdeRelatedConcepts={ cdeRelatedConcepts } loading={ cdesLoading } /> },
'kgs': { title: 'Knowledge Graphs', icon: <KnowledgeGraphsIcon />, content: <KnowledgeGraphsTab graphs={ graphs } />, },
'explanation': { title: 'Explanation', icon: <ExplanationIcon />, content: <ExplanationTab result={ result } /> },
'tranql': { title: 'TranQL', icon: <TranQLIcon />, content: <TranQLTab result={ result } graphs = { graphs } /> }
}
const links = {
'robokop' : { title: 'ROBOKOP', icon: <RobokopIcon/>, url: "https://robokop.renci.org/" }
Expand Down
2 changes: 1 addition & 1 deletion src/components/search/concept-modal/tabs/cdes/cde-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => {
), [cdeRelatedConcepts, cde])

const Highlighter = useCallback(({ ...props }) => (
<_Highlighter searchWords={highlight} {...props}/>
<_Highlighter autoEscape={ true } searchWords={highlight} {...props}/>
), [highlight])

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const RelatedConceptTag = ({ concept, highlight }) => {
setShowOptions(true)
}}>
<Text key={concept.name}>
<Highlighter searchWords={highlight} textToHighlight={ concept.name } />
<Highlighter autoEscape={ true } searchWords={highlight} textToHighlight={ concept.name } />
</Text>
</Tag>
</Popover>
Expand Down
250 changes: 250 additions & 0 deletions src/components/search/concept-modal/tabs/explanation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import { useMemo, useRef, useState } from 'react'
import { Button, Checkbox, Divider, Progress, Space, Switch, Typography } from 'antd'
import { presetPalettes } from '@ant-design/colors'
import { Pie } from '@ant-design/plots'
import { InfoTooltip } from '../../..'

const { Title, Text } = Typography

// Show the first 6 score components, unless show more is pressed.
const SHOW_MORE_CUTOFF = 6

const palette = [
presetPalettes.blue,
presetPalettes.gold,
presetPalettes.green,
presetPalettes.purple,
presetPalettes.volcano,
presetPalettes.cyan,
presetPalettes.magenta,
presetPalettes.yellow,
presetPalettes.red,
presetPalettes.lime,
presetPalettes.geekblue
].map((palette) => palette[5])

const parseScoreDetail = ({ value, description, details }) => {
if (value === 0) return null
if (description === "sum of:") {
return details.flatMap((detail) => parseScoreDetail(detail))
}

const explainPattern = /^weight\((?<fieldName>.+):(?<searchTerm>.+) in (?<segmentNumber>\d+)\) \[(?<similarityMetric>.+)\], result of:$/
const match = description.match(explainPattern)
if (match) {
let { fieldName, searchTerm, segmentNumber, similarityMetric } = match.groups
if (searchTerm.startsWith(`"`) && searchTerm.endsWith(`"`)) searchTerm = searchTerm.slice(1, -1)
return {
fieldMatch: fieldName,
termMatch: searchTerm,
source: description,
value
}
} else {
console.log("Failed to parse score explanation:", description)
return {
fieldMatch: null,
termMatch: null,
source: description,
value
}
}
}

export const ExplanationTab = ({ result }) => {
const { explanation } = result

const [showMore, setShowMore] = useState(false)
const [advancedBreakdown, setAdvancedBreakdown] = useState(false)
const pieRef = useRef()
const totalScore = useMemo(() => explanation.value, [explanation.value])
const scoreData = useMemo(() => (parseScoreDetail(explanation)
.filter((detail) => detail !== null)
// Reduce duplicate details into single details.
.reduce((acc, cur) => {
const existingDetail = acc.find((detail) => detail.source === cur.source)
if (!existingDetail) acc.push(cur)
else {
// If the exact detail already exists, add the scores.
existingDetail.value += cur.value
}
return acc
}, [])
// Reduce details down further into single field matches, if advanced breakdown is disabled.
// E.g. `name:heart` and `name:heart disease` would get merged into the same detail at this step.
.reduce((acc, cur) => {
if (advancedBreakdown) {
acc.push(cur)
return acc
}
const existingDetailWithField = acc.find((detail) => detail.fieldMatch === cur.fieldMatch)
if (!existingDetailWithField) acc.push(cur)
else {
// If a detail exists with the current field match, and not in advanced breakdown, add the scores.
if (Array.isArray(existingDetailWithField.termMatch)) existingDetailWithField.termMatch.push(cur.termMatch)
else existingDetailWithField.termMatch = [existingDetailWithField.termMatch, cur.termMatch]
existingDetailWithField.value += cur.value
}
return acc
}, [])
// Reduce details into chart data
.reduce((acc, cur) => {
const { fieldMatch, termMatch, source, value } = cur
const [fieldMatchName, fieldMatchDescription] = (
fieldMatch === "name" ? ["Name", "The name of this concept"]
: fieldMatch === "description" ? ["Description", "The description of this concept"]
: fieldMatch === "search_terms" ? ["Search terms", "Synonymous names for this concept"]
: fieldMatch === "optional_terms" ? ["Related terms", "Search terms for concepts related to this concept"]
: ["", ""]
)
const advancedBreakdownString = ` ${ fieldMatchName.endsWith("s") ? "contain" : "contains"} the term "${ termMatch }"`
if (fieldMatch && termMatch) acc.push({
name: `${ fieldMatchName }`,
description: `${ fieldMatchDescription }${ advancedBreakdown ? advancedBreakdownString : ""}`,
key: source,
matchedField: fieldMatch,
matchedTerms: termMatch,
failedParse: false,
value
})
else acc.push({
name: "Unknown",
description: "Could not parse explanation for this score component.",
key: source,
matchedField: null,
matchedTerms: null,
failedParse: true,
value
})
return acc
}, [])
.sort((a, b) => b.value - a.value)
), [explanation, advancedBreakdown])
const colorMap = useMemo(() => palette.slice(0, scoreData.length), [scoreData])
const pieConfig = useMemo(() => ({
data: scoreData,
// appendPadding: 10,
autoFit: false,
height: 310,
width: 400,
angleField: "value",
colorField: "key",
radius: 1,
innerRadius: 0.70,
renderer: "svg",
legend: false,
statistic: {
title: {
style: {
fontSize: "1em"
}
},
content: {
offsetY: 4,
content: explanation.value.toFixed(1),
style: {
fontSize: "1em"
}
}
},
label: {
type: "inner",
offset: "-50%",
style: {
textAlign: "center"
},
autoRotate: false,
formatter: (v) => v.value.toFixed(1)
},
tooltip: {
formatter: (datum) => {
const { name, value } = scoreData.find((d) => d.key === datum.key)
return { name, value }
}
},
color: colorMap
}), [scoreData, colorMap, explanation])
return (
<Space direction="vertical" size={ 8 }>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div style={{ display: "flex", flexDirection: "column" }}>
<Text style={{
fontSize: 15,
fontWeight: 500
}}>
Score breakdown
</Text>
<div>
<Text style={{ fontSize: 13 }} italic>Why am I seeing this result?</Text>
</div>
</div>
<div style={{ display: "flex", flexFlow: "row", alignItems: "center", marginRight: 16 }}>
<Checkbox checked={ advancedBreakdown } onChange={ () => setAdvancedBreakdown(!advancedBreakdown) } />
<span style={{ color: "rgba(0, 0, 0, 0.45)", fontStyle: "italic", fontSize: 12, marginLeft: 8 }}>Advanced</span>
</div>
</div>
<div style={{ marginTop: 16, display: "flex", paddingRight: 16 }}>
<Pie ref={ pieRef } { ...pieConfig } />
<Space direction="vertical" style={{ marginLeft: 16 }} size={ 12 }>
{
(showMore ? scoreData : scoreData.slice(0, SHOW_MORE_CUTOFF - 1)).map((detail, i) => (
<div key={ detail.key } style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 2 }}>
<span style={{
fontSize: 12,
textTransform: "uppercase",
letterSpacing: 0.25,
fontWeight: 600,
color: "rgba(0, 0, 0, 0.65)",
whiteSpace: "nowrap"
}}>
{ detail.name }
</span>
<Progress
className="explanation-score-progress"
strokeColor={ colorMap[i] }
percent={ ((detail.value / totalScore) * 100).toFixed(0) }
title={ detail.value.toFixed(2) }
style={{ marginLeft: 8 }}
/>
</div>
<div style={{
fontSize: 13,
fontStyle: "italic",
display: "inline-flex",
alignItems: "center"
}}>
{ detail.description }
{/* { !detail.failedParse && (
<InfoTooltip
title={
<div>
{ Array.isArray(detail.matchedTerms) ? detail.matchedTerms.join("/") : detail.matchedTerms }
&nbsp;matched with&nbsp;
{ result[detail.matchedField] }
</div>
}
placement="bottom"
trigger="hover"
iconProps={{ style: { marginLeft: 6, fontSize: 14, color: "rgba(0, 0, 0, 0.45)" } }}
/>
) } */}
</div>
</div>
))
}
{ scoreData.length > SHOW_MORE_CUTOFF && (
<Button
type="link"
size="small"
style={{ padding: 0 }}
onClick={ () => setShowMore(!showMore) }
>
{ showMore ? "Show less" : "Show more" }
</Button>
) }
</Space>
</div>
</Space>
)
}
1 change: 1 addition & 0 deletions src/components/search/concept-modal/tabs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './cdes'
export * from './knowledge-graphs'
export * from './overview'
export * from './studies'
export * from './explanation'
export * from './tranql'
export * from './robokop'
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const { Text } = Typography
export const StudyVariable = ({ variable, highlight, ...props }) => (
<div className="study-variables-list-item" {...props}>
<Text className="variable-name">
<Highlighter searchWords={ highlight } textToHighlight={ variable.name } /> &nbsp;
<Highlighter autoEscape={ true } searchWords={ highlight } textToHighlight={ variable.name } /> &nbsp;
({ variable.e_link ? <a href={ variable.e_link }>{ variable.id }</a> : variable.id })
</Text><br />
<Text className="variable-description">
<Highlighter searchWords={ highlight } textToHighlight={ variable.description } />
<Highlighter autoEscape={ true } searchWords={ highlight } textToHighlight={ variable.description } />
</Text>
</div>
)
2 changes: 1 addition & 1 deletion src/components/search/concept-modal/tabs/studies/study.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const Study = ({ study, highlight, collapsed, ...panelProps }) => {
<Panel
header={
<Text>
<Highlighter searchWords={ highlight } textToHighlight={ study.c_name } />{ ` ` }
<Highlighter autoEscape={ true } searchWords={ highlight } textToHighlight={ study.c_name } />{ ` ` }
(<Link to={ study.c_link }>{ study.c_id }</Link>)
</Text>
}
Expand Down
Loading

0 comments on commit 63383ff

Please sign in to comment.