UNPKG

auspice

Version:

Web app for visualizing pathogen evolution

314 lines (275 loc) 8.39 kB
import React, { Suspense, lazy } from "react"; import { connect } from "react-redux"; import styled from 'styled-components'; import Collapsible from "react-collapsible"; import { withTranslation } from "react-i18next"; import {FaAngleUp, FaAngleDown} from "react-icons/fa"; import { dataFont } from "../../globalStyles"; import Flex from "./flex"; import { applyFilter } from "../../actions/tree"; import { isValueValid } from "../../util/globals"; import hardCodedFooters from "./footer-descriptions"; import { SimpleFilter } from "../info/filterBadge"; const MarkdownDisplay = lazy(() => import("../markdownDisplay")); const FooterStyles = styled.div` margin-left: 30px; padding-bottom: 0px; font-family: ${dataFont}; font-size: 15px; font-weight: 300; color: rgb(136, 136, 136); line-height: 1.4; p { font-weight: 300; } ol { font-weight: 300; } ul { font-weight: 300; } h1 { font-weight: 700; font-size: 2.50em; margin: 0.2em 0; } h2 { font-weight: 600; font-size: 2.15em; margin: 0.2em 0; } h3 { font-weight: 500; font-size: 1.70em; margin: 0.2em 0; } h4 { font-weight: 500; font-size: 1.25em; margin: 0.1em 0; } h5 { font-weight: 500; font-size: 1.0em; margin: 0.1em 0; } h6 { font-weight: 500; font-size: 0.85em; margin: 0.1em 0; } // Style for code block pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 3px; } // Code within code block pre code { padding: 0; margin: 0; overflow: visible; font-size: 100%; line-height: inherit; word-wrap: normal; background-color: initial; border: 0; } // Inline code p code { padding: .2em .4em; margin: 0; font-size: 85%; background-color: rgba(27,31,35,.05); border-radius: 3px; } .line { margin-top: 15px; margin-bottom: 15px; border-bottom: 1px solid #CCC; } .acknowledgments { margin-top: 10px; } .filterList { margin-top: 10px; line-height: 1.0; } .imageContainer { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; } img { margin-left: 30px; margin-right: 30px; margin-top: 2px; margin-bottom: 2px; } table { font-weight: 300; margin-bottom: 1rem; th, td { text-align: left; padding: 0.45rem; vertical-align: top; border-top: 1px solid #dee2e6; } thead th { vertical-align: bottom; border-bottom: 2px solid #dee2e6; } tbody + tbody { border-top: 2px solid #dee2e6; } } `; export const getAcknowledgments = (metadata, dispatch) => { /** * If the metadata contains a description key, then it will take precedence the hard-coded * acknowledgements. Expects the text in the description to be in Markdown format. * Jover. December 2019. */ if (metadata.description) { return ( <Suspense fallback={<div />}> <MarkdownDisplay className="acknowledgments" mdstring={metadata.description} placeholder="This dataset contained acknowledgements to be displayed here, however it wasn't correctly formatted." /> </Suspense> ); } const preambleContent = "This work is made possible by the open sharing of genetic data by research groups from all over the world. We gratefully acknowledge their contributions."; const genericPreamble = (<div>{preambleContent}</div>); if (window.location.hostname === 'nextstrain.org') { return hardCodedFooters(dispatch, genericPreamble); } return (<div>{genericPreamble}</div>); }; const dispatchFilter = (dispatch, activeFilters, key, value) => { const activeValuesOfFilter = (activeFilters[key] || []).map((f) => f.value); const mode = activeValuesOfFilter.indexOf(value) === -1 ? "add" : "remove"; dispatch(applyFilter(mode, key, [value])); }; const removeFiltersButton = (dispatch, filterNames, outerClassName, label) => ( <SimpleFilter active extraStyles={{float: "right", margin: "0px 4px"}} onClick={() => {filterNames.forEach((n) => dispatch(applyFilter("set", n, [])));}} > {label} </SimpleFilter> ); const TitleContainer = styled.div` display: flex; justify-content: space-between; margin: 0px; padding: 0px; `; const IconContainer = styled.span` font-size: 22px; font-weight: 500; `; const CollapseTitle = ({name, isExpanded=false}) => ( <TitleContainer> {name} <IconContainer>{isExpanded ? <FaAngleUp /> : <FaAngleDown />}</IconContainer> </TitleContainer> ); @connect((state) => { return { tree: state.tree, totalStateCounts: state.tree.totalStateCounts, metadata: state.metadata, colorOptions: state.metadata.colorOptions, browserDimensions: state.browserDimensions.browserDimensions, activeFilters: state.controls.filters, filtersInFooter: state.controls.filtersInFooter }; }) class Footer extends React.Component { shouldComponentUpdate(nextProps) { if (this.props.tree.version !== nextProps.tree.version || this.props.browserDimensions !== nextProps.browserDimensions) { return true; } else if (Object.keys(this.props.activeFilters) !== Object.keys(nextProps.activeFilters)) { return true; } else if (Object.keys(this.props.activeFilters).length > 0) { for (const name of this.props.activeFilters) { if (this.props.activeFilters[name] !== nextProps.activeFilters[name]) { return true; } } } return false; } displayFilter(filterName) { const { t } = this.props; const totalStateCount = this.props.totalStateCounts[filterName]; if (!totalStateCount) return null; const filterTitle = this.props.metadata.colorings[filterName] ? this.props.metadata.colorings[filterName].title : filterName; const activeFilterItems = (this.props.activeFilters[filterName] || []).filter((x) => x.active).map((x) => x.value); const title = (<div> {t("Filter by {{filterTitle}}", {filterTitle: filterTitle}) + ` (n=${totalStateCount.size})`} {this.props.activeFilters?.[filterName]?.length ? removeFiltersButton(this.props.dispatch, [filterName], "inlineRight", t("Clear {{filterName}} filter", { filterName: filterName})) : null} </div>); return ( <div> <Collapsible triggerWhenOpen={<CollapseTitle name={title} isExpanded />} trigger={<CollapseTitle name={title} />} triggerStyle={{cursor: "pointer", textDecoration: "none"}} transitionTime={100} > <div className='filterList'> <Flex wrap="wrap" justifyContent="flex-start" alignItems="center"> { Array.from(totalStateCount.keys()) .filter((itemName) => isValueValid(itemName)) // remove invalid values present across the tree .sort() // filters are sorted alphabetically .map((itemName) => ( <SimpleFilter key={itemName} active={activeFilterItems.indexOf(itemName) !== -1} onClick={() => dispatchFilter(this.props.dispatch, this.props.activeFilters, filterName, itemName)} > <span> {`${itemName} (${totalStateCount.get(itemName)})`} </span> </SimpleFilter> )) } </Flex> </div> </Collapsible> </div> ); } render() { if (!this.props.metadata || !this.props.tree.nodes) return null; const width = this.props.width - 30; // need to subtract margin when calculating div width return ( <FooterStyles> <div style={{width: width}}> <div className='line'/> {getAcknowledgments(this.props.metadata, this.props.dispatch)} <div className='line'/> {this.props.filtersInFooter.map((name) => ( <div key={name}> {this.displayFilter(name)} <div className='line'/> </div> ))} </div> </FooterStyles> ); } } const WithTranslation = withTranslation()(Footer); export default WithTranslation;