Unverified Commit 87300f82 authored by Manoj Pandian Sakthivel's avatar Manoj Pandian Sakthivel Committed by GitHub

Species homepage stats (#352)

- Created the ExpandableSection shared component
- Created the species page accordion
- Created the sample-data and the transformation function to populate the species homepage accordion including the example links
Co-authored-by: Andrey Azov's avatarAndrey Azov <andrey@ebi.ac.uk>
parent c981fe33
Pipeline #103252 passed with stages
in 8 minutes and 42 seconds
......@@ -82,6 +82,7 @@ export const DefaultTranscriptListItem = (
/>
</div>
)}
<div className={transcriptsListStyles.middle}>
<div
className={styles.clickableTranscriptArea}
......
......@@ -23,7 +23,6 @@ import { BreakpointWidth } from 'src/global/globalConfig';
import { fetchGenomeData } from 'src/shared/state/genome/genomeActions';
import { setActiveGenomeId } from 'src/content/app/species/state/general/speciesGeneralSlice';
import { getCommittedSpeciesById } from 'src/content/app/species-selector/state/speciesSelectorSelectors';
import { isSidebarOpen } from 'src/content/app/species/state/sidebar/speciesSidebarSelectors';
import { toggleSidebar } from 'src/content/app/species/state/sidebar/speciesSidebarSlice';
......@@ -32,30 +31,21 @@ import SpeciesAppBar from './components/species-app-bar/SpeciesAppBar';
import { StandardAppLayout } from 'src/shared/components/layout';
import SpeciesMainView from 'src/content/app/species/components/species-main-view/SpeciesMainView';
import { RootState } from 'src/store';
type SpeciesPageParams = {
genomeId: string;
};
const SpeciesPage = () => {
const { genomeId } = useParams() as SpeciesPageParams;
const currentSpecies = useSelector((state: RootState) =>
getCommittedSpeciesById(state, genomeId)
);
const sidebarStatus = useSelector(isSidebarOpen);
const dispatch = useDispatch();
useEffect(() => {
dispatch(setActiveGenomeId(genomeId));
dispatch(fetchGenomeData(genomeId));
}, [genomeId]);
useEffect(() => {
if (!currentSpecies) {
dispatch(fetchGenomeData(genomeId));
}
}, [genomeId, currentSpecies]);
const sidebarContent = 'I am sidebar';
const sidebarNavigationContent = 'I am sidebar navigation';
const topbarContent = 'I am topbar content';
......
@import 'src/styles/common';
@mixin narrow-top-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
......@@ -18,5 +21,98 @@
}
.speciesLabelBlock {
padding: 0 75px 0 60px;
padding: 0 75px 0 45px;
}
.statsWrapper {
padding: 5px 20px 40px 120px;
max-width: 1381px;
}
.exampleLinkText {
cursor: pointer;
color: $blue;
position: relative;
}
.pointerBox {
padding: 6px 12px;
background: $black;
color: white;
p {
max-width: 200px;
}
}
.pointerBoxPointer {
fill: $black;
}
.collapsedContent {
display: grid;
padding: 6px 20px;
grid-template-columns: [title] 120px [first_summary_stat] 250px [second_summary_stat] auto [example-link] 150px;
align-items: center;
.title {
grid-column: title;
color: $medium-dark-grey;
}
.summaryStat {
grid-column: first_summary_stat;
.value {
font-size: 26px;
}
.unit {
color: $dark-grey;
padding-left: 5px;
}
}
.summaryStat + .summaryStat {
grid-column: second_summary_stat;
.value {
font-size: 22px;
}
.unit {
color: $dark-grey;
padding-left: 5px;
}
}
.exampleLink {
grid-column: example-link;
}
}
.statsGroup {
padding: 16px 20px 0;
display: grid;
grid-template-columns: [title] 120px [stats] auto;
.title {
grid-column: title;
color: $medium-dark-grey;
}
.stats {
grid-column: stats;
}
&WithExampleLink {
grid-template-columns: [title] 120px [stats] auto [example-link] 150px;
}
.exampleLink {
grid-column: example-link;
}
}
......@@ -16,12 +16,14 @@
import React from 'react';
import SpeciesMainViewStats from './SpeciesMainViewStats';
import SpeciesTitleArea from 'src/content/app/species/components/species-title-area/SpeciesTitleArea';
const SpeciesMainView = () => {
return (
<div>
<SpeciesTitleArea />
<SpeciesMainViewStats />
</div>
);
};
......
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, ReactNode } from 'react';
import { connect } from 'react-redux';
import classNames from 'classnames';
import ViewInAppPopup from 'src/shared/components/view-in-app-popup/ViewInAppPopup';
import SpeciesStats from 'src/content/app/species/components/species-stats/SpeciesStats';
import ExpandableSection from 'src/shared/components/expandable-section/ExpandableSection';
import {
getActiveGenomeId,
getActiveGenomeStats
} from 'src/content/app/species/state/general/speciesGeneralSelectors';
import { getGenomeExampleFocusObjects } from 'src/shared/state/genome/genomeSelectors';
import { fetchStatsForActiveGenome } from 'src/content/app/species/state/general/speciesGeneralSlice';
import { RootState } from 'src/store';
import { ExampleFocusObject } from 'src/shared/state/genome/genomeTypes';
import { GenomeStats } from '../../state/general/speciesGeneralSlice';
import { urlObj } from 'src/shared/components/view-in-app/ViewInApp';
import {
StatsSection,
sectionGroupsMap
} from '../../state/general/speciesGeneralHelper';
import styles from './SpeciesMainView.scss';
type Props = {
activeGenomeId: string | null;
genomeStats: GenomeStats | undefined;
exampleFocusObjects: ExampleFocusObject[];
fetchStatsForActiveGenome: () => void;
};
type ExampleLinkPopupProps = {
links: Partial<urlObj>;
children: ReactNode;
};
const ExampleLinkWithPopup = (props: ExampleLinkPopupProps) => {
return (
<div className={styles.exampleLink}>
<ViewInAppPopup links={props.links}>
<span className={styles.exampleLinkText}>{props.children}</span>
</ViewInAppPopup>
</div>
);
};
const getCollapsedContent = (statsSection: StatsSection) => {
const { summaryStats, section, exampleLinks } = statsSection;
const { title, exampleLinkText } = sectionGroupsMap[section];
return (
<div className={styles.collapsedContent}>
<span className={styles.title}>{title}</span>
{summaryStats?.length &&
summaryStats.map((summaryStat, index) => {
return (
<div className={styles.summaryStat} key={index}>
<span className={styles.value}>{summaryStat.primaryValue}</span>
<span className={styles.unit}>{summaryStat.primaryUnit}</span>
</div>
);
})}
{exampleLinks && (
<ExampleLinkWithPopup links={exampleLinks}>
{exampleLinkText}
</ExampleLinkWithPopup>
)}
</div>
);
};
const getExpandedContent = (statsSection: StatsSection) => {
const { groups, exampleLinks, section } = statsSection;
const { exampleLinkText } = sectionGroupsMap[section];
return (
<div className={styles.expandedContent}>
{groups.map((group, group_index) => {
const { title, stats } = group;
return stats.map((groupStats, row_index) => {
const statsGroupClassName = classNames(styles.statsGroup, {
[styles.statsGroupWithExampleLink]:
!group_index && !row_index && exampleLinkText
});
return (
<div key={row_index} className={statsGroupClassName}>
{row_index === 0 && <span className={styles.title}>{title}</span>}
<div className={styles.stats}>
{groupStats.map((stat, stat_index) => {
return <SpeciesStats key={stat_index} {...stat} />;
})}
</div>
{group_index === 0 && row_index === 0 && exampleLinks && (
<ExampleLinkWithPopup links={exampleLinks}>
{exampleLinkText}
</ExampleLinkWithPopup>
)}
</div>
);
});
})}
</div>
);
};
const SpeciesMainViewStats = (props: Props) => {
useEffect(() => {
if (!props.genomeStats && props.exampleFocusObjects?.length) {
props.fetchStatsForActiveGenome();
}
}, [props.genomeStats, props.activeGenomeId, props.exampleFocusObjects]);
if (!props.genomeStats || !props.exampleFocusObjects?.length) {
return null;
}
return (
<div className={styles.statsWrapper}>
{props.genomeStats.map((section, key) => {
return (
<ExpandableSection
key={key}
collapsedContent={getCollapsedContent(section)}
expandedContent={getExpandedContent(section)}
/>
);
})}
</div>
);
};
const mapStateToProps = (state: RootState) => {
const activeGenomeId = getActiveGenomeId(state);
return {
activeGenomeId,
genomeStats: getActiveGenomeStats(state),
exampleFocusObjects: activeGenomeId
? getGenomeExampleFocusObjects(state, activeGenomeId)
: []
};
};
const mapDispatchToProps = {
fetchStatsForActiveGenome
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(SpeciesMainViewStats);
......@@ -4,38 +4,35 @@ $secondary-text-color: $dark-grey;
.wrapper {
width: 250px;
margin-bottom: 30px;
display: inline-block;
vertical-align: top;
}
.preLabel {
color: $secondary-text-color;
font-size: 11px;
}
.label {
font-size: 12px;
.preLabel + .label {
margin-top: -3px;
font-weight: $bold;
}
.primaryValue {
font-size: 22px;
font-size: 26px;
padding-left: 30px;
}
.primaryUnit {
color: $secondary-text-color;
font-size: 13px;
color: $black;
padding-left: 5px;
font-weight: $light;
}
.secondaryValue {
font-size: 12px;
padding-left: 30px;
}
.secondaryUnit {
color: $secondary-text-color;
font-size: 11px;
color: $black;
padding-left: 3px;
font-weight: $light;
}
.link {
text-align: center;
......
......@@ -20,12 +20,12 @@ import classNames from 'classnames';
import defaultStyles from './SpeciesStats.scss';
type PrimaryDataProps = {
primaryValue: string;
primaryValue: string | number;
primaryUnit?: string;
};
type PropsWithSecondaryData = {
secondaryValue: string;
secondaryValue: string | number;
secondaryUnit?: string;
};
......@@ -39,9 +39,9 @@ type ClassNamesProps = {
wrapper?: string;
preLabel?: string;
label?: string;
primaryValue?: string;
primaryValue?: string | number;
primaryUnit?: string;
secondaryValue?: string;
secondaryValue?: string | number;
secondaryUnit?: string;
link?: string;
};
......@@ -81,7 +81,9 @@ const SpeciesStats = (props: SpeciesStatsProps) => {
return (
<div className={styles.wrapper}>
<span className={styles.preLabel}>{props.preLabel}</span>
{props.preLabel && (
<span className={styles.preLabel}>{props.preLabel}</span>
)}
<div className={styles.label}>{props.label}</div>
......
......@@ -18,3 +18,13 @@ import { RootState } from 'src/store';
export const getActiveGenomeId = (state: RootState) =>
state.speciesPage.general.activeGenomeId;
export const getActiveGenomeStats = (state: RootState) => {
const activeGenomeId = getActiveGenomeId(state);
if (!activeGenomeId) {
return;
}
return state.speciesPage.general.stats[activeGenomeId];
};
......@@ -13,15 +13,71 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Action,
createSlice,
PayloadAction,
ThunkAction
} from '@reduxjs/toolkit';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { getActiveGenomeId } from './speciesGeneralSelectors';
import { getGenomeExampleFocusObjects } from 'src/shared/state/genome/genomeSelectors';
import {
getStatsForSection,
StatsSection,
SpeciesStatsSection
} from 'src/content/app/species/state/general/speciesGeneralHelper';
import { RootState } from 'src/store';
export type GenomeStats = StatsSection[];
type SpeciesGeneralState = {
activeGenomeId: string | null;
stats: {
[genomeId: string]: GenomeStats | undefined;
};
};
const initialState: SpeciesGeneralState = {
activeGenomeId: null
activeGenomeId: null,
stats: {}
};
export const fetchStatsForActiveGenome = (): ThunkAction<
void,
any,
null,
Action<string>
> => (dispatch, getState: () => RootState) => {
const state = getState();
const activeGenomeId = getActiveGenomeId(state);
if (!activeGenomeId) {
return;
}
const exampleFocusObjects = getGenomeExampleFocusObjects(
state,
activeGenomeId
);
const speciesStats = Object.values(SpeciesStatsSection)
.map((section) =>
getStatsForSection({
genome_id: activeGenomeId,
section,
exampleFocusObjects
})
)
.filter(Boolean) as GenomeStats;
dispatch(
speciesGeneralSlice.actions.setStatsForGenomeId({
genomeId: activeGenomeId,
stats: speciesStats
})
);
};
const speciesGeneralSlice = createSlice({
......@@ -30,10 +86,20 @@ const speciesGeneralSlice = createSlice({
reducers: {
setActiveGenomeId(state, action: PayloadAction<string>) {
state.activeGenomeId = action.payload;
},
setStatsForGenomeId(
state,
action: PayloadAction<{ genomeId: string; stats: GenomeStats }>
) {
state.stats[action.payload.genomeId] = action.payload.stats;
}
}
});
export const { setActiveGenomeId } = speciesGeneralSlice.actions;
export const {
setActiveGenomeId,
setStatsForGenomeId
} = speciesGeneralSlice.actions;
export default speciesGeneralSlice.reducer;
@import 'src/styles/common';
.expandableSection {
border: 1px solid $medium-light-grey;
position: relative;
min-height: 45px;
padding-right: 55px;
}
.expandableSection + .expandableSection {
border-top: none;
}
.toggle {
position: absolute;
top: 0px;
right: 0px;
width: 85px;
height: 45px;
text-align: center;
cursor: pointer;
}
.chevron {
cursor: pointer;
fill: $blue;
height: 20px;
width: 20px;
top: 12px;
position: absolute;
left: 35px;
transform: rotate(90deg);
transition: transform linear 0.2s;
&Down {
transform: rotate(-90deg);
}
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/