auspice
Version:
Web app for visualizing pathogen evolution
284 lines (269 loc) • 9.29 kB
JavaScript
import React from "react";
import { connect } from "react-redux";
import Card from "../framework/card";
import { titleFont, headerFont, medGrey, darkGrey } from "../../globalStyles";
import { applyFilter, changeDateFilter, updateVisibleTipsAndBranchThicknesses } from "../../actions/tree";
import { getVisibleDateRange } from "../../util/treeVisibilityHelpers";
import { numericToCalendar } from "../../util/dateHelpers";
import { months, NODE_VISIBLE } from "../../util/globals";
import { displayFilterValueAsButton } from "../framework/footer";
import Byline from "./byline";
const plurals = {
country: "countries",
authors: "authors"
};
const pluralise = (word, n) => {
if (n === 1) {
if (word === "authors") word = "author"; // eslint-disable-line
} else {
if (word in plurals) word = plurals[word]; // eslint-disable-line
if (word.slice(-1).toLowerCase() !== "s") word+="s"; // eslint-disable-line
}
word = word.replace(/_/g, " "); // eslint-disable-line
return word;
};
const styliseDateRange = (date) => {
const dateStr = (typeof date === "number") ?
numericToCalendar(date) :
date;
const fields = dateStr.split('-');
// 2012-01-22
if (fields.length === 3) {
return `${months[fields[1]]} ${fields[0]}`;
}
// other cases like negative numbers
return dateStr;
};
const getNumSelectedTips = (nodes, visibility) => {
let count = 0;
nodes.forEach((d, idx) => {
if (!d.hasChildren && visibility[idx] === NODE_VISIBLE) count += 1;
});
return count;
};
const arrayToSentence = (arr, {prefix=undefined, suffix=undefined, capatalise=true, fullStop=true}={}) => {
let ret;
if (!arr.length) return '';
if (arr.length === 1) {
ret = arr[0];
} else {
ret = arr.slice(0, -1).join(", ") + " and " + arr[arr.length-1];
}
if (prefix) ret = prefix + " " + ret;
if (suffix) ret += " " + suffix;
if (capatalise) ret = ret.charAt(0).toUpperCase();
if (fullStop) ret += ".";
return ret + " ";
};
export const createSummary = (mainTreeNumTips, nodes, filters, visibility, visibleStateCounts, branchLengthsToDisplay) => {
const nSelectedSamples = getNumSelectedTips(nodes, visibility);
const sampledDateRange = getVisibleDateRange(nodes, visibility);
/* Number of genomes & their date range */
let summary = `Showing ${nSelectedSamples} of ${mainTreeNumTips} genomes`;
if (branchLengthsToDisplay !== "divOnly") {
summary += ` sampled between ${styliseDateRange(sampledDateRange[0])} and ${styliseDateRange(sampledDateRange[1])}`;
}
/* parse filters */
const filterTextArr = [];
Object.keys(filters).forEach((filterName) => {
const n = Object.keys(visibleStateCounts[filterName]).length;
if (!n) return;
filterTextArr.push(`${n} ${pluralise(filterName, n)}`);
});
const prefix = branchLengthsToDisplay !== "divOnly" ? "and comprising" : "comprising";
const filterText = arrayToSentence(filterTextArr, {prefix: prefix, capatalise: false});
if (filterText.length) {
summary += ` ${filterText}`;
} else {
summary += ". ";
}
return summary;
};
class Info extends React.Component {
constructor(props) {
super(props);
}
getStyles(width) {
let fontSize = 28;
if (this.props.browserDimensions.width < 1000) {
fontSize = 27;
}
if (this.props.browserDimensions.width < 800) {
fontSize = 26;
}
if (this.props.browserDimensions.width < 600) {
fontSize = 25;
}
if (this.props.browserDimensions.width < 400) {
fontSize = 24;
}
return {
base: {
width: width + 34,
display: "inline-block",
maxWidth: width,
marginTop: 0
},
title: {
fontFamily: titleFont,
fontSize: fontSize,
marginLeft: 0,
marginTop: 0,
marginBottom: 5,
fontWeight: 500,
color: darkGrey,
letterSpacing: "-0.5px",
lineHeight: 1.2
},
n: {
fontFamily: headerFont,
fontSize: 14,
marginLeft: 2,
marginTop: 5,
marginBottom: 5,
fontWeight: 500,
color: medGrey,
lineHeight: 1.4
}
};
}
addFilteredDatesButton(buttons) {
buttons.push(
<div key={"timefilter"} style={{display: "inline-block"}}>
<div
className={'boxed-item-icon'}
onClick={() => {
this.props.dispatch(changeDateFilter({newMin: this.props.absoluteDateMin, newMax: this.props.absoluteDateMax}));
}}
role="button"
tabIndex={0}
>
{'\xD7'}
</div>
<div className={"boxed-item active-with-icon"}>
{`${styliseDateRange(this.props.dateMin)} to ${styliseDateRange(this.props.dateMax)}`}
</div>
</div>
);
}
addNonAuthorFilterButton(buttons, filterName) {
this.props.filters[filterName].sort().forEach((itemName) => {
const display = (
<span>
{itemName}
{` (${this.props.totalStateCounts[filterName].get(itemName)})`}
</span>
);
buttons.push(displayFilterValueAsButton(this.props.dispatch, this.props.filters, filterName, itemName, display, true));
});
}
selectedStrainButton(strain) {
return (
<span>
{"Showing a single strain "}
<div style={{display: "inline-block"}}>
<div
className={'boxed-item-icon'}
onClick={() => {
this.props.dispatch(
updateVisibleTipsAndBranchThicknesses({tipSelected: {clear: true}, cladeSelected: this.props.selectedClade})
);
}}
role="button"
tabIndex={0}
>
{'\xD7'}
</div>
<div className={"boxed-item active-with-icon"}>
{strain}
</div>
</div>
</span>
);
}
clearFilterButton(field) {
return (
<span
style={{cursor: "pointer", color: '#5097BA'}}
key={field}
onClick={() => this.props.dispatch(applyFilter("set", field, []))}
role="button"
tabIndex={0}
>
{field}
</span>
);
}
renderTitle(styles) {
let title = "";
if (this.props.metadata.title) {
title = this.props.metadata.title;
}
return (
<div width={this.props.width} style={styles.title}>
{title}
</div>
);
}
render() {
if (!this.props.metadata || !this.props.nodes || !this.props.visibility) return null;
const styles = this.getStyles(this.props.width);
// const filtersWithValues = Object.keys(this.props.filters).filter((n) => this.props.filters[n].length > 0);
const animating = this.props.animationPlayPauseButton === "Pause";
const showExtended = !animating && !this.props.selectedStrain;
const datesMaxed = this.props.dateMin === this.props.absoluteDateMin && this.props.dateMax === this.props.absoluteDateMax;
/* the content is made up of two parts:
(1) the summary - e.g. Showing 4 of 379 sequences, from 1 author, 1 country and 1 region, dated Apr 2016 to Jun 2016.
(2) The active filters: Filtered to [[Metsky et al Zika Virus Evolution And Spread In The Americas (76)]], [[Colombia (28)]].
*/
const summary = createSummary(this.props.metadata.mainTreeNumTips, this.props.nodes, this.props.filters, this.props.visibility, this.props.visibleStateCounts, this.props.branchLengthsToDisplay);
/* part II - the active filters */
const filters = [];
Object.keys(this.props.filters)
.filter((n) => this.props.filters[n].length > 0)
.forEach((n) => this.addNonAuthorFilterButton(filters, n));
if (!datesMaxed) {this.addFilteredDatesButton(filters);}
return (
<Card center infocard>
<div style={styles.base}>
{this.renderTitle(styles)}
<Byline styles={styles} width={this.props.width} metadata={this.props.metadata}/>
<div width={this.props.width} style={styles.n}>
{animating ? `Animation in progress. ` : null}
{this.props.selectedStrain ? this.selectedStrainButton(this.props.selectedStrain) : null}
{/* part 1 - the summary */}
{showExtended ? summary : null}
{/* part 2 - the filters */}
{showExtended && filters.length ? (
<span>
{"Filtered to "}
{filters.map((d) => d)}
{". "}
</span>
) : null}
</div>
</div>
</Card>
);
}
}
export default Info;