Skip to content

Commit

Permalink
Merge pull request #1346 from nextstrain/ordinal-scatterplots
Browse files Browse the repository at this point in the history
Allow non-continuous scatterplot variables
  • Loading branch information
jameshadfield authored May 24, 2021
2 parents e47d9e2 + 898a387 commit b58f148
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 102 deletions.
37 changes: 37 additions & 0 deletions src/actions/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { CHANGE_LAYOUT } from "./types";
import { validateScatterVariables, addScatterAxisInfo} from "../util/scatterplotHelpers";

/**
* Redux Thunk to change a layout, including aspects of the scatterplot / clock layouts.
*/
export const changeLayout = ({layout, showBranches, showRegression, x, xLabel, y, yLabel}) => {
return (dispatch, getState) => {
if (window.NEXTSTRAIN && window.NEXTSTRAIN.animationTickReference) return;
const { controls, tree, metadata } = getState();

if (layout==="rect" || layout==="unrooted" || layout==="radial") {
dispatch({type: CHANGE_LAYOUT, layout, scatterVariables: controls.scatterVariables, canRenderBranchLabels: true});
return;
}

let scatterVariables = (layout==="clock" || layout==="scatter") ?
validateScatterVariables(controls, metadata, tree, layout==="clock") : // occurs when switching to this layout
controls.scatterVariables;

if (x && xLabel) scatterVariables = {...scatterVariables, ...addScatterAxisInfo({x, xLabel}, "x", controls, tree, metadata)};
if (y && yLabel) scatterVariables = {...scatterVariables, ...addScatterAxisInfo({y, yLabel}, "y", controls, tree, metadata)};
if (showBranches!==undefined) scatterVariables.showBranches = showBranches;
if (showRegression!==undefined) scatterVariables.showRegression = showRegression;
if (layout==="scatter" && (!scatterVariables.xContinuous || !scatterVariables.yContinuous)) {
scatterVariables.showRegression= false;
}

dispatch({
type: CHANGE_LAYOUT,
layout: layout || controls.layout,
scatterVariables: {...scatterVariables}, // ensures redux is aware of change
canRenderBranchLabels: scatterVariables.showBranches
});

};
};
2 changes: 1 addition & 1 deletion src/actions/recomputeReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ const checkAndCorrectErrorsInState = (state, metadata, query, tree, viewingNarra
// todo: these should be JSON definable (via display_defaults)
if (state.layout==="scatter" || state.layout==="clock") {
state.scatterVariables = validateScatterVariables(
state.scatterVariables, metadata.colorings, state.distanceMeasure, state.colorBy, state.layout==="clock"
state, metadata, tree, state.layout==="clock"
);
if (query.scatterX && query.scatterX!==state.scatterVariables.x) delete query.scatterX;
if (query.scatterY && query.scatterY!==state.scatterVariables.y) delete query.scatterY;
Expand Down
69 changes: 27 additions & 42 deletions src/components/controls/choose-layout.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/* eslint-disable react/jsx-no-bind */
/* ^^^ We can get away with this because <ChooseLayout> doesn't rerender frequently, but fixes are welcome */

import React from "react";
import PropTypes from 'prop-types';
import { connect } from "react-redux";
Expand All @@ -9,9 +6,9 @@ import { withTranslation } from 'react-i18next';
import Select from "react-select/lib/Select";
import * as icons from "../framework/svg-icons";
import { controlsWidth } from "../../util/globals";
import { collectAvailableScatterVariables, validateScatterVariables} from "../../util/scatterplotHelpers";
import { CHANGE_LAYOUT } from "../../actions/types";
import { collectAvailableScatterVariables} from "../../util/scatterplotHelpers";
import { SidebarSubtitle, SidebarButton } from "./styles";
import { changeLayout } from "../../actions/layout";
import Toggle from "./toggle";


Expand All @@ -29,9 +26,8 @@ export const RowContainer = styled.div`
return {
layout: state.controls.layout,
scatterVariables: state.controls.scatterVariables,
colorBy: state.controls.colorBy,
distanceMeasure: state.controls.distanceMeasure,
colorings: state.metadata.colorings,
colorBy: state.controls.colorBy,
showTreeToo: state.controls.showTreeToo,
branchLengthsToDisplay: state.controls.branchLengthsToDisplay
};
Expand All @@ -41,19 +37,8 @@ class ChooseLayout extends React.Component {
layout: PropTypes.string.isRequired,
dispatch: PropTypes.func.isRequired
}
constructor(props) {
super(props);
this.updateLayout = (layout, modifiedScatterVariables=undefined) => {
if (window.NEXTSTRAIN && window.NEXTSTRAIN.animationTickReference) return;
const scatterVariables = modifiedScatterVariables ?
{...this.props.scatterVariables, ...modifiedScatterVariables} :
this.props.scatterVariables;
this.props.dispatch({type: CHANGE_LAYOUT, layout, scatterVariables});
};
}

renderScatterplotAxesSelector() {
const options = collectAvailableScatterVariables(this.props.colorings);
const options = collectAvailableScatterVariables(this.props.colorings, this.props.colorBy);
const selectedX = options.filter((o) => o.value===this.props.scatterVariables.x)[0];
const selectedY = options.filter((o) => o.value===this.props.scatterVariables.y)[0];
const miscSelectProps = {options, clearable: false, searchable: false, multi: false, valueKey: "label"};
Expand All @@ -66,7 +51,7 @@ class ChooseLayout extends React.Component {
<Select
{...miscSelectProps}
value={selectedX}
onChange={(value) => this.updateLayout("scatter", {x: value.value, xLabel: value.label})}
onChange={(value) => this.props.dispatch(changeLayout({x: value.value, xLabel: value.label}))}
/>
</ScatterSelectContainer>
</ScatterVariableContainer>
Expand All @@ -77,7 +62,7 @@ class ChooseLayout extends React.Component {
<Select
{...miscSelectProps}
value={selectedY}
onChange={(value) => this.updateLayout("scatter", {y: value.value, yLabel: value.label})}
onChange={(value) => this.props.dispatch(changeLayout({y: value.value, yLabel: value.label}))}
/>
</ScatterSelectContainer>
</ScatterVariableContainer>
Expand All @@ -93,20 +78,26 @@ class ChooseLayout extends React.Component {
<Toggle
display
on={this.props.scatterVariables.showBranches}
callback={() => this.updateLayout(this.props.layout, {showBranches: !this.props.scatterVariables.showBranches})}
callback={() => this.props.dispatch(changeLayout({showBranches: !this.props.scatterVariables.showBranches}))}
label={"Show branches"}
/>
</ScatterVariableContainer>
<div style={{paddingTop: "2px"}}/>
<ScatterVariableContainer>
<Toggle
display
on={this.props.scatterVariables.showRegression}
callback={() => this.updateLayout(this.props.layout, {showRegression: !this.props.scatterVariables.showRegression})}
label={"Show regression"}
/>
</ScatterVariableContainer>
<div style={{paddingTop: "2px"}}/>
{
(this.props.scatterVariables.xContinuous && this.props.scatterVariables.yContinuous) && (
<>
<ScatterVariableContainer>
<Toggle
display
on={this.props.scatterVariables.showRegression}
callback={() => this.props.dispatch(changeLayout({showRegression: !this.props.scatterVariables.showRegression}))}
label={"Show regression"}
/>
</ScatterVariableContainer>
<div style={{paddingTop: "2px"}}/>
</>
)
}
</>
);
}
Expand All @@ -124,7 +115,7 @@ class ChooseLayout extends React.Component {
<RectangularTreeIcon width={25} selected={selected === "rect"}/>
<SidebarButton
selected={selected === "rect"}
onClick={() => this.updateLayout("rect")}
onClick={() => this.props.dispatch(changeLayout({layout: "rect"}))}
>
{t("sidebar:rectangular")}
</SidebarButton>
Expand All @@ -133,7 +124,7 @@ class ChooseLayout extends React.Component {
<RadialTreeIcon width={25} selected={selected === "radial"}/>
<SidebarButton
selected={selected === "radial"}
onClick={() => this.updateLayout("radial")}
onClick={() => this.props.dispatch(changeLayout({layout: "radial"}))}
>
{t("sidebar:radial")}
</SidebarButton>
Expand All @@ -142,7 +133,7 @@ class ChooseLayout extends React.Component {
<UnrootedTreeIcon width={25} selected={selected === "unrooted"}/>
<SidebarButton
selected={selected === "unrooted"}
onClick={() => this.updateLayout("unrooted")}
onClick={() => this.props.dispatch(changeLayout({layout: "unrooted"}))}
>
{t("sidebar:unrooted")}
</SidebarButton>
Expand All @@ -155,10 +146,7 @@ class ChooseLayout extends React.Component {
<ClockIcon width={25} selected={selected === "clock"}/>
<SidebarButton
selected={selected === "clock"}
onClick={() => this.updateLayout(
"clock",
validateScatterVariables(this.props.scatterVariables, this.props.colorings, this.props.distanceMeasure, this.props.colorBy, true)
)}
onClick={() => this.props.dispatch(changeLayout({layout: "clock"}))}
>
{t("sidebar:clock")}
</SidebarButton>
Expand All @@ -172,10 +160,7 @@ class ChooseLayout extends React.Component {
<ScatterIcon width={25} selected={selected === "scatter"}/>
<SidebarButton
selected={selected === "scatter"}
onClick={() => this.updateLayout(
"scatter",
validateScatterVariables(this.props.scatterVariables, this.props.colorings, this.props.distanceMeasure, this.props.colorBy, false)
)}
onClick={() => this.props.dispatch(changeLayout({layout: "scatter"}))}
>
{t("sidebar:scatter")}
</SidebarButton>
Expand Down
11 changes: 11 additions & 0 deletions src/components/tree/phyloTree/grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,13 @@ export const addGrid = function addGrid() {
(this.layout!=="scatter" && this.distance==="num_date")
) {
xGridPoints = computeTemporalGridPoints(xmin, xmax, xAxisPixels, "x");
} else if (this.layout==="scatter" && !this.scatterVariables.xContinuous) {
xGridPoints = {
majorGridPoints: this.xScale.domain().map((name) => ({
name, visibility: "visible", axis: "x", position: name
})),
minorGridPoints: []
};
} else {
xGridPoints = computeNumericGridPoints(xmin, xmax, layout, this.params.minorTicks, "x");
}
Expand Down Expand Up @@ -319,6 +326,10 @@ export const addGrid = function addGrid() {
const yAxisPixels = this.yScale.range()[1] - this.yScale.range()[0];
const temporalGrid = computeTemporalGridPoints(ymin, ymax, yAxisPixels, "y");
majorGridPoints.push(...temporalGrid.majorGridPoints);
} else if (this.layout==="scatter" && !this.scatterVariables.yContinuous) {
majorGridPoints.push(...this.yScale.domain().map((name) => ({
name, visibility: "visible", axis: "y", position: name
})));
} else {
const numericGrid = computeNumericGridPoints(ymin, ymax, layout, 1, "y");
majorGridPoints.push(...numericGrid.majorGridPoints);
Expand Down
Loading

0 comments on commit b58f148

Please sign in to comment.