From 8f3927589285e0fec3fccfe5186fc71aab259d84 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Mon, 3 Jun 2024 23:05:09 +0200 Subject: [PATCH 01/12] Fixed some minor issues noticed during QA --- src/components/Connections.tsx | 3 +- src/components/connections/SummaryDetails.tsx | 4 +- src/components/connections/SummaryHeader.tsx | 4 +- .../connections/SummaryInstructions.tsx | 91 ++++++++++++------- 4 files changed, 62 insertions(+), 40 deletions(-) diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 7f7e1d7..96c6db3 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -153,7 +153,8 @@ function Connections() { if ( selectedConnectionSummary && - Object.keys(selectedConnectionSummary.connections).length !== 0 + Object.keys(selectedConnectionSummary.connections).length !== 0 && + ksIds.length > 0 ) { setShowConnectionDetails(SummaryType.DetailedSummary); const ksMap = getKnowledgeStatementMap(ksIds, knowledgeStatements); diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index a24a196..638146d 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -104,10 +104,10 @@ const SummaryDetails = ({ Details - - + diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index f10b9b3..d33a41b 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -82,14 +82,14 @@ const SummaryHeader = ({ }, }} > - + diff --git a/src/components/connections/SummaryInstructions.tsx b/src/components/connections/SummaryInstructions.tsx index f82d1be..ea1063c 100644 --- a/src/components/connections/SummaryInstructions.tsx +++ b/src/components/connections/SummaryInstructions.tsx @@ -1,38 +1,59 @@ -import { Box, Typography } from '@mui/material'; -import React from 'react'; +import { Box, Typography } from '@mui/material' +import React from 'react' const SummaryInstructions = () => { - return ( - - - Select a square on the connectivity grid to view details of connections. - - - - SummaryInstructions - - - - ); -}; + return ( + + Select a square on the connectivity grid to view details of connections. + + +

How to use the tool

+

1. Click on a square in the connectivity grid to view details of connections.

+

2. Details of the selected connection will be displayed in the right panel.

+

-export default SummaryInstructions; +

Filter the data

+

- Use the filters to narrow down the data displayed in the grid.

+

- You can filter by data source, connection type, and more.

+

+ +

View the data

+

- Use the heatmap on the left and then summary heatmap to highlight the data of your interest

+

- Once you click on a cluster in the summary map all the details about the connections, like

+

- connection URI, species, sex, graph and triples will be displayed in the right panel.

+

+ +

Export the data

+

- Use the export button to download the data displayed in the grid.

+

- Data can be exported in CSV format.

+

+ +
+ +
+
+ ) +} + +export default SummaryInstructions From bef9a630f158025d90e695fbfab6bb69da7dedea Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Tue, 4 Jun 2024 18:56:52 +0200 Subject: [PATCH 02/12] ESCKAN-51: pulling data from json for db summary --- src/components/SummaryPage.tsx | 302 +++++++++++++++--- .../connections/SummaryInstructions.tsx | 160 +++++++--- src/components/summaryPage/Detail.tsx | 28 +- src/settings.ts | 22 +- 4 files changed, 386 insertions(+), 126 deletions(-) diff --git a/src/components/SummaryPage.tsx b/src/components/SummaryPage.tsx index 72a3e4b..15a9ba5 100644 --- a/src/components/SummaryPage.tsx +++ b/src/components/SummaryPage.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState, useEffect } from 'react'; import { Box, Divider, Stack, Tab, Tabs, Typography } from '@mui/material'; import { vars } from '../theme/variables.ts'; @@ -7,27 +8,18 @@ import { Notes } from './summaryPage/Notes.tsx'; import { TabPanel } from './summaryPage/TabPanel.tsx'; import InfoTab from './summaryPage/InfoTab.tsx'; import Loader from './common/Loader.tsx'; - -interface DataType { - [key: string]: { - [key: string]: string | number; - notes: string; - }; -} -type LabelsType = { - [key: string]: string; -}; +import { + SCKAN_DATABASE_SUMMARY_URL_LATEST, + SCKAN_DATABASE_SUMMARY_URL_PREVIOUS, + FILES, + DATABASE_FILES, +} from '../settings.ts'; const { primaryPurple600, gray500, white } = vars; -const databaseSummaryURL = - 'https://raw.githubusercontent.com/MetaCell/sckan-explorer/feature/ESCKAN-28/src/data/database_summary_data.json'; -const databaseSummaryLabelsURL = - 'https://raw.githubusercontent.com/MetaCell/sckan-explorer/feature/ESCKAN-28/src/data/database_summary_labels.json'; - const SummaryPage = () => { - const [data, setData] = useState(null); - const [labels, setLabels] = useState(null); + const [data, setData] = useState<{ [x: string]: null }>({}); + const [loaded, setLoaded] = useState(false); const [value, setValue] = useState(0); // @ts-expect-error Explanation: Handling Event properly @@ -35,22 +27,175 @@ const SummaryPage = () => { setValue(newValue); }; useEffect(() => { - fetch(databaseSummaryURL) - .then((response) => response.json()) - .then((jsonData) => { - setData(jsonData); - }) - .catch((error) => console.error('Error fetching data:', error)); + const dataToPull = { + Latest: { + [FILES.POPULATION]: null, + [FILES.PHENOTYPE]: null, + [FILES.SPECIES]: null, + [FILES.CATEGORY]: null, + }, + Previous: { + [FILES.POPULATION]: null, + [FILES.PHENOTYPE]: null, + [FILES.SPECIES]: null, + [FILES.CATEGORY]: null, + }, + }; + + const results = { + [FILES.POPULATION]: null, + [FILES.PHENOTYPE]: null, + [FILES.SPECIES]: null, + [FILES.CATEGORY]: null, + }; + + for (const file in FILES) { + const request = new XMLHttpRequest(); + request.open( + 'GET', + SCKAN_DATABASE_SUMMARY_URL_LATEST + DATABASE_FILES[file], + false, + ); + request.send(null); + if (request.status === 200) { + dataToPull.Latest[file] = JSON.parse(request.responseText); + } + } - fetch(databaseSummaryLabelsURL) - .then((response) => response.json()) - .then((jsonData) => { - setLabels(jsonData); - }) - .catch((error) => console.error('Error fetching labels:', error)); + for (const file in FILES) { + const request = new XMLHttpRequest(); + request.open( + 'GET', + SCKAN_DATABASE_SUMMARY_URL_PREVIOUS + DATABASE_FILES[file], + false, + ); + request.send(null); + if (request.status === 200) { + dataToPull.Previous[file] = JSON.parse(request.responseText); + } + } + + // @ts-expect-error Explanation: Handling the data properly + results[FILES.CATEGORY] = dataToPull.Latest[ + FILES.CATEGORY + ].results.bindings.map((item: any) => { + let filteredItem = null; + if (dataToPull.Previous[FILES.CATEGORY]) { + // @ts-expect-error Explanation: Handling the data properly + filteredItem = dataToPull.Previous[ + FILES.CATEGORY + ].results.bindings.filter( + (prevItem: any) => + prevItem.neuron_category.value === item.neuron_category.value, + ); + } + if (filteredItem.length) { + return { + label: item?.neuron_category?.value, + count: item?.population_count?.value, + change: + item.population_count.value - + filteredItem[0].population_count.value, + }; + } else { + return { + label: item.neuron_category.value, + count: item.population_count.value, + change: 0, + }; + } + }); + + // @ts-expect-error Explanation: Handling the data properly + results[FILES.PHENOTYPE] = dataToPull.Latest[ + FILES.PHENOTYPE + ].results.bindings.map((item: any) => { + let filteredItem = null; + if (dataToPull.Previous[FILES.PHENOTYPE]) { + // @ts-expect-error Explanation: Handling the data properly + filteredItem = dataToPull.Previous[ + FILES.PHENOTYPE + ].results.bindings.filter( + (prevItem: any) => + prevItem?.phenotype?.value === item?.phenotype?.value, + ); + } + if (filteredItem.length) { + return { + label: item?.phenotype?.value, + count: item?.count?.value, + change: item.count.value - filteredItem[0].count.value, + }; + } else { + return { + label: item?.phenotype?.value, + count: item?.count?.value, + change: 0, + }; + } + }); + + // @ts-expect-error Explanation: Handling the data properly + results[FILES.POPULATION] = dataToPull.Latest[ + FILES.POPULATION + ].results.bindings.map((item: any) => { + let filteredItem = null; + if (dataToPull.Previous[FILES.POPULATION]) { + // @ts-expect-error Explanation: Handling the data properly + filteredItem = dataToPull.Previous[ + FILES.POPULATION + ].results.bindings.filter( + (prevItem: any) => prevItem?.model?.value === item?.model?.value, + ); + } + if (filteredItem.length) { + return { + label: item?.model?.value + ' (' + item?.neuron_category?.value + ')', + count: item?.count?.value, + change: item.count.value - filteredItem[0].count.value, + }; + } else { + return { + label: item?.model?.value + ' (' + item?.neuron_category?.value + ')', + count: item?.count?.value, + change: 0, + }; + } + }); + + // @ts-expect-error Explanation: Handling the data properly + results[FILES.SPECIES] = dataToPull.Latest[ + FILES.SPECIES + ].results.bindings.map((item: any) => { + let filteredItem = null; + if (dataToPull.Previous[FILES.SPECIES]) { + // @ts-expect-error Explanation: Handling the data properly + filteredItem = dataToPull.Previous[ + FILES.SPECIES + ].results.bindings.filter( + (prevItem: any) => prevItem?.type?.value === item?.type?.value, + ); + } + if (filteredItem.length) { + return { + label: item?.type?.value + ' (' + item?.phenotype_label?.value + ')', + count: item?.count?.value, + change: item.count.value - filteredItem[0].count.value, + }; + } else { + return { + label: item?.type?.value + ' (' + item?.phenotype_label?.value + ')', + count: item?.count?.value, + change: 0, + }; + } + }); + + setLoaded(true); + setData(results); }, []); - if (!data || !labels) + if (!loaded) return ( @@ -77,7 +222,7 @@ const SummaryPage = () => { Database summary - Last updated on September 15, 2023 + Last updated on May 15, 2024
{ - {Object.keys(data).map((sectionName) => ( -
- {Object.entries(data[sectionName]).map(([key, value], index) => { - if (key.endsWith('changes') || key === 'notes') { - return null; - } - +
+ { + // @ts-expect-error Explanation: Handling the data properly + data[FILES.CATEGORY].map((item: any) => { + return ( + + ); + }) + } + + +
+
+ { + // @ts-expect-error Explanation: Handling the data properly + data[FILES.SPECIES].map((item: any) => { + return ( + + ); + }) + } + + +
+
+ { + // @ts-expect-error Explanation: Handling the data properly + data[FILES.PHENOTYPE].map((item: any) => { + return ( + + ); + }) + } + + +
+
+ { + // @ts-expect-error Explanation: Handling the data properly + data[FILES.POPULATION].map((item: any) => { return ( ); - })} - {data[sectionName].notes && ( - - )} - -
- ))} + }) + } + + +
diff --git a/src/components/connections/SummaryInstructions.tsx b/src/components/connections/SummaryInstructions.tsx index ea1063c..96fa757 100644 --- a/src/components/connections/SummaryInstructions.tsx +++ b/src/components/connections/SummaryInstructions.tsx @@ -1,59 +1,113 @@ -import { Box, Typography } from '@mui/material' -import React from 'react' +import { Box, Typography } from '@mui/material'; +import React from 'react'; const SummaryInstructions = () => { - return ( - - Select a square on the connectivity grid to view details of connections. - - -

How to use the tool

-

1. Click on a square in the connectivity grid to view details of connections.

-

2. Details of the selected connection will be displayed in the right panel.

-

+ return ( + + + Select a square on the connectivity grid to view details of connections. + + + +

How to use the tool

+

+ 1. Click on a square in the connectivity grid to view details of + connections. +

+

+ 2. Details of the selected connection will be displayed in the right + panel. +

+

-

Filter the data

-

- Use the filters to narrow down the data displayed in the grid.

-

- You can filter by data source, connection type, and more.

-

+

Filter the data

+

+ {' '} + - Use the filters to narrow down the data displayed in the grid. +

+

+ {' '} + - You can filter by data source, connection type, and more. +

+

-

View the data

-

- Use the heatmap on the left and then summary heatmap to highlight the data of your interest

-

- Once you click on a cluster in the summary map all the details about the connections, like

-

- connection URI, species, sex, graph and triples will be displayed in the right panel.

-

+

View the data

+

+ {' '} + - Use the heatmap on the left and then summary heatmap to highlight + the data of your interest +

+

+ {' '} + - Once you click on a cluster in the summary map all the details + about the connections, like{' '} +

+

+ {' '} + - connection URI, species, sex, graph and triples will be displayed + in the right panel. +

+

-

Export the data

-

- Use the export button to download the data displayed in the grid.

-

- Data can be exported in CSV format.

-

+

Export the data

+

+ {' '} + - Use the export button to download the data displayed in the grid. +

+

+ {' '} + - Data can be exported in CSV format. +

+

+
+
+
+ ); +}; -
- -
-
- ) -} - -export default SummaryInstructions +export default SummaryInstructions; diff --git a/src/components/summaryPage/Detail.tsx b/src/components/summaryPage/Detail.tsx index 82ca839..a2351d8 100644 --- a/src/components/summaryPage/Detail.tsx +++ b/src/components/summaryPage/Detail.tsx @@ -2,30 +2,15 @@ import { Stack, Tooltip, Typography } from '@mui/material'; import { vars } from '../../theme/variables.ts'; import { HelpCircle } from '../icons'; import IconButton from '@mui/material/IconButton'; -const { gray500, gray700, gray600 } = vars; - -interface SectionDataType { - [key: string]: string | number; -} - -type LabelsType = { - [key: string]: string; -}; +const { gray700, gray600 } = vars; interface DetailProps { keyName: string; - sectionData: SectionDataType; value: string | number; - labels: LabelsType; + labels: string; index: number; } -export const Detail = ({ - keyName, - sectionData, - value, - labels, - index, -}: DetailProps) => ( +export const Detail = ({ keyName, value, labels, index }: DetailProps) => ( - {labels[keyName]} + {labels} {index === 0 && ( @@ -65,13 +50,12 @@ export const Detail = ({ {value} - {sectionData[`${keyName}_changes`] && ( + {/* {sectionData[`${keyName}_changes`] && ( +{sectionData[`${keyName}_changes`]} change (since last stats) - )} + )} */} ); diff --git a/src/settings.ts b/src/settings.ts index a44e2ce..81c1bf6 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -3,8 +3,28 @@ export const SCKAN_JSON_URL = export const SCKAN_MAJOR_NERVES_JSON_URL = 'https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/master/sckan-explorer/json/major-nerves.json'; +export const SCKAN_DATABASE_SUMMARY_URL_LATEST = + 'https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/master/sckan-explorer/json/sckan-stats/sckan-version-2024-03-04/'; + +export const SCKAN_DATABASE_SUMMARY_URL_PREVIOUS = + 'https://raw.githubusercontent.com/smtifahim/SCKAN-Apps/master/sckan-explorer/json/sckan-stats/sckan-version-2024-03-04/'; + +export const FILES = { + POPULATION: 'POPULATION', + PHENOTYPE: 'PHENOTYPE', + SPECIES: 'SPECIES', + CATEGORY: 'CATEGORY', +}; + +export const DATABASE_FILES = { + [FILES.POPULATION]: 'stats-model-population-count.json', + [FILES.PHENOTYPE]: 'stats-phenotype-count.json', + [FILES.SPECIES]: 'stats-phenotype-value-count.json', + [FILES.CATEGORY]: 'stats-population-category-count.json', +}; + // TODO: To change to the env variable when the deployment gets integrated with other sckan projects @dario -export const COMPOSER_API_URL = 'https://composer.scicrunch.io/api'; +export const COMPOSER_API_URL = 'https://composer.sckan.dev.metacell.us/api'; //export const COMPOSER_API_URL = import.meta.env.VITE_COMPOSER_API_URL export const OTHER_X_AXIS_ID = 'OTHER_X'; From 21ba4785762b06b246785c9c06aa44ae6901da0b Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Wed, 5 Jun 2024 11:45:27 +0200 Subject: [PATCH 03/12] generate CSV --- src/components/connections/SummaryDetails.tsx | 15 +++++++++++++-- src/components/connections/SummaryHeader.tsx | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index 638146d..f671977 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -91,6 +91,11 @@ const SummaryDetails = ({ }, ]; + const generateCSV = () => { + console.log('Generating CSV'); + console.log(knowledgeStatementsMap); + }; + return ( @@ -104,10 +109,16 @@ const SummaryDetails = ({ Details - - + diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index d33a41b..827ed5a 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -12,6 +12,7 @@ import IconButton from '@mui/material/IconButton'; import { ArrowDown, ArrowRight, ArrowUp, HelpCircle } from '../icons'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import { SummaryType, KsMapType } from '../common/Types'; +import { useDataContext } from '../../context/DataContext.ts'; const { gray100, gray600A, gray500 } = vars; @@ -34,6 +35,8 @@ const SummaryHeader = ({ }: SummaryHeaderProps) => { const totalUniqueKS = Object.keys(knowledgeStatementsMap).length; + const { selectedConnectionSummary } = useDataContext(); + function getConnectionId() { return Object.keys(knowledgeStatementsMap)[connectionPage - 1] || ''; } @@ -51,6 +54,11 @@ const SummaryHeader = ({ } }; + const generateCSV = () => { + console.log('Generating CSV'); + console.log(selectedConnectionSummary); + }; + if (showDetails === SummaryType.Instruction) { return <>; } @@ -144,7 +152,7 @@ const SummaryHeader = ({ color: gray600A, }} > - {totalConnectionCount} connections + {totalConnectionCount} populations - + )} From 4b31533a103a1d3c6761f6acf5be907b1c027542 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 5 Jun 2024 13:50:02 +0100 Subject: [PATCH 04/12] ESCKAN 55 feat: Connect context filters with summary details --- src/components/Connections.tsx | 29 ++++++++-- src/components/ConnectivityGrid.tsx | 6 +- src/components/SummaryFiltersDropdown.tsx | 57 ++++++++++++------- .../common/CustomFilterDropdown.tsx | 13 +---- src/context/DataContext.ts | 16 ++---- src/services/heatmapService.ts | 32 +++++++---- src/services/summaryHeatmapService.ts | 29 +--------- 7 files changed, 91 insertions(+), 91 deletions(-) diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 96c6db3..4124e02 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -7,6 +7,7 @@ import { PhenotypeKsIdMap, SummaryType, KsMapType, + Option, } from './common/Types'; import { useDataContext } from '../context/DataContext.ts'; import { @@ -64,15 +65,26 @@ function Connections() { const [knowledgeStatementsMap, setKnowledgeStatementsMap] = useState({}); const [xAxis, setXAxis] = useState([]); + const [nerveFilters, setNerveFilters] = useState([]); + const [phenotypeFilters, setPhenotypeFilters] = useState([]); const { selectedConnectionSummary, majorNerves, hierarchicalNodes, knowledgeStatements, - summaryFilters, + filters, } = useDataContext(); + const summaryFilters = useMemo( + () => ({ + ...filters, + Nerve: nerveFilters, + Phenotype: phenotypeFilters, + }), + [filters, nerveFilters], + ); + useEffect(() => { // By default on the first render, show the instruction/summary if (selectedConnectionSummary) { @@ -88,7 +100,8 @@ function Connections() { const totalConnectionCount = Object.keys( selectedConnectionSummary?.connections || ({} as KsMapType), ).length; - const nerves = getNerveFilters(viasConnection, majorNerves); + + const availableNerves = getNerveFilters(viasConnection, majorNerves); useEffect(() => { // calculate the connectionsMap for the secondary heatmap @@ -114,7 +127,7 @@ function Connections() { knowledgeStatements, ]); - const selectedPhenotypes = useMemo( + const availablePhenotypes = useMemo( () => getAllPhenotypes(connectionsMap), [connectionsMap], ); @@ -256,8 +269,12 @@ function Connections() { - + )} diff --git a/src/components/ConnectivityGrid.tsx b/src/components/ConnectivityGrid.tsx index 8ebd336..9ac6242 100644 --- a/src/components/ConnectivityGrid.tsx +++ b/src/components/ConnectivityGrid.tsx @@ -75,10 +75,10 @@ function ConnectivityGrid() { }, [hierarchicalNodes]); const { heatmapData, detailedHeatmapData } = useMemo(() => { - const heatmapdata = getHeatmapData(yAxis, connectionsMap); + const heatmapData = getHeatmapData(yAxis, connectionsMap); return { - heatmapData: heatmapdata.heatmapMatrix, - detailedHeatmapData: heatmapdata.detailedHeatmap, + heatmapData: heatmapData.heatmapMatrix, + detailedHeatmapData: heatmapData.detailedHeatmap, }; }, [yAxis, connectionsMap]); diff --git a/src/components/SummaryFiltersDropdown.tsx b/src/components/SummaryFiltersDropdown.tsx index 10be293..490bdd0 100644 --- a/src/components/SummaryFiltersDropdown.tsx +++ b/src/components/SummaryFiltersDropdown.tsx @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { SummaryFilters, useDataContext } from '../context/DataContext'; import CustomFilterDropdown from './common/CustomFilterDropdown'; import { Option } from './common/Types'; import { Box } from '@mui/material'; @@ -10,7 +9,7 @@ import { import { OTHER_PHENOTYPE_LABEL } from '../settings'; interface FilterConfig { - id: keyof SummaryFilters; + id: 'Phenotype' | 'Nerve'; placeholder: string; searchPlaceholder: string; } @@ -31,12 +30,18 @@ const filterConfig: FilterConfig[] = [ const SummaryFiltersDropdown = ({ nerves, phenotypes, + nerveFilters, + setNerveFilters, + phenotypeFilters, + setPhenotypeFilters, }: { nerves: { [key: string]: string }; phenotypes: string[]; + nerveFilters: Option[]; + setNerveFilters: React.Dispatch>; + phenotypeFilters: Option[]; + setPhenotypeFilters: React.Dispatch>; }) => { - const { summaryFilters, setSummaryFilters } = useDataContext(); - const convertNervesToOptions = (nerves: { [key: string]: string; }): Option[] => { @@ -47,6 +52,7 @@ const SummaryFiltersDropdown = ({ content: [], })); }; + const convertPhenotypesToOptions = (phenotypes: string[]): Option[] => { return phenotypes .map((phenotype) => ({ @@ -62,22 +68,33 @@ const SummaryFiltersDropdown = ({ () => convertPhenotypesToOptions(phenotypes), [phenotypes], ); + const nerveOptions = useMemo(() => convertNervesToOptions(nerves), [nerves]); - const handleSelect = ( - filterKey: keyof typeof summaryFilters, - selectedOptions: Option[], - ) => { - setSummaryFilters((prevFilters) => ({ - ...prevFilters, - [filterKey]: selectedOptions, - })); + type FilterKey = 'Phenotype' | 'Nerve'; + + const filterStateMap: { + [K in FilterKey]: { + filters: Option[]; + setFilters: React.Dispatch>; + searchFunction: (value: string) => Option[]; + }; + } = { + Phenotype: { + filters: phenotypeFilters, + setFilters: setPhenotypeFilters, + searchFunction: (value: string) => + searchPhenotypeFilter(value, phenotypeOptions), + }, + Nerve: { + filters: nerveFilters, + setFilters: setNerveFilters, + searchFunction: (value: string) => searchNerveFilter(value, nerveOptions), + }, }; - const searchFunctions = { - Phenotype: (value: string) => - searchPhenotypeFilter(value, phenotypeOptions), - Nerve: (value: string) => searchNerveFilter(value, nerveOptions), + const handleSelect = (filterKey: FilterKey, selectedOptions: Option[]) => { + filterStateMap[filterKey].setFilters(selectedOptions); }; return ( @@ -88,13 +105,11 @@ const SummaryFiltersDropdown = ({ id={filter.id} placeholder={filter.placeholder} searchPlaceholder={filter.searchPlaceholder} - selectedOptions={summaryFilters[filter.id]} + selectedOptions={filterStateMap[filter.id].filters} onSearch={(searchValue: string) => - searchFunctions[filter.id](searchValue) - } - onSelect={(options: Option[]) => - handleSelect(filter.id as keyof SummaryFilters, options) + filterStateMap[filter.id].searchFunction(searchValue) } + onSelect={(options: Option[]) => handleSelect(filter.id, options)} /> ))} diff --git a/src/components/common/CustomFilterDropdown.tsx b/src/components/common/CustomFilterDropdown.tsx index 6d41cbb..caefd5b 100644 --- a/src/components/common/CustomFilterDropdown.tsx +++ b/src/components/common/CustomFilterDropdown.tsx @@ -21,11 +21,7 @@ import ClearOutlinedIcon from '@mui/icons-material/ClearOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { vars } from '../../theme/variables'; - -type OptionDetail = { - title: string; // What to display as the title/label for the property. - value: string; // The actual value/content for the property. -}; +import { Option } from './Types.ts'; const { gray100, @@ -39,13 +35,6 @@ const { gray50, } = vars; -export type Option = { - id: string; - label: string; - group: string; - content: OptionDetail[]; -}; - const transition = { transition: 'all ease-in-out .3s', }; diff --git a/src/context/DataContext.ts b/src/context/DataContext.ts index 35e1980..9db8cac 100644 --- a/src/context/DataContext.ts +++ b/src/context/DataContext.ts @@ -16,8 +16,7 @@ export interface Filters { Via: Option[]; } -export interface SummaryFilters { - Phenotype: Option[]; +export interface SummaryFilters extends Filters { Nerve: Option[]; } @@ -30,13 +29,11 @@ export interface ConnectionSummary { export interface DataContext { filters: Filters; - summaryFilters: SummaryFilters; majorNerves: Set; organs: Record; hierarchicalNodes: Record; knowledgeStatements: Record; setFilters: React.Dispatch>; - setSummaryFilters: React.Dispatch>; selectedConnectionSummary: ConnectionSummary | null; setConnectionSummary: React.Dispatch< React.SetStateAction @@ -53,18 +50,15 @@ export const DataContext = createContext({ apiNATOMY: [], Via: [], }, - summaryFilters: { - Phenotype: [], - Nerve: [], - }, majorNerves: new Set(), organs: {}, hierarchicalNodes: {}, knowledgeStatements: {}, - setFilters: () => {}, - setSummaryFilters: () => {}, + setFilters: () => { + }, selectedConnectionSummary: null, - setConnectionSummary: () => {}, + setConnectionSummary: () => { + }, phenotypesColorMap: {}, }); diff --git a/src/services/heatmapService.ts b/src/services/heatmapService.ts index b60b529..f987df9 100644 --- a/src/services/heatmapService.ts +++ b/src/services/heatmapService.ts @@ -11,7 +11,7 @@ import { KsMapType, LabelIdPair, } from '../components/common/Types.ts'; -import { Filters } from '../context/DataContext.ts'; +import { Filters, SummaryFilters } from '../context/DataContext.ts'; export function getYAxis( hierarchicalNodes: Record, @@ -207,13 +207,16 @@ export function filterOrgans( export function filterKnowledgeStatements( knowledgeStatements: Record, - filters: Filters, + filters: Filters | SummaryFilters, ): Record { const phenotypeIds = filters.Phenotype.map((option) => option.id); - const apiNATOMYIds = filters.apiNATOMY.map((option) => option.id); - const speciesIds = filters.Species.flatMap((option) => option.id); - const viaIds = filters.Via.flatMap((option) => option.id); - const originIds = filters.Origin.flatMap((option) => option.id); + const apiNATOMYIds = + (filters as Filters).apiNATOMY?.map((option) => option.id) || []; + const speciesIds = filters.Species?.flatMap((option) => option.id) || []; + const viaIds = filters.Via?.flatMap((option) => option.id) || []; + const originIds = filters.Origin?.flatMap((option) => option.id) || []; + const nerveIds = + (filters as SummaryFilters).Nerve?.map((option) => option.id) || []; return Object.entries(knowledgeStatements).reduce( (filtered, [id, ks]) => { @@ -223,22 +226,30 @@ export function filterKnowledgeStatements( !apiNATOMYIds.length || apiNATOMYIds.includes(ks.apinatomy); const speciesMatch = !speciesIds.length || - ks.species.some((species) => speciesIds.includes(species.id)); + ks.species?.some((species) => speciesIds.includes(species.id)); const viaMatch = !viaIds.length || ks.vias - .flatMap((via) => via.anatomical_entities) + ?.flatMap((via) => via.anatomical_entities) .some((via) => viaIds.includes(via.id)); const originMatch = !originIds.length || - ks.origins.some((origin) => originIds.includes(origin.id)); + ks.origins?.some((origin) => originIds.includes(origin.id)); + const nerveMatch = + !nerveIds.length || + ks.vias?.some((via) => + via.anatomical_entities + .map((entity) => entity.id) + .some((id) => nerveIds.includes(id)), + ); if ( phenotypeMatch && apiNATOMYMatch && speciesMatch && viaMatch && - originMatch + originMatch && + nerveMatch ) { filtered[id] = ks; } @@ -247,7 +258,6 @@ export function filterKnowledgeStatements( {} as Record, ); } - export function getHierarchyFromId( id: string, hierarchicalNodes: Record, diff --git a/src/services/summaryHeatmapService.ts b/src/services/summaryHeatmapService.ts index 125f732..7a0c0c0 100644 --- a/src/services/summaryHeatmapService.ts +++ b/src/services/summaryHeatmapService.ts @@ -12,6 +12,7 @@ import { BaseEntity, } from '../models/explorer.ts'; import { OTHER_PHENOTYPE_LABEL } from '../settings.ts'; +import { filterKnowledgeStatements } from './heatmapService.ts'; export const generatePhenotypeColors = (num: number) => { const scale = chroma @@ -79,32 +80,6 @@ export const getNerveFilters = ( return nerves; }; -export function summaryFilterKnowledgeStatements( - knowledgeStatements: Record, - summaryFilters: SummaryFilters, -): Record { - const phenotypeIds = summaryFilters.Phenotype.map((option) => option.id); - const nerveIds = summaryFilters.Nerve.map((option) => option.id); - return Object.entries(knowledgeStatements).reduce( - (filtered, [id, ks]) => { - const phenotypeMatch = - !phenotypeIds.length || phenotypeIds.includes(ks.phenotype); - const nerveMatch = - !nerveIds.length || - ks.vias?.some((via) => - via.anatomical_entities - .map((entity) => entity.id) - .some((id) => nerveIds.includes(id)), - ); - if (phenotypeMatch && nerveMatch) { - filtered[id] = ks; - } - return filtered; - }, - {} as Record, - ); -} - // NOTE: this function is similar to /services/heatmapService.ts - getHeatmapData // output type - PhenotypeKsIdMap[][] // ***** Recursive function - traverseItems ***** @@ -160,7 +135,7 @@ export function calculateSecondaryConnections( hierarchyNode: HierarchicalNode, ): Map { // Apply filters to organs and knowledge statements - const knowledgeStatements = summaryFilterKnowledgeStatements( + const knowledgeStatements = filterKnowledgeStatements( allKnowledgeStatements, summaryFilters, ); From 6e591f495be72045d4991c604548bf1a99969be4 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 5 Jun 2024 14:46:55 +0100 Subject: [PATCH 05/12] ESCKAN 55 feat: Update phenotypes filter logic --- src/components/Connections.tsx | 38 ++++++++++++++++----------- src/context/DataContext.ts | 6 ++--- src/services/summaryHeatmapService.ts | 20 +++++--------- src/settings.ts | 2 +- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 4124e02..6fa0838 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -51,6 +51,14 @@ const styles = { }; function Connections() { + const { + selectedConnectionSummary, + majorNerves, + hierarchicalNodes, + knowledgeStatements, + filters, + } = useDataContext(); + const [showConnectionDetails, setShowConnectionDetails] = useState(SummaryType.Instruction); const [connectionsMap, setConnectionsMap] = useState< @@ -66,15 +74,13 @@ function Connections() { useState({}); const [xAxis, setXAxis] = useState([]); const [nerveFilters, setNerveFilters] = useState([]); - const [phenotypeFilters, setPhenotypeFilters] = useState([]); + const [phenotypeFilters, setPhenotypeFilters] = useState( + filters.Phenotype, + ); - const { - selectedConnectionSummary, - majorNerves, - hierarchicalNodes, - knowledgeStatements, - filters, - } = useDataContext(); + useEffect(() => { + setPhenotypeFilters(filters.Phenotype); + }, [filters.Phenotype]); const summaryFilters = useMemo( () => ({ @@ -82,7 +88,7 @@ function Connections() { Nerve: nerveFilters, Phenotype: phenotypeFilters, }), - [filters, nerveFilters], + [filters, nerveFilters, phenotypeFilters], ); useEffect(() => { @@ -102,6 +108,13 @@ function Connections() { ).length; const availableNerves = getNerveFilters(viasConnection, majorNerves); + const availablePhenotypes = useMemo( + () => + selectedConnectionSummary + ? getAllPhenotypes(selectedConnectionSummary.connections) + : [], + [selectedConnectionSummary], + ); useEffect(() => { // calculate the connectionsMap for the secondary heatmap @@ -127,11 +140,6 @@ function Connections() { knowledgeStatements, ]); - const availablePhenotypes = useMemo( - () => getAllPhenotypes(connectionsMap), - [connectionsMap], - ); - useEffect(() => { // set the xAxis for the heatmap if (selectedConnectionSummary) { @@ -147,7 +155,7 @@ function Connections() { if (selectedConnectionSummary && hierarchicalNodes) { const hierarchyNode = { [selectedConnectionSummary.hierarchy.id]: - selectedConnectionSummary.hierarchy, + selectedConnectionSummary.hierarchy, }; const yHierarchicalItem = getYAxis(hierarchicalNodes, hierarchyNode); setYAxis(yHierarchicalItem); diff --git a/src/context/DataContext.ts b/src/context/DataContext.ts index 9db8cac..39e8e83 100644 --- a/src/context/DataContext.ts +++ b/src/context/DataContext.ts @@ -54,11 +54,9 @@ export const DataContext = createContext({ organs: {}, hierarchicalNodes: {}, knowledgeStatements: {}, - setFilters: () => { - }, + setFilters: () => {}, selectedConnectionSummary: null, - setConnectionSummary: () => { - }, + setConnectionSummary: () => {}, phenotypesColorMap: {}, }); diff --git a/src/services/summaryHeatmapService.ts b/src/services/summaryHeatmapService.ts index 7a0c0c0..f12e39b 100644 --- a/src/services/summaryHeatmapService.ts +++ b/src/services/summaryHeatmapService.ts @@ -47,21 +47,15 @@ export function getAllViasFromConnections(connections: KsMapType): { return vias; } -export function getAllPhenotypes( - connections: Map, -): string[] { +export function getAllPhenotypes(connections: KsMapType): string[] { const phenotypeNames: Set = new Set(); - connections.forEach((phenotypeKsIdMaps) => { - phenotypeKsIdMaps.forEach((phenotypeKsIdMap) => { - Object.keys(phenotypeKsIdMap).forEach((phenotype) => { - if (phenotype) { - phenotypeNames.add(phenotype); - } else { - phenotypeNames.add(OTHER_PHENOTYPE_LABEL); - } - }); - }); + Object.values(connections).forEach((ks) => { + if (ks.phenotype) { + phenotypeNames.add(ks.phenotype); + } else { + phenotypeNames.add(OTHER_PHENOTYPE_LABEL); + } }); return Array.from(phenotypeNames).sort(); diff --git a/src/settings.ts b/src/settings.ts index 81c1bf6..dd84195 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -24,7 +24,7 @@ export const DATABASE_FILES = { }; // TODO: To change to the env variable when the deployment gets integrated with other sckan projects @dario -export const COMPOSER_API_URL = 'https://composer.sckan.dev.metacell.us/api'; +export const COMPOSER_API_URL = 'https://composer.scicrunch.io/api'; //export const COMPOSER_API_URL = import.meta.env.VITE_COMPOSER_API_URL export const OTHER_X_AXIS_ID = 'OTHER_X'; From ab948fdb845f2eb94e441feab2632207ad4f4623 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 5 Jun 2024 17:32:41 +0100 Subject: [PATCH 06/12] ESCKAN-55 feat: Update selectedConnectionSummary properties and usage --- src/components/Connections.tsx | 35 +++++++------- src/components/ConnectivityGrid.tsx | 14 +++--- src/components/SummaryPage.tsx | 6 ++- src/components/common/Types.ts | 2 +- src/components/connections/SummaryDetails.tsx | 4 +- src/components/connections/SummaryHeader.tsx | 4 +- src/context/DataContext.ts | 16 +++---- src/context/DataContextProvider.tsx | 48 +++++++++++++------ src/services/heatmapService.ts | 28 +++-------- src/services/summaryHeatmapService.ts | 38 ++++++++++++--- 10 files changed, 111 insertions(+), 84 deletions(-) diff --git a/src/components/Connections.tsx b/src/components/Connections.tsx index 6fa0838..02336f5 100644 --- a/src/components/Connections.tsx +++ b/src/components/Connections.tsx @@ -6,7 +6,7 @@ import { HierarchicalItem, PhenotypeKsIdMap, SummaryType, - KsMapType, + KsRecord, Option, } from './common/Types'; import { useDataContext } from '../context/DataContext.ts'; @@ -71,16 +71,10 @@ function Connections() { y: number; } | null>(null); // useful for coordinates const [knowledgeStatementsMap, setKnowledgeStatementsMap] = - useState({}); + useState({}); const [xAxis, setXAxis] = useState([]); const [nerveFilters, setNerveFilters] = useState([]); - const [phenotypeFilters, setPhenotypeFilters] = useState( - filters.Phenotype, - ); - - useEffect(() => { - setPhenotypeFilters(filters.Phenotype); - }, [filters.Phenotype]); + const [phenotypeFilters, setPhenotypeFilters] = useState([]); const summaryFilters = useMemo( () => ({ @@ -100,18 +94,20 @@ function Connections() { // Values from the selected connection - In the SummaryType.summary const viasConnection = getAllViasFromConnections( - selectedConnectionSummary?.connections || ({} as KsMapType), + selectedConnectionSummary?.filteredKnowledgeStatements || ({} as KsRecord), ); const viasStatement = convertViaToString(Object.values(viasConnection)); const totalConnectionCount = Object.keys( - selectedConnectionSummary?.connections || ({} as KsMapType), + selectedConnectionSummary?.filteredKnowledgeStatements || ({} as KsRecord), ).length; const availableNerves = getNerveFilters(viasConnection, majorNerves); const availablePhenotypes = useMemo( () => selectedConnectionSummary - ? getAllPhenotypes(selectedConnectionSummary.connections) + ? getAllPhenotypes( + selectedConnectionSummary.filteredKnowledgeStatements, + ) : [], [selectedConnectionSummary], ); @@ -120,16 +116,16 @@ function Connections() { // calculate the connectionsMap for the secondary heatmap if ( selectedConnectionSummary && - selectedConnectionSummary.hierarchy && + selectedConnectionSummary.hierarchicalNode && hierarchicalNodes ) { const destinations = getDestinations(selectedConnectionSummary); const connections = calculateSecondaryConnections( hierarchicalNodes, destinations, - knowledgeStatements, + selectedConnectionSummary.filteredKnowledgeStatements, summaryFilters, - selectedConnectionSummary.hierarchy, + selectedConnectionSummary.hierarchicalNode, ); setConnectionsMap(connections); } @@ -154,8 +150,8 @@ function Connections() { // set the yAxis for the heatmap if (selectedConnectionSummary && hierarchicalNodes) { const hierarchyNode = { - [selectedConnectionSummary.hierarchy.id]: - selectedConnectionSummary.hierarchy, + [selectedConnectionSummary.hierarchicalNode.id]: + selectedConnectionSummary.hierarchicalNode, }; const yHierarchicalItem = getYAxis(hierarchicalNodes, hierarchyNode); setYAxis(yHierarchicalItem); @@ -174,7 +170,8 @@ function Connections() { if ( selectedConnectionSummary && - Object.keys(selectedConnectionSummary.connections).length !== 0 && + Object.keys(selectedConnectionSummary.filteredKnowledgeStatements) + .length !== 0 && ksIds.length > 0 ) { setShowConnectionDetails(SummaryType.DetailedSummary); @@ -218,7 +215,7 @@ function Connections() { Connection origin diff --git a/src/components/ConnectivityGrid.tsx b/src/components/ConnectivityGrid.tsx index 9ac6242..0969855 100644 --- a/src/components/ConnectivityGrid.tsx +++ b/src/components/ConnectivityGrid.tsx @@ -6,7 +6,6 @@ import { useDataContext } from '../context/DataContext.ts'; import { calculateConnections, getMinMaxConnections, - getHierarchyFromId, getXAxisOrgans, getYAxis, getHeatmapData, @@ -32,7 +31,7 @@ function ConnectivityGrid() { organs, knowledgeStatements, filters, - setConnectionSummary, + setSelectedConnectionSummary, } = useDataContext(); const [yAxis, setYAxis] = useState([]); @@ -88,15 +87,14 @@ function ConnectivityGrid() { const row = connectionsMap.get(yId); if (row) { const endOrgan = xAxisOrgans[x]; - const origin = detailedHeatmapData[y]; - const hierarchy = getHierarchyFromId(origin.id, hierarchicalNodes); + const nodeData = detailedHeatmapData[y]; + const hierarchicalNode = hierarchicalNodes[nodeData.id]; const ksMap = getKnowledgeStatementMap(row[x], knowledgeStatements); - setConnectionSummary({ - origin: origin.label, - endOrgan: endOrgan, + setSelectedConnectionSummary({ connections: ksMap, - hierarchy: hierarchy, + endOrgan: endOrgan, + hierarchicalNode: hierarchicalNode, }); } }; diff --git a/src/components/SummaryPage.tsx b/src/components/SummaryPage.tsx index 15a9ba5..c78da43 100644 --- a/src/components/SummaryPage.tsx +++ b/src/components/SummaryPage.tsx @@ -150,13 +150,15 @@ const SummaryPage = () => { } if (filteredItem.length) { return { - label: item?.model?.value + ' (' + item?.neuron_category?.value + ')', + label: + item?.model?.value + ' (' + item?.neuron_category?.value + ')', count: item?.count?.value, change: item.count.value - filteredItem[0].count.value, }; } else { return { - label: item?.model?.value + ' (' + item?.neuron_category?.value + ')', + label: + item?.model?.value + ' (' + item?.neuron_category?.value + ')', count: item?.count?.value, change: 0, }; diff --git a/src/components/common/Types.ts b/src/components/common/Types.ts index 92b84b2..fcee967 100644 --- a/src/components/common/Types.ts +++ b/src/components/common/Types.ts @@ -13,7 +13,7 @@ export type Option = { export type LabelIdPair = { labels: string[]; ids: string[] }; -export type KsMapType = Record; +export type KsRecord = Record; export type PhenotypeKsIdMap = { [phenotype: string]: { diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index f671977..14b6c45 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -6,7 +6,7 @@ import PopulationDisplay from './PopulationDisplay.tsx'; import CommonAccordion from '../common/Accordion.tsx'; import CommonChip from '../common/CommonChip.tsx'; import { ArrowOutward } from '../icons/index.tsx'; -import { KsMapType } from '../common/Types.ts'; +import { KsRecord } from '../common/Types.ts'; import { getConnectionDetails } from '../../services/summaryHeatmapService.ts'; const { gray500, gray700, gray800 } = vars; @@ -43,7 +43,7 @@ const RowStack = ({ ); type SummaryDetailsProps = { - knowledgeStatementsMap: KsMapType; + knowledgeStatementsMap: KsRecord; connectionPage: number; }; diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index 827ed5a..202cbfb 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -11,7 +11,7 @@ import { vars } from '../../theme/variables'; import IconButton from '@mui/material/IconButton'; import { ArrowDown, ArrowRight, ArrowUp, HelpCircle } from '../icons'; import Breadcrumbs from '@mui/material/Breadcrumbs'; -import { SummaryType, KsMapType } from '../common/Types'; +import { SummaryType, KsRecord } from '../common/Types'; import { useDataContext } from '../../context/DataContext.ts'; const { gray100, gray600A, gray500 } = vars; @@ -19,7 +19,7 @@ const { gray100, gray600A, gray500 } = vars; type SummaryHeaderProps = { showDetails: SummaryType; setShowDetails: (showDetails: SummaryType) => void; - knowledgeStatementsMap: KsMapType; + knowledgeStatementsMap: KsRecord; connectionPage: number; setConnectionPage: (connectionPage: number) => void; totalConnectionCount: number; diff --git a/src/context/DataContext.ts b/src/context/DataContext.ts index 39e8e83..b205cbe 100644 --- a/src/context/DataContext.ts +++ b/src/context/DataContext.ts @@ -5,7 +5,7 @@ import { KnowledgeStatement, } from '../models/explorer'; import { Option, PhenotypeDetail } from '../components/common/Types.ts'; -import { KsMapType } from '../components/common/Types'; +import { KsRecord } from '../components/common/Types'; export interface Filters { Origin: Option[]; @@ -21,10 +21,10 @@ export interface SummaryFilters extends Filters { } export interface ConnectionSummary { - connections: KsMapType; // displaying connection 1 of 5 - origin: string; + connections: KsRecord; + filteredKnowledgeStatements: KsRecord; + hierarchicalNode: HierarchicalNode; endOrgan: Organ; - hierarchy: HierarchicalNode; } export interface DataContext { @@ -35,9 +35,9 @@ export interface DataContext { knowledgeStatements: Record; setFilters: React.Dispatch>; selectedConnectionSummary: ConnectionSummary | null; - setConnectionSummary: React.Dispatch< - React.SetStateAction - >; + setSelectedConnectionSummary: ( + summary: Omit, + ) => void; phenotypesColorMap: Record; } @@ -56,7 +56,7 @@ export const DataContext = createContext({ knowledgeStatements: {}, setFilters: () => {}, selectedConnectionSummary: null, - setConnectionSummary: () => {}, + setSelectedConnectionSummary: () => {}, phenotypesColorMap: {}, }); diff --git a/src/context/DataContextProvider.tsx b/src/context/DataContextProvider.tsx index 0b18906..72ff9f6 100644 --- a/src/context/DataContextProvider.tsx +++ b/src/context/DataContextProvider.tsx @@ -1,10 +1,5 @@ -import { PropsWithChildren, useMemo, useState } from 'react'; -import { - DataContext, - Filters, - ConnectionSummary, - SummaryFilters, -} from './DataContext'; +import { PropsWithChildren, useEffect, useMemo, useState } from 'react'; +import { DataContext, Filters, ConnectionSummary } from './DataContext'; import { HierarchicalNode, KnowledgeStatement, @@ -13,6 +8,7 @@ import { import { PhenotypeDetail } from '../components/common/Types.ts'; import { generatePhenotypeColors } from '../services/summaryHeatmapService.ts'; import { OTHER_PHENOTYPE_LABEL } from '../settings.ts'; +import { filterKnowledgeStatements } from '../services/heatmapService.ts'; export const DataContextProvider = ({ hierarchicalNodes, @@ -34,10 +30,6 @@ export const DataContextProvider = ({ apiNATOMY: [], Via: [], }); - const [summaryFilters, setSummaryFilters] = useState({ - Phenotype: [], - Nerve: [], - }); const [selectedConnectionSummary, setSelectedConnectionSummary] = useState(null); @@ -61,17 +53,45 @@ export const DataContextProvider = ({ return colorMap; }, [phenotypes]); + const handleSetSelectedConnectionSummary = ( + summary: Omit, + ) => { + const filteredKnowledgeStatements = filterKnowledgeStatements( + summary.connections, + filters, + ); + setSelectedConnectionSummary({ + ...summary, + filteredKnowledgeStatements, + }); + }; + + useEffect(() => { + if (selectedConnectionSummary) { + const filteredKnowledgeStatements = filterKnowledgeStatements( + selectedConnectionSummary.connections, + filters, + ); + setSelectedConnectionSummary((prevSummary) => + prevSummary + ? { + ...prevSummary, + filteredKnowledgeStatements, + } + : null, + ); + } + }, [filters]); + const dataContextValue = { filters, - summaryFilters, - setSummaryFilters, organs, majorNerves, hierarchicalNodes, knowledgeStatements, setFilters, selectedConnectionSummary, - setConnectionSummary: setSelectedConnectionSummary, + setSelectedConnectionSummary: handleSetSelectedConnectionSummary, phenotypesColorMap, }; diff --git a/src/services/heatmapService.ts b/src/services/heatmapService.ts index f987df9..82a7acf 100644 --- a/src/services/heatmapService.ts +++ b/src/services/heatmapService.ts @@ -8,10 +8,10 @@ import { HierarchicalItem, HeatmapMatrixInformation, Option, - KsMapType, + KsRecord, LabelIdPair, } from '../components/common/Types.ts'; -import { Filters, SummaryFilters } from '../context/DataContext.ts'; +import { Filters } from '../context/DataContext.ts'; export function getYAxis( hierarchicalNodes: Record, @@ -207,7 +207,7 @@ export function filterOrgans( export function filterKnowledgeStatements( knowledgeStatements: Record, - filters: Filters | SummaryFilters, + filters: Filters, ): Record { const phenotypeIds = filters.Phenotype.map((option) => option.id); const apiNATOMYIds = @@ -215,8 +215,6 @@ export function filterKnowledgeStatements( const speciesIds = filters.Species?.flatMap((option) => option.id) || []; const viaIds = filters.Via?.flatMap((option) => option.id) || []; const originIds = filters.Origin?.flatMap((option) => option.id) || []; - const nerveIds = - (filters as SummaryFilters).Nerve?.map((option) => option.id) || []; return Object.entries(knowledgeStatements).reduce( (filtered, [id, ks]) => { @@ -235,21 +233,13 @@ export function filterKnowledgeStatements( const originMatch = !originIds.length || ks.origins?.some((origin) => originIds.includes(origin.id)); - const nerveMatch = - !nerveIds.length || - ks.vias?.some((via) => - via.anatomical_entities - .map((entity) => entity.id) - .some((id) => nerveIds.includes(id)), - ); if ( phenotypeMatch && apiNATOMYMatch && speciesMatch && viaMatch && - originMatch && - nerveMatch + originMatch ) { filtered[id] = ks; } @@ -258,18 +248,12 @@ export function filterKnowledgeStatements( {} as Record, ); } -export function getHierarchyFromId( - id: string, - hierarchicalNodes: Record, -): HierarchicalNode { - return hierarchicalNodes[id]; -} export function getKnowledgeStatementMap( ksIds: string[], knowledgeStatements: Record, -): KsMapType { - const ksMap: KsMapType = {}; +): KsRecord { + const ksMap: KsRecord = {}; ksIds.forEach((id: string) => { const ks = knowledgeStatements[id]; if (ks) { diff --git a/src/services/summaryHeatmapService.ts b/src/services/summaryHeatmapService.ts index f12e39b..1eec1e9 100644 --- a/src/services/summaryHeatmapService.ts +++ b/src/services/summaryHeatmapService.ts @@ -2,7 +2,7 @@ import chroma from 'chroma-js'; import { HierarchicalItem, PhenotypeKsIdMap, - KsMapType, + KsRecord, } from '../components/common/Types.ts'; import { ConnectionSummary, SummaryFilters } from '../context/DataContext.ts'; import { @@ -12,7 +12,6 @@ import { BaseEntity, } from '../models/explorer.ts'; import { OTHER_PHENOTYPE_LABEL } from '../settings.ts'; -import { filterKnowledgeStatements } from './heatmapService.ts'; export const generatePhenotypeColors = (num: number) => { const scale = chroma @@ -30,7 +29,7 @@ export function convertViaToString(via: string[]): string { return via[0]; } -export function getAllViasFromConnections(connections: KsMapType): { +export function getAllViasFromConnections(connections: KsRecord): { [key: string]: string; } { const vias: { [key: string]: string } = {}; @@ -47,7 +46,7 @@ export function getAllViasFromConnections(connections: KsMapType): { return vias; } -export function getAllPhenotypes(connections: KsMapType): string[] { +export function getAllPhenotypes(connections: KsRecord): string[] { const phenotypeNames: Set = new Set(); Object.values(connections).forEach((ks) => { @@ -74,6 +73,32 @@ export const getNerveFilters = ( return nerves; }; +export function summaryFilterKnowledgeStatements( + knowledgeStatements: Record, + summaryFilters: SummaryFilters, +): Record { + const phenotypeIds = summaryFilters.Phenotype.map((option) => option.id); + const nerveIds = summaryFilters.Nerve.map((option) => option.id); + return Object.entries(knowledgeStatements).reduce( + (filtered, [id, ks]) => { + const phenotypeMatch = + !phenotypeIds.length || phenotypeIds.includes(ks.phenotype); + const nerveMatch = + !nerveIds.length || + ks.vias?.some((via) => + via.anatomical_entities + .map((entity) => entity.id) + .some((id) => nerveIds.includes(id)), + ); + if (phenotypeMatch && nerveMatch) { + filtered[id] = ks; + } + return filtered; + }, + {} as Record, + ); +} + // NOTE: this function is similar to /services/heatmapService.ts - getHeatmapData // output type - PhenotypeKsIdMap[][] // ***** Recursive function - traverseItems ***** @@ -129,7 +154,8 @@ export function calculateSecondaryConnections( hierarchyNode: HierarchicalNode, ): Map { // Apply filters to organs and knowledge statements - const knowledgeStatements = filterKnowledgeStatements( + + const knowledgeStatements = summaryFilterKnowledgeStatements( allKnowledgeStatements, summaryFilters, ); @@ -237,7 +263,7 @@ export const getDestinations = ( }; export const getConnectionDetails = ( - uniqueKS: KsMapType, + uniqueKS: KsRecord, connectionPage: number, ): KnowledgeStatement => { return uniqueKS !== undefined From 799f59be52837459ea5a84c78931f402ac810df0 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 5 Jun 2024 17:52:49 +0100 Subject: [PATCH 07/12] ESCKAN-55 chore: Use specific type instead of inline --- src/components/SummaryFiltersDropdown.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SummaryFiltersDropdown.tsx b/src/components/SummaryFiltersDropdown.tsx index 490bdd0..1264f58 100644 --- a/src/components/SummaryFiltersDropdown.tsx +++ b/src/components/SummaryFiltersDropdown.tsx @@ -8,8 +8,10 @@ import { } from '../services/searchService'; import { OTHER_PHENOTYPE_LABEL } from '../settings'; +type FilterKey = 'Phenotype' | 'Nerve'; + interface FilterConfig { - id: 'Phenotype' | 'Nerve'; + id: FilterKey; placeholder: string; searchPlaceholder: string; } @@ -71,8 +73,6 @@ const SummaryFiltersDropdown = ({ const nerveOptions = useMemo(() => convertNervesToOptions(nerves), [nerves]); - type FilterKey = 'Phenotype' | 'Nerve'; - const filterStateMap: { [K in FilterKey]: { filters: Option[]; From d48ab504267cff84183b4cf0347d789a42123006 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 5 Jun 2024 19:24:11 +0100 Subject: [PATCH 08/12] ESCKAN-55 feat: Connect reset grid button --- src/components/ConnectivityGrid.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/components/ConnectivityGrid.tsx b/src/components/ConnectivityGrid.tsx index 0969855..058878a 100644 --- a/src/components/ConnectivityGrid.tsx +++ b/src/components/ConnectivityGrid.tsx @@ -31,6 +31,7 @@ function ConnectivityGrid() { organs, knowledgeStatements, filters, + setFilters, setSelectedConnectionSummary, } = useDataContext(); @@ -44,6 +45,7 @@ function ConnectivityGrid() { x: number; y: number; } | null>(null); + const [initialYAxis, setInitialYAxis] = useState([]); useEffect(() => { const connections = calculateConnections( @@ -71,6 +73,7 @@ function ConnectivityGrid() { useEffect(() => { const yAxis = getYAxis(hierarchicalNodes); setYAxis(yAxis); + setInitialYAxis(yAxis); }, [hierarchicalNodes]); const { heatmapData, detailedHeatmapData } = useMemo(() => { @@ -98,6 +101,20 @@ function ConnectivityGrid() { }); } }; + + const handleReset = () => { + setYAxis(initialYAxis); + setFilters({ + Origin: [], + EndOrgan: [], + Species: [], + Phenotype: [], + apiNATOMY: [], + Via: [], + }); + setSelectedCell(null); + }; + const isLoading = yAxis.length == 0; return isLoading ? ( @@ -154,6 +171,7 @@ function ConnectivityGrid() { background: 'transparent', }, }} + onClick={handleReset} > Reset grid From 2c9205ce6aca01924fa0ab7285568b704dd3b0c2 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Fri, 7 Jun 2024 00:09:35 +0200 Subject: [PATCH 09/12] csv generation --- src/components/connections/SummaryDetails.tsx | 112 +++++++++++++++++- src/components/connections/SummaryHeader.tsx | 111 ++++++++++++++++- 2 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index f671977..3cc20e4 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import React from 'react'; import { Typography, Button, Stack, Divider, Box } from '@mui/material'; import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded'; @@ -8,6 +9,7 @@ import CommonChip from '../common/CommonChip.tsx'; import { ArrowOutward } from '../icons/index.tsx'; import { KsMapType } from '../common/Types.ts'; import { getConnectionDetails } from '../../services/summaryHeatmapService.ts'; +import { getKnowledgeStatementMap } from '../../services/heatmapService.ts'; const { gray500, gray700, gray800 } = vars; @@ -92,8 +94,114 @@ const SummaryDetails = ({ ]; const generateCSV = () => { - console.log('Generating CSV'); - console.log(knowledgeStatementsMap); + const properties = [ + 'id', + 'statement_preview', + 'provenances', + 'journey', + 'phenotype', + 'laterality', + 'projection', + 'circuit_type', + 'sex', + 'species', + 'apinatomy', + 'journey', + 'origins', + 'vias', + 'destinations', + ]; + const keys = Object.keys(knowledgeStatementsMap); + const rows = [properties]; + keys.forEach((key) => { + const ks = knowledgeStatementsMap[key]; + const row = properties.map((property) => { + if (property === 'origins') { + const node = []; + // node.push('['); + ks[property].forEach((origin) => { + node.push( + 'URIs: ' + + origin['ontology_uri'] + + '; Label: ' + + origin['name'] + + ' # ', + ); + }); + // node.push(']'); + const toReturn = node + .join('') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'vias' || property === 'destinations') { + const node = []; + node.push('['); + ks[property].forEach((viaDest) => { + node.push( + viaDest['anatomical_entities'].map( + (e) => + 'URI: ' + e['ontology_uri'] + ' Label: ' + e['name'] + '; ', + ) + + '; Type: ' + + viaDest['type'] + + '; From: ' + + viaDest['from_entities'] + .map((e) => e['ontology_uri']) + .join('; ') + + ' # ', + ); + }); + node.push(']'); + const toReturn = node + .join('') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'sex') { + return ks[property].name + ' ' + ks[property].ontology_uri; + } else if (Array.isArray(ks[property])) { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .join(' # ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } + }); + rows.push(row); + }); + + let csvData = ''; + rows.forEach((e) => { + const toReturn = e + .map(String) + .map((v) => v.replaceAll('"', '""')) + .map((v) => `"${v}"`) + .join(','); + csvData += toReturn + '\n'; + }); + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8,' }); + const objUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', objUrl); + link.setAttribute('download', 'connections.csv'); + document.body.appendChild(link); + link.click(); }; return ( diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index 827ed5a..0683d8a 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -1,3 +1,4 @@ +// @ts-nocheck import { Box, Button, @@ -55,8 +56,114 @@ const SummaryHeader = ({ }; const generateCSV = () => { - console.log('Generating CSV'); - console.log(selectedConnectionSummary); + const properties = [ + 'id', + 'statement_preview', + 'provenances', + 'journey', + 'phenotype', + 'laterality', + 'projection', + 'circuit_type', + 'sex', + 'species', + 'apinatomy', + 'journey', + 'origins', + 'vias', + 'destinations', + ]; + const keys = Object.keys(selectedConnectionSummary['connections']); + const rows = [properties]; + keys.forEach((key) => { + const ks = selectedConnectionSummary['connections'][key]; + const row = properties.map((property) => { + if (property === 'origins') { + const node = []; + // node.push('['); + ks[property].forEach((origin) => { + node.push( + 'URIs: ' + + origin['ontology_uri'] + + '; Label: ' + + origin['name'] + + ' # ', + ); + }); + // node.push(']'); + const toReturn = node + .join('') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'vias' || property === 'destinations') { + const node = []; + node.push('['); + ks[property].forEach((viaDest) => { + node.push( + viaDest['anatomical_entities'].map( + (e) => + 'URI: ' + e['ontology_uri'] + ' Label: ' + e['name'] + '; ', + ) + + '; Type: ' + + viaDest['type'] + + '; From: ' + + viaDest['from_entities'] + .map((e) => e['ontology_uri']) + .join('; ') + + ' # ', + ); + }); + node.push(']'); + const toReturn = node + .join('') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'sex') { + return ks[property].name + ' ' + ks[property].ontology_uri; + } else if (Array.isArray(ks[property])) { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .join(' # ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } + }); + rows.push(row); + }); + + let csvData = ''; + rows.forEach((e) => { + const toReturn = e + .map(String) + .map((v) => v.replaceAll('"', '""')) + .map((v) => `"${v}"`) + .join(','); + csvData += toReturn + '\n'; + }); + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8,' }); + const objUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', objUrl); + link.setAttribute('download', 'connections.csv'); + document.body.appendChild(link); + link.click(); }; if (showDetails === SummaryType.Instruction) { From 9174659276790e55c6083039575eab36d2574d10 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Fri, 7 Jun 2024 11:22:25 +0200 Subject: [PATCH 10/12] Fixing few issues in the csv generation --- src/components/connections/SummaryDetails.tsx | 86 ++++++++++++++----- src/components/connections/SummaryHeader.tsx | 86 ++++++++++++++----- 2 files changed, 126 insertions(+), 46 deletions(-) diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index 3cc20e4..4ac4076 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -98,7 +98,6 @@ const SummaryDetails = ({ 'id', 'statement_preview', 'provenances', - 'journey', 'phenotype', 'laterality', 'projection', @@ -118,56 +117,97 @@ const SummaryDetails = ({ const row = properties.map((property) => { if (property === 'origins') { const node = []; - // node.push('['); ks[property].forEach((origin) => { node.push( - 'URIs: ' + + '[ URIs: ' + origin['ontology_uri'] + '; Label: ' + origin['name'] + - ' # ', + ' ]', ); }); - // node.push(']'); const toReturn = node - .join('') + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') .replaceAll(',', ';'); return toReturn; - } else if (property === 'vias' || property === 'destinations') { + } else if (property === 'vias') { const node = []; - node.push('['); - ks[property].forEach((viaDest) => { + ks[property].forEach((via) => { node.push( - viaDest['anatomical_entities'].map( - (e) => - 'URI: ' + e['ontology_uri'] + ' Label: ' + e['name'] + '; ', - ) + - '; Type: ' + - viaDest['type'] + + '[ (' + + via['anatomical_entities'] + .map( + (e) => + 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + via['type'] + '; From: ' + - viaDest['from_entities'] - .map((e) => e['ontology_uri']) - .join('; ') + - ' # ', + via['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', ); }); - node.push(']'); const toReturn = node - .join('') + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'destinations') { + const node = []; + ks[property].forEach((dest) => { + node.push( + '[ (' + + dest['anatomical_entities'] + .map( + (e) => + 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + dest['type'] + + '; From: ' + + dest['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', + ); + }); + const toReturn = node + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') .replaceAll(',', ';'); return toReturn; } else if (property === 'sex') { - return ks[property].name + ' ' + ks[property].ontology_uri; + if (ks[property].name && ks[property].ontology_uri) { + return ( + '[ URI: ' + + ks[property].ontology_uri + + '; Label: ' + + ks[property].name + + ' ]' + ); + } else { + return ''; + } + } else if (property === 'species') { + if (ks[property].length) { + return ks[property] + .map((e) => '[ URI: ' + e.id + '; Label: ' + e.name + ' ]') + .join(' & '); + } else { + return ''; + } } else if (Array.isArray(ks[property])) { // @ts-expect-error - TS doesn't know that ks[property] exists const toReturn = ks[property] - .join(' # ') + .map((v) => '[ ' + v + ' ]') + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index 0683d8a..2579622 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -60,7 +60,6 @@ const SummaryHeader = ({ 'id', 'statement_preview', 'provenances', - 'journey', 'phenotype', 'laterality', 'projection', @@ -80,56 +79,97 @@ const SummaryHeader = ({ const row = properties.map((property) => { if (property === 'origins') { const node = []; - // node.push('['); ks[property].forEach((origin) => { node.push( - 'URIs: ' + + '[ URIs: ' + origin['ontology_uri'] + '; Label: ' + origin['name'] + - ' # ', + ' ]', ); }); - // node.push(']'); const toReturn = node - .join('') + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') .replaceAll(',', ';'); return toReturn; - } else if (property === 'vias' || property === 'destinations') { + } else if (property === 'vias') { const node = []; - node.push('['); - ks[property].forEach((viaDest) => { + ks[property].forEach((via) => { node.push( - viaDest['anatomical_entities'].map( - (e) => - 'URI: ' + e['ontology_uri'] + ' Label: ' + e['name'] + '; ', - ) + - '; Type: ' + - viaDest['type'] + + '[ (' + + via['anatomical_entities'] + .map( + (e) => + 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + via['type'] + '; From: ' + - viaDest['from_entities'] - .map((e) => e['ontology_uri']) - .join('; ') + - ' # ', + via['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', ); }); - node.push(']'); const toReturn = node - .join('') + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'destinations') { + const node = []; + ks[property].forEach((dest) => { + node.push( + '[ (' + + dest['anatomical_entities'] + .map( + (e) => + 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + dest['type'] + + '; From: ' + + dest['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', + ); + }); + const toReturn = node + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') .replaceAll(',', ';'); return toReturn; } else if (property === 'sex') { - return ks[property].name + ' ' + ks[property].ontology_uri; + if (ks[property].name && ks[property].ontology_uri) { + return ( + '[ URI: ' + + ks[property].ontology_uri + + '; Label: ' + + ks[property].name + + ' ]' + ); + } else { + return ''; + } + } else if (property === 'species') { + if (ks[property].length) { + return ks[property] + .map((e) => '[ URI: ' + e.id + '; Label: ' + e.name + ' ]') + .join(' & '); + } else { + return ''; + } } else if (Array.isArray(ks[property])) { // @ts-expect-error - TS doesn't know that ks[property] exists const toReturn = ks[property] - .join(' # ') + .map((v) => '[ ' + v + ' ]') + .join(' & ') .replaceAll('\n', '. ') .replaceAll('\r', '') .replaceAll('\t', ' ') From 8376936873fd7802000f16780adff8813a5954c9 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Mon, 10 Jun 2024 16:02:36 +0200 Subject: [PATCH 11/12] lint issues --- src/components/connections/SummaryDetails.tsx | 2 +- src/components/connections/SummaryHeader.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index ff81d24..ffb4aab 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck import React from 'react'; import { Typography, Button, Stack, Divider, Box } from '@mui/material'; @@ -9,7 +10,6 @@ import CommonChip from '../common/CommonChip.tsx'; import { ArrowOutward } from '../icons/index.tsx'; import { KsRecord } from '../common/Types.ts'; import { getConnectionDetails } from '../../services/summaryHeatmapService.ts'; -import { getKnowledgeStatementMap } from '../../services/heatmapService.ts'; const { gray500, gray700, gray800 } = vars; diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index ef9822a..7eef3f8 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ // @ts-nocheck import { Box, From e2223ffc8664a194a1d6b062e052d79b7c8f64c5 Mon Sep 17 00:00:00 2001 From: Dario Del Piano Date: Mon, 10 Jun 2024 16:26:23 +0200 Subject: [PATCH 12/12] Fix linter issues --- src/components/connections/SummaryDetails.tsx | 146 +---------------- src/components/connections/SummaryHeader.tsx | 147 +---------------- src/context/DataContextProvider.tsx | 2 +- src/services/csvService.ts | 152 ++++++++++++++++++ 4 files changed, 158 insertions(+), 289 deletions(-) create mode 100644 src/services/csvService.ts diff --git a/src/components/connections/SummaryDetails.tsx b/src/components/connections/SummaryDetails.tsx index ffb4aab..b729fb6 100644 --- a/src/components/connections/SummaryDetails.tsx +++ b/src/components/connections/SummaryDetails.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import React from 'react'; import { Typography, Button, Stack, Divider, Box } from '@mui/material'; import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded'; @@ -10,6 +8,7 @@ import CommonChip from '../common/CommonChip.tsx'; import { ArrowOutward } from '../icons/index.tsx'; import { KsRecord } from '../common/Types.ts'; import { getConnectionDetails } from '../../services/summaryHeatmapService.ts'; +import { generateCsvService } from '../../services/csvService.ts'; const { gray500, gray700, gray800 } = vars; @@ -94,148 +93,7 @@ const SummaryDetails = ({ ]; const generateCSV = () => { - const properties = [ - 'id', - 'statement_preview', - 'provenances', - 'phenotype', - 'laterality', - 'projection', - 'circuit_type', - 'sex', - 'species', - 'apinatomy', - 'journey', - 'origins', - 'vias', - 'destinations', - ]; - const keys = Object.keys(knowledgeStatementsMap); - const rows = [properties]; - keys.forEach((key) => { - const ks = knowledgeStatementsMap[key]; - const row = properties.map((property) => { - if (property === 'origins') { - const node = []; - ks[property].forEach((origin) => { - node.push( - '[ URIs: ' + - origin['ontology_uri'] + - '; Label: ' + - origin['name'] + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'vias') { - const node = []; - ks[property].forEach((via) => { - node.push( - '[ (' + - via['anatomical_entities'] - .map( - (e) => - 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], - ) - .join(' & ') + - '); Type: ' + - via['type'] + - '; From: ' + - via['from_entities'].map((e) => e['ontology_uri']).join('; ') + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'destinations') { - const node = []; - ks[property].forEach((dest) => { - node.push( - '[ (' + - dest['anatomical_entities'] - .map( - (e) => - 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], - ) - .join(' & ') + - '); Type: ' + - dest['type'] + - '; From: ' + - dest['from_entities'].map((e) => e['ontology_uri']).join('; ') + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'sex') { - if (ks[property].name && ks[property].ontology_uri) { - return ( - '[ URI: ' + - ks[property].ontology_uri + - '; Label: ' + - ks[property].name + - ' ]' - ); - } else { - return ''; - } - } else if (property === 'species') { - if (ks[property].length) { - return ks[property] - .map((e) => '[ URI: ' + e.id + '; Label: ' + e.name + ' ]') - .join(' & '); - } else { - return ''; - } - } else if (Array.isArray(ks[property])) { - // @ts-expect-error - TS doesn't know that ks[property] exists - const toReturn = ks[property] - .map((v) => '[ ' + v + ' ]') - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else { - // @ts-expect-error - TS doesn't know that ks[property] exists - const toReturn = ks[property] - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } - }); - rows.push(row); - }); - - let csvData = ''; - rows.forEach((e) => { - const toReturn = e - .map(String) - .map((v) => v.replaceAll('"', '""')) - .map((v) => `"${v}"`) - .join(','); - csvData += toReturn + '\n'; - }); - const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8,' }); + const blob = generateCsvService(knowledgeStatementsMap); const objUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', objUrl); diff --git a/src/components/connections/SummaryHeader.tsx b/src/components/connections/SummaryHeader.tsx index 7eef3f8..ae0f037 100644 --- a/src/components/connections/SummaryHeader.tsx +++ b/src/components/connections/SummaryHeader.tsx @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ -// @ts-nocheck import { Box, Button, @@ -15,6 +13,7 @@ import { ArrowDown, ArrowRight, ArrowUp, HelpCircle } from '../icons'; import Breadcrumbs from '@mui/material/Breadcrumbs'; import { SummaryType, KsRecord } from '../common/Types'; import { useDataContext } from '../../context/DataContext.ts'; +import { generateCsvService } from '../../services/csvService.ts'; const { gray100, gray600A, gray500 } = vars; @@ -57,148 +56,8 @@ const SummaryHeader = ({ }; const generateCSV = () => { - const properties = [ - 'id', - 'statement_preview', - 'provenances', - 'phenotype', - 'laterality', - 'projection', - 'circuit_type', - 'sex', - 'species', - 'apinatomy', - 'journey', - 'origins', - 'vias', - 'destinations', - ]; - const keys = Object.keys(selectedConnectionSummary['connections']); - const rows = [properties]; - keys.forEach((key) => { - const ks = selectedConnectionSummary['connections'][key]; - const row = properties.map((property) => { - if (property === 'origins') { - const node = []; - ks[property].forEach((origin) => { - node.push( - '[ URIs: ' + - origin['ontology_uri'] + - '; Label: ' + - origin['name'] + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'vias') { - const node = []; - ks[property].forEach((via) => { - node.push( - '[ (' + - via['anatomical_entities'] - .map( - (e) => - 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], - ) - .join(' & ') + - '); Type: ' + - via['type'] + - '; From: ' + - via['from_entities'].map((e) => e['ontology_uri']).join('; ') + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'destinations') { - const node = []; - ks[property].forEach((dest) => { - node.push( - '[ (' + - dest['anatomical_entities'] - .map( - (e) => - 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], - ) - .join(' & ') + - '); Type: ' + - dest['type'] + - '; From: ' + - dest['from_entities'].map((e) => e['ontology_uri']).join('; ') + - ' ]', - ); - }); - const toReturn = node - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else if (property === 'sex') { - if (ks[property].name && ks[property].ontology_uri) { - return ( - '[ URI: ' + - ks[property].ontology_uri + - '; Label: ' + - ks[property].name + - ' ]' - ); - } else { - return ''; - } - } else if (property === 'species') { - if (ks[property].length) { - return ks[property] - .map((e) => '[ URI: ' + e.id + '; Label: ' + e.name + ' ]') - .join(' & '); - } else { - return ''; - } - } else if (Array.isArray(ks[property])) { - // @ts-expect-error - TS doesn't know that ks[property] exists - const toReturn = ks[property] - .map((v) => '[ ' + v + ' ]') - .join(' & ') - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } else { - // @ts-expect-error - TS doesn't know that ks[property] exists - const toReturn = ks[property] - .replaceAll('\n', '. ') - .replaceAll('\r', '') - .replaceAll('\t', ' ') - .replaceAll(',', ';'); - return toReturn; - } - }); - rows.push(row); - }); - - let csvData = ''; - rows.forEach((e) => { - const toReturn = e - .map(String) - .map((v) => v.replaceAll('"', '""')) - .map((v) => `"${v}"`) - .join(','); - csvData += toReturn + '\n'; - }); - const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8,' }); + // @ts-expect-error - TS doesn't know that selectedConnectionSummary exists + const blob = generateCsvService(selectedConnectionSummary['connections']); const objUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', objUrl); diff --git a/src/context/DataContextProvider.tsx b/src/context/DataContextProvider.tsx index 72ff9f6..4a66651 100644 --- a/src/context/DataContextProvider.tsx +++ b/src/context/DataContextProvider.tsx @@ -81,7 +81,7 @@ export const DataContextProvider = ({ : null, ); } - }, [filters]); + }, [filters, selectedConnectionSummary]); const dataContextValue = { filters, diff --git a/src/services/csvService.ts b/src/services/csvService.ts new file mode 100644 index 0000000..06855d6 --- /dev/null +++ b/src/services/csvService.ts @@ -0,0 +1,152 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +import { KnowledgeStatement } from '../models/explorer'; + +type csvData = { + // declar a type where we have knowledge statements objects linked to a key, similar to a Map + [key: string]: KnowledgeStatement; +}; + +export const generateCsvService = (data: csvData) => { + const properties = [ + 'id', + 'statement_preview', + 'provenances', + 'phenotype', + 'laterality', + 'projection', + 'circuit_type', + 'sex', + 'species', + 'apinatomy', + 'journey', + 'origins', + 'vias', + 'destinations', + ]; + const keys = Object.keys(data); + const rows = [properties]; + keys.forEach((key) => { + const ks = data[key]; + const row = properties.map((property) => { + if (property === 'origins') { + const node = [] as string[]; + ks[property].forEach((origin) => { + node.push( + '[ URIs: ' + + origin['ontology_uri'] + + '; Label: ' + + origin['name'] + + ' ]', + ); + }); + const toReturn = node + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'vias') { + const node = []; + ks[property].forEach((via) => { + node.push( + '[ (' + + via['anatomical_entities'] + .map( + (e) => 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + via['type'] + + '; From: ' + + via['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', + ); + }); + const toReturn = node + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'destinations') { + const node = []; + ks[property].forEach((dest) => { + node.push( + '[ (' + + dest['anatomical_entities'] + .map( + (e) => 'URI: ' + e['ontology_uri'] + '; Label: ' + e['name'], + ) + .join(' & ') + + '); Type: ' + + dest['type'] + + '; From: ' + + dest['from_entities'].map((e) => e['ontology_uri']).join('; ') + + ' ]', + ); + }); + const toReturn = node + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else if (property === 'sex') { + if (ks[property].name && ks[property].ontology_uri) { + return ( + '[ URI: ' + + ks[property].ontology_uri + + '; Label: ' + + ks[property].name + + ' ]' + ); + } else { + return ''; + } + } else if (property === 'species') { + if (ks[property].length) { + return ks[property] + .map((e) => '[ URI: ' + e.id + '; Label: ' + e.name + ' ]') + .join(' & '); + } else { + return ''; + } + } else if (Array.isArray(ks[property])) { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .map((v) => '[ ' + v + ' ]') + .join(' & ') + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } else { + // @ts-expect-error - TS doesn't know that ks[property] exists + const toReturn = ks[property] + .replaceAll('\n', '. ') + .replaceAll('\r', '') + .replaceAll('\t', ' ') + .replaceAll(',', ';'); + return toReturn; + } + }); + rows.push(row); + }); + + let csvData = ''; + rows.forEach((e) => { + const toReturn = e + .map(String) + .map((v) => v.replaceAll('"', '""')) + .map((v) => `"${v}"`) + .join(','); + csvData += toReturn + '\n'; + }); + const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8,' }); + return blob; +};